diff --git a/build.rs b/build.rs index 67e95fa8..33894f93 100644 --- a/build.rs +++ b/build.rs @@ -81,7 +81,7 @@ mod testgen_hs { return; } - let testgen_lib_version = "10.4.1.0"; + let testgen_lib_version = "10.4.1.1"; let suffix = if target_os == "windows" { ".zip" diff --git a/flake.lock b/flake.lock index cbd3041f..81c8124d 100644 --- a/flake.lock +++ b/flake.lock @@ -241,16 +241,16 @@ "testgen-hs": { "flake": false, "locked": { - "lastModified": 1748953639, - "narHash": "sha256-DgbM5UUsxYMPJ1nl19AnCi3VdTj8WElB1A7zSE8iPII=", + "lastModified": 1751542906, + "narHash": "sha256-FYz1P11du5tSakpl2ec9CGdwEB5h+0FcdGPU3zXte60=", "owner": "input-output-hk", "repo": "testgen-hs", - "rev": "4f81d654fbd4a2bfa9d00c99b75e9725e8abd8e7", + "rev": "7d58736ed578a9ccf8337376b302bb188b74303f", "type": "github" }, "original": { "owner": "input-output-hk", - "ref": "10.4.1.0", + "ref": "10.4.1.1", "repo": "testgen-hs", "type": "github" } diff --git a/flake.nix b/flake.nix index d1e30082..bf4ffedb 100644 --- a/flake.nix +++ b/flake.nix @@ -11,7 +11,7 @@ flake-compat.flake = false; cardano-node.url = "github:IntersectMBO/cardano-node/10.4.1"; cardano-node.flake = false; # otherwise, +2k dependencies we don’t really use - testgen-hs.url = "github:input-output-hk/testgen-hs/10.4.1.0"; # make sure it follows cardano-node + testgen-hs.url = "github:input-output-hk/testgen-hs/10.4.1.1"; # make sure it follows cardano-node testgen-hs.flake = false; # otherwise, +2k dependencies we don’t really use devshell.url = "github:numtide/devshell"; devshell.inputs.nixpkgs.follows = "nixpkgs"; diff --git a/nix/devshells.nix b/nix/devshells.nix index a218c2bb..638e5a55 100644 --- a/nix/devshells.nix +++ b/nix/devshells.nix @@ -72,6 +72,10 @@ in { name = "TESTGEN_HS_PATH"; value = lib.getExe internal.testgen-hs; } + { + name = "RUST_SRC_PATH"; + value = "${internal.rustPackages.rust-src}/lib/rustlib/src/rust/library"; + } ] ++ lib.optionals pkgs.stdenv.isDarwin [ { diff --git a/nix/internal/testgen-hs--enable-aarch64-linux.diff b/nix/internal/testgen-hs--enable-aarch64-linux.diff deleted file mode 100644 index 9ad78090..00000000 --- a/nix/internal/testgen-hs--enable-aarch64-linux.diff +++ /dev/null @@ -1,127 +0,0 @@ -diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml -index 197c881..59ce8cc 100644 ---- a/.github/workflows/release.yml -+++ b/.github/workflows/release.yml -@@ -30,6 +30,10 @@ jobs: - result-archive-x86_64-linux \ - .#hydraJobs.testgen-hs.x86_64-linux - -+ nix build -L --builders '' --max-jobs 0 --out-link \ -+ result-archive-aarch64-linux \ -+ .#hydraJobs.testgen-hs.aarch64-linux -+ - nix build -L --builders '' --max-jobs 0 --out-link \ - result-archive-x86_64-windows \ - .#hydraJobs.testgen-hs.x86_64-windows -@@ -48,6 +52,7 @@ jobs: - with: - files: | - result-archive-x86_64-linux/*.tar.bz2 -+ result-archive-aarch64-linux/*.tar.bz2 - result-archive-x86_64-darwin/*.tar.bz2 - result-archive-aarch64-darwin/*.tar.bz2 - result-archive-x86_64-windows/*.zip -diff --git a/.github/workflows/upload.yml b/.github/workflows/upload.yml -index e54ea87..6c3f02b 100644 ---- a/.github/workflows/upload.yml -+++ b/.github/workflows/upload.yml -@@ -22,6 +22,7 @@ jobs: - run: | - set -euo pipefail - nix build --builders "" --max-jobs 0 .#hydraJobs.testgen-hs.x86_64-linux && cp result/testgen-hs-*.* . -+ nix build --builders "" --max-jobs 0 .#hydraJobs.testgen-hs.aarch64-linux && cp result/testgen-hs-*.* . - nix build --builders "" --max-jobs 0 .#hydraJobs.testgen-hs.x86_64-windows && cp result/testgen-hs-*.* . - nix build --builders "" --max-jobs 0 .#hydraJobs.testgen-hs.x86_64-darwin && cp result/testgen-hs-*.* . - nix build --builders "" --max-jobs 0 .#hydraJobs.testgen-hs.aarch64-darwin && cp result/testgen-hs-*.* . -@@ -49,6 +50,12 @@ jobs: - name: testgen-hs-${{ env.version }}-x86_64-linux.tar.bz2 - path: testgen-hs-${{ env.version }}-x86_64-linux.tar.bz2 - if-no-files-found: error -+ - name: Upload Artifact (aarch64-linux) -+ uses: actions/upload-artifact@v4 -+ with: -+ name: testgen-hs-${{ env.version }}-aarch64-linux.tar.bz2 -+ path: testgen-hs-${{ env.version }}-aarch64-linux.tar.bz2 -+ if-no-files-found: error - - name: Upload Artifact (x86_64-windows) - uses: actions/upload-artifact@v4 - with: -diff --git a/flake.nix b/flake.nix -index 72d2f8f..b192741 100644 ---- a/flake.nix -+++ b/flake.nix -@@ -28,7 +28,7 @@ - targetSystem: import ./nix/internal.nix {inherit inputs targetSystem;} - ); - -- systems = ["x86_64-linux" "aarch64-darwin" "x86_64-darwin"]; -+ systems = ["x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86_64-darwin"]; - perSystem = { - config, - system, -diff --git a/nix/internal.nix b/nix/internal.nix -index 51248ac..61a7c70 100644 ---- a/nix/internal.nix -+++ b/nix/internal.nix -@@ -13,11 +13,32 @@ assert __elem targetSystem ["x86_64-linux" "aarch64-linux" "aarch64-darwin" "x86 - in rec { - defaultPackage = testgen-hs; - -- cardano-node-flake = (import inputs.flake-compat {src = inputs.cardano-node;}).defaultNix; -+ cardano-node-flake = let -+ unpatched = inputs.cardano-node; -+ in -+ (import inputs.flake-compat { -+ src = -+ if targetSystem != "aarch64-linux" -+ then unpatched -+ else { -+ outPath = toString (pkgs.runCommand "source" {} '' -+ cp -r ${unpatched} $out -+ chmod -R +w $out -+ cd $out -+ echo ${lib.escapeShellArg (builtins.toJSON [targetSystem])} $out/nix/supported-systems.nix -+ ${lib.optionalString (targetSystem == "aarch64-linux") '' -+ sed -r 's/"-fexternal-interpreter"//g' -i $out/nix/haskell.nix -+ ''} -+ ''); -+ inherit (unpatched) rev shortRev lastModified lastModifiedDate; -+ }; -+ }) -+ .defaultNix; - - cardano-node-packages = - { - x86_64-linux = cardano-node-flake.hydraJobs.x86_64-linux.musl; -+ aarch64-linux = cardano-node-flake.packages.aarch64-linux; - x86_64-darwin = cardano-node-flake.packages.x86_64-darwin; - aarch64-darwin = cardano-node-flake.packages.aarch64-darwin; - } -@@ -35,7 +56,10 @@ in rec { - cp -r ${unpatched} $out - chmod -R +w $out - cd $out -- echo ${lib.escapeShellArg (builtins.toJSON [targetSystem])} $out/nix/supported-systems.nix -+ echo ${lib.escapeShellArg (builtins.toJSON [targetSystem])} >$out/nix/supported-systems.nix -+ ${lib.optionalString (targetSystem == "aarch64-linux") '' -+ sed -r 's/"-fexternal-interpreter"//g' -i $out/nix/haskell.nix -+ ''} - cp -r ${../testgen-hs} ./testgen-hs - sed -r '/^packages:/ a\ testgen-hs' -i cabal.project - sed -r 's/other-modules:\s*/ , /g' -i cardano-submit-api/cardano-submit-api.cabal -@@ -51,6 +75,7 @@ in rec { - in - { - x86_64-linux = patched-flake.hydraJobs.x86_64-linux.musl.testgen-hs; -+ aarch64-linux = patched-flake.packages.aarch64-linux.testgen-hs; - x86_64-darwin = patched-flake.packages.x86_64-darwin.testgen-hs; - aarch64-darwin = patched-flake.packages.aarch64-darwin.testgen-hs; - x86_64-windows = patched-flake.legacyPackages.x86_64-linux.hydraJobs.windows.testgen-hs; -@@ -113,6 +138,7 @@ in rec { - aarch64-darwin = darwinLike; - x86_64-darwin = darwinLike; - x86_64-linux = linuxLike {}; -+ aarch64-linux = linuxLike {}; - x86_64-windows = linuxLike {useZip = true;}; - } - .${targetSystem}; diff --git a/nix/internal/unix.nix b/nix/internal/unix.nix index 1add453c..2864d946 100644 --- a/nix/internal/unix.nix +++ b/nix/internal/unix.nix @@ -217,24 +217,7 @@ in done ''; - testgen-hs-flake = let - unpatched = inputs.testgen-hs; - in - (import inputs.flake-compat { - src = - if targetSystem != "aarch64-linux" - then unpatched - else { - outPath = toString (pkgs.runCommand "source" {} '' - cp -r ${unpatched} $out - chmod -R +w $out - cd $out - patch -p1 -i ${./testgen-hs--enable-aarch64-linux.diff} - ''); - inherit (unpatched) rev shortRev lastModified lastModifiedDate; - }; - }) - .defaultNix; + testgen-hs-flake = (import inputs.flake-compat {src = inputs.testgen-hs;}).defaultNix; testgen-hs = testgen-hs-flake.packages.${targetSystem}.default; diff --git a/src/api/utils/txs/evaluate/model.rs b/src/api/utils/txs/evaluate/model.rs index cdf64e2a..a549903b 100644 --- a/src/api/utils/txs/evaluate/model.rs +++ b/src/api/utils/txs/evaluate/model.rs @@ -17,13 +17,13 @@ pub struct TxEvaluationRequest { } pub type AdditionalUtxoSet = Vec<(TxIn, TxOut)>; -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct TxIn { #[serde(rename = "txId")] pub tx_id: String, pub index: u64, } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct TxOut { pub address: String, pub value: Value, @@ -77,21 +77,21 @@ impl From for NativeScript { } } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] #[serde(untagged)] pub enum Datum { String(String), Map(HashMap), } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub struct Value { pub coins: u64, pub assets: Option>, // asset name and number. Asset number can be negative when burning assets but this behaviour changed in Conway. Now it can be only PositiveCoin } // This is originally missing PlutusV3 since blockfrost uses Ogmios v5.6 which has slightly different data structure -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] //#[serde(untagged)] pub enum Script { #[serde(rename = "plutus:v1")] @@ -104,7 +104,7 @@ pub enum Script { Native(ScriptNative), } -#[derive(Deserialize)] +#[derive(Deserialize, Debug)] pub enum ScriptNative { #[serde(rename = "any")] Any(Vec), diff --git a/src/cbor/tests.rs b/src/cbor/tests.rs index f0f28d10..173b11d4 100644 --- a/src/cbor/tests.rs +++ b/src/cbor/tests.rs @@ -11,6 +11,8 @@ use std::process::Command; #[cfg(not(feature = "tarpaulin"))] mod random; #[cfg(not(feature = "tarpaulin"))] +mod random_eval_tx; +#[cfg(not(feature = "tarpaulin"))] mod specific; #[derive(Deserialize, Debug)] @@ -30,6 +32,7 @@ pub struct CborTestSeed { #[derive(Debug, Clone, Copy)] #[allow(non_camel_case_types, dead_code)] pub enum CaseType { + Tx_Conway, ApplyTxErr_Byron, ApplyTxErr_Shelley, ApplyTxErr_Allegra, diff --git a/src/cbor/tests/random_eval_tx.rs b/src/cbor/tests/random_eval_tx.rs new file mode 100644 index 00000000..e87115c4 --- /dev/null +++ b/src/cbor/tests/random_eval_tx.rs @@ -0,0 +1,541 @@ +use super::*; + +// -------------------------- random evaluation tests -------------------------- // + +#[test] +#[allow(non_snake_case)] +fn proptest_eval_Tx_Conway_size_010() { + proptest_with_params(CaseType::Tx_Conway, 100, 10, None) +} + +// -------------------------- random UTxO decoder tests -------------------------- // + +#[test] +fn proptest_utxo_decoder() { + check_generated_cases(CaseType::Tx_Conway, 500, 10, 5, None, |case| { + let json: TestCaseJson = serde_json::from_value(case.json).unwrap(); + let utxo_cbor = json.utxo_set_cbor; + let utxo_decoded = utxo_decoder::decode_utxo(&hex::decode(&utxo_cbor).unwrap()); + if utxo_decoded.is_ok() { + Ok(()) + } else { + Err(utxo_cbor) + } + }) +} + +// -------------------------- helper functions -------------------------- // + +/// Tests the native Rust deserializer with the given params. +fn proptest_with_params( + case_type: CaseType, + num_cases: u32, + generator_size: u16, + seed: Option, +) { + use crate::api::utils::txs::evaluate::model::AdditionalUtxoSet; + + check_generated_cases(case_type, num_cases, generator_size, 5, seed, |case| { + let tx_cbor = case.cbor.clone(); + let json: TestCaseJson = serde_json::from_value(case.json).unwrap(); + let expected = json.execution_units; + let utxo_cbor = json.utxo_set_cbor; + let utxo: AdditionalUtxoSet = + utxo_decoder::decode_utxo(&hex::decode(&utxo_cbor).unwrap()).unwrap(); + + // TODO: okay, so now we have `tx_cbor`, `AdditionalUtxoSet`, and the `expected` JSON. + // TODO: it’s time to call `crate::cbor::evaluate_tx()`, but it needs a `cardano-node` 👀 + + Err(format!( + "Unimplemented:\n Transaction: {tx_cbor}\n Expected: {}\n UTxO: {utxo:?}", + serde_json::to_string_pretty(&expected) + .unwrap() + .replace("\n", "\n ") + )) + }) +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all(deserialize = "camelCase"))] +pub struct TestCaseJson { + pub execution_units: serde_json::Value, + #[serde(rename = "utxoSetCBOR")] + pub utxo_set_cbor: String, +} + +// -------------------------- draft UTxO decoder (could be wrong!) -------------------------- // + +mod utxo_decoder { + use crate::api::utils::txs::evaluate::model::{ + AdditionalUtxoSet, Script, ScriptNative, TxIn, TxOut, Value, + }; + use pallas_codec::minicbor; + + use anyhow::{Result, anyhow, bail}; + use bech32::{ToBase32, Variant, encode}; + use minicbor::{data::Type as CborType, decode::Decoder}; + use std::collections::HashMap; + + pub fn decode_utxo(bytes: &[u8]) -> Result { + let mut d = Decoder::new(bytes); + + let map_len = d + .map()? + .ok_or_else(|| anyhow!("UTxO map must be definite"))?; + let mut out = Vec::with_capacity(map_len as usize); + + for _ in 0..map_len { + let tx_in = decode_txin(&mut d)?; + let tx_out = decode_txout(&mut d, bytes)?; + out.push((tx_in, tx_out)); + } + Ok(out) + } + + fn decode_txin(d: &mut Decoder<'_>) -> Result { + match d.array()? { + Some(2) => {}, + Some(n) => bail!("TxIn array length {n} ≠ 2"), + None => bail!("TxIn must be definite-length"), + } + + let txid = d.bytes()?; + if txid.len() != 32 { + bail!("TxId len {} ≠ 32", txid.len()); + } + let idx = d.u64()?; + + Ok(TxIn { + tx_id: hex::encode(txid), + index: idx, + }) + } + + fn decode_txout<'b>(d: &mut Decoder<'b>, src: &'b [u8]) -> Result { + match d.datatype()? { + CborType::Map => decode_txout_map(d, src), + CborType::Array => decode_txout_array(d, src), + other => bail!("TxOut must be map or array, got {:?}", other), + } + } + + fn decode_txout_map<'b>(d: &mut Decoder<'b>, src: &'b [u8]) -> Result { + let pairs = d + .map()? + .ok_or_else(|| anyhow!("TxOut map must be definite"))?; + + let mut addr_bytes = None; + let mut value = None; + let mut datum_hash = None; + let mut datum = None; + let mut script = None; + + for _ in 0..pairs { + let key = d.u64()?; + match key { + 0 => { + let bytes = d.bytes()?; + addr_bytes = Some(bytes.to_vec()); + }, + + 1 => value = Some(decode_value(d)?), + + 2 => { + let start = d.position(); + match d.datatype()? { + CborType::Bytes => { + let bs = d.bytes()?; + if bs.len() == 32 { + datum_hash = Some(hex::encode(bs)); + } else { + datum = Some(hex::encode(bs)); + } + }, + _ => { + let raw = read_term(d, src, start)?; + datum = Some(hex::encode(&raw)); + }, + } + }, + + 3 => { + let start = d.position(); + let raw = read_term(d, src, start)?; + script = Some(parse_script(&raw)?); + }, + + _ => { + d.skip()?; + }, + } + } + + let addr_bytes = addr_bytes.ok_or_else(|| anyhow!("TxOut missing address"))?; + let value = value.ok_or_else(|| anyhow!("TxOut missing value"))?; + + let hrp = if addr_bytes.first().map_or(false, |b| b & 0b0001_0000 == 0) { + "addr" + } else { + "addr_test" + }; + let address = encode(hrp, addr_bytes.to_base32(), Variant::Bech32)?; + + Ok(TxOut { + address, + value, + datum_hash, + datum, + script, + }) + } + + fn decode_txout_array<'b>(d: &mut Decoder<'b>, src: &'b [u8]) -> Result { + let len = d + .array()? + .ok_or_else(|| anyhow!("TxOut must be definite"))?; + if !(2..=4).contains(&len) { + bail!("unexpected TxOut length {len}"); + } + + let addr_bytes = d.bytes()?; + let hrp = if addr_bytes.first().map_or(false, |b| b & 0b0001_0000 == 0) { + "addr" + } else { + "addr_test" + }; + let address = encode(hrp, addr_bytes.to_base32(), Variant::Bech32)?; + + let value = decode_value(d)?; + + let mut datum_hash = None; + let mut datum = None; + let mut script = None; + + match len { + 2 => {}, + 3 => { + let dh = d.bytes()?; + if dh.len() != 32 { + bail!("datumHash len {} ≠ 32", dh.len()); + } + datum_hash = Some(hex::encode(dh)); + }, + 4 => { + let start = d.position(); + match d.datatype()? { + CborType::Bytes => { + let bs = d.bytes()?; + if bs.len() == 32 { + datum_hash = Some(hex::encode(bs)); + } else { + datum = Some(hex::encode(bs)); + } + }, + _ => { + let bytes = read_term(d, src, start)?; + datum = Some(hex::encode(&bytes)); + }, + } + let start = d.position(); + let raw = read_term(d, src, start)?; + script = Some(parse_script(&raw)?); + }, + _ => unreachable!(), + } + + Ok(TxOut { + address, + value, + datum_hash, + datum, + script, + }) + } + + fn decode_value(d: &mut Decoder<'_>) -> Result { + match d.datatype()? { + CborType::U8 | CborType::U16 | CborType::U32 | CborType::U64 => Ok(Value { + coins: d.u64()?, + assets: None, + }), + + CborType::Map => { + let outer = d + .map()? + .ok_or_else(|| anyhow!("multi-asset must be definite"))?; + let mut assets = HashMap::::new(); + let mut coin = 0u64; + + for _ in 0..outer { + match d.datatype()? { + CborType::U8 | CborType::U16 | CborType::U32 | CborType::U64 => { + if d.u64()? != 0 { + bail!("unexpected integer key in value map"); + } + coin = d.u64()?; + }, + CborType::Bytes => { + let policy = d.bytes()?.to_vec(); + let inner = d + .map()? + .ok_or_else(|| anyhow!("asset map must be definite"))?; + for _ in 0..inner { + let name = d.bytes()?.to_vec(); + let qty = d.i64()?; + if qty < 0 { + bail!("burning not allowed in Conway value"); + } + let key = + format!("{}.{}", hex::encode(&policy), hex::encode(&name)); + assets.insert(key, qty as u64); + } + }, + t => bail!("unexpected CBOR type {:?} in value map", t), + } + } + + Ok(Value { + coins: coin, + assets: if assets.is_empty() { + None + } else { + Some(assets) + }, + }) + }, + + t => bail!("unexpected CBOR type {:?} for TxOut value", t), + } + } + + fn peel_tags(input: &[u8]) -> Result<&[u8]> { + let mut d = Decoder::new(input); + + while let CborType::Tag = d.datatype()? { + let _t = d.tag()?; // ignore tag number + } + + let start = d.position(); + Ok(&input[start..]) + } + + fn parse_script(bytes: &[u8]) -> Result