From 584f4a0629418c994cefd99413921d27b92e4a73 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Tue, 21 Oct 2025 11:26:26 +0200 Subject: [PATCH 01/36] feat: add empty block vrf validator module --- Cargo.lock | 154 ++++++++++++--- common/Cargo.toml | 2 + common/src/hash.rs | 127 ++++++++++++ common/src/lib.rs | 2 + common/src/messages.rs | 12 +- common/src/types.rs | 3 + common/src/vrf.rs | 184 ++++++++++++++++++ modules/block_vrf_validator/Cargo.toml | 28 +++ .../block_vrf_validator/src/assert_header.rs | 162 +++++++++++++++ .../src/block_vrf_validator.rs | 63 ++++++ modules/block_vrf_validator/src/state.rs | 6 + .../src/epoch_activity_publisher.rs | 24 ++- .../src/epoch_nonces_publisher.rs | 37 ++++ modules/epochs_state/src/epochs_state.rs | 49 +++-- modules/epochs_state/src/state.rs | 27 ++- processes/omnibus/Cargo.toml | 1 + processes/omnibus/src/main.rs | 2 + 17 files changed, 831 insertions(+), 52 deletions(-) create mode 100644 common/src/hash.rs create mode 100644 common/src/vrf.rs create mode 100644 modules/block_vrf_validator/Cargo.toml create mode 100644 modules/block_vrf_validator/src/assert_header.rs create mode 100644 modules/block_vrf_validator/src/block_vrf_validator.rs create mode 100644 modules/block_vrf_validator/src/state.rs create mode 100644 modules/epochs_state/src/epoch_nonces_publisher.rs diff --git a/Cargo.lock b/Cargo.lock index 8113613e..dbc5bd77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,8 +32,10 @@ dependencies = [ "serde", "serde_json", "serde_with 3.14.1", + "thiserror 2.0.17", "tokio", "tracing", + "vrf_dalek", ] [[package]] @@ -87,6 +89,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "acropolis_module_block_vrf_validator" +version = "0.1.0" +dependencies = [ + "acropolis_common", + "anyhow", + "caryatid_sdk", + "config", + "hex", + "imbl", + "pallas", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "acropolis_module_drdd_state" version = "0.1.0" @@ -368,6 +388,7 @@ dependencies = [ "acropolis_module_accounts_state", "acropolis_module_assets_state", "acropolis_module_block_unpacker", + "acropolis_module_block_vrf_validator", "acropolis_module_drdd_state", "acropolis_module_drep_state", "acropolis_module_epochs_state", @@ -625,7 +646,7 @@ dependencies = [ "nom 7.1.3", "num-traits", "rusticata-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] @@ -1072,6 +1093,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -1700,6 +1730,31 @@ dependencies = [ "memchr", ] +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "git+https://github.com/txpipe/curve25519-dalek?branch=ietf03_vrf_compat_ell2#70a36f41cfc3fbb7357ec3062201b911787decba" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -1889,7 +1944,7 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", + "block-buffer 0.10.4", "crypto-common", "subtle", ] @@ -1958,11 +2013,11 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 4.1.3", "ed25519", "rand_core 0.6.4", "serde", - "sha2", + "sha2 0.10.9", "subtle", "zeroize", ] @@ -2376,6 +2431,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.16" @@ -3362,9 +3428,9 @@ dependencies = [ "mithril-common", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "slog", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "walkdir", ] @@ -3392,7 +3458,7 @@ dependencies = [ "slog", "strum", "tar", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "uuid", "zstd", @@ -3428,10 +3494,10 @@ dependencies = [ "serde_bytes", "serde_json", "serde_with 3.14.1", - "sha2", + "sha2 0.10.9", "slog", "strum", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "typetag", "walkdir", @@ -3454,7 +3520,7 @@ dependencies = [ "rayon", "rug", "serde", - "thiserror 2.0.16", + "thiserror 2.0.17", ] [[package]] @@ -3691,7 +3757,7 @@ dependencies = [ "futures-sink", "js-sys", "pin-project-lite", - "thiserror 2.0.16", + "thiserror 2.0.17", "tracing", ] @@ -3721,7 +3787,7 @@ dependencies = [ "opentelemetry_sdk", "prost", "reqwest 0.12.23", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tonic 0.13.1", "tracing", @@ -3763,7 +3829,7 @@ dependencies = [ "percent-encoding", "rand 0.9.2", "serde_json", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tokio-stream", ] @@ -3795,8 +3861,8 @@ dependencies = [ "rand 0.9.2", "rc2", "sha1", - "sha2", - "thiserror 2.0.16", + "sha2 0.10.9", + "thiserror 2.0.17", "x509-parser", ] @@ -4161,7 +4227,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21e0a3a33733faeaf8651dfee72dd0f388f0c8e5ad496a3478fa5a922f49cfa8" dependencies = [ "memchr", - "thiserror 2.0.16", + "thiserror 2.0.17", "ucd-trie", ] @@ -4195,7 +4261,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42919b05089acbd0a5dcd5405fb304d17d1053847b81163d09c4ad18ce8e8420" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4289,7 +4355,7 @@ dependencies = [ "der", "pbkdf2", "scrypt", - "sha2", + "sha2 0.10.9", "spki", ] @@ -4487,7 +4553,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2 0.6.0", - "thiserror 2.0.16", + "thiserror 2.0.17", "tokio", "tracing", "web-time", @@ -4508,7 +4574,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror 2.0.16", + "thiserror 2.0.17", "tinyvec", "tracing", "web-time", @@ -4595,6 +4661,9 @@ name = "rand_core" version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] [[package]] name = "rand_core" @@ -5141,7 +5210,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5388,6 +5457,19 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + [[package]] name = "sha2" version = "0.10.9" @@ -5709,11 +5791,11 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl 2.0.16", + "thiserror-impl 2.0.17", ] [[package]] @@ -5729,9 +5811,9 @@ dependencies = [ [[package]] name = "thiserror-impl" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -6336,6 +6418,18 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" +[[package]] +name = "vrf_dalek" +version = "0.1.0" +source = "git+https://github.com/txpipe/vrf?rev=044b45a1a919ba9d9c2471fc5c4d441f13086676#044b45a1a919ba9d9c2471fc5c4d441f13086676" +dependencies = [ + "curve25519-dalek 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "curve25519-dalek 3.2.0 (git+https://github.com/txpipe/curve25519-dalek?branch=ietf03_vrf_compat_ell2)", + "rand_core 0.5.1", + "sha2 0.9.9", + "thiserror 1.0.69", +] + [[package]] name = "waker-fn" version = "1.2.0" @@ -6361,6 +6455,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -6928,7 +7028,7 @@ dependencies = [ "nom 7.1.3", "oid-registry", "rusticata-macros", - "thiserror 2.0.16", + "thiserror 2.0.17", "time", ] diff --git a/common/Cargo.toml b/common/Cargo.toml index c970d5da..04ffb83d 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -38,6 +38,8 @@ dashmap = { workspace = true } rayon = "1.11.0" cryptoxide = "0.5.1" blake2 = "0.10.6" +vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } +thiserror = "2.0.17" [lib] crate-type = ["rlib"] diff --git a/common/src/hash.rs b/common/src/hash.rs new file mode 100644 index 00000000..496fda2b --- /dev/null +++ b/common/src/hash.rs @@ -0,0 +1,127 @@ +use std::{fmt, ops::Deref, str::FromStr}; + +/// data that is a cryptographic [`struct@Hash`] of `BYTES` long. +/// +/// Possible values with Cardano are 32 bytes long (block hash or transaction +/// hash). Or 28 bytes long (as used in addresses) +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Hash([u8; BYTES]); + +impl Hash { + #[inline] + pub const fn new(bytes: [u8; BYTES]) -> Self { + Self(bytes) + } +} + +impl From<[u8; BYTES]> for Hash { + #[inline] + fn from(bytes: [u8; BYTES]) -> Self { + Self::new(bytes) + } +} + +impl From<&[u8]> for Hash { + fn from(value: &[u8]) -> Self { + let mut hash = [0; BYTES]; + hash.copy_from_slice(value); + Self::new(hash) + } +} + +impl AsRef<[u8]> for Hash { + #[inline] + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Deref for Hash { + type Target = [u8; BYTES]; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl PartialEq<[u8]> for Hash { + fn eq(&self, other: &[u8]) -> bool { + self.0.eq(other) + } +} + +impl fmt::Debug for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple(&format!("Hash<{BYTES}>")).field(&hex::encode(self)).finish() + } +} + +impl fmt::Display for Hash { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&hex::encode(self)) + } +} + +impl FromStr for Hash { + type Err = hex::FromHexError; + fn from_str(s: &str) -> Result { + let mut bytes = [0; BYTES]; + hex::decode_to_slice(s, &mut bytes)?; + Ok(Self::new(bytes)) + } +} + +impl minicbor::Encode for Hash { + fn encode( + &self, + e: &mut minicbor::Encoder, + _ctx: &mut C, + ) -> Result<(), minicbor::encode::Error> { + e.bytes(&self.0)?.ok() + } +} + +impl<'a, C, const BYTES: usize> minicbor::Decode<'a, C> for Hash { + fn decode( + d: &mut minicbor::Decoder<'a>, + _ctx: &mut C, + ) -> Result { + let bytes = d.bytes()?; + if bytes.len() == BYTES { + let mut hash = [0; BYTES]; + hash.copy_from_slice(bytes); + Ok(Self::new(hash)) + } else { + // TODO: minicbor does not allow for expecting a specific size byte array + // (in fact cbor is not good at it at all anyway) + Err(minicbor::decode::Error::message("Invalid hash size")) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn from_str() { + let _digest: Hash<28> = + "276fd18711931e2c0e21430192dbeac0e458093cd9d1fcd7210f64b3".parse().unwrap(); + + let _digest: Hash<32> = + "0d8d00cdd4657ac84d82f0a56067634a7adfdf43da41cb534bcaa45060973d21".parse().unwrap(); + } + + #[test] + #[should_panic] + fn from_str_fail_1() { + let _digest: Hash<28> = "27".parse().unwrap(); + } + + #[test] + #[should_panic] + fn from_str_fail_2() { + let _digest: Hash<32> = "0d8d00cdd465".parse().unwrap(); + } +} diff --git a/common/src/lib.rs b/common/src/lib.rs index cc564b42..45aacc13 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -5,6 +5,7 @@ pub mod calculations; pub mod cip19; pub mod crypto; pub mod genesis_values; +pub mod hash; pub mod ledger_state; pub mod math; pub mod messages; @@ -17,6 +18,7 @@ pub mod serialization; pub mod stake_addresses; pub mod state_history; pub mod types; +pub mod vrf; // Flattened re-exports pub use self::address::*; diff --git a/common/src/messages.rs b/common/src/messages.rs index c8d708df..f2f4e386 100644 --- a/common/src/messages.rs +++ b/common/src/messages.rs @@ -5,7 +5,7 @@ use crate::genesis_values::GenesisValues; use crate::ledger_state::SPOState; -use crate::protocol_params::{NonceHash, ProtocolParams}; +use crate::protocol_params::{NonceHash, Nonces, ProtocolParams}; use crate::queries::parameters::{ParametersStateQuery, ParametersStateQueryResponse}; use crate::queries::spdd::{SPDDStateQuery, SPDDStateQueryResponse}; use crate::queries::{ @@ -186,6 +186,11 @@ pub struct EpochActivityMessage { pub nonce: Option, } +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct EpochNoncesMessage { + pub nonces: Nonces, +} + #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct GovernanceProceduresMessage { /// Proposals @@ -293,8 +298,9 @@ pub enum CardanoMessage { PotDeltas(PotDeltasMessage), // Changes to pot balances BlockInfoMessage(BlockTxsMessage), // Transaction Info (total count, total output, total fees in a block) EpochActivity(EpochActivityMessage), // Total fees and VRF keys for an epoch - DRepState(DRepStateMessage), // Active DReps at epoch end - SPOState(SPOStateMessage), // Active SPOs at epoch end + EpochNonces(EpochNoncesMessage), // Epoch Nonces for Epoch N (published after the first block of Epoch N) + DRepState(DRepStateMessage), // Active DReps at epoch end + SPOState(SPOStateMessage), // Active SPOs at epoch end GovernanceProcedures(GovernanceProceduresMessage), // Governance procedures received // Protocol Parameters diff --git a/common/src/types.rs b/common/src/types.rs index 898ca34a..91974735 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -427,6 +427,9 @@ impl TxOutRef { } } +/// Slot +pub type Slot = u64; + /// Block Hash pub type BlockHash = [u8; 32]; diff --git a/common/src/vrf.rs b/common/src/vrf.rs new file mode 100644 index 00000000..ab91fe23 --- /dev/null +++ b/common/src/vrf.rs @@ -0,0 +1,184 @@ +use std::{array::TryFromSliceError, ops::Deref}; + +use anyhow::Result; +use blake2::{digest::consts::U32, Blake2b, Digest}; + +use crate::protocol_params::Nonce; +use thiserror::Error; +use vrf_dalek::{ + errors::VrfError, + vrf03::{PublicKey03, VrfProof03}, +}; + +/// A VRF public key +#[derive(Debug, PartialEq)] +pub struct PublicKey(PublicKey03); + +impl PublicKey { + /// Size of a VRF public key, in bytes. + pub const SIZE: usize = 32; + + /// Size of a VRF public key hash digest (Blake2b-256), in bytes. + pub const HASH_SIZE: usize = 32; +} + +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl Deref for PublicKey { + type Target = [u8; PublicKey::SIZE]; + + fn deref(&self) -> &Self::Target { + self.0.as_bytes() + } +} + +impl From<&[u8; Self::SIZE]> for PublicKey { + fn from(slice: &[u8; Self::SIZE]) -> Self { + PublicKey(PublicKey03::from_bytes(slice)) + } +} + +impl TryFrom<&[u8]> for PublicKey { + type Error = TryFromSliceError; + + fn try_from(slice: &[u8]) -> Result { + Ok(Self::from(<&[u8; Self::SIZE]>::try_from(slice)?)) + } +} + +/// A VRF input +/// + +pub type VrfInputHash = [u8; 32]; +pub type VrfProofHash = [u8; 64]; + +#[derive(Debug, PartialEq)] +pub struct VrfInput(VrfInputHash); + +impl VrfInput { + /// Size of a VRF input challenge, in bytes + pub const SIZE: usize = 32; + + /// Create a new input challenge from an absolute slot number and an epoch entropy (nonce) (a.k.a η0) + pub fn new(absolute_slot_number: u64, epoch_nonce: &Nonce) -> Self { + let mut hasher = Blake2b::::new(); + let mut data = Vec::::with_capacity(8 + 32); + data.extend_from_slice(&absolute_slot_number.to_be_bytes()); + if let Some(hash) = epoch_nonce.hash { + data.extend_from_slice(&hash); + } + hasher.update(&data); + let hash: VrfInputHash = hasher.finalize().into(); + VrfInput(hash) + } +} + +impl AsRef<[u8]> for VrfInput { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl Deref for VrfInput { + type Target = VrfInputHash; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From<&[u8; Self::SIZE]> for VrfInput { + fn from(slice: &[u8; Self::SIZE]) -> Self { + VrfInput(*slice) + } +} + +impl TryFrom<&[u8]> for VrfInput { + type Error = TryFromSliceError; + + fn try_from(slice: &[u8]) -> Result { + Ok(VrfInput::from(<&[u8; Self::SIZE]>::try_from(slice)?)) + } +} + +/// A VRF proof formed by an Edward point and two scalars. +#[derive(Debug)] +pub struct Proof(VrfProof03); + +impl Proof { + /// Size of a VRF proof, in bytes. + pub const SIZE: usize = 80; + + /// Size of a VRF proof hash digest (SHA512), in bytes. + pub const HASH_SIZE: usize = 64; + + /// Verify a proof signature with a vrf public key. This will return a hash to compare with the original + /// signature hash, but any non-error result is considered a successful verification without needing + /// to do the extra comparison check. + pub fn verify( + &self, + public_key: &PublicKey, + input: &VrfInput, + ) -> Result { + Ok(self.0.verify(&public_key.0, input.as_ref())?) + } +} + +#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum ProofFromBytesError { + #[error("Decompression from Edwards point failed.")] + DecompressionFailed, +} + +impl TryFrom<&[u8; Self::SIZE]> for Proof { + type Error = ProofFromBytesError; + + fn try_from(slice: &[u8; Self::SIZE]) -> Result { + Ok(Proof(VrfProof03::from_bytes(slice).map_err( + |e| match e { + VrfError::DecompressionFailed => ProofFromBytesError::DecompressionFailed, + _ => unreachable!( + "Other error than decompression failure found when deserialising proof: {e:?}" + ), + }, + )?)) + } +} + +impl From<&Proof> for [u8; Proof::SIZE] { + fn from(proof: &Proof) -> Self { + proof.0.to_bytes() + } +} + +impl From<&Proof> for [u8; Proof::HASH_SIZE] { + fn from(proof: &Proof) -> [u8; Proof::HASH_SIZE] { + proof.0.proof_to_hash() + } +} + +/// error that can be returned if the verification of a [`VrfProof`] fails +/// see [`VrfProof::verify`] +#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[error("VRF proof verification failed: {:?}", .0)] +pub struct ProofVerifyError( + #[from] + #[source] + #[serde(with = "serde_remote::VrfError")] + VrfError, +); + +mod serde_remote { + #[derive(serde::Serialize, serde::Deserialize)] + #[serde(remote = "super::VrfError")] + pub enum VrfError { + VerificationFailed, + DecompressionFailed, + PkSmallOrder, + VrfOutputInvalid, + } +} diff --git a/modules/block_vrf_validator/Cargo.toml b/modules/block_vrf_validator/Cargo.toml new file mode 100644 index 00000000..429b01f0 --- /dev/null +++ b/modules/block_vrf_validator/Cargo.toml @@ -0,0 +1,28 @@ +# Acropolis Block VRF Validator + +[package] +name = "acropolis_module_block_vrf_validator" +version = "0.1.0" +edition = "2021" +authors = ["Golddy "] +description = "Validate the VRF calculation in the block header" +license = "Apache-2.0" + +[dependencies] +acropolis_common = { path = "../../common" } + +caryatid_sdk = { workspace = true } + +anyhow = { workspace = true } +config = { workspace = true } +hex = { workspace = true } +imbl = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +serde_json = { workspace = true } +thiserror = "2.0.17" +serde = { workspace = true } +pallas = { workspace = true } + +[lib] +path = "src/block_vrf_validator.rs" diff --git a/modules/block_vrf_validator/src/assert_header.rs b/modules/block_vrf_validator/src/assert_header.rs new file mode 100644 index 00000000..1c78a9a7 --- /dev/null +++ b/modules/block_vrf_validator/src/assert_header.rs @@ -0,0 +1,162 @@ +use std::array::TryFromSliceError; + +use acropolis_common::{crypto::keyhash_256, protocol_params::Nonce, vrf, KeyHash, Slot}; +use pallas::ledger::primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; +use thiserror::Error; + +#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +pub enum AssertHeaderError { + #[error("{0}")] + KnownLeaderVrf(#[from] AssertKnownLeaderVrfError), + #[error("{0}")] + VrfProof(#[from] AssertVrfProofError), +} + +/// AssertKnownLeaderVrfError + +#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[error( + "declared leader's VRF credentials differs from those registered in the ledger (registered={} vs declared={})", + hex::encode(®istered_vrf[0..7]), + hex::encode(&declared_vrf[0..7]), +)] +pub struct AssertKnownLeaderVrfError { + registered_vrf: KeyHash, + declared_vrf: KeyHash, +} + +impl AssertKnownLeaderVrfError { + /// Asserts that the declared VRF credentials advertised in a block do indeed match those + /// registered for the corresponding leader. + pub fn new(registered_vrf_hash: &KeyHash, vrf_vkey: &[u8]) -> Result<(), Self> { + let declared_vrf_hash = keyhash_256(vrf_vkey); + if !declared_vrf_hash.eq(registered_vrf_hash) { + return Err(Self { + registered_vrf: registered_vrf_hash.clone(), + declared_vrf: declared_vrf_hash, + }); + } + Ok(()) + } +} + +// ------------------------------------------------------------ assert_vrf_proof + +#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +pub enum AssertVrfProofError { + #[error("Malformed VRF proof: {0}")] + MalformedProof(#[from] vrf::ProofFromBytesError), + + #[error("Invalid VRF proof: {0}")] + InvalidProof(vrf::ProofVerifyError, Slot, Nonce, Vec), + + #[error("could not convert slice to array")] + TryFromSliceError, + + #[error( + "Mismatch between the declared VRF proof hash in block ({}) and the computed one ({}).", + hex::encode(&.declared[0..7]), + hex::encode(&.computed[0..7]), + )] + ProofMismatch { + // this is Proof Hash (sha512 hash) + declared: Vec, + computed: Vec, + }, + + #[error( + "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", + hex::encode(&.declared[0..7]), + hex::encode(&.computed.as_slice()[0..7]), + )] + OutputMismatch { + declared: Vec, + computed: Vec, + }, +} + +impl From for AssertVrfProofError { + fn from(_: TryFromSliceError) -> Self { + Self::TryFromSliceError + } +} + +impl PartialEq for AssertVrfProofError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::MalformedProof(l0), Self::MalformedProof(r0)) => l0 == r0, + (Self::InvalidProof(l0, l1, l2, l3), Self::InvalidProof(r0, r1, r2, r3)) => { + l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3 + } + (Self::TryFromSliceError, Self::TryFromSliceError) => true, + ( + Self::ProofMismatch { + declared: l_declared, + computed: l_computed, + }, + Self::ProofMismatch { + declared: r_declared, + computed: r_computed, + }, + ) => l_declared == r_declared && l_computed == r_computed, + ( + Self::OutputMismatch { + declared: l_declared, + computed: l_computed, + }, + Self::OutputMismatch { + declared: r_declared, + computed: r_computed, + }, + ) => l_declared == r_declared && l_computed == r_computed, + _ => false, + } + } +} + +impl AssertVrfProofError { + /// Assert that the VRF output from the block and its corresponding hash. + pub fn new( + absolute_slot: Slot, + epoch_nonce: &Nonce, + leader_vrf_output: &[u8], + leader_public_key: &vrf::PublicKey, + // [u8; 64] + unsafe_vrf_proof_hash: &[u8], + // [u8; 80] + unsafe_vrf_proof: &[u8], + ) -> Result<(), Self> { + let input = &vrf::VrfInput::new(absolute_slot, epoch_nonce); + let block_proof_hash: [u8; vrf::Proof::HASH_SIZE] = unsafe_vrf_proof_hash.try_into()?; + let block_proof: [u8; vrf::Proof::SIZE] = unsafe_vrf_proof.try_into()?; + + // Verify the VRF proof + let vrf_proof = vrf::Proof::try_from(&block_proof)?; + let proof_hash = vrf_proof.verify(leader_public_key, input).map_err(|e| { + Self::InvalidProof( + e, + absolute_slot, + epoch_nonce.clone(), + leader_public_key.as_ref().to_vec(), + ) + })?; + if !proof_hash.as_slice().eq(&block_proof_hash) { + return Err(Self::ProofMismatch { + declared: block_proof_hash.to_vec(), + computed: proof_hash.to_vec(), + }); + } + + // The proof was valid. Make sure that the leader's output matches what was in the block + let calculated_leader_vrf_output = + derive_tagged_vrf_output(proof_hash.as_slice(), VrfDerivation::Leader); + if calculated_leader_vrf_output.as_slice() != leader_vrf_output { + return Err(Self::OutputMismatch { + declared: leader_vrf_output.to_vec(), + computed: calculated_leader_vrf_output, + }); + } + + Ok(()) + } +} diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs new file mode 100644 index 00000000..6fa6b218 --- /dev/null +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -0,0 +1,63 @@ +//! Acropolis Block VRF Validator module for Caryatid +//! Validate the VRF calculation in the block header +use acropolis_common::messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}; +use anyhow::Result; +use caryatid_sdk::{module, Context, Module}; +use config::Config; +use std::sync::Arc; +use tracing::{error, info}; +mod state; +use state::State; +mod assert_header; + +const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = + ("validation-vrf-publisher-topic", "cardano.validation.vrf"); +const DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC: (&str, &str) = + ("block-header-subscribe-topic", "cardano.block.header"); +const DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC: (&str, &str) = + ("epoch-nonces-subscribe-topic", "cardano.epoch.nonces"); + +/// Block VRF Validator module +#[module( + message_type(Message), + name = "block-vrf-validator", + description = "Validate the VRF calculation in the block header" +)] + +pub struct BlockVRFValidator; + +impl BlockVRFValidator { + async fn run() -> Result<()> { + Ok(()) + } + + pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { + // Publish topics + let validation_vrf_publisher_topic = config + .get_string(DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC.0) + .unwrap_or(DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC.1.to_string()); + info!("Creating publisher on '{validation_vrf_publisher_topic}'"); + + // Subscribe topics + let block_headers_subscribe_topic = config + .get_string(DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating block headers subscription on '{block_headers_subscribe_topic}'"); + + let epoch_nonces_subscribe_topic = config + .get_string(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating epoch nonces subscription on '{epoch_nonces_subscribe_topic}'"); + + // Subscribers + let block_headers_subscription = context.subscribe(&block_headers_subscribe_topic).await?; + let epoch_nonces_subscription = context.subscribe(&epoch_nonces_subscribe_topic).await?; + + // Start run task + context.run(async move { + Self::run().await.unwrap_or_else(|e| error!("Failed: {e}")); + }); + + Ok(()) + } +} diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs new file mode 100644 index 00000000..fc599160 --- /dev/null +++ b/modules/block_vrf_validator/src/state.rs @@ -0,0 +1,6 @@ +//! Acropolis block_vrf_validator: state storage + +#[derive(Default, Debug, Clone)] +pub struct State { + +} diff --git a/modules/epochs_state/src/epoch_activity_publisher.rs b/modules/epochs_state/src/epoch_activity_publisher.rs index 083d625f..9341ccaf 100644 --- a/modules/epochs_state/src/epoch_activity_publisher.rs +++ b/modules/epochs_state/src/epoch_activity_publisher.rs @@ -1,4 +1,7 @@ -use acropolis_common::messages::Message; +use acropolis_common::{ + messages::{CardanoMessage, EpochActivityMessage, Message}, + BlockInfo, +}; use caryatid_sdk::Context; use std::sync::Arc; @@ -17,8 +20,21 @@ impl EpochActivityPublisher { Self { context, topic } } - /// Publish the DRep Delegation Distribution - pub async fn publish(&mut self, message: Arc) -> anyhow::Result<()> { - self.context.message_bus.publish(&self.topic, message).await + /// Publish the Epoch Activity Message + pub async fn publish( + &mut self, + block_info: &BlockInfo, + ea: EpochActivityMessage, + ) -> anyhow::Result<()> { + self.context + .message_bus + .publish( + &self.topic, + Arc::new(Message::Cardano(( + block_info.clone(), + CardanoMessage::EpochActivity(ea), + ))), + ) + .await } } diff --git a/modules/epochs_state/src/epoch_nonces_publisher.rs b/modules/epochs_state/src/epoch_nonces_publisher.rs new file mode 100644 index 00000000..a2dc60b8 --- /dev/null +++ b/modules/epochs_state/src/epoch_nonces_publisher.rs @@ -0,0 +1,37 @@ +use acropolis_common::{ + messages::{CardanoMessage, EpochNoncesMessage, Message}, + protocol_params::Nonces, + BlockInfo, +}; +use caryatid_sdk::Context; +use std::sync::Arc; + +/// Message publisher for Epoch Nonces Message +pub struct EpochNoncesPublisher { + /// Module context + context: Arc>, + + /// Topic to publish on + topic: String, +} + +impl EpochNoncesPublisher { + /// Construct with context and topic to publish on + pub fn new(context: Arc>, topic: String) -> Self { + Self { context, topic } + } + + /// Publish the Epoch Nonces Message + pub async fn publish(&mut self, block_info: &BlockInfo, nonces: Nonces) -> anyhow::Result<()> { + self.context + .message_bus + .publish( + &self.topic, + Arc::new(Message::Cardano(( + block_info.clone(), + CardanoMessage::EpochNonces(EpochNoncesMessage { nonces }), + ))), + ) + .await + } +} diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index f57a57ea..5b334551 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -16,17 +16,18 @@ use config::Config; use pallas::ledger::traverse::MultiEraHeader; use std::sync::Arc; use tokio::sync::Mutex; -use tracing::{error, info, info_span}; +use tracing::{error, info, info_span, Instrument}; mod epoch_activity_publisher; +mod epoch_nonces_publisher; mod epochs_history; mod state; mod store_config; use state::State; use crate::{ - epoch_activity_publisher::EpochActivityPublisher, epochs_history::EpochsHistoryState, - store_config::StoreConfig, + epoch_activity_publisher::EpochActivityPublisher, epoch_nonces_publisher::EpochNoncesPublisher, + epochs_history::EpochsHistoryState, store_config::StoreConfig, }; const DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC: (&str, &str) = ( @@ -44,6 +45,8 @@ const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( const DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC: (&str, &str) = ("epoch-activity-publish-topic", "cardano.epoch.activity"); +const DEFAULT_EPOCH_NONCES_PUBLISH_TOPIC: (&str, &str) = + ("epoch-nonces-publish-topic", "cardano.epoch.nonces"); /// Epochs State module #[module( @@ -63,6 +66,7 @@ impl EpochsState { mut block_txs_subscription: Box>, mut protocol_parameters_subscription: Box>, mut epoch_activity_publisher: EpochActivityPublisher, + mut epoch_nonces_publisher: EpochNoncesPublisher, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; let genesis = match bootstrapped_message.as_ref() { @@ -134,27 +138,36 @@ impl EpochsState { // update epochs history epochs_history.handle_epoch_activity(&block_info, &ea); // publish epoch activity message - epoch_activity_publisher - .publish(Arc::new(Message::Cardano(( - block_info.clone(), - CardanoMessage::EpochActivity(ea), - )))) - .await - .unwrap_or_else(|e| error!("Failed to publish: {e}")); + epoch_activity_publisher.publish(&block_info, ea).await.unwrap_or_else( + |e| error!("Failed to publish epoch activity messages: {e}"), + ); } let span = info_span!( "epochs_state.handle_block_header", block = block_info.number ); - span.in_scope(|| { + async { if let Some(header) = header.as_ref() { match state.handle_block_header(&genesis, &block_info, &header) { - Ok(()) => {} + Ok(()) => { + if is_new_epoch { + if let Some(nonces) = state.get_nonces() { + epoch_nonces_publisher + .publish(&block_info, nonces) + .await + .unwrap_or_else(|e| { + error!("Failed to publish epoch nonces: {e}") + }); + } + } + } Err(e) => error!("Error handling block header: {e}"), } } - }); + } + .instrument(span) + .await; let span = info_span!("epochs_state.handle_mint", block = block_info.number); span.in_scope(|| { @@ -218,7 +231,12 @@ impl EpochsState { let epoch_activity_publish_topic = config .get_string(DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC.0) .unwrap_or(DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC.1.to_string()); - info!("Publishing on '{epoch_activity_publish_topic}'"); + info!("Publishing EpochActivityMessage on '{epoch_activity_publish_topic}'"); + + let epoch_nonces_publish_topic = config + .get_string(DEFAULT_EPOCH_NONCES_PUBLISH_TOPIC.0) + .unwrap_or(DEFAULT_EPOCH_NONCES_PUBLISH_TOPIC.1.to_string()); + info!("Publishing EpochNoncesMessage on '{epoch_nonces_publish_topic}'"); // query topic let epochs_query_topic = config @@ -250,6 +268,8 @@ impl EpochsState { // Publisher let epoch_activity_publisher = EpochActivityPublisher::new(context.clone(), epoch_activity_publish_topic); + let epoch_nonces_publisher = + EpochNoncesPublisher::new(context.clone(), epoch_nonces_publish_topic); // handle epochs query context.handle(&epochs_query_topic, move |message| { @@ -348,6 +368,7 @@ impl EpochsState { block_txs_subscription, protocol_parameters_subscription, epoch_activity_publisher, + epoch_nonces_publisher, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); diff --git a/modules/epochs_state/src/state.rs b/modules/epochs_state/src/state.rs index 0a46c07a..551c7c9b 100644 --- a/modules/epochs_state/src/state.rs +++ b/modules/epochs_state/src/state.rs @@ -1,4 +1,4 @@ -//! Acropolis epoch activity counter: state storage +//! Acropolis epochs_state: state storage use acropolis_common::{ crypto::keyhash_224, @@ -89,7 +89,7 @@ impl State { } } - // Handle a block header + /// Handle a block header pub fn handle_block_header( &mut self, genesis: &GenesisValues, @@ -177,8 +177,7 @@ impl State { }; self.nonces = Some(new_nonces); - }; - + } Ok(()) } @@ -234,6 +233,10 @@ impl State { epoch_activity } + pub fn get_nonces(&self) -> Option { + self.nonces.clone() + } + pub fn get_epoch_info(&self) -> EpochActivityMessage { EpochActivityMessage { epoch: self.epoch, @@ -684,6 +687,22 @@ mod tests { let block = make_new_epoch_block(209); let block_header = MultiEraHeader::decode(1, None, &e_210_first_block_header_cbor).unwrap(); assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); + println!( + "block header leader vrf output: {:?}", + block_header.leader_vrf_output().unwrap() + ); + println!( + "block header leader vrf output length: {:?}", + block_header.leader_vrf_output().unwrap().len() + ); + println!( + "block header nonce vrf output: {:?}", + block_header.nonce_vrf_output().unwrap() + ); + println!( + "block header nonce vrf output length: {:?}", + block_header.nonce_vrf_output().unwrap().len() + ); let nonces = state.nonces.unwrap(); let evolved = Nonce::from( diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index 52a81cf6..41d23331 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -21,6 +21,7 @@ acropolis_module_governance_state = { path = "../../modules/governance_state" } acropolis_module_parameters_state = { path = "../../modules/parameters_state" } acropolis_module_stake_delta_filter = { path = "../../modules/stake_delta_filter" } acropolis_module_epochs_state = { path = "../../modules/epochs_state" } +acropolis_module_block_vrf_validator = { path = "../../modules/block_vrf_validator" } acropolis_module_accounts_state = { path = "../../modules/accounts_state" } acropolis_module_rest_blockfrost = { path = "../../modules/rest_blockfrost" } acropolis_module_spdd_state = { path = "../../modules/spdd_state" } diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index ecbebffe..6b397d89 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -12,6 +12,7 @@ use tracing_subscriber; use acropolis_module_accounts_state::AccountsState; use acropolis_module_assets_state::AssetsState; use acropolis_module_block_unpacker::BlockUnpacker; +use acropolis_module_block_vrf_validator::BlockVRFValidator; use acropolis_module_drdd_state::DRDDState; use acropolis_module_drep_state::DRepState; use acropolis_module_epochs_state::EpochsState; @@ -98,6 +99,7 @@ pub async fn main() -> Result<()> { ParametersState::register(&mut process); StakeDeltaFilter::register(&mut process); EpochsState::register(&mut process); + BlockVRFValidator::register(&mut process); AccountsState::register(&mut process); AssetsState::register(&mut process); BlockfrostREST::register(&mut process); From ec6e5bfdc26eb93259514e65167f5e21b1f99213 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Tue, 21 Oct 2025 16:44:46 +0200 Subject: [PATCH 02/36] refactor: change module name --- Cargo.lock | 23 +- common/Cargo.toml | 2 - common/src/lib.rs | 1 - .../Cargo.toml | 9 +- .../src/assert_header.rs | 6 +- .../src/block_validator.rs} | 15 +- modules/block_validator/src/ouroboros/kes.rs | 214 ++++++++++++++++++ modules/block_validator/src/ouroboros/mod.rs | 2 + .../block_validator/src/ouroboros}/vrf.rs | 3 +- .../src/state.rs | 2 +- processes/omnibus/Cargo.toml | 2 +- processes/omnibus/src/main.rs | 4 +- 12 files changed, 258 insertions(+), 25 deletions(-) rename modules/{block_vrf_validator => block_validator}/Cargo.toml (56%) rename modules/{block_vrf_validator => block_validator}/src/assert_header.rs (99%) rename modules/{block_vrf_validator/src/block_vrf_validator.rs => block_validator/src/block_validator.rs} (84%) create mode 100644 modules/block_validator/src/ouroboros/kes.rs create mode 100644 modules/block_validator/src/ouroboros/mod.rs rename {common/src => modules/block_validator/src/ouroboros}/vrf.rs (99%) rename modules/{block_vrf_validator => block_validator}/src/state.rs (55%) diff --git a/Cargo.lock b/Cargo.lock index d88f8741..bf9cf1aa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,10 +33,8 @@ dependencies = [ "serde", "serde_json", "serde_with 3.14.1", - "thiserror 2.0.17", "tokio", "tracing", - "vrf_dalek", ] [[package]] @@ -112,21 +110,24 @@ dependencies = [ ] [[package]] -name = "acropolis_module_block_vrf_validator" +name = "acropolis_module_block_validator" version = "0.1.0" dependencies = [ "acropolis_common", "anyhow", + "blake2 0.10.6", "caryatid_sdk", "config", "hex", "imbl", + "kes-summed-ed25519 0.2.1 (git+https://github.com/txpipe/kes?rev=f69fb357d46f6a18925543d785850059569d7e78)", "pallas", "serde", "serde_json", "thiserror 2.0.17", "tokio", "tracing", + "vrf_dalek", ] [[package]] @@ -411,7 +412,7 @@ dependencies = [ "acropolis_module_address_state", "acropolis_module_assets_state", "acropolis_module_block_unpacker", - "acropolis_module_block_vrf_validator", + "acropolis_module_block_validator", "acropolis_module_drdd_state", "acropolis_module_drep_state", "acropolis_module_epochs_state", @@ -3179,6 +3180,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "kes-summed-ed25519" +version = "0.2.1" +source = "git+https://github.com/txpipe/kes?rev=f69fb357d46f6a18925543d785850059569d7e78#f69fb357d46f6a18925543d785850059569d7e78" +dependencies = [ + "blake2 0.10.6", + "ed25519-dalek", + "rand_core 0.6.4", + "thiserror 1.0.69", + "zeroize", +] + [[package]] name = "lapin" version = "2.5.4" @@ -3505,7 +3518,7 @@ dependencies = [ "ed25519-dalek", "fixed", "hex", - "kes-summed-ed25519", + "kes-summed-ed25519 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "mithril-build-script", "mithril-stm", "nom 8.0.0", diff --git a/common/Cargo.toml b/common/Cargo.toml index d64a1b64..3cb24e0f 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -39,8 +39,6 @@ dashmap = { workspace = true } rayon = "1.11.0" cryptoxide = "0.5.1" blake2 = "0.10.6" -vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } -thiserror = "2.0.17" [lib] crate-type = ["rlib"] diff --git a/common/src/lib.rs b/common/src/lib.rs index 45aacc13..780264fe 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -18,7 +18,6 @@ pub mod serialization; pub mod stake_addresses; pub mod state_history; pub mod types; -pub mod vrf; // Flattened re-exports pub use self::address::*; diff --git a/modules/block_vrf_validator/Cargo.toml b/modules/block_validator/Cargo.toml similarity index 56% rename from modules/block_vrf_validator/Cargo.toml rename to modules/block_validator/Cargo.toml index 429b01f0..61b31295 100644 --- a/modules/block_vrf_validator/Cargo.toml +++ b/modules/block_validator/Cargo.toml @@ -1,7 +1,7 @@ # Acropolis Block VRF Validator [package] -name = "acropolis_module_block_vrf_validator" +name = "acropolis_module_block_validator" version = "0.1.0" edition = "2021" authors = ["Golddy "] @@ -24,5 +24,10 @@ thiserror = "2.0.17" serde = { workspace = true } pallas = { workspace = true } +# The vrf crate has not been fully tested in production environments and still has several upstream issues that are open PRs but not merged yet. +vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } +kes-summed-ed25519 = { git = "https://github.com/txpipe/kes", rev = "f69fb357d46f6a18925543d785850059569d7e78" } +blake2 = "0.10.6" + [lib] -path = "src/block_vrf_validator.rs" +path = "src/block_validator.rs" diff --git a/modules/block_vrf_validator/src/assert_header.rs b/modules/block_validator/src/assert_header.rs similarity index 99% rename from modules/block_vrf_validator/src/assert_header.rs rename to modules/block_validator/src/assert_header.rs index 1c78a9a7..7025afdd 100644 --- a/modules/block_vrf_validator/src/assert_header.rs +++ b/modules/block_validator/src/assert_header.rs @@ -1,7 +1,7 @@ -use std::array::TryFromSliceError; - -use acropolis_common::{crypto::keyhash_256, protocol_params::Nonce, vrf, KeyHash, Slot}; +use crate::ouroboros::vrf; +use acropolis_common::{crypto::keyhash_256, protocol_params::Nonce, KeyHash, Slot}; use pallas::ledger::primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; +use std::array::TryFromSliceError; use thiserror::Error; #[derive(Error, Debug, serde::Serialize, serde::Deserialize)] diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_validator/src/block_validator.rs similarity index 84% rename from modules/block_vrf_validator/src/block_vrf_validator.rs rename to modules/block_validator/src/block_validator.rs index 6fa6b218..d1db84c3 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_validator/src/block_validator.rs @@ -9,24 +9,27 @@ use tracing::{error, info}; mod state; use state::State; mod assert_header; +mod ouroboros; const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = ("validation-vrf-publisher-topic", "cardano.validation.vrf"); +const DEFAULT_VALIDATION_KES_PUBLISHER_TOPIC: (&str, &str) = + ("validation-kes-publisher-topic", "cardano.validation.kes"); const DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC: (&str, &str) = ("block-header-subscribe-topic", "cardano.block.header"); const DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC: (&str, &str) = ("epoch-nonces-subscribe-topic", "cardano.epoch.nonces"); -/// Block VRF Validator module +/// Block Validator module #[module( message_type(Message), - name = "block-vrf-validator", - description = "Validate the VRF calculation in the block header" + name = "block-validator", + description = "Validate the block header" )] -pub struct BlockVRFValidator; +pub struct BlockValidator; -impl BlockVRFValidator { +impl BlockValidator { async fn run() -> Result<()> { Ok(()) } @@ -36,7 +39,7 @@ impl BlockVRFValidator { let validation_vrf_publisher_topic = config .get_string(DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC.0) .unwrap_or(DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC.1.to_string()); - info!("Creating publisher on '{validation_vrf_publisher_topic}'"); + info!("Creating validation VRF publisher on '{validation_vrf_publisher_topic}'"); // Subscribe topics let block_headers_subscribe_topic = config diff --git a/modules/block_validator/src/ouroboros/kes.rs b/modules/block_validator/src/ouroboros/kes.rs new file mode 100644 index 00000000..8e6cb43c --- /dev/null +++ b/modules/block_validator/src/ouroboros/kes.rs @@ -0,0 +1,214 @@ +use kes_summed_ed25519::{ + self as kes, + kes::{Sum6Kes, Sum6KesSig}, + traits::{KesSig, KesSk}, +}; +use std::{array::TryFromSliceError, ops::Deref}; +use thiserror::Error; + +// ------------------------------------------------------------------- SecretKey + +/// KES secret key +pub struct SecretKey<'a>(Sum6Kes<'a>); + +impl SecretKey<'_> { + /// Create a new KES secret key + pub fn from_bytes(sk_bytes: &mut Vec) -> Result, Error> { + // TODO: extend() could potentially re-allocate memory to a new location and copy the sk_bytes. + // This would leave the original memory containing the secret key without being wiped. + sk_bytes.extend([0u8; 4]); // default to period = 0 + let sum_6_kes = Sum6Kes::from_bytes(sk_bytes.as_mut_slice())?; + Ok(SecretKey(sum_6_kes)) + } + + /// Get the current period of the KES secret key + pub fn get_period(&self) -> u32 { + self.0.get_period() + } + + /// Update the KES secret key to the next period + pub fn update(&mut self) -> Result<(), Error> { + Ok(self.0.update()?) + } +} + +// ------------------------------------------------------------------- PublicKey + +/// KES public key +pub struct PublicKey(kes::PublicKey); + +impl PublicKey { + /// Size of a KES public key, in bytes; + pub const SIZE: usize = 32; +} + +impl AsRef<[u8]> for PublicKey { + fn as_ref(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl Deref for PublicKey { + type Target = [u8; Self::SIZE]; + + fn deref(&self) -> &Self::Target { + self.0.as_bytes().try_into().unwrap_or_else(|e| { + unreachable!( + "Impossible! Failed to convert KES public key ({}) back to slice of known size: {e:?}", + hex::encode(self.0), + ) + }) + } +} + +impl From<&SecretKey<'_>> for PublicKey { + fn from(sk: &SecretKey<'_>) -> Self { + PublicKey(sk.0.to_pk()) + } +} + +impl From<&[u8; PublicKey::SIZE]> for PublicKey { + fn from(bytes: &[u8; PublicKey::SIZE]) -> Self { + PublicKey(kes::PublicKey::from_bytes(bytes).unwrap_or_else(|e| { + unreachable!( + "Impossible! Failed to create a KES public key from a slice ({}) of known size: {e:?}", + hex::encode(bytes) + ) + })) + } +} + +impl TryFrom<&[u8]> for PublicKey { + type Error = TryFromSliceError; + + fn try_from(bytes: &[u8]) -> Result { + Ok(Self::from(<&[u8; Self::SIZE]>::try_from(bytes)?)) + } +} + +// ------------------------------------------------------------------- Signature + +/// KES signature +pub struct Signature(Sum6KesSig); + +impl Signature { + /// Size of a KES signature, in bytes; + pub const SIZE: usize = Sum6KesSig::SIZE; + + /// Verify the KES signature + pub fn verify(&self, kes_period: u32, kes_pk: &PublicKey, msg: &[u8]) -> Result<(), Error> { + Ok(self.0.verify(kes_period, &kes_pk.0, msg)?) + } +} + +impl From<&[u8; Self::SIZE]> for Signature { + fn from(bytes: &[u8; Self::SIZE]) -> Self { + Signature(Sum6KesSig::from_bytes(bytes).unwrap_or_else(|e| { + unreachable!("Impossible! Failed to create a KES signature from a slice ({}) of known size: {e:?}", hex::encode(bytes)) + })) + } +} + +impl TryFrom<&[u8]> for Signature { + type Error = TryFromSliceError; + + fn try_from(bytes: &[u8]) -> Result { + Ok(Self::from(<&[u8; Self::SIZE]>::try_from(bytes)?)) + } +} + +impl From<&Signature> for [u8; 448] { + fn from(sig: &Signature) -> Self { + sig.0.to_bytes() + } +} + +// ----------------------------------------------------------------------- Error + +/// KES error +#[derive(Error, Debug)] +pub enum Error { + #[error("KES error: {0}")] + Kes(#[from] kes_summed_ed25519::errors::Error), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn kes_key_evolution() { + let mut kes_sk_bytes = hex::decode( + "68b77b6e61925be0499d1445fd9210cec5bdfd5dd92662802eb2720ff70bc68fd89\ + 64580ff18bd2b232eb716dfbbeef82e2844b466ddd5dacaad9f15d3c753b3483541\ + 41e973d039b1147c48e71e5b7cadc6deb28c86e4ae4fc26e8bbe1695c3374d4eb10\ + 94a7a698722894301546466c750947778b18ac3270397efd2eced4d25ced55d2bd2\ + c09e7c0fa7b849d41787ca11defc91609d930a9870881a56a587bff20b2c5c59f63\ + ccb008be495917da3fcae536d05401b6771bb1f9356f031b3ddadbffbc426a9a23e\ + 34274b187f7e93892e990644f6273772a02d3e38bee7459ed6a9bb5760fe012e47a\ + 2e75880125e7fb072b2b7a626a5375e2039d8d748cb8ad4dd02697250d3155eee39\ + 308ecc2925405a8c15e1cbe556cc4315d43ee5101003639bcb33bd6e27da3885888\ + d7cca20b05cadbaa53941ef5282cde8f377c3bd0bf732cfac6b5d4d5597a1f72d81\ + bc0d8af634a4c760b309fe8959bbde666ff10310377b313860bd52d56fd7cb14963\ + 3beb1eb2e0076111df61e570a042f7cebae74a8de298a6f114938946230db42651e\ + a4eddf5df2d7d2f3016464073da8a9dc715817b43586a61874e576da7b47a2bb6c2\ + e19d4cbd5b1b39a24427e89b812cce6d30e0506e207f1eaab313c45a236068ea319\ + 958474237a5ffe02736e1c51c02a05999816c9253a557f09375c83acf5d7250f3bb\ + c638e10c58fb274e2002eed841ecef6a9cbc57c3157a7c3cf47e66b1741e8173b66\ + 76ac973bc9715027a3225087cabad45407b891416330485891dc9a3875488a26428\ + d20d581b629a8f4f42e3aa00cbcaae6c8e2b8f3fe033b874d1de6a3f8c321c92b77\ + 643f00d28e", + ) + .unwrap(); + + let kes_sk = SecretKey::from_bytes(&mut kes_sk_bytes).unwrap(); + assert_eq!( + hex::encode(PublicKey::from(&kes_sk)), + "2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292" + ); + } + + #[test] + fn kes_signature_verify() { + let kes_pk_bytes = + hex::decode("2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292") + .unwrap(); + let kes_pk = PublicKey::try_from(&kes_pk_bytes[..]).unwrap(); + let kes_signature_bytes = hex::decode( + "20f1c8f9ae672e6ec75b0aa63a85e7ab7865b95f6b2907a26b54c14f49184ab52cf\ + 98ef441bb71de50380325b34f16d84fc78d137467a1b49846747cf8ee4701c56f08\ + f198b94c468d46b67b271f5bc30ab2ad14b1bdbf2be0695a00fe4b02b3060fa5212\ + 8f4cce9c5759df0ba8d71fe99456bd2e333671e45110908d03a2ec3b38599d26adf\ + 182ba63f79900fdb2732947cf8e940a4cf1e8db9b4cf4c001dbd37c60d0e38851de\ + 4910807896153be455e13161342d4c6f7bb3e4d2d35dbbbba0ebcd161be2f1ec030\ + d2f5a6059ac89dfa70dc6b3d0bc2da179c62ae95c4f9c7ad9c0387b35bf2b45b325\ + d1e0a18c0c783a0779003bf23e7a6b00cc126c5e3d51a57d41ff1707a76fb2c306a\ + 67c21473b41f1d9a7f64a670ec172a2421da03d796fa97086de8812304f4f96bd45\ + 243d0a2ad6c48a69d9e2c0afbb1333acee607d18eb3a33818c3c9d5bb72cade8893\ + 79008bf60d436298cb0cfc6159332cb1af1de4f1d64e79c399d058ac4993704eed6\ + 7917093f89db6cde830383e69aa400ba3225087cabad45407b891416330485891dc\ + 9a3875488a26428d20d581b629a8f4f42e3aa00cbcaae6c8e2b8f3fe033b874d1de\ + 6a3f8c321c92b77643f00d28e", + ) + .unwrap(); + let kes_signature = Signature::try_from(&kes_signature_bytes[..]).unwrap(); + let kes_period = 36u32; + let kes_msg = hex::decode( + "8a1a00a50f121a0802d24458203deea82abe788d260b8987a522aadec86c9f098e8\ + 8a57d7cfcdb24f474a7afb65820cad3c900ca6baee9e65bf61073d900bfbca458ee\ + ca6d0b9f9931f5b1017a8cd65820576d49e98adfab65623dc16f9fff2edd210e8dd\ + 1d4588bfaf8af250beda9d3c7825840d944b8c81000fc1182ec02194ca9eca510fd\ + 84995d22bfe1842190b39d468e5ecbd863969e0c717b0071a371f748d44c895fa92\ + 33094cefcd3107410baabb19a5850f2a29f985d37ca8eb671c2847fab9cc45c9373\ + 8a430b4e43837e7f33028b190a7e55152b0e901548961a66d56eebe72d616f9e68f\ + d13e9955ccd8611c201a5b422ac8ef56af74cb657b5b868ce9d850f1945d1582063\ + 9d4986d17de3cac8079a3b25d671f339467aa3a9948e29992dafebf90f719f84582\ + 02e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b29217\ + 1903e958401feeeabc7460b19370f4050e986b558b149fdc8724b4a4805af8fe45c\ + 8e7a7c6753894ad7a1b9c313da269ddc5922e150da3b378977f1dfea79fc52fd2c1\ + 2f08820901", + ) + .unwrap(); + assert!(kes_signature.verify(kes_period, &kes_pk, &kes_msg).is_ok()); + } +} diff --git a/modules/block_validator/src/ouroboros/mod.rs b/modules/block_validator/src/ouroboros/mod.rs new file mode 100644 index 00000000..ef17b56c --- /dev/null +++ b/modules/block_validator/src/ouroboros/mod.rs @@ -0,0 +1,2 @@ +pub mod kes; +pub mod vrf; diff --git a/common/src/vrf.rs b/modules/block_validator/src/ouroboros/vrf.rs similarity index 99% rename from common/src/vrf.rs rename to modules/block_validator/src/ouroboros/vrf.rs index ab91fe23..c97bacae 100644 --- a/common/src/vrf.rs +++ b/modules/block_validator/src/ouroboros/vrf.rs @@ -1,9 +1,8 @@ use std::{array::TryFromSliceError, ops::Deref}; +use acropolis_common::protocol_params::Nonce; use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; - -use crate::protocol_params::Nonce; use thiserror::Error; use vrf_dalek::{ errors::VrfError, diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_validator/src/state.rs similarity index 55% rename from modules/block_vrf_validator/src/state.rs rename to modules/block_validator/src/state.rs index fc599160..c2fe2abe 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_validator/src/state.rs @@ -1,4 +1,4 @@ -//! Acropolis block_vrf_validator: state storage +//! Acropolis block_validator state storage #[derive(Default, Debug, Clone)] pub struct State { diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index b8ead63c..19244d71 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -21,7 +21,7 @@ acropolis_module_governance_state = { path = "../../modules/governance_state" } acropolis_module_parameters_state = { path = "../../modules/parameters_state" } acropolis_module_stake_delta_filter = { path = "../../modules/stake_delta_filter" } acropolis_module_epochs_state = { path = "../../modules/epochs_state" } -acropolis_module_block_vrf_validator = { path = "../../modules/block_vrf_validator" } +acropolis_module_block_validator = { path = "../../modules/block_validator" } acropolis_module_accounts_state = { path = "../../modules/accounts_state" } acropolis_module_rest_blockfrost = { path = "../../modules/rest_blockfrost" } acropolis_module_spdd_state = { path = "../../modules/spdd_state" } diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index 511e59e2..2b9af370 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -13,7 +13,7 @@ use acropolis_module_accounts_state::AccountsState; use acropolis_module_address_state::AddressState; use acropolis_module_assets_state::AssetsState; use acropolis_module_block_unpacker::BlockUnpacker; -use acropolis_module_block_vrf_validator::BlockVRFValidator; +use acropolis_module_block_validator::BlockValidator; use acropolis_module_drdd_state::DRDDState; use acropolis_module_drep_state::DRepState; use acropolis_module_epochs_state::EpochsState; @@ -100,7 +100,7 @@ pub async fn main() -> Result<()> { ParametersState::register(&mut process); StakeDeltaFilter::register(&mut process); EpochsState::register(&mut process); - BlockVRFValidator::register(&mut process); + BlockValidator::register(&mut process); AccountsState::register(&mut process); AddressState::register(&mut process); AssetsState::register(&mut process); From c130403139bdc5bec684661ef8038d7ac86e59fe Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 22 Oct 2025 10:06:20 +0200 Subject: [PATCH 03/36] fix: move assert_header --- .../block_validator/src/block_validator.rs | 1 - .../src/{ => ouroboros}/assert_header.rs | 33 ++++++++++--------- modules/block_validator/src/ouroboros/mod.rs | 1 + 3 files changed, 18 insertions(+), 17 deletions(-) rename modules/block_validator/src/{ => ouroboros}/assert_header.rs (88%) diff --git a/modules/block_validator/src/block_validator.rs b/modules/block_validator/src/block_validator.rs index d1db84c3..1e02d9a9 100644 --- a/modules/block_validator/src/block_validator.rs +++ b/modules/block_validator/src/block_validator.rs @@ -8,7 +8,6 @@ use std::sync::Arc; use tracing::{error, info}; mod state; use state::State; -mod assert_header; mod ouroboros; const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = diff --git a/modules/block_validator/src/assert_header.rs b/modules/block_validator/src/ouroboros/assert_header.rs similarity index 88% rename from modules/block_validator/src/assert_header.rs rename to modules/block_validator/src/ouroboros/assert_header.rs index 7025afdd..592f3a38 100644 --- a/modules/block_validator/src/assert_header.rs +++ b/modules/block_validator/src/ouroboros/assert_header.rs @@ -5,11 +5,11 @@ use std::array::TryFromSliceError; use thiserror::Error; #[derive(Error, Debug, serde::Serialize, serde::Deserialize)] -pub enum AssertHeaderError { +pub enum BlockHeaderValidationError { #[error("{0}")] - KnownLeaderVrf(#[from] AssertKnownLeaderVrfError), + KnownLeaderVrf(#[from] KnownLeaderVrfError), #[error("{0}")] - VrfProof(#[from] AssertVrfProofError), + VrfProof(#[from] VrfProofError), } /// AssertKnownLeaderVrfError @@ -20,12 +20,12 @@ pub enum AssertHeaderError { hex::encode(®istered_vrf[0..7]), hex::encode(&declared_vrf[0..7]), )] -pub struct AssertKnownLeaderVrfError { +pub struct KnownLeaderVrfError { registered_vrf: KeyHash, declared_vrf: KeyHash, } -impl AssertKnownLeaderVrfError { +impl KnownLeaderVrfError { /// Asserts that the declared VRF credentials advertised in a block do indeed match those /// registered for the corresponding leader. pub fn new(registered_vrf_hash: &KeyHash, vrf_vkey: &[u8]) -> Result<(), Self> { @@ -43,7 +43,7 @@ impl AssertKnownLeaderVrfError { // ------------------------------------------------------------ assert_vrf_proof #[derive(Error, Debug, serde::Serialize, serde::Deserialize)] -pub enum AssertVrfProofError { +pub enum VrfProofError { #[error("Malformed VRF proof: {0}")] MalformedProof(#[from] vrf::ProofFromBytesError), @@ -55,8 +55,8 @@ pub enum AssertVrfProofError { #[error( "Mismatch between the declared VRF proof hash in block ({}) and the computed one ({}).", - hex::encode(&.declared[0..7]), - hex::encode(&.computed[0..7]), + hex::encode(&declared[0..7]), + hex::encode(&computed[0..7]), )] ProofMismatch { // this is Proof Hash (sha512 hash) @@ -66,8 +66,8 @@ pub enum AssertVrfProofError { #[error( "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", - hex::encode(&.declared[0..7]), - hex::encode(&.computed.as_slice()[0..7]), + hex::encode(&declared[0..7]), + hex::encode(&computed[0..7]), )] OutputMismatch { declared: Vec, @@ -75,13 +75,13 @@ pub enum AssertVrfProofError { }, } -impl From for AssertVrfProofError { +impl From for VrfProofError { fn from(_: TryFromSliceError) -> Self { Self::TryFromSliceError } } -impl PartialEq for AssertVrfProofError { +impl PartialEq for VrfProofError { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::MalformedProof(l0), Self::MalformedProof(r0)) => l0 == r0, @@ -114,16 +114,17 @@ impl PartialEq for AssertVrfProofError { } } -impl AssertVrfProofError { - /// Assert that the VRF output from the block and its corresponding hash. +impl VrfProofError { + /// Validate the VRF output from the block and its corresponding hash. pub fn new( absolute_slot: Slot, epoch_nonce: &Nonce, leader_vrf_output: &[u8], + // Public Key from declared_vrf_key from block header leader_public_key: &vrf::PublicKey, - // [u8; 64] + // must be [u8; 64] unsafe_vrf_proof_hash: &[u8], - // [u8; 80] + // must be [u8; 80] unsafe_vrf_proof: &[u8], ) -> Result<(), Self> { let input = &vrf::VrfInput::new(absolute_slot, epoch_nonce); diff --git a/modules/block_validator/src/ouroboros/mod.rs b/modules/block_validator/src/ouroboros/mod.rs index ef17b56c..745dff6e 100644 --- a/modules/block_validator/src/ouroboros/mod.rs +++ b/modules/block_validator/src/ouroboros/mod.rs @@ -1,2 +1,3 @@ +pub mod assert_header; pub mod kes; pub mod vrf; From 6231c470e0bd249d47d943c1228ba126ff5ab0c0 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 23 Oct 2025 15:49:46 +0200 Subject: [PATCH 04/36] wip: add tpraos vrf proof validadtion step --- Cargo.lock | 19 +- common/src/genesis_values.rs | 124 +++++- common/src/protocol_params.rs | 28 ++ .../block_validator/src/block_validator.rs | 65 ---- .../src/ouroboros/assert_header.rs | 163 -------- modules/block_validator/src/ouroboros/kes.rs | 214 ----------- modules/block_validator/src/ouroboros/mod.rs | 3 - modules/block_validator/src/state.rs | 6 - .../Cargo.toml | 5 +- .../src/block_vrf_validator.rs | 188 +++++++++ .../block_vrf_validator/src/ouroboros/mod.rs | 5 + .../src/ouroboros/overlay_shedule.rs | 165 ++++++++ .../src/ouroboros/tpraos.rs | 52 +++ .../src/ouroboros/types.rs | 3 + .../src/ouroboros/vrf.rs | 28 +- .../src/ouroboros/vrf_validation.rs | 361 ++++++++++++++++++ modules/block_vrf_validator/src/state.rs | 71 ++++ modules/epochs_state/src/state.rs | 17 +- .../src/genesis_bootstrapper.rs | 18 +- modules/upstream_chain_fetcher/src/utils.rs | 3 + processes/omnibus/Cargo.toml | 2 +- processes/omnibus/src/main.rs | 4 +- 22 files changed, 1065 insertions(+), 479 deletions(-) delete mode 100644 modules/block_validator/src/block_validator.rs delete mode 100644 modules/block_validator/src/ouroboros/assert_header.rs delete mode 100644 modules/block_validator/src/ouroboros/kes.rs delete mode 100644 modules/block_validator/src/ouroboros/mod.rs delete mode 100644 modules/block_validator/src/state.rs rename modules/{block_validator => block_vrf_validator}/Cargo.toml (82%) create mode 100644 modules/block_vrf_validator/src/block_vrf_validator.rs create mode 100644 modules/block_vrf_validator/src/ouroboros/mod.rs create mode 100644 modules/block_vrf_validator/src/ouroboros/overlay_shedule.rs create mode 100644 modules/block_vrf_validator/src/ouroboros/tpraos.rs create mode 100644 modules/block_vrf_validator/src/ouroboros/types.rs rename modules/{block_validator => block_vrf_validator}/src/ouroboros/vrf.rs (81%) create mode 100644 modules/block_vrf_validator/src/ouroboros/vrf_validation.rs create mode 100644 modules/block_vrf_validator/src/state.rs diff --git a/Cargo.lock b/Cargo.lock index 5c50efda..cfcec8e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,7 +116,7 @@ dependencies = [ ] [[package]] -name = "acropolis_module_block_validator" +name = "acropolis_module_block_vrf_validator" version = "0.1.0" dependencies = [ "acropolis_common", @@ -126,7 +126,6 @@ dependencies = [ "config", "hex", "imbl", - "kes-summed-ed25519 0.2.1 (git+https://github.com/txpipe/kes?rev=f69fb357d46f6a18925543d785850059569d7e78)", "pallas", "serde", "serde_json", @@ -418,7 +417,7 @@ dependencies = [ "acropolis_module_address_state", "acropolis_module_assets_state", "acropolis_module_block_unpacker", - "acropolis_module_block_validator", + "acropolis_module_block_vrf_validator", "acropolis_module_drdd_state", "acropolis_module_drep_state", "acropolis_module_epochs_state", @@ -3158,18 +3157,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "kes-summed-ed25519" -version = "0.2.1" -source = "git+https://github.com/txpipe/kes?rev=f69fb357d46f6a18925543d785850059569d7e78#f69fb357d46f6a18925543d785850059569d7e78" -dependencies = [ - "blake2 0.10.6", - "ed25519-dalek", - "rand_core 0.6.4", - "thiserror 1.0.69", - "zeroize", -] - [[package]] name = "lapin" version = "2.5.5" @@ -3505,7 +3492,7 @@ dependencies = [ "ed25519-dalek", "fixed", "hex", - "kes-summed-ed25519 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "kes-summed-ed25519", "mithril-build-script", "mithril-stm", "nom 8.0.0", diff --git a/common/src/genesis_values.rs b/common/src/genesis_values.rs index 33781b81..134d98a0 100644 --- a/common/src/genesis_values.rs +++ b/common/src/genesis_values.rs @@ -1,16 +1,76 @@ -use crate::calculations::{ - epoch_to_first_slot_with_shelley_params, slot_to_epoch_with_shelley_params, - slot_to_timestamp_with_params, +use std::collections::BTreeMap; + +use crate::{ + calculations::{ + epoch_to_first_slot_with_shelley_params, slot_to_epoch_with_shelley_params, + slot_to_timestamp_with_params, + }, + hash::Hash, }; const MAINNET_SHELLEY_GENESIS_HASH: &str = "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81"; +pub type GenesisKey = Hash<28>; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] +pub struct GenDeleg { + // Pool Id + pub delegate: Hash<28>, + pub vrf: Hash<32>, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenesisDelegs(pub BTreeMap); + +impl AsRef> for GenesisDelegs { + fn as_ref(&self) -> &BTreeMap { + &self.0 + } +} + +impl From> for GenesisDelegs { + fn from(entries: Vec<(String, (String, String))>) -> Self { + let map = entries + .into_iter() + .map(|(key_hash, (delegate, vrf))| { + let key = Hash::new( + hex::decode(key_hash) + .expect("Invalid key hash hex string") + .try_into() + .expect("Invalid Genesis Key length"), + ); + let delegate_hash = Hash::new( + hex::decode(delegate) + .expect("Invalid delegate hex string") + .try_into() + .expect("Invalid delegate hash length"), + ); + let vrf_hash = Hash::new( + hex::decode(vrf) + .expect("Invalid VRF hex string") + .try_into() + .expect("Invalid VRF hash length"), + ); + ( + key, + GenDeleg { + delegate: delegate_hash, + vrf: vrf_hash, + }, + ) + }) + .collect(); + GenesisDelegs(map) + } +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct GenesisValues { pub byron_timestamp: u64, pub shelley_epoch: u64, pub shelley_epoch_len: u64, pub shelley_genesis_hash: [u8; 32], + pub genesis_delegs: GenesisDelegs, } impl GenesisValues { @@ -23,6 +83,64 @@ impl GenesisValues { .unwrap() .try_into() .unwrap(), + genesis_delegs: GenesisDelegs::from(vec![ + ( + "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c".to_string(), + ( + "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d".to_string(), + "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7" + .to_string(), + ), + ), + ( + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497".to_string(), + ( + "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048".to_string(), + "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc" + .to_string(), + ), + ), + ( + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1".to_string(), + ( + "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956".to_string(), + "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674" + .to_string(), + ), + ), + ( + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757".to_string(), + ( + "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011".to_string(), + "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2" + .to_string(), + ), + ), + ( + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82".to_string(), + ( + "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6".to_string(), + "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32" + .to_string(), + ), + ), + ( + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6".to_string(), + ( + "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b".to_string(), + "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af" + .to_string(), + ), + ), + ( + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b".to_string(), + ( + "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23".to_string(), + "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db" + .to_string(), + ), + ), + ]), } } diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index b3339ec0..c0172a8a 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -264,6 +264,34 @@ impl From for Nonce { } } +impl Nonce { + pub fn from_number(n: u64) -> Self { + let mut hasher = Blake2b::::new(); + hasher.update(&n.to_be_bytes()); + let hash: NonceHash = hasher.finalize().into(); + Self::from(hash) + } + + pub fn neutral() -> Self { + Self { + tag: NonceVariant::NeutralNonce, + hash: None, + } + } + + /// Seed constant for eta (randomness/entropy) computation + /// Used when generating the epoch nonce + pub fn seed_eta() -> Self { + Self::from_number(0) + } + + /// Seed constant for leader (L) computation + /// Used when determining if a stake pool is the slot leader + pub fn seed_l() -> Self { + Self::from_number(1) + } +} + #[derive( Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize, serde::Deserialize, )] diff --git a/modules/block_validator/src/block_validator.rs b/modules/block_validator/src/block_validator.rs deleted file mode 100644 index 1e02d9a9..00000000 --- a/modules/block_validator/src/block_validator.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Acropolis Block VRF Validator module for Caryatid -//! Validate the VRF calculation in the block header -use acropolis_common::messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}; -use anyhow::Result; -use caryatid_sdk::{module, Context, Module}; -use config::Config; -use std::sync::Arc; -use tracing::{error, info}; -mod state; -use state::State; -mod ouroboros; - -const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = - ("validation-vrf-publisher-topic", "cardano.validation.vrf"); -const DEFAULT_VALIDATION_KES_PUBLISHER_TOPIC: (&str, &str) = - ("validation-kes-publisher-topic", "cardano.validation.kes"); -const DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC: (&str, &str) = - ("block-header-subscribe-topic", "cardano.block.header"); -const DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC: (&str, &str) = - ("epoch-nonces-subscribe-topic", "cardano.epoch.nonces"); - -/// Block Validator module -#[module( - message_type(Message), - name = "block-validator", - description = "Validate the block header" -)] - -pub struct BlockValidator; - -impl BlockValidator { - async fn run() -> Result<()> { - Ok(()) - } - - pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { - // Publish topics - let validation_vrf_publisher_topic = config - .get_string(DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC.0) - .unwrap_or(DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC.1.to_string()); - info!("Creating validation VRF publisher on '{validation_vrf_publisher_topic}'"); - - // Subscribe topics - let block_headers_subscribe_topic = config - .get_string(DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC.0) - .unwrap_or(DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC.1.to_string()); - info!("Creating block headers subscription on '{block_headers_subscribe_topic}'"); - - let epoch_nonces_subscribe_topic = config - .get_string(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.0) - .unwrap_or(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.1.to_string()); - info!("Creating epoch nonces subscription on '{epoch_nonces_subscribe_topic}'"); - - // Subscribers - let block_headers_subscription = context.subscribe(&block_headers_subscribe_topic).await?; - let epoch_nonces_subscription = context.subscribe(&epoch_nonces_subscribe_topic).await?; - - // Start run task - context.run(async move { - Self::run().await.unwrap_or_else(|e| error!("Failed: {e}")); - }); - - Ok(()) - } -} diff --git a/modules/block_validator/src/ouroboros/assert_header.rs b/modules/block_validator/src/ouroboros/assert_header.rs deleted file mode 100644 index 592f3a38..00000000 --- a/modules/block_validator/src/ouroboros/assert_header.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::ouroboros::vrf; -use acropolis_common::{crypto::keyhash_256, protocol_params::Nonce, KeyHash, Slot}; -use pallas::ledger::primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; -use std::array::TryFromSliceError; -use thiserror::Error; - -#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] -pub enum BlockHeaderValidationError { - #[error("{0}")] - KnownLeaderVrf(#[from] KnownLeaderVrfError), - #[error("{0}")] - VrfProof(#[from] VrfProofError), -} - -/// AssertKnownLeaderVrfError - -#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -#[error( - "declared leader's VRF credentials differs from those registered in the ledger (registered={} vs declared={})", - hex::encode(®istered_vrf[0..7]), - hex::encode(&declared_vrf[0..7]), -)] -pub struct KnownLeaderVrfError { - registered_vrf: KeyHash, - declared_vrf: KeyHash, -} - -impl KnownLeaderVrfError { - /// Asserts that the declared VRF credentials advertised in a block do indeed match those - /// registered for the corresponding leader. - pub fn new(registered_vrf_hash: &KeyHash, vrf_vkey: &[u8]) -> Result<(), Self> { - let declared_vrf_hash = keyhash_256(vrf_vkey); - if !declared_vrf_hash.eq(registered_vrf_hash) { - return Err(Self { - registered_vrf: registered_vrf_hash.clone(), - declared_vrf: declared_vrf_hash, - }); - } - Ok(()) - } -} - -// ------------------------------------------------------------ assert_vrf_proof - -#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] -pub enum VrfProofError { - #[error("Malformed VRF proof: {0}")] - MalformedProof(#[from] vrf::ProofFromBytesError), - - #[error("Invalid VRF proof: {0}")] - InvalidProof(vrf::ProofVerifyError, Slot, Nonce, Vec), - - #[error("could not convert slice to array")] - TryFromSliceError, - - #[error( - "Mismatch between the declared VRF proof hash in block ({}) and the computed one ({}).", - hex::encode(&declared[0..7]), - hex::encode(&computed[0..7]), - )] - ProofMismatch { - // this is Proof Hash (sha512 hash) - declared: Vec, - computed: Vec, - }, - - #[error( - "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", - hex::encode(&declared[0..7]), - hex::encode(&computed[0..7]), - )] - OutputMismatch { - declared: Vec, - computed: Vec, - }, -} - -impl From for VrfProofError { - fn from(_: TryFromSliceError) -> Self { - Self::TryFromSliceError - } -} - -impl PartialEq for VrfProofError { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::MalformedProof(l0), Self::MalformedProof(r0)) => l0 == r0, - (Self::InvalidProof(l0, l1, l2, l3), Self::InvalidProof(r0, r1, r2, r3)) => { - l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3 - } - (Self::TryFromSliceError, Self::TryFromSliceError) => true, - ( - Self::ProofMismatch { - declared: l_declared, - computed: l_computed, - }, - Self::ProofMismatch { - declared: r_declared, - computed: r_computed, - }, - ) => l_declared == r_declared && l_computed == r_computed, - ( - Self::OutputMismatch { - declared: l_declared, - computed: l_computed, - }, - Self::OutputMismatch { - declared: r_declared, - computed: r_computed, - }, - ) => l_declared == r_declared && l_computed == r_computed, - _ => false, - } - } -} - -impl VrfProofError { - /// Validate the VRF output from the block and its corresponding hash. - pub fn new( - absolute_slot: Slot, - epoch_nonce: &Nonce, - leader_vrf_output: &[u8], - // Public Key from declared_vrf_key from block header - leader_public_key: &vrf::PublicKey, - // must be [u8; 64] - unsafe_vrf_proof_hash: &[u8], - // must be [u8; 80] - unsafe_vrf_proof: &[u8], - ) -> Result<(), Self> { - let input = &vrf::VrfInput::new(absolute_slot, epoch_nonce); - let block_proof_hash: [u8; vrf::Proof::HASH_SIZE] = unsafe_vrf_proof_hash.try_into()?; - let block_proof: [u8; vrf::Proof::SIZE] = unsafe_vrf_proof.try_into()?; - - // Verify the VRF proof - let vrf_proof = vrf::Proof::try_from(&block_proof)?; - let proof_hash = vrf_proof.verify(leader_public_key, input).map_err(|e| { - Self::InvalidProof( - e, - absolute_slot, - epoch_nonce.clone(), - leader_public_key.as_ref().to_vec(), - ) - })?; - if !proof_hash.as_slice().eq(&block_proof_hash) { - return Err(Self::ProofMismatch { - declared: block_proof_hash.to_vec(), - computed: proof_hash.to_vec(), - }); - } - - // The proof was valid. Make sure that the leader's output matches what was in the block - let calculated_leader_vrf_output = - derive_tagged_vrf_output(proof_hash.as_slice(), VrfDerivation::Leader); - if calculated_leader_vrf_output.as_slice() != leader_vrf_output { - return Err(Self::OutputMismatch { - declared: leader_vrf_output.to_vec(), - computed: calculated_leader_vrf_output, - }); - } - - Ok(()) - } -} diff --git a/modules/block_validator/src/ouroboros/kes.rs b/modules/block_validator/src/ouroboros/kes.rs deleted file mode 100644 index 8e6cb43c..00000000 --- a/modules/block_validator/src/ouroboros/kes.rs +++ /dev/null @@ -1,214 +0,0 @@ -use kes_summed_ed25519::{ - self as kes, - kes::{Sum6Kes, Sum6KesSig}, - traits::{KesSig, KesSk}, -}; -use std::{array::TryFromSliceError, ops::Deref}; -use thiserror::Error; - -// ------------------------------------------------------------------- SecretKey - -/// KES secret key -pub struct SecretKey<'a>(Sum6Kes<'a>); - -impl SecretKey<'_> { - /// Create a new KES secret key - pub fn from_bytes(sk_bytes: &mut Vec) -> Result, Error> { - // TODO: extend() could potentially re-allocate memory to a new location and copy the sk_bytes. - // This would leave the original memory containing the secret key without being wiped. - sk_bytes.extend([0u8; 4]); // default to period = 0 - let sum_6_kes = Sum6Kes::from_bytes(sk_bytes.as_mut_slice())?; - Ok(SecretKey(sum_6_kes)) - } - - /// Get the current period of the KES secret key - pub fn get_period(&self) -> u32 { - self.0.get_period() - } - - /// Update the KES secret key to the next period - pub fn update(&mut self) -> Result<(), Error> { - Ok(self.0.update()?) - } -} - -// ------------------------------------------------------------------- PublicKey - -/// KES public key -pub struct PublicKey(kes::PublicKey); - -impl PublicKey { - /// Size of a KES public key, in bytes; - pub const SIZE: usize = 32; -} - -impl AsRef<[u8]> for PublicKey { - fn as_ref(&self) -> &[u8] { - self.0.as_bytes() - } -} - -impl Deref for PublicKey { - type Target = [u8; Self::SIZE]; - - fn deref(&self) -> &Self::Target { - self.0.as_bytes().try_into().unwrap_or_else(|e| { - unreachable!( - "Impossible! Failed to convert KES public key ({}) back to slice of known size: {e:?}", - hex::encode(self.0), - ) - }) - } -} - -impl From<&SecretKey<'_>> for PublicKey { - fn from(sk: &SecretKey<'_>) -> Self { - PublicKey(sk.0.to_pk()) - } -} - -impl From<&[u8; PublicKey::SIZE]> for PublicKey { - fn from(bytes: &[u8; PublicKey::SIZE]) -> Self { - PublicKey(kes::PublicKey::from_bytes(bytes).unwrap_or_else(|e| { - unreachable!( - "Impossible! Failed to create a KES public key from a slice ({}) of known size: {e:?}", - hex::encode(bytes) - ) - })) - } -} - -impl TryFrom<&[u8]> for PublicKey { - type Error = TryFromSliceError; - - fn try_from(bytes: &[u8]) -> Result { - Ok(Self::from(<&[u8; Self::SIZE]>::try_from(bytes)?)) - } -} - -// ------------------------------------------------------------------- Signature - -/// KES signature -pub struct Signature(Sum6KesSig); - -impl Signature { - /// Size of a KES signature, in bytes; - pub const SIZE: usize = Sum6KesSig::SIZE; - - /// Verify the KES signature - pub fn verify(&self, kes_period: u32, kes_pk: &PublicKey, msg: &[u8]) -> Result<(), Error> { - Ok(self.0.verify(kes_period, &kes_pk.0, msg)?) - } -} - -impl From<&[u8; Self::SIZE]> for Signature { - fn from(bytes: &[u8; Self::SIZE]) -> Self { - Signature(Sum6KesSig::from_bytes(bytes).unwrap_or_else(|e| { - unreachable!("Impossible! Failed to create a KES signature from a slice ({}) of known size: {e:?}", hex::encode(bytes)) - })) - } -} - -impl TryFrom<&[u8]> for Signature { - type Error = TryFromSliceError; - - fn try_from(bytes: &[u8]) -> Result { - Ok(Self::from(<&[u8; Self::SIZE]>::try_from(bytes)?)) - } -} - -impl From<&Signature> for [u8; 448] { - fn from(sig: &Signature) -> Self { - sig.0.to_bytes() - } -} - -// ----------------------------------------------------------------------- Error - -/// KES error -#[derive(Error, Debug)] -pub enum Error { - #[error("KES error: {0}")] - Kes(#[from] kes_summed_ed25519::errors::Error), -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn kes_key_evolution() { - let mut kes_sk_bytes = hex::decode( - "68b77b6e61925be0499d1445fd9210cec5bdfd5dd92662802eb2720ff70bc68fd89\ - 64580ff18bd2b232eb716dfbbeef82e2844b466ddd5dacaad9f15d3c753b3483541\ - 41e973d039b1147c48e71e5b7cadc6deb28c86e4ae4fc26e8bbe1695c3374d4eb10\ - 94a7a698722894301546466c750947778b18ac3270397efd2eced4d25ced55d2bd2\ - c09e7c0fa7b849d41787ca11defc91609d930a9870881a56a587bff20b2c5c59f63\ - ccb008be495917da3fcae536d05401b6771bb1f9356f031b3ddadbffbc426a9a23e\ - 34274b187f7e93892e990644f6273772a02d3e38bee7459ed6a9bb5760fe012e47a\ - 2e75880125e7fb072b2b7a626a5375e2039d8d748cb8ad4dd02697250d3155eee39\ - 308ecc2925405a8c15e1cbe556cc4315d43ee5101003639bcb33bd6e27da3885888\ - d7cca20b05cadbaa53941ef5282cde8f377c3bd0bf732cfac6b5d4d5597a1f72d81\ - bc0d8af634a4c760b309fe8959bbde666ff10310377b313860bd52d56fd7cb14963\ - 3beb1eb2e0076111df61e570a042f7cebae74a8de298a6f114938946230db42651e\ - a4eddf5df2d7d2f3016464073da8a9dc715817b43586a61874e576da7b47a2bb6c2\ - e19d4cbd5b1b39a24427e89b812cce6d30e0506e207f1eaab313c45a236068ea319\ - 958474237a5ffe02736e1c51c02a05999816c9253a557f09375c83acf5d7250f3bb\ - c638e10c58fb274e2002eed841ecef6a9cbc57c3157a7c3cf47e66b1741e8173b66\ - 76ac973bc9715027a3225087cabad45407b891416330485891dc9a3875488a26428\ - d20d581b629a8f4f42e3aa00cbcaae6c8e2b8f3fe033b874d1de6a3f8c321c92b77\ - 643f00d28e", - ) - .unwrap(); - - let kes_sk = SecretKey::from_bytes(&mut kes_sk_bytes).unwrap(); - assert_eq!( - hex::encode(PublicKey::from(&kes_sk)), - "2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292" - ); - } - - #[test] - fn kes_signature_verify() { - let kes_pk_bytes = - hex::decode("2e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292") - .unwrap(); - let kes_pk = PublicKey::try_from(&kes_pk_bytes[..]).unwrap(); - let kes_signature_bytes = hex::decode( - "20f1c8f9ae672e6ec75b0aa63a85e7ab7865b95f6b2907a26b54c14f49184ab52cf\ - 98ef441bb71de50380325b34f16d84fc78d137467a1b49846747cf8ee4701c56f08\ - f198b94c468d46b67b271f5bc30ab2ad14b1bdbf2be0695a00fe4b02b3060fa5212\ - 8f4cce9c5759df0ba8d71fe99456bd2e333671e45110908d03a2ec3b38599d26adf\ - 182ba63f79900fdb2732947cf8e940a4cf1e8db9b4cf4c001dbd37c60d0e38851de\ - 4910807896153be455e13161342d4c6f7bb3e4d2d35dbbbba0ebcd161be2f1ec030\ - d2f5a6059ac89dfa70dc6b3d0bc2da179c62ae95c4f9c7ad9c0387b35bf2b45b325\ - d1e0a18c0c783a0779003bf23e7a6b00cc126c5e3d51a57d41ff1707a76fb2c306a\ - 67c21473b41f1d9a7f64a670ec172a2421da03d796fa97086de8812304f4f96bd45\ - 243d0a2ad6c48a69d9e2c0afbb1333acee607d18eb3a33818c3c9d5bb72cade8893\ - 79008bf60d436298cb0cfc6159332cb1af1de4f1d64e79c399d058ac4993704eed6\ - 7917093f89db6cde830383e69aa400ba3225087cabad45407b891416330485891dc\ - 9a3875488a26428d20d581b629a8f4f42e3aa00cbcaae6c8e2b8f3fe033b874d1de\ - 6a3f8c321c92b77643f00d28e", - ) - .unwrap(); - let kes_signature = Signature::try_from(&kes_signature_bytes[..]).unwrap(); - let kes_period = 36u32; - let kes_msg = hex::decode( - "8a1a00a50f121a0802d24458203deea82abe788d260b8987a522aadec86c9f098e8\ - 8a57d7cfcdb24f474a7afb65820cad3c900ca6baee9e65bf61073d900bfbca458ee\ - ca6d0b9f9931f5b1017a8cd65820576d49e98adfab65623dc16f9fff2edd210e8dd\ - 1d4588bfaf8af250beda9d3c7825840d944b8c81000fc1182ec02194ca9eca510fd\ - 84995d22bfe1842190b39d468e5ecbd863969e0c717b0071a371f748d44c895fa92\ - 33094cefcd3107410baabb19a5850f2a29f985d37ca8eb671c2847fab9cc45c9373\ - 8a430b4e43837e7f33028b190a7e55152b0e901548961a66d56eebe72d616f9e68f\ - d13e9955ccd8611c201a5b422ac8ef56af74cb657b5b868ce9d850f1945d1582063\ - 9d4986d17de3cac8079a3b25d671f339467aa3a9948e29992dafebf90f719f84582\ - 02e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b29217\ - 1903e958401feeeabc7460b19370f4050e986b558b149fdc8724b4a4805af8fe45c\ - 8e7a7c6753894ad7a1b9c313da269ddc5922e150da3b378977f1dfea79fc52fd2c1\ - 2f08820901", - ) - .unwrap(); - assert!(kes_signature.verify(kes_period, &kes_pk, &kes_msg).is_ok()); - } -} diff --git a/modules/block_validator/src/ouroboros/mod.rs b/modules/block_validator/src/ouroboros/mod.rs deleted file mode 100644 index 745dff6e..00000000 --- a/modules/block_validator/src/ouroboros/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod assert_header; -pub mod kes; -pub mod vrf; diff --git a/modules/block_validator/src/state.rs b/modules/block_validator/src/state.rs deleted file mode 100644 index c2fe2abe..00000000 --- a/modules/block_validator/src/state.rs +++ /dev/null @@ -1,6 +0,0 @@ -//! Acropolis block_validator state storage - -#[derive(Default, Debug, Clone)] -pub struct State { - -} diff --git a/modules/block_validator/Cargo.toml b/modules/block_vrf_validator/Cargo.toml similarity index 82% rename from modules/block_validator/Cargo.toml rename to modules/block_vrf_validator/Cargo.toml index 61b31295..ef8fd5ee 100644 --- a/modules/block_validator/Cargo.toml +++ b/modules/block_vrf_validator/Cargo.toml @@ -1,7 +1,7 @@ # Acropolis Block VRF Validator [package] -name = "acropolis_module_block_validator" +name = "acropolis_module_block_vrf_validator" version = "0.1.0" edition = "2021" authors = ["Golddy "] @@ -26,8 +26,7 @@ pallas = { workspace = true } # The vrf crate has not been fully tested in production environments and still has several upstream issues that are open PRs but not merged yet. vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } -kes-summed-ed25519 = { git = "https://github.com/txpipe/kes", rev = "f69fb357d46f6a18925543d785850059569d7e78" } blake2 = "0.10.6" [lib] -path = "src/block_validator.rs" +path = "src/block_vrf_validator.rs" diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs new file mode 100644 index 00000000..81dd3b0e --- /dev/null +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -0,0 +1,188 @@ +//! Acropolis Block VRF Validator module for Caryatid +//! Validate the VRF calculation in the block header +use acropolis_common::{ + messages::{CardanoMessage, Message, StateQuery, StateQueryResponse}, + state_history::{StateHistory, StateHistoryStore}, + BlockStatus, Era, +}; +use anyhow::Result; +use caryatid_sdk::{module, Context, Module, Subscription}; +use config::Config; +use pallas::ledger::traverse::MultiEraHeader; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::{error, info, info_span}; +mod state; +use state::State; + +use crate::ouroboros::vrf_validation::validate_vrf; +mod ouroboros; + +const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = + ("validation-vrf-publisher-topic", "cardano.validation.vrf"); +const DEFAULT_VALIDATION_KES_PUBLISHER_TOPIC: (&str, &str) = + ("validation-kes-publisher-topic", "cardano.validation.kes"); + +const DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC: (&str, &str) = ( + "bootstrapped-subscribe-topic", + "cardano.sequence.bootstrapped", +); +const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( + "protocol-parameters-subscribe-topic", + "cardano.protocol.parameters", +); +const DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC: (&str, &str) = + ("block-header-subscribe-topic", "cardano.block.header"); +const DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC: (&str, &str) = + ("epoch-nonces-subscribe-topic", "cardano.epoch.nonces"); +/// Block VRF Validator module +#[module( + message_type(Message), + name = "block-vrf-validator", + description = "Validate the VRF calculation in the block header" +)] + +pub struct BlockVrfValidator; + +impl BlockVrfValidator { + async fn run( + history: Arc>>, + mut bootstrapped_subscription: Box>, + mut protocol_parameters_subscription: Box>, + mut block_headers_subscription: Box>, + mut epoch_nonces_subscription: Box>, + ) -> Result<()> { + let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; + let genesis = match bootstrapped_message.as_ref() { + Message::Cardano((_, CardanoMessage::GenesisComplete(complete))) => { + complete.values.clone() + } + _ => panic!("Unexpected message in genesis completion topic: {bootstrapped_message:?}"), + }; + + // Consume initial protocol parameters + let _ = protocol_parameters_subscription.read().await?; + + loop { + // Get a mutable state + let mut state = history.lock().await.get_or_init_with(|| State::new()); + + // Read both topics in parallel + let block_headers_message_f = block_headers_subscription.read(); + let (_, message) = block_headers_message_f.await?; + match message.as_ref() { + Message::Cardano((block_info, CardanoMessage::BlockHeader(header_msg))) => { + // handle rollback here + if block_info.status == BlockStatus::RolledBack { + state = history.lock().await.get_rolled_back_state(block_info.number); + } + let is_new_epoch = block_info.new_epoch && block_info.epoch > 0; + + // read protocol parameters if new epoch + if is_new_epoch { + let (_, protocol_parameters_msg) = + protocol_parameters_subscription.read().await?; + if let Message::Cardano((_, CardanoMessage::ProtocolParams(params))) = + protocol_parameters_msg.as_ref() + { + state.handle_protocol_parameters(params); + } + } + + // decode header + // Derive the variant from the era - just enough to make + // MultiEraHeader::decode() work. + let variant = match block_info.era { + Era::Byron => 0, + Era::Shelley => 1, + Era::Allegra => 2, + Era::Mary => 3, + Era::Alonzo => 4, + _ => 5, + }; + let span = info_span!( + "block_vrf_validator.decode_header", + block = block_info.number + ); + let mut header = None; + span.in_scope(|| { + header = match MultiEraHeader::decode(variant, None, &header_msg.raw) { + Ok(header) => Some(header), + Err(e) => { + error!("Can't decode header {}: {e}", block_info.slot); + None + } + }; + }); + + let span = + info_span!("block_vrf_validator.validate", block = block_info.number); + span.in_scope(|| { + if let Some(header) = header.as_ref() { + state.validate_block_vrf(block_info, header, &genesis); + } + }); + } + _ => error!("Unexpected message type: {message:?}"), + } + } + + Ok(()) + } + + pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { + // Publish topics + let validation_vrf_publisher_topic = config + .get_string(DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC.0) + .unwrap_or(DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC.1.to_string()); + info!("Creating validation VRF publisher on '{validation_vrf_publisher_topic}'"); + + // Subscribe topics + let bootstrapped_subscribe_topic = config + .get_string(DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating subscriber for bootstrapped on '{bootstrapped_subscribe_topic}'"); + let protocol_parameters_subscribe_topic = config + .get_string(DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating subscriber for protocol parameters on '{protocol_parameters_subscribe_topic}'"); + + let block_headers_subscribe_topic = config + .get_string(DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_BLOCK_HEADER_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating block headers subscription on '{block_headers_subscribe_topic}'"); + + let epoch_nonces_subscribe_topic = config + .get_string(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating epoch nonces subscription on '{epoch_nonces_subscribe_topic}'"); + + // Subscribers + let bootstrapped_subscription = context.subscribe(&bootstrapped_subscribe_topic).await?; + let protocol_parameters_subscription = + context.subscribe(&protocol_parameters_subscribe_topic).await?; + let block_headers_subscription = context.subscribe(&block_headers_subscribe_topic).await?; + let epoch_nonces_subscription = context.subscribe(&epoch_nonces_subscribe_topic).await?; + + // state history + let history = Arc::new(Mutex::new(StateHistory::::new( + "block_vrf_validator", + StateHistoryStore::default_block_store(), + ))); + + // Start run task + context.run(async move { + Self::run( + history, + bootstrapped_subscription, + protocol_parameters_subscription, + block_headers_subscription, + epoch_nonces_subscription, + ) + .await + .unwrap_or_else(|e| error!("Failed: {e}")); + }); + + Ok(()) + } +} diff --git a/modules/block_vrf_validator/src/ouroboros/mod.rs b/modules/block_vrf_validator/src/ouroboros/mod.rs new file mode 100644 index 00000000..d90675aa --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/mod.rs @@ -0,0 +1,5 @@ +pub mod overlay_shedule; +mod tpraos; +mod types; +pub mod vrf; +pub mod vrf_validation; diff --git a/modules/block_vrf_validator/src/ouroboros/overlay_shedule.rs b/modules/block_vrf_validator/src/ouroboros/overlay_shedule.rs new file mode 100644 index 00000000..a31f88fc --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/overlay_shedule.rs @@ -0,0 +1,165 @@ +//! Ouroboros overlay schedule +//! This is to validate the blocks which are reserved for Genesis Keys. +//! +//! Reference: https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L318 +//! +//! https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/Rules/Overlay.hs#L332 + +use acropolis_common::{ + genesis_values::{GenDeleg, GenesisDelegs, GenesisKey}, + rational_number::RationalNumber, + rest_helper::ToCheckedF64, +}; +use anyhow::Result; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OBftSlot { + NonActiveSlot, + ActiveSlot(GenesisKey, GenDeleg), +} + +/// Determine if the given slot is reserved for the overlay schedule. +/// +/// # Arguments +/// * `epoch_slot` - The slot number delta of the block in the current epoch +/// (i.e. block's slot number - epoch's first slot number) +/// * `decentralisation_param` - The decentralization parameter +/// +/// # Returns +/// `true` if the slot is reserved for the overlay schedule +/// +/// If the slot is an overlay slot, then we skip StakeThreshold validation +/// since this block is produced by genesis key (without "lottery") +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L334 +pub fn is_overlay_slot(epoch_slot: u64, decentralisation_param: RationalNumber) -> Result { + let d = decentralisation_param.to_checked_f64("decentralisation_param").map_err(|e| { + anyhow::anyhow!("Failed to convert decentralisation parameter to f64: {}", e) + })?; + + // step function: ceiling of (x * d) + let step = |x: f64| (x * d).ceil() as i64; + + Ok(step(epoch_slot as f64) < step((epoch_slot as f64) + 1.0)) +} + +/// Classify a slot in the overlay schedule, determining which genesis node +/// should produce the block if it's an active overlay slot. +/// +/// # Arguments +/// * `epoch_slot` - The slot number delta of the block in the current epoch +/// * `genesis_keys` - Set of genesis node key hashes +/// * `decentralisation_param` - The decentralization parameter +/// * `active_slots_coeff` - The active slot coefficient +/// +/// # Returns +/// Classification of the slot (NonActiveSlot or ActiveSlot with genesis key) +pub fn classify_overlay_slot( + epoch_slot: u64, + genesis_delegs: &GenesisDelegs, + decentralisation_param: RationalNumber, + active_slots_coeff: RationalNumber, +) -> Result { + let d = decentralisation_param.to_checked_f64("decentralisation_param").map_err(|e| { + anyhow::anyhow!("Failed to convert decentralisation parameter to f64: {}", e) + })?; + let position = (epoch_slot as f64 * d).ceil() as i64; + + // Calculate active slot coefficient inverse + let asc_inv = active_slots_coeff + .recip() + .to_checked_f64("active_slots_coeff") + .map_err(|e| anyhow::anyhow!("Failed to convert active slots coefficient to f64: {}", e))? + .floor() as i64; + + let is_active = position % asc_inv == 0; + + if is_active { + let genesis_idx = ((position / asc_inv) % genesis_delegs.as_ref().len() as i64) as usize; + + // Get the element at index from the set + let (key_hash, gen_deleg) = genesis_delegs.as_ref().iter().nth(genesis_idx).unwrap(); + Ok(OBftSlot::ActiveSlot(key_hash.clone(), gen_deleg.clone())) + } else { + Ok(OBftSlot::NonActiveSlot) + } +} + +/// Look up a slot in the overlay schedule to determine if it's reserved +/// and, if so, which genesis node should produce the block. +/// +/// # Arguments +/// * `epoch_slot` - The slot number delta of the block in the current epoch +/// * `genesis_keys` - Set of genesis node key hashes and their delegations +/// * `decentralisation_param` - The decentralization parameter +/// * `active_slots_coeff` - The active slot coefficient +/// +/// # Returns +/// * `Some(OBftSlot)` if the slot is in the overlay schedule +/// * `None` if the slot is not in the overlay schedule +pub fn lookup_in_overlay_schedule( + epoch_slot: u64, + genesis_delegs: &GenesisDelegs, + decentralisation_param: RationalNumber, + active_slots_coeff: RationalNumber, +) -> Result> { + let is_overlay_slot = is_overlay_slot(epoch_slot, decentralisation_param)?; + if is_overlay_slot { + let obft_slot = classify_overlay_slot( + epoch_slot, + genesis_delegs, + decentralisation_param, + active_slots_coeff, + )?; + Ok(Some(obft_slot)) + } else { + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use acropolis_common::genesis_values::GenesisValues; + + use super::*; + + #[test] + fn test_lookup_in_overlay_schedule_1() { + let genesis_values = GenesisValues::mainnet(); + let genesis_delegs = genesis_values.genesis_delegs; + let decentralisation_param = RationalNumber::from(1); + let active_slots_coeff = RationalNumber::new(1, 20); + let epoch_slot = 0; + let obft_slot = lookup_in_overlay_schedule( + epoch_slot, + &genesis_delegs, + decentralisation_param, + active_slots_coeff, + ) + .unwrap(); + assert!(obft_slot.is_some()); + assert_eq!( + obft_slot.unwrap(), + OBftSlot::ActiveSlot( + genesis_delegs.as_ref().keys().nth(0).unwrap().clone(), + genesis_delegs.as_ref().values().nth(0).unwrap().clone() + ) + ); + } + + #[test] + fn test_lookup_in_overlay_schedule_2() { + let genesis_values = GenesisValues::mainnet(); + let genesis_delegs = genesis_values.genesis_delegs; + let decentralisation_param = RationalNumber::new(1, 2); + let active_slots_coeff = RationalNumber::new(1, 20); + let epoch_slot = 1; + let obft_slot = lookup_in_overlay_schedule( + epoch_slot, + &genesis_delegs, + decentralisation_param, + active_slots_coeff, + ) + .unwrap(); + assert!(obft_slot.is_none()); + } +} diff --git a/modules/block_vrf_validator/src/ouroboros/tpraos.rs b/modules/block_vrf_validator/src/ouroboros/tpraos.rs new file mode 100644 index 00000000..ace4d29e --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/tpraos.rs @@ -0,0 +1,52 @@ +use acropolis_common::protocol_params::Nonce; +use blake2::{digest::consts::U32, Blake2b, Digest}; + +use crate::ouroboros::types::Seed; + +/// Construct a seed to use in the VRF computation. +/// +/// This seed is used for VRF proofs in the Praos consensus protocol. +/// It combines the slot number and epoch nonce, optionally with a +/// universal constant for domain separation. +/// +/// # Arguments +/// +/// * `uc_nonce` - Universal constant nonce (domain separator) +/// - Use `seed_eta()` for randomness/eta computation +/// - Use `seed_l()` for leader election computation +/// * `slot` - The slot number +/// * `e_nonce` - The epoch nonce (randomness from the epoch) +/// +/// # Returns +/// +/// A `Seed` that can be used for VRF computation +/// +/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L405 +/// +pub fn mk_seed(uc_nonce: &Nonce, slot: u64, epoch_nonce: &Nonce) -> Seed { + // 8 bytes for slot + optionally 32 bytes for epoch nonce + let mut data = Vec::with_capacity(8 + 32); + data.extend_from_slice(&slot.to_be_bytes()); + if let Some(e_hash) = epoch_nonce.hash { + data.extend_from_slice(&e_hash); + } + let mut hasher = Blake2b::::new(); + hasher.update(data); + let seed_hash: [u8; 32] = hasher.finalize().into(); + + // XOR with universal constant if provided + let final_hash = match uc_nonce.hash.as_ref() { + Some(uc_hash) => xor_hash(&seed_hash, uc_hash), + None => seed_hash, + }; + + Seed::from(final_hash) +} + +fn xor_hash(hash1: &[u8; 32], hash2: &[u8; 32]) -> [u8; 32] { + let mut result = [0u8; 32]; + for i in 0..32 { + result[i] = hash1[i] ^ hash2[i]; + } + result +} diff --git a/modules/block_vrf_validator/src/ouroboros/types.rs b/modules/block_vrf_validator/src/ouroboros/types.rs new file mode 100644 index 00000000..a21b9199 --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/types.rs @@ -0,0 +1,3 @@ +use acropolis_common::hash::Hash; + +pub type Seed = Hash<32>; diff --git a/modules/block_validator/src/ouroboros/vrf.rs b/modules/block_vrf_validator/src/ouroboros/vrf.rs similarity index 81% rename from modules/block_validator/src/ouroboros/vrf.rs rename to modules/block_vrf_validator/src/ouroboros/vrf.rs index c97bacae..5533e09c 100644 --- a/modules/block_validator/src/ouroboros/vrf.rs +++ b/modules/block_vrf_validator/src/ouroboros/vrf.rs @@ -62,8 +62,26 @@ impl VrfInput { /// Size of a VRF input challenge, in bytes pub const SIZE: usize = 32; + /// Create a new input challenge from an absolute slot, an epoch entropy (nonce) and a domain specific nonce + /// This is for TPraos Protocol + pub fn mk_seed(absolute_slot: u64, epoch_nonce: &Nonce, uc_nonce: &Nonce) -> Self { + let mut hasher = Blake2b::::new(); + let mut data: Vec = Vec::::with_capacity(8 + 32); + data.extend_from_slice(&absolute_slot.to_be_bytes()); + if let Some(hash) = epoch_nonce.hash { + data.extend_from_slice(&hash); + } + hasher.update(&data); + let mut seed_hash: [u8; 32] = hasher.finalize().into(); + if let Some(uc_hash) = uc_nonce.hash.as_ref() { + seed_hash = xor_hash(&seed_hash, uc_hash); + } + VrfInput(seed_hash) + } + /// Create a new input challenge from an absolute slot number and an epoch entropy (nonce) (a.k.a η0) - pub fn new(absolute_slot_number: u64, epoch_nonce: &Nonce) -> Self { + /// This is for Praos Protocol + pub fn mk_vrf_input(absolute_slot_number: u64, epoch_nonce: &Nonce) -> Self { let mut hasher = Blake2b::::new(); let mut data = Vec::::with_capacity(8 + 32); data.extend_from_slice(&absolute_slot_number.to_be_bytes()); @@ -181,3 +199,11 @@ mod serde_remote { VrfOutputInvalid, } } + +fn xor_hash(hash1: &[u8; 32], hash2: &[u8; 32]) -> [u8; 32] { + let mut result = [0u8; 32]; + for i in 0..32 { + result[i] = hash1[i] ^ hash2[i]; + } + result +} diff --git a/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs b/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs new file mode 100644 index 00000000..017213cf --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs @@ -0,0 +1,361 @@ +use crate::ouroboros::{overlay_shedule, vrf}; +use acropolis_common::{ + crypto::keyhash_256, + genesis_values::GenesisDelegs, + protocol_params::{Nonce, PraosParams, ShelleyParams}, + BlockInfo, KeyHash, Slot, +}; +use anyhow::Result; +use pallas::ledger::{ + primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}, + traverse::MultiEraHeader, +}; +use std::array::TryFromSliceError; +use thiserror::Error; + +#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +pub enum VrfValidationError { + #[error("{0}")] + ShelleyParams(String), + #[error("{0}")] + KnownLeaderVrf(#[from] KnownLeaderVrfError), + #[error("{0}")] + TPraosVrfProof(#[from] TPraosVrfProofError), + #[error("{0}")] + PraosVrfProof(#[from] PraosVrfProofError), +} + +// ------------------------------------------------------------ assert_known_leader_vrf + +#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[error( + "declared leader's VRF credentials differs from those registered in the ledger (registered={} vs declared={})", + hex::encode(®istered_vrf[0..7]), + hex::encode(&declared_vrf[0..7]), +)] +pub struct KnownLeaderVrfError { + registered_vrf: KeyHash, + declared_vrf: KeyHash, +} + +impl KnownLeaderVrfError { + /// Asserts that the declared VRF credentials advertised in a block do indeed match those + /// registered for the corresponding leader. + pub fn new(registered_vrf_hash: &KeyHash, vrf_vkey: &[u8]) -> Result<(), Self> { + let declared_vrf_hash = keyhash_256(vrf_vkey); + if !declared_vrf_hash.eq(registered_vrf_hash) { + return Err(Self { + registered_vrf: registered_vrf_hash.clone(), + declared_vrf: declared_vrf_hash, + }); + } + Ok(()) + } +} + +// ------------------------------------------------------------ assert_tpraos_vrf_proof + +#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +pub enum TPraosVrfProofError { + #[error("Malformed Nonce VRF proof: {0}")] + MalformedNonceProof(vrf::ProofFromBytesError), + + /// (error, absolute_slot, nonce, leader_public_key) + #[error("Invalid Nonce VRF proof: {0}")] + InvalidNonceProof(vrf::ProofVerifyError, Slot, Nonce, Vec), + + #[error("Malformed Leader VRF proof: {0}")] + MalformedLeaderProof(vrf::ProofFromBytesError), + + #[error("Invalid Leader VRF proof: {0}")] + InvalidLeaderProof(vrf::ProofVerifyError, Slot, Nonce, Vec), + + #[error("could not convert slice to array")] + TryFromSliceError, + + #[error( + "Mismatch between the declared Nonce VRF proof hash in block ({}) and the computed one ({}).", + hex::encode(&declared[0..7]), + hex::encode(&computed[0..7]), + )] + NonceProofMismatch { + // this is Proof Hash (sha512 hash) + declared: Vec, + computed: Vec, + }, + + #[error( + "Mismatch between the declared Leader VRF proof hash in block ({}) and the computed one ({}).", + hex::encode(&declared[0..7]), + hex::encode(&computed[0..7]), + )] + LeaderProofMismatch { + declared: Vec, + computed: Vec, + }, +} + +impl From for TPraosVrfProofError { + fn from(_: TryFromSliceError) -> Self { + Self::TryFromSliceError + } +} + +impl PartialEq for TPraosVrfProofError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::MalformedNonceProof(l0), Self::MalformedNonceProof(r0)) => l0 == r0, + (Self::InvalidNonceProof(l0, l1, l2, l3), Self::InvalidNonceProof(r0, r1, r2, r3)) => { + l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3 + } + (Self::MalformedLeaderProof(l0), Self::MalformedLeaderProof(r0)) => l0 == r0, + ( + Self::InvalidLeaderProof(l0, l1, l2, l3), + Self::InvalidLeaderProof(r0, r1, r2, r3), + ) => l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3, + (Self::TryFromSliceError, Self::TryFromSliceError) => true, + ( + Self::NonceProofMismatch { + declared: l_declared, + computed: l_computed, + }, + Self::NonceProofMismatch { + declared: r_declared, + computed: r_computed, + }, + ) => l_declared == r_declared && l_computed == r_computed, + ( + Self::LeaderProofMismatch { + declared: l_declared, + computed: l_computed, + }, + Self::LeaderProofMismatch { + declared: r_declared, + computed: r_computed, + }, + ) => l_declared == r_declared && l_computed == r_computed, + _ => false, + } + } +} + +/// This is VRF Validation for TPraos Protocol +/// In TPraos, they use different validation flow than Praos Protocol. +/// Main difference is in TPraos, there are 2 VRF Outputs and Proofs when in Praos, those are combined into one using derived Tag. +/// So need to validate 2 VRF Proofs - one for Nonce and one for Leader +/// +impl TPraosVrfProofError { + /// Validate the VRF output from the block and its corresponding hash. + /// in TPraos Protocol + pub fn new( + absolute_slot: Slot, + epoch_nonce: &Nonce, + // Public Key from declared_vrf_key from block header + leader_public_key: &vrf::PublicKey, + unsafe_nonce_vrf_proof_hash: &[u8], // must be [u8; 64] + unsafe_nonce_vrf_proof: &[u8], // must be [u8; 80] + unsafe_leader_vrf_proof_hash: &[u8], // must be [u8; 64] + unsafe_leader_vrf_proof: &[u8], // must be [u8; 80] + ) -> Result<(), Self> { + // For nonce proof validation + let seed_eta = Nonce::seed_eta(); + // For leader proof validation + let seed_l = Nonce::seed_l(); + + // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L365 + let rho_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_eta); + // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L366 + let y_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_l); + + let nonce_vrf_proof_hash: [u8; vrf::Proof::HASH_SIZE] = + unsafe_nonce_vrf_proof_hash.try_into()?; + let nonce_vrf_proof: [u8; vrf::Proof::SIZE] = unsafe_nonce_vrf_proof.try_into()?; + let leader_vrf_proof_hash: [u8; vrf::Proof::HASH_SIZE] = + unsafe_leader_vrf_proof_hash.try_into()?; + let leader_vrf_proof: [u8; vrf::Proof::SIZE] = unsafe_leader_vrf_proof.try_into()?; + + // Verify Nonce VRF proof + let nonce_vrf_proof = + vrf::Proof::try_from(&nonce_vrf_proof).map_err(|e| Self::MalformedNonceProof(e))?; + let nonce_proof_hash = + nonce_vrf_proof.verify(leader_public_key, &rho_seed).map_err(|e| { + Self::InvalidNonceProof( + e, + absolute_slot, + epoch_nonce.clone(), + leader_public_key.as_ref().to_vec(), + ) + })?; + if !nonce_proof_hash.as_slice().eq(&nonce_vrf_proof_hash) { + return Err(Self::NonceProofMismatch { + declared: nonce_vrf_proof_hash.to_vec(), + computed: nonce_proof_hash.to_vec(), + }); + } + + // Verify Leader VRF proof + let leader_vrf_proof = + vrf::Proof::try_from(&leader_vrf_proof).map_err(|e| Self::MalformedLeaderProof(e))?; + let leader_proof_hash = + leader_vrf_proof.verify(leader_public_key, &y_seed).map_err(|e| { + Self::InvalidLeaderProof( + e, + absolute_slot, + epoch_nonce.clone(), + leader_public_key.as_ref().to_vec(), + ) + })?; + if !leader_proof_hash.as_slice().eq(&leader_vrf_proof_hash) { + return Err(Self::LeaderProofMismatch { + declared: leader_vrf_proof_hash.to_vec(), + computed: leader_proof_hash.to_vec(), + }); + } + + Ok(()) + } +} + +// ------------------------------------------------------------ assert_praos_vrf_proof + +#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +pub enum PraosVrfProofError { + #[error("Malformed VRF proof: {0}")] + MalformedProof(#[from] vrf::ProofFromBytesError), + + #[error("Invalid VRF proof: {0}")] + InvalidProof(vrf::ProofVerifyError, Slot, Nonce, Vec), + + #[error("could not convert slice to array")] + TryFromSliceError, + + #[error( + "Mismatch between the declared VRF proof hash in block ({}) and the computed one ({}).", + hex::encode(&declared[0..7]), + hex::encode(&computed[0..7]), + )] + ProofMismatch { + // this is Proof Hash (sha512 hash) + declared: Vec, + computed: Vec, + }, + + #[error( + "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", + hex::encode(&declared[0..7]), + hex::encode(&computed[0..7]), + )] + OutputMismatch { + declared: Vec, + computed: Vec, + }, +} + +impl From for PraosVrfProofError { + fn from(_: TryFromSliceError) -> Self { + Self::TryFromSliceError + } +} + +impl PartialEq for PraosVrfProofError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::MalformedProof(l0), Self::MalformedProof(r0)) => l0 == r0, + (Self::InvalidProof(l0, l1, l2, l3), Self::InvalidProof(r0, r1, r2, r3)) => { + l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3 + } + (Self::TryFromSliceError, Self::TryFromSliceError) => true, + ( + Self::ProofMismatch { + declared: l_declared, + computed: l_computed, + }, + Self::ProofMismatch { + declared: r_declared, + computed: r_computed, + }, + ) => l_declared == r_declared && l_computed == r_computed, + ( + Self::OutputMismatch { + declared: l_declared, + computed: l_computed, + }, + Self::OutputMismatch { + declared: r_declared, + computed: r_computed, + }, + ) => l_declared == r_declared && l_computed == r_computed, + _ => false, + } + } +} + +impl PraosVrfProofError { + /// Validate the VRF output from the block and its corresponding hash. + /// in TPraos Protocol + pub fn new( + absolute_slot: Slot, + epoch_nonce: &Nonce, + leader_vrf_output: &[u8], + // Public Key from declared_vrf_key from block header + leader_public_key: &vrf::PublicKey, + // must be [u8; 64] + unsafe_vrf_proof_hash: &[u8], + // must be [u8; 80] + unsafe_vrf_proof: &[u8], + ) -> Result<(), Self> { + let input = &vrf::VrfInput::mk_vrf_input(absolute_slot, epoch_nonce); + let block_proof_hash: [u8; vrf::Proof::HASH_SIZE] = unsafe_vrf_proof_hash.try_into()?; + let block_proof: [u8; vrf::Proof::SIZE] = unsafe_vrf_proof.try_into()?; + + // Verify the VRF proof + let vrf_proof = vrf::Proof::try_from(&block_proof)?; + let proof_hash = vrf_proof.verify(leader_public_key, input).map_err(|e| { + Self::InvalidProof( + e, + absolute_slot, + epoch_nonce.clone(), + leader_public_key.as_ref().to_vec(), + ) + })?; + if !proof_hash.as_slice().eq(&block_proof_hash) { + return Err(Self::ProofMismatch { + declared: block_proof_hash.to_vec(), + computed: proof_hash.to_vec(), + }); + } + + // The proof was valid. Make sure that the leader's output matches what was in the block + let calculated_leader_vrf_output = + derive_tagged_vrf_output(proof_hash.as_slice(), VrfDerivation::Leader); + if calculated_leader_vrf_output.as_slice() != leader_vrf_output { + return Err(Self::OutputMismatch { + declared: leader_vrf_output.to_vec(), + computed: calculated_leader_vrf_output, + }); + } + + Ok(()) + } +} + +pub fn validate_vrf( + block_info: &BlockInfo, + header: &MultiEraHeader, + shelley_params: &ShelleyParams, + praos_params: &PraosParams, + genesis_delegs: &GenesisDelegs, +) -> Result<(), VrfValidationError> { + let decentralisation_param = shelley_params.protocol_params.decentralisation_param; + let active_slots_coeff = praos_params.active_slots_coeff; + + // first look up for overlay slot + let obft_slot = overlay_shedule::lookup_in_overlay_schedule( + block_info.epoch_slot, + genesis_delegs, + decentralisation_param, + active_slots_coeff, + ); + + Ok(()) +} diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs new file mode 100644 index 00000000..49d62bb6 --- /dev/null +++ b/modules/block_vrf_validator/src/state.rs @@ -0,0 +1,71 @@ +//! Acropolis block_vrf_validator state storage + +use acropolis_common::{ + genesis_values::{GenesisDelegs, GenesisValues}, + messages::ProtocolParamsMessage, + protocol_params::{PraosParams, ShelleyParams}, + BlockInfo, +}; +use anyhow::Result; +use pallas::ledger::traverse::MultiEraHeader; + +use crate::ouroboros::vrf_validation::{self, VrfValidationError}; + +#[derive(Default, Debug, Clone)] +pub struct State { + // shelley params + pub shelly_params: Option, + + // protocol parameter for Praos and TPraos + pub praos_params: Option, +} + +impl State { + pub fn new() -> Self { + Self { + praos_params: None, + shelly_params: None, + } + } + + /// Handle protocol parameters updates + pub fn handle_protocol_parameters(&mut self, msg: &ProtocolParamsMessage) { + if let Some(shelly_params) = msg.params.shelley.as_ref() { + self.shelly_params = Some(shelly_params.clone()); + self.praos_params = Some(shelly_params.into()); + } + } + + pub fn validate_block_vrf( + &self, + block_info: &BlockInfo, + header: &MultiEraHeader, + genesis: &GenesisValues, + ) -> Result<(), VrfValidationError> { + // Validation starts after Shelley Era + if block_info.epoch < genesis.shelley_epoch { + return Ok(()); + } + + let Some(shelley_params) = self.shelly_params.as_ref() else { + return Err(VrfValidationError::ShelleyParams( + "Shelley Params are not set".to_string(), + )); + }; + let Some(praos_params) = self.praos_params.as_ref() else { + return Err(VrfValidationError::ShelleyParams( + "Praos Params are not set".to_string(), + )); + }; + + vrf_validation::validate_vrf( + block_info, + header, + shelley_params, + praos_params, + &genesis.genesis_delegs, + )?; + + Ok(()) + } +} diff --git a/modules/epochs_state/src/state.rs b/modules/epochs_state/src/state.rs index 551c7c9b..1f1c173b 100644 --- a/modules/epochs_state/src/state.rs +++ b/modules/epochs_state/src/state.rs @@ -269,7 +269,7 @@ mod tests { use super::*; use acropolis_common::{ - crypto::keyhash_224, + crypto::{keyhash_224, keyhash_256}, protocol_params::{Nonce, NonceHash}, state_history::{StateHistory, StateHistoryStore}, BlockHash, BlockInfo, BlockStatus, Era, @@ -522,6 +522,21 @@ mod tests { let block_header = MultiEraHeader::decode(1, None, &e_208_first_block_header_cbor).unwrap(); assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); + println!("slot: {:?}", block_header.slot()); + println!( + "issuer vkey: {:?}", + hex::encode(keyhash_224(block_header.issuer_vkey().unwrap())) + ); + println!( + "vrf vkey: {:?}", + hex::encode(keyhash_256(block_header.vrf_vkey().unwrap())) + ); + + println!( + "leader vrf output: {:?}", + block_header.leader_vrf_output().unwrap().len() + ); + let nonces = state.nonces.unwrap(); let evolved = Nonce::from( NonceHash::try_from( diff --git a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs index b256954c..60ef13d6 100644 --- a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs +++ b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs @@ -2,7 +2,7 @@ //! Reads genesis files and outputs initial UTXO events use acropolis_common::{ - genesis_values::GenesisValues, + genesis_values::{GenesisDelegs, GenesisValues}, messages::{ CardanoMessage, GenesisCompleteMessage, GenesisUTxOsMessage, Message, PotDeltasMessage, UTXODeltasMessage, @@ -205,6 +205,22 @@ impl GenesisBootstrapper { shelley_epoch: shelley_start_epoch, shelley_epoch_len: shelley_genesis.epoch_length.unwrap() as u64, shelley_genesis_hash, + genesis_delegs: GenesisDelegs::from( + shelley_genesis + .gen_delegs + .unwrap() + .iter() + .map(|(key, value)| { + ( + key.to_string(), + ( + value.delegate.as_ref().unwrap().to_string(), + value.vrf.as_ref().unwrap().to_string(), + ), + ) + }) + .collect::>(), + ), }; // Send completion message diff --git a/modules/upstream_chain_fetcher/src/utils.rs b/modules/upstream_chain_fetcher/src/utils.rs index dd3ee50b..2810bdbe 100644 --- a/modules/upstream_chain_fetcher/src/utils.rs +++ b/modules/upstream_chain_fetcher/src/utils.rs @@ -7,6 +7,7 @@ use config::Config; use pallas::network::facades; use pallas::network::facades::PeerClient; use serde::Deserialize; +use std::collections::BTreeMap; use std::sync::Arc; use tracing::{error, info}; @@ -94,6 +95,8 @@ impl FetcherConfig { shelley_epoch, shelley_epoch_len, shelley_genesis_hash, + // TODO: load genesis keys from config + genesis_keys: BTreeMap::new(), }) } diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index 19244d71..b8ead63c 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -21,7 +21,7 @@ acropolis_module_governance_state = { path = "../../modules/governance_state" } acropolis_module_parameters_state = { path = "../../modules/parameters_state" } acropolis_module_stake_delta_filter = { path = "../../modules/stake_delta_filter" } acropolis_module_epochs_state = { path = "../../modules/epochs_state" } -acropolis_module_block_validator = { path = "../../modules/block_validator" } +acropolis_module_block_vrf_validator = { path = "../../modules/block_vrf_validator" } acropolis_module_accounts_state = { path = "../../modules/accounts_state" } acropolis_module_rest_blockfrost = { path = "../../modules/rest_blockfrost" } acropolis_module_spdd_state = { path = "../../modules/spdd_state" } diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index 2b9af370..45c00398 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -13,7 +13,7 @@ use acropolis_module_accounts_state::AccountsState; use acropolis_module_address_state::AddressState; use acropolis_module_assets_state::AssetsState; use acropolis_module_block_unpacker::BlockUnpacker; -use acropolis_module_block_validator::BlockValidator; +use acropolis_module_block_vrf_validator::BlockVrfValidator; use acropolis_module_drdd_state::DRDDState; use acropolis_module_drep_state::DRepState; use acropolis_module_epochs_state::EpochsState; @@ -100,7 +100,7 @@ pub async fn main() -> Result<()> { ParametersState::register(&mut process); StakeDeltaFilter::register(&mut process); EpochsState::register(&mut process); - BlockValidator::register(&mut process); + BlockVrfValidator::register(&mut process); AccountsState::register(&mut process); AddressState::register(&mut process); AssetsState::register(&mut process); From 5fa68e9f54021d56ee7a674601bc0ce167f9558d Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 23 Oct 2025 16:15:08 +0200 Subject: [PATCH 05/36] refactor: move ouroboros to common --- Cargo.lock | 5 ++--- common/Cargo.toml | 4 ++++ common/src/lib.rs | 1 + {modules/block_vrf_validator => common}/src/ouroboros/mod.rs | 3 +-- .../src/ouroboros/overlay_shedule.rs | 4 ++-- .../block_vrf_validator => common}/src/ouroboros/tpraos.rs | 4 ++-- {modules/block_vrf_validator => common}/src/ouroboros/vrf.rs | 3 ++- .../src/ouroboros/vrf_validation.rs | 2 +- modules/block_vrf_validator/Cargo.toml | 5 ----- modules/block_vrf_validator/src/block_vrf_validator.rs | 3 --- modules/block_vrf_validator/src/ouroboros/types.rs | 3 --- modules/block_vrf_validator/src/state.rs | 5 ++--- 12 files changed, 17 insertions(+), 25 deletions(-) rename {modules/block_vrf_validator => common}/src/ouroboros/mod.rs (72%) rename {modules/block_vrf_validator => common}/src/ouroboros/overlay_shedule.rs (98%) rename {modules/block_vrf_validator => common}/src/ouroboros/tpraos.rs (95%) rename {modules/block_vrf_validator => common}/src/ouroboros/vrf.rs (99%) rename {modules/block_vrf_validator => common}/src/ouroboros/vrf_validation.rs (99%) delete mode 100644 modules/block_vrf_validator/src/ouroboros/types.rs diff --git a/Cargo.lock b/Cargo.lock index b0029595..a06e3f49 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,7 @@ dependencies = [ "minicbor 0.26.5", "num-rational", "num-traits", + "pallas 0.33.0", "rayon", "serde", "serde_json", @@ -54,6 +55,7 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tracing", + "vrf_dalek", ] [[package]] @@ -134,7 +136,6 @@ version = "0.1.0" dependencies = [ "acropolis_common", "anyhow", - "blake2 0.10.6", "caryatid_sdk", "config", "hex", @@ -142,10 +143,8 @@ dependencies = [ "pallas 0.33.0", "serde", "serde_json", - "thiserror 2.0.17", "tokio", "tracing", - "vrf_dalek", ] [[package]] diff --git a/common/Cargo.toml b/common/Cargo.toml index 572dd415..8056f95a 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -46,6 +46,10 @@ thiserror = "2.0.17" sha2 = "0.10.8" caryatid_process.workspace = true config.workspace = true +pallas = { workspace = true } + +# The vrf crate has not been fully tested in production environments and still has several upstream issues that are open PRs but not merged yet. +vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } [lib] crate-type = ["rlib"] diff --git a/common/src/lib.rs b/common/src/lib.rs index abf55551..08e98f8a 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -10,6 +10,7 @@ pub mod hash; pub mod ledger_state; pub mod math; pub mod messages; +pub mod ouroboros; pub mod params; pub mod protocol_params; pub mod queries; diff --git a/modules/block_vrf_validator/src/ouroboros/mod.rs b/common/src/ouroboros/mod.rs similarity index 72% rename from modules/block_vrf_validator/src/ouroboros/mod.rs rename to common/src/ouroboros/mod.rs index d90675aa..e06b3e0c 100644 --- a/modules/block_vrf_validator/src/ouroboros/mod.rs +++ b/common/src/ouroboros/mod.rs @@ -1,5 +1,4 @@ pub mod overlay_shedule; -mod tpraos; -mod types; +pub mod tpraos; pub mod vrf; pub mod vrf_validation; diff --git a/modules/block_vrf_validator/src/ouroboros/overlay_shedule.rs b/common/src/ouroboros/overlay_shedule.rs similarity index 98% rename from modules/block_vrf_validator/src/ouroboros/overlay_shedule.rs rename to common/src/ouroboros/overlay_shedule.rs index ecf166eb..e494a5d6 100644 --- a/modules/block_vrf_validator/src/ouroboros/overlay_shedule.rs +++ b/common/src/ouroboros/overlay_shedule.rs @@ -5,7 +5,7 @@ //! //! https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/Rules/Overlay.hs#L332 -use acropolis_common::{ +use crate::{ genesis_values::{GenDeleg, GenesisDelegs, GenesisKey}, rational_number::RationalNumber, rest_helper::ToCheckedF64, @@ -118,7 +118,7 @@ pub fn lookup_in_overlay_schedule( #[cfg(test)] mod tests { - use acropolis_common::genesis_values::GenesisValues; + use crate::genesis_values::GenesisValues; use super::*; diff --git a/modules/block_vrf_validator/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs similarity index 95% rename from modules/block_vrf_validator/src/ouroboros/tpraos.rs rename to common/src/ouroboros/tpraos.rs index ace4d29e..99cd6ea1 100644 --- a/modules/block_vrf_validator/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -1,7 +1,7 @@ -use acropolis_common::protocol_params::Nonce; +use crate::{hash::Hash, protocol_params::Nonce}; use blake2::{digest::consts::U32, Blake2b, Digest}; -use crate::ouroboros::types::Seed; +pub type Seed = Hash<32>; /// Construct a seed to use in the VRF computation. /// diff --git a/modules/block_vrf_validator/src/ouroboros/vrf.rs b/common/src/ouroboros/vrf.rs similarity index 99% rename from modules/block_vrf_validator/src/ouroboros/vrf.rs rename to common/src/ouroboros/vrf.rs index 5533e09c..c5e328c2 100644 --- a/modules/block_vrf_validator/src/ouroboros/vrf.rs +++ b/common/src/ouroboros/vrf.rs @@ -1,6 +1,5 @@ use std::{array::TryFromSliceError, ops::Deref}; -use acropolis_common::protocol_params::Nonce; use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; use thiserror::Error; @@ -9,6 +8,8 @@ use vrf_dalek::{ vrf03::{PublicKey03, VrfProof03}, }; +use crate::protocol_params::Nonce; + /// A VRF public key #[derive(Debug, PartialEq)] pub struct PublicKey(PublicKey03); diff --git a/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs similarity index 99% rename from modules/block_vrf_validator/src/ouroboros/vrf_validation.rs rename to common/src/ouroboros/vrf_validation.rs index 017213cf..8620dbcd 100644 --- a/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -1,5 +1,5 @@ use crate::ouroboros::{overlay_shedule, vrf}; -use acropolis_common::{ +use crate::{ crypto::keyhash_256, genesis_values::GenesisDelegs, protocol_params::{Nonce, PraosParams, ShelleyParams}, diff --git a/modules/block_vrf_validator/Cargo.toml b/modules/block_vrf_validator/Cargo.toml index ef8fd5ee..38420311 100644 --- a/modules/block_vrf_validator/Cargo.toml +++ b/modules/block_vrf_validator/Cargo.toml @@ -20,13 +20,8 @@ imbl = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } serde_json = { workspace = true } -thiserror = "2.0.17" serde = { workspace = true } pallas = { workspace = true } -# The vrf crate has not been fully tested in production environments and still has several upstream issues that are open PRs but not merged yet. -vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } -blake2 = "0.10.6" - [lib] path = "src/block_vrf_validator.rs" diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 7d4e5ff3..88217952 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -14,7 +14,6 @@ use tokio::sync::Mutex; use tracing::{error, info, info_span}; mod state; use state::State; -mod ouroboros; const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = ("validation-vrf-publisher-topic", "cardano.validation.vrf"); @@ -123,8 +122,6 @@ impl BlockVrfValidator { _ => error!("Unexpected message type: {message:?}"), } } - - Ok(()) } pub async fn init(&self, context: Arc>, config: Arc) -> Result<()> { diff --git a/modules/block_vrf_validator/src/ouroboros/types.rs b/modules/block_vrf_validator/src/ouroboros/types.rs deleted file mode 100644 index a21b9199..00000000 --- a/modules/block_vrf_validator/src/ouroboros/types.rs +++ /dev/null @@ -1,3 +0,0 @@ -use acropolis_common::hash::Hash; - -pub type Seed = Hash<32>; diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 49d62bb6..430cf627 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -1,16 +1,15 @@ //! Acropolis block_vrf_validator state storage use acropolis_common::{ - genesis_values::{GenesisDelegs, GenesisValues}, + genesis_values::GenesisValues, messages::ProtocolParamsMessage, + ouroboros::vrf_validation::{self, VrfValidationError}, protocol_params::{PraosParams, ShelleyParams}, BlockInfo, }; use anyhow::Result; use pallas::ledger::traverse::MultiEraHeader; -use crate::ouroboros::vrf_validation::{self, VrfValidationError}; - #[derive(Default, Debug, Clone)] pub struct State { // shelley params From 9d7b9fe209679c78b8eb5d2c5e2be4a5656b36f1 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 23 Oct 2025 17:25:22 +0200 Subject: [PATCH 06/36] refactor: vrf validation errors --- common/src/ouroboros/overlay_shedule.rs | 13 + common/src/ouroboros/vrf.rs | 37 ++- common/src/ouroboros/vrf_validation.rs | 382 ++++++++++++------------ common/src/protocol_params.rs | 11 +- 4 files changed, 240 insertions(+), 203 deletions(-) diff --git a/common/src/ouroboros/overlay_shedule.rs b/common/src/ouroboros/overlay_shedule.rs index e494a5d6..85758b5e 100644 --- a/common/src/ouroboros/overlay_shedule.rs +++ b/common/src/ouroboros/overlay_shedule.rs @@ -14,7 +14,11 @@ use anyhow::Result; #[derive(Debug, Clone, PartialEq, Eq)] pub enum OBftSlot { + /// Not an overlay slot - any pool can compete with VRF threshold + NotOverlay, + /// Overlay slot but no block should be produced (rare edge case) NonActiveSlot, + /// Active overlay slot reserved for specific genesis key ActiveSlot(GenesisKey, GenDeleg), } @@ -59,6 +63,10 @@ pub fn classify_overlay_slot( decentralisation_param: RationalNumber, active_slots_coeff: RationalNumber, ) -> Result { + if genesis_delegs.as_ref().is_empty() { + return Ok(OBftSlot::NotOverlay); + } + let d = decentralisation_param.to_checked_f64("decentralisation_param").map_err(|e| { anyhow::anyhow!("Failed to convert decentralisation parameter to f64: {}", e) })?; @@ -96,6 +104,11 @@ pub fn classify_overlay_slot( /// # Returns /// * `Some(OBftSlot)` if the slot is in the overlay schedule /// * `None` if the slot is not in the overlay schedule +/// +/// # Panics +/// `ShelleyParamsError` if: +/// - decentralisation_param is not a valid rational number +/// - active_slots_coeff is not a valid rational number pub fn lookup_in_overlay_schedule( epoch_slot: u64, genesis_delegs: &GenesisDelegs, diff --git a/common/src/ouroboros/vrf.rs b/common/src/ouroboros/vrf.rs index c5e328c2..d67c15d3 100644 --- a/common/src/ouroboros/vrf.rs +++ b/common/src/ouroboros/vrf.rs @@ -13,7 +13,7 @@ use crate::protocol_params::Nonce; /// A VRF public key #[derive(Debug, PartialEq)] pub struct PublicKey(PublicKey03); - +pub type PublicKeyHash = [u8; PublicKey::HASH_SIZE]; impl PublicKey { /// Size of a VRF public key, in bytes. pub const SIZE: usize = 32; @@ -63,8 +63,26 @@ impl VrfInput { /// Size of a VRF input challenge, in bytes pub const SIZE: usize = 32; - /// Create a new input challenge from an absolute slot, an epoch entropy (nonce) and a domain specific nonce - /// This is for TPraos Protocol + /// TPraos: Construct a seed to use in the VRF computation. + /// + /// This seed is used for VRF proofs in the Praos consensus protocol. + /// It combines the slot number and epoch nonce, optionally with a + /// universal constant for domain separation. + /// + /// # Arguments + /// + /// * `uc_nonce` - Universal constant nonce (domain separator) + /// - Use `seed_eta()` for randomness/eta computation + /// - Use `seed_l()` for leader election computation + /// * `slot` - The slot number + /// * `e_nonce` - The epoch nonce (randomness from the epoch) + /// + /// # Returns + /// + /// A `Seed` that can be used for VRF computation + /// + /// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L405 + /// pub fn mk_seed(absolute_slot: u64, epoch_nonce: &Nonce, uc_nonce: &Nonce) -> Self { let mut hasher = Blake2b::::new(); let mut data: Vec = Vec::::with_capacity(8 + 32); @@ -80,8 +98,17 @@ impl VrfInput { VrfInput(seed_hash) } - /// Create a new input challenge from an absolute slot number and an epoch entropy (nonce) (a.k.a η0) - /// This is for Praos Protocol + /// Praos: Construct VRF input from slot and epoch nonce + /// + /// # Arguments + /// * `slot` - Current slot number + /// * `epoch_nonce` - Epoch nonce (randomness for this epoch) + /// + /// # Returns + /// 32-byte input for VRF function + /// + /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos/VRF.hs#L67 + /// pub fn mk_vrf_input(absolute_slot_number: u64, epoch_nonce: &Nonce) -> Self { let mut hasher = Blake2b::::new(); let mut data = Vec::::with_capacity(8 + 32); diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index 8620dbcd..de1549fb 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -1,4 +1,5 @@ use crate::ouroboros::{overlay_shedule, vrf}; +use crate::PoolId; use crate::{ crypto::keyhash_256, genesis_values::GenesisDelegs, @@ -13,123 +14,181 @@ use pallas::ledger::{ use std::array::TryFromSliceError; use thiserror::Error; +/// Reference +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 #[derive(Error, Debug, serde::Serialize, serde::Deserialize)] pub enum VrfValidationError { + /// **Cause:** The Shelley protocol parameters used to validate the block, #[error("{0}")] - ShelleyParams(String), + InvalidShelleyParams(String), + /// **Cause:** Block issuer's pool ID is not registered in current stake distribution + #[error("Unknown Pool: {}", hex::encode(&pool_id))] + UnknownPool { pool_id: PoolId }, + /// **Cause:** The VRF key hash in the block header doesn't match the VRF key + /// registered with this stake pool in the ledger state #[error("{0}")] - KnownLeaderVrf(#[from] KnownLeaderVrfError), + WrongLeaderVrfKey(#[from] WrongLeaderVrfKeyError), + /// VRF nonce proof verification failed (TPraos rho - nonce proof) + /// **Cause:** The (rho - nonce) VRF proof failed verification #[error("{0}")] - TPraosVrfProof(#[from] TPraosVrfProofError), + TPraosBadNonceVrfProof(#[from] TPraosBadNonceVrfProofError), + /// VRF leader proof verification failed (TPraos y - leader proof) + /// **Cause:** The (y - leader) VRF proof failed verification #[error("{0}")] - PraosVrfProof(#[from] PraosVrfProofError), + TPraosBadLeaderVrfProof(#[from] TPraosBadLeaderVrfProofError), + /// VRF proof cryptographic verification failed (Praos single proof) + /// **Cause:** The cryptographic VRF proof is invalid + #[error("{0}")] + PraosBadVrfProof(#[from] PraosBadVrfProofError), + /// **Cause:** The VRF output is too large for this pool's stake. + /// The pool lost the slot lottery in TPraos Protocol + #[error("TPraos VRF Leader Value Too Big")] + TPraosVrfLeaderValueTooBig, + /// **Cause:** The VRF output is too large for this pool's stake. + /// The pool lost the slot lottery in Praos Protocol + #[error("Praos VRF Leader Value Too Big")] + PraosVrfLeaderValueTooBig, } -// ------------------------------------------------------------ assert_known_leader_vrf +// ------------------------------------------------------------ WrongLeaderVrfKeyError #[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[error( - "declared leader's VRF credentials differs from those registered in the ledger (registered={} vs declared={})", - hex::encode(®istered_vrf[0..7]), - hex::encode(&declared_vrf[0..7]), + "Wrong Leader VRF Key: Pool ID={}, Registered VRF Hash={}, Header VRF Hash={}", + hex::encode(&pool_id), + hex::encode(®istered_vrf_hash), + hex::encode(&header_vrf_hash), )] -pub struct KnownLeaderVrfError { - registered_vrf: KeyHash, - declared_vrf: KeyHash, +pub struct WrongLeaderVrfKeyError { + pool_id: PoolId, + registered_vrf_hash: KeyHash, + header_vrf_hash: KeyHash, } -impl KnownLeaderVrfError { - /// Asserts that the declared VRF credentials advertised in a block do indeed match those - /// registered for the corresponding leader. - pub fn new(registered_vrf_hash: &KeyHash, vrf_vkey: &[u8]) -> Result<(), Self> { - let declared_vrf_hash = keyhash_256(vrf_vkey); - if !declared_vrf_hash.eq(registered_vrf_hash) { +impl WrongLeaderVrfKeyError { + pub fn new( + pool_id: &PoolId, + registered_vrf_hash: &KeyHash, + vrf_vkey: &[u8], + ) -> Result<(), Self> { + let header_vrf_hash = keyhash_256(vrf_vkey); + if !registered_vrf_hash.eq(&header_vrf_hash) { return Err(Self { - registered_vrf: registered_vrf_hash.clone(), - declared_vrf: declared_vrf_hash, + pool_id: pool_id.clone(), + registered_vrf_hash: registered_vrf_hash.clone(), + header_vrf_hash: header_vrf_hash, }); } Ok(()) } } -// ------------------------------------------------------------ assert_tpraos_vrf_proof +// ------------------------------------------------------------ TPraosBadNonceVrfProofError #[derive(Error, Debug, serde::Serialize, serde::Deserialize)] -pub enum TPraosVrfProofError { - #[error("Malformed Nonce VRF proof: {0}")] - MalformedNonceProof(vrf::ProofFromBytesError), +pub enum TPraosBadNonceVrfProofError { + #[error("Bad Nonce VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] + BadVrfProof(Slot, Nonce, BadVrfProofError), +} - /// (error, absolute_slot, nonce, leader_public_key) - #[error("Invalid Nonce VRF proof: {0}")] - InvalidNonceProof(vrf::ProofVerifyError, Slot, Nonce, Vec), +impl TPraosBadNonceVrfProofError { + /// Validate the VRF output from the block and its corresponding hash. + /// in TPraos Protocol for Nonce + pub fn new( + absolute_slot: Slot, + epoch_nonce: &Nonce, + // Declared VRF Public Key from block header + leader_public_key: &vrf::PublicKey, + // Declared VRF Proof Hash from block header (sha512 hash) + unsafe_vrf_proof_hash: &[u8], + // Declared VRF Proof from block header (80 bytes) + unsafe_vrf_proof: &[u8], + ) -> Result<(), Self> { + // For nonce proof validation + let seed_eta = Nonce::seed_eta(); + // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L365 + let rho_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_eta); - #[error("Malformed Leader VRF proof: {0}")] - MalformedLeaderProof(vrf::ProofFromBytesError), + // Verify the Nonce VRF proof + BadVrfProofError::new( + &rho_seed, + leader_public_key, + unsafe_vrf_proof_hash, + unsafe_vrf_proof, + ) + .map_err(|e| Self::BadVrfProof(absolute_slot, epoch_nonce.clone(), e))?; + Ok(()) + } +} - #[error("Invalid Leader VRF proof: {0}")] - InvalidLeaderProof(vrf::ProofVerifyError, Slot, Nonce, Vec), +// ------------------------------------------------------------ TPraosBadLeaderVrfProofError - #[error("could not convert slice to array")] - TryFromSliceError, +#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +pub enum TPraosBadLeaderVrfProofError { + #[error("Bad Leader VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] + BadVrfProof(Slot, Nonce, BadVrfProofError), +} - #[error( - "Mismatch between the declared Nonce VRF proof hash in block ({}) and the computed one ({}).", - hex::encode(&declared[0..7]), - hex::encode(&computed[0..7]), - )] - NonceProofMismatch { - // this is Proof Hash (sha512 hash) - declared: Vec, - computed: Vec, - }, +impl TPraosBadLeaderVrfProofError { + /// Validate the VRF output from the block and its corresponding hash. + /// in TPraos Protocol for Leader + pub fn new( + absolute_slot: Slot, + epoch_nonce: &Nonce, + // Declared VRF Public Key from block header + leader_public_key: &vrf::PublicKey, + // Declared VRF Proof Hash from block header (sha512 hash) + unsafe_vrf_proof_hash: &[u8], + // Declared VRF Proof from block header (80 bytes) + unsafe_vrf_proof: &[u8], + ) -> Result<(), Self> { + // For leader proof validation + let seed_l = Nonce::seed_l(); + // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L366 + let y_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_l); + + // Verify the Leader VRF proof + BadVrfProofError::new( + &y_seed, + leader_public_key, + unsafe_vrf_proof_hash, + unsafe_vrf_proof, + ) + .map_err(|e| Self::BadVrfProof(absolute_slot, epoch_nonce.clone(), e))?; + Ok(()) + } +} + +// ------------------------------------------------------------ PraosBadVrfProofError + +#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +pub enum PraosBadVrfProofError { + #[error("Bad VRF proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] + BadVrfProof(Slot, Nonce, BadVrfProofError), #[error( - "Mismatch between the declared Leader VRF proof hash in block ({}) and the computed one ({}).", - hex::encode(&declared[0..7]), - hex::encode(&computed[0..7]), + "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", + hex::encode(&declared), + hex::encode(&computed), )] - LeaderProofMismatch { + OutputMismatch { declared: Vec, computed: Vec, }, } -impl From for TPraosVrfProofError { - fn from(_: TryFromSliceError) -> Self { - Self::TryFromSliceError - } -} - -impl PartialEq for TPraosVrfProofError { +impl PartialEq for PraosBadVrfProofError { fn eq(&self, other: &Self) -> bool { match (self, other) { - (Self::MalformedNonceProof(l0), Self::MalformedNonceProof(r0)) => l0 == r0, - (Self::InvalidNonceProof(l0, l1, l2, l3), Self::InvalidNonceProof(r0, r1, r2, r3)) => { - l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3 + (Self::BadVrfProof(l0, l1, l2), Self::BadVrfProof(r0, r1, r2)) => { + l0 == r0 && l1 == r1 && l2 == r2 } - (Self::MalformedLeaderProof(l0), Self::MalformedLeaderProof(r0)) => l0 == r0, - ( - Self::InvalidLeaderProof(l0, l1, l2, l3), - Self::InvalidLeaderProof(r0, r1, r2, r3), - ) => l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3, - (Self::TryFromSliceError, Self::TryFromSliceError) => true, - ( - Self::NonceProofMismatch { - declared: l_declared, - computed: l_computed, - }, - Self::NonceProofMismatch { - declared: r_declared, - computed: r_computed, - }, - ) => l_declared == r_declared && l_computed == r_computed, ( - Self::LeaderProofMismatch { + Self::OutputMismatch { declared: l_declared, computed: l_computed, }, - Self::LeaderProofMismatch { + Self::OutputMismatch { declared: r_declared, computed: r_computed, }, @@ -139,76 +198,38 @@ impl PartialEq for TPraosVrfProofError { } } -/// This is VRF Validation for TPraos Protocol -/// In TPraos, they use different validation flow than Praos Protocol. -/// Main difference is in TPraos, there are 2 VRF Outputs and Proofs when in Praos, those are combined into one using derived Tag. -/// So need to validate 2 VRF Proofs - one for Nonce and one for Leader -/// -impl TPraosVrfProofError { +impl PraosBadVrfProofError { /// Validate the VRF output from the block and its corresponding hash. - /// in TPraos Protocol + /// in Praos Protocol pub fn new( absolute_slot: Slot, epoch_nonce: &Nonce, - // Public Key from declared_vrf_key from block header + leader_vrf_output: &[u8], + // Declared VRF Public Key from block header leader_public_key: &vrf::PublicKey, - unsafe_nonce_vrf_proof_hash: &[u8], // must be [u8; 64] - unsafe_nonce_vrf_proof: &[u8], // must be [u8; 80] - unsafe_leader_vrf_proof_hash: &[u8], // must be [u8; 64] - unsafe_leader_vrf_proof: &[u8], // must be [u8; 80] + // Declared VRF Proof Hash from block header (sha512 hash) + unsafe_vrf_proof_hash: &[u8], + // Declared VRF Proof from block header (80 bytes) + unsafe_vrf_proof: &[u8], ) -> Result<(), Self> { - // For nonce proof validation - let seed_eta = Nonce::seed_eta(); - // For leader proof validation - let seed_l = Nonce::seed_l(); - - // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L365 - let rho_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_eta); - // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L366 - let y_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_l); + let input = vrf::VrfInput::mk_vrf_input(absolute_slot, epoch_nonce); - let nonce_vrf_proof_hash: [u8; vrf::Proof::HASH_SIZE] = - unsafe_nonce_vrf_proof_hash.try_into()?; - let nonce_vrf_proof: [u8; vrf::Proof::SIZE] = unsafe_nonce_vrf_proof.try_into()?; - let leader_vrf_proof_hash: [u8; vrf::Proof::HASH_SIZE] = - unsafe_leader_vrf_proof_hash.try_into()?; - let leader_vrf_proof: [u8; vrf::Proof::SIZE] = unsafe_leader_vrf_proof.try_into()?; - - // Verify Nonce VRF proof - let nonce_vrf_proof = - vrf::Proof::try_from(&nonce_vrf_proof).map_err(|e| Self::MalformedNonceProof(e))?; - let nonce_proof_hash = - nonce_vrf_proof.verify(leader_public_key, &rho_seed).map_err(|e| { - Self::InvalidNonceProof( - e, - absolute_slot, - epoch_nonce.clone(), - leader_public_key.as_ref().to_vec(), - ) - })?; - if !nonce_proof_hash.as_slice().eq(&nonce_vrf_proof_hash) { - return Err(Self::NonceProofMismatch { - declared: nonce_vrf_proof_hash.to_vec(), - computed: nonce_proof_hash.to_vec(), - }); - } + // Verify the VRF proof + BadVrfProofError::new( + &input, + leader_public_key, + unsafe_vrf_proof_hash, + unsafe_vrf_proof, + ) + .map_err(|e| Self::BadVrfProof(absolute_slot, epoch_nonce.clone(), e))?; - // Verify Leader VRF proof - let leader_vrf_proof = - vrf::Proof::try_from(&leader_vrf_proof).map_err(|e| Self::MalformedLeaderProof(e))?; - let leader_proof_hash = - leader_vrf_proof.verify(leader_public_key, &y_seed).map_err(|e| { - Self::InvalidLeaderProof( - e, - absolute_slot, - epoch_nonce.clone(), - leader_public_key.as_ref().to_vec(), - ) - })?; - if !leader_proof_hash.as_slice().eq(&leader_vrf_proof_hash) { - return Err(Self::LeaderProofMismatch { - declared: leader_vrf_proof_hash.to_vec(), - computed: leader_proof_hash.to_vec(), + // The proof was valid. Make sure that the leader's output matches what was in the block + let calculated_leader_vrf_output = + derive_tagged_vrf_output(unsafe_vrf_proof_hash, VrfDerivation::Leader); + if calculated_leader_vrf_output.as_slice() != leader_vrf_output { + return Err(Self::OutputMismatch { + declared: leader_vrf_output.to_vec(), + computed: calculated_leader_vrf_output, }); } @@ -216,53 +237,44 @@ impl TPraosVrfProofError { } } -// ------------------------------------------------------------ assert_praos_vrf_proof +// ------------------------------------------------------------ BadVrfProofError #[derive(Error, Debug, serde::Serialize, serde::Deserialize)] -pub enum PraosVrfProofError { +pub enum BadVrfProofError { #[error("Malformed VRF proof: {0}")] MalformedProof(#[from] vrf::ProofFromBytesError), #[error("Invalid VRF proof: {0}")] - InvalidProof(vrf::ProofVerifyError, Slot, Nonce, Vec), + /// (error, vrf_input_hash, vrf_public_key_hash) + InvalidProof(vrf::ProofVerifyError, Vec, Vec), #[error("could not convert slice to array")] TryFromSliceError, #[error( - "Mismatch between the declared VRF proof hash in block ({}) and the computed one ({}).", - hex::encode(&declared[0..7]), - hex::encode(&computed[0..7]), + "Mismatch between the declared VRF proof hash ({}) and the computed one ({}).", + hex::encode(&declared), + hex::encode(&computed), )] ProofMismatch { // this is Proof Hash (sha512 hash) declared: Vec, computed: Vec, }, - - #[error( - "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", - hex::encode(&declared[0..7]), - hex::encode(&computed[0..7]), - )] - OutputMismatch { - declared: Vec, - computed: Vec, - }, } -impl From for PraosVrfProofError { +impl From for BadVrfProofError { fn from(_: TryFromSliceError) -> Self { Self::TryFromSliceError } } -impl PartialEq for PraosVrfProofError { +impl PartialEq for BadVrfProofError { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::MalformedProof(l0), Self::MalformedProof(r0)) => l0 == r0, - (Self::InvalidProof(l0, l1, l2, l3), Self::InvalidProof(r0, r1, r2, r3)) => { - l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3 + (Self::InvalidProof(l0, l1, l2), Self::InvalidProof(r0, r1, r2)) => { + l0 == r0 && l1 == r1 && l2 == r2 } (Self::TryFromSliceError, Self::TryFromSliceError) => true, ( @@ -275,66 +287,41 @@ impl PartialEq for PraosVrfProofError { computed: r_computed, }, ) => l_declared == r_declared && l_computed == r_computed, - ( - Self::OutputMismatch { - declared: l_declared, - computed: l_computed, - }, - Self::OutputMismatch { - declared: r_declared, - computed: r_computed, - }, - ) => l_declared == r_declared && l_computed == r_computed, _ => false, } } } -impl PraosVrfProofError { - /// Validate the VRF output from the block and its corresponding hash. - /// in TPraos Protocol +impl BadVrfProofError { + /// Validate the VRF proof pub fn new( - absolute_slot: Slot, - epoch_nonce: &Nonce, - leader_vrf_output: &[u8], - // Public Key from declared_vrf_key from block header - leader_public_key: &vrf::PublicKey, - // must be [u8; 64] + vrf_input: &vrf::VrfInput, + // Declared VRF Public Key from block header + vrf_public_key: &vrf::PublicKey, + // Declared VRF Proof Hash from block header (sha512 hash) unsafe_vrf_proof_hash: &[u8], - // must be [u8; 80] + // Declared VRF Proof from block header (80 bytes) unsafe_vrf_proof: &[u8], ) -> Result<(), Self> { - let input = &vrf::VrfInput::mk_vrf_input(absolute_slot, epoch_nonce); - let block_proof_hash: [u8; vrf::Proof::HASH_SIZE] = unsafe_vrf_proof_hash.try_into()?; - let block_proof: [u8; vrf::Proof::SIZE] = unsafe_vrf_proof.try_into()?; + let vrf_proof: [u8; vrf::Proof::SIZE] = unsafe_vrf_proof.try_into()?; + let vrf_proof_hash: [u8; vrf::Proof::HASH_SIZE] = unsafe_vrf_proof_hash.try_into()?; + let vrf_proof = vrf::Proof::try_from(&vrf_proof)?; // Verify the VRF proof - let vrf_proof = vrf::Proof::try_from(&block_proof)?; - let proof_hash = vrf_proof.verify(leader_public_key, input).map_err(|e| { + let proof_hash = vrf_proof.verify(vrf_public_key, vrf_input).map_err(|e| { Self::InvalidProof( e, - absolute_slot, - epoch_nonce.clone(), - leader_public_key.as_ref().to_vec(), + vrf_input.as_ref().to_vec(), + vrf_public_key.as_ref().to_vec(), ) })?; - if !proof_hash.as_slice().eq(&block_proof_hash) { + if !proof_hash.as_slice().eq(&vrf_proof_hash) { return Err(Self::ProofMismatch { - declared: block_proof_hash.to_vec(), + declared: vrf_proof_hash.to_vec(), computed: proof_hash.to_vec(), }); } - // The proof was valid. Make sure that the leader's output matches what was in the block - let calculated_leader_vrf_output = - derive_tagged_vrf_output(proof_hash.as_slice(), VrfDerivation::Leader); - if calculated_leader_vrf_output.as_slice() != leader_vrf_output { - return Err(Self::OutputMismatch { - declared: leader_vrf_output.to_vec(), - computed: calculated_leader_vrf_output, - }); - } - Ok(()) } } @@ -355,7 +342,8 @@ pub fn validate_vrf( genesis_delegs, decentralisation_param, active_slots_coeff, - ); + ) + .map_err(|e| VrfValidationError::InvalidShelleyParams(e.to_string()))?; Ok(()) } diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index bcc9c965..28141461 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -9,8 +9,8 @@ use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; use chrono::{DateTime, Utc}; use serde_with::{hex::Hex, serde_as}; -use std::collections::HashMap; use std::ops::Deref; +use std::{collections::HashMap, fmt::Display}; #[derive(Debug, Default, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct ProtocolParams { @@ -253,6 +253,15 @@ pub struct Nonce { pub hash: Option, } +impl Display for Nonce { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.hash { + Some(hash) => write!(f, "{}", hex::encode(hash)), + None => write!(f, "NeutralNonce"), + } + } +} + impl Default for Nonce { fn default() -> Self { Self { From 570414d01bed16fced4dfe0a56acdf5f9ec0de2a Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 23 Oct 2025 19:24:28 +0200 Subject: [PATCH 07/36] refactor: restructure ouroboros tpraos validation logic - add test case to validate shelley's first block using validate_vrf_tpraos --- common/src/ouroboros/data/4490511.cbor | 1 + common/src/ouroboros/overlay_shedule.rs | 9 +- common/src/ouroboros/tpraos.rs | 192 +++++++++++++----- common/src/ouroboros/vrf_validation.rs | 89 +++++--- .../src/block_vrf_validator.rs | 2 - modules/block_vrf_validator/src/state.rs | 14 +- modules/epochs_state/src/state.rs | 33 +-- 7 files changed, 210 insertions(+), 130 deletions(-) create mode 100644 common/src/ouroboros/data/4490511.cbor diff --git a/common/src/ouroboros/data/4490511.cbor b/common/src/ouroboros/data/4490511.cbor new file mode 100644 index 00000000..3c5ac69a --- /dev/null +++ b/common/src/ouroboros/data/4490511.cbor @@ -0,0 +1 @@ +828f1a0044850f1a00448e005820f8084c61b6a238acec985b59310b6ecec49c0ab8352249afd7268da5cff2a45758209180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda58206d930cc9d1baade5cd1c70fbc025d3377ce946760c48e511d1abdf8acff6ff1c82584036ec5378d1f5041a59eb8d96e61de96f0950fb41b49ff511f7bc7fd109d4383e1d24be7034e6749c6612700dd5ceb0c66577b88a19ae286b1321d15bce1ab7365850405aa370ff009544a2be4aa5bc52c4456333f4f9b6571d66c5590d2e5629d08b3a3609f4bd0502ed5d0be1abdb7f2ab76aaeae47fe111b0335a4e4def64693162794b8d3c1ca71500f16b1e244724c03825840da5ccc9f8fd62f6c290b5bb2ed9a4258fc9481dd8a0ac80f8936702ad7709a87814d14dca02ce22d7a3e150d171e57914cc058081941e3c6737987524076b6935850402fcbd1d50e4b42fa81dce2eada18df1803af49273d05cbe4a1870983d86759a61005d5d942a53fb68389fe119b5dae823fa3cd6668dc257556bc52086fd879b082dace60edcf0cd592f63103bcd10e0358201033376be025cb705fd8dd02eda11cc73975a062b5d14ffd74d6ff69e69a2ff75820ae981f4a58d135f98d0a0c5aab9fc04944c8409f65187f9778b57905e43769570000584008d56fe9c28eeeb99bda8920a9887f468f1d74bb03c37ef3caffffbee68d88eb33341498ac145a2422c77546db61c2da782587cdf934d69b113ce52ab8bd8b0b02005901c03de52bc307718194c07824cdc0d2fa016e60b19bc1c7162978fdac55cf43d2ac7f3aea635ffc98d53570a98ac19c703dfda935aac478d84f0281f7c8bc7a180b6aa7e738e9381ebb86baf842af4ee229a3e3908cfec16153f1dd5e21dba0316e7625e4c2a8db495d80ca3c93862fcf4ad0a8a79a20f7d3d9733f9dccbb8ac6d623bbec2bb6dbf170a3c33d32a15d2cc02b3ce78c8d61af768fa82067254b2fe3bd7a1e5cdacf71b50998eec98a48b6970eacdc74d5ae40224f95850fbd88df96e192767c641a70ebf2bb9dc7d32a1e36d1113c09f5591622b35aed5c06bf7b894b882c56bd8e12b7da5b16a341e59d3239a3d14f9d0a5ac006dfdc258e215eacb563954ea234797754b9276f9746d1d6589e10f0dfda8130d422db636d02dec7c8b45ff97de40ac541421e3f2d1aed4246cc5bb54600fa567275046a979f3b4d067c0869732129234df68f1150b09285ecf71385034c79ace9e74186bd811770fcd43a449aa9466165731f92ae73ba4ef9c665a1f8851420a8b1568b711fabca735f474a6a800751f73fbbb3ed2ace0c8bdb2c14eed4032b085dd0ac8f6b0d984a441d337ffe6903aa794728a56360e106c84fc1b943af0e383fd1f1939a2416 \ No newline at end of file diff --git a/common/src/ouroboros/overlay_shedule.rs b/common/src/ouroboros/overlay_shedule.rs index 85758b5e..17035d59 100644 --- a/common/src/ouroboros/overlay_shedule.rs +++ b/common/src/ouroboros/overlay_shedule.rs @@ -14,8 +14,6 @@ use anyhow::Result; #[derive(Debug, Clone, PartialEq, Eq)] pub enum OBftSlot { - /// Not an overlay slot - any pool can compete with VRF threshold - NotOverlay, /// Overlay slot but no block should be produced (rare edge case) NonActiveSlot, /// Active overlay slot reserved for specific genesis key @@ -63,10 +61,6 @@ pub fn classify_overlay_slot( decentralisation_param: RationalNumber, active_slots_coeff: RationalNumber, ) -> Result { - if genesis_delegs.as_ref().is_empty() { - return Ok(OBftSlot::NotOverlay); - } - let d = decentralisation_param.to_checked_f64("decentralisation_param").map_err(|e| { anyhow::anyhow!("Failed to convert decentralisation parameter to f64: {}", e) })?; @@ -117,6 +111,9 @@ pub fn lookup_in_overlay_schedule( ) -> Result> { let is_overlay_slot = is_overlay_slot(epoch_slot, decentralisation_param)?; if is_overlay_slot { + if genesis_delegs.as_ref().is_empty() { + return Ok(None); + } let obft_slot = classify_overlay_slot( epoch_slot, genesis_delegs, diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index 99cd6ea1..4b0b93ac 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -1,52 +1,152 @@ -use crate::{hash::Hash, protocol_params::Nonce}; -use blake2::{digest::consts::U32, Blake2b, Digest}; - -pub type Seed = Hash<32>; - -/// Construct a seed to use in the VRF computation. -/// -/// This seed is used for VRF proofs in the Praos consensus protocol. -/// It combines the slot number and epoch nonce, optionally with a -/// universal constant for domain separation. -/// -/// # Arguments -/// -/// * `uc_nonce` - Universal constant nonce (domain separator) -/// - Use `seed_eta()` for randomness/eta computation -/// - Use `seed_l()` for leader election computation -/// * `slot` - The slot number -/// * `e_nonce` - The epoch nonce (randomness from the epoch) -/// -/// # Returns -/// -/// A `Seed` that can be used for VRF computation -/// -/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L405 -/// -pub fn mk_seed(uc_nonce: &Nonce, slot: u64, epoch_nonce: &Nonce) -> Seed { - // 8 bytes for slot + optionally 32 bytes for epoch nonce - let mut data = Vec::with_capacity(8 + 32); - data.extend_from_slice(&slot.to_be_bytes()); - if let Some(e_hash) = epoch_nonce.hash { - data.extend_from_slice(&e_hash); +use crate::ouroboros::overlay_shedule::OBftSlot; +use crate::ouroboros::vrf_validation::{ + TPraosBadLeaderVrfProofError, TPraosBadNonceVrfProofError, VrfValidation, VrfValidationError, + WrongGenesisLeaderVrfKeyError, +}; +use crate::ouroboros::{overlay_shedule, vrf}; +use crate::protocol_params::Nonce; +use crate::rational_number::RationalNumber; +use crate::{genesis_values::GenesisDelegs, protocol_params::PraosParams, BlockInfo}; +use anyhow::Result; +use pallas::ledger::primitives::VrfCert; +use pallas::ledger::traverse::MultiEraHeader; + +pub fn validate_vrf_tpraos<'a>( + block_info: &'a BlockInfo, + header: &'a MultiEraHeader, + praos_params: &'a PraosParams, + epoch_nonce: &'a Nonce, + decentralisation_param: RationalNumber, + genesis_delegs: &'a GenesisDelegs, +) -> Result>, VrfValidationError> { + let active_slots_coeff = praos_params.active_slots_coeff; + + // first look up for overlay slot + let obft_slot = overlay_shedule::lookup_in_overlay_schedule( + block_info.epoch_slot, + genesis_delegs, + decentralisation_param, + active_slots_coeff, + ) + .map_err(|e| VrfValidationError::InvalidShelleyParams(e.to_string()))?; + + match obft_slot { + None => { + // Regular Praos/TPraos rules apply + Ok(vec![]) + } + Some(OBftSlot::ActiveSlot(genesis_key, gen_deleg)) => { + // The given genesis key has authority to produce a block in this + // slot. Check whether we're its delegate. + let Some(vrf_vkey) = header.vrf_vkey() else { + return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); + }; + let declared_vrf_key: &[u8; vrf::PublicKey::HASH_SIZE] = vrf_vkey + .try_into() + .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; + let nonce_vrf_cert = + nonce_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingNonceVrfCert)?; + let leader_vrf_cert = + leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; + + Ok(vec![ + Box::new(move || { + WrongGenesisLeaderVrfKeyError::new(&genesis_key, &gen_deleg, vrf_vkey)?; + Ok(()) + }), + Box::new(move || { + TPraosBadNonceVrfProofError::new( + block_info.slot, + epoch_nonce, + &vrf::PublicKey::from(declared_vrf_key), + &nonce_vrf_cert.0.to_vec()[..], + &nonce_vrf_cert.1.to_vec()[..], + )?; + Ok(()) + }), + Box::new(move || { + TPraosBadLeaderVrfProofError::new( + block_info.slot, + epoch_nonce, + &vrf::PublicKey::from(declared_vrf_key), + &leader_vrf_cert.0.to_vec()[..], + &leader_vrf_cert.1.to_vec()[..], + )?; + Ok(()) + }), + ]) + } + Some(OBftSlot::NonActiveSlot) => { + // This is a non-active slot; nobody may produce a block + Ok(vec![]) + } } - let mut hasher = Blake2b::::new(); - hasher.update(data); - let seed_hash: [u8; 32] = hasher.finalize().into(); - - // XOR with universal constant if provided - let final_hash = match uc_nonce.hash.as_ref() { - Some(uc_hash) => xor_hash(&seed_hash, uc_hash), - None => seed_hash, - }; +} + +fn nonce_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { + match header { + MultiEraHeader::ShelleyCompatible(x) => Some(&x.header_body.nonce_vrf), + _ => None, + } +} - Seed::from(final_hash) +fn leader_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { + match header { + MultiEraHeader::ShelleyCompatible(x) => Some(&x.header_body.leader_vrf), + _ => None, + } } -fn xor_hash(hash1: &[u8; 32], hash2: &[u8; 32]) -> [u8; 32] { - let mut result = [0u8; 32]; - for i in 0..32 { - result[i] = hash1[i] ^ hash2[i]; +#[cfg(test)] +mod tests { + use crate::{ + genesis_values::GenesisValues, protocol_params::NonceHash, BlockHash, BlockStatus, Era, + }; + + use super::*; + + #[test] + fn test_4490511_block() { + let genesis_value = GenesisValues::mainnet(); + let praos_params = PraosParams::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let decentralisation_param = RationalNumber::from(1); + + let block_header_4490511: Vec = + hex::decode(include_str!("./data/4490511.cbor")).unwrap(); + let block_header = MultiEraHeader::decode(1, None, &block_header_4490511).unwrap(); + let block_info = BlockInfo { + status: BlockStatus::Immutable, + slot: 4492800, + hash: BlockHash::try_from( + hex::decode("aa83acbf5904c0edfe4d79b3689d3d00fcfc553cf360fd2229b98d464c28e9de") + .unwrap(), + ) + .unwrap(), + timestamp: 1596059091, + number: 4490511, + epoch: 208, + epoch_slot: 0, + new_epoch: true, + era: Era::Shelley, + }; + let vrf_validations = validate_vrf_tpraos( + &block_info, + &block_header, + &praos_params, + &epoch_nonce, + decentralisation_param, + &genesis_value.genesis_delegs, + ) + .unwrap(); + let result = vrf_validations.iter().try_for_each(|assert| assert()); + assert!(result.is_ok()); } - result } diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index de1549fb..e534a275 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -1,16 +1,9 @@ -use crate::ouroboros::{overlay_shedule, vrf}; +use crate::genesis_values::{GenDeleg, GenesisKey}; +use crate::ouroboros::vrf; use crate::PoolId; -use crate::{ - crypto::keyhash_256, - genesis_values::GenesisDelegs, - protocol_params::{Nonce, PraosParams, ShelleyParams}, - BlockInfo, KeyHash, Slot, -}; +use crate::{crypto::keyhash_256, protocol_params::Nonce, KeyHash, Slot}; use anyhow::Result; -use pallas::ledger::{ - primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}, - traverse::MultiEraHeader, -}; +use pallas::ledger::primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; use std::array::TryFromSliceError; use thiserror::Error; @@ -21,10 +14,23 @@ pub enum VrfValidationError { /// **Cause:** The Shelley protocol parameters used to validate the block, #[error("{0}")] InvalidShelleyParams(String), + /// **Cause:** The VRF key is missing from the block header + #[error("Missing VRF Key")] + MissingVrfVkey, + /// **Cause:** The VRF Cert is missing from the block header in TPraos Protocol + #[error("TPraos Missing Nonce VRF Cert")] + TPraosMissingNonceVrfCert, + /// **Cause:** The Leader VRF Cert is missing from the block header in TPraos Protocol + #[error("TPraos Missing Leader VRF Cert")] + TPraosMissingLeaderVrfCert, /// **Cause:** Block issuer's pool ID is not registered in current stake distribution #[error("Unknown Pool: {}", hex::encode(&pool_id))] UnknownPool { pool_id: PoolId }, /// **Cause:** The VRF key hash in the block header doesn't match the VRF key + /// registered with this stake pool in the ledger state for Overlay slot + #[error("{0}")] + WrongGenesisLeaderVrfKey(#[from] WrongGenesisLeaderVrfKeyError), + /// **Cause:** The VRF key hash in the block header doesn't match the VRF key /// registered with this stake pool in the ledger state #[error("{0}")] WrongLeaderVrfKey(#[from] WrongLeaderVrfKeyError), @@ -48,6 +54,43 @@ pub enum VrfValidationError { /// The pool lost the slot lottery in Praos Protocol #[error("Praos VRF Leader Value Too Big")] PraosVrfLeaderValueTooBig, + /// **Cause:** Some data has incorrect bytes + #[error("TryFromSlice: {0}")] + TryFromSlice(String), +} + +// ------------------------------------------------------------ WrongGenesisLeaderVrfKeyError + +#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[error( + "Wrong Genesis Leader VRF Key: Genesis Key={}, Registered VRF Hash={}, Header VRF Hash={}", + hex::encode(&genesis_key), + hex::encode(®istered_vrf_hash), + hex::encode(&header_vrf_hash), +)] +pub struct WrongGenesisLeaderVrfKeyError { + genesis_key: GenesisKey, + registered_vrf_hash: KeyHash, + header_vrf_hash: KeyHash, +} + +impl WrongGenesisLeaderVrfKeyError { + pub fn new( + genesis_key: &GenesisKey, + genesis_deleg: &GenDeleg, + vrf_vkey: &[u8], + ) -> Result<(), Self> { + let header_vrf_hash = keyhash_256(vrf_vkey); + let registered_vrf_hash = genesis_deleg.vrf.to_vec(); + if !registered_vrf_hash.eq(&header_vrf_hash) { + return Err(Self { + genesis_key: genesis_key.clone(), + registered_vrf_hash, + header_vrf_hash, + }); + } + Ok(()) + } } // ------------------------------------------------------------ WrongLeaderVrfKeyError @@ -76,7 +119,7 @@ impl WrongLeaderVrfKeyError { return Err(Self { pool_id: pool_id.clone(), registered_vrf_hash: registered_vrf_hash.clone(), - header_vrf_hash: header_vrf_hash, + header_vrf_hash, }); } Ok(()) @@ -326,24 +369,4 @@ impl BadVrfProofError { } } -pub fn validate_vrf( - block_info: &BlockInfo, - header: &MultiEraHeader, - shelley_params: &ShelleyParams, - praos_params: &PraosParams, - genesis_delegs: &GenesisDelegs, -) -> Result<(), VrfValidationError> { - let decentralisation_param = shelley_params.protocol_params.decentralisation_param; - let active_slots_coeff = praos_params.active_slots_coeff; - - // first look up for overlay slot - let obft_slot = overlay_shedule::lookup_in_overlay_schedule( - block_info.epoch_slot, - genesis_delegs, - decentralisation_param, - active_slots_coeff, - ) - .map_err(|e| VrfValidationError::InvalidShelleyParams(e.to_string()))?; - - Ok(()) -} +pub type VrfValidation<'a> = Box Result<(), VrfValidationError> + Send + Sync + 'a>; diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 88217952..c1cecdf8 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -17,8 +17,6 @@ use state::State; const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = ("validation-vrf-publisher-topic", "cardano.validation.vrf"); -const DEFAULT_VALIDATION_KES_PUBLISHER_TOPIC: (&str, &str) = - ("validation-kes-publisher-topic", "cardano.validation.kes"); const DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC: (&str, &str) = ( "bootstrapped-subscribe-topic", diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 430cf627..c8494586 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -3,7 +3,7 @@ use acropolis_common::{ genesis_values::GenesisValues, messages::ProtocolParamsMessage, - ouroboros::vrf_validation::{self, VrfValidationError}, + ouroboros::vrf_validation::VrfValidationError, protocol_params::{PraosParams, ShelleyParams}, BlockInfo, }; @@ -47,24 +47,16 @@ impl State { } let Some(shelley_params) = self.shelly_params.as_ref() else { - return Err(VrfValidationError::ShelleyParams( + return Err(VrfValidationError::InvalidShelleyParams( "Shelley Params are not set".to_string(), )); }; let Some(praos_params) = self.praos_params.as_ref() else { - return Err(VrfValidationError::ShelleyParams( + return Err(VrfValidationError::InvalidShelleyParams( "Praos Params are not set".to_string(), )); }; - vrf_validation::validate_vrf( - block_info, - header, - shelley_params, - praos_params, - &genesis.genesis_delegs, - )?; - Ok(()) } } diff --git a/modules/epochs_state/src/state.rs b/modules/epochs_state/src/state.rs index 6ec61a61..24e7b34c 100644 --- a/modules/epochs_state/src/state.rs +++ b/modules/epochs_state/src/state.rs @@ -269,7 +269,7 @@ mod tests { use super::*; use acropolis_common::{ - crypto::{keyhash_224, keyhash_256}, + crypto::keyhash_224, protocol_params::{Nonce, NonceHash}, state_history::{StateHistory, StateHistoryStore}, BlockHash, BlockInfo, BlockStatus, Era, @@ -522,21 +522,6 @@ mod tests { let block_header = MultiEraHeader::decode(1, None, &e_208_first_block_header_cbor).unwrap(); assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); - println!("slot: {:?}", block_header.slot()); - println!( - "issuer vkey: {:?}", - hex::encode(keyhash_224(block_header.issuer_vkey().unwrap())) - ); - println!( - "vrf vkey: {:?}", - hex::encode(keyhash_256(block_header.vrf_vkey().unwrap())) - ); - - println!( - "leader vrf output: {:?}", - block_header.leader_vrf_output().unwrap().len() - ); - let nonces = state.nonces.unwrap(); let evolved = Nonce::from( NonceHash::try_from( @@ -702,22 +687,6 @@ mod tests { let block = make_new_epoch_block(209); let block_header = MultiEraHeader::decode(1, None, &e_210_first_block_header_cbor).unwrap(); assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); - println!( - "block header leader vrf output: {:?}", - block_header.leader_vrf_output().unwrap() - ); - println!( - "block header leader vrf output length: {:?}", - block_header.leader_vrf_output().unwrap().len() - ); - println!( - "block header nonce vrf output: {:?}", - block_header.nonce_vrf_output().unwrap() - ); - println!( - "block header nonce vrf output length: {:?}", - block_header.nonce_vrf_output().unwrap().len() - ); let nonces = state.nonces.unwrap(); let evolved = Nonce::from( From c2880e5a1bf03dde2c4d4c5e33d9f21420f2f10f Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 24 Oct 2025 16:27:04 +0200 Subject: [PATCH 08/36] feat: add stake threshold check --- Cargo.lock | 55 ++++++++++++++++ Cargo.toml | 1 + common/Cargo.toml | 2 + common/src/ouroboros/vrf_validation.rs | 73 +++++++++++++++++++-- modules/epochs_state/src/epochs_state.rs | 15 ++++- modules/upstream_chain_fetcher/src/utils.rs | 1 - processes/omnibus/omnibus.toml | 2 +- processes/omnibus/src/main.rs | 2 +- 8 files changed, 140 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a06e3f49..6e0dd4fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,6 +35,7 @@ dependencies = [ "crc", "cryptoxide 0.5.1", "dashmap", + "dashu-int", "fraction", "futures", "gcd", @@ -46,6 +47,7 @@ dependencies = [ "num-rational", "num-traits", "pallas 0.33.0", + "pallas-math", "rayon", "serde", "serde_json", @@ -2038,6 +2040,26 @@ dependencies = [ "parking_lot_core 0.9.12", ] +[[package]] +name = "dashu-base" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b80bf6b85aa68c58ffea2ddb040109943049ce3fbdf4385d0380aef08ef289" + +[[package]] +name = "dashu-int" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee99d08031ca34a4d044efbbb21dff9b8c54bb9d8c82a189187c0651ffdb9fbf" +dependencies = [ + "cfg-if", + "dashu-base", + "num-modular", + "num-order", + "rustversion", + "static_assertions", +] + [[package]] name = "data-encoding" version = "2.9.0" @@ -3822,6 +3844,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-rational" version = "0.4.2" @@ -4246,6 +4283,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "pallas-math" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "430e208033850256e555b9916a6e397a976287007175f03e3afc1f749e2f9d86" +dependencies = [ + "dashu-base", + "dashu-int", + "regex", + "thiserror 1.0.69", +] + [[package]] name = "pallas-network" version = "0.32.1" @@ -5983,6 +6032,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "std-semaphore" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 9efddb72..4f62a8dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ dashmap = "6.1.0" hex = "0.4" imbl = { version = "5.0.0", features = ["serde"] } pallas = "0.33.0" +pallas-math = "0.33.0" pallas-addresses = "0.33.0" pallas-crypto = "0.33.0" pallas-primitives = "0.33.0" diff --git a/common/Cargo.toml b/common/Cargo.toml index 8056f95a..7ecca776 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -47,9 +47,11 @@ sha2 = "0.10.8" caryatid_process.workspace = true config.workspace = true pallas = { workspace = true } +pallas-math = { workspace = true } # The vrf crate has not been fully tested in production environments and still has several upstream issues that are open PRs but not merged yet. vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } +dashu-int = "0.4.1" [lib] crate-type = ["rlib"] diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index e534a275..689a51fa 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -1,9 +1,12 @@ use crate::genesis_values::{GenDeleg, GenesisKey}; use crate::ouroboros::vrf; +use crate::rational_number::RationalNumber; use crate::PoolId; use crate::{crypto::keyhash_256, protocol_params::Nonce, KeyHash, Slot}; use anyhow::Result; +use dashu_int::UBig; use pallas::ledger::primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; +use pallas_math::math::{ExpOrdering, FixedDecimal, FixedPrecision}; use std::array::TryFromSliceError; use thiserror::Error; @@ -47,13 +50,9 @@ pub enum VrfValidationError { #[error("{0}")] PraosBadVrfProof(#[from] PraosBadVrfProofError), /// **Cause:** The VRF output is too large for this pool's stake. - /// The pool lost the slot lottery in TPraos Protocol - #[error("TPraos VRF Leader Value Too Big")] - TPraosVrfLeaderValueTooBig, - /// **Cause:** The VRF output is too large for this pool's stake. - /// The pool lost the slot lottery in Praos Protocol - #[error("Praos VRF Leader Value Too Big")] - PraosVrfLeaderValueTooBig, + /// The pool lost the slot lottery + #[error("VRF Leader Value Too Big")] + VrfLeaderValueTooBig(#[from] VrfLeaderValueTooBigError), /// **Cause:** Some data has incorrect bytes #[error("TryFromSlice: {0}")] TryFromSlice(String), @@ -280,6 +279,66 @@ impl PraosBadVrfProofError { } } +// ------------------------------------------------------------ TPraosVrfLeaderValueTooBigError + +/// Reference +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L430 +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L527 +/// +/// Check that the certified input natural is valid for being slot leader. This means we check that +/// p < 1 - (1 - f)^σ +/// where p = certNat / certNatMax. (certNat is 64bytes for TPraos and 32bytes for Praos) + +/// let q = 1 - p and c = ln(1 - f) +/// then p < 1 - (1 - f)^σ => 1 / (1 - p) < exp(-σ * c) => 1 / q < exp(-σ * c) +/// Reference +/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L331 +/// +#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +pub enum VrfLeaderValueTooBigError { + #[error("VRF Leader Value Too Big")] + VrfLeaderValueTooBig, +} + +impl VrfLeaderValueTooBigError { + pub fn new( + leader_vrf_output: &[u8], + leader_relative_stake: RationalNumber, + active_slot_coeff: RationalNumber, + ) -> Result<(), Self> { + let certified_leader_vrf = &FixedDecimal::from(leader_vrf_output); + let output_size_bits = leader_vrf_output.len() * 8; + let cert_nat_max = FixedDecimal::from(UBig::ONE << output_size_bits); + let leader_relative_stake = FixedDecimal::from(UBig::from(*leader_relative_stake.numer())) + / FixedDecimal::from(UBig::from(*leader_relative_stake.denom())); + let active_slot_coeff = FixedDecimal::from(UBig::from(*active_slot_coeff.numer())) + / FixedDecimal::from(UBig::from(*active_slot_coeff.denom())); + + let denominator = &cert_nat_max - certified_leader_vrf; + let recip_q = &cert_nat_max / &denominator; + let c = (&FixedDecimal::from(1u64) - &active_slot_coeff).ln(); + let x = -(leader_relative_stake * c); + let ordering = x.exp_cmp(1000, 3, &recip_q); + match ordering.estimation { + ExpOrdering::LT => Ok(()), + ExpOrdering::GT | ExpOrdering::UNKNOWN => Err(Self::VrfLeaderValueTooBig), + } + } +} + +/// Check that the certified input natural is valid for being slot leader. This means we check that +/// p < 1 - (1 - f)^σ +/// where p = certNat / certNatMax. (certNat is 64bytes for TPraos and 32bytes for Praos) + +/// let q = 1 - p and c = ln(1 - f) +/// then p < 1 - (1 - f)^σ => 1 / (1 - p) < exp(-σ * c) => 1 / q < exp(-σ * c) +/// Reference +/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L331 +/// +/// NOTE: +/// We are using Pallas Math Library +/// + // ------------------------------------------------------------ BadVrfProofError #[derive(Error, Debug, serde::Serialize, serde::Deserialize)] diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index 28bc8826..04498d10 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -14,7 +14,7 @@ use anyhow::Result; use caryatid_sdk::{message_bus::Subscription, module, Context, Module}; use config::Config; use pallas::ledger::traverse::MultiEraHeader; -use std::sync::Arc; +use std::{io::Write, sync::Arc}; use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; @@ -48,6 +48,13 @@ const DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC: (&str, &str) = const DEFAULT_EPOCH_NONCES_PUBLISH_TOPIC: (&str, &str) = ("epoch-nonces-publish-topic", "cardano.epoch.nonces"); +// store block header cbor +fn write_header_cbor(header: &MultiEraHeader) -> Result<()> { + let mut file = std::fs::File::create("4576496.cbor")?; + file.write_all(hex::encode(header.cbor()).as_bytes())?; + Ok(()) +} + /// Epochs State module #[module( message_type(Message), @@ -133,6 +140,12 @@ impl EpochsState { }; }); + if let Some(header) = header.as_ref() { + if block_info.number == 4576496 { + write_header_cbor(header); + } + } + if is_new_epoch { let ea = state.end_epoch(block_info); // update epochs history diff --git a/modules/upstream_chain_fetcher/src/utils.rs b/modules/upstream_chain_fetcher/src/utils.rs index a6e6f266..2a0f2563 100644 --- a/modules/upstream_chain_fetcher/src/utils.rs +++ b/modules/upstream_chain_fetcher/src/utils.rs @@ -7,7 +7,6 @@ use config::Config; use pallas::network::facades; use pallas::network::facades::PeerClient; use serde::Deserialize; -use std::collections::BTreeMap; use std::sync::Arc; use tracing::{error, info}; diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index 20bccd9a..cd4f888c 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -8,7 +8,7 @@ genesis-key = "5b3139312c36362c3134302c3138352c3133382c31312c3233372c3230372c323 # Download max age in hours. E.g. 8 means 8 hours (if there isn't any snapshot within this time range download from Mithril) download-max-age = "never" # Pause constraint E.g. "epoch:100", "block:1200" -pause = "none" +pause = "epoch:212" [module.upstream-chain-fetcher] sync-point = "snapshot" diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index 632a4f59..7bf9c8c3 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -112,7 +112,7 @@ pub async fn main() -> Result<()> { ParametersState::register(&mut process); StakeDeltaFilter::register(&mut process); EpochsState::register(&mut process); - BlockVrfValidator::register(&mut process); + // BlockVrfValidator::register(&mut process); AccountsState::register(&mut process); AddressState::register(&mut process); AssetsState::register(&mut process); From 6c0124daff8ba1f574a4cab495663d94dd262c6c Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 24 Oct 2025 16:39:28 +0200 Subject: [PATCH 09/36] revert: omnibus --- processes/omnibus/omnibus.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index cd4f888c..20bccd9a 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -8,7 +8,7 @@ genesis-key = "5b3139312c36362c3134302c3138352c3133382c31312c3233372c3230372c323 # Download max age in hours. E.g. 8 means 8 hours (if there isn't any snapshot within this time range download from Mithril) download-max-age = "never" # Pause constraint E.g. "epoch:100", "block:1200" -pause = "epoch:212" +pause = "none" [module.upstream-chain-fetcher] sync-point = "snapshot" From 9bec09a819712124e196d5acd9686de851ccf399 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 24 Oct 2025 16:58:22 +0200 Subject: [PATCH 10/36] refactor: read epoch nonce in block vrf validator and update types for validation error --- common/src/ouroboros/vrf.rs | 10 +++---- common/src/ouroboros/vrf_validation.rs | 16 +++++----- common/src/validation.rs | 6 ++-- .../src/block_vrf_validator.rs | 30 ++++++++++++------- modules/block_vrf_validator/src/state.rs | 13 ++++++-- processes/omnibus/src/main.rs | 2 +- 6 files changed, 49 insertions(+), 28 deletions(-) diff --git a/common/src/ouroboros/vrf.rs b/common/src/ouroboros/vrf.rs index d67c15d3..944ed256 100644 --- a/common/src/ouroboros/vrf.rs +++ b/common/src/ouroboros/vrf.rs @@ -106,9 +106,9 @@ impl VrfInput { /// /// # Returns /// 32-byte input for VRF function - /// + /// /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos/VRF.hs#L67 - /// + /// pub fn mk_vrf_input(absolute_slot_number: u64, epoch_nonce: &Nonce) -> Self { let mut hasher = Blake2b::::new(); let mut data = Vec::::with_capacity(8 + 32); @@ -173,7 +173,7 @@ impl Proof { } } -#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum ProofFromBytesError { #[error("Decompression from Edwards point failed.")] DecompressionFailed, @@ -208,7 +208,7 @@ impl From<&Proof> for [u8; Proof::HASH_SIZE] { /// error that can be returned if the verification of a [`VrfProof`] fails /// see [`VrfProof::verify`] -#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[error("VRF proof verification failed: {:?}", .0)] pub struct ProofVerifyError( #[from] @@ -218,7 +218,7 @@ pub struct ProofVerifyError( ); mod serde_remote { - #[derive(serde::Serialize, serde::Deserialize)] + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[serde(remote = "super::VrfError")] pub enum VrfError { VerificationFailed, diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index 689a51fa..4f2f84d0 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -12,7 +12,7 @@ use thiserror::Error; /// Reference /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 -#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum VrfValidationError { /// **Cause:** The Shelley protocol parameters used to validate the block, #[error("{0}")] @@ -60,7 +60,7 @@ pub enum VrfValidationError { // ------------------------------------------------------------ WrongGenesisLeaderVrfKeyError -#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[error( "Wrong Genesis Leader VRF Key: Genesis Key={}, Registered VRF Hash={}, Header VRF Hash={}", hex::encode(&genesis_key), @@ -94,7 +94,7 @@ impl WrongGenesisLeaderVrfKeyError { // ------------------------------------------------------------ WrongLeaderVrfKeyError -#[derive(Error, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[error( "Wrong Leader VRF Key: Pool ID={}, Registered VRF Hash={}, Header VRF Hash={}", hex::encode(&pool_id), @@ -127,7 +127,7 @@ impl WrongLeaderVrfKeyError { // ------------------------------------------------------------ TPraosBadNonceVrfProofError -#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum TPraosBadNonceVrfProofError { #[error("Bad Nonce VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] BadVrfProof(Slot, Nonce, BadVrfProofError), @@ -165,7 +165,7 @@ impl TPraosBadNonceVrfProofError { // ------------------------------------------------------------ TPraosBadLeaderVrfProofError -#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum TPraosBadLeaderVrfProofError { #[error("Bad Leader VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] BadVrfProof(Slot, Nonce, BadVrfProofError), @@ -203,7 +203,7 @@ impl TPraosBadLeaderVrfProofError { // ------------------------------------------------------------ PraosBadVrfProofError -#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum PraosBadVrfProofError { #[error("Bad VRF proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] BadVrfProof(Slot, Nonce, BadVrfProofError), @@ -294,7 +294,7 @@ impl PraosBadVrfProofError { /// Reference /// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L331 /// -#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum VrfLeaderValueTooBigError { #[error("VRF Leader Value Too Big")] VrfLeaderValueTooBig, @@ -341,7 +341,7 @@ impl VrfLeaderValueTooBigError { // ------------------------------------------------------------ BadVrfProofError -#[derive(Error, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] pub enum BadVrfProofError { #[error("Malformed VRF proof: {0}")] MalformedProof(#[from] vrf::ProofFromBytesError), diff --git a/common/src/validation.rs b/common/src/validation.rs index 1501af67..01e7b881 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -5,11 +5,13 @@ use thiserror::Error; +use crate::ouroboros::vrf_validation::VrfValidationError; + /// Validation error #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Error)] pub enum ValidationError { - #[error("VRF failure")] - BadVRF, + #[error("VRF failure: {0}")] + BadVRF(#[from] VrfValidationError), #[error("KES failure")] BadKES, diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index c1cecdf8..7c2f8419 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -43,8 +43,8 @@ impl BlockVrfValidator { async fn run( history: Arc>>, mut bootstrapped_subscription: Box>, - mut protocol_parameters_subscription: Box>, mut blocks_subscription: Box>, + mut protocol_parameters_subscription: Box>, mut epoch_nonces_subscription: Box>, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; @@ -61,9 +61,8 @@ impl BlockVrfValidator { loop { // Get a mutable state let mut state = history.lock().await.get_or_init_with(|| State::new()); - - // Read both topics in parallel let (_, message) = blocks_subscription.read().await?; + match message.as_ref() { Message::Cardano((block_info, CardanoMessage::BlockAvailable(block_msg))) => { // handle rollback here @@ -74,13 +73,24 @@ impl BlockVrfValidator { // read protocol parameters if new epoch if is_new_epoch { - let (_, protocol_parameters_msg) = - protocol_parameters_subscription.read().await?; - if let Message::Cardano((_, CardanoMessage::ProtocolParams(params))) = - protocol_parameters_msg.as_ref() - { - state.handle_protocol_parameters(params); + let protocol_parameters_message_f = protocol_parameters_subscription.read(); + let epoch_nonces_message_f = epoch_nonces_subscription.read(); + let (_, protocol_parameters_msg) = protocol_parameters_message_f.await?; + let (_, epoch_nonces_msg) = epoch_nonces_message_f.await?; + + match protocol_parameters_msg.as_ref() { + Message::Cardano((_, CardanoMessage::ProtocolParams(msg))) => { + state.handle_protocol_parameters(msg); + } + _ => error!("Unexpected message type: {protocol_parameters_msg:?}"), } + + match epoch_nonces_msg.as_ref() { + Message::Cardano((_, CardanoMessage::EpochNonces(msg))) => { + state.handle_epoch_nonces(msg); + } + _ => error!("Unexpected message type: {epoch_nonces_msg:?}"), + }; } // decode header @@ -167,8 +177,8 @@ impl BlockVrfValidator { Self::run( history, bootstrapped_subscription, - protocol_parameters_subscription, blocks_subscription, + protocol_parameters_subscription, epoch_nonces_subscription, ) .await diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index c8494586..9a7ba6ff 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -2,9 +2,9 @@ use acropolis_common::{ genesis_values::GenesisValues, - messages::ProtocolParamsMessage, + messages::{EpochNoncesMessage, ProtocolParamsMessage}, ouroboros::vrf_validation::VrfValidationError, - protocol_params::{PraosParams, ShelleyParams}, + protocol_params::{Nonces, PraosParams, ShelleyParams}, BlockInfo, }; use anyhow::Result; @@ -17,6 +17,9 @@ pub struct State { // protocol parameter for Praos and TPraos pub praos_params: Option, + + // epoch nonces + pub epoch_nonces: Option, } impl State { @@ -24,6 +27,7 @@ impl State { Self { praos_params: None, shelly_params: None, + epoch_nonces: None, } } @@ -35,6 +39,11 @@ impl State { } } + /// Handle epoch nonces updates + pub fn handle_epoch_nonces(&mut self, msg: &EpochNoncesMessage) { + self.epoch_nonces = Some(msg.nonces.clone()); + } + pub fn validate_block_vrf( &self, block_info: &BlockInfo, diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index 7bf9c8c3..632a4f59 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -112,7 +112,7 @@ pub async fn main() -> Result<()> { ParametersState::register(&mut process); StakeDeltaFilter::register(&mut process); EpochsState::register(&mut process); - // BlockVrfValidator::register(&mut process); + BlockVrfValidator::register(&mut process); AccountsState::register(&mut process); AddressState::register(&mut process); AssetsState::register(&mut process); From 46b3501ae630b64258209e888713b399877edd72 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 24 Oct 2025 17:56:47 +0200 Subject: [PATCH 11/36] refactor: add validation for non obft slots --- common/src/ouroboros/tpraos.rs | 45 ++++++++++++++++++++++-- common/src/ouroboros/vrf_validation.rs | 3 ++ modules/epochs_state/src/epochs_state.rs | 13 ------- 3 files changed, 45 insertions(+), 16 deletions(-) diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index 4b0b93ac..9866ab56 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -1,11 +1,13 @@ +use crate::crypto::keyhash_224; use crate::ouroboros::overlay_shedule::OBftSlot; use crate::ouroboros::vrf_validation::{ TPraosBadLeaderVrfProofError, TPraosBadNonceVrfProofError, VrfValidation, VrfValidationError, - WrongGenesisLeaderVrfKeyError, + WrongGenesisLeaderVrfKeyError, WrongLeaderVrfKeyError, }; use crate::ouroboros::{overlay_shedule, vrf}; use crate::protocol_params::Nonce; use crate::rational_number::RationalNumber; +use crate::PoolId; use crate::{genesis_values::GenesisDelegs, protocol_params::PraosParams, BlockInfo}; use anyhow::Result; use pallas::ledger::primitives::VrfCert; @@ -32,8 +34,45 @@ pub fn validate_vrf_tpraos<'a>( match obft_slot { None => { - // Regular Praos/TPraos rules apply - Ok(vec![]) + let Some(issuer_vkey) = header.issuer_vkey() else { + return Ok(vec![Box::new(|| Err(VrfValidationError::MissingIssuerKey))]); + }; + let pool_id: PoolId = keyhash_224(issuer_vkey); + + let Some(vrf_vkey) = header.vrf_vkey() else { + return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); + }; + let declared_vrf_key: &[u8; vrf::PublicKey::HASH_SIZE] = vrf_vkey + .try_into() + .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; + let nonce_vrf_cert = + nonce_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingNonceVrfCert)?; + let leader_vrf_cert = + leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; + + // Regular TPraos rules apply + Ok(vec![ + Box::new(move || { + TPraosBadNonceVrfProofError::new( + block_info.slot, + epoch_nonce, + &vrf::PublicKey::from(declared_vrf_key), + &nonce_vrf_cert.0.to_vec()[..], + &nonce_vrf_cert.1.to_vec()[..], + )?; + Ok(()) + }), + Box::new(move || { + TPraosBadLeaderVrfProofError::new( + block_info.slot, + epoch_nonce, + &vrf::PublicKey::from(declared_vrf_key), + &leader_vrf_cert.0.to_vec()[..], + &leader_vrf_cert.1.to_vec()[..], + )?; + Ok(()) + }), + ]) } Some(OBftSlot::ActiveSlot(genesis_key, gen_deleg)) => { // The given genesis key has authority to produce a block in this diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index 4f2f84d0..0fee2b0b 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -17,6 +17,9 @@ pub enum VrfValidationError { /// **Cause:** The Shelley protocol parameters used to validate the block, #[error("{0}")] InvalidShelleyParams(String), + /// **Cause:** The Issuer Key is missing from the block header + #[error("Missing Issuer Key")] + MissingIssuerKey, /// **Cause:** The VRF key is missing from the block header #[error("Missing VRF Key")] MissingVrfVkey, diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index 04498d10..43264af4 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -48,13 +48,6 @@ const DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC: (&str, &str) = const DEFAULT_EPOCH_NONCES_PUBLISH_TOPIC: (&str, &str) = ("epoch-nonces-publish-topic", "cardano.epoch.nonces"); -// store block header cbor -fn write_header_cbor(header: &MultiEraHeader) -> Result<()> { - let mut file = std::fs::File::create("4576496.cbor")?; - file.write_all(hex::encode(header.cbor()).as_bytes())?; - Ok(()) -} - /// Epochs State module #[module( message_type(Message), @@ -140,12 +133,6 @@ impl EpochsState { }; }); - if let Some(header) = header.as_ref() { - if block_info.number == 4576496 { - write_header_cbor(header); - } - } - if is_new_epoch { let ea = state.end_epoch(block_info); // update epochs history From 2475252700cd553cab89254e21e156cb70e62ddd Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Tue, 28 Oct 2025 14:05:43 +0100 Subject: [PATCH 12/36] chore: add spo state subscription --- .../block_vrf_validator/src/block_vrf_validator.rs | 11 +++++++++++ modules/epochs_state/src/epochs_state.rs | 2 +- .../src/mithril_snapshot_fetcher.rs | 4 ++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 7c2f8419..6163c820 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -30,6 +30,9 @@ const DEFAULT_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = ("blocks-subscribe-topic", "cardano.block.proposed"); const DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC: (&str, &str) = ("epoch-nonces-subscribe-topic", "cardano.epoch.nonces"); +const DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC: (&str, &str) = + ("spo-state-subscribe-topic", "cardano.spo.state"); + /// Block VRF Validator module #[module( message_type(Message), @@ -46,6 +49,7 @@ impl BlockVrfValidator { mut blocks_subscription: Box>, mut protocol_parameters_subscription: Box>, mut epoch_nonces_subscription: Box>, + mut spo_state_subscription: Box>, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; let genesis = match bootstrapped_message.as_ref() { @@ -159,12 +163,18 @@ impl BlockVrfValidator { .unwrap_or(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating epoch nonces subscription on '{epoch_nonces_subscribe_topic}'"); + let spo_state_subscribe_topic = config + .get_string(DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating spo state subscription on '{spo_state_subscribe_topic}'"); + // Subscribers let bootstrapped_subscription = context.subscribe(&bootstrapped_subscribe_topic).await?; let protocol_parameters_subscription = context.subscribe(&protocol_parameters_subscribe_topic).await?; let blocks_subscription = context.subscribe(&blocks_subscribe_topic).await?; let epoch_nonces_subscription = context.subscribe(&epoch_nonces_subscribe_topic).await?; + let spo_state_subscription = context.subscribe(&spo_state_subscribe_topic).await?; // state history let history = Arc::new(Mutex::new(StateHistory::::new( @@ -180,6 +190,7 @@ impl BlockVrfValidator { blocks_subscription, protocol_parameters_subscription, epoch_nonces_subscription, + spo_state_subscription, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index 43264af4..28bc8826 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -14,7 +14,7 @@ use anyhow::Result; use caryatid_sdk::{message_bus::Subscription, module, Context, Module}; use config::Config; use pallas::ledger::traverse::MultiEraHeader; -use std::{io::Write, sync::Arc}; +use std::sync::Arc; use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; diff --git a/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs b/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs index 84c10703..5ee24faf 100644 --- a/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs +++ b/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs @@ -357,6 +357,10 @@ impl MithrilSnapshotFetcher { CardanoMessage::BlockAvailable(message), )); + if block_info.epoch >= 10 { + info!("Publishing block available: for epoch {}", block_info.epoch); + } + context .message_bus .publish(&block_topic, Arc::new(message_enum)) From 3cdc3c4a24e76a120d7b79a68ce899b28917b4e9 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 29 Oct 2025 14:10:22 +0100 Subject: [PATCH 13/36] feat: implement snapshot state for vrf validation --- common/src/ouroboros/data/4576496.cbor | 1 + common/src/ouroboros/mod.rs | 1 + common/src/ouroboros/overlay_shedule.rs | 18 +-- common/src/ouroboros/praos.rs | 100 +++++++++++++++ common/src/ouroboros/tpraos.rs | 118 ++++++++++++++++-- common/src/ouroboros/vrf_validation.rs | 9 +- .../src/block_vrf_validator.rs | 64 ++++++++-- modules/block_vrf_validator/src/snapshot.rs | 41 ++++++ modules/block_vrf_validator/src/state.rs | 87 +++++++++++-- .../src/mithril_snapshot_fetcher.rs | 4 - processes/omnibus/.gitignore | 2 + 11 files changed, 402 insertions(+), 43 deletions(-) create mode 100644 common/src/ouroboros/data/4576496.cbor create mode 100644 common/src/ouroboros/praos.rs create mode 100644 modules/block_vrf_validator/src/snapshot.rs diff --git a/common/src/ouroboros/data/4576496.cbor b/common/src/ouroboros/data/4576496.cbor new file mode 100644 index 00000000..c58d4f5f --- /dev/null +++ b/common/src/ouroboros/data/4576496.cbor @@ -0,0 +1 @@ +828f1a0045d4f01a005eebcd5820d0547673599cc31048011657b1ac2ae14ff15b2103702b4071fd0c0e1caaeecf5820330b133b489d79d039c7feceefe5acb9149f636d5d84943788ce8783c1c098245820e9dffe03f31be0431b2199696250f94ec599916ff5618080b428ac7c5ea5f610825840ce902041f973eb263968e1de7f6255de7a9ddcafa343c2d2ed89c4daaca121d87621f683634ef07ff8234f69e381bd593e6229e92b2416fb463e5b50f1df675a5850b6568f768f3e4ca83dee729f934b83438fd693945f6c08110c1d9963024c904c1f836b51792a839aaedeb9381f65273abee234ea9e3205810e08ec03413244e2345aa58992d60e30aa38c3191f11db03825840000f783edf754904b325197136c4cdf5badf6a32ee7b86758d005aed320eb298efcd137da7507a51a7c580211017b14b08267c947b179358ebd5ee77d6b473b4585051632cef6eb7a950d94086d8f8770028d32ff55dc39741f7d45e061fd558383d5e18eb15aa6192a9c0bd16a5dbfcbee0dc041d158a2c50e67e80d3fc4a78dab889a5b703ab815afece18d00ed0431f0019018c582040fd0376795eec5777148337d4a5d37b72c358c30da034de8bac5e23c4525407582029429acc1d7d18b05039b379a7fe94dc95bd86631433ecb41b523e7d04a974c801015840c2b36ecd955d90a8adac85b4aa8a3dbdf022108794a255763c84c460db3b6eca45fb26dd5bc5cce9812f3199527e2c249473de89bd459888f12c964f58b1fc0c01005901c0c28b49147a742ee1a357f459315234446df337e2933df741307268fc064c32c0d465e2785f29ac9a79bfe55db85be4f3e36bfc2b7e15f76db91c46cbc2d27601b08c6020c1aee33d51ab7a66f9432d61d1dbb5bc3a7f6b183d414b94ffacbb459600a4ef2b27ece3c5682b50d1316dff928d7e627cb4f6754321e6465acb16cd360c41571150cd17dafdb642a27bbd5e80ce24a30ac2126a5c27fd866f7e43ab6c446de810e15924cf0b05873c6f4e49e518c139333d6d47c1ae709771ac5abf95270c741298d567251066675609c71743a4cf61b525eabbea0f27d14e53a558210dbc7397354abda4d323b5b82460b6c8a1cb654f62af1962bfd3ec1a818e016be9e84c4ea1007a2edb5d392ea5cd84e4d92f2dd92fb1d80502f18df873a29915616650b4a697289bfec648eb72f0867bb1c6bd9c85fc25215078139f770a30e79b0f764bbef90a3933a82bb4f190e418f361c0426da2521c47c9330e96c876d8ec496117bf9a3cdb89420018e33e2977077fff74120fe8385ecd7729aee75742b31c09d4112ac7e777aced184056e8de92c3519e26ffdc98d85e3bd46725488bcad48a5da411985577277605a9c65a82f5224c179ac2da52be2e497b37de6b \ No newline at end of file diff --git a/common/src/ouroboros/mod.rs b/common/src/ouroboros/mod.rs index e06b3e0c..75d1d8e8 100644 --- a/common/src/ouroboros/mod.rs +++ b/common/src/ouroboros/mod.rs @@ -1,4 +1,5 @@ pub mod overlay_shedule; +pub mod praos; pub mod tpraos; pub mod vrf; pub mod vrf_validation; diff --git a/common/src/ouroboros/overlay_shedule.rs b/common/src/ouroboros/overlay_shedule.rs index 17035d59..5c5b47b8 100644 --- a/common/src/ouroboros/overlay_shedule.rs +++ b/common/src/ouroboros/overlay_shedule.rs @@ -33,7 +33,7 @@ pub enum OBftSlot { /// If the slot is an overlay slot, then we skip StakeThreshold validation /// since this block is produced by genesis key (without "lottery") /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L334 -pub fn is_overlay_slot(epoch_slot: u64, decentralisation_param: RationalNumber) -> Result { +pub fn is_overlay_slot(epoch_slot: u64, decentralisation_param: &RationalNumber) -> Result { let d = decentralisation_param.to_checked_f64("decentralisation_param").map_err(|e| { anyhow::anyhow!("Failed to convert decentralisation parameter to f64: {}", e) })?; @@ -58,8 +58,8 @@ pub fn is_overlay_slot(epoch_slot: u64, decentralisation_param: RationalNumber) pub fn classify_overlay_slot( epoch_slot: u64, genesis_delegs: &GenesisDelegs, - decentralisation_param: RationalNumber, - active_slots_coeff: RationalNumber, + decentralisation_param: &RationalNumber, + active_slots_coeff: &RationalNumber, ) -> Result { let d = decentralisation_param.to_checked_f64("decentralisation_param").map_err(|e| { anyhow::anyhow!("Failed to convert decentralisation parameter to f64: {}", e) @@ -106,8 +106,8 @@ pub fn classify_overlay_slot( pub fn lookup_in_overlay_schedule( epoch_slot: u64, genesis_delegs: &GenesisDelegs, - decentralisation_param: RationalNumber, - active_slots_coeff: RationalNumber, + decentralisation_param: &RationalNumber, + active_slots_coeff: &RationalNumber, ) -> Result> { let is_overlay_slot = is_overlay_slot(epoch_slot, decentralisation_param)?; if is_overlay_slot { @@ -142,8 +142,8 @@ mod tests { let obft_slot = lookup_in_overlay_schedule( epoch_slot, &genesis_delegs, - decentralisation_param, - active_slots_coeff, + &decentralisation_param, + &active_slots_coeff, ) .unwrap(); assert!(obft_slot.is_some()); @@ -166,8 +166,8 @@ mod tests { let obft_slot = lookup_in_overlay_schedule( epoch_slot, &genesis_delegs, - decentralisation_param, - active_slots_coeff, + &decentralisation_param, + &active_slots_coeff, ) .unwrap(); assert!(obft_slot.is_none()); diff --git a/common/src/ouroboros/praos.rs b/common/src/ouroboros/praos.rs new file mode 100644 index 00000000..981ca1ac --- /dev/null +++ b/common/src/ouroboros/praos.rs @@ -0,0 +1,100 @@ +use std::collections::HashMap; + +use crate::crypto::keyhash_224; +use crate::ouroboros::vrf; +use crate::ouroboros::vrf_validation::{ + TPraosBadLeaderVrfProofError, TPraosBadNonceVrfProofError, VrfLeaderValueTooBigError, + VrfValidation, VrfValidationError, WrongLeaderVrfKeyError, +}; +use crate::protocol_params::Nonce; +use crate::rational_number::RationalNumber; +use crate::{protocol_params::PraosParams, BlockInfo}; +use crate::{KeyHash, PoolId}; +use anyhow::Result; +use pallas::ledger::primitives::VrfCert; +use pallas::ledger::traverse::MultiEraHeader; + +pub fn validate_vrf_praos<'a>( + block_info: &'a BlockInfo, + header: &'a MultiEraHeader, + epoch_nonce: &'a Nonce, + praos_params: &'a PraosParams, + active_spos: &'a HashMap, + active_spdd: &'a HashMap, + total_active_stake: u64, +) -> Result>, VrfValidationError> { + let active_slots_coeff = praos_params.active_slots_coeff; + + let Some(issuer_vkey) = header.issuer_vkey() else { + return Ok(vec![Box::new(|| Err(VrfValidationError::MissingIssuerKey))]); + }; + let pool_id: PoolId = keyhash_224(issuer_vkey); + let registered_vrf_key_hash = + active_spos.get(&pool_id).ok_or(VrfValidationError::UnknownPool { + pool_id: pool_id.clone(), + })?; + + let pool_stake = active_spdd.get(&pool_id).unwrap_or(&0); + let relative_stake = RationalNumber::new(*pool_stake, total_active_stake); + + let Some(vrf_vkey) = header.vrf_vkey() else { + return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); + }; + let declared_vrf_key: &[u8; vrf::PublicKey::HASH_SIZE] = vrf_vkey + .try_into() + .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; + let nonce_vrf_cert = + nonce_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingNonceVrfCert)?; + let leader_vrf_cert = + leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; + + // Regular TPraos rules apply + Ok(vec![ + Box::new(move || { + WrongLeaderVrfKeyError::new(&pool_id, registered_vrf_key_hash, vrf_vkey)?; + Ok(()) + }), + Box::new(move || { + TPraosBadNonceVrfProofError::new( + block_info.slot, + epoch_nonce, + &vrf::PublicKey::from(declared_vrf_key), + &nonce_vrf_cert.0.to_vec()[..], + &nonce_vrf_cert.1.to_vec()[..], + )?; + Ok(()) + }), + Box::new(move || { + TPraosBadLeaderVrfProofError::new( + block_info.slot, + epoch_nonce, + &vrf::PublicKey::from(declared_vrf_key), + &leader_vrf_cert.0.to_vec()[..], + &leader_vrf_cert.1.to_vec()[..], + )?; + Ok(()) + }), + Box::new(move || { + VrfLeaderValueTooBigError::new( + &leader_vrf_cert.0.to_vec()[..], + &relative_stake, + &active_slots_coeff, + )?; + Ok(()) + }), + ]) +} + +fn nonce_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { + match header { + MultiEraHeader::ShelleyCompatible(x) => Some(&x.header_body.nonce_vrf), + _ => None, + } +} + +fn leader_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { + match header { + MultiEraHeader::ShelleyCompatible(x) => Some(&x.header_body.leader_vrf), + _ => None, + } +} diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index 9866ab56..886446ba 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -1,25 +1,32 @@ +use std::collections::HashMap; + use crate::crypto::keyhash_224; use crate::ouroboros::overlay_shedule::OBftSlot; use crate::ouroboros::vrf_validation::{ - TPraosBadLeaderVrfProofError, TPraosBadNonceVrfProofError, VrfValidation, VrfValidationError, - WrongGenesisLeaderVrfKeyError, WrongLeaderVrfKeyError, + TPraosBadLeaderVrfProofError, TPraosBadNonceVrfProofError, VrfLeaderValueTooBigError, + VrfValidation, VrfValidationError, WrongGenesisLeaderVrfKeyError, WrongLeaderVrfKeyError, }; use crate::ouroboros::{overlay_shedule, vrf}; use crate::protocol_params::Nonce; use crate::rational_number::RationalNumber; -use crate::PoolId; +use crate::serialization::Bech32WithHrp; use crate::{genesis_values::GenesisDelegs, protocol_params::PraosParams, BlockInfo}; +use crate::{KeyHash, PoolId}; use anyhow::Result; use pallas::ledger::primitives::VrfCert; use pallas::ledger::traverse::MultiEraHeader; +use tracing::info; pub fn validate_vrf_tpraos<'a>( block_info: &'a BlockInfo, header: &'a MultiEraHeader, - praos_params: &'a PraosParams, epoch_nonce: &'a Nonce, - decentralisation_param: RationalNumber, genesis_delegs: &'a GenesisDelegs, + praos_params: &'a PraosParams, + active_spos: &'a HashMap, + active_spdd: &'a HashMap, + total_active_stake: u64, + decentralisation_param: RationalNumber, ) -> Result>, VrfValidationError> { let active_slots_coeff = praos_params.active_slots_coeff; @@ -27,8 +34,8 @@ pub fn validate_vrf_tpraos<'a>( let obft_slot = overlay_shedule::lookup_in_overlay_schedule( block_info.epoch_slot, genesis_delegs, - decentralisation_param, - active_slots_coeff, + &decentralisation_param, + &active_slots_coeff, ) .map_err(|e| VrfValidationError::InvalidShelleyParams(e.to_string()))?; @@ -38,6 +45,13 @@ pub fn validate_vrf_tpraos<'a>( return Ok(vec![Box::new(|| Err(VrfValidationError::MissingIssuerKey))]); }; let pool_id: PoolId = keyhash_224(issuer_vkey); + let registered_vrf_key_hash = + active_spos.get(&pool_id).ok_or(VrfValidationError::UnknownPool { + pool_id: pool_id.clone(), + })?; + + let pool_stake = active_spdd.get(&pool_id).unwrap_or(&0); + let relative_stake = RationalNumber::new(*pool_stake, total_active_stake); let Some(vrf_vkey) = header.vrf_vkey() else { return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); @@ -50,8 +64,18 @@ pub fn validate_vrf_tpraos<'a>( let leader_vrf_cert = leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; + let pool_id_bech32 = pool_id.to_bech32_with_hrp("pool").unwrap(); + info!( + "epoch: {}, block: {}, pool_id: {}, active_stake: {}, total_active_stake: {}", + block_info.epoch, block_info.number, pool_id_bech32, pool_stake, total_active_stake + ); + // Regular TPraos rules apply Ok(vec![ + Box::new(move || { + WrongLeaderVrfKeyError::new(&pool_id, registered_vrf_key_hash, vrf_vkey)?; + Ok(()) + }), Box::new(move || { TPraosBadNonceVrfProofError::new( block_info.slot, @@ -72,6 +96,14 @@ pub fn validate_vrf_tpraos<'a>( )?; Ok(()) }), + Box::new(move || { + VrfLeaderValueTooBigError::new( + &leader_vrf_cert.0.to_vec()[..], + &relative_stake, + &active_slots_coeff, + )?; + Ok(()) + }), ]) } Some(OBftSlot::ActiveSlot(genesis_key, gen_deleg)) => { @@ -139,7 +171,8 @@ fn leader_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { #[cfg(test)] mod tests { use crate::{ - genesis_values::GenesisValues, protocol_params::NonceHash, BlockHash, BlockStatus, Era, + crypto::keyhash_256, genesis_values::GenesisValues, protocol_params::NonceHash, + serialization::Bech32WithHrp, BlockHash, BlockStatus, Era, }; use super::*; @@ -176,16 +209,81 @@ mod tests { new_epoch: true, era: Era::Shelley, }; + let active_spos = HashMap::new(); + let active_spdd = HashMap::new(); let vrf_validations = validate_vrf_tpraos( &block_info, &block_header, - &praos_params, &epoch_nonce, + &genesis_value.genesis_delegs, + &praos_params, + &active_spos, + &active_spdd, + 1, decentralisation_param, + ) + .unwrap(); + let result: Result<(), VrfValidationError> = + vrf_validations.iter().try_for_each(|assert| assert()); + assert!(result.is_ok()); + } + + #[test] + fn test_4576496_block() { + let genesis_value = GenesisValues::mainnet(); + let praos_params = PraosParams::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4576496: Vec = + hex::decode(include_str!("./data/4576496.cbor")).unwrap(); + let block_header = MultiEraHeader::decode(1, None, &block_header_4576496).unwrap(); + let block_info = BlockInfo { + status: BlockStatus::Immutable, + slot: 6220749, + hash: BlockHash::try_from( + hex::decode("d78e446b6540612e161ebdda32ee1715ef0f9fc68e890c7e3aae167b0354f998") + .unwrap(), + ) + .unwrap(), + timestamp: 1597787040, + number: 4576496, + epoch: 211, + epoch_slot: 431949, + new_epoch: false, + era: Era::Shelley, + }; + let pool_id = Vec::::from_bech32_with_hrp( + "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + "pool", + ) + .unwrap(); + let active_spos: HashMap = HashMap::from([( + pool_id.clone(), + keyhash_256(block_header.vrf_vkey().unwrap()), + )]); + let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); + let vrf_validations = validate_vrf_tpraos( + &block_info, + &block_header, + &epoch_nonce, &genesis_value.genesis_delegs, + &praos_params, + &active_spos, + &active_spdd, + 10177811974823000, + decentralisation_param, ) .unwrap(); - let result = vrf_validations.iter().try_for_each(|assert| assert()); + let result: Result<(), VrfValidationError> = + vrf_validations.iter().try_for_each(|assert| assert()); assert!(result.is_ok()); } } diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index 0fee2b0b..ce4f9b95 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -17,6 +17,9 @@ pub enum VrfValidationError { /// **Cause:** The Shelley protocol parameters used to validate the block, #[error("{0}")] InvalidShelleyParams(String), + /// **Cause**: The epoch nonces are not set + #[error("Epoch Nonces are missing")] + MissingEpochNonces, /// **Cause:** The Issuer Key is missing from the block header #[error("Missing Issuer Key")] MissingIssuerKey, @@ -282,7 +285,7 @@ impl PraosBadVrfProofError { } } -// ------------------------------------------------------------ TPraosVrfLeaderValueTooBigError +// ------------------------------------------------------------ VrfLeaderValueTooBigError /// Reference /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L430 @@ -306,8 +309,8 @@ pub enum VrfLeaderValueTooBigError { impl VrfLeaderValueTooBigError { pub fn new( leader_vrf_output: &[u8], - leader_relative_stake: RationalNumber, - active_slot_coeff: RationalNumber, + leader_relative_stake: &RationalNumber, + active_slot_coeff: &RationalNumber, ) -> Result<(), Self> { let certified_leader_vrf = &FixedDecimal::from(leader_vrf_output); let output_size_bits = leader_vrf_output.len() * 8; diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 6163c820..f57ae67b 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -14,6 +14,7 @@ use tokio::sync::Mutex; use tracing::{error, info, info_span}; mod state; use state::State; +mod snapshot; const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = ("validation-vrf-publisher-topic", "cardano.validation.vrf"); @@ -32,6 +33,8 @@ const DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC: (&str, &str) = ("epoch-nonces-subscribe-topic", "cardano.epoch.nonces"); const DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC: (&str, &str) = ("spo-state-subscribe-topic", "cardano.spo.state"); +const DEFAULT_SPDD_SUBSCRIBE_TOPIC: (&str, &str) = + ("spdd-subscribe-topic", "cardano.spo.distribution"); /// Block VRF Validator module #[module( @@ -50,6 +53,7 @@ impl BlockVrfValidator { mut protocol_parameters_subscription: Box>, mut epoch_nonces_subscription: Box>, mut spo_state_subscription: Box>, + mut spdd_subscription: Box>, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; let genesis = match bootstrapped_message.as_ref() { @@ -75,26 +79,57 @@ impl BlockVrfValidator { } let is_new_epoch = block_info.new_epoch && block_info.epoch > 0; - // read protocol parameters if new epoch if is_new_epoch { + // read epoch boundary messages let protocol_parameters_message_f = protocol_parameters_subscription.read(); let epoch_nonces_message_f = epoch_nonces_subscription.read(); - let (_, protocol_parameters_msg) = protocol_parameters_message_f.await?; - let (_, epoch_nonces_msg) = epoch_nonces_message_f.await?; + let spo_state_message_f = spo_state_subscription.read(); + let spdd_msg_f = spdd_subscription.read(); - match protocol_parameters_msg.as_ref() { + let (_, protocol_parameters_msg) = protocol_parameters_message_f.await?; + let span = info_span!( + "block_vrf_validator.handle_protocol_parameters", + epoch = block_info.epoch + ); + span.in_scope(|| match protocol_parameters_msg.as_ref() { Message::Cardano((_, CardanoMessage::ProtocolParams(msg))) => { state.handle_protocol_parameters(msg); } _ => error!("Unexpected message type: {protocol_parameters_msg:?}"), - } + }); - match epoch_nonces_msg.as_ref() { + let (_, epoch_nonces_msg) = epoch_nonces_message_f.await?; + let span = info_span!( + "block_vrf_validator.handle_epoch_nonces", + epoch = block_info.epoch + ); + span.in_scope(|| match epoch_nonces_msg.as_ref() { Message::Cardano((_, CardanoMessage::EpochNonces(msg))) => { state.handle_epoch_nonces(msg); } _ => error!("Unexpected message type: {epoch_nonces_msg:?}"), - }; + }); + + let (_, spo_state_msg) = spo_state_message_f.await?; + let (_, spdd_msg) = spdd_msg_f.await?; + let span = info_span!( + "block_vrf_validator.handle_new_snapshot", + epoch = block_info.epoch + ); + span.in_scope(|| match (spo_state_msg.as_ref(), spdd_msg.as_ref()) { + ( + Message::Cardano((_, CardanoMessage::SPOState(spo_state_msg))), + Message::Cardano(( + _, + CardanoMessage::SPOStakeDistribution(spdd_msg), + )), + ) => { + state.handle_new_snapshot(&spo_state_msg, &spdd_msg); + } + _ => { + error!("Unexpected message type: {spo_state_msg:?} or {spdd_msg:?}") + } + }); } // decode header @@ -127,7 +162,13 @@ impl BlockVrfValidator { info_span!("block_vrf_validator.validate", block = block_info.number); span.in_scope(|| { if let Some(header) = header.as_ref() { - state.validate_block_vrf(block_info, header, &genesis); + let result = state.validate_block_vrf(block_info, header, &genesis); + if let Err(e) = result { + error!( + "VRF validation failed for block {}: {e}", + block_info.number + ); + }; } }); } @@ -168,6 +209,11 @@ impl BlockVrfValidator { .unwrap_or(DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating spo state subscription on '{spo_state_subscribe_topic}'"); + let spdd_subscribe_topic = config + .get_string(DEFAULT_SPDD_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_SPDD_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating spdd subscription on '{spdd_subscribe_topic}'"); + // Subscribers let bootstrapped_subscription = context.subscribe(&bootstrapped_subscribe_topic).await?; let protocol_parameters_subscription = @@ -175,6 +221,7 @@ impl BlockVrfValidator { let blocks_subscription = context.subscribe(&blocks_subscribe_topic).await?; let epoch_nonces_subscription = context.subscribe(&epoch_nonces_subscribe_topic).await?; let spo_state_subscription = context.subscribe(&spo_state_subscribe_topic).await?; + let spdd_subscription = context.subscribe(&spdd_subscribe_topic).await?; // state history let history = Arc::new(Mutex::new(StateHistory::::new( @@ -191,6 +238,7 @@ impl BlockVrfValidator { protocol_parameters_subscription, epoch_nonces_subscription, spo_state_subscription, + spdd_subscription, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); diff --git a/modules/block_vrf_validator/src/snapshot.rs b/modules/block_vrf_validator/src/snapshot.rs new file mode 100644 index 00000000..70cbf9a0 --- /dev/null +++ b/modules/block_vrf_validator/src/snapshot.rs @@ -0,0 +1,41 @@ +use std::collections::HashMap; + +use acropolis_common::{ + messages::{SPOStakeDistributionMessage, SPOStateMessage}, + KeyHash, PoolId, +}; + +/// Epoch data for block vrf validation +#[derive(Debug, Default)] +pub struct Snapshot { + /// Map of pool_id to its vrf_key_hash + pub active_spos: HashMap, + + /// active stakes keyed by pool id + pub active_stakes: HashMap, + + pub total_active_stakes: u64, +} + +impl From<(&SPOStateMessage, &SPOStakeDistributionMessage)> for Snapshot { + fn from((spo_state_msg, spdd_msg): (&SPOStateMessage, &SPOStakeDistributionMessage)) -> Self { + let active_spos: HashMap = spo_state_msg + .spos + .iter() + .map(|registration| { + ( + registration.operator.clone(), + registration.vrf_key_hash.clone(), + ) + }) + .collect(); + let active_stakes: HashMap = + spdd_msg.spos.iter().map(|(pool_id, stake)| (pool_id.clone(), stake.live)).collect(); + let total_active_stakes = active_stakes.iter().map(|(_, stake)| stake).sum(); + Self { + active_spos, + active_stakes, + total_active_stakes, + } + } +} diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 9a7ba6ff..340926d5 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -1,25 +1,50 @@ //! Acropolis block_vrf_validator state storage +use std::sync::Arc; + use acropolis_common::{ genesis_values::GenesisValues, - messages::{EpochNoncesMessage, ProtocolParamsMessage}, - ouroboros::vrf_validation::VrfValidationError, + messages::{ + EpochNoncesMessage, ProtocolParamsMessage, SPOStakeDistributionMessage, SPOStateMessage, + }, + ouroboros::{self, vrf_validation::VrfValidationError}, protocol_params::{Nonces, PraosParams, ShelleyParams}, - BlockInfo, + BlockInfo, Era, }; use anyhow::Result; use pallas::ledger::traverse::MultiEraHeader; +use crate::snapshot::Snapshot; + +#[derive(Default, Debug, Clone)] +pub struct EpochSnapshots { + pub mark: Arc, + pub set: Arc, + pub go: Arc, +} + +impl EpochSnapshots { + /// Push a new snapshot + pub fn push(&mut self, latest: Snapshot) { + self.go = self.set.clone(); + self.set = self.mark.clone(); + self.mark = Arc::new(latest); + } +} + #[derive(Default, Debug, Clone)] pub struct State { - // shelley params + /// shelley params pub shelly_params: Option, - // protocol parameter for Praos and TPraos + /// protocol parameter for Praos and TPraos pub praos_params: Option, - // epoch nonces + /// epoch nonces pub epoch_nonces: Option, + + /// epoch snapshots + pub epoch_snapshots: EpochSnapshots, } impl State { @@ -28,10 +53,10 @@ impl State { praos_params: None, shelly_params: None, epoch_nonces: None, + epoch_snapshots: EpochSnapshots::default(), } } - /// Handle protocol parameters updates pub fn handle_protocol_parameters(&mut self, msg: &ProtocolParamsMessage) { if let Some(shelly_params) = msg.params.shelley.as_ref() { self.shelly_params = Some(shelly_params.clone()); @@ -39,11 +64,19 @@ impl State { } } - /// Handle epoch nonces updates pub fn handle_epoch_nonces(&mut self, msg: &EpochNoncesMessage) { self.epoch_nonces = Some(msg.nonces.clone()); } + pub fn handle_new_snapshot( + &mut self, + spo_state_msg: &SPOStateMessage, + spdd_msg: &SPOStakeDistributionMessage, + ) { + let new_snapshot = Snapshot::from((spo_state_msg, spdd_msg)); + self.epoch_snapshots.push(new_snapshot); + } + pub fn validate_block_vrf( &self, block_info: &BlockInfo, @@ -65,7 +98,43 @@ impl State { "Praos Params are not set".to_string(), )); }; + let Some(epoch_nonces) = self.epoch_nonces.as_ref() else { + return Err(VrfValidationError::MissingEpochNonces); + }; + let decentralisation_param = shelley_params.protocol_params.decentralisation_param; - Ok(()) + let is_tpraos = match block_info.era { + Era::Shelley => true, + Era::Allegra => true, + Era::Mary => true, + Era::Alonzo => true, + _ => false, + }; + + if is_tpraos { + let vrf_validations = ouroboros::tpraos::validate_vrf_tpraos( + block_info, + header, + &epoch_nonces.active, + &genesis.genesis_delegs, + praos_params, + &self.epoch_snapshots.go.active_spos, + &self.epoch_snapshots.go.active_stakes, + self.epoch_snapshots.go.total_active_stakes, + decentralisation_param, + )?; + vrf_validations.iter().try_for_each(|assert| assert()) + } else { + let vrf_validations = ouroboros::praos::validate_vrf_praos( + block_info, + header, + &epoch_nonces.active, + praos_params, + &self.epoch_snapshots.go.active_spos, + &self.epoch_snapshots.go.active_stakes, + self.epoch_snapshots.go.total_active_stakes, + )?; + vrf_validations.iter().try_for_each(|assert| assert()) + } } } diff --git a/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs b/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs index 5ee24faf..84c10703 100644 --- a/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs +++ b/modules/mithril_snapshot_fetcher/src/mithril_snapshot_fetcher.rs @@ -357,10 +357,6 @@ impl MithrilSnapshotFetcher { CardanoMessage::BlockAvailable(message), )); - if block_info.epoch >= 10 { - info!("Publishing block available: for epoch {}", block_info.epoch); - } - context .message_bus .publish(&block_topic, Arc::new(message_enum)) diff --git a/processes/omnibus/.gitignore b/processes/omnibus/.gitignore index 1fa2d34e..48de8004 100644 --- a/processes/omnibus/.gitignore +++ b/processes/omnibus/.gitignore @@ -6,3 +6,5 @@ cache # DB files *_db + +blocks/ From 0a07b2c0c16e0b7cb2c5b1c05c0c3d82ce008a13 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 29 Oct 2025 14:20:32 +0100 Subject: [PATCH 14/36] fix: cargo fmt --- common/src/hash.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/src/hash.rs b/common/src/hash.rs index eff7601a..ab22e961 100644 --- a/common/src/hash.rs +++ b/common/src/hash.rs @@ -149,4 +149,4 @@ mod tests { fn from_str_fail_2() { let _digest: Hash<32> = "0d8d00cdd465".parse().unwrap(); } -} \ No newline at end of file +} From 4fec2042e8725732f6fe093d2ee3b6c505970c24 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 29 Oct 2025 14:49:26 +0100 Subject: [PATCH 15/36] fix: typo --- modules/block_vrf_validator/src/state.rs | 12 ++++++------ modules/epochs_state/src/state.rs | 6 +++--- modules/rest_blockfrost/src/handlers/pools.rs | 4 ++-- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 340926d5..936945c5 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -35,7 +35,7 @@ impl EpochSnapshots { #[derive(Default, Debug, Clone)] pub struct State { /// shelley params - pub shelly_params: Option, + pub shelley_params: Option, /// protocol parameter for Praos and TPraos pub praos_params: Option, @@ -51,16 +51,16 @@ impl State { pub fn new() -> Self { Self { praos_params: None, - shelly_params: None, + shelley_params: None, epoch_nonces: None, epoch_snapshots: EpochSnapshots::default(), } } pub fn handle_protocol_parameters(&mut self, msg: &ProtocolParamsMessage) { - if let Some(shelly_params) = msg.params.shelley.as_ref() { - self.shelly_params = Some(shelly_params.clone()); - self.praos_params = Some(shelly_params.into()); + if let Some(shelley_params) = msg.params.shelley.as_ref() { + self.shelley_params = Some(shelley_params.clone()); + self.praos_params = Some(shelley_params.into()); } } @@ -88,7 +88,7 @@ impl State { return Ok(()); } - let Some(shelley_params) = self.shelly_params.as_ref() else { + let Some(shelley_params) = self.shelley_params.as_ref() else { return Err(VrfValidationError::InvalidShelleyParams( "Shelley Params are not set".to_string(), )); diff --git a/modules/epochs_state/src/state.rs b/modules/epochs_state/src/state.rs index 4f3e38f4..312b20af 100644 --- a/modules/epochs_state/src/state.rs +++ b/modules/epochs_state/src/state.rs @@ -54,7 +54,7 @@ pub struct State { // fees seen this epoch epoch_fees: u64, - // nonces will be set starting from Shelly Era + // nonces will be set starting from Shelley Era nonces: Option, // protocol parameter for Praos and TPraos @@ -84,8 +84,8 @@ impl State { /// Handle protocol parameters updates pub fn handle_protocol_parameters(&mut self, msg: &ProtocolParamsMessage) { - if let Some(shelly_params) = msg.params.shelley.as_ref() { - self.praos_params = Some(shelly_params.into()); + if let Some(shelley_params) = msg.params.shelley.as_ref() { + self.praos_params = Some(shelley_params.into()); } } diff --git a/modules/rest_blockfrost/src/handlers/pools.rs b/modules/rest_blockfrost/src/handlers/pools.rs index cba7a279..7254c169 100644 --- a/modules/rest_blockfrost/src/handlers/pools.rs +++ b/modules/rest_blockfrost/src/handlers/pools.rs @@ -212,7 +212,7 @@ async fn handle_pools_extended_blockfrost( // check optimal_pool_sizing is Some let Some(optimal_pool_sizing) = optimal_pool_sizing else { - // if it is before Shelly Era + // if it is before Shelley Era return Ok(RESTResponse::with_json(200, "[]")); }; @@ -591,7 +591,7 @@ async fn handle_pools_spo_blockfrost( let live_stakes_info = live_stakes_info?; let total_blocks_minted = total_blocks_minted?; let Some(optimal_pool_sizing) = optimal_pool_sizing? else { - // if it is before Shelly Era + // if it is before Shelley Era return Ok(RESTResponse::with_json(404, "Pool Not Found")); }; let pool_updates = pool_updates?; From 0cba1a7dd3ef48675c2b23c98109ace57c396fc8 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 29 Oct 2025 14:51:53 +0100 Subject: [PATCH 16/36] chore: simplify is_tpraos bool check --- common/src/ouroboros/vrf.rs | 2 +- common/src/protocol_params.rs | 2 +- modules/block_vrf_validator/src/state.rs | 11 ++++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/common/src/ouroboros/vrf.rs b/common/src/ouroboros/vrf.rs index 944ed256..3b508bb5 100644 --- a/common/src/ouroboros/vrf.rs +++ b/common/src/ouroboros/vrf.rs @@ -73,7 +73,7 @@ impl VrfInput { /// /// * `uc_nonce` - Universal constant nonce (domain separator) /// - Use `seed_eta()` for randomness/eta computation - /// - Use `seed_l()` for leader election computation + /// - Use `seed_l()` for leader election computation /// * `slot` - The slot number /// * `e_nonce` - The epoch nonce (randomness from the epoch) /// diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index f9f0a180..9d27c19b 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -301,7 +301,7 @@ impl Nonce { Self::from_number(0) } - /// Seed constant for leader (L) computation + /// Seed constant for leader (L) computation /// Used when determining if a stake pool is the slot leader pub fn seed_l() -> Self { Self::from_number(1) diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 936945c5..078784f2 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -103,13 +103,10 @@ impl State { }; let decentralisation_param = shelley_params.protocol_params.decentralisation_param; - let is_tpraos = match block_info.era { - Era::Shelley => true, - Era::Allegra => true, - Era::Mary => true, - Era::Alonzo => true, - _ => false, - }; + let is_tpraos = matches!( + block_info.era, + Era::Shelley | Era::Allegra | Era::Mary | Era::Alonzo + ); if is_tpraos { let vrf_validations = ouroboros::tpraos::validate_vrf_tpraos( From a64ae1f91a49896c51aa0055a80eb883691a7f9b Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 29 Oct 2025 15:46:43 +0100 Subject: [PATCH 17/36] feat: add vrf validation publisher --- common/src/ouroboros/tpraos.rs | 5 -- .../src/block_vrf_validator.rs | 29 +++++++---- .../src/vrf_validation_publisher.rs | 50 +++++++++++++++++++ 3 files changed, 70 insertions(+), 14 deletions(-) create mode 100644 modules/block_vrf_validator/src/vrf_validation_publisher.rs diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index 886446ba..6c7dca8f 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -15,7 +15,6 @@ use crate::{KeyHash, PoolId}; use anyhow::Result; use pallas::ledger::primitives::VrfCert; use pallas::ledger::traverse::MultiEraHeader; -use tracing::info; pub fn validate_vrf_tpraos<'a>( block_info: &'a BlockInfo, @@ -65,10 +64,6 @@ pub fn validate_vrf_tpraos<'a>( leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; let pool_id_bech32 = pool_id.to_bech32_with_hrp("pool").unwrap(); - info!( - "epoch: {}, block: {}, pool_id: {}, active_stake: {}, total_active_stake: {}", - block_info.epoch, block_info.number, pool_id_bech32, pool_stake, total_active_stake - ); // Regular TPraos rules apply Ok(vec![ diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index f57ae67b..c804c66f 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -11,10 +11,13 @@ use config::Config; use pallas::ledger::traverse::MultiEraHeader; use std::sync::Arc; use tokio::sync::Mutex; -use tracing::{error, info, info_span}; +use tracing::{error, info, info_span, Instrument}; mod state; use state::State; + +use crate::vrf_validation_publisher::VrfValidationPublisher; mod snapshot; +mod vrf_validation_publisher; const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = ("validation-vrf-publisher-topic", "cardano.validation.vrf"); @@ -48,6 +51,7 @@ pub struct BlockVrfValidator; impl BlockVrfValidator { async fn run( history: Arc>>, + mut vrf_validation_publisher: VrfValidationPublisher, mut bootstrapped_subscription: Box>, mut blocks_subscription: Box>, mut protocol_parameters_subscription: Box>, @@ -160,17 +164,19 @@ impl BlockVrfValidator { let span = info_span!("block_vrf_validator.validate", block = block_info.number); - span.in_scope(|| { + async { if let Some(header) = header.as_ref() { let result = state.validate_block_vrf(block_info, header, &genesis); - if let Err(e) = result { - error!( - "VRF validation failed for block {}: {e}", - block_info.number - ); - }; + if let Err(e) = vrf_validation_publisher + .publish_vrf_validation(block_info, result) + .await + { + error!("Failed to publish VRF validation: {e}") + } } - }); + } + .instrument(span) + .await; } _ => error!("Unexpected message type: {message:?}"), } @@ -214,6 +220,10 @@ impl BlockVrfValidator { .unwrap_or(DEFAULT_SPDD_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating spdd subscription on '{spdd_subscribe_topic}'"); + // publishers + let vrf_validation_publisher = + VrfValidationPublisher::new(context.clone(), validation_vrf_publisher_topic); + // Subscribers let bootstrapped_subscription = context.subscribe(&bootstrapped_subscribe_topic).await?; let protocol_parameters_subscription = @@ -233,6 +243,7 @@ impl BlockVrfValidator { context.run(async move { Self::run( history, + vrf_validation_publisher, bootstrapped_subscription, blocks_subscription, protocol_parameters_subscription, diff --git a/modules/block_vrf_validator/src/vrf_validation_publisher.rs b/modules/block_vrf_validator/src/vrf_validation_publisher.rs new file mode 100644 index 00000000..35c0296d --- /dev/null +++ b/modules/block_vrf_validator/src/vrf_validation_publisher.rs @@ -0,0 +1,50 @@ +use acropolis_common::{ + messages::{CardanoMessage, Message}, + ouroboros::vrf_validation::VrfValidationError, + validation::{ValidationError, ValidationStatus}, + BlockInfo, +}; +use caryatid_sdk::Context; +use std::sync::Arc; +use tracing::error; + +/// Message publisher for Block header Vrf Validation Result +pub struct VrfValidationPublisher { + /// Module context + context: Arc>, + + /// Topic to publish on + topic: String, +} + +impl VrfValidationPublisher { + /// Construct with context and topic to publish on + pub fn new(context: Arc>, topic: String) -> Self { + Self { context, topic } + } + + /// Publish the SPDD + pub async fn publish_vrf_validation( + &mut self, + block: &BlockInfo, + validation_result: Result<(), VrfValidationError>, + ) -> anyhow::Result<()> { + let validation_status = match validation_result { + Ok(_) => ValidationStatus::Go, + Err(error) => { + error!("VRF validation failed: {}", error.clone()); + ValidationStatus::NoGo(ValidationError::from(error)) + } + }; + self.context + .message_bus + .publish( + &self.topic, + Arc::new(Message::Cardano(( + block.clone(), + CardanoMessage::BlockValidation(validation_status), + ))), + ) + .await + } +} From 91c7a8738fb9c4a7593f23db55255b2ec1613679 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 29 Oct 2025 15:49:22 +0100 Subject: [PATCH 18/36] chore: remove unused var --- common/src/ouroboros/tpraos.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index 6c7dca8f..e3f5d088 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -9,7 +9,6 @@ use crate::ouroboros::vrf_validation::{ use crate::ouroboros::{overlay_shedule, vrf}; use crate::protocol_params::Nonce; use crate::rational_number::RationalNumber; -use crate::serialization::Bech32WithHrp; use crate::{genesis_values::GenesisDelegs, protocol_params::PraosParams, BlockInfo}; use crate::{KeyHash, PoolId}; use anyhow::Result; @@ -63,8 +62,6 @@ pub fn validate_vrf_tpraos<'a>( let leader_vrf_cert = leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; - let pool_id_bech32 = pool_id.to_bech32_with_hrp("pool").unwrap(); - // Regular TPraos rules apply Ok(vec![ Box::new(move || { From f591f6fa4f474212ce66f74448ba7aaaa0528efd Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 29 Oct 2025 16:35:32 +0100 Subject: [PATCH 19/36] refactor: publish only active epoch nonce --- common/src/messages.rs | 12 +++---- common/src/ouroboros/vrf_validation.rs | 6 ++-- .../src/block_vrf_validator.rs | 32 ++++++++--------- modules/block_vrf_validator/src/state.rs | 22 ++++++------ ..._publisher.rs => epoch_nonce_publisher.rs} | 16 ++++----- modules/epochs_state/src/epochs_state.rs | 34 +++++++++---------- 6 files changed, 60 insertions(+), 62 deletions(-) rename modules/epochs_state/src/{epoch_nonces_publisher.rs => epoch_nonce_publisher.rs} (65%) diff --git a/common/src/messages.rs b/common/src/messages.rs index e09948cb..4781eca8 100644 --- a/common/src/messages.rs +++ b/common/src/messages.rs @@ -6,7 +6,7 @@ use crate::commands::transactions::{TransactionsCommand, TransactionsCommandResponse}; use crate::genesis_values::GenesisValues; use crate::ledger_state::SPOState; -use crate::protocol_params::{NonceHash, Nonces, ProtocolParams}; +use crate::protocol_params::{Nonce, NonceHash, ProtocolParams}; use crate::queries::parameters::{ParametersStateQuery, ParametersStateQueryResponse}; use crate::queries::spdd::{SPDDStateQuery, SPDDStateQueryResponse}; use crate::queries::utxos::{UTxOStateQuery, UTxOStateQueryResponse}; @@ -187,8 +187,8 @@ pub struct EpochActivityMessage { } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct EpochNoncesMessage { - pub nonces: Nonces, +pub struct EpochNonceMessage { + pub nonce: Nonce, } #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] @@ -299,9 +299,9 @@ pub enum CardanoMessage { PotDeltas(PotDeltasMessage), // Changes to pot balances BlockInfoMessage(BlockTxsMessage), // Transaction Info (total count, total output, total fees in a block) EpochActivity(EpochActivityMessage), // Total fees and VRF keys for an epoch - EpochNonces(EpochNoncesMessage), // Epoch Nonces for Epoch N (published after the first block of Epoch N) - DRepState(DRepStateMessage), // Active DReps at epoch end - SPOState(SPOStateMessage), // Active SPOs at epoch end + EpochNonce(EpochNonceMessage), // Epoch Nonce for Epoch N (published after the first block of Epoch N) + DRepState(DRepStateMessage), // Active DReps at epoch end + SPOState(SPOStateMessage), // Active SPOs at epoch end GovernanceProcedures(GovernanceProceduresMessage), // Governance procedures received // Protocol Parameters diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index ce4f9b95..121abd38 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -17,9 +17,9 @@ pub enum VrfValidationError { /// **Cause:** The Shelley protocol parameters used to validate the block, #[error("{0}")] InvalidShelleyParams(String), - /// **Cause**: The epoch nonces are not set - #[error("Epoch Nonces are missing")] - MissingEpochNonces, + /// **Cause**: The epoch nonce are not set + #[error("Epoch Nonce are missing")] + MissingEpochNonce, /// **Cause:** The Issuer Key is missing from the block header #[error("Missing Issuer Key")] MissingIssuerKey, diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index c804c66f..628ce5d1 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -32,8 +32,8 @@ const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( ); const DEFAULT_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = ("blocks-subscribe-topic", "cardano.block.proposed"); -const DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC: (&str, &str) = - ("epoch-nonces-subscribe-topic", "cardano.epoch.nonces"); +const DEFAULT_EPOCH_NONCE_SUBSCRIBE_TOPIC: (&str, &str) = + ("epoch-nonce-subscribe-topic", "cardano.epoch.nonce"); const DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC: (&str, &str) = ("spo-state-subscribe-topic", "cardano.spo.state"); const DEFAULT_SPDD_SUBSCRIBE_TOPIC: (&str, &str) = @@ -55,7 +55,7 @@ impl BlockVrfValidator { mut bootstrapped_subscription: Box>, mut blocks_subscription: Box>, mut protocol_parameters_subscription: Box>, - mut epoch_nonces_subscription: Box>, + mut epoch_nonce_subscription: Box>, mut spo_state_subscription: Box>, mut spdd_subscription: Box>, ) -> Result<()> { @@ -86,7 +86,7 @@ impl BlockVrfValidator { if is_new_epoch { // read epoch boundary messages let protocol_parameters_message_f = protocol_parameters_subscription.read(); - let epoch_nonces_message_f = epoch_nonces_subscription.read(); + let epoch_nonce_message_f = epoch_nonce_subscription.read(); let spo_state_message_f = spo_state_subscription.read(); let spdd_msg_f = spdd_subscription.read(); @@ -102,16 +102,16 @@ impl BlockVrfValidator { _ => error!("Unexpected message type: {protocol_parameters_msg:?}"), }); - let (_, epoch_nonces_msg) = epoch_nonces_message_f.await?; + let (_, epoch_nonce_msg) = epoch_nonce_message_f.await?; let span = info_span!( - "block_vrf_validator.handle_epoch_nonces", + "block_vrf_validator.handle_epoch_nonce", epoch = block_info.epoch ); - span.in_scope(|| match epoch_nonces_msg.as_ref() { - Message::Cardano((_, CardanoMessage::EpochNonces(msg))) => { - state.handle_epoch_nonces(msg); + span.in_scope(|| match epoch_nonce_msg.as_ref() { + Message::Cardano((_, CardanoMessage::EpochNonce(msg))) => { + state.handle_epoch_nonce(msg); } - _ => error!("Unexpected message type: {epoch_nonces_msg:?}"), + _ => error!("Unexpected message type: {epoch_nonce_msg:?}"), }); let (_, spo_state_msg) = spo_state_message_f.await?; @@ -205,10 +205,10 @@ impl BlockVrfValidator { .unwrap_or(DEFAULT_BLOCKS_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating blocks subscription on '{blocks_subscribe_topic}'"); - let epoch_nonces_subscribe_topic = config - .get_string(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.0) - .unwrap_or(DEFAULT_EPOCH_NONCES_SUBSCRIBE_TOPIC.1.to_string()); - info!("Creating epoch nonces subscription on '{epoch_nonces_subscribe_topic}'"); + let epoch_nonce_subscribe_topic = config + .get_string(DEFAULT_EPOCH_NONCE_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_EPOCH_NONCE_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating epoch nonce subscription on '{epoch_nonce_subscribe_topic}'"); let spo_state_subscribe_topic = config .get_string(DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC.0) @@ -229,7 +229,7 @@ impl BlockVrfValidator { let protocol_parameters_subscription = context.subscribe(&protocol_parameters_subscribe_topic).await?; let blocks_subscription = context.subscribe(&blocks_subscribe_topic).await?; - let epoch_nonces_subscription = context.subscribe(&epoch_nonces_subscribe_topic).await?; + let epoch_nonce_subscription = context.subscribe(&epoch_nonce_subscribe_topic).await?; let spo_state_subscription = context.subscribe(&spo_state_subscribe_topic).await?; let spdd_subscription = context.subscribe(&spdd_subscribe_topic).await?; @@ -247,7 +247,7 @@ impl BlockVrfValidator { bootstrapped_subscription, blocks_subscription, protocol_parameters_subscription, - epoch_nonces_subscription, + epoch_nonce_subscription, spo_state_subscription, spdd_subscription, ) diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 078784f2..7ac0419a 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -5,10 +5,10 @@ use std::sync::Arc; use acropolis_common::{ genesis_values::GenesisValues, messages::{ - EpochNoncesMessage, ProtocolParamsMessage, SPOStakeDistributionMessage, SPOStateMessage, + EpochNonceMessage, ProtocolParamsMessage, SPOStakeDistributionMessage, SPOStateMessage, }, ouroboros::{self, vrf_validation::VrfValidationError}, - protocol_params::{Nonces, PraosParams, ShelleyParams}, + protocol_params::{Nonce, PraosParams, ShelleyParams}, BlockInfo, Era, }; use anyhow::Result; @@ -40,8 +40,8 @@ pub struct State { /// protocol parameter for Praos and TPraos pub praos_params: Option, - /// epoch nonces - pub epoch_nonces: Option, + /// epoch nonce + pub epoch_nonce: Option, /// epoch snapshots pub epoch_snapshots: EpochSnapshots, @@ -52,7 +52,7 @@ impl State { Self { praos_params: None, shelley_params: None, - epoch_nonces: None, + epoch_nonce: None, epoch_snapshots: EpochSnapshots::default(), } } @@ -64,8 +64,8 @@ impl State { } } - pub fn handle_epoch_nonces(&mut self, msg: &EpochNoncesMessage) { - self.epoch_nonces = Some(msg.nonces.clone()); + pub fn handle_epoch_nonce(&mut self, msg: &EpochNonceMessage) { + self.epoch_nonce = Some(msg.nonce.clone()); } pub fn handle_new_snapshot( @@ -98,8 +98,8 @@ impl State { "Praos Params are not set".to_string(), )); }; - let Some(epoch_nonces) = self.epoch_nonces.as_ref() else { - return Err(VrfValidationError::MissingEpochNonces); + let Some(epoch_nonce) = self.epoch_nonce.as_ref() else { + return Err(VrfValidationError::MissingEpochNonce); }; let decentralisation_param = shelley_params.protocol_params.decentralisation_param; @@ -112,7 +112,7 @@ impl State { let vrf_validations = ouroboros::tpraos::validate_vrf_tpraos( block_info, header, - &epoch_nonces.active, + &epoch_nonce, &genesis.genesis_delegs, praos_params, &self.epoch_snapshots.go.active_spos, @@ -125,7 +125,7 @@ impl State { let vrf_validations = ouroboros::praos::validate_vrf_praos( block_info, header, - &epoch_nonces.active, + &epoch_nonce, praos_params, &self.epoch_snapshots.go.active_spos, &self.epoch_snapshots.go.active_stakes, diff --git a/modules/epochs_state/src/epoch_nonces_publisher.rs b/modules/epochs_state/src/epoch_nonce_publisher.rs similarity index 65% rename from modules/epochs_state/src/epoch_nonces_publisher.rs rename to modules/epochs_state/src/epoch_nonce_publisher.rs index a2dc60b8..4703aad0 100644 --- a/modules/epochs_state/src/epoch_nonces_publisher.rs +++ b/modules/epochs_state/src/epoch_nonce_publisher.rs @@ -1,13 +1,13 @@ use acropolis_common::{ - messages::{CardanoMessage, EpochNoncesMessage, Message}, - protocol_params::Nonces, + messages::{CardanoMessage, EpochNonceMessage, Message}, + protocol_params::Nonce, BlockInfo, }; use caryatid_sdk::Context; use std::sync::Arc; -/// Message publisher for Epoch Nonces Message -pub struct EpochNoncesPublisher { +/// Message publisher for Epoch Nonce Message +pub struct EpochNoncePublisher { /// Module context context: Arc>, @@ -15,21 +15,21 @@ pub struct EpochNoncesPublisher { topic: String, } -impl EpochNoncesPublisher { +impl EpochNoncePublisher { /// Construct with context and topic to publish on pub fn new(context: Arc>, topic: String) -> Self { Self { context, topic } } - /// Publish the Epoch Nonces Message - pub async fn publish(&mut self, block_info: &BlockInfo, nonces: Nonces) -> anyhow::Result<()> { + /// Publish the Epoch Nonce Message + pub async fn publish(&mut self, block_info: &BlockInfo, nonce: Nonce) -> anyhow::Result<()> { self.context .message_bus .publish( &self.topic, Arc::new(Message::Cardano(( block_info.clone(), - CardanoMessage::EpochNonces(EpochNoncesMessage { nonces }), + CardanoMessage::EpochNonce(EpochNonceMessage { nonce }), ))), ) .await diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index 28bc8826..d5e0678d 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -17,18 +17,16 @@ use pallas::ledger::traverse::MultiEraHeader; use std::sync::Arc; use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; - mod epoch_activity_publisher; -mod epoch_nonces_publisher; +mod epoch_nonce_publisher; mod epochs_history; mod state; mod store_config; -use state::State; - use crate::{ - epoch_activity_publisher::EpochActivityPublisher, epoch_nonces_publisher::EpochNoncesPublisher, + epoch_activity_publisher::EpochActivityPublisher, epoch_nonce_publisher::EpochNoncePublisher, epochs_history::EpochsHistoryState, store_config::StoreConfig, }; +use state::State; const DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC: (&str, &str) = ( "bootstrapped-subscribe-topic", @@ -45,8 +43,8 @@ const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( const DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC: (&str, &str) = ("epoch-activity-publish-topic", "cardano.epoch.activity"); -const DEFAULT_EPOCH_NONCES_PUBLISH_TOPIC: (&str, &str) = - ("epoch-nonces-publish-topic", "cardano.epoch.nonces"); +const DEFAULT_EPOCH_NONCE_PUBLISH_TOPIC: (&str, &str) = + ("epoch-nonce-publish-topic", "cardano.epoch.nonce"); /// Epochs State module #[module( @@ -66,7 +64,7 @@ impl EpochsState { mut block_txs_subscription: Box>, mut protocol_parameters_subscription: Box>, mut epoch_activity_publisher: EpochActivityPublisher, - mut epoch_nonces_publisher: EpochNoncesPublisher, + mut epoch_nonce_publisher: EpochNoncePublisher, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; let genesis = match bootstrapped_message.as_ref() { @@ -153,11 +151,11 @@ impl EpochsState { Ok(()) => { if is_new_epoch { if let Some(nonces) = state.get_nonces() { - epoch_nonces_publisher - .publish(&block_info, nonces) + epoch_nonce_publisher + .publish(&block_info, nonces.active) .await .unwrap_or_else(|e| { - error!("Failed to publish epoch nonces: {e}") + error!("Failed to publish epoch nonce: {e}") }); } } @@ -233,10 +231,10 @@ impl EpochsState { .unwrap_or(DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC.1.to_string()); info!("Publishing EpochActivityMessage on '{epoch_activity_publish_topic}'"); - let epoch_nonces_publish_topic = config - .get_string(DEFAULT_EPOCH_NONCES_PUBLISH_TOPIC.0) - .unwrap_or(DEFAULT_EPOCH_NONCES_PUBLISH_TOPIC.1.to_string()); - info!("Publishing EpochNoncesMessage on '{epoch_nonces_publish_topic}'"); + let epoch_nonce_publish_topic = config + .get_string(DEFAULT_EPOCH_NONCE_PUBLISH_TOPIC.0) + .unwrap_or(DEFAULT_EPOCH_NONCE_PUBLISH_TOPIC.1.to_string()); + info!("Publishing EpochNonceMessage on '{epoch_nonce_publish_topic}'"); // query topic let epochs_query_topic = config @@ -268,8 +266,8 @@ impl EpochsState { // Publisher let epoch_activity_publisher = EpochActivityPublisher::new(context.clone(), epoch_activity_publish_topic); - let epoch_nonces_publisher = - EpochNoncesPublisher::new(context.clone(), epoch_nonces_publish_topic); + let epoch_nonce_publisher = + EpochNoncePublisher::new(context.clone(), epoch_nonce_publish_topic); // handle epochs query context.handle(&epochs_query_topic, move |message| { @@ -368,7 +366,7 @@ impl EpochsState { block_txs_subscription, protocol_parameters_subscription, epoch_activity_publisher, - epoch_nonces_publisher, + epoch_nonce_publisher, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); From 7d3d5eec734a8ca775337f709fab8c7aca9eb60c Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 29 Oct 2025 17:03:16 +0100 Subject: [PATCH 20/36] refactor: check block info sync in vrf validator --- Cargo.toml | 1 + .../src/block_vrf_validator.rs | 38 ++++++++++++++++--- processes/omnibus/Cargo.toml | 2 +- processes/omnibus/omnibus.toml | 2 + processes/omnibus/src/main.rs | 2 +- 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 49c15fcd..b051845f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "modules/consensus", # Chooses favoured chain across multiple options "modules/chain_store", # Tracks historical information about blocks and TXs "modules/tx_submitter", # Submits TXs to peers + "modules/block_vrf_validator", # Validate the VRF calculation in the block header # Process builds "processes/omnibus", # All-inclusive omnibus process diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 628ce5d1..18c647b8 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -1,9 +1,10 @@ //! Acropolis Block VRF Validator module for Caryatid //! Validate the VRF calculation in the block header + use acropolis_common::{ messages::{CardanoMessage, Message}, state_history::{StateHistory, StateHistoryStore}, - BlockStatus, Era, + BlockInfo, BlockStatus, Era, }; use anyhow::Result; use caryatid_sdk::{module, Context, Module, Subscription}; @@ -74,6 +75,7 @@ impl BlockVrfValidator { // Get a mutable state let mut state = history.lock().await.get_or_init_with(|| State::new()); let (_, message) = blocks_subscription.read().await?; + let mut current_block: Option = None; match message.as_ref() { Message::Cardano((block_info, CardanoMessage::BlockAvailable(block_msg))) => { @@ -81,6 +83,7 @@ impl BlockVrfValidator { if block_info.status == BlockStatus::RolledBack { state = history.lock().await.get_rolled_back_state(block_info.number); } + current_block = Some(block_info.clone()); let is_new_epoch = block_info.new_epoch && block_info.epoch > 0; if is_new_epoch { @@ -96,7 +99,8 @@ impl BlockVrfValidator { epoch = block_info.epoch ); span.in_scope(|| match protocol_parameters_msg.as_ref() { - Message::Cardano((_, CardanoMessage::ProtocolParams(msg))) => { + Message::Cardano((block_info, CardanoMessage::ProtocolParams(msg))) => { + Self::check_sync(¤t_block, block_info); state.handle_protocol_parameters(msg); } _ => error!("Unexpected message type: {protocol_parameters_msg:?}"), @@ -108,7 +112,8 @@ impl BlockVrfValidator { epoch = block_info.epoch ); span.in_scope(|| match epoch_nonce_msg.as_ref() { - Message::Cardano((_, CardanoMessage::EpochNonce(msg))) => { + Message::Cardano((block_info, CardanoMessage::EpochNonce(msg))) => { + Self::check_sync(¤t_block, block_info); state.handle_epoch_nonce(msg); } _ => error!("Unexpected message type: {epoch_nonce_msg:?}"), @@ -122,12 +127,17 @@ impl BlockVrfValidator { ); span.in_scope(|| match (spo_state_msg.as_ref(), spdd_msg.as_ref()) { ( - Message::Cardano((_, CardanoMessage::SPOState(spo_state_msg))), Message::Cardano(( - _, + block_info_1, + CardanoMessage::SPOState(spo_state_msg), + )), + Message::Cardano(( + block_info_2, CardanoMessage::SPOStakeDistribution(spdd_msg), )), ) => { + Self::check_sync(¤t_block, block_info_1); + Self::check_sync(¤t_block, block_info_2); state.handle_new_snapshot(&spo_state_msg, &spdd_msg); } _ => { @@ -180,6 +190,11 @@ impl BlockVrfValidator { } _ => error!("Unexpected message type: {message:?}"), } + + // Commit the new state + if let Some(block_info) = current_block { + history.lock().await.commit(block_info.number, state); + } } } @@ -257,4 +272,17 @@ impl BlockVrfValidator { Ok(()) } + + /// Check for synchronisation + fn check_sync(expected: &Option, actual: &BlockInfo) { + if let Some(ref block) = expected { + if block.number != actual.number { + error!( + expected = block.number, + actual = actual.number, + "Messages out of sync" + ); + } + } + } } diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index 342e53a6..93e991f9 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -21,7 +21,6 @@ acropolis_module_governance_state = { path = "../../modules/governance_state" } acropolis_module_parameters_state = { path = "../../modules/parameters_state" } acropolis_module_stake_delta_filter = { path = "../../modules/stake_delta_filter" } acropolis_module_epochs_state = { path = "../../modules/epochs_state" } -acropolis_module_block_vrf_validator = { path = "../../modules/block_vrf_validator" } acropolis_module_accounts_state = { path = "../../modules/accounts_state" } acropolis_module_rest_blockfrost = { path = "../../modules/rest_blockfrost" } acropolis_module_spdd_state = { path = "../../modules/spdd_state" } @@ -31,6 +30,7 @@ acropolis_module_chain_store = { path = "../../modules/chain_store" } acropolis_module_address_state = { path = "../../modules/address_state" } acropolis_module_consensus = { path = "../../modules/consensus" } acropolis_module_historical_accounts_state = { path = "../../modules/historical_accounts_state" } +acropolis_module_block_vrf_validator = { path = "../../modules/block_vrf_validator" } caryatid_process = { workspace = true } caryatid_sdk = { workspace = true } diff --git a/processes/omnibus/omnibus.toml b/processes/omnibus/omnibus.toml index 6922217e..7de217e1 100644 --- a/processes/omnibus/omnibus.toml +++ b/processes/omnibus/omnibus.toml @@ -137,6 +137,8 @@ store-totals = false # Enables /addresses/{address}/transactions endpoint store-transactions = false +[module.block-vrf-validator] + [module.clock] [module.rest-server] diff --git a/processes/omnibus/src/main.rs b/processes/omnibus/src/main.rs index 632a4f59..85eae5c5 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -112,7 +112,6 @@ pub async fn main() -> Result<()> { ParametersState::register(&mut process); StakeDeltaFilter::register(&mut process); EpochsState::register(&mut process); - BlockVrfValidator::register(&mut process); AccountsState::register(&mut process); AddressState::register(&mut process); AssetsState::register(&mut process); @@ -122,6 +121,7 @@ pub async fn main() -> Result<()> { DRDDState::register(&mut process); Consensus::register(&mut process); ChainStore::register(&mut process); + BlockVrfValidator::register(&mut process); Clock::::register(&mut process); RESTServer::::register(&mut process); From d968c88cd970e8ef3d1191f297603470282640d3 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 30 Oct 2025 08:15:10 +0100 Subject: [PATCH 21/36] refactor: epoch nonce publisher to publish nonces when there are not set, use active stake(set) instead of go snapshot --- common/src/messages.rs | 2 +- common/src/ouroboros/data/4556956.cbor | 1 + common/src/ouroboros/tpraos.rs | 61 ++++++++++++++++++- .../src/block_vrf_validator.rs | 2 +- modules/block_vrf_validator/src/state.rs | 14 ++--- .../src/vrf_validation_publisher.rs | 6 +- .../epochs_state/src/epoch_nonce_publisher.rs | 13 +++- modules/epochs_state/src/epochs_state.rs | 15 +++-- 8 files changed, 92 insertions(+), 22 deletions(-) create mode 100644 common/src/ouroboros/data/4556956.cbor diff --git a/common/src/messages.rs b/common/src/messages.rs index 4781eca8..2393ab6d 100644 --- a/common/src/messages.rs +++ b/common/src/messages.rs @@ -188,7 +188,7 @@ pub struct EpochActivityMessage { #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct EpochNonceMessage { - pub nonce: Nonce, + pub nonce: Option, } #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/ouroboros/data/4556956.cbor b/common/src/ouroboros/data/4556956.cbor new file mode 100644 index 00000000..68eb2b74 --- /dev/null +++ b/common/src/ouroboros/data/4556956.cbor @@ -0,0 +1 @@ +828f1a0045889c1a0058e1515820d5852411975dc3c5da5042f48372aa533b96c6d24017f9291cf65e730b5835555820330b133b489d79d039c7feceefe5acb9149f636d5d84943788ce8783c1c098245820e9dffe03f31be0431b2199696250f94ec599916ff5618080b428ac7c5ea5f6108258403962c816bc3989fe2b817e3a8f8deb54f6b0cf69105c7b041b3f1e90dfbf05a96c8912e11046647053d8e0c43fddf984082231946d3250a4182659e9b18cdd39585084e17ca6179f8d416f9bdc11f39714cb546ba0bc2274cf9d2c560a83d76942a31c55f30e95b310a9db9726c81cf37913f1a687dee0f21b5c553d863d393a4b1128425bb11901a6dc0a5bcd5d2eae82028258400018db71ca133eaa423d45ebfc33b6dbe2d35296f89e4ca3fe09810d8805255414ad3b5c97d0c3c7eca86d8d4c07035aa479bd6af9d9e986b8c4f9a5f76c9382585024df0024d93acdc1eb8b2a3a7b0f348fba765a4f27590eb6420458b34507ee655089866644bee297644c63fe8bc2aa3574a6b6a76e34fcaa5ce4010dd2648fb7118fbc7b6899024fd6369c43155c450f0358201033376be025cb705fd8dd02eda11cc73975a062b5d14ffd74d6ff69e69a2ff7582029429acc1d7d18b05039b379a7fe94dc95bd86631433ecb41b523e7d04a974c801015840c2b36ecd955d90a8adac85b4aa8a3dbdf022108794a255763c84c460db3b6eca45fb26dd5bc5cce9812f3199527e2c249473de89bd459888f12c964f58b1fc0c01005901c0f04b785404897ff6e361e76bc9a5f62c5b1af03e255a6110b09fc3be9d50ba1f2314076c09320a9bc9b755b6f9483325f9dbdab108bd8e412a63b1e15e54d60d17a33403716c4d5a57e851b90f21569cfdf4b7cd74ceb725d9173714d8ea7dda528f6848b0001f921382cdd64ae1ba60d7f725af64f10c572ed56ae28e187e4fe11c45ea269b508bcb6c08748be32e9bcf27df9c8853dc7272a278e60120f7fbe927d8614bbc3a1d9bab4a00199313f2f81f6b62a2f35496c04c1d7f8dae984695270c741298d567251066675609c71743a4cf61b525eabbea0f27d14e53a558210dbc7397354abda4d323b5b82460b6c8a1cb654f62af1962bfd3ec1a818e016be9e84c4ea1007a2edb5d392ea5cd84e4d92f2dd92fb1d80502f18df873a29915616650b4a697289bfec648eb72f0867bb1c6bd9c85fc25215078139f770a30e79b0f764bbef90a3933a82bb4f190e418f361c0426da2521c47c9330e96c876d8ec496117bf9a3cdb89420018e33e2977077fff74120fe8385ecd7729aee75742b31c09d4112ac7e777aced184056e8de92c3519e26ffdc98d85e3bd46725488bcad48a5da411985577277605a9c65a82f5224c179ac2da52be2e497b37de6b \ No newline at end of file diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index e3f5d088..a2a40d09 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -170,7 +170,7 @@ mod tests { use super::*; #[test] - fn test_4490511_block() { + fn test_4490511_block_produced_by_genesis_key() { let genesis_value = GenesisValues::mainnet(); let praos_params = PraosParams::mainnet(); let epoch_nonce = Nonce::from( @@ -220,6 +220,65 @@ mod tests { assert!(result.is_ok()); } + #[test] + fn test_4556956_block() { + let genesis_value = GenesisValues::mainnet(); + let praos_params = PraosParams::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4556956: Vec = + hex::decode(include_str!("./data/4556956.cbor")).unwrap(); + let block_header = MultiEraHeader::decode(1, None, &block_header_4556956).unwrap(); + let block_info = BlockInfo { + status: BlockStatus::Immutable, + slot: 5824849, + hash: BlockHash::try_from( + hex::decode("1038b2c76a23ea7d89cbd84d7744c97560eb3412661beed6959d748e24ff8229") + .unwrap(), + ) + .unwrap(), + timestamp: 1597391140, + number: 4556956, + epoch: 211, + epoch_slot: 36049, + new_epoch: false, + era: Era::Shelley, + }; + let pool_id = Vec::::from_bech32_with_hrp( + "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + "pool", + ) + .unwrap(); + let active_spos: HashMap = HashMap::from([( + pool_id.clone(), + keyhash_256(block_header.vrf_vkey().unwrap()), + )]); + let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); + let vrf_validations = validate_vrf_tpraos( + &block_info, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + &praos_params, + &active_spos, + &active_spdd, + 10177811974823000, + decentralisation_param, + ) + .unwrap(); + let result: Result<(), VrfValidationError> = + vrf_validations.iter().try_for_each(|assert| assert()); + assert!(result.is_ok()); + } + #[test] fn test_4576496_block() { let genesis_value = GenesisValues::mainnet(); diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 18c647b8..1d90389a 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -74,9 +74,9 @@ impl BlockVrfValidator { loop { // Get a mutable state let mut state = history.lock().await.get_or_init_with(|| State::new()); - let (_, message) = blocks_subscription.read().await?; let mut current_block: Option = None; + let (_, message) = blocks_subscription.read().await?; match message.as_ref() { Message::Cardano((block_info, CardanoMessage::BlockAvailable(block_msg))) => { // handle rollback here diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 7ac0419a..a40569f9 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -65,7 +65,7 @@ impl State { } pub fn handle_epoch_nonce(&mut self, msg: &EpochNonceMessage) { - self.epoch_nonce = Some(msg.nonce.clone()); + self.epoch_nonce = msg.nonce.clone(); } pub fn handle_new_snapshot( @@ -115,9 +115,9 @@ impl State { &epoch_nonce, &genesis.genesis_delegs, praos_params, - &self.epoch_snapshots.go.active_spos, - &self.epoch_snapshots.go.active_stakes, - self.epoch_snapshots.go.total_active_stakes, + &self.epoch_snapshots.set.active_spos, + &self.epoch_snapshots.set.active_stakes, + self.epoch_snapshots.set.total_active_stakes, decentralisation_param, )?; vrf_validations.iter().try_for_each(|assert| assert()) @@ -127,9 +127,9 @@ impl State { header, &epoch_nonce, praos_params, - &self.epoch_snapshots.go.active_spos, - &self.epoch_snapshots.go.active_stakes, - self.epoch_snapshots.go.total_active_stakes, + &self.epoch_snapshots.set.active_spos, + &self.epoch_snapshots.set.active_stakes, + self.epoch_snapshots.set.total_active_stakes, )?; vrf_validations.iter().try_for_each(|assert| assert()) } diff --git a/modules/block_vrf_validator/src/vrf_validation_publisher.rs b/modules/block_vrf_validator/src/vrf_validation_publisher.rs index 35c0296d..8e855cf4 100644 --- a/modules/block_vrf_validator/src/vrf_validation_publisher.rs +++ b/modules/block_vrf_validator/src/vrf_validation_publisher.rs @@ -32,7 +32,11 @@ impl VrfValidationPublisher { let validation_status = match validation_result { Ok(_) => ValidationStatus::Go, Err(error) => { - error!("VRF validation failed: {}", error.clone()); + error!( + "VRF validation failed: {} of block {}", + error.clone(), + block.number + ); ValidationStatus::NoGo(ValidationError::from(error)) } }; diff --git a/modules/epochs_state/src/epoch_nonce_publisher.rs b/modules/epochs_state/src/epoch_nonce_publisher.rs index 4703aad0..b292c833 100644 --- a/modules/epochs_state/src/epoch_nonce_publisher.rs +++ b/modules/epochs_state/src/epoch_nonce_publisher.rs @@ -1,6 +1,6 @@ use acropolis_common::{ messages::{CardanoMessage, EpochNonceMessage, Message}, - protocol_params::Nonce, + protocol_params::Nonces, BlockInfo, }; use caryatid_sdk::Context; @@ -22,14 +22,21 @@ impl EpochNoncePublisher { } /// Publish the Epoch Nonce Message - pub async fn publish(&mut self, block_info: &BlockInfo, nonce: Nonce) -> anyhow::Result<()> { + pub async fn publish( + &mut self, + block_info: &BlockInfo, + nonces: Option, + ) -> anyhow::Result<()> { + let active_nonce = nonces.map(|nonces| nonces.active); self.context .message_bus .publish( &self.topic, Arc::new(Message::Cardano(( block_info.clone(), - CardanoMessage::EpochNonce(EpochNonceMessage { nonce }), + CardanoMessage::EpochNonce(EpochNonceMessage { + nonce: active_nonce, + }), ))), ) .await diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index d5e0678d..ba471be1 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -150,14 +150,13 @@ impl EpochsState { match state.handle_block_header(&genesis, &block_info, &header) { Ok(()) => { if is_new_epoch { - if let Some(nonces) = state.get_nonces() { - epoch_nonce_publisher - .publish(&block_info, nonces.active) - .await - .unwrap_or_else(|e| { - error!("Failed to publish epoch nonce: {e}") - }); - } + let nonces = state.get_nonces(); + epoch_nonce_publisher + .publish(&block_info, nonces) + .await + .unwrap_or_else(|e| { + error!("Failed to publish epoch nonce: {e}") + }); } } Err(e) => error!("Error handling block header: {e}"), From 2b5dcd402b26bdb4d7a2b6a9e12d9b87811ff858 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 30 Oct 2025 08:48:04 +0100 Subject: [PATCH 22/36] refactor: add move test cases to tpraos validation --- common/src/ouroboros/tpraos.rs | 203 +++++++++++++++++++++-- common/src/ouroboros/vrf_validation.rs | 25 +-- modules/block_vrf_validator/src/state.rs | 14 +- 3 files changed, 213 insertions(+), 29 deletions(-) diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index a2a40d09..6cd62588 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -203,7 +203,7 @@ mod tests { }; let active_spos = HashMap::new(); let active_spdd = HashMap::new(); - let vrf_validations = validate_vrf_tpraos( + let result = validate_vrf_tpraos( &block_info, &block_header, &epoch_nonce, @@ -214,9 +214,7 @@ mod tests { 1, decentralisation_param, ) - .unwrap(); - let result: Result<(), VrfValidationError> = - vrf_validations.iter().try_for_each(|assert| assert()); + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); assert!(result.is_ok()); } @@ -262,7 +260,7 @@ mod tests { keyhash_256(block_header.vrf_vkey().unwrap()), )]); let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); - let vrf_validations = validate_vrf_tpraos( + let result = validate_vrf_tpraos( &block_info, &block_header, &epoch_nonce, @@ -273,9 +271,7 @@ mod tests { 10177811974823000, decentralisation_param, ) - .unwrap(); - let result: Result<(), VrfValidationError> = - vrf_validations.iter().try_for_each(|assert| assert()); + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); assert!(result.is_ok()); } @@ -321,7 +317,7 @@ mod tests { keyhash_256(block_header.vrf_vkey().unwrap()), )]); let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); - let vrf_validations = validate_vrf_tpraos( + let result = validate_vrf_tpraos( &block_info, &block_header, &epoch_nonce, @@ -332,9 +328,192 @@ mod tests { 10177811974823000, decentralisation_param, ) - .unwrap(); - let result: Result<(), VrfValidationError> = - vrf_validations.iter().try_for_each(|assert| assert()); + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); assert!(result.is_ok()); } + + #[test] + fn test_4576496_block_as_unknown_pool() { + let genesis_value = GenesisValues::mainnet(); + let praos_params = PraosParams::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4576496: Vec = + hex::decode(include_str!("./data/4576496.cbor")).unwrap(); + let block_header = MultiEraHeader::decode(1, None, &block_header_4576496).unwrap(); + let block_info = BlockInfo { + status: BlockStatus::Immutable, + slot: 6220749, + hash: BlockHash::try_from( + hex::decode("d78e446b6540612e161ebdda32ee1715ef0f9fc68e890c7e3aae167b0354f998") + .unwrap(), + ) + .unwrap(), + timestamp: 1597787040, + number: 4576496, + epoch: 211, + epoch_slot: 431949, + new_epoch: false, + era: Era::Shelley, + }; + let pool_id = Vec::::from_bech32_with_hrp( + "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + "pool", + ) + .unwrap(); + let active_spos: HashMap = HashMap::from([]); + let active_spdd = HashMap::from([]); + let result = validate_vrf_tpraos( + &block_info, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + &praos_params, + &active_spos, + &active_spdd, + 10177811974823000, + decentralisation_param, + ) + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + VrfValidationError::UnknownPool { pool_id } + ); + } + + #[test] + fn test_4576496_block_as_wrong_leader_vrf_key() { + let genesis_value = GenesisValues::mainnet(); + let praos_params = PraosParams::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4576496: Vec = + hex::decode(include_str!("./data/4576496.cbor")).unwrap(); + let block_header = MultiEraHeader::decode(1, None, &block_header_4576496).unwrap(); + let block_info = BlockInfo { + status: BlockStatus::Immutable, + slot: 6220749, + hash: BlockHash::try_from( + hex::decode("d78e446b6540612e161ebdda32ee1715ef0f9fc68e890c7e3aae167b0354f998") + .unwrap(), + ) + .unwrap(), + timestamp: 1597787040, + number: 4576496, + epoch: 211, + epoch_slot: 431949, + new_epoch: false, + era: Era::Shelley, + }; + let pool_id = Vec::::from_bech32_with_hrp( + "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + "pool", + ) + .unwrap(); + let active_spos: HashMap = + HashMap::from([(pool_id.clone(), keyhash_256(&[0; 64]))]); + let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); + let result = validate_vrf_tpraos( + &block_info, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + &praos_params, + &active_spos, + &active_spdd, + 10177811974823000, + decentralisation_param, + ) + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + VrfValidationError::WrongLeaderVrfKey(WrongLeaderVrfKeyError { + pool_id: pool_id.clone(), + registered_vrf_hash: keyhash_256(&[0; 64]), + header_vrf_hash: keyhash_256(block_header.vrf_vkey().unwrap()), + }) + ); + } + + #[test] + fn test_4576496_block_with_small_active_stake() { + let genesis_value = GenesisValues::mainnet(); + let praos_params = PraosParams::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4576496: Vec = + hex::decode(include_str!("./data/4576496.cbor")).unwrap(); + let block_header = MultiEraHeader::decode(1, None, &block_header_4576496).unwrap(); + let block_info = BlockInfo { + status: BlockStatus::Immutable, + slot: 6220749, + hash: BlockHash::try_from( + hex::decode("d78e446b6540612e161ebdda32ee1715ef0f9fc68e890c7e3aae167b0354f998") + .unwrap(), + ) + .unwrap(), + timestamp: 1597787040, + number: 4576496, + epoch: 211, + epoch_slot: 431949, + new_epoch: false, + era: Era::Shelley, + }; + let pool_id = Vec::::from_bech32_with_hrp( + "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", + "pool", + ) + .unwrap(); + let active_spos: HashMap = HashMap::from([( + pool_id.clone(), + keyhash_256(block_header.vrf_vkey().unwrap()), + )]); + // small active stake (correct one is 75284250207839) + let active_spdd = HashMap::from([(pool_id.clone(), 75284250207)]); + let result = validate_vrf_tpraos( + &block_info, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + &praos_params, + &active_spos, + &active_spdd, + 10177811974823000, + decentralisation_param, + ) + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + VrfValidationError::VrfLeaderValueTooBig( + VrfLeaderValueTooBigError::VrfLeaderValueTooBig + ) + ); + } } diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index 121abd38..e397224e 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -12,7 +12,7 @@ use thiserror::Error; /// Reference /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 -#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] pub enum VrfValidationError { /// **Cause:** The Shelley protocol parameters used to validate the block, #[error("{0}")] @@ -74,9 +74,9 @@ pub enum VrfValidationError { hex::encode(&header_vrf_hash), )] pub struct WrongGenesisLeaderVrfKeyError { - genesis_key: GenesisKey, - registered_vrf_hash: KeyHash, - header_vrf_hash: KeyHash, + pub genesis_key: GenesisKey, + pub registered_vrf_hash: KeyHash, + pub header_vrf_hash: KeyHash, } impl WrongGenesisLeaderVrfKeyError { @@ -108,9 +108,9 @@ impl WrongGenesisLeaderVrfKeyError { hex::encode(&header_vrf_hash), )] pub struct WrongLeaderVrfKeyError { - pool_id: PoolId, - registered_vrf_hash: KeyHash, - header_vrf_hash: KeyHash, + pub pool_id: PoolId, + pub registered_vrf_hash: KeyHash, + pub header_vrf_hash: KeyHash, } impl WrongLeaderVrfKeyError { @@ -133,7 +133,7 @@ impl WrongLeaderVrfKeyError { // ------------------------------------------------------------ TPraosBadNonceVrfProofError -#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum TPraosBadNonceVrfProofError { #[error("Bad Nonce VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] BadVrfProof(Slot, Nonce, BadVrfProofError), @@ -171,7 +171,7 @@ impl TPraosBadNonceVrfProofError { // ------------------------------------------------------------ TPraosBadLeaderVrfProofError -#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum TPraosBadLeaderVrfProofError { #[error("Bad Leader VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] BadVrfProof(Slot, Nonce, BadVrfProofError), @@ -293,14 +293,17 @@ impl PraosBadVrfProofError { /// /// Check that the certified input natural is valid for being slot leader. This means we check that /// p < 1 - (1 - f)^σ -/// where p = certNat / certNatMax. (certNat is 64bytes for TPraos and 32bytes for Praos) +/// **Variables** +/// `p` = `certNat` / `certNatMax`. (`certNat` is 64bytes for TPraos and 32bytes for Praos) +/// `σ` (sigma) = pool's relative stake (pools active stake / total active stake) +/// `f` = active slot coefficient (e.g., 0.05 = 5%) /// let q = 1 - p and c = ln(1 - f) /// then p < 1 - (1 - f)^σ => 1 / (1 - p) < exp(-σ * c) => 1 / q < exp(-σ * c) /// Reference /// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L331 /// -#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] pub enum VrfLeaderValueTooBigError { #[error("VRF Leader Value Too Big")] VrfLeaderValueTooBig, diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index a40569f9..fe23963d 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -109,7 +109,7 @@ impl State { ); if is_tpraos { - let vrf_validations = ouroboros::tpraos::validate_vrf_tpraos( + let result = ouroboros::tpraos::validate_vrf_tpraos( block_info, header, &epoch_nonce, @@ -119,10 +119,11 @@ impl State { &self.epoch_snapshots.set.active_stakes, self.epoch_snapshots.set.total_active_stakes, decentralisation_param, - )?; - vrf_validations.iter().try_for_each(|assert| assert()) + ) + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + result } else { - let vrf_validations = ouroboros::praos::validate_vrf_praos( + let result = ouroboros::praos::validate_vrf_praos( block_info, header, &epoch_nonce, @@ -130,8 +131,9 @@ impl State { &self.epoch_snapshots.set.active_spos, &self.epoch_snapshots.set.active_stakes, self.epoch_snapshots.set.total_active_stakes, - )?; - vrf_validations.iter().try_for_each(|assert| assert()) + ) + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + result } } } From 6c148fb044af63c25fd73749103777ae45e8ca70 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 30 Oct 2025 08:52:25 +0100 Subject: [PATCH 23/36] revert: git ignore file --- processes/omnibus/.gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/processes/omnibus/.gitignore b/processes/omnibus/.gitignore index 48de8004..1fa2d34e 100644 --- a/processes/omnibus/.gitignore +++ b/processes/omnibus/.gitignore @@ -6,5 +6,3 @@ cache # DB files *_db - -blocks/ From d19c1d86e3334df3ab92c02aef7d4feeb358e843 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 30 Oct 2025 09:18:12 +0100 Subject: [PATCH 24/36] fix: update praos vrf validation to use one vrf --- common/src/ouroboros/praos.rs | 43 +++++++++----------------- common/src/ouroboros/vrf_validation.rs | 6 ++++ 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/common/src/ouroboros/praos.rs b/common/src/ouroboros/praos.rs index 981ca1ac..f58379b6 100644 --- a/common/src/ouroboros/praos.rs +++ b/common/src/ouroboros/praos.rs @@ -3,8 +3,8 @@ use std::collections::HashMap; use crate::crypto::keyhash_224; use crate::ouroboros::vrf; use crate::ouroboros::vrf_validation::{ - TPraosBadLeaderVrfProofError, TPraosBadNonceVrfProofError, VrfLeaderValueTooBigError, - VrfValidation, VrfValidationError, WrongLeaderVrfKeyError, + PraosBadVrfProofError, VrfLeaderValueTooBigError, VrfValidation, VrfValidationError, + WrongLeaderVrfKeyError, }; use crate::protocol_params::Nonce; use crate::rational_number::RationalNumber; @@ -43,10 +43,7 @@ pub fn validate_vrf_praos<'a>( let declared_vrf_key: &[u8; vrf::PublicKey::HASH_SIZE] = vrf_vkey .try_into() .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; - let nonce_vrf_cert = - nonce_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingNonceVrfCert)?; - let leader_vrf_cert = - leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; + let vrf_cert = vrf_result(header).ok_or(VrfValidationError::PraosMissingVrfCert)?; // Regular TPraos rules apply Ok(vec![ @@ -55,28 +52,23 @@ pub fn validate_vrf_praos<'a>( Ok(()) }), Box::new(move || { - TPraosBadNonceVrfProofError::new( + PraosBadVrfProofError::new( block_info.slot, epoch_nonce, + &header + .leader_vrf_output() + .map_err(|_| VrfValidationError::PraosMissingLeaderVrfOutput)?[..], &vrf::PublicKey::from(declared_vrf_key), - &nonce_vrf_cert.0.to_vec()[..], - &nonce_vrf_cert.1.to_vec()[..], - )?; - Ok(()) - }), - Box::new(move || { - TPraosBadLeaderVrfProofError::new( - block_info.slot, - epoch_nonce, - &vrf::PublicKey::from(declared_vrf_key), - &leader_vrf_cert.0.to_vec()[..], - &leader_vrf_cert.1.to_vec()[..], + &vrf_cert.0.to_vec()[..], + &vrf_cert.1.to_vec()[..], )?; Ok(()) }), Box::new(move || { VrfLeaderValueTooBigError::new( - &leader_vrf_cert.0.to_vec()[..], + &header + .leader_vrf_output() + .map_err(|_| VrfValidationError::PraosMissingLeaderVrfOutput)?[..], &relative_stake, &active_slots_coeff, )?; @@ -85,16 +77,9 @@ pub fn validate_vrf_praos<'a>( ]) } -fn nonce_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { - match header { - MultiEraHeader::ShelleyCompatible(x) => Some(&x.header_body.nonce_vrf), - _ => None, - } -} - -fn leader_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { +fn vrf_result<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { match header { - MultiEraHeader::ShelleyCompatible(x) => Some(&x.header_body.leader_vrf), + MultiEraHeader::BabbageCompatible(x) => Some(&x.header_body.vrf_result), _ => None, } } diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index e397224e..1d3dccf2 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -32,6 +32,12 @@ pub enum VrfValidationError { /// **Cause:** The Leader VRF Cert is missing from the block header in TPraos Protocol #[error("TPraos Missing Leader VRF Cert")] TPraosMissingLeaderVrfCert, + /// **Cause:** The VRF output is missing from the block header in Praos Protocol + #[error("Praos Missing Leader VRF Output")] + PraosMissingLeaderVrfOutput, + /// **Cause:** The VRF Cert is missing from the block header in Praos Protocol + #[error("Praos Missing VRF Cert")] + PraosMissingVrfCert, /// **Cause:** Block issuer's pool ID is not registered in current stake distribution #[error("Unknown Pool: {}", hex::encode(&pool_id))] UnknownPool { pool_id: PoolId }, From 10c70c071f532abdfe2a007e72e6e60756b48260 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 30 Oct 2025 09:23:04 +0100 Subject: [PATCH 25/36] fix: typo --- common/src/ouroboros/mod.rs | 2 +- .../ouroboros/{overlay_shedule.rs => overlay_schedule.rs} | 0 common/src/ouroboros/tpraos.rs | 6 +++--- 3 files changed, 4 insertions(+), 4 deletions(-) rename common/src/ouroboros/{overlay_shedule.rs => overlay_schedule.rs} (100%) diff --git a/common/src/ouroboros/mod.rs b/common/src/ouroboros/mod.rs index 75d1d8e8..9d6b274e 100644 --- a/common/src/ouroboros/mod.rs +++ b/common/src/ouroboros/mod.rs @@ -1,4 +1,4 @@ -pub mod overlay_shedule; +pub mod overlay_schedule; pub mod praos; pub mod tpraos; pub mod vrf; diff --git a/common/src/ouroboros/overlay_shedule.rs b/common/src/ouroboros/overlay_schedule.rs similarity index 100% rename from common/src/ouroboros/overlay_shedule.rs rename to common/src/ouroboros/overlay_schedule.rs diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index 6cd62588..dddc5ae9 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -1,12 +1,12 @@ use std::collections::HashMap; use crate::crypto::keyhash_224; -use crate::ouroboros::overlay_shedule::OBftSlot; +use crate::ouroboros::overlay_schedule::OBftSlot; use crate::ouroboros::vrf_validation::{ TPraosBadLeaderVrfProofError, TPraosBadNonceVrfProofError, VrfLeaderValueTooBigError, VrfValidation, VrfValidationError, WrongGenesisLeaderVrfKeyError, WrongLeaderVrfKeyError, }; -use crate::ouroboros::{overlay_shedule, vrf}; +use crate::ouroboros::{overlay_schedule, vrf}; use crate::protocol_params::Nonce; use crate::rational_number::RationalNumber; use crate::{genesis_values::GenesisDelegs, protocol_params::PraosParams, BlockInfo}; @@ -29,7 +29,7 @@ pub fn validate_vrf_tpraos<'a>( let active_slots_coeff = praos_params.active_slots_coeff; // first look up for overlay slot - let obft_slot = overlay_shedule::lookup_in_overlay_schedule( + let obft_slot = overlay_schedule::lookup_in_overlay_schedule( block_info.epoch_slot, genesis_delegs, &decentralisation_param, From a0c00453e2a9bc49a2c41ca0e80a2cb1a7f621bb Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Thu, 30 Oct 2025 16:44:57 +0100 Subject: [PATCH 26/36] test: add praos test case --- common/src/ouroboros/data/7854823.cbor | 1 + common/src/ouroboros/praos.rs | 65 +++++++++++++++++++ common/src/ouroboros/tpraos.rs | 18 +++-- .../src/block_vrf_validator.rs | 16 ++--- 4 files changed, 84 insertions(+), 16 deletions(-) create mode 100644 common/src/ouroboros/data/7854823.cbor diff --git a/common/src/ouroboros/data/7854823.cbor b/common/src/ouroboros/data/7854823.cbor new file mode 100644 index 00000000..1199b9b6 --- /dev/null +++ b/common/src/ouroboros/data/7854823.cbor @@ -0,0 +1 @@ +828a1a0077dae71a046344c15820991e0770751806103894d8fffaf86fcc90f238c6ac00bdc7cb0a953e4a59fd93582041ec779d32f5ca1fc81a8cb4152e6bc1f0ab569abe1c0de8b66d76242d23a70f5820b5b6a6b790e6926f6ba856255aaf3eb5192cc80c03eeec7469cd3f64344bafb78258403f42ef44b8525ee45ca45a094191f00b359e9205a400b0607c87fe488f3b44442afc9e7c3e28ded9080c71d84eb7840ecb2e7240721ee243b45a761048ad6c53585054ac3b2bc94b54705b951146a83cba92041fdf537644628c64e9549365e74c10502fb7c23f34d7daa8327843f728513b225044a9390f86fb2a83951c4a39a13c17472c71cf50d74f1ce12d04dc85e9021a00015b1158202c1657b98fabe337cff8811a0b198dd36dfa61a415fdc5e5759f43cfa1103c48845820249caec7a56b2ede7dfdb956178fc6d7d5c96b5f35e63ddc7987e9261ac277c70b1902065840c4bbbda856bebac009d32c276080a54bb6322ab3252d09ad4c7c4a3e3f9b8931dd81b1795dbd165d8d3ca493d77966eb682bfff0edc1d151ef0ad42efb91ba0d8207005901c03824c9afe0176e3abf9e9bcf6972fb8c408d218fe0eab8026780a9995c7cc8fbf2a6f7d54fcb138bcff109b578d020baf5dbae751dad81980d52146afc615905ea103db0ff10d488fc9d0dbfa410066768d93c566fc346ff4b81bd41137df20c446771dd0d24399d61a9caeddce7abb55e6647247158ac252cf87e56871f4535857f6dbbf90b99d9b4bd6ca20705f7deff4e7d3220c002c4e7e5d96927751d96e08c92886ac223a4f7141625da97819a3b574dbf80ca50418f33ce2858b41f2fe63290bda8fcfe8382defc7ce721ed6d96466efe69a0bcb31a3951447b3e73c07e167240a99b492a71b0f9a6af321620d16b944b7b7ce89353ce0b65fc19f3744357603fca23f2b0056c607f817b24ee15b51008ab43e470016e46c0a1589bf184b46b79e808750f7a96fa747387414b9e691b95e7003e825308e4cd4fcf67d87233cb90282e3a5e65ce5cd3448e2a5d5ee6b232a431a45c252ed9a04dbee9cf89c5d5f975481b3be2623bf4a517205b69516d59e0a1877643e25485846911252315499cc705061ee74bd8a6ca2a9ffe46f2905839372b008074d775b6f6f7bb3f245263d306f0194133f947909ff4af9f9815800f1173ef7ea602886f55e1fd \ No newline at end of file diff --git a/common/src/ouroboros/praos.rs b/common/src/ouroboros/praos.rs index f58379b6..1f1740d5 100644 --- a/common/src/ouroboros/praos.rs +++ b/common/src/ouroboros/praos.rs @@ -83,3 +83,68 @@ fn vrf_result<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { _ => None, } } + +#[cfg(test)] +mod tests { + use crate::{ + crypto::keyhash_256, protocol_params::NonceHash, serialization::Bech32WithHrp, BlockHash, + BlockStatus, Era, + }; + + use super::*; + + #[test] + fn test_7854823_block() { + let praos_params = PraosParams::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("8dad163edf4607452fec9c5955d593fb598ca728bae162138f88da6667bba79b") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + + let block_header_7854823: Vec = + hex::decode(include_str!("./data/7854823.cbor")).unwrap(); + let block_info = BlockInfo { + status: BlockStatus::Immutable, + slot: 73614529, + hash: BlockHash::try_from( + hex::decode("4884996cff870563ffddab5d1255a82a58482ba9351536f5b72c882f883c8947") + .unwrap(), + ) + .unwrap(), + timestamp: 1665180820, + number: 7854823, + epoch: 368, + epoch_slot: 1729, + new_epoch: false, + era: Era::Babbage, + }; + let block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_7854823).unwrap(); + let pool_id = Vec::::from_bech32_with_hrp( + "pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t", + "pool", + ) + .unwrap(); + let active_spos = HashMap::from([( + pool_id.clone(), + keyhash_256(block_header.vrf_vkey().unwrap()), + )]); + let active_spdd = HashMap::from([(pool_id.clone(), 64590523391239)]); + let result = validate_vrf_praos( + &block_info, + &block_header, + &epoch_nonce, + &praos_params, + &active_spos, + &active_spdd, + 25069171797357766, + ) + .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + println!("{:?}", result); + assert!(result.is_ok()); + } +} diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index dddc5ae9..838e4f47 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -185,7 +185,6 @@ mod tests { let block_header_4490511: Vec = hex::decode(include_str!("./data/4490511.cbor")).unwrap(); - let block_header = MultiEraHeader::decode(1, None, &block_header_4490511).unwrap(); let block_info = BlockInfo { status: BlockStatus::Immutable, slot: 4492800, @@ -201,6 +200,8 @@ mod tests { new_epoch: true, era: Era::Shelley, }; + let block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4490511).unwrap(); let active_spos = HashMap::new(); let active_spdd = HashMap::new(); let result = validate_vrf_tpraos( @@ -234,7 +235,6 @@ mod tests { let block_header_4556956: Vec = hex::decode(include_str!("./data/4556956.cbor")).unwrap(); - let block_header = MultiEraHeader::decode(1, None, &block_header_4556956).unwrap(); let block_info = BlockInfo { status: BlockStatus::Immutable, slot: 5824849, @@ -250,6 +250,8 @@ mod tests { new_epoch: false, era: Era::Shelley, }; + let block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4556956).unwrap(); let pool_id = Vec::::from_bech32_with_hrp( "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", "pool", @@ -291,7 +293,6 @@ mod tests { let block_header_4576496: Vec = hex::decode(include_str!("./data/4576496.cbor")).unwrap(); - let block_header = MultiEraHeader::decode(1, None, &block_header_4576496).unwrap(); let block_info = BlockInfo { status: BlockStatus::Immutable, slot: 6220749, @@ -307,6 +308,8 @@ mod tests { new_epoch: false, era: Era::Shelley, }; + let block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); let pool_id = Vec::::from_bech32_with_hrp( "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", "pool", @@ -348,7 +351,6 @@ mod tests { let block_header_4576496: Vec = hex::decode(include_str!("./data/4576496.cbor")).unwrap(); - let block_header = MultiEraHeader::decode(1, None, &block_header_4576496).unwrap(); let block_info = BlockInfo { status: BlockStatus::Immutable, slot: 6220749, @@ -364,6 +366,8 @@ mod tests { new_epoch: false, era: Era::Shelley, }; + let block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); let pool_id = Vec::::from_bech32_with_hrp( "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", "pool", @@ -406,7 +410,6 @@ mod tests { let block_header_4576496: Vec = hex::decode(include_str!("./data/4576496.cbor")).unwrap(); - let block_header = MultiEraHeader::decode(1, None, &block_header_4576496).unwrap(); let block_info = BlockInfo { status: BlockStatus::Immutable, slot: 6220749, @@ -422,6 +425,8 @@ mod tests { new_epoch: false, era: Era::Shelley, }; + let block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); let pool_id = Vec::::from_bech32_with_hrp( "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", "pool", @@ -469,7 +474,6 @@ mod tests { let block_header_4576496: Vec = hex::decode(include_str!("./data/4576496.cbor")).unwrap(); - let block_header = MultiEraHeader::decode(1, None, &block_header_4576496).unwrap(); let block_info = BlockInfo { status: BlockStatus::Immutable, slot: 6220749, @@ -485,6 +489,8 @@ mod tests { new_epoch: false, era: Era::Shelley, }; + let block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); let pool_id = Vec::::from_bech32_with_hrp( "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", "pool", diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index 1d90389a..b574f63f 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -4,7 +4,7 @@ use acropolis_common::{ messages::{CardanoMessage, Message}, state_history::{StateHistory, StateHistoryStore}, - BlockInfo, BlockStatus, Era, + BlockInfo, BlockStatus, }; use anyhow::Result; use caryatid_sdk::{module, Context, Module, Subscription}; @@ -149,21 +149,17 @@ impl BlockVrfValidator { // decode header // Derive the variant from the era - just enough to make // MultiEraHeader::decode() work. - let variant = match block_info.era { - Era::Byron => 0, - Era::Shelley => 1, - Era::Allegra => 2, - Era::Mary => 3, - Era::Alonzo => 4, - _ => 5, - }; let span = info_span!( "block_vrf_validator.decode_header", block = block_info.number ); let mut header = None; span.in_scope(|| { - header = match MultiEraHeader::decode(variant, None, &block_msg.header) { + header = match MultiEraHeader::decode( + block_info.era as u8, + None, + &block_msg.header, + ) { Ok(header) => Some(header), Err(e) => { error!("Can't decode header {}: {e}", block_info.slot); From c82b74bc9a857e8a6764601060c4ae1cdddbf265 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 5 Nov 2025 15:10:12 +0100 Subject: [PATCH 27/36] fix: build errors --- common/src/genesis_values.rs | 114 ++++-------------- common/src/ouroboros/overlay_schedule.rs | 13 +- common/src/ouroboros/praos.rs | 20 ++- common/src/ouroboros/tpraos.rs | 80 ++++++------ common/src/ouroboros/vrf_validation.rs | 41 +++---- common/src/types.rs | 32 ++++- modules/block_vrf_validator/src/snapshot.rs | 6 +- .../src/genesis_bootstrapper.rs | 20 +-- modules/upstream_chain_fetcher/src/utils.rs | 5 +- 9 files changed, 145 insertions(+), 186 deletions(-) diff --git a/common/src/genesis_values.rs b/common/src/genesis_values.rs index 134d98a0..0c52d4aa 100644 --- a/common/src/genesis_values.rs +++ b/common/src/genesis_values.rs @@ -1,76 +1,20 @@ -use std::collections::BTreeMap; - use crate::{ calculations::{ epoch_to_first_slot_with_shelley_params, slot_to_epoch_with_shelley_params, slot_to_timestamp_with_params, }, - hash::Hash, + GenesisDelegates, }; const MAINNET_SHELLEY_GENESIS_HASH: &str = "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81"; -pub type GenesisKey = Hash<28>; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)] -pub struct GenDeleg { - // Pool Id - pub delegate: Hash<28>, - pub vrf: Hash<32>, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct GenesisDelegs(pub BTreeMap); - -impl AsRef> for GenesisDelegs { - fn as_ref(&self) -> &BTreeMap { - &self.0 - } -} - -impl From> for GenesisDelegs { - fn from(entries: Vec<(String, (String, String))>) -> Self { - let map = entries - .into_iter() - .map(|(key_hash, (delegate, vrf))| { - let key = Hash::new( - hex::decode(key_hash) - .expect("Invalid key hash hex string") - .try_into() - .expect("Invalid Genesis Key length"), - ); - let delegate_hash = Hash::new( - hex::decode(delegate) - .expect("Invalid delegate hex string") - .try_into() - .expect("Invalid delegate hash length"), - ); - let vrf_hash = Hash::new( - hex::decode(vrf) - .expect("Invalid VRF hex string") - .try_into() - .expect("Invalid VRF hash length"), - ); - ( - key, - GenDeleg { - delegate: delegate_hash, - vrf: vrf_hash, - }, - ) - }) - .collect(); - GenesisDelegs(map) - } -} - #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct GenesisValues { pub byron_timestamp: u64, pub shelley_epoch: u64, pub shelley_epoch_len: u64, pub shelley_genesis_hash: [u8; 32], - pub genesis_delegs: GenesisDelegs, + pub genesis_delegs: GenesisDelegates, } impl GenesisValues { @@ -83,64 +27,58 @@ impl GenesisValues { .unwrap() .try_into() .unwrap(), - genesis_delegs: GenesisDelegs::from(vec![ + genesis_delegs: GenesisDelegates::try_from(vec![ ( - "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c".to_string(), + "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c", ( - "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d".to_string(), - "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7" - .to_string(), + "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d", + "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7", ), ), ( - "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497".to_string(), + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497", ( - "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048".to_string(), - "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc" - .to_string(), + "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048", + "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc", ), ), ( - "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1".to_string(), + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1", ( - "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956".to_string(), - "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674" - .to_string(), + "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956", + "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674", ), ), ( - "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757".to_string(), + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757", ( - "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011".to_string(), - "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2" - .to_string(), + "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011", + "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2", ), ), ( - "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82".to_string(), + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82", ( - "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6".to_string(), - "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32" - .to_string(), + "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6", + "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32", ), ), ( - "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6".to_string(), + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6", ( - "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b".to_string(), - "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af" - .to_string(), + "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b", + "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af", ), ), ( - "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b".to_string(), + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b", ( - "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23".to_string(), - "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db" - .to_string(), + "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23", + "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db", ), ), - ]), + ]) + .unwrap(), } } diff --git a/common/src/ouroboros/overlay_schedule.rs b/common/src/ouroboros/overlay_schedule.rs index 5c5b47b8..4345f979 100644 --- a/common/src/ouroboros/overlay_schedule.rs +++ b/common/src/ouroboros/overlay_schedule.rs @@ -6,18 +6,17 @@ //! https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/Rules/Overlay.hs#L332 use crate::{ - genesis_values::{GenDeleg, GenesisDelegs, GenesisKey}, - rational_number::RationalNumber, - rest_helper::ToCheckedF64, + rational_number::RationalNumber, rest_helper::ToCheckedF64, GenesisDelegate, GenesisDelegates, + GenesisKeyhash, }; use anyhow::Result; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub enum OBftSlot { /// Overlay slot but no block should be produced (rare edge case) NonActiveSlot, /// Active overlay slot reserved for specific genesis key - ActiveSlot(GenesisKey, GenDeleg), + ActiveSlot(GenesisKeyhash, GenesisDelegate), } /// Determine if the given slot is reserved for the overlay schedule. @@ -57,7 +56,7 @@ pub fn is_overlay_slot(epoch_slot: u64, decentralisation_param: &RationalNumber) /// Classification of the slot (NonActiveSlot or ActiveSlot with genesis key) pub fn classify_overlay_slot( epoch_slot: u64, - genesis_delegs: &GenesisDelegs, + genesis_delegs: &GenesisDelegates, decentralisation_param: &RationalNumber, active_slots_coeff: &RationalNumber, ) -> Result { @@ -105,7 +104,7 @@ pub fn classify_overlay_slot( /// - active_slots_coeff is not a valid rational number pub fn lookup_in_overlay_schedule( epoch_slot: u64, - genesis_delegs: &GenesisDelegs, + genesis_delegs: &GenesisDelegates, decentralisation_param: &RationalNumber, active_slots_coeff: &RationalNumber, ) -> Result> { diff --git a/common/src/ouroboros/praos.rs b/common/src/ouroboros/praos.rs index 1f1740d5..84d556eb 100644 --- a/common/src/ouroboros/praos.rs +++ b/common/src/ouroboros/praos.rs @@ -9,7 +9,7 @@ use crate::ouroboros::vrf_validation::{ use crate::protocol_params::Nonce; use crate::rational_number::RationalNumber; use crate::{protocol_params::PraosParams, BlockInfo}; -use crate::{KeyHash, PoolId}; +use crate::{PoolId, VrfKeyHash}; use anyhow::Result; use pallas::ledger::primitives::VrfCert; use pallas::ledger::traverse::MultiEraHeader; @@ -19,7 +19,7 @@ pub fn validate_vrf_praos<'a>( header: &'a MultiEraHeader, epoch_nonce: &'a Nonce, praos_params: &'a PraosParams, - active_spos: &'a HashMap, + active_spos: &'a HashMap, active_spdd: &'a HashMap, total_active_stake: u64, ) -> Result>, VrfValidationError> { @@ -28,7 +28,7 @@ pub fn validate_vrf_praos<'a>( let Some(issuer_vkey) = header.issuer_vkey() else { return Ok(vec![Box::new(|| Err(VrfValidationError::MissingIssuerKey))]); }; - let pool_id: PoolId = keyhash_224(issuer_vkey); + let pool_id = PoolId::from(keyhash_224(issuer_vkey)); let registered_vrf_key_hash = active_spos.get(&pool_id).ok_or(VrfValidationError::UnknownPool { pool_id: pool_id.clone(), @@ -87,8 +87,8 @@ fn vrf_result<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { #[cfg(test)] mod tests { use crate::{ - crypto::keyhash_256, protocol_params::NonceHash, serialization::Bech32WithHrp, BlockHash, - BlockStatus, Era, + crypto::keyhash_256, protocol_params::NonceHash, serialization::Bech32Conversion, + BlockHash, BlockStatus, Era, }; use super::*; @@ -124,14 +124,12 @@ mod tests { }; let block_header = MultiEraHeader::decode(block_info.era as u8, None, &block_header_7854823).unwrap(); - let pool_id = Vec::::from_bech32_with_hrp( - "pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t", - "pool", - ) - .unwrap(); + let pool_id = + PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") + .unwrap(); let active_spos = HashMap::from([( pool_id.clone(), - keyhash_256(block_header.vrf_vkey().unwrap()), + VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), )]); let active_spdd = HashMap::from([(pool_id.clone(), 64590523391239)]); let result = validate_vrf_praos( diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index 838e4f47..e5d18f09 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -9,8 +9,8 @@ use crate::ouroboros::vrf_validation::{ use crate::ouroboros::{overlay_schedule, vrf}; use crate::protocol_params::Nonce; use crate::rational_number::RationalNumber; -use crate::{genesis_values::GenesisDelegs, protocol_params::PraosParams, BlockInfo}; -use crate::{KeyHash, PoolId}; +use crate::{protocol_params::PraosParams, BlockInfo}; +use crate::{GenesisDelegates, PoolId, VrfKeyHash}; use anyhow::Result; use pallas::ledger::primitives::VrfCert; use pallas::ledger::traverse::MultiEraHeader; @@ -19,9 +19,9 @@ pub fn validate_vrf_tpraos<'a>( block_info: &'a BlockInfo, header: &'a MultiEraHeader, epoch_nonce: &'a Nonce, - genesis_delegs: &'a GenesisDelegs, + genesis_delegs: &'a GenesisDelegates, praos_params: &'a PraosParams, - active_spos: &'a HashMap, + active_spos: &'a HashMap, active_spdd: &'a HashMap, total_active_stake: u64, decentralisation_param: RationalNumber, @@ -42,7 +42,7 @@ pub fn validate_vrf_tpraos<'a>( let Some(issuer_vkey) = header.issuer_vkey() else { return Ok(vec![Box::new(|| Err(VrfValidationError::MissingIssuerKey))]); }; - let pool_id: PoolId = keyhash_224(issuer_vkey); + let pool_id = PoolId::from(keyhash_224(issuer_vkey)); let registered_vrf_key_hash = active_spos.get(&pool_id).ok_or(VrfValidationError::UnknownPool { pool_id: pool_id.clone(), @@ -54,7 +54,7 @@ pub fn validate_vrf_tpraos<'a>( let Some(vrf_vkey) = header.vrf_vkey() else { return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); }; - let declared_vrf_key: &[u8; vrf::PublicKey::HASH_SIZE] = vrf_vkey + let declared_vrf_key: &[u8; vrf::PublicKey::SIZE] = vrf_vkey .try_into() .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; let nonce_vrf_cert = @@ -104,7 +104,7 @@ pub fn validate_vrf_tpraos<'a>( let Some(vrf_vkey) = header.vrf_vkey() else { return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); }; - let declared_vrf_key: &[u8; vrf::PublicKey::HASH_SIZE] = vrf_vkey + let declared_vrf_key: &[u8; vrf::PublicKey::SIZE] = vrf_vkey .try_into() .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; let nonce_vrf_cert = @@ -164,7 +164,7 @@ fn leader_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { mod tests { use crate::{ crypto::keyhash_256, genesis_values::GenesisValues, protocol_params::NonceHash, - serialization::Bech32WithHrp, BlockHash, BlockStatus, Era, + serialization::Bech32Conversion, BlockHash, BlockStatus, Era, }; use super::*; @@ -252,14 +252,12 @@ mod tests { }; let block_header = MultiEraHeader::decode(block_info.era as u8, None, &block_header_4556956).unwrap(); - let pool_id = Vec::::from_bech32_with_hrp( - "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", - "pool", - ) - .unwrap(); - let active_spos: HashMap = HashMap::from([( + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = HashMap::from([( pool_id.clone(), - keyhash_256(block_header.vrf_vkey().unwrap()), + VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), )]); let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); let result = validate_vrf_tpraos( @@ -310,14 +308,12 @@ mod tests { }; let block_header = MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); - let pool_id = Vec::::from_bech32_with_hrp( - "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", - "pool", - ) - .unwrap(); - let active_spos: HashMap = HashMap::from([( + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = HashMap::from([( pool_id.clone(), - keyhash_256(block_header.vrf_vkey().unwrap()), + VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), )]); let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); let result = validate_vrf_tpraos( @@ -368,12 +364,10 @@ mod tests { }; let block_header = MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); - let pool_id = Vec::::from_bech32_with_hrp( - "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", - "pool", - ) - .unwrap(); - let active_spos: HashMap = HashMap::from([]); + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = HashMap::from([]); let active_spdd = HashMap::from([]); let result = validate_vrf_tpraos( &block_info, @@ -427,13 +421,11 @@ mod tests { }; let block_header = MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); - let pool_id = Vec::::from_bech32_with_hrp( - "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", - "pool", - ) - .unwrap(); - let active_spos: HashMap = - HashMap::from([(pool_id.clone(), keyhash_256(&[0; 64]))]); + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = + HashMap::from([(pool_id.clone(), VrfKeyHash::from(keyhash_256(&[0; 64])))]); let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); let result = validate_vrf_tpraos( &block_info, @@ -452,8 +444,10 @@ mod tests { result.unwrap_err(), VrfValidationError::WrongLeaderVrfKey(WrongLeaderVrfKeyError { pool_id: pool_id.clone(), - registered_vrf_hash: keyhash_256(&[0; 64]), - header_vrf_hash: keyhash_256(block_header.vrf_vkey().unwrap()), + registered_vrf_key_hash: VrfKeyHash::from(keyhash_256(&[0; 64])), + header_vrf_key_hash: VrfKeyHash::from(keyhash_256( + block_header.vrf_vkey().unwrap() + )), }) ); } @@ -491,14 +485,12 @@ mod tests { }; let block_header = MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); - let pool_id = Vec::::from_bech32_with_hrp( - "pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy", - "pool", - ) - .unwrap(); - let active_spos: HashMap = HashMap::from([( + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = HashMap::from([( pool_id.clone(), - keyhash_256(block_header.vrf_vkey().unwrap()), + VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), )]); // small active stake (correct one is 75284250207839) let active_spdd = HashMap::from([(pool_id.clone(), 75284250207)]); diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index 1d3dccf2..edf3d425 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -1,8 +1,7 @@ -use crate::genesis_values::{GenDeleg, GenesisKey}; use crate::ouroboros::vrf; use crate::rational_number::RationalNumber; -use crate::PoolId; -use crate::{crypto::keyhash_256, protocol_params::Nonce, KeyHash, Slot}; +use crate::{crypto::keyhash_256, protocol_params::Nonce, Slot}; +use crate::{GenesisDelegate, GenesisKeyhash, PoolId, VrfKeyHash}; use anyhow::Result; use dashu_int::UBig; use pallas::ledger::primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; @@ -80,23 +79,23 @@ pub enum VrfValidationError { hex::encode(&header_vrf_hash), )] pub struct WrongGenesisLeaderVrfKeyError { - pub genesis_key: GenesisKey, - pub registered_vrf_hash: KeyHash, - pub header_vrf_hash: KeyHash, + pub genesis_key: GenesisKeyhash, + pub registered_vrf_hash: VrfKeyHash, + pub header_vrf_hash: VrfKeyHash, } impl WrongGenesisLeaderVrfKeyError { pub fn new( - genesis_key: &GenesisKey, - genesis_deleg: &GenDeleg, + genesis_key: &GenesisKeyhash, + genesis_deleg: &GenesisDelegate, vrf_vkey: &[u8], ) -> Result<(), Self> { - let header_vrf_hash = keyhash_256(vrf_vkey); - let registered_vrf_hash = genesis_deleg.vrf.to_vec(); + let header_vrf_hash = VrfKeyHash::from(keyhash_256(vrf_vkey)); + let registered_vrf_hash = &genesis_deleg.vrf; if !registered_vrf_hash.eq(&header_vrf_hash) { return Err(Self { genesis_key: genesis_key.clone(), - registered_vrf_hash, + registered_vrf_hash: registered_vrf_hash.clone(), header_vrf_hash, }); } @@ -108,29 +107,29 @@ impl WrongGenesisLeaderVrfKeyError { #[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[error( - "Wrong Leader VRF Key: Pool ID={}, Registered VRF Hash={}, Header VRF Hash={}", + "Wrong Leader VRF Key: Pool ID={}, Registered VRF Key Hash={}, Header VRF Key Hash={}", hex::encode(&pool_id), - hex::encode(®istered_vrf_hash), - hex::encode(&header_vrf_hash), + hex::encode(®istered_vrf_key_hash), + hex::encode(&header_vrf_key_hash), )] pub struct WrongLeaderVrfKeyError { pub pool_id: PoolId, - pub registered_vrf_hash: KeyHash, - pub header_vrf_hash: KeyHash, + pub registered_vrf_key_hash: VrfKeyHash, + pub header_vrf_key_hash: VrfKeyHash, } impl WrongLeaderVrfKeyError { pub fn new( pool_id: &PoolId, - registered_vrf_hash: &KeyHash, + registered_vrf_key_hash: &VrfKeyHash, vrf_vkey: &[u8], ) -> Result<(), Self> { - let header_vrf_hash = keyhash_256(vrf_vkey); - if !registered_vrf_hash.eq(&header_vrf_hash) { + let header_vrf_key_hash = VrfKeyHash::from(keyhash_256(vrf_vkey)); + if !registered_vrf_key_hash.eq(&header_vrf_key_hash) { return Err(Self { pool_id: pool_id.clone(), - registered_vrf_hash: registered_vrf_hash.clone(), - header_vrf_hash, + registered_vrf_key_hash: registered_vrf_key_hash.clone(), + header_vrf_key_hash, }); } Ok(()) diff --git a/common/src/types.rs b/common/src/types.rs index 02d987f5..4899d138 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -16,6 +16,7 @@ use hex::decode; use regex::Regex; use serde::{Deserialize, Serialize}; use serde_with::{hex::Hex, serde_as}; +use std::collections::BTreeMap; use std::{ cmp::Ordering, collections::{HashMap, HashSet}, @@ -1385,7 +1386,36 @@ pub struct GenesisDelegate { #[serde_as(as = "Hex")] pub delegate: Hash<28>, #[serde_as(as = "Hex")] - pub vrf: Vec, + pub vrf: VrfKeyHash, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GenesisDelegates(pub BTreeMap); + +impl TryFrom> for GenesisDelegates { + type Error = anyhow::Error; + fn try_from(entries: Vec<(&str, (&str, &str))>) -> Result { + Ok(GenesisDelegates( + entries + .into_iter() + .map(|(genesis_key_str, (delegate_str, vrf_str))| { + let genesis_key = GenesisKeyhash::from_str(genesis_key_str) + .map_err(|e| anyhow::anyhow!("Invalid genesis key hash: {}", e))?; + let delegate = Hash::<28>::from_str(delegate_str) + .map_err(|e| anyhow::anyhow!("Invalid genesis delegate: {}", e))?; + let vrf = VrfKeyHash::from_str(vrf_str) + .map_err(|e| anyhow::anyhow!("Invalid genesis VRF: {}", e))?; + Ok((genesis_key, GenesisDelegate { delegate, vrf })) + }) + .collect::>()?, + )) + } +} + +impl AsRef> for GenesisDelegates { + fn as_ref(&self) -> &BTreeMap { + &self.0 + } } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] diff --git a/modules/block_vrf_validator/src/snapshot.rs b/modules/block_vrf_validator/src/snapshot.rs index 70cbf9a0..f943d9c9 100644 --- a/modules/block_vrf_validator/src/snapshot.rs +++ b/modules/block_vrf_validator/src/snapshot.rs @@ -2,14 +2,14 @@ use std::collections::HashMap; use acropolis_common::{ messages::{SPOStakeDistributionMessage, SPOStateMessage}, - KeyHash, PoolId, + PoolId, VrfKeyHash, }; /// Epoch data for block vrf validation #[derive(Debug, Default)] pub struct Snapshot { /// Map of pool_id to its vrf_key_hash - pub active_spos: HashMap, + pub active_spos: HashMap, /// active stakes keyed by pool id pub active_stakes: HashMap, @@ -19,7 +19,7 @@ pub struct Snapshot { impl From<(&SPOStateMessage, &SPOStakeDistributionMessage)> for Snapshot { fn from((spo_state_msg, spdd_msg): (&SPOStateMessage, &SPOStakeDistributionMessage)) -> Self { - let active_spos: HashMap = spo_state_msg + let active_spos: HashMap = spo_state_msg .spos .iter() .map(|registration| { diff --git a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs index 5c9b9587..4bc3f9d5 100644 --- a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs +++ b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs @@ -2,13 +2,14 @@ //! Reads genesis files and outputs initial UTXO events use acropolis_common::{ - genesis_values::{GenesisDelegs, GenesisValues}, + genesis_values::GenesisValues, messages::{ CardanoMessage, GenesisCompleteMessage, GenesisUTxOsMessage, Message, PotDeltasMessage, UTXODeltasMessage, }, - Address, BlockHash, BlockInfo, BlockStatus, ByronAddress, Era, Lovelace, LovelaceDelta, Pot, - PotDelta, TxHash, TxIdentifier, TxOutRef, TxOutput, UTXODelta, UTxOIdentifier, Value, + Address, BlockHash, BlockInfo, BlockStatus, ByronAddress, Era, GenesisDelegates, Lovelace, + LovelaceDelta, Pot, PotDelta, TxHash, TxIdentifier, TxOutRef, TxOutput, UTXODelta, + UTxOIdentifier, Value, }; use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; @@ -205,22 +206,23 @@ impl GenesisBootstrapper { shelley_epoch: shelley_start_epoch, shelley_epoch_len: shelley_genesis.epoch_length.unwrap() as u64, shelley_genesis_hash, - genesis_delegs: GenesisDelegs::from( + genesis_delegs: GenesisDelegates::try_from( shelley_genesis .gen_delegs .unwrap() .iter() .map(|(key, value)| { ( - key.to_string(), + key.as_str(), ( - value.delegate.as_ref().unwrap().to_string(), - value.vrf.as_ref().unwrap().to_string(), + value.delegate.as_ref().unwrap().as_str(), + value.vrf.as_ref().unwrap().as_str(), ), ) }) - .collect::>(), - ), + .collect::>(), + ) + .unwrap(), }; // Send completion message diff --git a/modules/upstream_chain_fetcher/src/utils.rs b/modules/upstream_chain_fetcher/src/utils.rs index 2a0f2563..14b5075d 100644 --- a/modules/upstream_chain_fetcher/src/utils.rs +++ b/modules/upstream_chain_fetcher/src/utils.rs @@ -1,6 +1,7 @@ use crate::UpstreamCacheRecord; -use acropolis_common::genesis_values::{GenesisDelegs, GenesisValues}; +use acropolis_common::genesis_values::GenesisValues; use acropolis_common::messages::{CardanoMessage, Message}; +use acropolis_common::GenesisDelegates; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::Context; use config::Config; @@ -93,7 +94,7 @@ impl FetcherConfig { shelley_epoch_len, shelley_genesis_hash, // TODO: load genesis keys from config - genesis_delegs: GenesisDelegs::from(vec![]), + genesis_delegs: GenesisDelegates::try_from(vec![]).unwrap(), }) } From 7604607c326d0878f6c93aef54d6f1ccaa7db0dd Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Wed, 5 Nov 2025 15:52:23 +0100 Subject: [PATCH 28/36] fix: clippy --- common/src/ouroboros/overlay_schedule.rs | 28 ++++++++-------- common/src/ouroboros/praos.rs | 9 ++---- common/src/ouroboros/tpraos.rs | 23 +++++++------ common/src/ouroboros/vrf.rs | 2 -- common/src/ouroboros/vrf_validation.rs | 32 +++++++++---------- .../src/block_vrf_validator.rs | 5 +-- modules/block_vrf_validator/src/snapshot.rs | 11 ++----- modules/block_vrf_validator/src/state.rs | 4 +-- 8 files changed, 50 insertions(+), 64 deletions(-) diff --git a/common/src/ouroboros/overlay_schedule.rs b/common/src/ouroboros/overlay_schedule.rs index 4345f979..bcc8b10c 100644 --- a/common/src/ouroboros/overlay_schedule.rs +++ b/common/src/ouroboros/overlay_schedule.rs @@ -5,11 +5,9 @@ //! //! https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/Rules/Overlay.hs#L332 -use crate::{ - rational_number::RationalNumber, rest_helper::ToCheckedF64, GenesisDelegate, GenesisDelegates, - GenesisKeyhash, -}; +use crate::{rational_number::RationalNumber, GenesisDelegate, GenesisDelegates, GenesisKeyhash}; use anyhow::Result; +use num_traits::ToPrimitive; #[derive(Debug, Clone, PartialEq)] pub enum OBftSlot { @@ -33,9 +31,9 @@ pub enum OBftSlot { /// since this block is produced by genesis key (without "lottery") /// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L334 pub fn is_overlay_slot(epoch_slot: u64, decentralisation_param: &RationalNumber) -> Result { - let d = decentralisation_param.to_checked_f64("decentralisation_param").map_err(|e| { - anyhow::anyhow!("Failed to convert decentralisation parameter to f64: {}", e) - })?; + let d = decentralisation_param + .to_f64() + .ok_or_else(|| anyhow::anyhow!("Failed to convert decentralisation parameter to f64"))?; // step function: ceiling of (x * d) let step = |x: f64| (x * d).ceil() as i64; @@ -60,16 +58,16 @@ pub fn classify_overlay_slot( decentralisation_param: &RationalNumber, active_slots_coeff: &RationalNumber, ) -> Result { - let d = decentralisation_param.to_checked_f64("decentralisation_param").map_err(|e| { - anyhow::anyhow!("Failed to convert decentralisation parameter to f64: {}", e) - })?; + let d = decentralisation_param + .to_f64() + .ok_or_else(|| anyhow::anyhow!("Failed to convert decentralisation parameter to f64"))?; let position = (epoch_slot as f64 * d).ceil() as i64; // Calculate active slot coefficient inverse let asc_inv = active_slots_coeff .recip() - .to_checked_f64("active_slots_coeff") - .map_err(|e| anyhow::anyhow!("Failed to convert active slots coefficient to f64: {}", e))? + .to_f64() + .ok_or_else(|| anyhow::anyhow!("Failed to convert active slots coefficient to f64"))? .floor() as i64; let is_active = position % asc_inv == 0; @@ -79,7 +77,7 @@ pub fn classify_overlay_slot( // Get the element at index from the set let (key_hash, gen_deleg) = genesis_delegs.as_ref().iter().nth(genesis_idx).unwrap(); - Ok(OBftSlot::ActiveSlot(key_hash.clone(), gen_deleg.clone())) + Ok(OBftSlot::ActiveSlot(*key_hash, gen_deleg.clone())) } else { Ok(OBftSlot::NonActiveSlot) } @@ -149,8 +147,8 @@ mod tests { assert_eq!( obft_slot.unwrap(), OBftSlot::ActiveSlot( - genesis_delegs.as_ref().keys().nth(0).unwrap().clone(), - genesis_delegs.as_ref().values().nth(0).unwrap().clone() + *genesis_delegs.as_ref().keys().next().unwrap(), + genesis_delegs.as_ref().values().next().unwrap().clone() ) ); } diff --git a/common/src/ouroboros/praos.rs b/common/src/ouroboros/praos.rs index 84d556eb..0847833c 100644 --- a/common/src/ouroboros/praos.rs +++ b/common/src/ouroboros/praos.rs @@ -30,9 +30,7 @@ pub fn validate_vrf_praos<'a>( }; let pool_id = PoolId::from(keyhash_224(issuer_vkey)); let registered_vrf_key_hash = - active_spos.get(&pool_id).ok_or(VrfValidationError::UnknownPool { - pool_id: pool_id.clone(), - })?; + active_spos.get(&pool_id).ok_or(VrfValidationError::UnknownPool { pool_id })?; let pool_stake = active_spdd.get(&pool_id).unwrap_or(&0); let relative_stake = RationalNumber::new(*pool_stake, total_active_stake); @@ -128,10 +126,10 @@ mod tests { PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") .unwrap(); let active_spos = HashMap::from([( - pool_id.clone(), + pool_id, VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), )]); - let active_spdd = HashMap::from([(pool_id.clone(), 64590523391239)]); + let active_spdd = HashMap::from([(pool_id, 64590523391239)]); let result = validate_vrf_praos( &block_info, &block_header, @@ -142,7 +140,6 @@ mod tests { 25069171797357766, ) .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); - println!("{:?}", result); assert!(result.is_ok()); } } diff --git a/common/src/ouroboros/tpraos.rs b/common/src/ouroboros/tpraos.rs index e5d18f09..a77358d9 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/common/src/ouroboros/tpraos.rs @@ -15,6 +15,7 @@ use anyhow::Result; use pallas::ledger::primitives::VrfCert; use pallas::ledger::traverse::MultiEraHeader; +#[allow(clippy::too_many_arguments)] pub fn validate_vrf_tpraos<'a>( block_info: &'a BlockInfo, header: &'a MultiEraHeader, @@ -44,9 +45,7 @@ pub fn validate_vrf_tpraos<'a>( }; let pool_id = PoolId::from(keyhash_224(issuer_vkey)); let registered_vrf_key_hash = - active_spos.get(&pool_id).ok_or(VrfValidationError::UnknownPool { - pool_id: pool_id.clone(), - })?; + active_spos.get(&pool_id).ok_or(VrfValidationError::UnknownPool { pool_id })?; let pool_stake = active_spdd.get(&pool_id).unwrap_or(&0); let relative_stake = RationalNumber::new(*pool_stake, total_active_stake); @@ -256,10 +255,10 @@ mod tests { PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") .unwrap(); let active_spos: HashMap = HashMap::from([( - pool_id.clone(), + pool_id, VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), )]); - let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); + let active_spdd = HashMap::from([(pool_id, 75284250207839)]); let result = validate_vrf_tpraos( &block_info, &block_header, @@ -312,10 +311,10 @@ mod tests { PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") .unwrap(); let active_spos: HashMap = HashMap::from([( - pool_id.clone(), + pool_id, VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), )]); - let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); + let active_spdd = HashMap::from([(pool_id, 75284250207839)]); let result = validate_vrf_tpraos( &block_info, &block_header, @@ -425,8 +424,8 @@ mod tests { PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") .unwrap(); let active_spos: HashMap = - HashMap::from([(pool_id.clone(), VrfKeyHash::from(keyhash_256(&[0; 64])))]); - let active_spdd = HashMap::from([(pool_id.clone(), 75284250207839)]); + HashMap::from([(pool_id, VrfKeyHash::from(keyhash_256(&[0; 64])))]); + let active_spdd = HashMap::from([(pool_id, 75284250207839)]); let result = validate_vrf_tpraos( &block_info, &block_header, @@ -443,7 +442,7 @@ mod tests { assert_eq!( result.unwrap_err(), VrfValidationError::WrongLeaderVrfKey(WrongLeaderVrfKeyError { - pool_id: pool_id.clone(), + pool_id, registered_vrf_key_hash: VrfKeyHash::from(keyhash_256(&[0; 64])), header_vrf_key_hash: VrfKeyHash::from(keyhash_256( block_header.vrf_vkey().unwrap() @@ -489,11 +488,11 @@ mod tests { PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") .unwrap(); let active_spos: HashMap = HashMap::from([( - pool_id.clone(), + pool_id, VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), )]); // small active stake (correct one is 75284250207839) - let active_spdd = HashMap::from([(pool_id.clone(), 75284250207)]); + let active_spdd = HashMap::from([(pool_id, 75284250207)]); let result = validate_vrf_tpraos( &block_info, &block_header, diff --git a/common/src/ouroboros/vrf.rs b/common/src/ouroboros/vrf.rs index 3b508bb5..289950c6 100644 --- a/common/src/ouroboros/vrf.rs +++ b/common/src/ouroboros/vrf.rs @@ -51,8 +51,6 @@ impl TryFrom<&[u8]> for PublicKey { } /// A VRF input -/// - pub type VrfInputHash = [u8; 32]; pub type VrfProofHash = [u8; 64]; diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs index edf3d425..24d1192b 100644 --- a/common/src/ouroboros/vrf_validation.rs +++ b/common/src/ouroboros/vrf_validation.rs @@ -38,7 +38,7 @@ pub enum VrfValidationError { #[error("Praos Missing VRF Cert")] PraosMissingVrfCert, /// **Cause:** Block issuer's pool ID is not registered in current stake distribution - #[error("Unknown Pool: {}", hex::encode(&pool_id))] + #[error("Unknown Pool: {}", hex::encode(pool_id))] UnknownPool { pool_id: PoolId }, /// **Cause:** The VRF key hash in the block header doesn't match the VRF key /// registered with this stake pool in the ledger state for Overlay slot @@ -74,9 +74,9 @@ pub enum VrfValidationError { #[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[error( "Wrong Genesis Leader VRF Key: Genesis Key={}, Registered VRF Hash={}, Header VRF Hash={}", - hex::encode(&genesis_key), - hex::encode(®istered_vrf_hash), - hex::encode(&header_vrf_hash), + hex::encode(genesis_key), + hex::encode(registered_vrf_hash), + hex::encode(header_vrf_hash) )] pub struct WrongGenesisLeaderVrfKeyError { pub genesis_key: GenesisKeyhash, @@ -94,8 +94,8 @@ impl WrongGenesisLeaderVrfKeyError { let registered_vrf_hash = &genesis_deleg.vrf; if !registered_vrf_hash.eq(&header_vrf_hash) { return Err(Self { - genesis_key: genesis_key.clone(), - registered_vrf_hash: registered_vrf_hash.clone(), + genesis_key: *genesis_key, + registered_vrf_hash: *registered_vrf_hash, header_vrf_hash, }); } @@ -108,9 +108,9 @@ impl WrongGenesisLeaderVrfKeyError { #[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] #[error( "Wrong Leader VRF Key: Pool ID={}, Registered VRF Key Hash={}, Header VRF Key Hash={}", - hex::encode(&pool_id), - hex::encode(®istered_vrf_key_hash), - hex::encode(&header_vrf_key_hash), + hex::encode(pool_id), + hex::encode(registered_vrf_key_hash), + hex::encode(header_vrf_key_hash) )] pub struct WrongLeaderVrfKeyError { pub pool_id: PoolId, @@ -127,8 +127,8 @@ impl WrongLeaderVrfKeyError { let header_vrf_key_hash = VrfKeyHash::from(keyhash_256(vrf_vkey)); if !registered_vrf_key_hash.eq(&header_vrf_key_hash) { return Err(Self { - pool_id: pool_id.clone(), - registered_vrf_key_hash: registered_vrf_key_hash.clone(), + pool_id: *pool_id, + registered_vrf_key_hash: *registered_vrf_key_hash, header_vrf_key_hash, }); } @@ -221,8 +221,8 @@ pub enum PraosBadVrfProofError { #[error( "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", - hex::encode(&declared), - hex::encode(&computed), + hex::encode(declared), + hex::encode(computed) )] OutputMismatch { declared: Vec, @@ -302,7 +302,6 @@ impl PraosBadVrfProofError { /// `p` = `certNat` / `certNatMax`. (`certNat` is 64bytes for TPraos and 32bytes for Praos) /// `σ` (sigma) = pool's relative stake (pools active stake / total active stake) /// `f` = active slot coefficient (e.g., 0.05 = 5%) - /// let q = 1 - p and c = ln(1 - f) /// then p < 1 - (1 - f)^σ => 1 / (1 - p) < exp(-σ * c) => 1 / q < exp(-σ * c) /// Reference @@ -343,7 +342,6 @@ impl VrfLeaderValueTooBigError { /// Check that the certified input natural is valid for being slot leader. This means we check that /// p < 1 - (1 - f)^σ /// where p = certNat / certNatMax. (certNat is 64bytes for TPraos and 32bytes for Praos) - /// let q = 1 - p and c = ln(1 - f) /// then p < 1 - (1 - f)^σ => 1 / (1 - p) < exp(-σ * c) => 1 / q < exp(-σ * c) /// Reference @@ -369,8 +367,8 @@ pub enum BadVrfProofError { #[error( "Mismatch between the declared VRF proof hash ({}) and the computed one ({}).", - hex::encode(&declared), - hex::encode(&computed), + hex::encode(declared), + hex::encode(computed) )] ProofMismatch { // this is Proof Hash (sha512 hash) diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index b574f63f..b473ccce 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -50,6 +50,7 @@ const DEFAULT_SPDD_SUBSCRIBE_TOPIC: (&str, &str) = pub struct BlockVrfValidator; impl BlockVrfValidator { + #[allow(clippy::too_many_arguments)] async fn run( history: Arc>>, mut vrf_validation_publisher: VrfValidationPublisher, @@ -73,7 +74,7 @@ impl BlockVrfValidator { loop { // Get a mutable state - let mut state = history.lock().await.get_or_init_with(|| State::new()); + let mut state = history.lock().await.get_or_init_with(State::new); let mut current_block: Option = None; let (_, message) = blocks_subscription.read().await?; @@ -138,7 +139,7 @@ impl BlockVrfValidator { ) => { Self::check_sync(¤t_block, block_info_1); Self::check_sync(¤t_block, block_info_2); - state.handle_new_snapshot(&spo_state_msg, &spdd_msg); + state.handle_new_snapshot(spo_state_msg, spdd_msg); } _ => { error!("Unexpected message type: {spo_state_msg:?} or {spdd_msg:?}") diff --git a/modules/block_vrf_validator/src/snapshot.rs b/modules/block_vrf_validator/src/snapshot.rs index f943d9c9..72de3eb5 100644 --- a/modules/block_vrf_validator/src/snapshot.rs +++ b/modules/block_vrf_validator/src/snapshot.rs @@ -22,16 +22,11 @@ impl From<(&SPOStateMessage, &SPOStakeDistributionMessage)> for Snapshot { let active_spos: HashMap = spo_state_msg .spos .iter() - .map(|registration| { - ( - registration.operator.clone(), - registration.vrf_key_hash.clone(), - ) - }) + .map(|registration| (registration.operator, registration.vrf_key_hash)) .collect(); let active_stakes: HashMap = - spdd_msg.spos.iter().map(|(pool_id, stake)| (pool_id.clone(), stake.live)).collect(); - let total_active_stakes = active_stakes.iter().map(|(_, stake)| stake).sum(); + spdd_msg.spos.iter().map(|(pool_id, stake)| (*pool_id, stake.live)).collect(); + let total_active_stakes = active_stakes.values().sum(); Self { active_spos, active_stakes, diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index fe23963d..236db52b 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -112,7 +112,7 @@ impl State { let result = ouroboros::tpraos::validate_vrf_tpraos( block_info, header, - &epoch_nonce, + epoch_nonce, &genesis.genesis_delegs, praos_params, &self.epoch_snapshots.set.active_spos, @@ -126,7 +126,7 @@ impl State { let result = ouroboros::praos::validate_vrf_praos( block_info, header, - &epoch_nonce, + epoch_nonce, praos_params, &self.epoch_snapshots.set.active_spos, &self.epoch_snapshots.set.active_stakes, From 5fd8bf046b2b9e4b91b66afcdd14fe736319f869 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 7 Nov 2025 07:39:36 +0100 Subject: [PATCH 29/36] fix: move ouroboros to block_vrf_validator --- Cargo.lock | 10 +- common/Cargo.toml | 10 +- common/src/lib.rs | 1 - common/src/messages.rs | 12 +- common/src/ouroboros/vrf_validation.rs | 443 ------------------ common/src/protocol_params.rs | 2 +- common/src/rational_number.rs | 2 +- common/src/types.rs | 30 +- common/src/validation.rs | 193 +++++++- modules/block_vrf_validator/Cargo.toml | 8 + .../src/block_vrf_validator.rs | 73 +-- .../src/ouroboros/data/4490511.cbor | 0 .../src/ouroboros/data/4556956.cbor | 0 .../src/ouroboros/data/4576496.cbor | 0 .../src/ouroboros/data/7854823.cbor | 0 .../block_vrf_validator}/src/ouroboros/mod.rs | 0 .../src/ouroboros/overlay_schedule.rs | 6 +- .../src/ouroboros/praos.rs | 66 +-- .../src/ouroboros/tpraos.rs | 138 +++--- .../block_vrf_validator}/src/ouroboros/vrf.rs | 4 +- .../src/ouroboros/vrf_validation.rs | 218 +++++++++ modules/block_vrf_validator/src/state.rs | 64 ++- .../src/vrf_validation_publisher.rs | 4 +- .../epochs_state/src/epoch_nonce_publisher.rs | 44 -- modules/epochs_state/src/epochs_state.rs | 57 +-- modules/epochs_state/src/state.rs | 20 +- 26 files changed, 661 insertions(+), 744 deletions(-) delete mode 100644 common/src/ouroboros/vrf_validation.rs rename {common => modules/block_vrf_validator}/src/ouroboros/data/4490511.cbor (100%) rename {common => modules/block_vrf_validator}/src/ouroboros/data/4556956.cbor (100%) rename {common => modules/block_vrf_validator}/src/ouroboros/data/4576496.cbor (100%) rename {common => modules/block_vrf_validator}/src/ouroboros/data/7854823.cbor (100%) rename {common => modules/block_vrf_validator}/src/ouroboros/mod.rs (100%) rename {common => modules/block_vrf_validator}/src/ouroboros/overlay_schedule.rs (97%) rename {common => modules/block_vrf_validator}/src/ouroboros/praos.rs (70%) rename {common => modules/block_vrf_validator}/src/ouroboros/tpraos.rs (80%) rename {common => modules/block_vrf_validator}/src/ouroboros/vrf.rs (98%) create mode 100644 modules/block_vrf_validator/src/ouroboros/vrf_validation.rs delete mode 100644 modules/epochs_state/src/epoch_nonce_publisher.rs diff --git a/Cargo.lock b/Cargo.lock index 38986408..d6f3f452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,7 +35,6 @@ dependencies = [ "crc", "cryptoxide 0.5.1", "dashmap", - "dashu-int", "fraction", "futures", "gcd", @@ -46,8 +45,6 @@ dependencies = [ "minicbor 0.26.5", "num-rational", "num-traits", - "pallas 0.33.0", - "pallas-math", "rayon", "regex", "serde", @@ -58,7 +55,6 @@ dependencies = [ "thiserror 2.0.17", "tokio", "tracing", - "vrf_dalek", ] [[package]] @@ -136,15 +132,21 @@ version = "0.1.0" dependencies = [ "acropolis_common", "anyhow", + "blake2 0.10.6", "caryatid_sdk", "config", + "dashu-int", "hex", "imbl", + "num-traits", "pallas 0.33.0", + "pallas-math", "serde", "serde_json", + "thiserror 2.0.17", "tokio", "tracing", + "vrf_dalek", ] [[package]] diff --git a/common/Cargo.toml b/common/Cargo.toml index 3e1e93f8..2892e8e8 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -45,14 +45,8 @@ rayon = "1.11.0" cryptoxide = "0.5.1" thiserror = "2.0.17" sha2 = "0.10.8" -caryatid_process.workspace = true -config.workspace = true -pallas = { workspace = true } -pallas-math = { workspace = true } - -# The vrf crate has not been fully tested in production environments and still has several upstream issues that are open PRs but not merged yet. -vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } -dashu-int = "0.4.1" +caryatid_process = { workspace = true } +config = { workspace = true } [lib] crate-type = ["rlib"] diff --git a/common/src/lib.rs b/common/src/lib.rs index 900cbc1a..0ca5a58a 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -10,7 +10,6 @@ pub mod hash; pub mod ledger_state; pub mod math; pub mod messages; -pub mod ouroboros; pub mod params; pub mod protocol_params; pub mod queries; diff --git a/common/src/messages.rs b/common/src/messages.rs index 384a46a2..50df2d1f 100644 --- a/common/src/messages.rs +++ b/common/src/messages.rs @@ -6,7 +6,7 @@ use crate::commands::transactions::{TransactionsCommand, TransactionsCommandResponse}; use crate::genesis_values::GenesisValues; use crate::ledger_state::SPOState; -use crate::protocol_params::{Nonce, NonceHash, ProtocolParams}; +use crate::protocol_params::{Nonce, ProtocolParams}; use crate::queries::parameters::{ParametersStateQuery, ParametersStateQueryResponse}; use crate::queries::spdd::{SPDDStateQuery, SPDDStateQueryResponse}; use crate::queries::utxos::{UTxOStateQuery, UTxOStateQueryResponse}; @@ -182,11 +182,6 @@ pub struct EpochActivityMessage { pub spo_blocks: Vec<(PoolId, usize)>, /// Nonce - pub nonce: Option, -} - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct EpochNonceMessage { pub nonce: Option, } @@ -298,9 +293,8 @@ pub enum CardanoMessage { PotDeltas(PotDeltasMessage), // Changes to pot balances BlockInfoMessage(BlockTxsMessage), // Transaction Info (total count, total output, total fees in a block) EpochActivity(EpochActivityMessage), // Total fees and VRF keys for an epoch - EpochNonce(EpochNonceMessage), // Epoch Nonce for Epoch N (published after the first block of Epoch N) - DRepState(DRepStateMessage), // Active DReps at epoch end - SPOState(SPOStateMessage), // Active SPOs at epoch end + DRepState(DRepStateMessage), // Active DReps at epoch end + SPOState(SPOStateMessage), // Active SPOs at epoch end GovernanceProcedures(GovernanceProceduresMessage), // Governance procedures received // Protocol Parameters diff --git a/common/src/ouroboros/vrf_validation.rs b/common/src/ouroboros/vrf_validation.rs deleted file mode 100644 index 24d1192b..00000000 --- a/common/src/ouroboros/vrf_validation.rs +++ /dev/null @@ -1,443 +0,0 @@ -use crate::ouroboros::vrf; -use crate::rational_number::RationalNumber; -use crate::{crypto::keyhash_256, protocol_params::Nonce, Slot}; -use crate::{GenesisDelegate, GenesisKeyhash, PoolId, VrfKeyHash}; -use anyhow::Result; -use dashu_int::UBig; -use pallas::ledger::primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; -use pallas_math::math::{ExpOrdering, FixedDecimal, FixedPrecision}; -use std::array::TryFromSliceError; -use thiserror::Error; - -/// Reference -/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 -#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] -pub enum VrfValidationError { - /// **Cause:** The Shelley protocol parameters used to validate the block, - #[error("{0}")] - InvalidShelleyParams(String), - /// **Cause**: The epoch nonce are not set - #[error("Epoch Nonce are missing")] - MissingEpochNonce, - /// **Cause:** The Issuer Key is missing from the block header - #[error("Missing Issuer Key")] - MissingIssuerKey, - /// **Cause:** The VRF key is missing from the block header - #[error("Missing VRF Key")] - MissingVrfVkey, - /// **Cause:** The VRF Cert is missing from the block header in TPraos Protocol - #[error("TPraos Missing Nonce VRF Cert")] - TPraosMissingNonceVrfCert, - /// **Cause:** The Leader VRF Cert is missing from the block header in TPraos Protocol - #[error("TPraos Missing Leader VRF Cert")] - TPraosMissingLeaderVrfCert, - /// **Cause:** The VRF output is missing from the block header in Praos Protocol - #[error("Praos Missing Leader VRF Output")] - PraosMissingLeaderVrfOutput, - /// **Cause:** The VRF Cert is missing from the block header in Praos Protocol - #[error("Praos Missing VRF Cert")] - PraosMissingVrfCert, - /// **Cause:** Block issuer's pool ID is not registered in current stake distribution - #[error("Unknown Pool: {}", hex::encode(pool_id))] - UnknownPool { pool_id: PoolId }, - /// **Cause:** The VRF key hash in the block header doesn't match the VRF key - /// registered with this stake pool in the ledger state for Overlay slot - #[error("{0}")] - WrongGenesisLeaderVrfKey(#[from] WrongGenesisLeaderVrfKeyError), - /// **Cause:** The VRF key hash in the block header doesn't match the VRF key - /// registered with this stake pool in the ledger state - #[error("{0}")] - WrongLeaderVrfKey(#[from] WrongLeaderVrfKeyError), - /// VRF nonce proof verification failed (TPraos rho - nonce proof) - /// **Cause:** The (rho - nonce) VRF proof failed verification - #[error("{0}")] - TPraosBadNonceVrfProof(#[from] TPraosBadNonceVrfProofError), - /// VRF leader proof verification failed (TPraos y - leader proof) - /// **Cause:** The (y - leader) VRF proof failed verification - #[error("{0}")] - TPraosBadLeaderVrfProof(#[from] TPraosBadLeaderVrfProofError), - /// VRF proof cryptographic verification failed (Praos single proof) - /// **Cause:** The cryptographic VRF proof is invalid - #[error("{0}")] - PraosBadVrfProof(#[from] PraosBadVrfProofError), - /// **Cause:** The VRF output is too large for this pool's stake. - /// The pool lost the slot lottery - #[error("VRF Leader Value Too Big")] - VrfLeaderValueTooBig(#[from] VrfLeaderValueTooBigError), - /// **Cause:** Some data has incorrect bytes - #[error("TryFromSlice: {0}")] - TryFromSlice(String), -} - -// ------------------------------------------------------------ WrongGenesisLeaderVrfKeyError - -#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -#[error( - "Wrong Genesis Leader VRF Key: Genesis Key={}, Registered VRF Hash={}, Header VRF Hash={}", - hex::encode(genesis_key), - hex::encode(registered_vrf_hash), - hex::encode(header_vrf_hash) -)] -pub struct WrongGenesisLeaderVrfKeyError { - pub genesis_key: GenesisKeyhash, - pub registered_vrf_hash: VrfKeyHash, - pub header_vrf_hash: VrfKeyHash, -} - -impl WrongGenesisLeaderVrfKeyError { - pub fn new( - genesis_key: &GenesisKeyhash, - genesis_deleg: &GenesisDelegate, - vrf_vkey: &[u8], - ) -> Result<(), Self> { - let header_vrf_hash = VrfKeyHash::from(keyhash_256(vrf_vkey)); - let registered_vrf_hash = &genesis_deleg.vrf; - if !registered_vrf_hash.eq(&header_vrf_hash) { - return Err(Self { - genesis_key: *genesis_key, - registered_vrf_hash: *registered_vrf_hash, - header_vrf_hash, - }); - } - Ok(()) - } -} - -// ------------------------------------------------------------ WrongLeaderVrfKeyError - -#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -#[error( - "Wrong Leader VRF Key: Pool ID={}, Registered VRF Key Hash={}, Header VRF Key Hash={}", - hex::encode(pool_id), - hex::encode(registered_vrf_key_hash), - hex::encode(header_vrf_key_hash) -)] -pub struct WrongLeaderVrfKeyError { - pub pool_id: PoolId, - pub registered_vrf_key_hash: VrfKeyHash, - pub header_vrf_key_hash: VrfKeyHash, -} - -impl WrongLeaderVrfKeyError { - pub fn new( - pool_id: &PoolId, - registered_vrf_key_hash: &VrfKeyHash, - vrf_vkey: &[u8], - ) -> Result<(), Self> { - let header_vrf_key_hash = VrfKeyHash::from(keyhash_256(vrf_vkey)); - if !registered_vrf_key_hash.eq(&header_vrf_key_hash) { - return Err(Self { - pool_id: *pool_id, - registered_vrf_key_hash: *registered_vrf_key_hash, - header_vrf_key_hash, - }); - } - Ok(()) - } -} - -// ------------------------------------------------------------ TPraosBadNonceVrfProofError - -#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum TPraosBadNonceVrfProofError { - #[error("Bad Nonce VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] - BadVrfProof(Slot, Nonce, BadVrfProofError), -} - -impl TPraosBadNonceVrfProofError { - /// Validate the VRF output from the block and its corresponding hash. - /// in TPraos Protocol for Nonce - pub fn new( - absolute_slot: Slot, - epoch_nonce: &Nonce, - // Declared VRF Public Key from block header - leader_public_key: &vrf::PublicKey, - // Declared VRF Proof Hash from block header (sha512 hash) - unsafe_vrf_proof_hash: &[u8], - // Declared VRF Proof from block header (80 bytes) - unsafe_vrf_proof: &[u8], - ) -> Result<(), Self> { - // For nonce proof validation - let seed_eta = Nonce::seed_eta(); - // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L365 - let rho_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_eta); - - // Verify the Nonce VRF proof - BadVrfProofError::new( - &rho_seed, - leader_public_key, - unsafe_vrf_proof_hash, - unsafe_vrf_proof, - ) - .map_err(|e| Self::BadVrfProof(absolute_slot, epoch_nonce.clone(), e))?; - Ok(()) - } -} - -// ------------------------------------------------------------ TPraosBadLeaderVrfProofError - -#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum TPraosBadLeaderVrfProofError { - #[error("Bad Leader VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] - BadVrfProof(Slot, Nonce, BadVrfProofError), -} - -impl TPraosBadLeaderVrfProofError { - /// Validate the VRF output from the block and its corresponding hash. - /// in TPraos Protocol for Leader - pub fn new( - absolute_slot: Slot, - epoch_nonce: &Nonce, - // Declared VRF Public Key from block header - leader_public_key: &vrf::PublicKey, - // Declared VRF Proof Hash from block header (sha512 hash) - unsafe_vrf_proof_hash: &[u8], - // Declared VRF Proof from block header (80 bytes) - unsafe_vrf_proof: &[u8], - ) -> Result<(), Self> { - // For leader proof validation - let seed_l = Nonce::seed_l(); - // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L366 - let y_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_l); - - // Verify the Leader VRF proof - BadVrfProofError::new( - &y_seed, - leader_public_key, - unsafe_vrf_proof_hash, - unsafe_vrf_proof, - ) - .map_err(|e| Self::BadVrfProof(absolute_slot, epoch_nonce.clone(), e))?; - Ok(()) - } -} - -// ------------------------------------------------------------ PraosBadVrfProofError - -#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub enum PraosBadVrfProofError { - #[error("Bad VRF proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] - BadVrfProof(Slot, Nonce, BadVrfProofError), - - #[error( - "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", - hex::encode(declared), - hex::encode(computed) - )] - OutputMismatch { - declared: Vec, - computed: Vec, - }, -} - -impl PartialEq for PraosBadVrfProofError { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::BadVrfProof(l0, l1, l2), Self::BadVrfProof(r0, r1, r2)) => { - l0 == r0 && l1 == r1 && l2 == r2 - } - ( - Self::OutputMismatch { - declared: l_declared, - computed: l_computed, - }, - Self::OutputMismatch { - declared: r_declared, - computed: r_computed, - }, - ) => l_declared == r_declared && l_computed == r_computed, - _ => false, - } - } -} - -impl PraosBadVrfProofError { - /// Validate the VRF output from the block and its corresponding hash. - /// in Praos Protocol - pub fn new( - absolute_slot: Slot, - epoch_nonce: &Nonce, - leader_vrf_output: &[u8], - // Declared VRF Public Key from block header - leader_public_key: &vrf::PublicKey, - // Declared VRF Proof Hash from block header (sha512 hash) - unsafe_vrf_proof_hash: &[u8], - // Declared VRF Proof from block header (80 bytes) - unsafe_vrf_proof: &[u8], - ) -> Result<(), Self> { - let input = vrf::VrfInput::mk_vrf_input(absolute_slot, epoch_nonce); - - // Verify the VRF proof - BadVrfProofError::new( - &input, - leader_public_key, - unsafe_vrf_proof_hash, - unsafe_vrf_proof, - ) - .map_err(|e| Self::BadVrfProof(absolute_slot, epoch_nonce.clone(), e))?; - - // The proof was valid. Make sure that the leader's output matches what was in the block - let calculated_leader_vrf_output = - derive_tagged_vrf_output(unsafe_vrf_proof_hash, VrfDerivation::Leader); - if calculated_leader_vrf_output.as_slice() != leader_vrf_output { - return Err(Self::OutputMismatch { - declared: leader_vrf_output.to_vec(), - computed: calculated_leader_vrf_output, - }); - } - - Ok(()) - } -} - -// ------------------------------------------------------------ VrfLeaderValueTooBigError - -/// Reference -/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L430 -/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L527 -/// -/// Check that the certified input natural is valid for being slot leader. This means we check that -/// p < 1 - (1 - f)^σ -/// **Variables** -/// `p` = `certNat` / `certNatMax`. (`certNat` is 64bytes for TPraos and 32bytes for Praos) -/// `σ` (sigma) = pool's relative stake (pools active stake / total active stake) -/// `f` = active slot coefficient (e.g., 0.05 = 5%) -/// let q = 1 - p and c = ln(1 - f) -/// then p < 1 - (1 - f)^σ => 1 / (1 - p) < exp(-σ * c) => 1 / q < exp(-σ * c) -/// Reference -/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L331 -/// -#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -pub enum VrfLeaderValueTooBigError { - #[error("VRF Leader Value Too Big")] - VrfLeaderValueTooBig, -} - -impl VrfLeaderValueTooBigError { - pub fn new( - leader_vrf_output: &[u8], - leader_relative_stake: &RationalNumber, - active_slot_coeff: &RationalNumber, - ) -> Result<(), Self> { - let certified_leader_vrf = &FixedDecimal::from(leader_vrf_output); - let output_size_bits = leader_vrf_output.len() * 8; - let cert_nat_max = FixedDecimal::from(UBig::ONE << output_size_bits); - let leader_relative_stake = FixedDecimal::from(UBig::from(*leader_relative_stake.numer())) - / FixedDecimal::from(UBig::from(*leader_relative_stake.denom())); - let active_slot_coeff = FixedDecimal::from(UBig::from(*active_slot_coeff.numer())) - / FixedDecimal::from(UBig::from(*active_slot_coeff.denom())); - - let denominator = &cert_nat_max - certified_leader_vrf; - let recip_q = &cert_nat_max / &denominator; - let c = (&FixedDecimal::from(1u64) - &active_slot_coeff).ln(); - let x = -(leader_relative_stake * c); - let ordering = x.exp_cmp(1000, 3, &recip_q); - match ordering.estimation { - ExpOrdering::LT => Ok(()), - ExpOrdering::GT | ExpOrdering::UNKNOWN => Err(Self::VrfLeaderValueTooBig), - } - } -} - -/// Check that the certified input natural is valid for being slot leader. This means we check that -/// p < 1 - (1 - f)^σ -/// where p = certNat / certNatMax. (certNat is 64bytes for TPraos and 32bytes for Praos) -/// let q = 1 - p and c = ln(1 - f) -/// then p < 1 - (1 - f)^σ => 1 / (1 - p) < exp(-σ * c) => 1 / q < exp(-σ * c) -/// Reference -/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L331 -/// -/// NOTE: -/// We are using Pallas Math Library -/// - -// ------------------------------------------------------------ BadVrfProofError - -#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] -pub enum BadVrfProofError { - #[error("Malformed VRF proof: {0}")] - MalformedProof(#[from] vrf::ProofFromBytesError), - - #[error("Invalid VRF proof: {0}")] - /// (error, vrf_input_hash, vrf_public_key_hash) - InvalidProof(vrf::ProofVerifyError, Vec, Vec), - - #[error("could not convert slice to array")] - TryFromSliceError, - - #[error( - "Mismatch between the declared VRF proof hash ({}) and the computed one ({}).", - hex::encode(declared), - hex::encode(computed) - )] - ProofMismatch { - // this is Proof Hash (sha512 hash) - declared: Vec, - computed: Vec, - }, -} - -impl From for BadVrfProofError { - fn from(_: TryFromSliceError) -> Self { - Self::TryFromSliceError - } -} - -impl PartialEq for BadVrfProofError { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::MalformedProof(l0), Self::MalformedProof(r0)) => l0 == r0, - (Self::InvalidProof(l0, l1, l2), Self::InvalidProof(r0, r1, r2)) => { - l0 == r0 && l1 == r1 && l2 == r2 - } - (Self::TryFromSliceError, Self::TryFromSliceError) => true, - ( - Self::ProofMismatch { - declared: l_declared, - computed: l_computed, - }, - Self::ProofMismatch { - declared: r_declared, - computed: r_computed, - }, - ) => l_declared == r_declared && l_computed == r_computed, - _ => false, - } - } -} - -impl BadVrfProofError { - /// Validate the VRF proof - pub fn new( - vrf_input: &vrf::VrfInput, - // Declared VRF Public Key from block header - vrf_public_key: &vrf::PublicKey, - // Declared VRF Proof Hash from block header (sha512 hash) - unsafe_vrf_proof_hash: &[u8], - // Declared VRF Proof from block header (80 bytes) - unsafe_vrf_proof: &[u8], - ) -> Result<(), Self> { - let vrf_proof: [u8; vrf::Proof::SIZE] = unsafe_vrf_proof.try_into()?; - let vrf_proof_hash: [u8; vrf::Proof::HASH_SIZE] = unsafe_vrf_proof_hash.try_into()?; - let vrf_proof = vrf::Proof::try_from(&vrf_proof)?; - - // Verify the VRF proof - let proof_hash = vrf_proof.verify(vrf_public_key, vrf_input).map_err(|e| { - Self::InvalidProof( - e, - vrf_input.as_ref().to_vec(), - vrf_public_key.as_ref().to_vec(), - ) - })?; - if !proof_hash.as_slice().eq(&vrf_proof_hash) { - return Err(Self::ProofMismatch { - declared: vrf_proof_hash.to_vec(), - computed: proof_hash.to_vec(), - }); - } - - Ok(()) - } -} - -pub type VrfValidation<'a> = Box Result<(), VrfValidationError> + Send + Sync + 'a>; diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index 76574b7f..e713d38c 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -302,7 +302,7 @@ impl From for Nonce { impl Nonce { pub fn from_number(n: u64) -> Self { let mut hasher = Blake2b::::new(); - hasher.update(&n.to_be_bytes()); + hasher.update(n.to_be_bytes()); let hash: NonceHash = hasher.finalize().into(); Self::from(hash) } diff --git a/common/src/rational_number.rs b/common/src/rational_number.rs index 9196ce30..5c825bfc 100644 --- a/common/src/rational_number.rs +++ b/common/src/rational_number.rs @@ -159,7 +159,7 @@ mod tests { fn test_chameleon_serialization() -> Result<()> { for n in 0..=1000 { let ch = [ - &ChameleonFraction::Float(f32::from_str(&format!("0.{:03}", n))?), + &ChameleonFraction::Float(f32::from_str(&format!("0.{n:03}"))?), &ChameleonFraction::Fraction { numerator: n, denominator: 1000, diff --git a/common/src/types.rs b/common/src/types.rs index 4899d138..9adc0cd5 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -116,7 +116,7 @@ impl TryFrom for Era { impl Display for Era { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{:?}", self) + write!(f, "{self:?}") } } @@ -695,14 +695,10 @@ impl Credential { let key_hash = decode(hex_str.to_owned().into_bytes())?; if key_hash.len() != 28 { Err(anyhow!( - "Invalid hash length for {:?}, expected 28 bytes", - hex_str + "Invalid hash length for {hex_str:?}, expected 28 bytes" )) } else { - key_hash - .as_slice() - .try_into() - .map_err(|e| anyhow!("Failed to convert to KeyHash {}", e)) + key_hash.as_slice().try_into().map_err(|e| anyhow!("Failed to convert to KeyHash {e}")) } } @@ -713,16 +709,15 @@ impl Credential { Ok(Credential::AddrKeyHash(Self::hex_string_to_hash(hash)?)) } else { Err(anyhow!( - "Incorrect credential {}, expected scriptHash- or keyHash- prefix", - credential + "Incorrect credential {credential}, expected scriptHash- or keyHash- prefix" )) } } pub fn to_json_string(&self) -> String { match self { - Self::ScriptHash(hash) => format!("scriptHash-{}", hash), - Self::AddrKeyHash(hash) => format!("keyHash-{}", hash), + Self::ScriptHash(hash) => format!("scriptHash-{hash}"), + Self::AddrKeyHash(hash) => format!("keyHash-{hash}"), } } @@ -748,8 +743,7 @@ impl Credential { "drep" => Ok(Credential::AddrKeyHash(hash)), "drep_script" => Ok(Credential::ScriptHash(hash)), _ => Err(anyhow!( - "Invalid HRP for DRep Bech32, expected 'drep' or 'drep_script', got '{}'", - hrp + "Invalid HRP for DRep Bech32, expected 'drep' or 'drep_script', got '{hrp}'" )), } } @@ -1256,7 +1250,7 @@ impl GovActionId { let (hrp, data) = bech32::decode(bech32_str)?; if hrp != Hrp::parse("gov_action")? { - return Err(anyhow!("Invalid HRP, expected 'gov_action', got: {}", hrp)); + return Err(anyhow!("Invalid HRP, expected 'gov_action', got: {hrp}")); } if data.len() < 33 { @@ -1288,7 +1282,7 @@ impl GovActionId { impl Display for GovActionId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self.to_bech32() { - Ok(s) => write!(f, "{}", s), + Ok(s) => write!(f, "{s}"), Err(e) => { tracing::error!("GovActionId to_bech32 failed: {:?}", e); write!(f, "") @@ -1400,11 +1394,11 @@ impl TryFrom> for GenesisDelegates { .into_iter() .map(|(genesis_key_str, (delegate_str, vrf_str))| { let genesis_key = GenesisKeyhash::from_str(genesis_key_str) - .map_err(|e| anyhow::anyhow!("Invalid genesis key hash: {}", e))?; + .map_err(|e| anyhow::anyhow!("Invalid genesis key hash: {e}"))?; let delegate = Hash::<28>::from_str(delegate_str) - .map_err(|e| anyhow::anyhow!("Invalid genesis delegate: {}", e))?; + .map_err(|e| anyhow::anyhow!("Invalid genesis delegate: {e}"))?; let vrf = VrfKeyHash::from_str(vrf_str) - .map_err(|e| anyhow::anyhow!("Invalid genesis VRF: {}", e))?; + .map_err(|e| anyhow::anyhow!("Invalid genesis VRF: {e}"))?; Ok((genesis_key, GenesisDelegate { delegate, vrf })) }) .collect::>()?, diff --git a/common/src/validation.rs b/common/src/validation.rs index 01e7b881..2e2e2bd1 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -3,9 +3,11 @@ // We don't use these types in the acropolis_common crate itself #![allow(dead_code)] +use std::array::TryFromSliceError; + use thiserror::Error; -use crate::ouroboros::vrf_validation::VrfValidationError; +use crate::{protocol_params::Nonce, GenesisKeyhash, PoolId, Slot, VrfKeyHash}; /// Validation error #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Error)] @@ -29,3 +31,192 @@ pub enum ValidationStatus { /// Error NoGo(ValidationError), } + +/// Reference +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L342 +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize, PartialEq)] +pub enum VrfValidationError { + /// **Cause:** Block issuer's pool ID is not registered in current stake distribution + #[error("Unknown Pool: {}", hex::encode(pool_id))] + UnknownPool { pool_id: PoolId }, + /// **Cause:** The VRF key hash in the block header doesn't match the VRF key + /// registered with this stake pool in the ledger state for Overlay slot + #[error("{0}")] + WrongGenesisLeaderVrfKey(#[from] WrongGenesisLeaderVrfKeyError), + /// **Cause:** The VRF key hash in the block header doesn't match the VRF key + /// registered with this stake pool in the ledger state + #[error("{0}")] + WrongLeaderVrfKey(#[from] WrongLeaderVrfKeyError), + /// VRF nonce proof verification failed (TPraos rho - nonce proof) + /// **Cause:** The (rho - nonce) VRF proof failed verification + #[error("{0}")] + TPraosBadNonceVrfProof(#[from] TPraosBadNonceVrfProofError), + /// VRF leader proof verification failed (TPraos y - leader proof) + /// **Cause:** The (y - leader) VRF proof failed verification + #[error("{0}")] + TPraosBadLeaderVrfProof(#[from] TPraosBadLeaderVrfProofError), + /// VRF proof cryptographic verification failed (Praos single proof) + /// **Cause:** The cryptographic VRF proof is invalid + #[error("{0}")] + PraosBadVrfProof(#[from] PraosBadVrfProofError), + /// **Cause:** The VRF output is too large for this pool's stake. + /// The pool lost the slot lottery + #[error("VRF Leader Value Too Big")] + VrfLeaderValueTooBig(#[from] VrfLeaderValueTooBigError), + /// **Cause:** Some data has incorrect bytes + #[error("TryFromSlice: {0}")] + TryFromSlice(String), + /// **Cause:** Other errors (e.g. Invalid shelley params, praos params, missing data) + #[error("{0}")] + Other(String), +} + +/// Validation function for VRF +pub type VrfValidation<'a> = Box Result<(), VrfValidationError> + Send + Sync + 'a>; + +// ------------------------------------------------------------ WrongGenesisLeaderVrfKeyError + +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[error( + "Wrong Genesis Leader VRF Key: Genesis Key={}, Registered VRF Hash={}, Header VRF Hash={}", + hex::encode(genesis_key), + hex::encode(registered_vrf_hash), + hex::encode(header_vrf_hash) +)] +pub struct WrongGenesisLeaderVrfKeyError { + pub genesis_key: GenesisKeyhash, + pub registered_vrf_hash: VrfKeyHash, + pub header_vrf_hash: VrfKeyHash, +} + +// ------------------------------------------------------------ WrongLeaderVrfKeyError + +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +#[error( + "Wrong Leader VRF Key: Pool ID={}, Registered VRF Key Hash={}, Header VRF Key Hash={}", + hex::encode(pool_id), + hex::encode(registered_vrf_key_hash), + hex::encode(header_vrf_key_hash) +)] +pub struct WrongLeaderVrfKeyError { + pub pool_id: PoolId, + pub registered_vrf_key_hash: VrfKeyHash, + pub header_vrf_key_hash: VrfKeyHash, +} + +// ------------------------------------------------------------ TPraosBadNonceVrfProofError + +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum TPraosBadNonceVrfProofError { + #[error("Bad Nonce VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] + BadVrfProof(Slot, Nonce, BadVrfProofError), +} + +// ------------------------------------------------------------ TPraosBadLeaderVrfProofError + +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum TPraosBadLeaderVrfProofError { + #[error("Bad Leader VRF Proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] + BadVrfProof(Slot, Nonce, BadVrfProofError), +} + +// ------------------------------------------------------------ PraosBadVrfProofError + +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum PraosBadVrfProofError { + #[error("Bad VRF proof: Slot={0}, Epoch Nonce={1}, Bad VRF Proof={2}")] + BadVrfProof(Slot, Nonce, BadVrfProofError), + + #[error( + "Mismatch between the declared VRF output in block ({}) and the computed one ({}).", + hex::encode(declared), + hex::encode(computed) + )] + OutputMismatch { + declared: Vec, + computed: Vec, + }, +} + +impl PartialEq for PraosBadVrfProofError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::BadVrfProof(l0, l1, l2), Self::BadVrfProof(r0, r1, r2)) => { + l0 == r0 && l1 == r1 && l2 == r2 + } + ( + Self::OutputMismatch { + declared: l_declared, + computed: l_computed, + }, + Self::OutputMismatch { + declared: r_declared, + computed: r_computed, + }, + ) => l_declared == r_declared && l_computed == r_computed, + _ => false, + } + } +} + +// ------------------------------------------------------------ VrfLeaderValueTooBigError +#[derive(Error, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum VrfLeaderValueTooBigError { + #[error("VRF Leader Value Too Big")] + VrfLeaderValueTooBig, +} + +// ------------------------------------------------------------ BadVrfProofError + +#[derive(Error, Clone, Debug, serde::Serialize, serde::Deserialize)] +pub enum BadVrfProofError { + #[error("Malformed VRF proof: {0}")] + MalformedProof(String), + + #[error("Invalid VRF proof: {0}")] + /// (error, vrf_input_hash, vrf_public_key_hash) + InvalidProof(String, Vec, Vec), + + #[error("could not convert slice to array")] + TryFromSliceError, + + #[error( + "Mismatch between the declared VRF proof hash ({}) and the computed one ({}).", + hex::encode(declared), + hex::encode(computed) + )] + ProofMismatch { + // this is Proof Hash (sha512 hash) + declared: Vec, + computed: Vec, + }, +} + +impl From for BadVrfProofError { + fn from(_: TryFromSliceError) -> Self { + Self::TryFromSliceError + } +} + +impl PartialEq for BadVrfProofError { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::MalformedProof(l0), Self::MalformedProof(r0)) => l0 == r0, + (Self::InvalidProof(l0, l1, l2), Self::InvalidProof(r0, r1, r2)) => { + l0 == r0 && l1 == r1 && l2 == r2 + } + (Self::TryFromSliceError, Self::TryFromSliceError) => true, + ( + Self::ProofMismatch { + declared: l_declared, + computed: l_computed, + }, + Self::ProofMismatch { + declared: r_declared, + computed: r_computed, + }, + ) => l_declared == r_declared && l_computed == r_computed, + _ => false, + } + } +} diff --git a/modules/block_vrf_validator/Cargo.toml b/modules/block_vrf_validator/Cargo.toml index 38420311..8b14ef55 100644 --- a/modules/block_vrf_validator/Cargo.toml +++ b/modules/block_vrf_validator/Cargo.toml @@ -21,7 +21,15 @@ tokio = { workspace = true } tracing = { workspace = true } serde_json = { workspace = true } serde = { workspace = true } +blake2 = "0.10.6" +num-traits = "0.2" +thiserror = "2.0.17" pallas = { workspace = true } +pallas-math = { workspace = true } +dashu-int = "0.4.1" + +# The vrf crate has not been fully tested in production environments and still has several upstream issues that are open PRs but not merged yet. +vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } [lib] path = "src/block_vrf_validator.rs" diff --git a/modules/block_vrf_validator/src/block_vrf_validator.rs b/modules/block_vrf_validator/src/block_vrf_validator.rs index b473ccce..69ecd04f 100644 --- a/modules/block_vrf_validator/src/block_vrf_validator.rs +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -9,12 +9,12 @@ use acropolis_common::{ use anyhow::Result; use caryatid_sdk::{module, Context, Module, Subscription}; use config::Config; -use pallas::ledger::traverse::MultiEraHeader; use std::sync::Arc; use tokio::sync::Mutex; use tracing::{error, info, info_span, Instrument}; mod state; use state::State; +mod ouroboros; use crate::vrf_validation_publisher::VrfValidationPublisher; mod snapshot; @@ -33,8 +33,8 @@ const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( ); const DEFAULT_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = ("blocks-subscribe-topic", "cardano.block.proposed"); -const DEFAULT_EPOCH_NONCE_SUBSCRIBE_TOPIC: (&str, &str) = - ("epoch-nonce-subscribe-topic", "cardano.epoch.nonce"); +const DEFAULT_EPOCH_ACTIVITY_SUBSCRIBE_TOPIC: (&str, &str) = + ("epoch-activity-subscribe-topic", "cardano.epoch.activity"); const DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC: (&str, &str) = ("spo-state-subscribe-topic", "cardano.spo.state"); const DEFAULT_SPDD_SUBSCRIBE_TOPIC: (&str, &str) = @@ -57,7 +57,7 @@ impl BlockVrfValidator { mut bootstrapped_subscription: Box>, mut blocks_subscription: Box>, mut protocol_parameters_subscription: Box>, - mut epoch_nonce_subscription: Box>, + mut epoch_activity_subscription: Box>, mut spo_state_subscription: Box>, mut spdd_subscription: Box>, ) -> Result<()> { @@ -90,7 +90,7 @@ impl BlockVrfValidator { if is_new_epoch { // read epoch boundary messages let protocol_parameters_message_f = protocol_parameters_subscription.read(); - let epoch_nonce_message_f = epoch_nonce_subscription.read(); + let epoch_activity_message_f = epoch_activity_subscription.read(); let spo_state_message_f = spo_state_subscription.read(); let spdd_msg_f = spdd_subscription.read(); @@ -107,17 +107,17 @@ impl BlockVrfValidator { _ => error!("Unexpected message type: {protocol_parameters_msg:?}"), }); - let (_, epoch_nonce_msg) = epoch_nonce_message_f.await?; + let (_, epoch_activity_msg) = epoch_activity_message_f.await?; let span = info_span!( - "block_vrf_validator.handle_epoch_nonce", + "block_vrf_validator.handle_epoch_activity", epoch = block_info.epoch ); - span.in_scope(|| match epoch_nonce_msg.as_ref() { - Message::Cardano((block_info, CardanoMessage::EpochNonce(msg))) => { + span.in_scope(|| match epoch_activity_msg.as_ref() { + Message::Cardano((block_info, CardanoMessage::EpochActivity(msg))) => { Self::check_sync(¤t_block, block_info); - state.handle_epoch_nonce(msg); + state.handle_epoch_activity(msg); } - _ => error!("Unexpected message type: {epoch_nonce_msg:?}"), + _ => error!("Unexpected message type: {epoch_activity_msg:?}"), }); let (_, spo_state_msg) = spo_state_message_f.await?; @@ -147,39 +147,17 @@ impl BlockVrfValidator { }); } - // decode header - // Derive the variant from the era - just enough to make - // MultiEraHeader::decode() work. - let span = info_span!( - "block_vrf_validator.decode_header", - block = block_info.number - ); - let mut header = None; - span.in_scope(|| { - header = match MultiEraHeader::decode( - block_info.era as u8, - None, - &block_msg.header, - ) { - Ok(header) => Some(header), - Err(e) => { - error!("Can't decode header {}: {e}", block_info.slot); - None - } - }; - }); - let span = info_span!("block_vrf_validator.validate", block = block_info.number); async { - if let Some(header) = header.as_ref() { - let result = state.validate_block_vrf(block_info, header, &genesis); - if let Err(e) = vrf_validation_publisher - .publish_vrf_validation(block_info, result) - .await - { - error!("Failed to publish VRF validation: {e}") - } + let result = state + .validate_block_vrf(block_info, &block_msg.header, &genesis) + .map_err(|e| *e); + if let Err(e) = vrf_validation_publisher + .publish_vrf_validation(block_info, result) + .await + { + error!("Failed to publish VRF validation: {e}") } } .instrument(span) @@ -217,10 +195,10 @@ impl BlockVrfValidator { .unwrap_or(DEFAULT_BLOCKS_SUBSCRIBE_TOPIC.1.to_string()); info!("Creating blocks subscription on '{blocks_subscribe_topic}'"); - let epoch_nonce_subscribe_topic = config - .get_string(DEFAULT_EPOCH_NONCE_SUBSCRIBE_TOPIC.0) - .unwrap_or(DEFAULT_EPOCH_NONCE_SUBSCRIBE_TOPIC.1.to_string()); - info!("Creating epoch nonce subscription on '{epoch_nonce_subscribe_topic}'"); + let epoch_activity_subscribe_topic = config + .get_string(DEFAULT_EPOCH_ACTIVITY_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_EPOCH_ACTIVITY_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating epoch activity subscription on '{epoch_activity_subscribe_topic}'"); let spo_state_subscribe_topic = config .get_string(DEFAULT_SPO_STATE_SUBSCRIBE_TOPIC.0) @@ -241,7 +219,8 @@ impl BlockVrfValidator { let protocol_parameters_subscription = context.subscribe(&protocol_parameters_subscribe_topic).await?; let blocks_subscription = context.subscribe(&blocks_subscribe_topic).await?; - let epoch_nonce_subscription = context.subscribe(&epoch_nonce_subscribe_topic).await?; + let epoch_activity_subscription = + context.subscribe(&epoch_activity_subscribe_topic).await?; let spo_state_subscription = context.subscribe(&spo_state_subscribe_topic).await?; let spdd_subscription = context.subscribe(&spdd_subscribe_topic).await?; @@ -259,7 +238,7 @@ impl BlockVrfValidator { bootstrapped_subscription, blocks_subscription, protocol_parameters_subscription, - epoch_nonce_subscription, + epoch_activity_subscription, spo_state_subscription, spdd_subscription, ) diff --git a/common/src/ouroboros/data/4490511.cbor b/modules/block_vrf_validator/src/ouroboros/data/4490511.cbor similarity index 100% rename from common/src/ouroboros/data/4490511.cbor rename to modules/block_vrf_validator/src/ouroboros/data/4490511.cbor diff --git a/common/src/ouroboros/data/4556956.cbor b/modules/block_vrf_validator/src/ouroboros/data/4556956.cbor similarity index 100% rename from common/src/ouroboros/data/4556956.cbor rename to modules/block_vrf_validator/src/ouroboros/data/4556956.cbor diff --git a/common/src/ouroboros/data/4576496.cbor b/modules/block_vrf_validator/src/ouroboros/data/4576496.cbor similarity index 100% rename from common/src/ouroboros/data/4576496.cbor rename to modules/block_vrf_validator/src/ouroboros/data/4576496.cbor diff --git a/common/src/ouroboros/data/7854823.cbor b/modules/block_vrf_validator/src/ouroboros/data/7854823.cbor similarity index 100% rename from common/src/ouroboros/data/7854823.cbor rename to modules/block_vrf_validator/src/ouroboros/data/7854823.cbor diff --git a/common/src/ouroboros/mod.rs b/modules/block_vrf_validator/src/ouroboros/mod.rs similarity index 100% rename from common/src/ouroboros/mod.rs rename to modules/block_vrf_validator/src/ouroboros/mod.rs diff --git a/common/src/ouroboros/overlay_schedule.rs b/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs similarity index 97% rename from common/src/ouroboros/overlay_schedule.rs rename to modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs index bcc8b10c..ba03dc83 100644 --- a/common/src/ouroboros/overlay_schedule.rs +++ b/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs @@ -5,7 +5,9 @@ //! //! https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/Rules/Overlay.hs#L332 -use crate::{rational_number::RationalNumber, GenesisDelegate, GenesisDelegates, GenesisKeyhash}; +use acropolis_common::{ + rational_number::RationalNumber, GenesisDelegate, GenesisDelegates, GenesisKeyhash, +}; use anyhow::Result; use num_traits::ToPrimitive; @@ -125,7 +127,7 @@ pub fn lookup_in_overlay_schedule( #[cfg(test)] mod tests { - use crate::genesis_values::GenesisValues; + use acropolis_common::genesis_values::GenesisValues; use super::*; diff --git a/common/src/ouroboros/praos.rs b/modules/block_vrf_validator/src/ouroboros/praos.rs similarity index 70% rename from common/src/ouroboros/praos.rs rename to modules/block_vrf_validator/src/ouroboros/praos.rs index 0847833c..dd9a46d2 100644 --- a/common/src/ouroboros/praos.rs +++ b/modules/block_vrf_validator/src/ouroboros/praos.rs @@ -1,18 +1,19 @@ -use std::collections::HashMap; - -use crate::crypto::keyhash_224; -use crate::ouroboros::vrf; -use crate::ouroboros::vrf_validation::{ - PraosBadVrfProofError, VrfLeaderValueTooBigError, VrfValidation, VrfValidationError, - WrongLeaderVrfKeyError, +use crate::ouroboros::{ + vrf, + vrf_validation::{ + validate_leader_vrf_key, validate_praos_vrf_proof, validate_vrf_leader_value, + }, +}; +use acropolis_common::{ + crypto::keyhash_224, + protocol_params::{Nonce, PraosParams}, + rational_number::RationalNumber, + validation::{VrfValidation, VrfValidationError}, + BlockInfo, PoolId, VrfKeyHash, }; -use crate::protocol_params::Nonce; -use crate::rational_number::RationalNumber; -use crate::{protocol_params::PraosParams, BlockInfo}; -use crate::{PoolId, VrfKeyHash}; use anyhow::Result; -use pallas::ledger::primitives::VrfCert; -use pallas::ledger::traverse::MultiEraHeader; +use pallas::ledger::{primitives::VrfCert, traverse::MultiEraHeader}; +use std::collections::HashMap; pub fn validate_vrf_praos<'a>( block_info: &'a BlockInfo, @@ -22,11 +23,15 @@ pub fn validate_vrf_praos<'a>( active_spos: &'a HashMap, active_spdd: &'a HashMap, total_active_stake: u64, -) -> Result>, VrfValidationError> { +) -> Result>, Box> { let active_slots_coeff = praos_params.active_slots_coeff; let Some(issuer_vkey) = header.issuer_vkey() else { - return Ok(vec![Box::new(|| Err(VrfValidationError::MissingIssuerKey))]); + return Ok(vec![Box::new(|| { + Err(VrfValidationError::Other( + "Issuer Key is not set".to_string(), + )) + })]); }; let pool_id = PoolId::from(keyhash_224(issuer_vkey)); let registered_vrf_key_hash = @@ -36,26 +41,29 @@ pub fn validate_vrf_praos<'a>( let relative_stake = RationalNumber::new(*pool_stake, total_active_stake); let Some(vrf_vkey) = header.vrf_vkey() else { - return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); + return Ok(vec![Box::new(|| { + Err(VrfValidationError::Other("VRF Key is not set".to_string())) + })]); }; let declared_vrf_key: &[u8; vrf::PublicKey::HASH_SIZE] = vrf_vkey .try_into() .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; - let vrf_cert = vrf_result(header).ok_or(VrfValidationError::PraosMissingVrfCert)?; + let vrf_cert = + vrf_result(header).ok_or(VrfValidationError::Other("VRF Cert is not set".to_string()))?; // Regular TPraos rules apply Ok(vec![ Box::new(move || { - WrongLeaderVrfKeyError::new(&pool_id, registered_vrf_key_hash, vrf_vkey)?; + validate_leader_vrf_key(&pool_id, registered_vrf_key_hash, vrf_vkey)?; Ok(()) }), Box::new(move || { - PraosBadVrfProofError::new( + validate_praos_vrf_proof( block_info.slot, epoch_nonce, - &header - .leader_vrf_output() - .map_err(|_| VrfValidationError::PraosMissingLeaderVrfOutput)?[..], + &header.leader_vrf_output().map_err(|_| { + VrfValidationError::Other("Leader VRF Output is not set".to_string()) + })?[..], &vrf::PublicKey::from(declared_vrf_key), &vrf_cert.0.to_vec()[..], &vrf_cert.1.to_vec()[..], @@ -63,10 +71,10 @@ pub fn validate_vrf_praos<'a>( Ok(()) }), Box::new(move || { - VrfLeaderValueTooBigError::new( - &header - .leader_vrf_output() - .map_err(|_| VrfValidationError::PraosMissingLeaderVrfOutput)?[..], + validate_vrf_leader_value( + &header.leader_vrf_output().map_err(|_| { + VrfValidationError::Other("Leader VRF Output is not set".to_string()) + })?[..], &relative_stake, &active_slots_coeff, )?; @@ -84,7 +92,7 @@ fn vrf_result<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { #[cfg(test)] mod tests { - use crate::{ + use acropolis_common::{ crypto::keyhash_256, protocol_params::NonceHash, serialization::Bech32Conversion, BlockHash, BlockStatus, Era, }; @@ -139,7 +147,9 @@ mod tests { &active_spdd, 25069171797357766, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); assert!(result.is_ok()); } } diff --git a/common/src/ouroboros/tpraos.rs b/modules/block_vrf_validator/src/ouroboros/tpraos.rs similarity index 80% rename from common/src/ouroboros/tpraos.rs rename to modules/block_vrf_validator/src/ouroboros/tpraos.rs index a77358d9..a62186fc 100644 --- a/common/src/ouroboros/tpraos.rs +++ b/modules/block_vrf_validator/src/ouroboros/tpraos.rs @@ -1,19 +1,21 @@ -use std::collections::HashMap; - -use crate::crypto::keyhash_224; -use crate::ouroboros::overlay_schedule::OBftSlot; -use crate::ouroboros::vrf_validation::{ - TPraosBadLeaderVrfProofError, TPraosBadNonceVrfProofError, VrfLeaderValueTooBigError, - VrfValidation, VrfValidationError, WrongGenesisLeaderVrfKeyError, WrongLeaderVrfKeyError, +use crate::ouroboros::{ + overlay_schedule::{self, OBftSlot}, + vrf, + vrf_validation::{ + validate_genesis_leader_vrf_key, validate_leader_vrf_key, validate_tpraos_leader_vrf_proof, + validate_tpraos_nonce_vrf_proof, validate_vrf_leader_value, + }, +}; +use acropolis_common::{ + crypto::keyhash_224, + protocol_params::{Nonce, PraosParams}, + rational_number::RationalNumber, + validation::{VrfValidation, VrfValidationError}, + BlockInfo, GenesisDelegates, PoolId, VrfKeyHash, }; -use crate::ouroboros::{overlay_schedule, vrf}; -use crate::protocol_params::Nonce; -use crate::rational_number::RationalNumber; -use crate::{protocol_params::PraosParams, BlockInfo}; -use crate::{GenesisDelegates, PoolId, VrfKeyHash}; use anyhow::Result; -use pallas::ledger::primitives::VrfCert; -use pallas::ledger::traverse::MultiEraHeader; +use pallas::ledger::{primitives::VrfCert, traverse::MultiEraHeader}; +use std::collections::HashMap; #[allow(clippy::too_many_arguments)] pub fn validate_vrf_tpraos<'a>( @@ -26,7 +28,7 @@ pub fn validate_vrf_tpraos<'a>( active_spdd: &'a HashMap, total_active_stake: u64, decentralisation_param: RationalNumber, -) -> Result>, VrfValidationError> { +) -> Result>, Box> { let active_slots_coeff = praos_params.active_slots_coeff; // first look up for overlay slot @@ -36,12 +38,16 @@ pub fn validate_vrf_tpraos<'a>( &decentralisation_param, &active_slots_coeff, ) - .map_err(|e| VrfValidationError::InvalidShelleyParams(e.to_string()))?; + .map_err(|e| VrfValidationError::Other(e.to_string()))?; match obft_slot { None => { let Some(issuer_vkey) = header.issuer_vkey() else { - return Ok(vec![Box::new(|| Err(VrfValidationError::MissingIssuerKey))]); + return Ok(vec![Box::new(|| { + Err(VrfValidationError::Other( + "Issuer Key is not set".to_string(), + )) + })]); }; let pool_id = PoolId::from(keyhash_224(issuer_vkey)); let registered_vrf_key_hash = @@ -51,24 +57,28 @@ pub fn validate_vrf_tpraos<'a>( let relative_stake = RationalNumber::new(*pool_stake, total_active_stake); let Some(vrf_vkey) = header.vrf_vkey() else { - return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); + return Ok(vec![Box::new(|| { + Err(VrfValidationError::Other("VRF Key is not set".to_string())) + })]); }; let declared_vrf_key: &[u8; vrf::PublicKey::SIZE] = vrf_vkey .try_into() .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; - let nonce_vrf_cert = - nonce_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingNonceVrfCert)?; - let leader_vrf_cert = - leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; + let nonce_vrf_cert = nonce_vrf_cert(header).ok_or(VrfValidationError::Other( + "Nonce VRF Cert is not set".to_string(), + ))?; + let leader_vrf_cert = leader_vrf_cert(header).ok_or(VrfValidationError::Other( + "Leader VRF Cert is not set".to_string(), + ))?; // Regular TPraos rules apply Ok(vec![ Box::new(move || { - WrongLeaderVrfKeyError::new(&pool_id, registered_vrf_key_hash, vrf_vkey)?; + validate_leader_vrf_key(&pool_id, registered_vrf_key_hash, vrf_vkey)?; Ok(()) }), Box::new(move || { - TPraosBadNonceVrfProofError::new( + validate_tpraos_nonce_vrf_proof( block_info.slot, epoch_nonce, &vrf::PublicKey::from(declared_vrf_key), @@ -78,7 +88,7 @@ pub fn validate_vrf_tpraos<'a>( Ok(()) }), Box::new(move || { - TPraosBadLeaderVrfProofError::new( + validate_tpraos_leader_vrf_proof( block_info.slot, epoch_nonce, &vrf::PublicKey::from(declared_vrf_key), @@ -88,7 +98,7 @@ pub fn validate_vrf_tpraos<'a>( Ok(()) }), Box::new(move || { - VrfLeaderValueTooBigError::new( + validate_vrf_leader_value( &leader_vrf_cert.0.to_vec()[..], &relative_stake, &active_slots_coeff, @@ -101,23 +111,27 @@ pub fn validate_vrf_tpraos<'a>( // The given genesis key has authority to produce a block in this // slot. Check whether we're its delegate. let Some(vrf_vkey) = header.vrf_vkey() else { - return Ok(vec![Box::new(|| Err(VrfValidationError::MissingVrfVkey))]); + return Ok(vec![Box::new(|| { + Err(VrfValidationError::Other("VRF Key is not set".to_string())) + })]); }; let declared_vrf_key: &[u8; vrf::PublicKey::SIZE] = vrf_vkey .try_into() .map_err(|_| VrfValidationError::TryFromSlice("Invalid Vrf Key".to_string()))?; - let nonce_vrf_cert = - nonce_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingNonceVrfCert)?; - let leader_vrf_cert = - leader_vrf_cert(header).ok_or(VrfValidationError::TPraosMissingLeaderVrfCert)?; + let nonce_vrf_cert = nonce_vrf_cert(header).ok_or(VrfValidationError::Other( + "Nonce VRF Cert is not set".to_string(), + ))?; + let leader_vrf_cert = leader_vrf_cert(header).ok_or(VrfValidationError::Other( + "Leader VRF Cert is not set".to_string(), + ))?; Ok(vec![ Box::new(move || { - WrongGenesisLeaderVrfKeyError::new(&genesis_key, &gen_deleg, vrf_vkey)?; + validate_genesis_leader_vrf_key(&genesis_key, &gen_deleg, vrf_vkey)?; Ok(()) }), Box::new(move || { - TPraosBadNonceVrfProofError::new( + validate_tpraos_nonce_vrf_proof( block_info.slot, epoch_nonce, &vrf::PublicKey::from(declared_vrf_key), @@ -127,7 +141,7 @@ pub fn validate_vrf_tpraos<'a>( Ok(()) }), Box::new(move || { - TPraosBadLeaderVrfProofError::new( + validate_tpraos_leader_vrf_proof( block_info.slot, epoch_nonce, &vrf::PublicKey::from(declared_vrf_key), @@ -161,9 +175,13 @@ fn leader_vrf_cert<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { #[cfg(test)] mod tests { - use crate::{ - crypto::keyhash_256, genesis_values::GenesisValues, protocol_params::NonceHash, - serialization::Bech32Conversion, BlockHash, BlockStatus, Era, + use acropolis_common::{ + crypto::keyhash_256, + genesis_values::GenesisValues, + protocol_params::NonceHash, + serialization::Bech32Conversion, + validation::{VrfLeaderValueTooBigError, WrongLeaderVrfKeyError}, + BlockHash, BlockStatus, Era, }; use super::*; @@ -214,7 +232,9 @@ mod tests { 1, decentralisation_param, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); assert!(result.is_ok()); } @@ -270,7 +290,9 @@ mod tests { 10177811974823000, decentralisation_param, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); assert!(result.is_ok()); } @@ -326,7 +348,9 @@ mod tests { 10177811974823000, decentralisation_param, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); assert!(result.is_ok()); } @@ -379,11 +403,13 @@ mod tests { 10177811974823000, decentralisation_param, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); assert!(result.is_err()); assert_eq!( result.unwrap_err(), - VrfValidationError::UnknownPool { pool_id } + Box::new(VrfValidationError::UnknownPool { pool_id }) ); } @@ -437,17 +463,21 @@ mod tests { 10177811974823000, decentralisation_param, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); assert!(result.is_err()); assert_eq!( result.unwrap_err(), - VrfValidationError::WrongLeaderVrfKey(WrongLeaderVrfKeyError { - pool_id, - registered_vrf_key_hash: VrfKeyHash::from(keyhash_256(&[0; 64])), - header_vrf_key_hash: VrfKeyHash::from(keyhash_256( - block_header.vrf_vkey().unwrap() - )), - }) + Box::new(VrfValidationError::WrongLeaderVrfKey( + WrongLeaderVrfKeyError { + pool_id, + registered_vrf_key_hash: VrfKeyHash::from(keyhash_256(&[0; 64])), + header_vrf_key_hash: VrfKeyHash::from(keyhash_256( + block_header.vrf_vkey().unwrap() + )), + } + )) ); } @@ -504,13 +534,15 @@ mod tests { 10177811974823000, decentralisation_param, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); assert!(result.is_err()); assert_eq!( result.unwrap_err(), - VrfValidationError::VrfLeaderValueTooBig( + Box::new(VrfValidationError::VrfLeaderValueTooBig( VrfLeaderValueTooBigError::VrfLeaderValueTooBig - ) + )) ); } } diff --git a/common/src/ouroboros/vrf.rs b/modules/block_vrf_validator/src/ouroboros/vrf.rs similarity index 98% rename from common/src/ouroboros/vrf.rs rename to modules/block_vrf_validator/src/ouroboros/vrf.rs index 289950c6..139c59f7 100644 --- a/common/src/ouroboros/vrf.rs +++ b/modules/block_vrf_validator/src/ouroboros/vrf.rs @@ -1,5 +1,6 @@ use std::{array::TryFromSliceError, ops::Deref}; +use acropolis_common::protocol_params::Nonce; use anyhow::Result; use blake2::{digest::consts::U32, Blake2b, Digest}; use thiserror::Error; @@ -8,12 +9,9 @@ use vrf_dalek::{ vrf03::{PublicKey03, VrfProof03}, }; -use crate::protocol_params::Nonce; - /// A VRF public key #[derive(Debug, PartialEq)] pub struct PublicKey(PublicKey03); -pub type PublicKeyHash = [u8; PublicKey::HASH_SIZE]; impl PublicKey { /// Size of a VRF public key, in bytes. pub const SIZE: usize = 32; diff --git a/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs b/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs new file mode 100644 index 00000000..5ca77b25 --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/vrf_validation.rs @@ -0,0 +1,218 @@ +use crate::ouroboros::vrf; +use acropolis_common::{ + crypto::keyhash_256, + protocol_params::Nonce, + rational_number::RationalNumber, + validation::{ + BadVrfProofError, PraosBadVrfProofError, TPraosBadLeaderVrfProofError, + TPraosBadNonceVrfProofError, VrfLeaderValueTooBigError, WrongGenesisLeaderVrfKeyError, + WrongLeaderVrfKeyError, + }, + GenesisDelegate, GenesisKeyhash, PoolId, Slot, VrfKeyHash, +}; +use anyhow::Result; +use dashu_int::UBig; +use pallas::ledger::primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; +use pallas_math::math::{ExpOrdering, FixedDecimal, FixedPrecision}; + +pub fn validate_genesis_leader_vrf_key( + genesis_key: &GenesisKeyhash, + genesis_deleg: &GenesisDelegate, + vrf_vkey: &[u8], +) -> Result<(), WrongGenesisLeaderVrfKeyError> { + let header_vrf_hash = VrfKeyHash::from(keyhash_256(vrf_vkey)); + let registered_vrf_hash = &genesis_deleg.vrf; + if !registered_vrf_hash.eq(&header_vrf_hash) { + return Err(WrongGenesisLeaderVrfKeyError { + genesis_key: *genesis_key, + registered_vrf_hash: *registered_vrf_hash, + header_vrf_hash, + }); + } + Ok(()) +} + +pub fn validate_leader_vrf_key( + pool_id: &PoolId, + registered_vrf_key_hash: &VrfKeyHash, + vrf_vkey: &[u8], +) -> Result<(), WrongLeaderVrfKeyError> { + let header_vrf_key_hash = VrfKeyHash::from(keyhash_256(vrf_vkey)); + if !registered_vrf_key_hash.eq(&header_vrf_key_hash) { + return Err(WrongLeaderVrfKeyError { + pool_id: *pool_id, + registered_vrf_key_hash: *registered_vrf_key_hash, + header_vrf_key_hash, + }); + } + Ok(()) +} + +/// Validate the VRF output from the block and its corresponding hash. +/// in TPraos Protocol for Nonce +pub fn validate_tpraos_nonce_vrf_proof( + absolute_slot: Slot, + epoch_nonce: &Nonce, + leader_public_key: &vrf::PublicKey, + unsafe_vrf_proof_hash: &[u8], + unsafe_vrf_proof: &[u8], +) -> Result<(), TPraosBadNonceVrfProofError> { + // For nonce proof validation + let seed_eta = Nonce::seed_eta(); + // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L365 + let rho_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_eta); + + // Verify the Nonce VRF proof + validate_vrf_proof( + &rho_seed, + leader_public_key, + unsafe_vrf_proof_hash, + unsafe_vrf_proof, + ) + .map_err(|e| TPraosBadNonceVrfProofError::BadVrfProof(absolute_slot, epoch_nonce.clone(), e))?; + Ok(()) +} + +/// Validate the VRF output from the block and its corresponding hash. +/// in TPraos Protocol for Leader +pub fn validate_tpraos_leader_vrf_proof( + absolute_slot: Slot, + epoch_nonce: &Nonce, + // Declared VRF Public Key from block header + leader_public_key: &vrf::PublicKey, + // Declared VRF Proof Hash from block header (sha512 hash) + unsafe_vrf_proof_hash: &[u8], + // Declared VRF Proof from block header (80 bytes) + unsafe_vrf_proof: &[u8], +) -> Result<(), TPraosBadLeaderVrfProofError> { + // For leader proof validation + let seed_l = Nonce::seed_l(); + // https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L366 + let y_seed = vrf::VrfInput::mk_seed(absolute_slot, epoch_nonce, &seed_l); + + // Verify the Leader VRF proof + validate_vrf_proof( + &y_seed, + leader_public_key, + unsafe_vrf_proof_hash, + unsafe_vrf_proof, + ) + .map_err(|e| { + TPraosBadLeaderVrfProofError::BadVrfProof(absolute_slot, epoch_nonce.clone(), e) + })?; + Ok(()) +} + +/// Validate the VRF output from the block and its corresponding hash. +/// in Praos Protocol +pub fn validate_praos_vrf_proof( + absolute_slot: Slot, + epoch_nonce: &Nonce, + leader_vrf_output: &[u8], + // Declared VRF Public Key from block header + leader_public_key: &vrf::PublicKey, + // Declared VRF Proof Hash from block header (sha512 hash) + unsafe_vrf_proof_hash: &[u8], + // Declared VRF Proof from block header (80 bytes) + unsafe_vrf_proof: &[u8], +) -> Result<(), PraosBadVrfProofError> { + let input = vrf::VrfInput::mk_vrf_input(absolute_slot, epoch_nonce); + + // Verify the VRF proof + validate_vrf_proof( + &input, + leader_public_key, + unsafe_vrf_proof_hash, + unsafe_vrf_proof, + ) + .map_err(|e| PraosBadVrfProofError::BadVrfProof(absolute_slot, epoch_nonce.clone(), e))?; + + // The proof was valid. Make sure that the leader's output matches what was in the block + let calculated_leader_vrf_output = + derive_tagged_vrf_output(unsafe_vrf_proof_hash, VrfDerivation::Leader); + if calculated_leader_vrf_output.as_slice() != leader_vrf_output { + return Err(PraosBadVrfProofError::OutputMismatch { + declared: leader_vrf_output.to_vec(), + computed: calculated_leader_vrf_output, + }); + } + + Ok(()) +} + +/// Reference +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/TPraos.hs#L430 +/// https://github.com/IntersectMBO/ouroboros-consensus/blob/e3c52b7c583bdb6708fac4fdaa8bf0b9588f5a88/ouroboros-consensus-protocol/src/ouroboros-consensus-protocol/Ouroboros/Consensus/Protocol/Praos.hs#L527 +/// +/// Check that the certified input natural is valid for being slot leader. This means we check that +/// p < 1 - (1 - f)^σ +/// **Variables** +/// `p` = `certNat` / `certNatMax`. (`certNat` is 64bytes for TPraos and 32bytes for Praos) +/// `σ` (sigma) = pool's relative stake (pools active stake / total active stake) +/// `f` = active slot coefficient (e.g., 0.05 = 5%) +/// let q = 1 - p and c = ln(1 - f) +/// then p < 1 - (1 - f)^σ => 1 / (1 - p) < exp(-σ * c) => 1 / q < exp(-σ * c) +/// Reference +/// https://github.com/IntersectMBO/cardano-ledger/blob/24ef1741c5e0109e4d73685a24d8e753e225656d/libs/cardano-protocol-tpraos/src/Cardano/Protocol/TPraos/BHeader.hs#L331 +/// +/// NOTE: +/// We are using Pallas Math Library +/// +pub fn validate_vrf_leader_value( + leader_vrf_output: &[u8], + leader_relative_stake: &RationalNumber, + active_slot_coeff: &RationalNumber, +) -> Result<(), VrfLeaderValueTooBigError> { + let certified_leader_vrf = &FixedDecimal::from(leader_vrf_output); + let output_size_bits = leader_vrf_output.len() * 8; + let cert_nat_max = FixedDecimal::from(UBig::ONE << output_size_bits); + let leader_relative_stake = FixedDecimal::from(UBig::from(*leader_relative_stake.numer())) + / FixedDecimal::from(UBig::from(*leader_relative_stake.denom())); + let active_slot_coeff = FixedDecimal::from(UBig::from(*active_slot_coeff.numer())) + / FixedDecimal::from(UBig::from(*active_slot_coeff.denom())); + + let denominator = &cert_nat_max - certified_leader_vrf; + let recip_q = &cert_nat_max / &denominator; + let c = (&FixedDecimal::from(1u64) - &active_slot_coeff).ln(); + let x = -(leader_relative_stake * c); + let ordering = x.exp_cmp(1000, 3, &recip_q); + match ordering.estimation { + ExpOrdering::LT => Ok(()), + ExpOrdering::GT | ExpOrdering::UNKNOWN => { + Err(VrfLeaderValueTooBigError::VrfLeaderValueTooBig) + } + } +} + +/// Validate the VRF proof +pub fn validate_vrf_proof( + vrf_input: &vrf::VrfInput, + // Declared VRF Public Key from block header + vrf_public_key: &vrf::PublicKey, + // Declared VRF Proof Hash from block header (sha512 hash) + unsafe_vrf_proof_hash: &[u8], + // Declared VRF Proof from block header (80 bytes) + unsafe_vrf_proof: &[u8], +) -> Result<(), BadVrfProofError> { + let vrf_proof: [u8; vrf::Proof::SIZE] = unsafe_vrf_proof.try_into()?; + let vrf_proof_hash: [u8; vrf::Proof::HASH_SIZE] = unsafe_vrf_proof_hash.try_into()?; + let vrf_proof = vrf::Proof::try_from(&vrf_proof) + .map_err(|e| BadVrfProofError::MalformedProof(e.to_string()))?; + + // Verify the VRF proof + let proof_hash = vrf_proof.verify(vrf_public_key, vrf_input).map_err(|e| { + BadVrfProofError::InvalidProof( + e.to_string(), + vrf_input.as_ref().to_vec(), + vrf_public_key.as_ref().to_vec(), + ) + })?; + if !proof_hash.as_slice().eq(&vrf_proof_hash) { + return Err(BadVrfProofError::ProofMismatch { + declared: vrf_proof_hash.to_vec(), + computed: proof_hash.to_vec(), + }); + } + + Ok(()) +} diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 236db52b..c830126b 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -2,19 +2,20 @@ use std::sync::Arc; +use crate::{ouroboros, snapshot::Snapshot}; use acropolis_common::{ genesis_values::GenesisValues, messages::{ - EpochNonceMessage, ProtocolParamsMessage, SPOStakeDistributionMessage, SPOStateMessage, + EpochActivityMessage, ProtocolParamsMessage, SPOStakeDistributionMessage, SPOStateMessage, }, - ouroboros::{self, vrf_validation::VrfValidationError}, - protocol_params::{Nonce, PraosParams, ShelleyParams}, + protocol_params::{Nonce, PraosParams}, + rational_number::RationalNumber, + validation::VrfValidationError, BlockInfo, Era, }; use anyhow::Result; use pallas::ledger::traverse::MultiEraHeader; - -use crate::snapshot::Snapshot; +use tracing::error; #[derive(Default, Debug, Clone)] pub struct EpochSnapshots { @@ -35,7 +36,7 @@ impl EpochSnapshots { #[derive(Default, Debug, Clone)] pub struct State { /// shelley params - pub shelley_params: Option, + pub decentralisation_param: Option, /// protocol parameter for Praos and TPraos pub praos_params: Option, @@ -51,7 +52,7 @@ impl State { pub fn new() -> Self { Self { praos_params: None, - shelley_params: None, + decentralisation_param: None, epoch_nonce: None, epoch_snapshots: EpochSnapshots::default(), } @@ -59,12 +60,13 @@ impl State { pub fn handle_protocol_parameters(&mut self, msg: &ProtocolParamsMessage) { if let Some(shelley_params) = msg.params.shelley.as_ref() { - self.shelley_params = Some(shelley_params.clone()); + self.decentralisation_param = + Some(shelley_params.protocol_params.decentralisation_param); self.praos_params = Some(shelley_params.into()); } } - pub fn handle_epoch_nonce(&mut self, msg: &EpochNonceMessage) { + pub fn handle_epoch_activity(&mut self, msg: &EpochActivityMessage) { self.epoch_nonce = msg.nonce.clone(); } @@ -80,28 +82,40 @@ impl State { pub fn validate_block_vrf( &self, block_info: &BlockInfo, - header: &MultiEraHeader, + raw_header: &[u8], genesis: &GenesisValues, - ) -> Result<(), VrfValidationError> { + ) -> Result<(), Box> { + let header = match MultiEraHeader::decode(block_info.era as u8, None, raw_header) { + Ok(header) => header, + Err(e) => { + error!("Can't decode header {}: {e}", block_info.slot); + return Err(Box::new(VrfValidationError::Other(format!( + "Can't decode header {}: {e}", + block_info.slot + )))); + } + }; + // Validation starts after Shelley Era if block_info.epoch < genesis.shelley_epoch { return Ok(()); } - let Some(shelley_params) = self.shelley_params.as_ref() else { - return Err(VrfValidationError::InvalidShelleyParams( - "Shelley Params are not set".to_string(), - )); + let Some(decentralisation_param) = self.decentralisation_param else { + return Err(Box::new(VrfValidationError::Other( + "Decentralisation Param is not set".to_string(), + ))); }; let Some(praos_params) = self.praos_params.as_ref() else { - return Err(VrfValidationError::InvalidShelleyParams( + return Err(Box::new(VrfValidationError::Other( "Praos Params are not set".to_string(), - )); + ))); }; let Some(epoch_nonce) = self.epoch_nonce.as_ref() else { - return Err(VrfValidationError::MissingEpochNonce); + return Err(Box::new(VrfValidationError::Other( + "Epoch Nonce is not set".to_string(), + ))); }; - let decentralisation_param = shelley_params.protocol_params.decentralisation_param; let is_tpraos = matches!( block_info.era, @@ -111,7 +125,7 @@ impl State { if is_tpraos { let result = ouroboros::tpraos::validate_vrf_tpraos( block_info, - header, + &header, epoch_nonce, &genesis.genesis_delegs, praos_params, @@ -120,19 +134,23 @@ impl State { self.epoch_snapshots.set.total_active_stakes, decentralisation_param, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); result } else { let result = ouroboros::praos::validate_vrf_praos( block_info, - header, + &header, epoch_nonce, praos_params, &self.epoch_snapshots.set.active_spos, &self.epoch_snapshots.set.active_stakes, self.epoch_snapshots.set.total_active_stakes, ) - .and_then(|vrf_validations| vrf_validations.iter().try_for_each(|assert| assert())); + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); result } } diff --git a/modules/block_vrf_validator/src/vrf_validation_publisher.rs b/modules/block_vrf_validator/src/vrf_validation_publisher.rs index 8e855cf4..7dcbba3c 100644 --- a/modules/block_vrf_validator/src/vrf_validation_publisher.rs +++ b/modules/block_vrf_validator/src/vrf_validation_publisher.rs @@ -1,7 +1,6 @@ use acropolis_common::{ messages::{CardanoMessage, Message}, - ouroboros::vrf_validation::VrfValidationError, - validation::{ValidationError, ValidationStatus}, + validation::{ValidationError, ValidationStatus, VrfValidationError}, BlockInfo, }; use caryatid_sdk::Context; @@ -23,7 +22,6 @@ impl VrfValidationPublisher { Self { context, topic } } - /// Publish the SPDD pub async fn publish_vrf_validation( &mut self, block: &BlockInfo, diff --git a/modules/epochs_state/src/epoch_nonce_publisher.rs b/modules/epochs_state/src/epoch_nonce_publisher.rs deleted file mode 100644 index b292c833..00000000 --- a/modules/epochs_state/src/epoch_nonce_publisher.rs +++ /dev/null @@ -1,44 +0,0 @@ -use acropolis_common::{ - messages::{CardanoMessage, EpochNonceMessage, Message}, - protocol_params::Nonces, - BlockInfo, -}; -use caryatid_sdk::Context; -use std::sync::Arc; - -/// Message publisher for Epoch Nonce Message -pub struct EpochNoncePublisher { - /// Module context - context: Arc>, - - /// Topic to publish on - topic: String, -} - -impl EpochNoncePublisher { - /// Construct with context and topic to publish on - pub fn new(context: Arc>, topic: String) -> Self { - Self { context, topic } - } - - /// Publish the Epoch Nonce Message - pub async fn publish( - &mut self, - block_info: &BlockInfo, - nonces: Option, - ) -> anyhow::Result<()> { - let active_nonce = nonces.map(|nonces| nonces.active); - self.context - .message_bus - .publish( - &self.topic, - Arc::new(Message::Cardano(( - block_info.clone(), - CardanoMessage::EpochNonce(EpochNonceMessage { - nonce: active_nonce, - }), - ))), - ) - .await - } -} diff --git a/modules/epochs_state/src/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index ba471be1..1761b627 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -16,15 +16,14 @@ use config::Config; use pallas::ledger::traverse::MultiEraHeader; use std::sync::Arc; use tokio::sync::Mutex; -use tracing::{error, info, info_span, Instrument}; +use tracing::{error, info, info_span}; mod epoch_activity_publisher; -mod epoch_nonce_publisher; mod epochs_history; mod state; mod store_config; use crate::{ - epoch_activity_publisher::EpochActivityPublisher, epoch_nonce_publisher::EpochNoncePublisher, - epochs_history::EpochsHistoryState, store_config::StoreConfig, + epoch_activity_publisher::EpochActivityPublisher, epochs_history::EpochsHistoryState, + store_config::StoreConfig, }; use state::State; @@ -43,8 +42,6 @@ const DEFAULT_PROTOCOL_PARAMETERS_SUBSCRIBE_TOPIC: (&str, &str) = ( const DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC: (&str, &str) = ("epoch-activity-publish-topic", "cardano.epoch.activity"); -const DEFAULT_EPOCH_NONCE_PUBLISH_TOPIC: (&str, &str) = - ("epoch-nonce-publish-topic", "cardano.epoch.nonce"); /// Epochs State module #[module( @@ -64,7 +61,6 @@ impl EpochsState { mut block_txs_subscription: Box>, mut protocol_parameters_subscription: Box>, mut epoch_activity_publisher: EpochActivityPublisher, - mut epoch_nonce_publisher: EpochNoncePublisher, ) -> Result<()> { let (_, bootstrapped_message) = bootstrapped_subscription.read().await?; let genesis = match bootstrapped_message.as_ref() { @@ -131,41 +127,25 @@ impl EpochsState { }; }); + let span = info_span!("epochs_state.evolve_nonces", block = block_info.number); + span.in_scope(|| { + if let Some(header) = header.as_ref() { + if let Err(e) = state.evolve_nonces(&genesis, block_info, header) { + error!("Error handling block header: {e}"); + } + } + }); + if is_new_epoch { let ea = state.end_epoch(block_info); // update epochs history epochs_history.handle_epoch_activity(block_info, &ea); // publish epoch activity message - epoch_activity_publisher.publish(&block_info, ea).await.unwrap_or_else( + epoch_activity_publisher.publish(block_info, ea).await.unwrap_or_else( |e| error!("Failed to publish epoch activity messages: {e}"), ); } - let span = info_span!( - "epochs_state.handle_block_header", - block = block_info.number - ); - async { - if let Some(header) = header.as_ref() { - match state.handle_block_header(&genesis, &block_info, &header) { - Ok(()) => { - if is_new_epoch { - let nonces = state.get_nonces(); - epoch_nonce_publisher - .publish(&block_info, nonces) - .await - .unwrap_or_else(|e| { - error!("Failed to publish epoch nonce: {e}") - }); - } - } - Err(e) => error!("Error handling block header: {e}"), - } - } - } - .instrument(span) - .await; - let span = info_span!("epochs_state.handle_mint", block = block_info.number); span.in_scope(|| { if let Some(header) = header.as_ref() { @@ -230,11 +210,6 @@ impl EpochsState { .unwrap_or(DEFAULT_EPOCH_ACTIVITY_PUBLISH_TOPIC.1.to_string()); info!("Publishing EpochActivityMessage on '{epoch_activity_publish_topic}'"); - let epoch_nonce_publish_topic = config - .get_string(DEFAULT_EPOCH_NONCE_PUBLISH_TOPIC.0) - .unwrap_or(DEFAULT_EPOCH_NONCE_PUBLISH_TOPIC.1.to_string()); - info!("Publishing EpochNonceMessage on '{epoch_nonce_publish_topic}'"); - // query topic let epochs_query_topic = config .get_string(DEFAULT_EPOCHS_QUERY_TOPIC.0) @@ -265,8 +240,6 @@ impl EpochsState { // Publisher let epoch_activity_publisher = EpochActivityPublisher::new(context.clone(), epoch_activity_publish_topic); - let epoch_nonce_publisher = - EpochNoncePublisher::new(context.clone(), epoch_nonce_publish_topic); // handle epochs query context.handle(&epochs_query_topic, move |message| { @@ -345,8 +318,7 @@ impl EpochsState { } _ => EpochsStateQueryResponse::Error(format!( - "Unimplemented query variant: {:?}", - query + "Unimplemented query variant: {query:?}" )), }; Arc::new(Message::StateQueryResponse(StateQueryResponse::Epochs( @@ -365,7 +337,6 @@ impl EpochsState { block_txs_subscription, protocol_parameters_subscription, epoch_activity_publisher, - epoch_nonce_publisher, ) .await .unwrap_or_else(|e| error!("Failed: {e}")); diff --git a/modules/epochs_state/src/state.rs b/modules/epochs_state/src/state.rs index 318c7de4..08002465 100644 --- a/modules/epochs_state/src/state.rs +++ b/modules/epochs_state/src/state.rs @@ -89,8 +89,8 @@ impl State { } } - /// Handle a block header - pub fn handle_block_header( + /// Evolve Nonces + pub fn evolve_nonces( &mut self, genesis: &GenesisValues, block_info: &BlockInfo, @@ -233,10 +233,6 @@ impl State { epoch_activity } - pub fn get_nonces(&self) -> Option { - self.nonces.clone() - } - pub fn get_epoch_info(&self) -> EpochActivityMessage { EpochActivityMessage { epoch: self.epoch, @@ -254,7 +250,7 @@ impl State { total_outputs: self.epoch_outputs, total_fees: self.epoch_fees, spo_blocks: self.blocks_minted.iter().map(|(k, v)| (*k, *v)).collect(), - nonce: self.nonces.as_ref().and_then(|n| n.active.hash), + nonce: self.nonces.as_ref().map(|n| n.active.clone()), } } @@ -528,7 +524,7 @@ mod tests { hex::decode(include_str!("../data/4490511.cbor")).unwrap(); let block = make_new_epoch_block(208); let block_header = MultiEraHeader::decode(1, None, &e_208_first_block_header_cbor).unwrap(); - assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); + assert!(state.evolve_nonces(&genesis_value, &block, &block_header).is_ok()); let nonces = state.nonces.unwrap(); let evolved = Nonce::from( @@ -558,12 +554,12 @@ mod tests { hex::decode(include_str!("../data/4490512.cbor")).unwrap(); let block = make_new_epoch_block(208); let block_header = MultiEraHeader::decode(1, None, &e_208_first_block_header_cbor).unwrap(); - assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); + assert!(state.evolve_nonces(&genesis_value, &block, &block_header).is_ok()); let block = make_block(208); let block_header = MultiEraHeader::decode(1, None, &e_208_second_block_header_cbor).unwrap(); - assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); + assert!(state.evolve_nonces(&genesis_value, &block, &block_header).is_ok()); let evolved = Nonce::from( NonceHash::try_from( @@ -622,7 +618,7 @@ mod tests { hex::decode(include_str!("../data/4512067.cbor")).unwrap(); let block = make_new_epoch_block(209); let block_header = MultiEraHeader::decode(1, None, &e_209_first_block_header_cbor).unwrap(); - assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); + assert!(state.evolve_nonces(&genesis_value, &block, &block_header).is_ok()); let nonces = state.nonces.unwrap(); let evolved = Nonce::from( @@ -694,7 +690,7 @@ mod tests { hex::decode(include_str!("../data/4533637.cbor")).unwrap(); let block = make_new_epoch_block(209); let block_header = MultiEraHeader::decode(1, None, &e_210_first_block_header_cbor).unwrap(); - assert!(state.handle_block_header(&genesis_value, &block, &block_header).is_ok()); + assert!(state.evolve_nonces(&genesis_value, &block, &block_header).is_ok()); let nonces = state.nonces.unwrap(); let evolved = Nonce::from( From f897a27868226b639fa14e1a58bc33c6e848d460 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 7 Nov 2025 08:01:53 +0100 Subject: [PATCH 30/36] fix: only save mark and set snapshots --- modules/block_vrf_validator/src/state.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index c830126b..27a57e21 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -21,13 +21,11 @@ use tracing::error; pub struct EpochSnapshots { pub mark: Arc, pub set: Arc, - pub go: Arc, } impl EpochSnapshots { /// Push a new snapshot pub fn push(&mut self, latest: Snapshot) { - self.go = self.set.clone(); self.set = self.mark.clone(); self.mark = Arc::new(latest); } From 9fedc007910b5018e9e5346c8f90df3ce12f89cc Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 7 Nov 2025 14:01:59 +0100 Subject: [PATCH 31/36] fix: only use active_slots_coeff in block_vrf_validator --- .../src/ouroboros/praos.rs | 10 ++--- .../src/ouroboros/tpraos.rs | 44 +++++++++---------- modules/block_vrf_validator/src/state.rs | 20 ++++----- 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/modules/block_vrf_validator/src/ouroboros/praos.rs b/modules/block_vrf_validator/src/ouroboros/praos.rs index dd9a46d2..e6275faf 100644 --- a/modules/block_vrf_validator/src/ouroboros/praos.rs +++ b/modules/block_vrf_validator/src/ouroboros/praos.rs @@ -6,7 +6,7 @@ use crate::ouroboros::{ }; use acropolis_common::{ crypto::keyhash_224, - protocol_params::{Nonce, PraosParams}, + protocol_params::Nonce, rational_number::RationalNumber, validation::{VrfValidation, VrfValidationError}, BlockInfo, PoolId, VrfKeyHash, @@ -19,13 +19,11 @@ pub fn validate_vrf_praos<'a>( block_info: &'a BlockInfo, header: &'a MultiEraHeader, epoch_nonce: &'a Nonce, - praos_params: &'a PraosParams, + active_slots_coeff: RationalNumber, active_spos: &'a HashMap, active_spdd: &'a HashMap, total_active_stake: u64, ) -> Result>, Box> { - let active_slots_coeff = praos_params.active_slots_coeff; - let Some(issuer_vkey) = header.issuer_vkey() else { return Ok(vec![Box::new(|| { Err(VrfValidationError::Other( @@ -101,7 +99,6 @@ mod tests { #[test] fn test_7854823_block() { - let praos_params = PraosParams::mainnet(); let epoch_nonce = Nonce::from( NonceHash::try_from( hex::decode("8dad163edf4607452fec9c5955d593fb598ca728bae162138f88da6667bba79b") @@ -110,6 +107,7 @@ mod tests { ) .unwrap(), ); + let active_slots_coeff = RationalNumber::new(1, 20); let block_header_7854823: Vec = hex::decode(include_str!("./data/7854823.cbor")).unwrap(); @@ -142,7 +140,7 @@ mod tests { &block_info, &block_header, &epoch_nonce, - &praos_params, + active_slots_coeff, &active_spos, &active_spdd, 25069171797357766, diff --git a/modules/block_vrf_validator/src/ouroboros/tpraos.rs b/modules/block_vrf_validator/src/ouroboros/tpraos.rs index a62186fc..2eed4dde 100644 --- a/modules/block_vrf_validator/src/ouroboros/tpraos.rs +++ b/modules/block_vrf_validator/src/ouroboros/tpraos.rs @@ -8,7 +8,7 @@ use crate::ouroboros::{ }; use acropolis_common::{ crypto::keyhash_224, - protocol_params::{Nonce, PraosParams}, + protocol_params::Nonce, rational_number::RationalNumber, validation::{VrfValidation, VrfValidationError}, BlockInfo, GenesisDelegates, PoolId, VrfKeyHash, @@ -23,14 +23,12 @@ pub fn validate_vrf_tpraos<'a>( header: &'a MultiEraHeader, epoch_nonce: &'a Nonce, genesis_delegs: &'a GenesisDelegates, - praos_params: &'a PraosParams, + active_slots_coeff: RationalNumber, + decentralisation_param: RationalNumber, active_spos: &'a HashMap, active_spdd: &'a HashMap, total_active_stake: u64, - decentralisation_param: RationalNumber, ) -> Result>, Box> { - let active_slots_coeff = praos_params.active_slots_coeff; - // first look up for overlay slot let obft_slot = overlay_schedule::lookup_in_overlay_schedule( block_info.epoch_slot, @@ -189,7 +187,6 @@ mod tests { #[test] fn test_4490511_block_produced_by_genesis_key() { let genesis_value = GenesisValues::mainnet(); - let praos_params = PraosParams::mainnet(); let epoch_nonce = Nonce::from( NonceHash::try_from( hex::decode("1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81") @@ -198,6 +195,7 @@ mod tests { ) .unwrap(), ); + let active_slots_coeff = RationalNumber::new(1, 20); let decentralisation_param = RationalNumber::from(1); let block_header_4490511: Vec = @@ -226,11 +224,11 @@ mod tests { &block_header, &epoch_nonce, &genesis_value.genesis_delegs, - &praos_params, + active_slots_coeff, + decentralisation_param, &active_spos, &active_spdd, 1, - decentralisation_param, ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) @@ -241,7 +239,6 @@ mod tests { #[test] fn test_4556956_block() { let genesis_value = GenesisValues::mainnet(); - let praos_params = PraosParams::mainnet(); let epoch_nonce = Nonce::from( NonceHash::try_from( hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") @@ -250,6 +247,7 @@ mod tests { ) .unwrap(), ); + let active_slots_coeff = RationalNumber::new(1, 20); let decentralisation_param = RationalNumber::new(9, 10); let block_header_4556956: Vec = @@ -284,11 +282,11 @@ mod tests { &block_header, &epoch_nonce, &genesis_value.genesis_delegs, - &praos_params, + active_slots_coeff, + decentralisation_param, &active_spos, &active_spdd, 10177811974823000, - decentralisation_param, ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) @@ -299,7 +297,6 @@ mod tests { #[test] fn test_4576496_block() { let genesis_value = GenesisValues::mainnet(); - let praos_params = PraosParams::mainnet(); let epoch_nonce = Nonce::from( NonceHash::try_from( hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") @@ -308,6 +305,7 @@ mod tests { ) .unwrap(), ); + let active_slots_coeff = RationalNumber::new(1, 20); let decentralisation_param = RationalNumber::new(9, 10); let block_header_4576496: Vec = @@ -342,11 +340,11 @@ mod tests { &block_header, &epoch_nonce, &genesis_value.genesis_delegs, - &praos_params, + active_slots_coeff, + decentralisation_param, &active_spos, &active_spdd, 10177811974823000, - decentralisation_param, ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) @@ -357,7 +355,6 @@ mod tests { #[test] fn test_4576496_block_as_unknown_pool() { let genesis_value = GenesisValues::mainnet(); - let praos_params = PraosParams::mainnet(); let epoch_nonce = Nonce::from( NonceHash::try_from( hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") @@ -366,6 +363,7 @@ mod tests { ) .unwrap(), ); + let active_slots_coeff = RationalNumber::new(1, 20); let decentralisation_param = RationalNumber::new(9, 10); let block_header_4576496: Vec = @@ -397,11 +395,11 @@ mod tests { &block_header, &epoch_nonce, &genesis_value.genesis_delegs, - &praos_params, + active_slots_coeff, + decentralisation_param, &active_spos, &active_spdd, 10177811974823000, - decentralisation_param, ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) @@ -416,7 +414,6 @@ mod tests { #[test] fn test_4576496_block_as_wrong_leader_vrf_key() { let genesis_value = GenesisValues::mainnet(); - let praos_params = PraosParams::mainnet(); let epoch_nonce = Nonce::from( NonceHash::try_from( hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") @@ -425,6 +422,7 @@ mod tests { ) .unwrap(), ); + let active_slots_coeff = RationalNumber::new(1, 20); let decentralisation_param = RationalNumber::new(9, 10); let block_header_4576496: Vec = @@ -457,11 +455,11 @@ mod tests { &block_header, &epoch_nonce, &genesis_value.genesis_delegs, - &praos_params, + active_slots_coeff, + decentralisation_param, &active_spos, &active_spdd, 10177811974823000, - decentralisation_param, ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) @@ -484,7 +482,6 @@ mod tests { #[test] fn test_4576496_block_with_small_active_stake() { let genesis_value = GenesisValues::mainnet(); - let praos_params = PraosParams::mainnet(); let epoch_nonce = Nonce::from( NonceHash::try_from( hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") @@ -493,6 +490,7 @@ mod tests { ) .unwrap(), ); + let active_slots_coeff = RationalNumber::new(1, 20); let decentralisation_param = RationalNumber::new(9, 10); let block_header_4576496: Vec = @@ -528,11 +526,11 @@ mod tests { &block_header, &epoch_nonce, &genesis_value.genesis_delegs, - &praos_params, + active_slots_coeff, + decentralisation_param, &active_spos, &active_spdd, 10177811974823000, - decentralisation_param, ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 27a57e21..8c5f919a 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -8,7 +8,7 @@ use acropolis_common::{ messages::{ EpochActivityMessage, ProtocolParamsMessage, SPOStakeDistributionMessage, SPOStateMessage, }, - protocol_params::{Nonce, PraosParams}, + protocol_params::Nonce, rational_number::RationalNumber, validation::VrfValidationError, BlockInfo, Era, @@ -33,11 +33,9 @@ impl EpochSnapshots { #[derive(Default, Debug, Clone)] pub struct State { - /// shelley params pub decentralisation_param: Option, - /// protocol parameter for Praos and TPraos - pub praos_params: Option, + pub active_slots_coeff: Option, /// epoch nonce pub epoch_nonce: Option, @@ -49,7 +47,7 @@ pub struct State { impl State { pub fn new() -> Self { Self { - praos_params: None, + active_slots_coeff: None, decentralisation_param: None, epoch_nonce: None, epoch_snapshots: EpochSnapshots::default(), @@ -60,7 +58,7 @@ impl State { if let Some(shelley_params) = msg.params.shelley.as_ref() { self.decentralisation_param = Some(shelley_params.protocol_params.decentralisation_param); - self.praos_params = Some(shelley_params.into()); + self.active_slots_coeff = Some(shelley_params.active_slots_coeff); } } @@ -104,9 +102,9 @@ impl State { "Decentralisation Param is not set".to_string(), ))); }; - let Some(praos_params) = self.praos_params.as_ref() else { + let Some(active_slots_coeff) = self.active_slots_coeff else { return Err(Box::new(VrfValidationError::Other( - "Praos Params are not set".to_string(), + "Active Slots Coeff is not set".to_string(), ))); }; let Some(epoch_nonce) = self.epoch_nonce.as_ref() else { @@ -126,11 +124,11 @@ impl State { &header, epoch_nonce, &genesis.genesis_delegs, - praos_params, + active_slots_coeff, + decentralisation_param, &self.epoch_snapshots.set.active_spos, &self.epoch_snapshots.set.active_stakes, self.epoch_snapshots.set.total_active_stakes, - decentralisation_param, ) .and_then(|vrf_validations| { vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) @@ -141,7 +139,7 @@ impl State { block_info, &header, epoch_nonce, - praos_params, + active_slots_coeff, &self.epoch_snapshots.set.active_spos, &self.epoch_snapshots.set.active_stakes, self.epoch_snapshots.set.total_active_stakes, From 17968922bab0e300bb2ebd158b0a3efe6840c337 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Fri, 7 Nov 2025 16:29:31 +0100 Subject: [PATCH 32/36] fix: add not active slot in overlay error --- common/src/validation.rs | 4 ++++ modules/block_vrf_validator/src/ouroboros/tpraos.rs | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/common/src/validation.rs b/common/src/validation.rs index 2e2e2bd1..d0e55735 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -63,6 +63,10 @@ pub enum VrfValidationError { /// The pool lost the slot lottery #[error("VRF Leader Value Too Big")] VrfLeaderValueTooBig(#[from] VrfLeaderValueTooBigError), + /// **Cause:** This slot is in the overlay schedule but marked as non-active. + /// It's an intentional gap slot where no blocks should be produced. + #[error("Not Active slot in overlay schedule: {slot}")] + NotActiveSlotInOverlaySchedule { slot: Slot }, /// **Cause:** Some data has incorrect bytes #[error("TryFromSlice: {0}")] TryFromSlice(String), diff --git a/modules/block_vrf_validator/src/ouroboros/tpraos.rs b/modules/block_vrf_validator/src/ouroboros/tpraos.rs index 2eed4dde..65f21e3d 100644 --- a/modules/block_vrf_validator/src/ouroboros/tpraos.rs +++ b/modules/block_vrf_validator/src/ouroboros/tpraos.rs @@ -152,7 +152,11 @@ pub fn validate_vrf_tpraos<'a>( } Some(OBftSlot::NonActiveSlot) => { // This is a non-active slot; nobody may produce a block - Ok(vec![]) + Ok(vec![Box::new(|| { + Err(VrfValidationError::NotActiveSlotInOverlaySchedule { + slot: block_info.slot, + }) + })]) } } } From b60f211c15220a4fbb6ad33d5b4122067971810b Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Sat, 8 Nov 2025 09:43:56 +0100 Subject: [PATCH 33/36] fix: use Hash for shelley genesis hash type --- common/src/genesis_values.rs | 10 +++++----- common/src/types.rs | 4 ++-- .../src/genesis_bootstrapper.rs | 5 +++-- .../src/snapshot_bootstrapper.rs | 14 +++++++------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/common/src/genesis_values.rs b/common/src/genesis_values.rs index 0c52d4aa..8776d212 100644 --- a/common/src/genesis_values.rs +++ b/common/src/genesis_values.rs @@ -1,8 +1,11 @@ +use std::str::FromStr; + use crate::{ calculations::{ epoch_to_first_slot_with_shelley_params, slot_to_epoch_with_shelley_params, slot_to_timestamp_with_params, }, + hash::Hash, GenesisDelegates, }; const MAINNET_SHELLEY_GENESIS_HASH: &str = @@ -13,7 +16,7 @@ pub struct GenesisValues { pub byron_timestamp: u64, pub shelley_epoch: u64, pub shelley_epoch_len: u64, - pub shelley_genesis_hash: [u8; 32], + pub shelley_genesis_hash: Hash<32>, pub genesis_delegs: GenesisDelegates, } @@ -23,10 +26,7 @@ impl GenesisValues { byron_timestamp: 1506203091, shelley_epoch: 208, shelley_epoch_len: 432000, - shelley_genesis_hash: hex::decode(MAINNET_SHELLEY_GENESIS_HASH) - .unwrap() - .try_into() - .unwrap(), + shelley_genesis_hash: Hash::<32>::from_str(MAINNET_SHELLEY_GENESIS_HASH).unwrap(), genesis_delegs: GenesisDelegates::try_from(vec![ ( "ad5463153dc3d24b9ff133e46136028bdc1edbb897f5a7cf1b37950c", diff --git a/common/src/types.rs b/common/src/types.rs index a66b75ce..ec19d7d5 100644 --- a/common/src/types.rs +++ b/common/src/types.rs @@ -1765,8 +1765,8 @@ impl Voter { impl Display for Voter { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self.to_bech32() { - Ok(addr) => write!(f, "{}", addr), - Err(e) => write!(f, "", e), + Ok(addr) => write!(f, "{addr}"), + Err(e) => write!(f, ""), } } } diff --git a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs index aa9ffa28..c81ebc73 100644 --- a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs +++ b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs @@ -3,6 +3,7 @@ use acropolis_common::{ genesis_values::GenesisValues, + hash::Hash, messages::{ CardanoMessage, GenesisCompleteMessage, GenesisUTxOsMessage, Message, PotDeltasMessage, UTXODeltasMessage, @@ -41,11 +42,11 @@ const SANCHONET_SHELLEY_START_EPOCH: u64 = 0; // Initial reserves (=maximum ever Lovelace supply) const INITIAL_RESERVES: Lovelace = 45_000_000_000_000_000; -fn hash_genesis_bytes(raw_bytes: &[u8]) -> [u8; 32] { +fn hash_genesis_bytes(raw_bytes: &[u8]) -> Hash<32> { let mut hasher = Blake2b::::new(); hasher.update(raw_bytes); let hash: [u8; 32] = hasher.finalize().into(); - hash + Hash::<32>::new(hash) } /// Genesis bootstrapper module diff --git a/modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs b/modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs index da5f2ce1..459a222b 100644 --- a/modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs +++ b/modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs @@ -1,7 +1,8 @@ -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use acropolis_common::{ genesis_values::GenesisValues, + hash::Hash, messages::{CardanoMessage, GenesisCompleteMessage, Message}, snapshot::{ streaming_snapshot::{ @@ -86,12 +87,11 @@ impl SnapshotHandler { byron_timestamp: 1506203091, // Byron mainnet genesis timestamp shelley_epoch: 208, // Shelley started at epoch 208 on mainnet shelley_epoch_len: 432000, // 5 days in seconds - shelley_genesis_hash: [ - // Shelley mainnet genesis hash (placeholder - should be from config) - 0x1a, 0x3d, 0x98, 0x7a, 0x95, 0xad, 0xd2, 0x3e, 0x4f, 0x4d, 0x2d, 0x78, 0x74, 0x9f, - 0x96, 0x65, 0xd4, 0x1e, 0x48, 0x3e, 0xf2, 0xa2, 0x22, 0x9c, 0x4b, 0x0b, 0xf3, 0x9f, - 0xad, 0x7d, 0x5e, 0x27, - ], + // Shelley mainnet genesis hash (placeholder - should be from config) + shelley_genesis_hash: Hash::<32>::from_str( + "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81", + ) + .unwrap(), genesis_delegs: GenesisDelegates::try_from(vec![]).unwrap(), }) } From f4a7a3eda04817704ca518ad06cd41ad51aa27f7 Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Mon, 10 Nov 2025 15:33:28 +0100 Subject: [PATCH 34/36] fix: skip decode before shelley era --- modules/block_vrf_validator/src/state.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs index 8c5f919a..03d4a877 100644 --- a/modules/block_vrf_validator/src/state.rs +++ b/modules/block_vrf_validator/src/state.rs @@ -81,6 +81,11 @@ impl State { raw_header: &[u8], genesis: &GenesisValues, ) -> Result<(), Box> { + // Validation starts after Shelley Era + if block_info.epoch < genesis.shelley_epoch { + return Ok(()); + } + let header = match MultiEraHeader::decode(block_info.era as u8, None, raw_header) { Ok(header) => header, Err(e) => { @@ -92,11 +97,6 @@ impl State { } }; - // Validation starts after Shelley Era - if block_info.epoch < genesis.shelley_epoch { - return Ok(()); - } - let Some(decentralisation_param) = self.decentralisation_param else { return Err(Box::new(VrfValidationError::Other( "Decentralisation Param is not set".to_string(), From 57bf63e293c9f1cd4170938851ff1282e0c2275b Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Tue, 11 Nov 2025 06:06:01 +0100 Subject: [PATCH 35/36] fix: cargo shear --- Cargo.lock | 2 -- modules/block_vrf_validator/Cargo.toml | 2 -- 2 files changed, 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03a4f8f9..f78c73d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,12 +127,10 @@ dependencies = [ "config", "dashu-int", "hex", - "imbl", "num-traits", "pallas 0.33.0", "pallas-math", "serde", - "serde_json", "thiserror 2.0.17", "tokio", "tracing", diff --git a/modules/block_vrf_validator/Cargo.toml b/modules/block_vrf_validator/Cargo.toml index 8b14ef55..1031be2d 100644 --- a/modules/block_vrf_validator/Cargo.toml +++ b/modules/block_vrf_validator/Cargo.toml @@ -16,10 +16,8 @@ caryatid_sdk = { workspace = true } anyhow = { workspace = true } config = { workspace = true } hex = { workspace = true } -imbl = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } -serde_json = { workspace = true } serde = { workspace = true } blake2 = "0.10.6" num-traits = "0.2" From a77a3ed6b3716ce2b05211ffbf67abbb75c4952a Mon Sep 17 00:00:00 2001 From: Golddy Dev Date: Tue, 11 Nov 2025 16:53:59 +0100 Subject: [PATCH 36/36] fix: cargo audit issue with vrf dalek --- Cargo.lock | 141 +++++++++++++++++++------ modules/block_vrf_validator/Cargo.toml | 2 +- 2 files changed, 108 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f78c73d6..81fa4447 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,11 +130,11 @@ dependencies = [ "num-traits", "pallas 0.33.0", "pallas-math", + "pallas_vrf", "serde", "thiserror 2.0.17", "tokio", "tracing", - "vrf_dalek", ] [[package]] @@ -478,7 +478,7 @@ dependencies = [ "caryatid_module_rest_server", "caryatid_module_spy", "caryatid_process", - "clap", + "clap 4.5.51", "config", "opentelemetry", "opentelemetry-otlp", @@ -518,7 +518,7 @@ dependencies = [ "caryatid_module_spy", "caryatid_process", "caryatid_sdk", - "clap", + "clap 4.5.51", "config", "opentelemetry", "opentelemetry-otlp", @@ -558,7 +558,7 @@ dependencies = [ "anyhow", "caryatid_process", "caryatid_sdk", - "clap", + "clap 4.5.51", "config", "tokio", "tracing", @@ -678,6 +678,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + [[package]] name = "anstream" version = "0.6.21" @@ -943,6 +952,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -1610,6 +1630,21 @@ dependencies = [ "libloading", ] +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap", + "unicode-width", + "vec_map", +] + [[package]] name = "clap" version = "4.5.51" @@ -1629,7 +1664,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim", + "strsim 0.11.1", ] [[package]] @@ -1957,19 +1992,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "curve25519-dalek" -version = "3.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.5.1", - "subtle", - "zeroize", -] - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -2039,7 +2061,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.109", ] @@ -2053,7 +2075,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "strsim", + "strsim 0.11.1", "syn 2.0.109", ] @@ -2820,6 +2842,15 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.5.2" @@ -3855,7 +3886,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi", + "hermit-abi 0.5.2", "libc", ] @@ -4461,6 +4492,23 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "pallas_vrf" +version = "0.1.0" +source = "git+https://github.com/txpipe/vrf?rev=62ef4c7252ed05df98611da191e20c0dd144f025#62ef4c7252ed05df98611da191e20c0dd144f025" +dependencies = [ + "clap 2.34.0", + "curve25519-dalek 3.2.0", + "getrandom 0.3.4", + "hex", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "serde", + "serde_json", + "sha2 0.9.9", + "thiserror 1.0.69", +] + [[package]] name = "parking" version = "2.2.1" @@ -4757,7 +4805,7 @@ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", - "hermit-abi", + "hermit-abi 0.5.2", "pin-project-lite", "rustix", "windows-sys 0.61.2", @@ -4994,6 +5042,16 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -5981,6 +6039,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ae9eec00137a8eed469fb4148acd9fc6ac8c3f9b110f52cd34698c8b5bfa0e" +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + [[package]] name = "strsim" version = "0.11.1" @@ -6148,6 +6212,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -6692,6 +6765,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "untrusted" version = "0.9.0" @@ -6790,6 +6869,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + [[package]] name = "version_check" version = "0.9.5" @@ -6802,18 +6887,6 @@ version = "0.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" -[[package]] -name = "vrf_dalek" -version = "0.1.0" -source = "git+https://github.com/txpipe/vrf?rev=044b45a1a919ba9d9c2471fc5c4d441f13086676#044b45a1a919ba9d9c2471fc5c4d441f13086676" -dependencies = [ - "curve25519-dalek 3.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "curve25519-dalek 3.2.0 (git+https://github.com/txpipe/curve25519-dalek?branch=ietf03_vrf_compat_ell2)", - "rand_core 0.5.1", - "sha2 0.9.9", - "thiserror 1.0.69", -] - [[package]] name = "walkdir" version = "2.5.0" diff --git a/modules/block_vrf_validator/Cargo.toml b/modules/block_vrf_validator/Cargo.toml index 1031be2d..a813d76a 100644 --- a/modules/block_vrf_validator/Cargo.toml +++ b/modules/block_vrf_validator/Cargo.toml @@ -27,7 +27,7 @@ pallas-math = { workspace = true } dashu-int = "0.4.1" # The vrf crate has not been fully tested in production environments and still has several upstream issues that are open PRs but not merged yet. -vrf_dalek = { git = "https://github.com/txpipe/vrf", rev = "044b45a1a919ba9d9c2471fc5c4d441f13086676" } +vrf_dalek = { package = "pallas_vrf", git = "https://github.com/txpipe/vrf", rev = "62ef4c7252ed05df98611da191e20c0dd144f025" } [lib] path = "src/block_vrf_validator.rs"