diff --git a/.config/dictionaries/project.dic b/.config/dictionaries/project.dic index c6b5856020..ebd535eb85 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/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebdb814a16..0f9084eed6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: with: forge_version: 0.2.0 - test_reporting: - if: always() - needs: ci - uses: ./.github/workflows/generate-allure-report.yml \ No newline at end of file + #test_reporting: + # if: always() + # needs: ci + # uses: ./.github/workflows/generate-allure-report.yml \ No newline at end of file diff --git a/.github/workflows/generate-allure-report.yml b/.github/workflows/generate-allure-report.yml.disabled similarity index 85% rename from .github/workflows/generate-allure-report.yml rename to .github/workflows/generate-allure-report.yml.disabled index 4952fb4810..0c0972d877 100644 --- a/.github/workflows/generate-allure-report.yml +++ b/.github/workflows/generate-allure-report.yml.disabled @@ -13,10 +13,6 @@ concurrency: cancel-in-progress: true env: - AWS_REGION: eu-central-1 - AWS_ROLE_ARN: arn:aws:iam::332405224602:role/ci - EARTHLY_TARGET: docker - ECR_REGISTRY: 332405224602.dkr.ecr.eu-central-1.amazonaws.com ALLURE_REPORT_PATH: allure-report COVERAGE_REPORT_PATH: coverage-report REPORT_EXT: .junit-report.xml @@ -29,24 +25,22 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Setup CI - uses: input-output-hk/catalyst-ci/actions/setup@master + - name: Install Forge + uses: input-output-hk/catalyst-forge/actions/install@ci/v1.5.0 with: - aws_role_arn: ${{ env.AWS_ROLE_ARN }} - aws_region: ${{ env.AWS_REGION }} - earthly_runner_secret: ${{ secrets.EARTHLY_RUNNER_SECRET }} + version: 0.8.0 + if: always() + + - name: Setup CI + uses: input-output-hk/catalyst-forge/actions/setup@ci/v1.5.0 - name: Get catalyst libs unit test report - uses: input-output-hk/catalyst-ci/actions/run@master + uses: input-output-hk/catalyst-forge/actions/run@ci/v1.5.0 if: always() continue-on-error: true with: - earthfile: ./rust/ - flags: - targets: build - target_flags: - runner_address: ${{ secrets.EARTHLY_SATELLITE_ADDRESS }} - artifact: "false" + command: run + args: ./catalyst-libs+build - name: Collect and upload test reports uses: actions/upload-artifact@v4 @@ -107,7 +101,7 @@ jobs: - name: Comment PR with Allure report link if: ${{ always() && github.event_name == 'pull_request' && steps.allure.outputs.report_url }} - uses: thollander/actions-comment-pull-request@v2 + uses: thollander/actions-comment-pull-request@v3 with: message: | ${{ steps.allure.outputs.test_result_icon }} [Test Report](${{ steps.allure.outputs.report_url }}) | ${\color{lightgreen}Pass: ${{ steps.allure.outputs.test_result_passed }}/${{ steps.allure.outputs.test_result_total }}}$ | ${\color{red}Fail: ${{ steps.allure.outputs.test_result_failed }}/${{ steps.allure.outputs.test_result_total }}}$ | diff --git a/.github/workflows/semantic_pull_request.yml b/.github/workflows/semantic_pull_request.yml index 6b520e06d9..5b0ad2465e 100644 --- a/.github/workflows/semantic_pull_request.yml +++ b/.github/workflows/semantic_pull_request.yml @@ -26,6 +26,7 @@ jobs: rust/cbork rust/hermes-ipfs rust/rbac-registration + rust/cardano-blockchain-types dart docs general diff --git a/Earthfile b/Earthfile index 1dae7616c3..0a6e076eb5 100644 --- a/Earthfile +++ b/Earthfile @@ -1,7 +1,7 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/mdlint:v3.2.23 AS mdlint-ci -IMPORT github.com/input-output-hk/catalyst-ci/earthly/cspell:v3.2.23 AS cspell-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/mdlint:v3.2.27 AS mdlint-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/cspell:v3.2.27 AS cspell-ci FROM debian:stable-slim diff --git a/docs/Earthfile b/docs/Earthfile index e7c53be6ca..38a0fb3541 100644 --- a/docs/Earthfile +++ b/docs/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/docs:v3.2.23 AS docs-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/docs:v3.2.27 AS docs-ci IMPORT .. AS repo diff --git a/docs/src/architecture/08_concepts/catalyst_voting/cddl/Earthfile b/docs/src/architecture/08_concepts/catalyst_voting/cddl/Earthfile index 0fe8c1b435..db9c8d6863 100644 --- a/docs/src/architecture/08_concepts/catalyst_voting/cddl/Earthfile +++ b/docs/src/architecture/08_concepts/catalyst_voting/cddl/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/cddl:v3.2.23 AS cddl-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/cddl:v3.2.27 AS cddl-ci check-cddl: FROM cddl-ci+cddl-base diff --git a/docs/src/architecture/08_concepts/immutable_ledger/cddl/Earthfile b/docs/src/architecture/08_concepts/immutable_ledger/cddl/Earthfile index 60e24f1584..84fa4e9a54 100644 --- a/docs/src/architecture/08_concepts/immutable_ledger/cddl/Earthfile +++ b/docs/src/architecture/08_concepts/immutable_ledger/cddl/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/cddl:v3.2.23 AS cddl-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/cddl:v3.2.27 AS cddl-ci check-cddl: FROM cddl-ci+cddl-base diff --git a/rust/.cargo/config.toml b/rust/.cargo/config.toml index 6641db1faa..cd8aa55e3e 100644 --- a/rust/.cargo/config.toml +++ b/rust/.cargo/config.toml @@ -27,11 +27,7 @@ rustflags = [ [build] rustflags = [] -rustdocflags = [ - "--enable-index-page", - "-Z", - "unstable-options", -] +rustdocflags = [] [profile.dev] opt-level = 1 diff --git a/rust/Cargo.toml b/rust/Cargo.toml index e01edc7e30..282be4d5f4 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 ea762219c5..19426537aa 100644 --- a/rust/Earthfile +++ b/rust/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.25 AS rust-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust:v3.2.27 AS rust-ci COPY_SRC: FUNCTION @@ -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=immutable-ledger --libs=vote-tx-v1 --libs=vote-tx-v2" \ --args4="--bins=cbork/cbork --libs=rbac-registration --libs=signed_doc" \ @@ -63,7 +64,7 @@ build: --coverage="cat-libs.coverage.info" \ --docs="true" - SAVE ARTIFACT target/$TARGETARCH/doc doc + SAVE ARTIFACT target/doc doc SAVE ARTIFACT target/release/cbork cbork # build-src-check: Check for any caching issues with the source we are building against. diff --git a/rust/Justfile b/rust/Justfile index 6d7e59495a..c5b1e31feb 100644 --- a/rust/Justfile +++ b/rust/Justfile @@ -30,7 +30,8 @@ code-lint: cargo lint -r # Pre Push Checks -pre-push: sync-cfg code-format code-lint license-check +pre-push: sync-cfg code-format code-lint + # license-check # Make sure we can actually build inside Earthly which needs to happen in CI. earthly +check earthly +build diff --git a/rust/c509-certificate/Cargo.toml b/rust/c509-certificate/Cargo.toml index 0174b0c58d..4a4e70b1f1 100644 --- a/rust/c509-certificate/Cargo.toml +++ b/rust/c509-certificate/Cargo.toml @@ -23,26 +23,28 @@ hex = "0.4.3" oid = "0.2.1" oid-registry = "0.7.1" asn1-rs = "0.6.2" -anyhow = "1.0.89" +anyhow = "1.0.95" bimap = "0.6.3" once_cell = "1.20.2" strum = "0.26.3" strum_macros = "0.26.4" -regex = "1.11.0" +regex = "1.11.1" ed25519-dalek = { version = "2.1.1", features = ["pem"] } -thiserror = "1.0.64" -serde = { version = "1.0.210", features = ["derive"] } -wasm-bindgen = "0.2.93" -serde-wasm-bindgen = "0.6.5" +thiserror = "2.0.9" +serde = { version = "1.0.217", features = ["derive"] } + +# Only re-enable when building targeting wasm is detected, should not be used in a non wasm build. +#wasm-bindgen = "0.2.99" +#serde-wasm-bindgen = "0.6.5" [package.metadata.cargo-machete] ignored = ["strum"] [dev-dependencies] -clap = { version = "4.5.19", features = ["derive"] } -serde_json = "1.0.128" +clap = { version = "4.5.23", features = ["derive"] } +serde_json = "1.0.134" rand = "0.8.5" -chrono = "0.4.38" +chrono = "0.4.39" [[example]] name = "c509" diff --git a/rust/c509-certificate/Earthfile b/rust/c509-certificate/Earthfile index a2235832e6..e2836bc125 100644 --- a/rust/c509-certificate/Earthfile +++ b/rust/c509-certificate/Earthfile @@ -1,6 +1,6 @@ VERSION 0.8 -IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust::v3.2.23 AS rust-ci +IMPORT github.com/input-output-hk/catalyst-ci/earthly/rust::v3.2.27 AS rust-ci IMPORT .. AS rust-local @@ -13,7 +13,7 @@ IMPORT ../.. AS repo # - Create a publish target which can publish the library to NPM for both Web and NodeJS. # - Create a simple example web app and nodejs app which uses the library, and can be used to # test it after publishing. -build-wasm: +skip-build-wasm: # for now skip building wasm. FROM rust-local+build COPY repo+repo-docs/repo/LICENSE-APACHE c509-certificate/. @@ -28,6 +28,6 @@ build-wasm: # js-wasm-package-locally : Generate the wasm package and save it locally js-wasm-package-locally: - FROM +build-wasm + FROM +skip-build-wasm SAVE ARTIFACT /pkg AS LOCAL ./pkg diff --git a/rust/c509-certificate/examples/cli/main.rs b/rust/c509-certificate/examples/cli/main.rs index ddc750f8e7..185291dd63 100644 --- a/rust/c509-certificate/examples/cli/main.rs +++ b/rust/c509-certificate/examples/cli/main.rs @@ -85,7 +85,7 @@ impl Cli { None => None, }; - generate(&json_file, output, sk.as_ref(), &key_type) + generate(&json_file, output, sk.as_ref(), key_type) }, Cli::Verify { file, public_key } => verify(&file, public_key), Cli::Decode { file, output } => decode(&file, output), @@ -143,7 +143,7 @@ const SELF_SIGNED_INT: u8 = 2; /// A function to generate C509 certificate. fn generate( file: &PathBuf, output: Option, private_key: Option<&PrivateKey>, - key_type: &Option, + key_type: Option, ) -> anyhow::Result<()> { let data = fs::read_to_string(file)?; let c509_json: C509Json = serde_json::from_str(&data)?; @@ -247,10 +247,15 @@ fn parse_public_key(public_key: &str) -> anyhow::Result { } /// Get the key type. Currently support only Ed25519. -fn get_key_type(key_type: &Option) -> anyhow::Result<(Oid<'static>, Option)> { - match key_type.as_deref() { - Some("ed25519") | None => Ok(ED25519), - Some(_) => Err(anyhow::anyhow!("Currently only support Ed25519")), +fn get_key_type(key_type: Option) -> anyhow::Result<(Oid<'static>, Option)> { + match key_type { + None => Err(anyhow::anyhow!("Currently only support Ed25519")), + Some(key_type) => { + match key_type.as_str() { + "ed25519" => Ok(ED25519), + _ => Err(anyhow::anyhow!("Currently only support Ed25519")), + } + }, } } diff --git a/rust/c509-certificate/src/lib.rs b/rust/c509-certificate/src/lib.rs index 5003ab74d3..b2292d9eaf 100644 --- a/rust/c509-certificate/src/lib.rs +++ b/rust/c509-certificate/src/lib.rs @@ -64,7 +64,8 @@ pub mod signing; pub mod subject_pub_key_algo; mod tables; pub mod time; -pub mod wasm_binding; +// Only re-enable when building with wasm is detected, should not be used in a non wasm +// build. pub mod wasm_binding; /// Generate a signed or unsigned C509 certificate. /// @@ -78,7 +79,6 @@ pub mod wasm_binding; /// # Errors /// /// Returns an error if the generated data is invalid. - pub fn generate(tbs_cert: &TbsCert, private_key: Option<&PrivateKey>) -> anyhow::Result> { // Encode the TbsCert let encoded_tbs = { diff --git a/rust/c509-certificate/src/oid.rs b/rust/c509-certificate/src/oid.rs index a3f45be75d..db4524a518 100644 --- a/rust/c509-certificate/src/oid.rs +++ b/rust/c509-certificate/src/oid.rs @@ -113,7 +113,7 @@ impl Encode<()> for C509oid { impl Decode<'_, ()> for C509oid { /// Decode an OID /// Decode the OID as unwrapped OID (~oid) - as bytes string without tag. - + /// /// # Returns /// /// A C509oid instance. diff --git a/rust/c509-certificate/src/signing.rs b/rust/c509-certificate/src/signing.rs index 898ca15ff1..6fc732c8ad 100644 --- a/rust/c509-certificate/src/signing.rs +++ b/rust/c509-certificate/src/signing.rs @@ -7,7 +7,7 @@ use ed25519_dalek::{ pkcs8::{DecodePrivateKey, DecodePublicKey}, SigningKey, VerifyingKey, }; -use wasm_bindgen::prelude::wasm_bindgen; +// use wasm_bindgen::prelude::wasm_bindgen; /// Public or private key decoding from string error. #[derive(thiserror::Error, Debug)] @@ -17,7 +17,7 @@ struct KeyPemDecodingError; /// Ed25519 private key instance. /// Wrapper over `ed25519_dalek::SigningKey`. #[allow(dead_code)] -#[wasm_bindgen] +//#[wasm_bindgen] #[derive(Clone, Debug, PartialEq, Eq)] pub struct PrivateKey(SigningKey); @@ -95,7 +95,7 @@ impl FromStr for PrivateKey { /// Ed25519 public key instance. /// Wrapper over `ed25519_dalek::VerifyingKey`. #[derive(Clone, Debug, PartialEq, Eq)] -#[wasm_bindgen] +//#[wasm_bindgen] pub struct PublicKey(VerifyingKey); #[allow(dead_code)] diff --git a/rust/cardano-blockchain-types/Cargo.toml b/rust/cardano-blockchain-types/Cargo.toml new file mode 100644 index 0000000000..6c95c7b02c --- /dev/null +++ b/rust/cardano-blockchain-types/Cargo.toml @@ -0,0 +1,36 @@ +[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-crypto = { 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" } + +ouroboros = "0.18.4" +tracing = "0.1.41" +anyhow = "1.0.94" +chrono = "0.4.39" +strum = { version = "0.26.3", features = ["derive"] } +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/Readme.md b/rust/cardano-blockchain-types/Readme.md new file mode 100644 index 0000000000..afa508052a --- /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 0000000000..f65f5bfb55 --- /dev/null +++ b/rust/cardano-blockchain-types/deps.tmp @@ -0,0 +1,61 @@ +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 0000000000..a5b79fefbd --- /dev/null +++ b/rust/cardano-blockchain-types/src/auxdata/aux_data.rs @@ -0,0 +1,172 @@ +//! 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) => { + // CBOR tag identifier 259 for auxiliary data in the Alonzo and beyond eras + // https://github.com/IntersectMBO/cardano-ledger/blob/78b32d585fd4a0340fb2b184959fb0d46f32c8d2/eras/conway/impl/cddl-files/conway.cddl#L526 + 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(format!( + "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 0000000000..554ede16bb --- /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 0000000000..4b107d13c2 --- /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 0000000000..2729ae7135 --- /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 0000000000..a8c725204a --- /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 0000000000..7a0192b570 --- /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 0000000000..8109020c7e --- /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