diff --git a/.github/discord.svg b/.github/discord.svg new file mode 100644 index 0000000..a915ff0 --- /dev/null +++ b/.github/discord.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..69d17ee --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,78 @@ +# Based on https://github.com/actions-rs/meta/blob/master/recipes/quickstart.md + +on: + push: { } + +name: Validate + +jobs: + check: + name: Check + strategy: + fail-fast: false + matrix: + os: [ windows-latest, ubuntu-latest, macOS-latest ] + rust: [ stable ] + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust }} + + - name: Run cargo check + run: cargo check + + test: + name: Test Suite + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Run cargo test + run: cargo test + + test-windows: + name: Test Suite Windows + runs-on: windows-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + + - name: Run cargo test + run: cargo test + + lints: + name: Lints + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + with: + toolchain: stable + components: rustfmt, clippy + + - name: Run cargo fmt + run: cargo fmt --all -- --check + + - name: Run cargo clippy + run: cargo clippy -- -D warnings diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3e72f08 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +**/Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +.idea/ + + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..72a654a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,6 @@ +[workspace] +resolver = "2" +members = [ + "ouroboros", + "ouroboros-praos", +] diff --git a/README.md b/README.md index 9823e78..b0a71a5 100644 --- a/README.md +++ b/README.md @@ -1 +1,18 @@ -Initial Commit +# Ouroboros + +![Build Status](https://github.com/pragma-org/ouroboros/actions/workflows/validate.yml/badge.svg?branch=main) + +Ouroboros is a family of proof-of-stake (PoS) consensus protocols used in blockchain technology. It was designed to be secure, scalable, and energy-efficient. Ouroboros is notable for being the first PoS protocol to be mathematically proven secure and for being the consensus algorithm behind the Cardano blockchain. Key features of Ouroboros include: + +**Proof-of-Stake**: Unlike proof-of-work (PoW) systems, Ouroboros relies on stakeholders to validate transactions and create new blocks, which significantly reduces energy consumption. + +**Security**: Ouroboros has been rigorously analyzed and proven secure under certain cryptographic assumptions. + +**Scalability**: The protocol is designed to support a large number of transactions per second, making it suitable for large-scale applications. + +**Incentives**: It includes mechanisms to incentivize honest behavior among participants, ensuring the network remains secure and efficient. +Ouroboros operates in epochs, which are divided into slots. In each slot, a slot leader is elected to add a block to the blockchain. The election process is based on the stake each participant holds, with higher stakes increasing the probability of being selected as a slot leader. + +## Repository Layout + +The ouroboros crate contains the generic traits related to any Ouroboros consensus protocol. The sub-libraries contain the specific implementations of Ouroboros, such as Ouroboros TPraos, Ouroboros Praos, and Ouroboros Genesis. diff --git a/ouroboros-praos/Cargo.toml b/ouroboros-praos/Cargo.toml new file mode 100644 index 0000000..e1263e0 --- /dev/null +++ b/ouroboros-praos/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "ouroboros-praos" +version = "0.1.0" +edition = "2021" + +[dependencies] +hex = "0.4" +ouroboros = { path = "../ouroboros" } +pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "5db91a6994922f423badb88f77b3f9423c7367af" } +pallas-math = { git = "https://github.com/txpipe/pallas", rev = "5db91a6994922f423badb88f77b3f9423c7367af" } +pallas-primitives = { git = "https://github.com/txpipe/pallas", rev = "5db91a6994922f423badb88f77b3f9423c7367af" } +tracing = "0.1" + +[dev-dependencies] +ctor = "0.2" +insta = { version = "1.40.0", features = ["yaml"] } +mockall = "0.13" +pallas-traverse = { git = "https://github.com/txpipe/pallas", rev = "5db91a6994922f423badb88f77b3f9423c7367af" } +tracing-subscriber = "0.3" \ No newline at end of file diff --git a/ouroboros-praos/src/consensus/mod.rs b/ouroboros-praos/src/consensus/mod.rs new file mode 100644 index 0000000..a7b9c85 --- /dev/null +++ b/ouroboros-praos/src/consensus/mod.rs @@ -0,0 +1,302 @@ +use ouroboros::ledger::{issuer_vkey_to_pool_id, PoolId, PoolInfo}; +use ouroboros::validator::Validator; +use pallas_crypto::hash::{Hash, Hasher}; +use pallas_crypto::vrf::{ + VrfProof, VrfProofBytes, VrfProofHashBytes, VrfPublicKey, VrfPublicKeyBytes, +}; +use pallas_math::math::{ExpOrdering, FixedDecimal, FixedPrecision}; +use pallas_primitives::babbage; +use pallas_primitives::babbage::{derive_tagged_vrf_output, VrfDerivation}; +use std::ops::Deref; +use std::sync::LazyLock; +use tracing::{error, span, trace, warn}; + +/// The certified natural max value represents 2^256 in praos consensus +static CERTIFIED_NATURAL_MAX: LazyLock = LazyLock::new(|| { + FixedDecimal::from_str( + "1157920892373161954235709850086879078532699846656405640394575840079131296399360000000000000000000000000000000000", + 34, + ) + .expect("Infallible") +}); + +/// Validator for a block using praos consensus. +pub struct BlockValidator<'b> { + header: &'b babbage::Header, + pool_info: &'b dyn PoolInfo, + epoch_nonce: &'b Hash<32>, + // c is the ln(1-active_slots_coeff). Usually ln(1-0.05) + c: &'b FixedDecimal, +} + +impl<'b> BlockValidator<'b> { + pub fn new( + header: &'b babbage::Header, + pool_info: &'b dyn PoolInfo, + epoch_nonce: &'b Hash<32>, + c: &'b FixedDecimal, + ) -> Self { + Self { + header, + pool_info, + epoch_nonce, + c, + } + } + + fn validate_babbage_compatible(&self) -> bool { + let span = span!(tracing::Level::TRACE, "validate_babbage_compatible"); + let _enter = span.enter(); + + // Grab all the values we need to validate the block + let issuer_vkey = &self.header.header_body.issuer_vkey; + let pool_id: PoolId = issuer_vkey_to_pool_id(issuer_vkey); + let vrf_vkey: VrfPublicKeyBytes = (&self.header.header_body.vrf_vkey).into(); + if !self.ledger_matches_block_vrf_key_hash(&pool_id, &vrf_vkey) { + // Fail fast if the vrf key hash in the block does not match the ledger + return false; + } + let sigma: FixedDecimal = match self.pool_info.sigma(&pool_id) { + Ok(sigma) => { + FixedDecimal::from(sigma.numerator) / FixedDecimal::from(sigma.denominator) + } + Err(error) => { + warn!("{:?} - {:?}", error, pool_id); + return false; + } + }; + let absolute_slot = self.header.header_body.slot; + + // Get the leader VRF output hash from the block vrf result + let leader_vrf_output = &self.header.header_body.leader_vrf_output(); + + let block_vrf_proof_hash: VrfProofHashBytes = + (&self.header.header_body.vrf_result.0).into(); + let block_vrf_proof: VrfProofBytes = (&self.header.header_body.vrf_result.1).into(); + let kes_signature = self.header.body_signature.as_slice(); + + trace!("pool_id: {}", pool_id); + trace!("block vrf_vkey: {}", hex::encode(vrf_vkey)); + trace!("sigma: {}", sigma); + trace!("absolute_slot: {}", absolute_slot); + trace!("leader_vrf_output: {}", hex::encode(leader_vrf_output)); + trace!( + "block_vrf_proof_hash: {}", + hex::encode(block_vrf_proof_hash.as_slice()) + ); + trace!( + "block_vrf_proof: {}", + hex::encode(block_vrf_proof.as_slice()) + ); + trace!("kes_signature: {}", hex::encode(kes_signature)); + + // Calculate the VRF input seed so we can verify the VRF output against it. + let vrf_input_seed = self.mk_vrf_input(absolute_slot, self.epoch_nonce.as_ref()); + trace!("vrf_input_seed: {}", vrf_input_seed); + + // Verify the VRF proof + let vrf_proof = VrfProof::from(&block_vrf_proof); + let vrf_vkey = VrfPublicKey::from(&vrf_vkey); + match vrf_proof.verify(&vrf_vkey, vrf_input_seed.as_ref()) { + Ok(proof_hash) => { + if proof_hash.as_slice() != block_vrf_proof_hash.as_slice() { + error!("VRF proof hash mismatch"); + false + } else { + // The proof was valid. Make sure that our leader_vrf_output matches what was in the block + trace!("certified_proof_hash: {}", hex::encode(proof_hash)); + let calculated_leader_vrf_output = + derive_tagged_vrf_output(proof_hash.as_slice(), VrfDerivation::Leader); + if calculated_leader_vrf_output.as_slice() != leader_vrf_output.as_slice() { + error!( + "Leader VRF output hash mismatch. was: {}, expected: {}", + hex::encode(calculated_leader_vrf_output), + hex::encode(leader_vrf_output) + ); + false + } else { + // The leader VRF output hash matches what was in the block + // Now we need to check if the pool had enough sigma stake to produce this block + if self.pool_meets_delegation_threshold( + &sigma, + absolute_slot, + leader_vrf_output.as_slice(), + ) { + // TODO: Validate the KES signature + true + } else { + false + } + } + } + } + Err(error) => { + error!("Could not verify block vrf: {}", error); + false + } + } + } + + /// Verify that the pool meets the delegation threshold + fn pool_meets_delegation_threshold( + &self, + sigma: &FixedDecimal, + absolute_slot: u64, + leader_vrf_output: &[u8], + ) -> bool { + let certified_leader_vrf: FixedDecimal = leader_vrf_output.into(); + let denominator = CERTIFIED_NATURAL_MAX.deref() - &certified_leader_vrf; + let recip_q = CERTIFIED_NATURAL_MAX.deref() / &denominator; + let x = -(sigma * self.c); + + trace!("certified_leader_vrf: {}", certified_leader_vrf); + trace!("denominator: {}", denominator); + trace!("recip_q: {}", recip_q); + trace!("c: {}", self.c); + trace!("x: {}", x); + + let ordering = x.exp_cmp(1000, 3, &recip_q); + match ordering.estimation { + ExpOrdering::LT => { + trace!( + "Slot: {} - IS Leader: {} < {}", + absolute_slot, + recip_q, + ordering.approx + ); + true + } + _ => { + trace!( + "Slot: {} - NOT Leader: {} >= {}", + absolute_slot, + recip_q, + ordering.approx + ); + false + } + } + } + + /// Validate that the VRF key hash in the block matches the VRF key hash in the ledger + fn ledger_matches_block_vrf_key_hash( + &self, + pool_id: &PoolId, + vrf_vkey: &VrfPublicKeyBytes, + ) -> bool { + let vrf_vkey_hash: Hash<32> = Hasher::<256>::hash(vrf_vkey); + trace!("block vrf_vkey_hash: {}", hex::encode(vrf_vkey_hash)); + let ledger_vrf_vkey_hash = match self.pool_info.vrf_vkey_hash(pool_id) { + Ok(ledger_vrf_vkey_hash) => ledger_vrf_vkey_hash, + Err(error) => { + warn!("{:?} - {:?}", error, pool_id); + return false; + } + }; + if vrf_vkey_hash != ledger_vrf_vkey_hash { + error!( + "VRF vkey hash in block ({}) does not match registered ledger vrf vkey hash ({})", + hex::encode(vrf_vkey_hash), + hex::encode(ledger_vrf_vkey_hash) + ); + return false; + } + true + } + + fn mk_vrf_input(&self, absolute_slot: u64, eta0: &[u8]) -> Hash<32> { + trace!("mk_vrf_input() absolute_slot {}", absolute_slot); + let mut hasher = Hasher::<256>::new(); + hasher.input(&absolute_slot.to_be_bytes()); + hasher.input(eta0); + hasher.finalize() + } +} + +impl Validator for BlockValidator<'_> { + fn validate(&self) -> bool { + self.validate_babbage_compatible() + } +} + +#[cfg(test)] +mod tests { + use crate::consensus::BlockValidator; + use ctor::ctor; + use mockall::predicate::eq; + use ouroboros::ledger::{MockPoolInfo, PoolId, PoolSigma}; + use ouroboros::validator::Validator; + use pallas_crypto::hash::Hash; + use pallas_math::math::{FixedDecimal, FixedPrecision}; + use pallas_traverse::MultiEraHeader; + + #[ctor] + fn init() { + // set rust log level to TRACE + // std::env::set_var("RUST_LOG", "ouroboros-praos=trace"); + + // initialize tracing crate + tracing_subscriber::fmt::init(); + } + + #[test] + fn test_validate_conway_block() { + let test_block = include_bytes!("../../tests/data/mainnet_blockheader_10817298.cbor"); + let test_block_hex = hex::encode(test_block); + insta::assert_snapshot!(test_block_hex); + let test_vector = vec![ + ( + "00beef0a9be2f6d897ed24a613cf547bb20cd282a04edfc53d477114", + "c0d1f9b040d2f6fd7fc8775d24753d6db4b697429f11404a6178a0a4a005867b", + "c7937fc47fecbe687891b3decd71e904d1e129598aa3852481d295eea3ea3ada", + 25626202470912_u64, + 22586623335121436_u64, + true, + ), + ( + "00beef0a9be2f6d897ed24a613cf547bb20cd282a04edfc53d477114", + "c0d1f9b040d2f6fd7fc8775d24753d6db4b697429f11404a6178a0a4a005867b", + "c7937fc47fecbe687891b3decd71e904d1e129598aa3852481d295eea3ea3ada", + 6026202470912_u64, + 22586623335121436_u64, + false, + ), + ]; + insta::assert_yaml_snapshot!(test_vector); + + for (pool_id_str, vrf_vkey_hash_str, epoch_nonce_str, numerator, denominator, expected) in + test_vector + { + let pool_id: PoolId = pool_id_str.parse().unwrap(); + let vrf_vkey_hash: Hash<32> = vrf_vkey_hash_str.parse().unwrap(); + let epoch_nonce: Hash<32> = epoch_nonce_str.parse().unwrap(); + + let active_slots_coeff: FixedDecimal = + FixedDecimal::from(5u64) / FixedDecimal::from(100u64); + let c = (FixedDecimal::from(1u64) - active_slots_coeff).ln(); + let conway_block_tag: u8 = 6; + let multi_era_header = + MultiEraHeader::decode(conway_block_tag, None, test_block).unwrap(); + let babbage_header = multi_era_header.as_babbage().expect("Infallible"); + assert_eq!(babbage_header.header_body.slot, 134402628u64); + + let mut pool_info = MockPoolInfo::new(); + pool_info + .expect_sigma() + .with(eq(pool_id)) + .returning(move |_| { + Ok(PoolSigma { + numerator, + denominator, + }) + }); + pool_info + .expect_vrf_vkey_hash() + .with(eq(pool_id)) + .returning(move |_| Ok(vrf_vkey_hash)); + + let block_validator = BlockValidator::new(babbage_header, &pool_info, &epoch_nonce, &c); + assert_eq!(block_validator.validate(), expected); + } + } +} diff --git a/ouroboros-praos/src/consensus/snapshots/ouroboros_praos__consensus__tests__validate_conway_block-2.snap b/ouroboros-praos/src/consensus/snapshots/ouroboros_praos__consensus__tests__validate_conway_block-2.snap new file mode 100644 index 0000000..e855468 --- /dev/null +++ b/ouroboros-praos/src/consensus/snapshots/ouroboros_praos__consensus__tests__validate_conway_block-2.snap @@ -0,0 +1,16 @@ +--- +source: ouroboros-praos/src/consensus/mod.rs +expression: test_vector +--- +- - 00beef0a9be2f6d897ed24a613cf547bb20cd282a04edfc53d477114 + - c0d1f9b040d2f6fd7fc8775d24753d6db4b697429f11404a6178a0a4a005867b + - c7937fc47fecbe687891b3decd71e904d1e129598aa3852481d295eea3ea3ada + - 25626202470912 + - 22586623335121436 + - true +- - 00beef0a9be2f6d897ed24a613cf547bb20cd282a04edfc53d477114 + - c0d1f9b040d2f6fd7fc8775d24753d6db4b697429f11404a6178a0a4a005867b + - c7937fc47fecbe687891b3decd71e904d1e129598aa3852481d295eea3ea3ada + - 6026202470912 + - 22586623335121436 + - false diff --git a/ouroboros-praos/src/consensus/snapshots/ouroboros_praos__consensus__tests__validate_conway_block.snap b/ouroboros-praos/src/consensus/snapshots/ouroboros_praos__consensus__tests__validate_conway_block.snap new file mode 100644 index 0000000..0a8b6d3 --- /dev/null +++ b/ouroboros-praos/src/consensus/snapshots/ouroboros_praos__consensus__tests__validate_conway_block.snap @@ -0,0 +1,5 @@ +--- +source: ouroboros-praos/src/consensus/mod.rs +expression: test_block_hex +--- +828a1a00a50f121a0802d24458203deea82abe788d260b8987a522aadec86c9f098e88a57d7cfcdb24f474a7afb65820cad3c900ca6baee9e65bf61073d900bfbca458eeca6d0b9f9931f5b1017a8cd65820576d49e98adfab65623dc16f9fff2edd210e8dd1d4588bfaf8af250beda9d3c7825840d944b8c81000fc1182ec02194ca9eca510fd84995d22bfe1842190b39d468e5ecbd863969e0c717b0071a371f748d44c895fa9233094cefcd3107410baabb19a5850f2a29f985d37ca8eb671c2847fab9cc45c93738a430b4e43837e7f33028b190a7e55152b0e901548961a66d56eebe72d616f9e68fd13e9955ccd8611c201a5b422ac8ef56af74cb657b5b868ce9d850f1945d15820639d4986d17de3cac8079a3b25d671f339467aa3a9948e29992dafebf90f719f8458202e5823037de29647e495b97d9dd7bf739f7ebc11d3701c8d0720f55618e1b292171903e958401feeeabc7460b19370f4050e986b558b149fdc8724b4a4805af8fe45c8e7a7c6753894ad7a1b9c313da269ddc5922e150da3b378977f1dfea79fc52fd2c12f088209015901c020f1c8f9ae672e6ec75b0aa63a85e7ab7865b95f6b2907a26b54c14f49184ab52cf98ef441bb71de50380325b34f16d84fc78d137467a1b49846747cf8ee4701c56f08f198b94c468d46b67b271f5bc30ab2ad14b1bdbf2be0695a00fe4b02b3060fa52128f4cce9c5759df0ba8d71fe99456bd2e333671e45110908d03a2ec3b38599d26adf182ba63f79900fdb2732947cf8e940a4cf1e8db9b4cf4c001dbd37c60d0e38851de4910807896153be455e13161342d4c6f7bb3e4d2d35dbbbba0ebcd161be2f1ec030d2f5a6059ac89dfa70dc6b3d0bc2da179c62ae95c4f9c7ad9c0387b35bf2b45b325d1e0a18c0c783a0779003bf23e7a6b00cc126c5e3d51a57d41ff1707a76fb2c306a67c21473b41f1d9a7f64a670ec172a2421da03d796fa97086de8812304f4f96bd45243d0a2ad6c48a69d9e2c0afbb1333acee607d18eb3a33818c3c9d5bb72cade889379008bf60d436298cb0cfc6159332cb1af1de4f1d64e79c399d058ac4993704eed67917093f89db6cde830383e69aa400ba3225087cabad45407b891416330485891dc9a3875488a26428d20d581b629a8f4f42e3aa00cbcaae6c8e2b8f3fe033b874d1de6a3f8c321c92b77643f00d28e diff --git a/ouroboros-praos/src/lib.rs b/ouroboros-praos/src/lib.rs new file mode 100644 index 0000000..4021407 --- /dev/null +++ b/ouroboros-praos/src/lib.rs @@ -0,0 +1 @@ +pub mod consensus; diff --git a/ouroboros-praos/tests/data/mainnet_blockheader_10817298.cbor b/ouroboros-praos/tests/data/mainnet_blockheader_10817298.cbor new file mode 100644 index 0000000..e4fdec7 Binary files /dev/null and b/ouroboros-praos/tests/data/mainnet_blockheader_10817298.cbor differ diff --git a/ouroboros/Cargo.toml b/ouroboros/Cargo.toml new file mode 100644 index 0000000..9063e17 --- /dev/null +++ b/ouroboros/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "ouroboros" +version = "0.1.0" +edition = "2021" + +[dependencies] +hex = "0.4.3" +mockall = "0.13" +pallas-codec = { git = "https://github.com/txpipe/pallas", rev = "5db91a6994922f423badb88f77b3f9423c7367af" } +pallas-crypto = { git = "https://github.com/txpipe/pallas", rev = "5db91a6994922f423badb88f77b3f9423c7367af" } +thiserror = "1.0" + +[dev-dependencies] +insta = { version = "1.40.0", features = ["yaml"] } \ No newline at end of file diff --git a/ouroboros/src/ledger/mod.rs b/ouroboros/src/ledger/mod.rs new file mode 100644 index 0000000..52a5c8c --- /dev/null +++ b/ouroboros/src/ledger/mod.rs @@ -0,0 +1,59 @@ +use mockall::automock; +use pallas_codec::utils::Bytes; +use pallas_crypto::hash::{Hash, Hasher}; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("PoolId not found")] + PoolIdNotFound, +} + +pub type PoolId = Hash<28>; + +/// The sigma value of a pool. This is a rational number that represents the total value of the +/// delegated stake in the pool over the total value of the active stake in the network. This value +/// is tracked in the ledger state and recorded as a snapshot value at each epoch. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PoolSigma { + pub numerator: u64, + pub denominator: u64, +} + +/// The pool info trait provides a lookup mechanism for pool data. This is sourced from the ledger +#[automock] +pub trait PoolInfo: Send + Sync { + /// Performs a lookup of a pool_id to its sigma value. This usually represents a different set of + /// sigma snapshot data depending on whether we need to look up the pool_id in the current epoch + /// or in the future. + fn sigma(&self, pool_id: &PoolId) -> Result; + + /// Hashes the vrf vkey of a pool. + fn vrf_vkey_hash(&self, pool_id: &PoolId) -> Result, Error>; +} + +/// The node's cold vkey is hashed with blake2b224 to create the pool id +pub fn issuer_vkey_to_pool_id(issuer_vkey: &Bytes) -> PoolId { + Hasher::<224>::hash(issuer_vkey) +} + +#[cfg(test)] +mod tests { + use crate::ledger::issuer_vkey_to_pool_id; + use pallas_codec::utils::Bytes; + + #[test] + fn test_issuer_vkey_to_pool_id() { + let test_vector = vec![( + "cad3c900ca6baee9e65bf61073d900bfbca458eeca6d0b9f9931f5b1017a8cd6", + "00beef0a9be2f6d897ed24a613cf547bb20cd282a04edfc53d477114", + )]; + insta::assert_yaml_snapshot!(test_vector); + + for (issuer_vkey_str, expected_pool_id_str) in test_vector { + let issuer_vkey: Bytes = issuer_vkey_str.parse().unwrap(); + let pool_id = issuer_vkey_to_pool_id(&issuer_vkey); + assert_eq!(pool_id.to_string(), expected_pool_id_str); + } + } +} diff --git a/ouroboros/src/ledger/snapshots/ouroboros__ledger__tests__issuer_vkey_to_pool_id.snap b/ouroboros/src/ledger/snapshots/ouroboros__ledger__tests__issuer_vkey_to_pool_id.snap new file mode 100644 index 0000000..e056008 --- /dev/null +++ b/ouroboros/src/ledger/snapshots/ouroboros__ledger__tests__issuer_vkey_to_pool_id.snap @@ -0,0 +1,6 @@ +--- +source: ouroboros/src/ledger/mod.rs +expression: test_vector +--- +- - cad3c900ca6baee9e65bf61073d900bfbca458eeca6d0b9f9931f5b1017a8cd6 + - 00beef0a9be2f6d897ed24a613cf547bb20cd282a04edfc53d477114 diff --git a/ouroboros/src/lib.rs b/ouroboros/src/lib.rs new file mode 100644 index 0000000..0f1ebd4 --- /dev/null +++ b/ouroboros/src/lib.rs @@ -0,0 +1,2 @@ +pub mod ledger; +pub mod validator; diff --git a/ouroboros/src/validator/mod.rs b/ouroboros/src/validator/mod.rs new file mode 100644 index 0000000..47ff76d --- /dev/null +++ b/ouroboros/src/validator/mod.rs @@ -0,0 +1,6 @@ +/// Generic trait for validating any type of data. Designed to be used across threads so validations +/// can be done in parallel. Validators should handle all error cases internally and simply return +/// a boolean indicating if the validation was successful. +pub trait Validator: Send + Sync { + fn validate(&self) -> bool; +}