diff --git a/Cargo.lock b/Cargo.lock index d237f4ca..d6f3f452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -50,7 +50,7 @@ dependencies = [ "serde", "serde_json", "serde_with 3.15.1", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.17", "tokio", @@ -126,6 +126,29 @@ dependencies = [ "tracing", ] +[[package]] +name = "acropolis_module_block_vrf_validator" +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]] name = "acropolis_module_chain_store" version = "0.1.0" @@ -476,6 +499,7 @@ dependencies = [ "acropolis_module_address_state", "acropolis_module_assets_state", "acropolis_module_block_unpacker", + "acropolis_module_block_vrf_validator", "acropolis_module_chain_store", "acropolis_module_consensus", "acropolis_module_drdd_state", @@ -1243,6 +1267,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" @@ -1920,6 +1953,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" @@ -2031,6 +2089,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" @@ -2109,7 +2187,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", ] @@ -2178,11 +2256,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", ] @@ -2605,6 +2683,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" @@ -2614,7 +2703,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -3557,7 +3646,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "wasi", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.61.2", ] @@ -3585,7 +3674,7 @@ dependencies = [ "mithril-common", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "slog", "thiserror 2.0.17", "tokio", @@ -3651,7 +3740,7 @@ dependencies = [ "serde_bytes", "serde_json", "serde_with 3.15.1", - "sha2", + "sha2 0.10.9", "slog", "strum", "thiserror 2.0.17", @@ -3799,6 +3888,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" @@ -4015,7 +4119,7 @@ dependencies = [ "rand 0.9.2", "rc2", "sha1", - "sha2", + "sha2 0.10.9", "thiserror 2.0.17", "x509-parser", ] @@ -4223,6 +4327,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" @@ -4607,7 +4723,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -4701,7 +4817,7 @@ dependencies = [ "der", "pbkdf2", "scrypt", - "sha2", + "sha2 0.10.9", "spki", ] @@ -5007,6 +5123,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" @@ -5547,7 +5666,7 @@ checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" dependencies = [ "pbkdf2", "salsa20", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -5793,6 +5912,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" @@ -5944,6 +6076,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" @@ -6756,6 +6894,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" @@ -6781,6 +6931,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" diff --git a/Cargo.toml b/Cargo.toml index 035c1edb..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 @@ -49,6 +50,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 17edce8f..2892e8e8 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -45,8 +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 +caryatid_process = { workspace = true } +config = { workspace = true } [lib] crate-type = ["rlib"] diff --git a/common/src/genesis_values.rs b/common/src/genesis_values.rs index 33781b81..8776d212 100644 --- a/common/src/genesis_values.rs +++ b/common/src/genesis_values.rs @@ -1,6 +1,12 @@ -use crate::calculations::{ - epoch_to_first_slot_with_shelley_params, slot_to_epoch_with_shelley_params, - slot_to_timestamp_with_params, +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 = "1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81"; @@ -10,7 +16,8 @@ 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, } impl GenesisValues { @@ -19,10 +26,59 @@ 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", + ( + "d9e5c76ad5ee778960804094a389f0b546b5c2b140a62f8ec43ea54d", + "64fa87e8b29a5b7bfbd6795677e3e878c505bc4a3649485d366b50abadec92d7", + ), + ), + ( + "b9547b8a57656539a8d9bc42c008e38d9c8bd9c8adbb1e73ad529497", + ( + "855d6fc1e54274e331e34478eeac8d060b0b90c1f9e8a2b01167c048", + "66d5167a1f426bd1adcc8bbf4b88c280d38c148d135cb41e3f5a39f948ad7fcc", + ), + ), + ( + "60baee25cbc90047e83fd01e1e57dc0b06d3d0cb150d0ab40bbfead1", + ( + "7f72a1826ae3b279782ab2bc582d0d2958de65bd86b2c4f82d8ba956", + "c0546d9aa5740afd569d3c2d9c412595cd60822bb6d9a4e8ce6c43d12bd0f674", + ), + ), + ( + "f7b341c14cd58fca4195a9b278cce1ef402dc0e06deb77e543cd1757", + ( + "69ae12f9e45c0c9122356c8e624b1fbbed6c22a2e3b4358cf0cb5011", + "6394a632af51a32768a6f12dac3485d9c0712d0b54e3f389f355385762a478f2", + ), + ), + ( + "162f94554ac8c225383a2248c245659eda870eaa82d0ef25fc7dcd82", + ( + "4485708022839a7b9b8b639a939c85ec0ed6999b5b6dc651b03c43f6", + "aba81e764b71006c515986bf7b37a72fbb5554f78e6775f08e384dbd572a4b32", + ), + ), + ( + "2075a095b3c844a29c24317a94a643ab8e22d54a3a3a72a420260af6", + ( + "6535db26347283990a252313a7903a45e3526ec25ddba381c071b25b", + "fcaca997b8105bd860876348fc2c6e68b13607f9bbd23515cd2193b555d267af", + ), + ), + ( + "268cfc0b89e910ead22e0ade91493d8212f53f3e2164b2e4bef0819b", + ( + "1d4f2e1fda43070d71bb22a5522f86943c7c18aeb4fa47a362c27e23", + "63ef48bc5355f3e7973100c371d6a095251c80ceb40559f4750aa7014a6fb6db", + ), + ), + ]) + .unwrap(), } } diff --git a/common/src/messages.rs b/common/src/messages.rs index 6ae7ff7f..a7f9d1c7 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, 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,7 +182,7 @@ pub struct EpochActivityMessage { pub spo_blocks: Vec<(PoolId, usize)>, /// Nonce - pub nonce: Option, + pub nonce: Option, } #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] diff --git a/common/src/protocol_params.rs b/common/src/protocol_params.rs index 8cc1a5a6..e713d38c 100644 --- a/common/src/protocol_params.rs +++ b/common/src/protocol_params.rs @@ -9,8 +9,8 @@ use anyhow::{bail, 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 { @@ -272,6 +272,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 { @@ -290,6 +299,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) + } +} + impl From for Nonce { fn from(hash: BlockHash) -> Self { Self { 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 41ecc62e..ec19d7d5 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}, @@ -115,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:?}") } } @@ -627,6 +628,9 @@ impl TxOutRef { } } +/// Slot +pub type Slot = u64; + /// Amount of Ada, in Lovelace pub type Lovelace = u64; pub type LovelaceDelta = i64; @@ -706,14 +710,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}")) } } @@ -724,16 +724,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}"), } } @@ -759,8 +758,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}'" )), } } @@ -1267,7 +1265,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 { @@ -1299,7 +1297,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, "") @@ -1397,7 +1395,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)] @@ -1738,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/common/src/validation.rs b/common/src/validation.rs index 1501af67..d0e55735 100644 --- a/common/src/validation.rs +++ b/common/src/validation.rs @@ -3,13 +3,17 @@ // We don't use these types in the acropolis_common crate itself #![allow(dead_code)] +use std::array::TryFromSliceError; + use thiserror::Error; +use crate::{protocol_params::Nonce, GenesisKeyhash, PoolId, Slot, VrfKeyHash}; + /// 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, @@ -27,3 +31,196 @@ 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:** 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), + /// **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 new file mode 100644 index 00000000..8b14ef55 --- /dev/null +++ b/modules/block_vrf_validator/Cargo.toml @@ -0,0 +1,35 @@ +# 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 } +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 new file mode 100644 index 00000000..69ecd04f --- /dev/null +++ b/modules/block_vrf_validator/src/block_vrf_validator.rs @@ -0,0 +1,264 @@ +//! 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}, + BlockInfo, BlockStatus, +}; +use anyhow::Result; +use caryatid_sdk::{module, Context, Module, Subscription}; +use config::Config; +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; +mod vrf_validation_publisher; + +const DEFAULT_VALIDATION_VRF_PUBLISHER_TOPIC: (&str, &str) = + ("validation-vrf-publisher-topic", "cardano.validation.vrf"); + +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_BLOCKS_SUBSCRIBE_TOPIC: (&str, &str) = + ("blocks-subscribe-topic", "cardano.block.proposed"); +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) = + ("spdd-subscribe-topic", "cardano.spo.distribution"); + +/// 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 { + #[allow(clippy::too_many_arguments)] + async fn run( + history: Arc>>, + mut vrf_validation_publisher: VrfValidationPublisher, + mut bootstrapped_subscription: Box>, + mut blocks_subscription: Box>, + mut protocol_parameters_subscription: Box>, + mut epoch_activity_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() { + 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); + 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 + 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 { + // read epoch boundary messages + let protocol_parameters_message_f = protocol_parameters_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(); + + 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((block_info, CardanoMessage::ProtocolParams(msg))) => { + Self::check_sync(¤t_block, block_info); + state.handle_protocol_parameters(msg); + } + _ => error!("Unexpected message type: {protocol_parameters_msg:?}"), + }); + + let (_, epoch_activity_msg) = epoch_activity_message_f.await?; + let span = info_span!( + "block_vrf_validator.handle_epoch_activity", + epoch = block_info.epoch + ); + 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_activity(msg); + } + _ => error!("Unexpected message type: {epoch_activity_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(( + 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); + } + _ => { + error!("Unexpected message type: {spo_state_msg:?} or {spdd_msg:?}") + } + }); + } + + let span = + info_span!("block_vrf_validator.validate", block = block_info.number); + async { + 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) + .await; + } + _ => error!("Unexpected message type: {message:?}"), + } + + // Commit the new state + if let Some(block_info) = current_block { + history.lock().await.commit(block_info.number, state); + } + } + } + + 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 blocks_subscribe_topic = config + .get_string(DEFAULT_BLOCKS_SUBSCRIBE_TOPIC.0) + .unwrap_or(DEFAULT_BLOCKS_SUBSCRIBE_TOPIC.1.to_string()); + info!("Creating blocks subscription on '{blocks_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) + .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}'"); + + // 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 = + context.subscribe(&protocol_parameters_subscribe_topic).await?; + let blocks_subscription = context.subscribe(&blocks_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?; + + // 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, + vrf_validation_publisher, + bootstrapped_subscription, + blocks_subscription, + protocol_parameters_subscription, + epoch_activity_subscription, + spo_state_subscription, + spdd_subscription, + ) + .await + .unwrap_or_else(|e| error!("Failed: {e}")); + }); + + 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/modules/block_vrf_validator/src/ouroboros/data/4490511.cbor b/modules/block_vrf_validator/src/ouroboros/data/4490511.cbor new file mode 100644 index 00000000..3c5ac69a --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/data/4490511.cbor @@ -0,0 +1 @@ +828f1a0044850f1a00448e005820f8084c61b6a238acec985b59310b6ecec49c0ab8352249afd7268da5cff2a45758209180d818e69cd997e34663c418a648c076f2e19cd4194e486e159d8580bc6cda58206d930cc9d1baade5cd1c70fbc025d3377ce946760c48e511d1abdf8acff6ff1c82584036ec5378d1f5041a59eb8d96e61de96f0950fb41b49ff511f7bc7fd109d4383e1d24be7034e6749c6612700dd5ceb0c66577b88a19ae286b1321d15bce1ab7365850405aa370ff009544a2be4aa5bc52c4456333f4f9b6571d66c5590d2e5629d08b3a3609f4bd0502ed5d0be1abdb7f2ab76aaeae47fe111b0335a4e4def64693162794b8d3c1ca71500f16b1e244724c03825840da5ccc9f8fd62f6c290b5bb2ed9a4258fc9481dd8a0ac80f8936702ad7709a87814d14dca02ce22d7a3e150d171e57914cc058081941e3c6737987524076b6935850402fcbd1d50e4b42fa81dce2eada18df1803af49273d05cbe4a1870983d86759a61005d5d942a53fb68389fe119b5dae823fa3cd6668dc257556bc52086fd879b082dace60edcf0cd592f63103bcd10e0358201033376be025cb705fd8dd02eda11cc73975a062b5d14ffd74d6ff69e69a2ff75820ae981f4a58d135f98d0a0c5aab9fc04944c8409f65187f9778b57905e43769570000584008d56fe9c28eeeb99bda8920a9887f468f1d74bb03c37ef3caffffbee68d88eb33341498ac145a2422c77546db61c2da782587cdf934d69b113ce52ab8bd8b0b02005901c03de52bc307718194c07824cdc0d2fa016e60b19bc1c7162978fdac55cf43d2ac7f3aea635ffc98d53570a98ac19c703dfda935aac478d84f0281f7c8bc7a180b6aa7e738e9381ebb86baf842af4ee229a3e3908cfec16153f1dd5e21dba0316e7625e4c2a8db495d80ca3c93862fcf4ad0a8a79a20f7d3d9733f9dccbb8ac6d623bbec2bb6dbf170a3c33d32a15d2cc02b3ce78c8d61af768fa82067254b2fe3bd7a1e5cdacf71b50998eec98a48b6970eacdc74d5ae40224f95850fbd88df96e192767c641a70ebf2bb9dc7d32a1e36d1113c09f5591622b35aed5c06bf7b894b882c56bd8e12b7da5b16a341e59d3239a3d14f9d0a5ac006dfdc258e215eacb563954ea234797754b9276f9746d1d6589e10f0dfda8130d422db636d02dec7c8b45ff97de40ac541421e3f2d1aed4246cc5bb54600fa567275046a979f3b4d067c0869732129234df68f1150b09285ecf71385034c79ace9e74186bd811770fcd43a449aa9466165731f92ae73ba4ef9c665a1f8851420a8b1568b711fabca735f474a6a800751f73fbbb3ed2ace0c8bdb2c14eed4032b085dd0ac8f6b0d984a441d337ffe6903aa794728a56360e106c84fc1b943af0e383fd1f1939a2416 \ No newline at end of file diff --git a/modules/block_vrf_validator/src/ouroboros/data/4556956.cbor b/modules/block_vrf_validator/src/ouroboros/data/4556956.cbor new file mode 100644 index 00000000..68eb2b74 --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/data/4556956.cbor @@ -0,0 +1 @@ +828f1a0045889c1a0058e1515820d5852411975dc3c5da5042f48372aa533b96c6d24017f9291cf65e730b5835555820330b133b489d79d039c7feceefe5acb9149f636d5d84943788ce8783c1c098245820e9dffe03f31be0431b2199696250f94ec599916ff5618080b428ac7c5ea5f6108258403962c816bc3989fe2b817e3a8f8deb54f6b0cf69105c7b041b3f1e90dfbf05a96c8912e11046647053d8e0c43fddf984082231946d3250a4182659e9b18cdd39585084e17ca6179f8d416f9bdc11f39714cb546ba0bc2274cf9d2c560a83d76942a31c55f30e95b310a9db9726c81cf37913f1a687dee0f21b5c553d863d393a4b1128425bb11901a6dc0a5bcd5d2eae82028258400018db71ca133eaa423d45ebfc33b6dbe2d35296f89e4ca3fe09810d8805255414ad3b5c97d0c3c7eca86d8d4c07035aa479bd6af9d9e986b8c4f9a5f76c9382585024df0024d93acdc1eb8b2a3a7b0f348fba765a4f27590eb6420458b34507ee655089866644bee297644c63fe8bc2aa3574a6b6a76e34fcaa5ce4010dd2648fb7118fbc7b6899024fd6369c43155c450f0358201033376be025cb705fd8dd02eda11cc73975a062b5d14ffd74d6ff69e69a2ff7582029429acc1d7d18b05039b379a7fe94dc95bd86631433ecb41b523e7d04a974c801015840c2b36ecd955d90a8adac85b4aa8a3dbdf022108794a255763c84c460db3b6eca45fb26dd5bc5cce9812f3199527e2c249473de89bd459888f12c964f58b1fc0c01005901c0f04b785404897ff6e361e76bc9a5f62c5b1af03e255a6110b09fc3be9d50ba1f2314076c09320a9bc9b755b6f9483325f9dbdab108bd8e412a63b1e15e54d60d17a33403716c4d5a57e851b90f21569cfdf4b7cd74ceb725d9173714d8ea7dda528f6848b0001f921382cdd64ae1ba60d7f725af64f10c572ed56ae28e187e4fe11c45ea269b508bcb6c08748be32e9bcf27df9c8853dc7272a278e60120f7fbe927d8614bbc3a1d9bab4a00199313f2f81f6b62a2f35496c04c1d7f8dae984695270c741298d567251066675609c71743a4cf61b525eabbea0f27d14e53a558210dbc7397354abda4d323b5b82460b6c8a1cb654f62af1962bfd3ec1a818e016be9e84c4ea1007a2edb5d392ea5cd84e4d92f2dd92fb1d80502f18df873a29915616650b4a697289bfec648eb72f0867bb1c6bd9c85fc25215078139f770a30e79b0f764bbef90a3933a82bb4f190e418f361c0426da2521c47c9330e96c876d8ec496117bf9a3cdb89420018e33e2977077fff74120fe8385ecd7729aee75742b31c09d4112ac7e777aced184056e8de92c3519e26ffdc98d85e3bd46725488bcad48a5da411985577277605a9c65a82f5224c179ac2da52be2e497b37de6b \ No newline at end of file diff --git a/modules/block_vrf_validator/src/ouroboros/data/4576496.cbor b/modules/block_vrf_validator/src/ouroboros/data/4576496.cbor new file mode 100644 index 00000000..c58d4f5f --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/data/4576496.cbor @@ -0,0 +1 @@ +828f1a0045d4f01a005eebcd5820d0547673599cc31048011657b1ac2ae14ff15b2103702b4071fd0c0e1caaeecf5820330b133b489d79d039c7feceefe5acb9149f636d5d84943788ce8783c1c098245820e9dffe03f31be0431b2199696250f94ec599916ff5618080b428ac7c5ea5f610825840ce902041f973eb263968e1de7f6255de7a9ddcafa343c2d2ed89c4daaca121d87621f683634ef07ff8234f69e381bd593e6229e92b2416fb463e5b50f1df675a5850b6568f768f3e4ca83dee729f934b83438fd693945f6c08110c1d9963024c904c1f836b51792a839aaedeb9381f65273abee234ea9e3205810e08ec03413244e2345aa58992d60e30aa38c3191f11db03825840000f783edf754904b325197136c4cdf5badf6a32ee7b86758d005aed320eb298efcd137da7507a51a7c580211017b14b08267c947b179358ebd5ee77d6b473b4585051632cef6eb7a950d94086d8f8770028d32ff55dc39741f7d45e061fd558383d5e18eb15aa6192a9c0bd16a5dbfcbee0dc041d158a2c50e67e80d3fc4a78dab889a5b703ab815afece18d00ed0431f0019018c582040fd0376795eec5777148337d4a5d37b72c358c30da034de8bac5e23c4525407582029429acc1d7d18b05039b379a7fe94dc95bd86631433ecb41b523e7d04a974c801015840c2b36ecd955d90a8adac85b4aa8a3dbdf022108794a255763c84c460db3b6eca45fb26dd5bc5cce9812f3199527e2c249473de89bd459888f12c964f58b1fc0c01005901c0c28b49147a742ee1a357f459315234446df337e2933df741307268fc064c32c0d465e2785f29ac9a79bfe55db85be4f3e36bfc2b7e15f76db91c46cbc2d27601b08c6020c1aee33d51ab7a66f9432d61d1dbb5bc3a7f6b183d414b94ffacbb459600a4ef2b27ece3c5682b50d1316dff928d7e627cb4f6754321e6465acb16cd360c41571150cd17dafdb642a27bbd5e80ce24a30ac2126a5c27fd866f7e43ab6c446de810e15924cf0b05873c6f4e49e518c139333d6d47c1ae709771ac5abf95270c741298d567251066675609c71743a4cf61b525eabbea0f27d14e53a558210dbc7397354abda4d323b5b82460b6c8a1cb654f62af1962bfd3ec1a818e016be9e84c4ea1007a2edb5d392ea5cd84e4d92f2dd92fb1d80502f18df873a29915616650b4a697289bfec648eb72f0867bb1c6bd9c85fc25215078139f770a30e79b0f764bbef90a3933a82bb4f190e418f361c0426da2521c47c9330e96c876d8ec496117bf9a3cdb89420018e33e2977077fff74120fe8385ecd7729aee75742b31c09d4112ac7e777aced184056e8de92c3519e26ffdc98d85e3bd46725488bcad48a5da411985577277605a9c65a82f5224c179ac2da52be2e497b37de6b \ No newline at end of file diff --git a/modules/block_vrf_validator/src/ouroboros/data/7854823.cbor b/modules/block_vrf_validator/src/ouroboros/data/7854823.cbor new file mode 100644 index 00000000..1199b9b6 --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/data/7854823.cbor @@ -0,0 +1 @@ +828a1a0077dae71a046344c15820991e0770751806103894d8fffaf86fcc90f238c6ac00bdc7cb0a953e4a59fd93582041ec779d32f5ca1fc81a8cb4152e6bc1f0ab569abe1c0de8b66d76242d23a70f5820b5b6a6b790e6926f6ba856255aaf3eb5192cc80c03eeec7469cd3f64344bafb78258403f42ef44b8525ee45ca45a094191f00b359e9205a400b0607c87fe488f3b44442afc9e7c3e28ded9080c71d84eb7840ecb2e7240721ee243b45a761048ad6c53585054ac3b2bc94b54705b951146a83cba92041fdf537644628c64e9549365e74c10502fb7c23f34d7daa8327843f728513b225044a9390f86fb2a83951c4a39a13c17472c71cf50d74f1ce12d04dc85e9021a00015b1158202c1657b98fabe337cff8811a0b198dd36dfa61a415fdc5e5759f43cfa1103c48845820249caec7a56b2ede7dfdb956178fc6d7d5c96b5f35e63ddc7987e9261ac277c70b1902065840c4bbbda856bebac009d32c276080a54bb6322ab3252d09ad4c7c4a3e3f9b8931dd81b1795dbd165d8d3ca493d77966eb682bfff0edc1d151ef0ad42efb91ba0d8207005901c03824c9afe0176e3abf9e9bcf6972fb8c408d218fe0eab8026780a9995c7cc8fbf2a6f7d54fcb138bcff109b578d020baf5dbae751dad81980d52146afc615905ea103db0ff10d488fc9d0dbfa410066768d93c566fc346ff4b81bd41137df20c446771dd0d24399d61a9caeddce7abb55e6647247158ac252cf87e56871f4535857f6dbbf90b99d9b4bd6ca20705f7deff4e7d3220c002c4e7e5d96927751d96e08c92886ac223a4f7141625da97819a3b574dbf80ca50418f33ce2858b41f2fe63290bda8fcfe8382defc7ce721ed6d96466efe69a0bcb31a3951447b3e73c07e167240a99b492a71b0f9a6af321620d16b944b7b7ce89353ce0b65fc19f3744357603fca23f2b0056c607f817b24ee15b51008ab43e470016e46c0a1589bf184b46b79e808750f7a96fa747387414b9e691b95e7003e825308e4cd4fcf67d87233cb90282e3a5e65ce5cd3448e2a5d5ee6b232a431a45c252ed9a04dbee9cf89c5d5f975481b3be2623bf4a517205b69516d59e0a1877643e25485846911252315499cc705061ee74bd8a6ca2a9ffe46f2905839372b008074d775b6f6f7bb3f245263d306f0194133f947909ff4af9f9815800f1173ef7ea602886f55e1fd \ No newline at end of file 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..9d6b274e --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/mod.rs @@ -0,0 +1,5 @@ +pub mod overlay_schedule; +pub mod praos; +pub mod tpraos; +pub mod vrf; +pub mod vrf_validation; diff --git a/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs b/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs new file mode 100644 index 00000000..ba03dc83 --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/overlay_schedule.rs @@ -0,0 +1,174 @@ +//! 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::{ + rational_number::RationalNumber, GenesisDelegate, GenesisDelegates, GenesisKeyhash, +}; +use anyhow::Result; +use num_traits::ToPrimitive; + +#[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(GenesisKeyhash, GenesisDelegate), +} + +/// 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_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; + + 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_delegs` - Set of genesis node key hashes and their delegations +/// * `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: &GenesisDelegates, + decentralisation_param: &RationalNumber, + active_slots_coeff: &RationalNumber, +) -> Result { + 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_f64() + .ok_or_else(|| anyhow::anyhow!("Failed to convert active slots coefficient to f64"))? + .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, 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_delegs` - 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 +/// +/// # 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: &GenesisDelegates, + decentralisation_param: &RationalNumber, + active_slots_coeff: &RationalNumber, +) -> 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, + 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().next().unwrap(), + genesis_delegs.as_ref().values().next().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/praos.rs b/modules/block_vrf_validator/src/ouroboros/praos.rs new file mode 100644 index 00000000..e6275faf --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/praos.rs @@ -0,0 +1,153 @@ +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, + rational_number::RationalNumber, + validation::{VrfValidation, VrfValidationError}, + BlockInfo, PoolId, VrfKeyHash, +}; +use anyhow::Result; +use pallas::ledger::{primitives::VrfCert, traverse::MultiEraHeader}; +use std::collections::HashMap; + +pub fn validate_vrf_praos<'a>( + block_info: &'a BlockInfo, + header: &'a MultiEraHeader, + epoch_nonce: &'a Nonce, + active_slots_coeff: RationalNumber, + active_spos: &'a HashMap, + active_spdd: &'a HashMap, + total_active_stake: u64, +) -> Result>, Box> { + let Some(issuer_vkey) = header.issuer_vkey() else { + 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 = + 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); + + let Some(vrf_vkey) = header.vrf_vkey() else { + 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::Other("VRF Cert is not set".to_string()))?; + + // Regular TPraos rules apply + Ok(vec![ + Box::new(move || { + validate_leader_vrf_key(&pool_id, registered_vrf_key_hash, vrf_vkey)?; + Ok(()) + }), + Box::new(move || { + validate_praos_vrf_proof( + block_info.slot, + epoch_nonce, + &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()[..], + )?; + Ok(()) + }), + Box::new(move || { + 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, + )?; + Ok(()) + }), + ]) +} + +fn vrf_result<'a>(header: &'a MultiEraHeader) -> Option<&'a VrfCert> { + match header { + MultiEraHeader::BabbageCompatible(x) => Some(&x.header_body.vrf_result), + _ => None, + } +} + +#[cfg(test)] +mod tests { + use acropolis_common::{ + crypto::keyhash_256, protocol_params::NonceHash, serialization::Bech32Conversion, + BlockHash, BlockStatus, Era, + }; + + use super::*; + + #[test] + fn test_7854823_block() { + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("8dad163edf4607452fec9c5955d593fb598ca728bae162138f88da6667bba79b") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let active_slots_coeff = RationalNumber::new(1, 20); + + 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 = + PoolId::from_bech32("pool195gdnmj6smzuakm4etxsxw3fgh8asqc4awtcskpyfnkpcvh2v8t") + .unwrap(); + let active_spos = HashMap::from([( + pool_id, + VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), + )]); + let active_spdd = HashMap::from([(pool_id, 64590523391239)]); + let result = validate_vrf_praos( + &block_info, + &block_header, + &epoch_nonce, + active_slots_coeff, + &active_spos, + &active_spdd, + 25069171797357766, + ) + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_ok()); + } +} 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..65f21e3d --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/tpraos.rs @@ -0,0 +1,550 @@ +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, + rational_number::RationalNumber, + validation::{VrfValidation, VrfValidationError}, + BlockInfo, GenesisDelegates, PoolId, VrfKeyHash, +}; +use anyhow::Result; +use pallas::ledger::{primitives::VrfCert, traverse::MultiEraHeader}; +use std::collections::HashMap; + +#[allow(clippy::too_many_arguments)] +pub fn validate_vrf_tpraos<'a>( + block_info: &'a BlockInfo, + header: &'a MultiEraHeader, + epoch_nonce: &'a Nonce, + genesis_delegs: &'a GenesisDelegates, + active_slots_coeff: RationalNumber, + decentralisation_param: RationalNumber, + active_spos: &'a HashMap, + active_spdd: &'a HashMap, + total_active_stake: u64, +) -> Result>, Box> { + // first look up for overlay slot + let obft_slot = overlay_schedule::lookup_in_overlay_schedule( + block_info.epoch_slot, + genesis_delegs, + &decentralisation_param, + &active_slots_coeff, + ) + .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::Other( + "Issuer Key is not set".to_string(), + )) + })]); + }; + 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 })?; + + 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::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::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 || { + validate_leader_vrf_key(&pool_id, registered_vrf_key_hash, vrf_vkey)?; + Ok(()) + }), + Box::new(move || { + validate_tpraos_nonce_vrf_proof( + 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 || { + validate_tpraos_leader_vrf_proof( + 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 || { + validate_vrf_leader_value( + &leader_vrf_cert.0.to_vec()[..], + &relative_stake, + &active_slots_coeff, + )?; + Ok(()) + }), + ]) + } + 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::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::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 || { + validate_genesis_leader_vrf_key(&genesis_key, &gen_deleg, vrf_vkey)?; + Ok(()) + }), + Box::new(move || { + validate_tpraos_nonce_vrf_proof( + 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 || { + validate_tpraos_leader_vrf_proof( + 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![Box::new(|| { + Err(VrfValidationError::NotActiveSlotInOverlaySchedule { + slot: block_info.slot, + }) + })]) + } + } +} + +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, + } +} + +#[cfg(test)] +mod tests { + use acropolis_common::{ + crypto::keyhash_256, + genesis_values::GenesisValues, + protocol_params::NonceHash, + serialization::Bech32Conversion, + validation::{VrfLeaderValueTooBigError, WrongLeaderVrfKeyError}, + BlockHash, BlockStatus, Era, + }; + + use super::*; + + #[test] + fn test_4490511_block_produced_by_genesis_key() { + let genesis_value = GenesisValues::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("1a3be38bcbb7911969283716ad7aa550250226b76a61fc51cc9a9a35d9276d81") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let active_slots_coeff = RationalNumber::new(1, 20); + let decentralisation_param = RationalNumber::from(1); + + let block_header_4490511: Vec = + hex::decode(include_str!("./data/4490511.cbor")).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 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( + &block_info, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + active_slots_coeff, + decentralisation_param, + &active_spos, + &active_spdd, + 1, + ) + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_ok()); + } + + #[test] + fn test_4556956_block() { + let genesis_value = GenesisValues::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let active_slots_coeff = RationalNumber::new(1, 20); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4556956: Vec = + hex::decode(include_str!("./data/4556956.cbor")).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 block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4556956).unwrap(); + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = HashMap::from([( + pool_id, + VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), + )]); + let active_spdd = HashMap::from([(pool_id, 75284250207839)]); + let result = validate_vrf_tpraos( + &block_info, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + active_slots_coeff, + decentralisation_param, + &active_spos, + &active_spdd, + 10177811974823000, + ) + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_ok()); + } + + #[test] + fn test_4576496_block() { + let genesis_value = GenesisValues::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let active_slots_coeff = RationalNumber::new(1, 20); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4576496: Vec = + hex::decode(include_str!("./data/4576496.cbor")).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 block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = HashMap::from([( + pool_id, + VrfKeyHash::from(keyhash_256(block_header.vrf_vkey().unwrap())), + )]); + let active_spdd = HashMap::from([(pool_id, 75284250207839)]); + let result = validate_vrf_tpraos( + &block_info, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + active_slots_coeff, + decentralisation_param, + &active_spos, + &active_spdd, + 10177811974823000, + ) + .and_then(|vrf_validations| { + vrf_validations.iter().try_for_each(|assert| assert().map_err(Box::new)) + }); + assert!(result.is_ok()); + } + + #[test] + fn test_4576496_block_as_unknown_pool() { + let genesis_value = GenesisValues::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let active_slots_coeff = RationalNumber::new(1, 20); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4576496: Vec = + hex::decode(include_str!("./data/4576496.cbor")).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 block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); + 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, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + active_slots_coeff, + decentralisation_param, + &active_spos, + &active_spdd, + 10177811974823000, + ) + .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(), + Box::new(VrfValidationError::UnknownPool { pool_id }) + ); + } + + #[test] + fn test_4576496_block_as_wrong_leader_vrf_key() { + let genesis_value = GenesisValues::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let active_slots_coeff = RationalNumber::new(1, 20); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4576496: Vec = + hex::decode(include_str!("./data/4576496.cbor")).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 block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = + 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, + &epoch_nonce, + &genesis_value.genesis_delegs, + active_slots_coeff, + decentralisation_param, + &active_spos, + &active_spdd, + 10177811974823000, + ) + .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(), + 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() + )), + } + )) + ); + } + + #[test] + fn test_4576496_block_with_small_active_stake() { + let genesis_value = GenesisValues::mainnet(); + let epoch_nonce = Nonce::from( + NonceHash::try_from( + hex::decode("3fac34ac3d7d1ac6c976ba68b1509b1ee3aafdbf6de96e10789e488e13e16bd7") + .unwrap() + .as_slice(), + ) + .unwrap(), + ); + let active_slots_coeff = RationalNumber::new(1, 20); + let decentralisation_param = RationalNumber::new(9, 10); + + let block_header_4576496: Vec = + hex::decode(include_str!("./data/4576496.cbor")).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 block_header = + MultiEraHeader::decode(block_info.era as u8, None, &block_header_4576496).unwrap(); + let pool_id = + PoolId::from_bech32("pool1pu5jlj4q9w9jlxeu370a3c9myx47md5j5m2str0naunn2q3lkdy") + .unwrap(); + let active_spos: HashMap = HashMap::from([( + 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, 75284250207)]); + let result = validate_vrf_tpraos( + &block_info, + &block_header, + &epoch_nonce, + &genesis_value.genesis_delegs, + active_slots_coeff, + decentralisation_param, + &active_spos, + &active_spdd, + 10177811974823000, + ) + .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(), + Box::new(VrfValidationError::VrfLeaderValueTooBig( + VrfLeaderValueTooBigError::VrfLeaderValueTooBig + )) + ); + } +} diff --git a/modules/block_vrf_validator/src/ouroboros/vrf.rs b/modules/block_vrf_validator/src/ouroboros/vrf.rs new file mode 100644 index 00000000..139c59f7 --- /dev/null +++ b/modules/block_vrf_validator/src/ouroboros/vrf.rs @@ -0,0 +1,233 @@ +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; +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; + + /// 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); + 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) + } + + /// 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); + 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, Clone, 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, Clone, 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(Debug, Clone, serde::Serialize, serde::Deserialize)] + #[serde(remote = "super::VrfError")] + pub enum VrfError { + VerificationFailed, + DecompressionFailed, + PkSmallOrder, + 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..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/snapshot.rs b/modules/block_vrf_validator/src/snapshot.rs new file mode 100644 index 00000000..72de3eb5 --- /dev/null +++ b/modules/block_vrf_validator/src/snapshot.rs @@ -0,0 +1,36 @@ +use std::collections::HashMap; + +use acropolis_common::{ + messages::{SPOStakeDistributionMessage, SPOStateMessage}, + 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, + + /// 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, registration.vrf_key_hash)) + .collect(); + let active_stakes: HashMap = + 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, + total_active_stakes, + } + } +} diff --git a/modules/block_vrf_validator/src/state.rs b/modules/block_vrf_validator/src/state.rs new file mode 100644 index 00000000..8c5f919a --- /dev/null +++ b/modules/block_vrf_validator/src/state.rs @@ -0,0 +1,153 @@ +//! Acropolis block_vrf_validator state storage + +use std::sync::Arc; + +use crate::{ouroboros, snapshot::Snapshot}; +use acropolis_common::{ + genesis_values::GenesisValues, + messages::{ + EpochActivityMessage, ProtocolParamsMessage, SPOStakeDistributionMessage, SPOStateMessage, + }, + protocol_params::Nonce, + rational_number::RationalNumber, + validation::VrfValidationError, + BlockInfo, Era, +}; +use anyhow::Result; +use pallas::ledger::traverse::MultiEraHeader; +use tracing::error; + +#[derive(Default, Debug, Clone)] +pub struct EpochSnapshots { + pub mark: Arc, + pub set: Arc, +} + +impl EpochSnapshots { + /// Push a new snapshot + pub fn push(&mut self, latest: Snapshot) { + self.set = self.mark.clone(); + self.mark = Arc::new(latest); + } +} + +#[derive(Default, Debug, Clone)] +pub struct State { + pub decentralisation_param: Option, + + pub active_slots_coeff: Option, + + /// epoch nonce + pub epoch_nonce: Option, + + /// epoch snapshots + pub epoch_snapshots: EpochSnapshots, +} + +impl State { + pub fn new() -> Self { + Self { + active_slots_coeff: None, + decentralisation_param: None, + epoch_nonce: None, + epoch_snapshots: EpochSnapshots::default(), + } + } + + pub fn handle_protocol_parameters(&mut self, msg: &ProtocolParamsMessage) { + if let Some(shelley_params) = msg.params.shelley.as_ref() { + self.decentralisation_param = + Some(shelley_params.protocol_params.decentralisation_param); + self.active_slots_coeff = Some(shelley_params.active_slots_coeff); + } + } + + pub fn handle_epoch_activity(&mut self, msg: &EpochActivityMessage) { + self.epoch_nonce = msg.nonce.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, + raw_header: &[u8], + genesis: &GenesisValues, + ) -> 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(decentralisation_param) = self.decentralisation_param else { + return Err(Box::new(VrfValidationError::Other( + "Decentralisation Param is not set".to_string(), + ))); + }; + let Some(active_slots_coeff) = self.active_slots_coeff else { + return Err(Box::new(VrfValidationError::Other( + "Active Slots Coeff is not set".to_string(), + ))); + }; + let Some(epoch_nonce) = self.epoch_nonce.as_ref() else { + return Err(Box::new(VrfValidationError::Other( + "Epoch Nonce is not set".to_string(), + ))); + }; + + let is_tpraos = matches!( + block_info.era, + Era::Shelley | Era::Allegra | Era::Mary | Era::Alonzo + ); + + if is_tpraos { + let result = ouroboros::tpraos::validate_vrf_tpraos( + block_info, + &header, + epoch_nonce, + &genesis.genesis_delegs, + active_slots_coeff, + decentralisation_param, + &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().map_err(Box::new)) + }); + result + } else { + let result = ouroboros::praos::validate_vrf_praos( + block_info, + &header, + epoch_nonce, + active_slots_coeff, + &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().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 new file mode 100644 index 00000000..7dcbba3c --- /dev/null +++ b/modules/block_vrf_validator/src/vrf_validation_publisher.rs @@ -0,0 +1,52 @@ +use acropolis_common::{ + messages::{CardanoMessage, Message}, + validation::{ValidationError, ValidationStatus, VrfValidationError}, + 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 } + } + + 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: {} of block {}", + error.clone(), + block.number + ); + ValidationStatus::NoGo(ValidationError::from(error)) + } + }; + self.context + .message_bus + .publish( + &self.topic, + Arc::new(Message::Cardano(( + block.clone(), + CardanoMessage::BlockValidation(validation_status), + ))), + ) + .await + } +} 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/epochs_state.rs b/modules/epochs_state/src/epochs_state.rs index 4387c354..af8a5990 100644 --- a/modules/epochs_state/src/epochs_state.rs +++ b/modules/epochs_state/src/epochs_state.rs @@ -18,17 +18,15 @@ use pallas::ledger::traverse::MultiEraHeader; use std::sync::Arc; use tokio::sync::Mutex; use tracing::{error, info, info_span}; - mod epoch_activity_publisher; mod epochs_history; mod state; mod store_config; -use state::State; - use crate::{ epoch_activity_publisher::EpochActivityPublisher, epochs_history::EpochsHistoryState, store_config::StoreConfig, }; +use state::State; const DEFAULT_BOOTSTRAPPED_SUBSCRIBE_TOPIC: (&str, &str) = ( "bootstrapped-subscribe-topic", @@ -130,33 +128,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(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(|| { - if let Some(header) = header.as_ref() { - match state.handle_block_header(&genesis, block_info, header) { - Ok(()) => {} - Err(e) => error!("Error handling block header: {e}"), - } - } - }); - let span = info_span!("epochs_state.handle_mint", block = block_info.number); span.in_scope(|| { if let Some(header) = header.as_ref() { @@ -219,7 +209,7 @@ 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}'"); // query topic let epochs_query_topic = config @@ -339,8 +329,7 @@ impl EpochsState { } _ => EpochsStateQueryResponse::Error(QueryError::not_implemented(format!( - "Unimplemented query variant: {:?}", - query + "Unimplemented query variant: {query:?}" ))), }; Arc::new(Message::StateQueryResponse(StateQueryResponse::Epochs( diff --git a/modules/epochs_state/src/state.rs b/modules/epochs_state/src/state.rs index cbd3792b..08002465 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, @@ -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,13 +84,13 @@ 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()); } } - // Handle a block header - pub fn handle_block_header( + /// Evolve Nonces + pub fn evolve_nonces( &mut self, genesis: &GenesisValues, block_info: &BlockInfo, @@ -177,8 +177,7 @@ impl State { }; self.nonces = Some(new_nonces); - }; - + } Ok(()) } @@ -251,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()), } } @@ -525,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( @@ -555,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( @@ -619,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( @@ -691,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( diff --git a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs index b1777c74..c81ebc73 100644 --- a/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs +++ b/modules/genesis_bootstrapper/src/genesis_bootstrapper.rs @@ -3,12 +3,14 @@ use acropolis_common::{ genesis_values::GenesisValues, + hash::Hash, 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}; @@ -40,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 @@ -206,6 +208,23 @@ impl GenesisBootstrapper { shelley_epoch: shelley_start_epoch, shelley_epoch_len: shelley_genesis.epoch_length.unwrap() as u64, shelley_genesis_hash, + genesis_delegs: GenesisDelegates::try_from( + shelley_genesis + .gen_delegs + .unwrap() + .iter() + .map(|(key, value)| { + ( + key.as_str(), + ( + value.delegate.as_ref().unwrap().as_str(), + value.vrf.as_ref().unwrap().as_str(), + ), + ) + }) + .collect::>(), + ) + .unwrap(), }; // Send completion message diff --git a/modules/rest_blockfrost/src/handlers/pools.rs b/modules/rest_blockfrost/src/handlers/pools.rs index 4ddf256f..8d6ab23e 100644 --- a/modules/rest_blockfrost/src/handlers/pools.rs +++ b/modules/rest_blockfrost/src/handlers/pools.rs @@ -197,7 +197,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, "[]")); }; @@ -564,7 +564,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?; diff --git a/modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs b/modules/snapshot_bootstrapper/src/snapshot_bootstrapper.rs index 8da7b006..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::{ @@ -11,7 +12,7 @@ use acropolis_common::{ StreamingSnapshotParser, }, stake_addresses::AccountState, - BlockHash, BlockInfo, BlockStatus, Era, + BlockHash, BlockInfo, BlockStatus, Era, GenesisDelegates, }; use anyhow::Result; use caryatid_sdk::{module, Context, Module}; @@ -86,12 +87,12 @@ 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(), }) } diff --git a/modules/upstream_chain_fetcher/src/utils.rs b/modules/upstream_chain_fetcher/src/utils.rs index 415ed1c8..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::GenesisValues; use acropolis_common::messages::{CardanoMessage, Message}; +use acropolis_common::GenesisDelegates; use anyhow::{anyhow, bail, Result}; use caryatid_sdk::Context; use config::Config; @@ -92,6 +93,8 @@ impl FetcherConfig { shelley_epoch, shelley_epoch_len, shelley_genesis_hash, + // TODO: load genesis keys from config + genesis_delegs: GenesisDelegates::try_from(vec![]).unwrap(), }) } diff --git a/processes/omnibus/Cargo.toml b/processes/omnibus/Cargo.toml index 7814b5c8..93e991f9 100644 --- a/processes/omnibus/Cargo.toml +++ b/processes/omnibus/Cargo.toml @@ -30,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 ab2d4ca2..a978dbc3 100644 --- a/processes/omnibus/src/main.rs +++ b/processes/omnibus/src/main.rs @@ -12,6 +12,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_chain_store::ChainStore; use acropolis_module_consensus::Consensus; use acropolis_module_drdd_state::DRDDState; @@ -119,6 +120,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);