From dba341c7272e6e55d6d00f9c13f4312e742ca299 Mon Sep 17 00:00:00 2001 From: Steven Johnson Date: Tue, 17 Dec 2024 14:51:48 +0700 Subject: [PATCH 01/40] feat(rust): add cardano-blockchain-types crate --- .config/dictionaries/project.dic | 1 + rust/Cargo.toml | 1 + rust/Earthfile | 3 +- rust/cardano-blockchain-types/Cargo.toml | 37 + rust/cardano-blockchain-types/Justfile | 41 + rust/cardano-blockchain-types/Readme.md | 6 + rust/cardano-blockchain-types/deps.tmp | 62 ++ .../src/auxdata/aux_data.rs | 170 ++++ .../src/auxdata/block.rs | 68 ++ .../src/auxdata/metadatum.rs | 122 +++ .../src/auxdata/metadatum_label.rs | 40 + .../src/auxdata/metadatum_value.rs | 41 + .../src/auxdata/mod.rs | 12 + .../src/auxdata/scripts.rs | 127 +++ .../src/conversion.rs | 55 ++ .../src/data/Readme.md | 15 + .../src/data/mainnet-genesis.vkey | 1 + .../src/data/preprod-genesis.vkey | 1 + .../src/data/preview-genesis.vkey | 1 + rust/cardano-blockchain-types/src/hashes.rs | 138 ++++ rust/cardano-blockchain-types/src/lib.rs | 25 + .../src/multi_era_block_data.rs | 728 ++++++++++++++++++ rust/cardano-blockchain-types/src/network.rs | 395 ++++++++++ rust/cardano-blockchain-types/src/point.rs | 588 ++++++++++++++ rust/cardano-blockchain-types/src/slot.rs | 35 + .../cardano-blockchain-types/src/txn_index.rs | 23 + .../src/txn_witness.rs | 143 ++++ .../test_data/allegra.block | 1 + .../test_data/alonzo.block | 1 + .../test_data/babbage.block | 1 + .../test_data/byron.block | 1 + .../test_data/conway_tx_rbac/Readme.md | 33 + .../test_data/conway_tx_rbac/conway_1.block | 1 + .../test_data/conway_tx_rbac/conway_2.block | 1 + .../test_data/conway_tx_rbac/conway_3.block | 1 + .../test_data/mary.block | 1 + .../test_data/shelley.block | 1 + .../12345/immutable/12345.chunk | 0 .../12346/immutable/12346.chunk | 0 .../12347/immutable/12347.chunk | 0 .../123abc/immutable/123abc.chunk | 0 41 files changed, 2920 insertions(+), 1 deletion(-) create mode 100644 rust/cardano-blockchain-types/Cargo.toml create mode 100644 rust/cardano-blockchain-types/Justfile create mode 100644 rust/cardano-blockchain-types/Readme.md create mode 100644 rust/cardano-blockchain-types/deps.tmp create mode 100644 rust/cardano-blockchain-types/src/auxdata/aux_data.rs create mode 100644 rust/cardano-blockchain-types/src/auxdata/block.rs create mode 100644 rust/cardano-blockchain-types/src/auxdata/metadatum.rs create mode 100644 rust/cardano-blockchain-types/src/auxdata/metadatum_label.rs create mode 100644 rust/cardano-blockchain-types/src/auxdata/metadatum_value.rs create mode 100644 rust/cardano-blockchain-types/src/auxdata/mod.rs create mode 100644 rust/cardano-blockchain-types/src/auxdata/scripts.rs create mode 100644 rust/cardano-blockchain-types/src/conversion.rs create mode 100644 rust/cardano-blockchain-types/src/data/Readme.md create mode 100644 rust/cardano-blockchain-types/src/data/mainnet-genesis.vkey create mode 100644 rust/cardano-blockchain-types/src/data/preprod-genesis.vkey create mode 100644 rust/cardano-blockchain-types/src/data/preview-genesis.vkey create mode 100644 rust/cardano-blockchain-types/src/hashes.rs create mode 100644 rust/cardano-blockchain-types/src/lib.rs create mode 100644 rust/cardano-blockchain-types/src/multi_era_block_data.rs create mode 100644 rust/cardano-blockchain-types/src/network.rs create mode 100644 rust/cardano-blockchain-types/src/point.rs create mode 100644 rust/cardano-blockchain-types/src/slot.rs create mode 100644 rust/cardano-blockchain-types/src/txn_index.rs create mode 100644 rust/cardano-blockchain-types/src/txn_witness.rs create mode 100644 rust/cardano-blockchain-types/test_data/allegra.block create mode 100644 rust/cardano-blockchain-types/test_data/alonzo.block create mode 100644 rust/cardano-blockchain-types/test_data/babbage.block create mode 100644 rust/cardano-blockchain-types/test_data/byron.block create mode 100644 rust/cardano-blockchain-types/test_data/conway_tx_rbac/Readme.md create mode 100644 rust/cardano-blockchain-types/test_data/conway_tx_rbac/conway_1.block create mode 100644 rust/cardano-blockchain-types/test_data/conway_tx_rbac/conway_2.block create mode 100644 rust/cardano-blockchain-types/test_data/conway_tx_rbac/conway_3.block create mode 100644 rust/cardano-blockchain-types/test_data/mary.block create mode 100644 rust/cardano-blockchain-types/test_data/shelley.block create mode 100644 rust/cardano-blockchain-types/test_data/test_snapshot_id/12345/immutable/12345.chunk create mode 100644 rust/cardano-blockchain-types/test_data/test_snapshot_id/12346/immutable/12346.chunk create mode 100644 rust/cardano-blockchain-types/test_data/test_snapshot_id/12347/immutable/12347.chunk create mode 100644 rust/cardano-blockchain-types/test_data/test_snapshot_id/123abc/immutable/123abc.chunk diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index 7463a94cb..d96079470 100644 --- a/.config/dictionaries/project.dic +++ b/.config/dictionaries/project.dic @@ -10,6 +10,7 @@ Arissara asyncio Attributes auditability +auxdata babystep backpressure bech diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e01edc7e3..282be4d5f 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -2,6 +2,7 @@ resolver = "2" members = [ "c509-certificate", + "cardano-blockchain-types", "cardano-chain-follower", "hermes-ipfs", "cbork", diff --git a/rust/Earthfile b/rust/Earthfile index 04ec97997..e673e1eb2 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -9,6 +9,7 @@ COPY_SRC: Cargo.toml clippy.toml deny.toml rustfmt.toml \ .cargo .config \ c509-certificate \ + cardano-blockchain-types \ cardano-chain-follower \ catalyst-voting vote-tx-v1 vote-tx-v2 \ cbork cbork-abnf-parser cbork-cddl-parser \ @@ -53,7 +54,7 @@ build: DO rust-ci+EXECUTE \ --cmd="/scripts/std_build.py" \ - --args1="--libs=c509-certificate --libs=cardano-chain-follower --libs=hermes-ipfs" \ + --args1="--libs=c509-certificate --libs=cardano-blockchain-types --libs=cardano-chain-follower --libs=hermes-ipfs" \ --args2="--libs=cbork-cddl-parser --libs=cbork-abnf-parser" \ --args3="--libs=catalyst-voting --libs=vote-tx-v1 --libs=vote-tx-v2" \ --args4="--bins=cbork/cbork --libs=rbac-registration --libs=signed_doc" \ diff --git a/rust/cardano-blockchain-types/Cargo.toml b/rust/cardano-blockchain-types/Cargo.toml new file mode 100644 index 000000000..f13d51bee --- /dev/null +++ b/rust/cardano-blockchain-types/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "cardano-blockchain-types" +description = "Common Cardano Blockchain data types for use in both applications and crates" +keywords = ["cardano", "catalyst",] +version = "0.0.1" +authors = [ + "Steven Johnson " +] +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[lints] +workspace = true + +[dependencies] +pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } +pallas-hardano = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } +pallas-crypto = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } + +ouroboros = "0.18.4" +tracing = "0.1.41" +anyhow = "1.0.94" +chrono = "0.4.39" +strum = { version = "0.26.3", features = ["derive"] } +strum_macros = "0.26.4" +dirs = "5.0.1" +hex = "0.4.3" +dashmap = "6.1.0" +blake2b_simd = "1.0.2" +minicbor = { version = "0.25.1", features = ["alloc"] } +num-traits = "0.2.19" +ed25519-dalek = "2.1.1" \ No newline at end of file diff --git a/rust/cardano-blockchain-types/Justfile b/rust/cardano-blockchain-types/Justfile new file mode 100644 index 000000000..877c88b05 --- /dev/null +++ b/rust/cardano-blockchain-types/Justfile @@ -0,0 +1,41 @@ +# use with https://github.com/casey/just +# +# Cardano chain follower developer convenience functions + +# Format the rust code +code-format: + cargo +nightly fmtfix + cargo +nightly fmtchk + +# Run long running developer test for mithril downloading. +run-mithril-download-example-preprod: code-format + cargo build -r --package cardano-chain-follower --example follow_chains --features mimalloc + RUST_LOG="error,follow_chains=debug,cardano_chain_follower=debug,mithril-client=debug" \ + ../target/release/examples/follow_chains --preprod + +run-mithril-download-example-preprod-high-dl-bandwidth: code-format + cargo build -r --package cardano-chain-follower --example follow_chains --features mimalloc + RUST_LOG="error,follow_chains=debug,cardano_chain_follower=debug,mithril-client=debug" \ + ../target/release/examples/follow_chains --preprod --mithril-sync-workers 64 --mithril-sync-chunk-size 16 --mithril-sync-queue-ahead=6 + +run-mithril-download-example-preprod-conservative-dl-bandwidth: code-format + cargo build -r --package cardano-chain-follower --example follow_chains --features mimalloc + RUST_LOG="error,follow_chains=debug,cardano_chain_follower=debug,mithril-client=debug" \ + ../target/release/examples/follow_chains --preprod --mithril-sync-workers 8 --mithril-sync-chunk-size 1 --mithril-sync-queue-ahead=2 + +run-mithril-download-example-preview: code-format + cargo build -r --package cardano-chain-follower --example follow_chains --features mimalloc + RUST_LOG="error,follow_chains=debug,cardano_chain_follower=debug,mithril-client=debug" \ + ../target/release/examples/follow_chains --preview + +# Run long running developer test for mithril downloading. +run-mithril-download-example-mainnet: code-format + cargo build -r --package cardano-chain-follower --example follow_chains --features mimalloc + RUST_LOG="error,follow_chains=debug,cardano_chain_follower=debug,mithril-client=debug" \ + ../target/release/examples/follow_chains --mainnet + +# Run long running developer test for mithril downloading. +debug-heap-mithril-download-example: + cargo build --package cardano-chain-follower --example follow_chains + RUST_LOG="error,follow_chains=debug,cardano_chain_follower=debug,mithril-client=debug" \ + heaptrack ../target/debug/examples/follow_chains --preprod diff --git a/rust/cardano-blockchain-types/Readme.md b/rust/cardano-blockchain-types/Readme.md new file mode 100644 index 000000000..afa508052 --- /dev/null +++ b/rust/cardano-blockchain-types/Readme.md @@ -0,0 +1,6 @@ +# Improved version of Pallas Multi Era Block + +Adds features to the Pallas Multi Era Block to allow us to re-use it between the different cardano crates, +and the services that use them. + +The original source was `cardano-chain-follower`. diff --git a/rust/cardano-blockchain-types/deps.tmp b/rust/cardano-blockchain-types/deps.tmp new file mode 100644 index 000000000..e0b47377c --- /dev/null +++ b/rust/cardano-blockchain-types/deps.tmp @@ -0,0 +1,62 @@ + +rbac-registration = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.8" } + +thiserror = "1.0.64" +tokio = { version = "1.40.0", features = [ + "macros", + "rt", + "net", + "rt-multi-thread", +] } +tracing = "0.1.40" +tracing-log = "0.2.0" +dashmap = "6.1.0" +url = "2.5.2" +anyhow = "1.0.89" +chrono = "0.4.38" +async-trait = "0.1.83" +dirs = "5.0.1" +futures = "0.3.31" +humantime = "2.1.0" +crossbeam-skiplist = "0.1.3" +crossbeam-channel = "0.5.13" +crossbeam-epoch = "0.9.18" +strum = "0.26.3" +ouroboros = "0.18.4" +hex = "0.4.3" +rayon = "1.10.0" +serde = "1.0.210" +serde_json = "1.0.128" +mimalloc = { version = "0.1.43", optional = true } +memx = "0.1.32" +fmmap = { version = "0.3.3", features = ["sync", "tokio-async"] } +minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] } +zstd = "0.13.2" +ed25519-dalek = "2.1.1" +blake2b_simd = "1.0.2" +num-traits = "0.2.19" +logcall = "0.1.9" +tar = "0.4.42" +ureq = { version = "2.10.1", features = ["native-certs"] } +http = "1.1.0" +hickory-resolver = { version = "0.24.1", features = ["dns-over-rustls"] } +moka = { version = "0.12.8", features = ["sync"] } + +hex = "0.4.3" +anyhow = "1.0.89" +strum_macros = "0.26.4" +regex = "1.11.0" +minicbor = { version = "0.25.1", features = ["alloc", "derive", "half"] } +brotli = "7.0.0" +zstd = "0.13.2" +x509-cert = "0.2.5" +der-parser = "9.0.0" +bech32 = "0.11.0" +dashmap = "6.1.0" +blake2b_simd = "1.0.2" +tracing = "0.1.40" +ed25519-dalek = "2.1.1" +uuid = "1.11.0" + +c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git" , tag = "v0.0.3" } +pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" } diff --git a/rust/cardano-blockchain-types/src/auxdata/aux_data.rs b/rust/cardano-blockchain-types/src/auxdata/aux_data.rs new file mode 100644 index 000000000..445ee7b95 --- /dev/null +++ b/rust/cardano-blockchain-types/src/auxdata/aux_data.rs @@ -0,0 +1,170 @@ +//! Auxiliary Data Decoding + +use minicbor::Decode; + +use super::{ + metadatum::Metadata, + metadatum_label::MetadatumLabel, + metadatum_value::MetadatumValue, + scripts::{MutableTransactionScriptsMap, ScriptArray, ScriptType, TransactionScripts}, +}; + +/// Auxiliary Data (Metadata) for a single Transaction in a block +#[derive(Clone, Debug)] +#[allow(clippy::module_name_repetitions)] +pub struct TransactionAuxData { + /// Metadata attached to a transaction + metadata: Metadata, + /// Scripts attached to a transaction + #[allow(dead_code)] + scripts: TransactionScripts, +} + +impl Decode<'_, ()> for TransactionAuxData { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + // Check what kind of aux data we have to deal with + match d.datatype() { + // Shelley: https://github.com/IntersectMBO/cardano-ledger/blob/78b32d585fd4a0340fb2b184959fb0d46f32c8d2/eras/conway/impl/cddl-files/conway.cddl#L522 + Ok(minicbor::data::Type::Map) => { + Ok(TransactionAuxData { + metadata: Metadata::decode(d, &mut ())?, + scripts: TransactionScripts::default(), + }) + }, + // Shelley-MA: https://github.com/IntersectMBO/cardano-ledger/blob/78b32d585fd4a0340fb2b184959fb0d46f32c8d2/eras/conway/impl/cddl-files/conway.cddl#L523 + Ok(minicbor::data::Type::Array) => Self::decode_shelley_ma_array(d), + // Maybe Alonzo and beyond: https://github.com/IntersectMBO/cardano-ledger/blob/78b32d585fd4a0340fb2b184959fb0d46f32c8d2/eras/conway/impl/cddl-files/conway.cddl#L526 + Ok(minicbor::data::Type::Tag) => Self::decode_alonzo_plus_map(d), + Ok(unexpected) => { + let msg = format!( + "Error decoding Transaction Aux Data: Unexpected datatype {unexpected}" + ); + Err(minicbor::decode::Error::message(&msg)) + }, + Err(error) => { + let msg = format!("Error decoding Transaction Aux Data: {error}"); + Err(minicbor::decode::Error::message(msg)) + }, + } + } +} + +impl TransactionAuxData { + /// Get metadata with the given label. + #[must_use] + pub fn metadata(&self, label: MetadatumLabel) -> Option<&MetadatumValue> { + self.metadata.get(label) + } + + /// Decode a Shelley-MA Auxiliary Data Array + fn decode_shelley_ma_array(d: &mut minicbor::Decoder) -> Result { + match d.array() { + Ok(Some(entries)) => { + if entries != 2 { + let msg = format!( + "Error decoding Transaction Aux Data: Script Data Array Expected 2 entries, found {entries}." + ); + return Err(minicbor::decode::Error::message(&msg)); + } + }, + Ok(None) => { + return Err(minicbor::decode::Error::message( + "Error decoding Transaction Aux Data: Indefinite Array found decoding Metadata. Invalid.")); + }, + Err(error) => { + return Err(minicbor::decode::Error::message(format!( + "Error decoding Transaction Aux Data: {error}." + ))); + }, + }; + + let metadata = Metadata::decode(d, &mut ())?; + let script_array = ScriptArray::decode(d, &mut ScriptType::Native)?; + + let scripts = MutableTransactionScriptsMap::default(); + scripts.insert(ScriptType::Native, script_array); + + Ok(Self { + metadata, + scripts: scripts.into(), + }) + } + + /// Decode an Alonzo Plus MAP + fn decode_alonzo_plus_map(d: &mut minicbor::Decoder) -> Result { + match d.tag() { + Ok(tag) => { + if tag.as_u64() != 259 { + return Err(minicbor::decode::Error::message(format!( + "Invalid tag for Alonzo+ Aux Data. Expected 259, found {tag}." + ))); + } + }, + Err(error) => { + return Err(minicbor::decode::Error::message(format!( + "Error decoding Transaction Alonzo+ Aux Data: {error}." + ))); + }, + } + + let entries = match d.map() { + Ok(Some(entries)) => entries, + Ok(None) => { + return Err(minicbor::decode::Error::message( + "Indefinite Map found decoding Alonzo+ Metadata. Invalid.", + )) + }, + Err(error) => { + return Err(minicbor::decode::Error::message(format!( + "Error decoding Transaction Alonzo+ Aux Data: {error}." + ))) + }, + }; + + // Make the default versions of the metadata and script types + let mut metadata = Metadata::default(); + let scripts = MutableTransactionScriptsMap::default(); + + // iterate the map + for _ in 0..entries { + let script_type = match d.u64() { + Ok(key) => { + if let Ok(script_type) = ScriptType::try_from(key) { + script_type + } else { + // Only fails if its Metadata and not a script. + if metadata.is_empty() { + metadata = Metadata::decode(d, &mut ())?; + continue; + } + return Err(minicbor::decode::Error::message( + "Multiple Alonzo+ Metadata entries found. Invalid.", + )); + } + }, + + Err(error) => { + return Err(minicbor::decode::Error::message(format!( + "Error decoding Alonzo+ Metadata Aux Data Type Key: {error}" + ))); + }, + }; + + let mut ctx = script_type; + + let script_array = ScriptArray::decode(d, &mut ctx)?; + if scripts.insert(script_type, script_array).is_some() { + return Err(minicbor::decode::Error::message( + "Multiple Alonzo+ Script entries of type {script_type} found. Invalid.", + )); + } + } + + Ok(Self { + metadata, + scripts: scripts.into(), + }) + } +} diff --git a/rust/cardano-blockchain-types/src/auxdata/block.rs b/rust/cardano-blockchain-types/src/auxdata/block.rs new file mode 100644 index 000000000..6d8940b11 --- /dev/null +++ b/rust/cardano-blockchain-types/src/auxdata/block.rs @@ -0,0 +1,68 @@ +//! Decoded Metadata for a Block + +use std::sync::Arc; + +use anyhow::bail; +use dashmap::DashMap; +use pallas::ledger::traverse::MultiEraBlock; + +use super::aux_data::TransactionAuxData; +use crate::txn_index::TxnIndex; + +/// Auxiliary Data for every transaction within a block. +#[derive(Debug)] +#[allow(clippy::module_name_repetitions)] +pub struct BlockAuxData(Arc>); + +impl BlockAuxData { + /// Get `TransactionAuxData` for the given `TxnIndex` if any. + #[must_use] + pub fn get(&self, txn_idx: TxnIndex) -> Option<&TransactionAuxData> { + self.0.get(&txn_idx) + } +} + +impl Default for BlockAuxData { + fn default() -> Self { + BlockAuxData(Arc::new(DashMap::default().into_read_only())) + } +} + +impl TryFrom<&MultiEraBlock<'_>> for BlockAuxData { + type Error = anyhow::Error; + + fn try_from(block: &MultiEraBlock) -> Result { + let aux_data = DashMap::::new(); + // Note, while this code looks redundant, it is not because all the types are not + // compatible Even though they have similar names, and ultimately the same inner + // functionality. This means we need to distinctly encode the three different + // loops with the same code. + if block.has_aux_data() { + if let Some(_metadata) = block.as_byron() { + // Nothing to do here. + } else if let Some(alonzo_block) = block.as_alonzo() { + for (txn_idx, metadata) in alonzo_block.auxiliary_data_set.iter() { + let mut d = minicbor::Decoder::new(metadata.raw_cbor()); + let txn_aux_data = d.decode::()?; + aux_data.insert(TxnIndex::from_saturating(*txn_idx), txn_aux_data); + } + } else if let Some(babbage_block) = block.as_babbage() { + for (txn_idx, metadata) in babbage_block.auxiliary_data_set.iter() { + let mut d = minicbor::Decoder::new(metadata.raw_cbor()); + let txn_aux_data = d.decode::()?; + aux_data.insert(TxnIndex::from_saturating(*txn_idx), txn_aux_data); + } + } else if let Some(conway_block) = block.as_conway() { + for (txn_idx, metadata) in conway_block.auxiliary_data_set.iter() { + let mut d = minicbor::Decoder::new(metadata.raw_cbor()); + let txn_aux_data = d.decode::()?; + aux_data.insert(TxnIndex::from_saturating(*txn_idx), txn_aux_data); + } + } else { + bail!("Undecodable metadata, unknown Era"); + }; + } + + Ok(Self(Arc::new(aux_data.into_read_only()))) + } +} diff --git a/rust/cardano-blockchain-types/src/auxdata/metadatum.rs b/rust/cardano-blockchain-types/src/auxdata/metadatum.rs new file mode 100644 index 000000000..88da589e1 --- /dev/null +++ b/rust/cardano-blockchain-types/src/auxdata/metadatum.rs @@ -0,0 +1,122 @@ +//! Raw metadata + +use std::sync::Arc; + +use dashmap::DashMap; +use minicbor::Decode; + +use super::{metadatum_label::MetadatumLabel, metadatum_value::MetadatumValue}; +use crate::conversion::from_saturating; + +/// Transaction Metadata +/// See: +#[derive(Clone, Debug)] +pub struct Metadata(Arc); + +#[derive(Clone, Debug)] +/// Transaction Metadata - Inner +/// See: +pub struct MetadataInner { + /// Sequence the metadatum labels appear in the metadata. + seq: Vec, + /// K/V of metadata items. + map: dashmap::ReadOnlyView, +} + +impl Default for MetadataInner { + fn default() -> Self { + Self { + seq: Vec::new(), + map: DashMap::default().into_read_only(), + } + } +} + +impl Metadata { + /// Does the metadata contain the label? + #[must_use] + pub fn contains(&self, label: MetadatumLabel) -> bool { + self.0.map.contains_key(&label) + } + + /// Get the requested labels value + #[must_use] + pub fn get(&self, label: MetadatumLabel) -> Option<&MetadatumValue> { + self.0.map.get(&label) + } + + /// Are there any entries + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.seq.len() == 0 + } +} + +impl Default for Metadata { + fn default() -> Self { + Metadata(Arc::new(MetadataInner::default())) + } +} + +impl Decode<'_, ()> for Metadata { + fn decode( + d: &mut minicbor::Decoder<'_>, ctx: &mut (), + ) -> Result { + let (entries, mut sequence, metadata) = match d.map() { + Ok(Some(entries)) => { + ( + entries, + Vec::with_capacity(from_saturating(entries)), + DashMap::with_capacity(from_saturating(entries)), + ) + }, + Ok(None) => { + // Sadly... Indefinite Maps are allowed in Cardano CBOR Encoding. + (u64::MAX, Vec::new(), DashMap::new()) + }, + Err(error) => { + return Err(minicbor::decode::Error::message(format!( + "Error decoding Metadata map: {error}" + ))); + }, + }; + + for _ in 0..entries { + let label = MetadatumLabel::decode(d, ctx)?; + let value = MetadatumValue::decode(d, ctx)?; + + sequence.push(label); + let _unused = metadata.insert(label, value); + + // Look for End Sentinel IF its an indefinite MAP + // (which we know because entries is u64::MAX). + if entries == u64::MAX { + match d.datatype() { + Ok(minicbor::data::Type::Break) => { + // Skip over the break token. + let _unused = d.skip(); + break; + }, + Ok(_) => (), // Not break, so do next loop, should be the next key. + Err(error) => { + return Err(minicbor::decode::Error::message(format!( + "Error decoding indefinite Metadata map end sentinel: {error}" + ))); + }, + } + } + } + + // Reduce metadata map and seq to smallest size. + sequence.shrink_to_fit(); + metadata.shrink_to_fit(); + + // Make map immutable + let metadata = metadata.into_read_only(); + + Ok(Self(Arc::new(MetadataInner { + seq: sequence, + map: metadata, + }))) + } +} diff --git a/rust/cardano-blockchain-types/src/auxdata/metadatum_label.rs b/rust/cardano-blockchain-types/src/auxdata/metadatum_label.rs new file mode 100644 index 000000000..2729ae713 --- /dev/null +++ b/rust/cardano-blockchain-types/src/auxdata/metadatum_label.rs @@ -0,0 +1,40 @@ +//! Metadatum label + +use minicbor::Decode; + +/// The identifying key for the Metadata item. +/// See: +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct MetadatumLabel(u64); + +impl MetadatumLabel { + // TODO: Add all the known labels from https://github.com/cardano-foundation/CIPs/blob/master/CIP-0010/registry.json + + /// CIP-020 Message Metadatum Label + pub const CIP020_MESSAGE: MetadatumLabel = MetadatumLabel(674); + /// CIP-036 Auxiliary Data Metadatum Label + pub const CIP036_AUXDATA: MetadatumLabel = MetadatumLabel(61283); + /// CIP-036 Registration Metadatum Label + pub const CIP036_REGISTRATION: MetadatumLabel = MetadatumLabel(61284); + /// CIP-036 Witness Metadatum Label + pub const CIP036_WITNESS: MetadatumLabel = MetadatumLabel(61285); + /// CIP-XXX X509 RBAC Registration Metadatum Label + pub const CIP509_RBAC: MetadatumLabel = MetadatumLabel(509); +} + +impl Decode<'_, ()> for MetadatumLabel { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + let label = match d.u64() { + Ok(key) => key, + Err(error) => { + return Err(minicbor::decode::Error::message(format!( + "Error decoding Metadatum label: {error}" + ))); + }, + }; + + Ok(Self(label)) + } +} diff --git a/rust/cardano-blockchain-types/src/auxdata/metadatum_value.rs b/rust/cardano-blockchain-types/src/auxdata/metadatum_value.rs new file mode 100644 index 000000000..a8c725204 --- /dev/null +++ b/rust/cardano-blockchain-types/src/auxdata/metadatum_value.rs @@ -0,0 +1,41 @@ +//! Metadatum value + +use std::sync::Arc; + +use minicbor::Decode; + +/// Metadatum CBOR Encoded value +/// See: +#[derive(Clone, Debug)] +pub struct MetadatumValue(Arc>); + +impl Decode<'_, ()> for MetadatumValue { + fn decode( + d: &mut minicbor::Decoder<'_>, _ctx: &mut (), + ) -> Result { + // Get the start of the raw CBOR value we are going to extract. + let value_start = d.position(); + if let Err(error) = d.skip() { + return Err(minicbor::decode::Error::message(format!( + "Error decoding Metadatum value: {error}" + ))); + } + // Get the end of the raw value + let value_end = d.position(); + let Some(value_slice) = d.input().get(value_start..value_end) else { + return Err(minicbor::decode::Error::message( + "Error decoding Metadatum value: Unable to extract raw value slice.", + )); + }; + + // Intentionally copy the data into a vec, so that we don't have any self-reference + // issues. + Ok(Self(Arc::new(value_slice.to_vec()))) + } +} + +impl AsRef<[u8]> for MetadatumValue { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} diff --git a/rust/cardano-blockchain-types/src/auxdata/mod.rs b/rust/cardano-blockchain-types/src/auxdata/mod.rs new file mode 100644 index 000000000..7a0192b57 --- /dev/null +++ b/rust/cardano-blockchain-types/src/auxdata/mod.rs @@ -0,0 +1,12 @@ +//! Metadata decoding and validating. + +// We CAN NOT use the Pallas library metadata decoding because it does not preserve raw +// metadata values which are critical for performing operations like signature checks on +// data. So we have a bespoke metadata decoder here. + +pub mod aux_data; +pub mod block; +pub mod metadatum; +pub mod metadatum_label; +pub mod metadatum_value; +pub mod scripts; diff --git a/rust/cardano-blockchain-types/src/auxdata/scripts.rs b/rust/cardano-blockchain-types/src/auxdata/scripts.rs new file mode 100644 index 000000000..8109020c7 --- /dev/null +++ b/rust/cardano-blockchain-types/src/auxdata/scripts.rs @@ -0,0 +1,127 @@ +//! Smart Contract types + +use std::sync::Arc; + +use anyhow::anyhow; +use dashmap::DashMap; + +/// Raw, Script +#[derive(Clone, Default, Debug)] +#[allow(dead_code)] +pub struct Script(Arc>); + +impl minicbor::Decode<'_, ScriptType> for Script { + fn decode( + d: &mut minicbor::Decoder<'_>, ctx: &mut ScriptType, + ) -> Result { + let script_type = *ctx; + + if script_type == ScriptType::Native { + // Native Scripts are actually CBOR arrays, so capture their data as bytes for + // later processing. + // See: https://github.com/IntersectMBO/cardano-ledger/blob/78b32d585fd4a0340fb2b184959fb0d46f32c8d2/eras/conway/impl/cddl-files/conway.cddl#L542-L560 + let value_start = d.position(); + if let Err(error) = d.skip() { + return Err(minicbor::decode::Error::message(format!( + "Error decoding native script value: {error}" + ))); + } + let value_end = d.position(); + let Some(value_slice) = d.input().get(value_start..value_end) else { + return Err(minicbor::decode::Error::message( + "Invalid metadata value found. Unable to extract native script slice.", + )); + }; + Ok(Self(Arc::new(value_slice.to_vec()))) + } else { + // Plutus is encoded as a bytes string. Extract the script contents. + // See: https://github.com/IntersectMBO/cardano-ledger/blob/78b32d585fd4a0340fb2b184959fb0d46f32c8d2/eras/conway/impl/cddl-files/conway.cddl#L450-L452 + let script = match d.bytes() { + Ok(script) => script, + Err(error) => { + return Err(minicbor::decode::Error::message(format!( + "Error decoding plutus script data: {error}" + ))) + }, + }; + Ok(Self(Arc::new(script.to_vec()))) + } + } +} + +/// Array of Scripts +#[derive(Default, Clone, Debug)] +#[allow(dead_code)] +pub struct ScriptArray(Arc>); + +impl minicbor::Decode<'_, ScriptType> for ScriptArray { + fn decode( + d: &mut minicbor::Decoder<'_>, ctx: &mut ScriptType, + ) -> Result { + let mut scripts: Vec