Merge branch 'main' into lang

This commit is contained in:
rvcas 2022-09-25 17:44:20 -04:00
commit c08f6a8454
No known key found for this signature in database
GPG Key ID: C09B64E263F7D68C
21 changed files with 6328 additions and 161 deletions

164
Cargo.lock generated
View File

@ -24,10 +24,18 @@ dependencies = [
[[package]]
name = "aiken"
version = "0.0.11"
version = "0.0.13"
dependencies = [
"anyhow",
"clap",
"hex",
"pallas-addresses",
"pallas-codec",
"pallas-crypto",
"pallas-primitives",
"pallas-traverse",
"serde",
"serde_json",
"uplc",
]
@ -43,9 +51,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.64"
version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9a8f622bcf6ff3df478e9deba3e03e4e04b300f8e6a139e192c05fa3490afc7"
checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602"
[[package]]
name = "arrayvec"
@ -126,9 +134,9 @@ dependencies = [
[[package]]
name = "clap"
version = "3.2.20"
version = "3.2.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd"
checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750"
dependencies = [
"atty",
"bitflags",
@ -311,15 +319,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.132"
version = "0.2.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
[[package]]
name = "lock_api"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390"
checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df"
dependencies = [
"autocfg",
"scopeguard",
@ -357,34 +365,14 @@ dependencies = [
"syn",
]
[[package]]
name = "minicbor"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e575910763b21a0db7df5e142907fe944bff84d1dfc78e2ba92e7f3bdfd36b"
dependencies = [
"half",
"minicbor-derive 0.11.0",
]
[[package]]
name = "minicbor"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a20020e8e2d1881d8736f64011bb5ff99f1db9947ce3089706945c8915695cb"
dependencies = [
"minicbor-derive 0.12.0",
]
[[package]]
name = "minicbor-derive"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a86c5f04def8fb7735ae918bb589af82f985526f4c62e0249544b668b2f456"
dependencies = [
"proc-macro2",
"quote",
"syn",
"half",
"minicbor-derive",
]
[[package]]
@ -409,9 +397,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.14.0"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "os_str_bytes"
@ -420,32 +408,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff"
[[package]]
name = "pallas-codec"
version = "0.12.0"
name = "pallas-addresses"
version = "0.14.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dce0ea17341c1a0e43e2bb4a637740198dcb09826879ce3ac5ae1c6f4398a5d"
checksum = "1188a2434037b74129f8d209a37a1d09721b900e6e378255db1a91abc37199bc"
dependencies = [
"minicbor 0.17.1",
"base58",
"bech32",
"hex",
"pallas-codec",
"pallas-crypto",
"thiserror",
]
[[package]]
name = "pallas-codec"
version = "0.14.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c035a772aa84e858e53b7c98e6036eaa216d8a699bb9c826787722bde13d05"
dependencies = [
"hex",
"minicbor",
"serde",
]
[[package]]
name = "pallas-crypto"
version = "0.12.0"
version = "0.14.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051226367cd851895c73e3115d378b58495ade1ee60c3a154c0d0c30554565fa"
checksum = "2841f9225dcd6a78c6f386d4d5e76bcdbecd7b4489455b2d2485b105bf4c0499"
dependencies = [
"cryptoxide",
"hex",
"pallas-codec",
"rand_core",
"serde",
"thiserror",
]
[[package]]
name = "pallas-primitives"
version = "0.12.0"
version = "0.14.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a0fcc7d5a7120bc2b2e203ec5e7f8088107c500c0eb665569d0e77a910d3c0"
checksum = "3e51824547f7a1e1a6574ecec4bf8557f3819f435132873c0bae97acba81cbb1"
dependencies = [
"base58",
"bech32",
@ -457,6 +462,20 @@ dependencies = [
"serde_json",
]
[[package]]
name = "pallas-traverse"
version = "0.14.0-alpha.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3291d1ae31cd803b9142fb32e1fcb0a58fc1d65b3bbe1d527d14b322db6eb019"
dependencies = [
"hex",
"pallas-addresses",
"pallas-codec",
"pallas-crypto",
"pallas-primitives",
"thiserror",
]
[[package]]
name = "parking_lot"
version = "0.12.1"
@ -557,9 +576,9 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5"
[[package]]
name = "proc-macro2"
version = "1.0.43"
version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
checksum = "7bd7356a8122b6c4a24a82b278680c73357984ca2fc79a0f9fa6dea7dced7c58"
dependencies = [
"unicode-ident",
]
@ -628,9 +647,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.6.3"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
@ -694,9 +713,23 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.144"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
@ -723,9 +756,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.99"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13"
checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
dependencies = [
"proc-macro2",
"quote",
@ -757,24 +790,24 @@ dependencies = [
[[package]]
name = "textwrap"
version = "0.15.0"
version = "0.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb"
checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16"
[[package]]
name = "thiserror"
version = "1.0.34"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1b05ca9d106ba7d2e31a9dab4a64e7be2cce415321966ea3132c49a656e252"
checksum = "0a99cb8c4b9a8ef0e7907cd3b617cc8dc04d571c4e73c8ae403d80ac160bb122"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.34"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8f2591983642de85c921015f3f070c665a197ed69e417af436115e3a1407487"
checksum = "3a891860d3c8d66fec8e73ddb3765f90082374dbaaa833407b904a94f1a7eb43"
dependencies = [
"proc-macro2",
"quote",
@ -798,35 +831,40 @@ checksum = "0685c84d5d54d1c26f7d3eb96cd41550adb97baed141a761cf335d3d33bcd0ae"
[[package]]
name = "unicode-ident"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf"
checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a"
[[package]]
name = "unicode-width"
version = "0.1.9"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "uplc"
version = "0.0.11"
version = "0.0.13"
dependencies = [
"anyhow",
"cryptoxide",
"flat-rs",
"hex",
"minicbor 0.18.0",
"pallas-addresses",
"pallas-codec",
"pallas-crypto",
"pallas-primitives",
"pallas-traverse",
"peg",
"pretty",
"proptest",
"serde",
"serde_json",
"thiserror",
]

View File

@ -1,5 +0,0 @@
(program
1.0.0
[(force (force (builtin fstPair))) (con (pair integer bytestring) (22, #1122aabb))]
)

View File

@ -1,7 +1,7 @@
[package]
name = "aiken"
description = "Cardano smart contract language and toolchain"
version = "0.0.11"
version = "0.0.13"
edition = "2021"
repository = "https://github.com/txpipe/aiken"
homepage = "https://github.com/txpipe/aiken"
@ -13,4 +13,12 @@ authors = ["Lucas Rosa <x@rvcas.dev>", "Kasey White <kwhitemsg@gmail.com>"]
[dependencies]
anyhow = "1.0.57"
clap = { version = "3.1.14", features = ["derive"] }
uplc = { path = '../uplc', version = "0.0.11" }
hex = "0.4.3"
pallas-addresses = "0.14.0-alpha.3"
pallas-codec = "0.14.0-alpha.3"
pallas-crypto = "0.14.0-alpha.3"
pallas-primitives = "0.14.0-alpha.3"
pallas-traverse = "0.14.0-alpha.3"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.85"
uplc = { path = '../uplc', version = "0.0.13" }

View File

@ -16,22 +16,58 @@ pub enum Args {
/// Project name
name: PathBuf,
},
/// A subcommand for working with transactions
#[clap(subcommand)]
Tx(TxCommand),
/// A subcommand for working with Untyped Plutus Core
#[clap(subcommand)]
Uplc(UplcCommand),
}
/// Commands for working with transactions
#[derive(Subcommand)]
pub enum TxCommand {
/// Simulate a transaction by evaluating it's script
Simulate {
/// A file containing cbor hex for a transaction
input: PathBuf,
/// Toggle whether input is raw cbor or a hex string
#[clap(short, long)]
cbor: bool,
/// A file containing cbor hex for the raw inputs
raw_inputs: PathBuf,
/// A file containing cbor hex for the raw outputs
raw_outputs: PathBuf,
/// Time between each slot
#[clap(short, long, default_value_t = 1000)]
slot_length: u64,
/// Time of shelley hardfork
#[clap(long, default_value_t = 1596059091000)]
zero_time: u64,
/// Slot number at the start of the shelley hardfork
#[clap(long, default_value_t = 4492800)]
zero_slot: u64,
},
}
/// Commands for working with Untyped Plutus Core
#[derive(Subcommand)]
pub enum UplcCommand {
/// Evaluate an Untyped Plutus Core program
Eval {
/// Handle input as flat bytes
script: PathBuf,
#[clap(short, long)]
flat: bool,
/// File to load and evaluate
input: PathBuf,
/// Arguments to pass to the uplc program
args: Vec<String>,
},
/// Encode textual Untyped Plutus Core to flat bytes
Flat {

View File

@ -1,14 +1,23 @@
use std::{fmt::Write as _, fs};
use pallas_primitives::{
babbage::{TransactionInput, TransactionOutput},
Fragment,
};
use pallas_traverse::{Era, MultiEraTx};
use uplc::{
ast::{DeBruijn, FakeNamedDeBruijn, Name, NamedDeBruijn, Program, Term},
machine::cost_model::ExBudget,
parser,
tx::{
self,
script_context::{ResolvedInput, SlotConfig},
},
};
mod args;
use args::{Args, UplcCommand};
use args::{Args, TxCommand, UplcCommand};
fn main() -> anyhow::Result<()> {
let args = Args::default();
@ -39,38 +48,142 @@ fn main() -> anyhow::Result<()> {
}
}
Args::Uplc(uplc) => match uplc {
UplcCommand::Flat { input, print, out } => {
Args::Tx(tx_cmd) => match tx_cmd {
TxCommand::Simulate {
input,
cbor,
raw_inputs,
raw_outputs,
slot_length,
zero_time,
zero_slot,
} => {
let (tx_bytes, inputs_bytes, outputs_bytes) = if cbor {
(
fs::read(input)?,
fs::read(raw_inputs)?,
fs::read(raw_outputs)?,
)
} else {
let cbor_hex = fs::read_to_string(input)?;
let inputs_hex = fs::read_to_string(raw_inputs)?;
let outputs_hex = fs::read_to_string(raw_outputs)?;
(
hex::decode(cbor_hex.trim())?,
hex::decode(inputs_hex.trim())?,
hex::decode(outputs_hex.trim())?,
)
};
let tx = MultiEraTx::decode(Era::Babbage, &tx_bytes)
.or_else(|_| MultiEraTx::decode(Era::Alonzo, &tx_bytes))?;
let inputs = Vec::<TransactionInput>::decode_fragment(&inputs_bytes).unwrap();
let outputs = Vec::<TransactionOutput>::decode_fragment(&outputs_bytes).unwrap();
let resolved_inputs: Vec<ResolvedInput> = inputs
.iter()
.zip(outputs.iter())
.map(|(input, output)| ResolvedInput {
input: input.clone(),
output: output.clone(),
})
.collect();
println!("Simulating: {}", tx.hash());
if let Some(tx_babbage) = tx.as_babbage() {
let slot_config = SlotConfig {
zero_time,
zero_slot,
slot_length,
};
let result = tx::eval_phase_two(
tx_babbage,
&resolved_inputs,
None,
None,
&slot_config,
true,
);
match result {
Ok(redeemers) => {
println!("\nTotal Budget Used\n-----------------\n");
let total_budget_used = redeemers.iter().fold(
ExBudget { mem: 0, cpu: 0 },
|accum, curr| ExBudget {
mem: accum.mem + curr.ex_units.mem as i64,
cpu: accum.cpu + curr.ex_units.steps as i64,
},
);
println!("mem: {}", total_budget_used.mem);
println!("cpu: {}", total_budget_used.cpu);
}
Err(err) => {
eprintln!("\nError\n-----\n\n{}\n", err);
}
}
}
}
},
Args::Uplc(uplc_cmd) => match uplc_cmd {
UplcCommand::Flat {
input,
print,
out,
cbor_hex,
} => {
let code = std::fs::read_to_string(&input)?;
let program = parser::program(&code)?;
let program = Program::<DeBruijn>::try_from(program)?;
let bytes = program.to_flat()?;
if cbor_hex {
let bytes = program.to_flat()?;
if print {
let mut output = String::new();
if print {
let mut output = String::new();
for (i, byte) in bytes.iter().enumerate() {
let _ = write!(output, "{:08b}", byte);
for (i, byte) in bytes.iter().enumerate() {
let _ = write!(output, "{:08b}", byte);
if (i + 1) % 4 == 0 {
output.push('\n');
} else {
output.push(' ');
if (i + 1) % 4 == 0 {
output.push('\n');
} else {
output.push(' ');
}
}
}
println!("{}", output);
} else {
let out_name = if let Some(out) = out {
out
println!("{}", output);
} else {
format!("{}.flat", input.file_stem().unwrap().to_str().unwrap())
};
let out_name = if let Some(out) = out {
out
} else {
format!("{}.flat", input.file_stem().unwrap().to_str().unwrap())
};
fs::write(&out_name, &bytes)?;
fs::write(&out_name, &bytes)?;
}
} else {
let cbor = program.to_hex()?;
if print {
println!("{}", &cbor);
} else {
let out_name = if let Some(out) = out {
out
} else {
format!("{}.cbor", input.file_stem().unwrap().to_str().unwrap())
};
fs::write(&out_name, &cbor)?;
}
}
}
@ -87,11 +200,24 @@ fn main() -> anyhow::Result<()> {
fs::write(&input, pretty)?;
}
}
UplcCommand::Unflat {
input,
print,
out,
cbor_hex,
} => {
let program = if cbor_hex {
let cbor = std::fs::read_to_string(&input)?;
UplcCommand::Unflat { input, print, out } => {
let bytes = std::fs::read(&input)?;
let mut cbor_buffer = Vec::new();
let mut flat_buffer = Vec::new();
let program = Program::<DeBruijn>::from_flat(&bytes)?;
Program::<DeBruijn>::from_hex(cbor.trim(), &mut cbor_buffer, &mut flat_buffer)?
} else {
let bytes = std::fs::read(&input)?;
Program::<DeBruijn>::from_flat(&bytes)?
};
let program: Program<Name> = program.try_into()?;
@ -110,21 +236,27 @@ fn main() -> anyhow::Result<()> {
}
}
UplcCommand::Eval { input, flat } => {
let program = if flat {
let bytes = std::fs::read(&input)?;
UplcCommand::Eval { script, flat, args } => {
let mut program = if flat {
let bytes = std::fs::read(&script)?;
let prog = Program::<FakeNamedDeBruijn>::from_flat(&bytes)?;
prog.into()
} else {
let code = std::fs::read_to_string(&input)?;
let code = std::fs::read_to_string(&script)?;
let prog = parser::program(&code)?;
Program::<NamedDeBruijn>::try_from(prog)?
};
for arg in args {
let term: Term<NamedDeBruijn> = parser::term(&arg)?.try_into()?;
program = program.apply_term(&term);
}
let (term, cost, logs) = program.eval();
match term {
@ -149,7 +281,10 @@ fn main() -> anyhow::Result<()> {
"\nBudget\n------\ncpu: {}\nmemory: {}\n",
cost.cpu, cost.mem
);
println!("\nLogs\n----\n{}", logs.join("\n"))
if !logs.is_empty() {
println!("\nLogs\n----\n{}", logs.join("\n"))
}
}
},
}

View File

@ -1,7 +1,7 @@
[package]
name = "uplc"
description = "Utilities for working with Untyped Plutus Core"
version = "0.0.11"
version = "0.0.13"
edition = "2021"
repository = "https://github.com/txpipe/aiken/crates/uplc"
homepage = "https://github.com/txpipe/aiken"
@ -16,12 +16,17 @@ exclude = ["test_data/*"]
cryptoxide = "0.4.2"
flat-rs = { path = "../flat", version = "0.0.10" }
hex = "0.4.3"
minicbor = { version = "0.18.0", features = ["std"] }
pallas-codec = "0.12.0"
pallas-primitives = "0.12.0"
pallas-addresses = "0.14.0-alpha.3"
pallas-codec = "0.14.0-alpha.3"
pallas-crypto = "0.14.0-alpha.3"
pallas-primitives = "0.14.0-alpha.3"
pallas-traverse = "0.14.0-alpha.3"
peg = "0.8.0"
pretty = "0.11.3"
thiserror = "1.0.31"
anyhow = "1.0.57"
serde = { version = "1.0.144", features = ["derive"] }
serde_json = "1.0.85"
[dev-dependencies]
hex = "0.4.3"

View File

@ -1,13 +1,13 @@
use std::{fmt::Display, rc::Rc};
use pallas_primitives::alonzo::PlutusData;
use pallas_primitives::{alonzo::PlutusData, babbage::Language};
use crate::{
builtins::DefaultFunction,
debruijn::{self, Converter},
flat::Binder,
machine::{
cost_model::{CostModel, ExBudget},
cost_model::{initialize_cost_model, CostModel, ExBudget},
Machine,
},
};
@ -39,6 +39,33 @@ where
term: applied_term,
}
}
/// We use this to apply the validator to Datum,
/// then redeemer, then ScriptContext. If datum is
/// even necessary (i.e. minting policy).
pub fn apply_term(&self, term: &Term<T>) -> Self {
let applied_term = Term::Apply {
function: Rc::new(self.term.clone()),
argument: Rc::new(term.clone()),
};
Program {
version: self.version,
term: applied_term,
}
}
pub fn apply_data(&self, plutus_data: PlutusData) -> Self {
let applied_term = Term::Apply {
function: Rc::new(self.term.clone()),
argument: Rc::new(Term::Constant(Constant::Data(plutus_data))),
};
Program {
version: self.version,
term: applied_term,
}
}
}
impl<'a, T> Display for Program<T>
@ -461,7 +488,54 @@ impl Program<NamedDeBruijn> {
ExBudget,
Vec<String>,
) {
let mut machine = Machine::new(CostModel::default(), ExBudget::default(), 200);
let mut machine = Machine::new(
Language::PlutusV2,
CostModel::default(),
ExBudget::default(),
200,
);
let term = machine.run(&self.term);
(term, machine.ex_budget, machine.logs)
}
/// Evaluate a Program as PlutusV1
pub fn eval_v1(
&self,
) -> (
Result<Term<NamedDeBruijn>, crate::machine::Error>,
ExBudget,
Vec<String>,
) {
let mut machine = Machine::new(Language::PlutusV1, CostModel::v1(), ExBudget::v1(), 200);
let term = machine.run(&self.term);
(term, machine.ex_budget, machine.logs)
}
pub fn eval_as(
&self,
version: &Language,
costs: &[i64],
initial_budget: Option<&ExBudget>,
) -> (
Result<Term<NamedDeBruijn>, crate::machine::Error>,
ExBudget,
Vec<String>,
) {
let budget = match initial_budget {
Some(b) => *b,
None => ExBudget::default(),
};
let mut machine = Machine::new(
version.clone(),
initialize_cost_model(version, costs),
budget,
200, //slippage
);
let term = machine.run(&self.term);
@ -482,3 +556,9 @@ impl Program<DeBruijn> {
program.eval()
}
}
impl Term<NamedDeBruijn> {
pub fn is_valid_script_result(&self) -> bool {
!matches!(self, Term::Error)
}
}

View File

@ -31,8 +31,11 @@ where
T: Binder<'b> + Debug,
{
pub fn from_cbor(bytes: &'b [u8], buffer: &'b mut Vec<u8>) -> Result<Self, de::Error> {
let flat_bytes: Vec<u8> =
minicbor::decode(bytes).map_err(|err| de::Error::Message(err.to_string()))?;
let mut cbor_decoder = pallas_codec::minicbor::Decoder::new(bytes);
let flat_bytes = cbor_decoder
.bytes()
.map_err(|err| de::Error::Message(err.to_string()))?;
buffer.extend(flat_bytes);
@ -58,7 +61,15 @@ where
pub fn to_cbor(&self) -> Result<Vec<u8>, en::Error> {
let flat_bytes = self.flat()?;
minicbor::to_vec(&flat_bytes).map_err(|err| en::Error::Message(err.to_string()))
let mut bytes = Vec::new();
let mut cbor_encoder = pallas_codec::minicbor::Encoder::new(&mut bytes);
cbor_encoder
.bytes(&flat_bytes)
.map_err(|err| en::Error::Message(err.to_string()))?;
Ok(bytes)
}
// convenient so that people don't need to depend on the flat crate
@ -179,8 +190,10 @@ where
6 => Ok(Term::Error),
7 => Ok(Term::Builtin(DefaultFunction::decode(d)?)),
x => Err(de::Error::Message(format!(
"Unknown term constructor tag: {}",
x
"Unknown term constructor tag: {} and buffer position is {} and buffer length is {}",
x,
d.buffer.len() - d.pos,
d.buffer.len()
))),
}
}

View File

@ -6,10 +6,11 @@ pub mod machine;
pub mod parser;
mod pretty;
pub mod program_builder;
pub mod tx;
pub use pallas_primitives::alonzo::PlutusData;
pub type Error = Box<dyn std::error::Error>;
use pallas_primitives::Fragment;
use pallas_primitives::{Error, Fragment};
pub fn plutus_data(bytes: &[u8]) -> Result<PlutusData, Error> {
PlutusData::decode_fragment(bytes)

View File

@ -11,7 +11,7 @@ mod runtime;
use cost_model::{ExBudget, StepKind};
pub use error::Error;
use pallas_primitives::babbage::{BigInt, PlutusData};
use pallas_primitives::babbage::{BigInt, Language, PlutusData};
use self::{cost_model::CostModel, runtime::BuiltinRuntime};
@ -39,10 +39,16 @@ pub struct Machine {
unbudgeted_steps: [u32; 8],
pub logs: Vec<String>,
stack: Vec<MachineStep>,
version: Language,
}
impl Machine {
pub fn new(costs: CostModel, initial_budget: ExBudget, slippage: u32) -> Machine {
pub fn new(
version: Language,
costs: CostModel,
initial_budget: ExBudget,
slippage: u32,
) -> Machine {
Machine {
costs,
ex_budget: initial_budget,
@ -50,6 +56,7 @@ impl Machine {
unbudgeted_steps: [0; 8],
logs: vec![],
stack: vec![],
version,
}
}
@ -347,8 +354,10 @@ impl Machine {
runtime: BuiltinRuntime,
) -> Result<Value, Error> {
if runtime.is_ready() {
let cost = runtime.to_ex_budget(&self.costs.builtin_costs);
let cost = match self.version {
Language::PlutusV1 => runtime.to_ex_budget_v1(&self.costs.builtin_costs),
Language::PlutusV2 => runtime.to_ex_budget_v2(&self.costs.builtin_costs),
};
self.spend_budget(cost)?;
runtime.call(&mut self.logs)
@ -448,7 +457,13 @@ impl Value {
((i.abs() as f64).log2().floor() as i64 / 64) + 1
}
}
Constant::ByteString(b) => (((b.len() as i64 - 1) / 8) + 1),
Constant::ByteString(b) => {
if b.is_empty() {
1
} else {
((b.len() as i64 - 1) / 8) + 1
}
}
Constant::String(s) => s.chars().count() as i64,
Constant::Unit => 1,
Constant::Bool(_) => 1,
@ -488,7 +503,7 @@ impl Value {
PlutusData::Map(m) => {
let mut new_stack: VecDeque<&PlutusData>;
// create new stack with of items from the list of pairs of data
new_stack = m.deref().iter().fold(VecDeque::new(), |mut acc, d| {
new_stack = m.iter().fold(VecDeque::new(), |mut acc, d| {
acc.push_back(&d.0);
acc.push_back(&d.1);
acc
@ -499,7 +514,7 @@ impl Value {
}
PlutusData::BigInt(i) => {
if let BigInt::Int(g) = i {
let numb: i64 = (*g).try_into().unwrap();
let numb: i128 = (*g).try_into().unwrap();
total += Value::Con(Constant::Integer(numb as isize)).to_ex_mem();
} else {
unreachable!()
@ -517,14 +532,6 @@ impl Value {
new_stack.append(&mut stack);
stack = new_stack;
}
PlutusData::ArrayIndef(a) => {
// create new stack with of items from the list of data
let mut new_stack: VecDeque<&PlutusData> =
VecDeque::from_iter(a.deref().iter());
// Append old stack to the back of the new stack
new_stack.append(&mut stack);
stack = new_stack;
}
}
}
total

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,10 @@
use std::string::FromUtf8Error;
use thiserror::Error;
use crate::ast::{NamedDeBruijn, Term, Type};
use super::{ExBudget, Value};
#[derive(Error, Debug)]
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Over budget mem: {} & cpu: {}", .0.mem, .0.cpu)]
OutOfExError(ExBudget),

View File

@ -1,6 +1,5 @@
use std::ops::Deref;
use pallas_codec::utils::{KeyValuePairs, MaybeIndefArray};
use pallas_primitives::babbage::{BigInt, Constr, PlutusData};
use crate::{
@ -64,8 +63,12 @@ impl BuiltinRuntime {
Ok(())
}
pub fn to_ex_budget(&self, costs: &BuiltinCosts) -> ExBudget {
costs.to_ex_budget(self.fun, &self.args)
pub fn to_ex_budget_v2(&self, costs: &BuiltinCosts) -> ExBudget {
costs.to_ex_budget_v2(self.fun, &self.args)
}
pub fn to_ex_budget_v1(&self, costs: &BuiltinCosts) -> ExBudget {
costs.to_ex_budget_v1(self.fun, &self.args)
}
}
@ -664,8 +667,7 @@ impl DefaultFunction {
DefaultFunction::ChooseData => match &args[0] {
Value::Con(Constant::Data(PlutusData::Constr(_))) => Ok(args[1].clone()),
Value::Con(Constant::Data(PlutusData::Map(_))) => Ok(args[2].clone()),
Value::Con(Constant::Data(PlutusData::Array(_)))
| Value::Con(Constant::Data(PlutusData::ArrayIndef(_))) => Ok(args[3].clone()),
Value::Con(Constant::Data(PlutusData::Array(_))) => Ok(args[3].clone()),
Value::Con(Constant::Data(PlutusData::BigInt(_))) => Ok(args[4].clone()),
Value::Con(Constant::Data(PlutusData::BoundedBytes(_))) => Ok(args[5].clone()),
_ => unreachable!(),
@ -687,7 +689,7 @@ impl DefaultFunction {
// TODO: handle other types of constructor tags
tag: convert_constr_to_tag(*i as u64),
any_constructor: None,
fields: MaybeIndefArray::Indef(data_list),
fields: data_list,
});
Ok(Value::Con(Constant::Data(constr_data)))
}
@ -695,21 +697,23 @@ impl DefaultFunction {
},
DefaultFunction::MapData => match &args[0] {
Value::Con(Constant::ProtoList(_, list)) => {
let data_list: Vec<(PlutusData, PlutusData)> = list
.iter()
.map(|item| match item {
let mut map = Vec::new();
for item in list {
match item {
Constant::ProtoPair(Type::Data, Type::Data, left, right) => {
match (*left.clone(), *right.clone()) {
(Constant::Data(key), Constant::Data(value)) => (key, value),
(Constant::Data(key), Constant::Data(value)) => {
map.push((key, value));
}
_ => unreachable!(),
}
}
_ => unreachable!(),
})
.collect();
Ok(Value::Con(Constant::Data(PlutusData::Map(
KeyValuePairs::Def(data_list),
))))
}
}
Ok(Value::Con(Constant::Data(PlutusData::Map(map.into()))))
}
_ => unreachable!(),
},
@ -722,9 +726,8 @@ impl DefaultFunction {
_ => unreachable!(),
})
.collect();
Ok(Value::Con(Constant::Data(PlutusData::ArrayIndef(
MaybeIndefArray::Indef(data_list),
))))
Ok(Value::Con(Constant::Data(PlutusData::Array(data_list))))
}
_ => unreachable!(),
},
@ -779,8 +782,7 @@ impl DefaultFunction {
_ => unreachable!(),
},
DefaultFunction::UnListData => match &args[0] {
Value::Con(Constant::Data(PlutusData::Array(l)))
| Value::Con(Constant::Data(PlutusData::ArrayIndef(l))) => {
Value::Con(Constant::Data(PlutusData::Array(l))) => {
Ok(Value::Con(Constant::ProtoList(
Type::Data,
l.deref()
@ -794,7 +796,7 @@ impl DefaultFunction {
DefaultFunction::UnIData => match &args[0] {
Value::Con(Constant::Data(PlutusData::BigInt(b))) => {
if let BigInt::Int(i) = b {
let x: i64 = (*i).try_into().unwrap();
let x: i128 = (*i).try_into().unwrap();
Ok(Value::Con(Constant::Integer(x as isize)))
} else {

View File

@ -6,6 +6,7 @@ use crate::{
};
use interner::Interner;
use pallas_primitives::{alonzo::PlutusData, Fragment};
use peg::{error::ParseError, str::LineCol};
mod interner;
@ -24,6 +25,19 @@ pub fn program(src: &str) -> Result<Program<Name>, ParseError<LineCol>> {
Ok(program)
}
pub fn term(src: &str) -> Result<Term<Name>, ParseError<LineCol>> {
// initialize the string interner to get unique name
let mut interner = Interner::new();
// run the generated parser
let mut term = uplc::term(src)?;
// assign proper unique ids in place
interner.term(&mut term);
Ok(term)
}
peg::parser! {
grammar uplc() for str {
pub rule program() -> Program<Name>
@ -36,7 +50,7 @@ peg::parser! {
(major as usize, minor as usize, patch as usize)
}
rule term() -> Term<Name>
pub rule term() -> Term<Name>
= constant()
/ builtin()
/ var()
@ -53,6 +67,7 @@ peg::parser! {
/ constant_string()
/ constant_unit()
/ constant_bool()
/ constant_data()
) _* ")" {
Term::Constant(con)
}
@ -110,6 +125,15 @@ peg::parser! {
rule number() -> isize
= n:$("-"* ['0'..='9']+) {? n.parse().or(Err("isize")) }
rule constant_data() -> Constant
= "data" _+ "#" i:ident()* {
Constant::Data(
PlutusData::decode_fragment(
hex::decode(String::from_iter(i)).unwrap().as_slice()
).unwrap()
)
}
rule name() -> Name
= text:ident() { Name { text, unique: 0.into() } }

132
crates/uplc/src/tx.rs Normal file
View File

@ -0,0 +1,132 @@
use pallas_primitives::{
babbage::{CostMdls, MintedTx, Redeemer, TransactionInput, TransactionOutput},
Fragment,
};
use pallas_traverse::{Era, MultiEraTx};
use error::Error;
pub use eval::get_script_and_datum_lookup_table;
pub use phase_one::eval_phase_one;
use script_context::{ResolvedInput, SlotConfig};
use crate::machine::cost_model::ExBudget;
pub mod error;
mod eval;
mod phase_one;
pub mod script_context;
#[cfg(test)]
mod tests;
mod to_plutus_data;
/// Evaluate the scripts in a transaction using
/// the UPLC Cek Machine. This function collects
/// redeemers with ExUnits calculated from the evaluation.
/// You may optionally run a subset of phase one checks on
/// redeemers and scripts.
pub fn eval_phase_two(
tx: &MintedTx,
utxos: &[ResolvedInput],
cost_mdls: Option<&CostMdls>,
initial_budget: Option<&ExBudget>,
slot_config: &SlotConfig,
run_phase_one: bool,
) -> Result<Vec<Redeemer>, Error> {
let redeemers = tx.transaction_witness_set.redeemer.as_ref();
let lookup_table = get_script_and_datum_lookup_table(tx, utxos);
if run_phase_one {
// subset of phase 1 check on redeemers and scripts
eval_phase_one(tx, utxos, &lookup_table)?;
}
match redeemers {
Some(rs) => {
let mut collected_redeemers = vec![];
for redeemer in rs.iter() {
let redeemer = eval::eval_redeemer(
tx,
utxos,
slot_config,
redeemer,
&lookup_table,
cost_mdls,
initial_budget,
)?;
collected_redeemers.push(redeemer)
}
Ok(collected_redeemers)
}
None => Ok(vec![]),
}
}
/// This function is the same as [`eval_phase_two`]
/// but the inputs are raw bytes.
/// initial_budget expects (cpu, mem).
/// slot_config (zero_time, zero_slot, slot_length)
pub fn eval_phase_two_raw(
tx_bytes: &[u8],
utxos_bytes: &[(Vec<u8>, Vec<u8>)],
cost_mdls_bytes: &[u8],
initial_budget: (u64, u64),
slot_config: (u64, u64, u64),
run_phase_one: bool,
) -> Result<Vec<Vec<u8>>, Error> {
let multi_era_tx = MultiEraTx::decode(Era::Babbage, tx_bytes)
.or_else(|_| MultiEraTx::decode(Era::Alonzo, tx_bytes))?;
let cost_mdls = CostMdls::decode_fragment(cost_mdls_bytes)?;
let budget = ExBudget {
cpu: initial_budget.0 as i64,
mem: initial_budget.1 as i64,
};
let mut utxos = Vec::new();
for (input, output) in utxos_bytes {
utxos.push(ResolvedInput {
input: TransactionInput::decode_fragment(input)?,
output: TransactionOutput::decode_fragment(output)?,
});
}
let sc = SlotConfig {
zero_time: slot_config.0,
zero_slot: slot_config.1,
slot_length: slot_config.2,
};
match multi_era_tx {
MultiEraTx::Babbage(tx) => {
match eval_phase_two(
&tx,
&utxos,
Some(&cost_mdls),
Some(&budget),
&sc,
run_phase_one,
) {
Ok(redeemers) => Ok(redeemers
.iter()
.map(|r| r.encode_fragment().unwrap())
.collect()),
Err(_) => Err(Error::NoRedeemers),
}
}
// MultiEraTx::AlonzoCompatible(tx, _) => match eval_tx(&tx, &utxos, &sc) {
// Ok(redeemers) => Ok(redeemers
// .iter()
// .map(|r| r.encode_fragment().unwrap())
// .collect()),
// Err(_) => Err(()),
// },
// TODO: I probably did a mistake here with using MintedTx which is only compatible with Babbage tx.
_ => todo!("Wrong era. Please use babbage"),
}
}

View File

@ -0,0 +1,52 @@
use crate::machine::{self, cost_model::ExBudget};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("{0}")]
Address(#[from] pallas_addresses::Error),
#[error("Only shelley reward addresses can be a part of withdrawals")]
BadWithdrawalAddress,
#[error("{0}")]
FlatDecode(#[from] flat_rs::de::Error),
#[error("{0}")]
FragmentDecode(#[from] pallas_primitives::Error),
#[error("{}\n\n{:#?}\n\n{}", .0, .1, .2.join("\n"))]
Machine(machine::Error, ExBudget, Vec<String>),
#[error("Native script can't be executed in phase-two")]
NativeScriptPhaseTwo,
#[error("Can't eval without redeemers")]
NoRedeemers,
#[error("Mismatch in required redeemers: {} {}", .missing.join(" "), .extra.join(" "))]
RequiredRedeemersMismatch {
missing: Vec<String>,
extra: Vec<String>,
},
#[error("Extraneous redeemer found: Tag {:?}, Index {}", tag, index)]
ExtraneousRedeemer { tag: String, index: u32 },
#[error("Resolved Input not found")]
ResolvedInputNotFound,
#[error("A key hash cannot be the hash of a script")]
ScriptKeyHash,
#[error("PlutusV1 cost model not found.")]
V1CostModelNotFound,
#[error("PlutusV2 cost model not found.")]
V2CostModelNotFound,
#[error("Wrong era, Please use Babbage or Alonzo: {0}")]
WrongEra(#[from] pallas_codec::minicbor::decode::Error),
#[error("Byron address not allowed in Plutus.")]
ByronAddressNotAllowed,
#[error("Inline datum not allowed in PlutusV1.")]
InlineDatumNotAllowed,
#[error("Script and input reference not allowed in PlutusV1.")]
ScriptAndInputRefNotAllowed,
#[error("Address doesn't contain a payment credential.")]
NoPaymentCredential,
#[error("Missing required datum in witness set. Datum hash: {}", hash)]
MissingRequiredDatum { hash: String },
#[error("Missing required script. Script hash: {}", hash)]
MissingRequiredScript { hash: String },
#[error("Missing required inline datum or datum hash in script input.")]
MissingRequiredInlineDatumOrHash,
#[error("Only stake deregistration and delegation are valid certificate script purposes.")]
OnlyStakeDeregAndDelegAllowed,
}

833
crates/uplc/src/tx/eval.rs Normal file
View File

@ -0,0 +1,833 @@
use crate::{
ast::{FakeNamedDeBruijn, NamedDeBruijn, Program},
machine::cost_model::ExBudget,
PlutusData,
};
use pallas_addresses::{Address, ScriptHash, StakePayload};
use pallas_codec::utils::{KeyValuePairs, MaybeIndefArray};
use pallas_crypto::hash::Hash;
use pallas_primitives::babbage::{
Certificate, CostMdls, DatumHash, DatumOption, ExUnits, Language, Mint, MintedTx, NativeScript,
PlutusV1Script, PlutusV2Script, PolicyId, Redeemer, RedeemerTag, RewardAccount, Script,
StakeCredential, TransactionInput, TransactionOutput, Value, Withdrawals,
};
use pallas_traverse::{ComputeHash, OriginalHash};
use std::{collections::HashMap, convert::TryInto, ops::Deref, vec};
use super::{
script_context::{
ResolvedInput, ScriptContext, ScriptPurpose, SlotConfig, TimeRange, TxInInfo, TxInfo,
TxInfoV1, TxInfoV2, TxOut,
},
to_plutus_data::{MintValue, ToPlutusData},
Error,
};
fn slot_to_begin_posix_time(slot: u64, sc: &SlotConfig) -> u64 {
let ms_after_begin = (slot - sc.zero_slot) * sc.slot_length;
sc.zero_time + ms_after_begin
}
fn slot_range_to_posix_time_range(slot_range: TimeRange, sc: &SlotConfig) -> TimeRange {
TimeRange {
lower_bound: slot_range
.lower_bound
.map(|lower_bound| slot_to_begin_posix_time(lower_bound, sc)),
upper_bound: slot_range
.upper_bound
.map(|upper_bound| slot_to_begin_posix_time(upper_bound, sc)),
}
}
#[derive(Debug, PartialEq, Clone)]
pub enum ScriptVersion {
Native(NativeScript),
V1(PlutusV1Script),
V2(PlutusV2Script),
}
#[derive(Debug, PartialEq, Clone)]
enum ExecutionPurpose {
WithDatum(ScriptVersion, PlutusData), // Spending
NoDatum(ScriptVersion), // Minting, Wdrl, DCert
}
pub struct DataLookupTable {
datum: HashMap<DatumHash, PlutusData>,
scripts: HashMap<ScriptHash, ScriptVersion>,
}
impl DataLookupTable {
pub fn scripts(&self) -> HashMap<ScriptHash, ScriptVersion> {
self.scripts.clone()
}
}
pub fn get_tx_in_info_v1(
inputs: &[TransactionInput],
utxos: &[ResolvedInput],
) -> Result<Vec<TxInInfo>, Error> {
inputs
.iter()
.map(|input| {
let utxo = match utxos.iter().find(|utxo| utxo.input == *input) {
Some(resolved) => resolved,
None => return Err(Error::ResolvedInputNotFound),
};
let address = Address::from_bytes(match &utxo.output {
TransactionOutput::Legacy(output) => output.address.as_ref(),
TransactionOutput::PostAlonzo(output) => output.address.as_ref(),
})
.unwrap();
match address {
Address::Byron(_) => {
return Err(Error::ByronAddressNotAllowed);
}
Address::Stake(_) => {
return Err(Error::NoPaymentCredential);
}
_ => {}
};
match &utxo.output {
TransactionOutput::Legacy(_) => {}
TransactionOutput::PostAlonzo(output) => {
if let Some(DatumOption::Data(_)) = output.datum_option {
return Err(Error::InlineDatumNotAllowed);
}
if output.script_ref.is_some() {
return Err(Error::ScriptAndInputRefNotAllowed);
}
}
}
Ok(TxInInfo {
out_ref: utxo.input.clone(),
resolved: TxOut::V1(utxo.output.clone()),
})
})
.collect()
}
fn get_tx_in_info_v2(
inputs: &[TransactionInput],
utxos: &[ResolvedInput],
) -> Result<Vec<TxInInfo>, Error> {
inputs
.iter()
.map(|input| {
let utxo = match utxos.iter().find(|utxo| utxo.input == *input) {
Some(resolved) => resolved,
None => return Err(Error::ResolvedInputNotFound),
};
let address = Address::from_bytes(match &utxo.output {
TransactionOutput::Legacy(output) => output.address.as_ref(),
TransactionOutput::PostAlonzo(output) => output.address.as_ref(),
})
.unwrap();
match address {
Address::Byron(_) => {
return Err(Error::ByronAddressNotAllowed);
}
Address::Stake(_) => {
return Err(Error::NoPaymentCredential);
}
_ => {}
};
Ok(TxInInfo {
out_ref: utxo.input.clone(),
resolved: TxOut::V2(utxo.output.clone()),
})
})
.collect()
}
fn get_script_purpose(
redeemer: &Redeemer,
inputs: &[TransactionInput],
mint: &Option<Mint>,
dcert: &Option<Vec<Certificate>>,
wdrl: &Option<Withdrawals>,
) -> Result<ScriptPurpose, Error> {
// sorting according to specs section 4.1: https://hydra.iohk.io/build/18583827/download/1/alonzo-changes.pdf
let tag = redeemer.tag.clone();
let index = redeemer.index;
match tag {
RedeemerTag::Mint => {
// sort lexical by policy id
let mut policy_ids = mint
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(policy_id, _)| *policy_id)
.collect::<Vec<PolicyId>>();
policy_ids.sort();
match policy_ids.get(index as usize) {
Some(policy_id) => Ok(ScriptPurpose::Minting(*policy_id)),
None => Err(Error::ExtraneousRedeemer {
tag: "Mint".to_string(),
index,
}),
}
}
RedeemerTag::Spend => {
// sort lexical by tx_hash and index
let mut inputs = inputs.to_vec();
inputs.sort();
match inputs.get(index as usize) {
Some(input) => Ok(ScriptPurpose::Spending(input.clone())),
None => Err(Error::ExtraneousRedeemer {
tag: "Spend".to_string(),
index,
}),
}
}
RedeemerTag::Reward => {
// sort lexical by reward account
let mut reward_accounts = wdrl
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(racnt, _)| racnt.clone())
.collect::<Vec<RewardAccount>>();
reward_accounts.sort();
let reward_account = match reward_accounts.get(index as usize) {
Some(ra) => ra.clone(),
None => {
return Err(Error::ExtraneousRedeemer {
tag: "Reward".to_string(),
index,
})
}
};
let address = Address::from_bytes(&reward_account)?;
let credential = match address {
Address::Stake(stake_address) => match stake_address.payload() {
StakePayload::Script(script_hash) => StakeCredential::Scripthash(*script_hash),
StakePayload::Stake(_) => {
return Err(Error::ScriptKeyHash);
}
},
_ => return Err(Error::BadWithdrawalAddress),
};
Ok(ScriptPurpose::Rewarding(credential))
}
RedeemerTag::Cert => {
// sort by order given in the tx (just take it as it is basically)
match dcert
.as_ref()
.unwrap_or(&MaybeIndefArray::Indef(vec![]))
.get(index as usize)
{
Some(cert) => Ok(ScriptPurpose::Certifying(cert.clone())),
None => Err(Error::ExtraneousRedeemer {
tag: "Cert".to_string(),
index,
}),
}
}
}
}
fn get_tx_info_v1(
tx: &MintedTx,
utxos: &[ResolvedInput],
slot_config: &SlotConfig,
) -> Result<TxInfo, Error> {
let body = tx.transaction_body.clone();
if body.reference_inputs.is_some() {
return Err(Error::ScriptAndInputRefNotAllowed);
}
let inputs = get_tx_in_info_v1(&body.inputs, utxos)?;
let outputs = body
.outputs
.iter()
.map(|output| TxOut::V1(output.clone()))
.collect();
let fee = Value::Coin(body.fee);
let mint = body.mint.clone().unwrap_or(KeyValuePairs::Indef(vec![]));
let dcert = body.certificates.clone().unwrap_or_default();
let wdrl = body
.withdrawals
.clone()
.unwrap_or(KeyValuePairs::Indef(vec![]))
.deref()
.clone();
let valid_range = slot_range_to_posix_time_range(
TimeRange {
lower_bound: body.validity_interval_start,
upper_bound: body.ttl,
},
slot_config,
);
let signatories = body.required_signers.clone().unwrap_or_default();
let data = tx
.transaction_witness_set
.plutus_data
.as_ref()
.unwrap_or(&vec![])
.iter()
.map(|d| (d.original_hash(), d.clone().unwrap()))
.collect();
let id = tx.transaction_body.compute_hash();
Ok(TxInfo::V1(TxInfoV1 {
inputs,
outputs,
fee,
mint: MintValue { mint_value: mint },
dcert,
wdrl,
valid_range,
signatories,
data,
id,
}))
}
fn get_tx_info_v2(
tx: &MintedTx,
utxos: &[ResolvedInput],
slot_config: &SlotConfig,
) -> Result<TxInfo, Error> {
let body = tx.transaction_body.clone();
let inputs = get_tx_in_info_v2(&body.inputs, utxos)?;
let reference_inputs =
get_tx_in_info_v2(&body.reference_inputs.clone().unwrap_or_default(), utxos)?;
let outputs = body
.outputs
.iter()
.map(|output| TxOut::V2(output.clone()))
.collect();
let fee = Value::Coin(body.fee);
let mint = body.mint.clone().unwrap_or(KeyValuePairs::Indef(vec![]));
let dcert = body.certificates.clone().unwrap_or_default();
let wdrl = body
.withdrawals
.clone()
.unwrap_or(KeyValuePairs::Indef(vec![]));
let valid_range = slot_range_to_posix_time_range(
TimeRange {
lower_bound: body.validity_interval_start,
upper_bound: body.ttl,
},
slot_config,
);
let signatories = body.required_signers.clone().unwrap_or_default();
let redeemers = KeyValuePairs::Indef(
tx.transaction_witness_set
.redeemer
.as_ref()
.unwrap_or(&MaybeIndefArray::Indef(vec![]))
.iter()
.map(|r| {
(
get_script_purpose(
r,
&tx.transaction_body.inputs,
&tx.transaction_body.mint,
&tx.transaction_body.certificates,
&tx.transaction_body.withdrawals,
)
.unwrap(),
r.clone(),
)
})
.collect(),
);
let data = KeyValuePairs::Indef(
tx.transaction_witness_set
.plutus_data
.as_ref()
.unwrap_or(&vec![])
.iter()
.map(|d| (d.original_hash(), d.clone().unwrap()))
.collect(),
);
let id = tx.transaction_body.compute_hash();
Ok(TxInfo::V2(TxInfoV2 {
inputs,
reference_inputs,
outputs,
fee,
mint: MintValue { mint_value: mint },
dcert,
wdrl,
valid_range,
signatories,
redeemers,
data,
id,
}))
}
fn get_execution_purpose(
utxos: &[ResolvedInput],
script_purpose: &ScriptPurpose,
lookup_table: &DataLookupTable,
) -> Result<ExecutionPurpose, Error> {
match script_purpose {
ScriptPurpose::Minting(policy_id) => {
let policy_id_array: [u8; 28] = policy_id.to_vec().try_into().unwrap();
let hash = Hash::from(policy_id_array);
let script = match lookup_table.scripts.get(&hash) {
Some(s) => s.clone(),
None => {
return Err(Error::MissingRequiredScript {
hash: hash.to_string(),
})
}
};
Ok(ExecutionPurpose::NoDatum(script))
}
ScriptPurpose::Spending(out_ref) => {
let utxo = match utxos.iter().find(|utxo| utxo.input == *out_ref) {
Some(resolved) => resolved,
None => return Err(Error::ResolvedInputNotFound),
};
match &utxo.output {
TransactionOutput::Legacy(output) => {
let address = Address::from_bytes(&output.address).unwrap();
match address {
Address::Shelley(shelley_address) => {
let hash = shelley_address.payment().as_hash();
let script = match lookup_table.scripts.get(hash) {
Some(s) => s.clone(),
None => {
return Err(Error::MissingRequiredScript {
hash: hash.to_string(),
})
}
};
let datum_hash = match &output.datum_hash {
Some(hash) => hash,
None => return Err(Error::MissingRequiredInlineDatumOrHash),
};
let datum = match lookup_table.datum.get(datum_hash) {
Some(d) => d.clone(),
None => {
return Err(Error::MissingRequiredDatum {
hash: datum_hash.to_string(),
})
}
};
Ok(ExecutionPurpose::WithDatum(script, datum))
}
_ => Err(Error::ScriptKeyHash),
}
}
TransactionOutput::PostAlonzo(output) => {
let address = Address::from_bytes(&output.address).unwrap();
match address {
Address::Shelley(shelley_address) => {
let hash = shelley_address.payment().as_hash();
let script = match lookup_table.scripts.get(hash) {
Some(s) => s.clone(),
None => {
return Err(Error::MissingRequiredScript {
hash: hash.to_string(),
})
}
};
let datum = match &output.datum_option {
Some(DatumOption::Hash(hash)) => {
match lookup_table.datum.get(hash) {
Some(d) => d.clone(),
None => {
return Err(Error::MissingRequiredDatum {
hash: hash.to_string(),
})
}
}
}
Some(DatumOption::Data(data)) => data.0.clone(),
_ => return Err(Error::MissingRequiredInlineDatumOrHash),
};
Ok(ExecutionPurpose::WithDatum(script, datum))
}
_ => Err(Error::ScriptKeyHash),
}
}
}
}
ScriptPurpose::Rewarding(stake_credential) => {
let script_hash = match stake_credential {
StakeCredential::Scripthash(hash) => *hash,
_ => return Err(Error::ScriptKeyHash),
};
let script = match lookup_table.scripts.get(&script_hash) {
Some(s) => s.clone(),
None => {
return Err(Error::MissingRequiredScript {
hash: script_hash.to_string(),
})
}
};
Ok(ExecutionPurpose::NoDatum(script))
}
ScriptPurpose::Certifying(cert) => match cert {
Certificate::StakeDeregistration(stake_credential) => {
let script_hash = match stake_credential {
StakeCredential::Scripthash(hash) => *hash,
_ => return Err(Error::ScriptKeyHash),
};
let script = match lookup_table.scripts.get(&script_hash) {
Some(s) => s.clone(),
None => {
return Err(Error::MissingRequiredScript {
hash: script_hash.to_string(),
})
}
};
Ok(ExecutionPurpose::NoDatum(script))
}
Certificate::StakeDelegation(stake_credential, _) => {
let script_hash = match stake_credential {
StakeCredential::Scripthash(hash) => *hash,
_ => return Err(Error::ScriptKeyHash),
};
let script = match lookup_table.scripts.get(&script_hash) {
Some(s) => s.clone(),
None => {
return Err(Error::MissingRequiredScript {
hash: script_hash.to_string(),
})
}
};
Ok(ExecutionPurpose::NoDatum(script))
}
_ => Err(Error::OnlyStakeDeregAndDelegAllowed),
},
}
}
pub fn get_script_and_datum_lookup_table(
tx: &MintedTx,
utxos: &[ResolvedInput],
) -> DataLookupTable {
let mut datum = HashMap::new();
let mut scripts = HashMap::new();
// discovery in witness set
let plutus_data_witnesses = tx
.transaction_witness_set
.plutus_data
.clone()
.unwrap_or_default();
let scripts_native_witnesses = tx
.transaction_witness_set
.native_script
.clone()
.unwrap_or_default();
let scripts_v1_witnesses = tx
.transaction_witness_set
.plutus_v1_script
.clone()
.unwrap_or_default();
let scripts_v2_witnesses = tx
.transaction_witness_set
.plutus_v2_script
.clone()
.unwrap_or_default();
for plutus_data in plutus_data_witnesses.iter() {
datum.insert(plutus_data.original_hash(), plutus_data.clone().unwrap());
}
for script in scripts_native_witnesses.iter() {
scripts.insert(script.compute_hash(), ScriptVersion::Native(script.clone()));
}
for script in scripts_v1_witnesses.iter() {
scripts.insert(script.compute_hash(), ScriptVersion::V1(script.clone()));
}
for script in scripts_v2_witnesses.iter() {
scripts.insert(script.compute_hash(), ScriptVersion::V2(script.clone()));
}
// discovery in utxos (script ref)
for utxo in utxos.iter() {
match &utxo.output {
TransactionOutput::Legacy(_) => {}
TransactionOutput::PostAlonzo(output) => {
if let Some(script) = &output.script_ref {
match &script.0 {
Script::NativeScript(ns) => {
scripts.insert(ns.compute_hash(), ScriptVersion::Native(ns.clone()));
}
Script::PlutusV1Script(v1) => {
scripts.insert(v1.compute_hash(), ScriptVersion::V1(v1.clone()));
}
Script::PlutusV2Script(v2) => {
scripts.insert(v2.compute_hash(), ScriptVersion::V2(v2.clone()));
}
}
}
}
}
}
DataLookupTable { datum, scripts }
}
pub fn eval_redeemer(
tx: &MintedTx,
utxos: &[ResolvedInput],
slot_config: &SlotConfig,
redeemer: &Redeemer,
lookup_table: &DataLookupTable,
cost_mdls_opt: Option<&CostMdls>,
initial_budget: Option<&ExBudget>,
) -> Result<Redeemer, Error> {
let purpose = get_script_purpose(
redeemer,
&tx.transaction_body.inputs,
&tx.transaction_body.mint,
&tx.transaction_body.certificates,
&tx.transaction_body.withdrawals,
)?;
let execution_purpose: ExecutionPurpose = get_execution_purpose(utxos, &purpose, lookup_table)?;
match execution_purpose {
ExecutionPurpose::WithDatum(script_version, datum) => match script_version {
ScriptVersion::V1(script) => {
let tx_info = get_tx_info_v1(tx, utxos, slot_config)?;
let script_context = ScriptContext { tx_info, purpose };
let program: Program<NamedDeBruijn> = {
let mut buffer = Vec::new();
let prog = Program::<FakeNamedDeBruijn>::from_cbor(&script.0, &mut buffer)?;
prog.into()
};
let program = program
.apply_data(datum)
.apply_data(redeemer.data.clone())
.apply_data(script_context.to_plutus_data());
let (result, budget, logs) = if let Some(cost_mdls) = cost_mdls_opt {
let costs = if let Some(costs) = &cost_mdls.plutus_v1 {
costs
} else {
return Err(Error::V1CostModelNotFound);
};
program.eval_as(&Language::PlutusV1, costs, initial_budget)
} else {
program.eval_v1()
};
match result {
Ok(_) => (),
Err(err) => return Err(Error::Machine(err, budget, logs)),
}
let initial_budget = match initial_budget {
Some(b) => *b,
None => ExBudget::default(),
};
let new_redeemer = Redeemer {
tag: redeemer.tag.clone(),
index: redeemer.index,
data: redeemer.data.clone(),
ex_units: ExUnits {
mem: (initial_budget.mem - budget.mem) as u32,
steps: (initial_budget.cpu - budget.cpu) as u64,
},
};
Ok(new_redeemer)
}
ScriptVersion::V2(script) => {
let tx_info = get_tx_info_v2(tx, utxos, slot_config)?;
let script_context = ScriptContext { tx_info, purpose };
let program: Program<NamedDeBruijn> = {
let mut buffer = Vec::new();
let prog = Program::<FakeNamedDeBruijn>::from_cbor(&script.0, &mut buffer)?;
prog.into()
};
let program = program
.apply_data(datum)
.apply_data(redeemer.data.clone())
.apply_data(script_context.to_plutus_data());
let (result, budget, logs) = if let Some(cost_mdls) = cost_mdls_opt {
let costs = if let Some(costs) = &cost_mdls.plutus_v2 {
costs
} else {
return Err(Error::V2CostModelNotFound);
};
program.eval_as(&Language::PlutusV2, costs, initial_budget)
} else {
program.eval()
};
match result {
Ok(_) => (),
Err(err) => return Err(Error::Machine(err, budget, logs)),
}
let initial_budget = match initial_budget {
Some(b) => *b,
None => ExBudget::default(),
};
let new_redeemer = Redeemer {
tag: redeemer.tag.clone(),
index: redeemer.index,
data: redeemer.data.clone(),
ex_units: ExUnits {
mem: (initial_budget.mem - budget.mem) as u32,
steps: (initial_budget.cpu - budget.cpu) as u64,
},
};
Ok(new_redeemer)
}
ScriptVersion::Native(_) => Err(Error::NativeScriptPhaseTwo),
},
ExecutionPurpose::NoDatum(script_version) => match script_version {
ScriptVersion::V1(script) => {
let tx_info = get_tx_info_v1(tx, utxos, slot_config)?;
let script_context = ScriptContext { tx_info, purpose };
let program: Program<NamedDeBruijn> = {
let mut buffer = Vec::new();
let prog = Program::<FakeNamedDeBruijn>::from_cbor(&script.0, &mut buffer)?;
prog.into()
};
let program = program
.apply_data(redeemer.data.clone())
.apply_data(script_context.to_plutus_data());
let (result, budget, logs) = if let Some(cost_mdls) = cost_mdls_opt {
let costs = if let Some(costs) = &cost_mdls.plutus_v1 {
costs
} else {
return Err(Error::V1CostModelNotFound);
};
program.eval_as(&Language::PlutusV1, costs, initial_budget)
} else {
program.eval_v1()
};
match result {
Ok(_) => (),
Err(err) => return Err(Error::Machine(err, budget, logs)),
}
let initial_budget = match initial_budget {
Some(b) => *b,
None => ExBudget::default(),
};
let new_redeemer = Redeemer {
tag: redeemer.tag.clone(),
index: redeemer.index,
data: redeemer.data.clone(),
ex_units: ExUnits {
mem: (initial_budget.mem - budget.mem) as u32,
steps: (initial_budget.cpu - budget.cpu) as u64,
},
};
Ok(new_redeemer)
}
ScriptVersion::V2(script) => {
let tx_info = get_tx_info_v2(tx, utxos, slot_config)?;
let script_context = ScriptContext { tx_info, purpose };
let program: Program<NamedDeBruijn> = {
let mut buffer = Vec::new();
let prog = Program::<FakeNamedDeBruijn>::from_cbor(&script.0, &mut buffer)?;
prog.into()
};
let program = program
.apply_data(redeemer.data.clone())
.apply_data(script_context.to_plutus_data());
let (result, budget, logs) = if let Some(cost_mdls) = cost_mdls_opt {
let costs = if let Some(costs) = &cost_mdls.plutus_v2 {
costs
} else {
return Err(Error::V2CostModelNotFound);
};
program.eval_as(&Language::PlutusV2, costs, initial_budget)
} else {
program.eval()
};
match result {
Ok(_) => (),
Err(err) => return Err(Error::Machine(err, budget, logs)),
}
let initial_budget = match initial_budget {
Some(b) => *b,
None => ExBudget::default(),
};
let new_redeemer = Redeemer {
tag: redeemer.tag.clone(),
index: redeemer.index,
data: redeemer.data.clone(),
ex_units: ExUnits {
mem: (initial_budget.mem - budget.mem) as u32,
steps: (initial_budget.cpu - budget.cpu) as u64,
},
};
Ok(new_redeemer)
}
ScriptVersion::Native(_) => Err(Error::NativeScriptPhaseTwo),
},
}
}

View File

@ -0,0 +1,324 @@
use std::collections::HashMap;
use pallas_addresses::{Address, ScriptHash, ShelleyPaymentPart, StakePayload};
use pallas_codec::utils::{KeyValuePairs, MaybeIndefArray};
use pallas_primitives::babbage::{
Certificate, MintedTx, PolicyId, RedeemerTag, RewardAccount, StakeCredential, TransactionOutput,
};
use super::{
error::Error,
eval::{DataLookupTable, ScriptVersion},
script_context::{ResolvedInput, ScriptPurpose},
};
// TODO: include in pallas eventually?
#[derive(Debug, PartialEq, Clone)]
struct RedeemerPtr {
tag: RedeemerTag,
index: u32,
}
type AlonzoScriptsNeeded = Vec<(ScriptPurpose, ScriptHash)>;
// subset of phase-1 ledger checks related to scripts
pub fn eval_phase_one(
tx: &MintedTx,
utxos: &[ResolvedInput],
lookup_table: &DataLookupTable,
) -> Result<(), Error> {
let scripts_needed = scripts_needed(tx, utxos)?;
validate_missing_scripts(&scripts_needed, lookup_table.scripts())?;
has_exact_set_of_redeemers(tx, &scripts_needed, lookup_table.scripts())?;
Ok(())
}
pub fn validate_missing_scripts(
needed: &AlonzoScriptsNeeded,
txscripts: HashMap<ScriptHash, ScriptVersion>,
) -> Result<(), Error> {
let received_hashes = txscripts.keys().copied().collect::<Vec<ScriptHash>>();
let needed_hashes = needed.iter().map(|x| x.1).collect::<Vec<ScriptHash>>();
let missing: Vec<_> = needed_hashes
.clone()
.into_iter()
.filter(|x| !received_hashes.contains(x))
.map(|x| format!("[Missing (sh: {})]", x))
.collect();
let extra: Vec<_> = received_hashes
.into_iter()
.filter(|x| !needed_hashes.contains(x))
.map(|x| format!("[Extraneous (sh: {:?})]", x))
.collect();
if !missing.is_empty() || !extra.is_empty() {
let missing_errors = missing.join(" ");
let extra_errors = extra.join(" ");
unreachable!(
"Mismatch in required scripts: {} {}",
missing_errors, extra_errors
);
}
Ok(())
}
pub fn scripts_needed(
tx: &MintedTx,
utxos: &[ResolvedInput],
) -> Result<AlonzoScriptsNeeded, Error> {
let mut needed = Vec::new();
let txb = tx.transaction_body.clone();
let mut spend = Vec::new();
for input in txb.inputs.iter() {
let utxo = match utxos.iter().find(|utxo| utxo.input == *input) {
Some(u) => u,
None => return Err(Error::ResolvedInputNotFound),
};
let address = Address::from_bytes(match &utxo.output {
TransactionOutput::Legacy(output) => output.address.as_ref(),
TransactionOutput::PostAlonzo(output) => output.address.as_ref(),
})?;
if let Address::Shelley(a) = address {
if let ShelleyPaymentPart::Script(h) = a.payment() {
spend.push((ScriptPurpose::Spending(input.clone()), *h));
}
}
}
let mut reward = txb
.withdrawals
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.filter_map(|(acnt, _)| {
let address = Address::from_bytes(acnt).unwrap();
if let Address::Stake(a) = address {
if let StakePayload::Script(h) = a.payload() {
let cred = StakeCredential::Scripthash(*h);
return Some((ScriptPurpose::Rewarding(cred), *h));
}
}
None
})
.collect::<AlonzoScriptsNeeded>();
let mut cert = txb
.certificates
.clone()
.unwrap_or_default()
.iter()
.filter_map(|cert| {
// only Dereg and Deleg certs can require scripts
match cert {
Certificate::StakeDeregistration(StakeCredential::Scripthash(h)) => {
Some((ScriptPurpose::Certifying(cert.clone()), *h))
}
Certificate::StakeDelegation(StakeCredential::Scripthash(h), _) => {
Some((ScriptPurpose::Certifying(cert.clone()), *h))
}
_ => None,
}
})
.collect::<AlonzoScriptsNeeded>();
let mut mint = txb
.mint
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(policy_id, _)| (ScriptPurpose::Minting(*policy_id), *policy_id))
.collect::<AlonzoScriptsNeeded>();
needed.append(&mut spend);
needed.append(&mut reward);
needed.append(&mut cert);
needed.append(&mut mint);
Ok(needed)
}
/// hasExactSetOfRedeemers in Ledger Spec, but we pass `txscripts` directly
pub fn has_exact_set_of_redeemers(
tx: &MintedTx,
needed: &AlonzoScriptsNeeded,
tx_scripts: HashMap<ScriptHash, ScriptVersion>,
) -> Result<(), Error> {
let mut redeemers_needed = Vec::new();
for (script_purpose, script_hash) in needed {
let redeemer_ptr = build_redeemer_ptr(tx, script_purpose)?;
let script = tx_scripts.get(script_hash);
if let (Some(ptr), Some(script)) = (redeemer_ptr, script) {
match script {
ScriptVersion::V1(_) => {
redeemers_needed.push((ptr, script_purpose.clone(), *script_hash))
}
ScriptVersion::V2(_) => {
redeemers_needed.push((ptr, script_purpose.clone(), *script_hash))
}
ScriptVersion::Native(_) => (),
}
}
}
let wits_redeemer_ptrs: Vec<RedeemerPtr> = tx
.transaction_witness_set
.redeemer
.as_ref()
.unwrap_or(&MaybeIndefArray::Indef(vec![]))
.iter()
.map(|r| RedeemerPtr {
tag: r.tag.clone(),
index: r.index,
})
.collect();
let needed_redeemer_ptrs: Vec<RedeemerPtr> =
redeemers_needed.iter().map(|x| x.0.clone()).collect();
let missing: Vec<_> = redeemers_needed
.into_iter()
.filter(|x| !wits_redeemer_ptrs.contains(&x.0))
.map(|x| {
format!(
"[Missing (redeemer_ptr: {:?}, script_purpose: {:?}, script_hash: {})]",
x.0, x.1, x.2,
)
})
.collect();
let extra: Vec<_> = wits_redeemer_ptrs
.into_iter()
.filter(|x| !needed_redeemer_ptrs.contains(x))
.map(|x| format!("[Extraneous (redeemer_ptr: {:?})]", x))
.collect();
if !missing.is_empty() || !extra.is_empty() {
Err(Error::RequiredRedeemersMismatch { missing, extra })
} else {
Ok(())
}
}
/// builds a redeemer pointer (tag, index) from a script purpose by setting the tag
/// according to the type of the script purpose, and the index according to the
/// placement of script purpose inside its container.
fn build_redeemer_ptr(
tx: &MintedTx,
script_purpose: &ScriptPurpose,
) -> Result<Option<RedeemerPtr>, Error> {
let tx_body = tx.transaction_body.clone();
match script_purpose {
ScriptPurpose::Minting(hash) => {
let mut policy_ids = tx_body
.mint
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(policy_id, _)| *policy_id)
.collect::<Vec<PolicyId>>();
policy_ids.sort();
let maybe_idx = policy_ids.iter().position(|x| x == hash);
match maybe_idx {
Some(idx) => Ok(Some(RedeemerPtr {
tag: RedeemerTag::Mint,
index: idx as u32,
})),
None => Ok(None),
}
}
ScriptPurpose::Spending(txin) => {
let mut inputs = tx_body.inputs.to_vec();
inputs.sort_by(
|i_a, i_b| match i_a.transaction_id.cmp(&i_b.transaction_id) {
std::cmp::Ordering::Less => std::cmp::Ordering::Less,
std::cmp::Ordering::Equal => i_a.index.cmp(&i_b.index),
std::cmp::Ordering::Greater => std::cmp::Ordering::Greater,
},
);
let maybe_idx = inputs.iter().position(|x| x == txin);
match maybe_idx {
Some(idx) => Ok(Some(RedeemerPtr {
tag: RedeemerTag::Spend,
index: idx as u32,
})),
None => Ok(None),
}
}
ScriptPurpose::Rewarding(racnt) => {
let mut reward_accounts = tx_body
.withdrawals
.as_ref()
.unwrap_or(&KeyValuePairs::Indef(vec![]))
.iter()
.map(|(acnt, _)| acnt.clone())
.collect::<Vec<RewardAccount>>();
reward_accounts.sort();
let mut maybe_idx = None;
for (idx, x) in reward_accounts.iter().enumerate() {
let cred = match Address::from_bytes(x).unwrap() {
Address::Stake(a) => match a.payload() {
StakePayload::Script(sh) => StakeCredential::Scripthash(*sh),
StakePayload::Stake(_) => {
return Err(Error::ScriptKeyHash);
}
},
_ => return Err(Error::BadWithdrawalAddress),
};
if cred == *racnt {
maybe_idx = Some(idx);
}
}
match maybe_idx {
Some(idx) => Ok(Some(RedeemerPtr {
tag: RedeemerTag::Reward,
index: idx as u32,
})),
None => Ok(None),
}
}
ScriptPurpose::Certifying(d) => {
let maybe_idx = tx_body
.certificates
.as_ref()
.unwrap_or(&MaybeIndefArray::Indef(vec![]))
.iter()
.position(|x| x == d);
match maybe_idx {
Some(idx) => Ok(Some(RedeemerPtr {
tag: RedeemerTag::Cert,
index: idx as u32,
})),
None => Ok(None),
}
}
}
}

View File

@ -0,0 +1,99 @@
use pallas_codec::utils::KeyValuePairs;
use pallas_crypto::hash::Hash;
use pallas_primitives::babbage::{
AddrKeyhash, Certificate, Coin, DatumHash, PlutusData, PolicyId, Redeemer, RewardAccount,
StakeCredential, TransactionInput, TransactionOutput, Value, Withdrawals,
};
use serde::Deserialize;
use super::to_plutus_data::MintValue;
#[derive(Debug, PartialEq, Clone, Deserialize)]
pub struct ResolvedInput {
pub input: TransactionInput,
pub output: TransactionOutput,
}
#[derive(Debug, PartialEq, Clone)]
pub struct TxInInfo {
pub out_ref: TransactionInput,
pub resolved: TxOut,
}
#[derive(Debug, PartialEq, Clone)]
pub enum TxOut {
V1(TransactionOutput),
V2(TransactionOutput),
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum ScriptPurpose {
Minting(PolicyId),
Spending(TransactionInput),
Rewarding(StakeCredential),
Certifying(Certificate),
}
#[derive(Debug, PartialEq, Clone)]
pub struct TxInfoV1 {
pub inputs: Vec<TxInInfo>,
pub outputs: Vec<TxOut>,
pub fee: Value,
pub mint: MintValue,
pub dcert: Vec<Certificate>,
pub wdrl: Vec<(RewardAccount, Coin)>,
pub valid_range: TimeRange,
pub signatories: Vec<AddrKeyhash>,
pub data: Vec<(DatumHash, PlutusData)>,
pub id: Hash<32>,
}
#[derive(Debug, PartialEq, Clone)]
pub struct TxInfoV2 {
pub inputs: Vec<TxInInfo>,
pub reference_inputs: Vec<TxInInfo>,
pub outputs: Vec<TxOut>,
pub fee: Value,
pub mint: MintValue,
pub dcert: Vec<Certificate>,
pub wdrl: Withdrawals,
pub valid_range: TimeRange,
pub signatories: Vec<AddrKeyhash>,
pub redeemers: KeyValuePairs<ScriptPurpose, Redeemer>,
pub data: KeyValuePairs<DatumHash, PlutusData>,
pub id: Hash<32>,
}
#[derive(Debug, PartialEq, Clone)]
pub enum TxInfo {
V1(TxInfoV1),
V2(TxInfoV2),
}
#[derive(Debug, PartialEq, Clone)]
pub struct ScriptContext {
pub tx_info: TxInfo,
pub purpose: ScriptPurpose,
}
//---- Time conversion: slot range => posix time range
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct TimeRange {
pub lower_bound: Option<u64>,
pub upper_bound: Option<u64>,
}
pub struct SlotConfig {
pub slot_length: u64,
pub zero_slot: u64,
pub zero_time: u64,
}
impl Default for SlotConfig {
fn default() -> Self {
Self {
slot_length: 1000,
zero_slot: 4492800,
zero_time: 1596059091000,
}
}
}

1719
crates/uplc/src/tx/tests.rs Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,611 @@
use pallas_addresses::{Address, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_codec::utils::{AnyUInt, Bytes, Int, KeyValuePairs};
use pallas_crypto::hash::Hash;
use pallas_primitives::babbage::{AssetName, BigInt, Constr, Mint, PlutusData, ScriptRef};
use pallas_primitives::babbage::{
Certificate, DatumOption, Redeemer, Script, StakeCredential, TransactionInput,
TransactionOutput, Value,
};
use pallas_traverse::ComputeHash;
use super::script_context::{ScriptContext, ScriptPurpose, TimeRange, TxInInfo, TxInfo, TxOut};
fn wrap_with_constr(index: u64, data: PlutusData) -> PlutusData {
PlutusData::Constr(Constr {
tag: constr_index(index),
any_constructor: None,
fields: vec![data],
})
}
fn wrap_multiple_with_constr(index: u64, data: Vec<PlutusData>) -> PlutusData {
PlutusData::Constr(Constr {
tag: constr_index(index),
any_constructor: None,
fields: data,
})
}
fn empty_constr(index: u64) -> PlutusData {
PlutusData::Constr(Constr {
tag: constr_index(index),
any_constructor: None,
fields: vec![],
})
}
/// Translate constructor index to cbor tag.
fn constr_index(index: u64) -> u64 {
121 + index
}
pub trait ToPlutusData {
fn to_plutus_data(&self) -> PlutusData;
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct MintValue {
pub mint_value: Mint,
}
impl ToPlutusData for Address {
fn to_plutus_data(&self) -> PlutusData {
match self {
Address::Shelley(shelley_address) => {
let payment_part = shelley_address.payment();
let stake_part = shelley_address.delegation();
let payment_part_plutus_data = match payment_part {
ShelleyPaymentPart::Key(payment_keyhash) => {
wrap_with_constr(0, payment_keyhash.to_plutus_data())
}
ShelleyPaymentPart::Script(script_hash) => {
wrap_with_constr(1, script_hash.to_plutus_data())
}
};
let stake_part_plutus_data = match stake_part {
ShelleyDelegationPart::Key(stake_keyhash) => {
Some(StakeCredential::AddrKeyhash(*stake_keyhash)).to_plutus_data()
}
ShelleyDelegationPart::Script(script_hash) => {
Some(StakeCredential::Scripthash(*script_hash)).to_plutus_data()
}
ShelleyDelegationPart::Pointer(pointer) => Some(wrap_multiple_with_constr(
1,
vec![
pointer.slot().to_plutus_data(),
pointer.tx_idx().to_plutus_data(),
pointer.cert_idx().to_plutus_data(),
],
))
.to_plutus_data(),
ShelleyDelegationPart::Null => None::<StakeCredential>.to_plutus_data(),
};
wrap_multiple_with_constr(0, vec![payment_part_plutus_data, stake_part_plutus_data])
}
_ => unreachable!(),
}
}
}
impl ToPlutusData for TransactionInput {
fn to_plutus_data(&self) -> PlutusData {
wrap_multiple_with_constr(
0,
vec![
wrap_with_constr(0, self.transaction_id.to_plutus_data()),
PlutusData::BigInt(BigInt::Int((self.index as i64).into())),
],
)
}
}
impl<const BYTES: usize> ToPlutusData for Hash<BYTES> {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BoundedBytes(self.to_vec().into())
}
}
impl ToPlutusData for Bytes {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BoundedBytes(self.clone())
}
}
impl<K: ToPlutusData, V: ToPlutusData> ToPlutusData for (K, V) {
fn to_plutus_data(&self) -> PlutusData {
wrap_multiple_with_constr(0, vec![self.0.to_plutus_data(), self.1.to_plutus_data()])
}
}
impl<A> ToPlutusData for Vec<A>
where
A: ToPlutusData,
{
fn to_plutus_data(&self) -> PlutusData {
PlutusData::Array(self.iter().map(|p| p.to_plutus_data()).collect())
}
}
impl<K, V> ToPlutusData for KeyValuePairs<K, V>
where
K: ToPlutusData + Clone,
V: ToPlutusData + Clone,
{
fn to_plutus_data(&self) -> PlutusData {
let mut data_vec: Vec<(PlutusData, PlutusData)> = vec![];
for (key, value) in self.iter() {
data_vec.push((key.to_plutus_data(), value.to_plutus_data()))
}
PlutusData::Map(KeyValuePairs::Def(data_vec))
}
}
impl<A: ToPlutusData> ToPlutusData for Option<A> {
fn to_plutus_data(&self) -> PlutusData {
match self {
None => empty_constr(1),
Some(data) => wrap_with_constr(0, data.to_plutus_data()),
}
}
}
// Does this here surely overwrite Option from above for DatumOption?
impl ToPlutusData for Option<DatumOption> {
// NoOutputDatum = 0 | OutputDatumHash = 1 | OutputDatum = 2
fn to_plutus_data(&self) -> PlutusData {
match self {
None => empty_constr(0),
Some(option) => match option {
DatumOption::Hash(hash) => wrap_with_constr(1, hash.to_plutus_data()),
DatumOption::Data(data) => wrap_with_constr(2, data.0.clone()),
},
}
}
}
impl ToPlutusData for AnyUInt {
fn to_plutus_data(&self) -> PlutusData {
match self {
AnyUInt::U8(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))),
AnyUInt::U16(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))),
AnyUInt::U32(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))),
AnyUInt::U64(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))),
AnyUInt::MajorByte(n) => PlutusData::BigInt(BigInt::Int(Int::from(*n as i64))), // is this correct? I don't know exactly what is does
}
}
}
impl ToPlutusData for Int {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BigInt(BigInt::Int(*self))
}
}
impl ToPlutusData for BigInt {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BigInt(self.clone())
}
}
impl ToPlutusData for i64 {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BigInt(BigInt::Int(Int::from(*self)))
}
}
impl ToPlutusData for u64 {
fn to_plutus_data(&self) -> PlutusData {
PlutusData::BigInt(BigInt::Int(Int::from(*self as i64)))
}
}
impl ToPlutusData for Value {
fn to_plutus_data(&self) -> PlutusData {
match self {
Value::Coin(coin) => PlutusData::Map(KeyValuePairs::Def(vec![(
Bytes::from(vec![]).to_plutus_data(),
PlutusData::Map(KeyValuePairs::Def(vec![(
AssetName::from(vec![]).to_plutus_data(),
coin.to_plutus_data(),
)])),
)])),
Value::Multiasset(coin, multiassets) => {
let mut data_vec: Vec<(PlutusData, PlutusData)> = vec![(
Bytes::from(vec![]).to_plutus_data(),
PlutusData::Map(KeyValuePairs::Def(vec![(
AssetName::from(vec![]).to_plutus_data(),
coin.to_plutus_data(),
)])),
)];
for (policy_id, assets) in multiassets.iter() {
let mut assets_vec = vec![];
for (asset, amount) in assets.iter() {
assets_vec.push((asset.to_plutus_data(), amount.to_plutus_data()));
}
data_vec.push((
policy_id.to_plutus_data(),
PlutusData::Map(KeyValuePairs::Def(assets_vec)),
));
}
PlutusData::Map(KeyValuePairs::Def(data_vec))
}
}
}
}
impl ToPlutusData for MintValue {
fn to_plutus_data(&self) -> PlutusData {
let mut data_vec: Vec<(PlutusData, PlutusData)> = vec![(
Bytes::from(vec![]).to_plutus_data(),
PlutusData::Map(KeyValuePairs::Def(vec![(
AssetName::from(vec![]).to_plutus_data(),
0_i64.to_plutus_data(),
)])),
)];
for (policy_id, assets) in self.mint_value.iter() {
let mut assets_vec = vec![];
for (asset, amount) in assets.iter() {
assets_vec.push((asset.to_plutus_data(), amount.to_plutus_data()));
}
data_vec.push((
policy_id.to_plutus_data(),
PlutusData::Map(KeyValuePairs::Def(assets_vec)),
));
}
PlutusData::Map(KeyValuePairs::Def(data_vec))
}
}
impl ToPlutusData for ScriptRef {
fn to_plutus_data(&self) -> PlutusData {
match &self.0 {
Script::NativeScript(native_script) => native_script.compute_hash().to_plutus_data(),
Script::PlutusV1Script(plutus_v1) => plutus_v1.compute_hash().to_plutus_data(),
Script::PlutusV2Script(plutus_v2) => plutus_v2.compute_hash().to_plutus_data(),
}
}
}
impl ToPlutusData for TxOut {
fn to_plutus_data(&self) -> PlutusData {
match self {
TxOut::V1(output) => match output {
TransactionOutput::Legacy(legacy_output) => wrap_multiple_with_constr(
0,
vec![
Address::from_bytes(&legacy_output.address)
.unwrap()
.to_plutus_data(),
legacy_output.amount.to_plutus_data(),
legacy_output.datum_hash.to_plutus_data(),
],
),
TransactionOutput::PostAlonzo(post_alonzo_output) => wrap_multiple_with_constr(
0,
vec![
Address::from_bytes(&post_alonzo_output.address)
.unwrap()
.to_plutus_data(),
post_alonzo_output.value.to_plutus_data(),
match post_alonzo_output.datum_option {
Some(DatumOption::Hash(hash)) => Some(hash).to_plutus_data(),
_ => None::<DatumOption>.to_plutus_data(),
},
],
),
},
TxOut::V2(output) => match output {
TransactionOutput::Legacy(legacy_output) => wrap_multiple_with_constr(
0,
vec![
Address::from_bytes(&legacy_output.address)
.unwrap()
.to_plutus_data(),
legacy_output.amount.to_plutus_data(),
match legacy_output.datum_hash {
Some(hash) => wrap_with_constr(1, hash.to_plutus_data()),
_ => empty_constr(0),
},
None::<ScriptRef>.to_plutus_data(),
],
),
TransactionOutput::PostAlonzo(post_alonzo_output) => wrap_multiple_with_constr(
0,
vec![
Address::from_bytes(&post_alonzo_output.address)
.unwrap()
.to_plutus_data(),
post_alonzo_output.value.to_plutus_data(),
post_alonzo_output.datum_option.to_plutus_data(),
post_alonzo_output.script_ref.to_plutus_data(),
],
),
},
}
}
}
impl ToPlutusData for StakeCredential {
// Stake Credential needs to be wrapped inside another Constr, because we could have either a StakingHash or a StakingPtr
// The current implementation of StakeCredential doesn't capture the credential of a Pointer address.
// So a StakeCredential for a Pointer address needs to be converted separately
fn to_plutus_data(&self) -> PlutusData {
match self {
StakeCredential::AddrKeyhash(addr_keyhas) => {
wrap_with_constr(0, wrap_with_constr(0, addr_keyhas.to_plutus_data()))
}
StakeCredential::Scripthash(script_hash) => {
wrap_with_constr(0, wrap_with_constr(1, script_hash.to_plutus_data()))
}
}
}
}
impl ToPlutusData for Certificate {
fn to_plutus_data(&self) -> PlutusData {
match self {
Certificate::StakeRegistration(stake_credential) => {
wrap_with_constr(0, stake_credential.to_plutus_data())
}
Certificate::StakeDeregistration(stake_credential) => {
wrap_with_constr(1, stake_credential.to_plutus_data())
}
Certificate::StakeDelegation(stake_credential, pool_keyhash) => {
wrap_multiple_with_constr(
2,
vec![
stake_credential.to_plutus_data(),
pool_keyhash.to_plutus_data(),
],
)
}
Certificate::PoolRegistration {
operator,
vrf_keyhash,
pledge: _,
cost: _,
margin: _,
reward_account: _,
pool_owners: _,
relays: _,
pool_metadata: _,
} => wrap_multiple_with_constr(
3,
vec![operator.to_plutus_data(), vrf_keyhash.to_plutus_data()],
),
Certificate::PoolRetirement(pool_keyhash, epoch) => wrap_multiple_with_constr(
4,
vec![pool_keyhash.to_plutus_data(), epoch.to_plutus_data()],
),
Certificate::GenesisKeyDelegation(_, _, _) => empty_constr(5),
Certificate::MoveInstantaneousRewardsCert(_) => empty_constr(6),
}
}
}
impl ToPlutusData for Redeemer {
fn to_plutus_data(&self) -> PlutusData {
self.data.clone()
}
}
impl ToPlutusData for PlutusData {
fn to_plutus_data(&self) -> PlutusData {
self.clone()
}
}
impl ToPlutusData for TimeRange {
fn to_plutus_data(&self) -> PlutusData {
match &self {
TimeRange {
lower_bound: Some(lower_bound),
upper_bound: None,
} => {
wrap_multiple_with_constr(
0,
vec![
// LowerBound
wrap_multiple_with_constr(
0,
vec![
// Finite
wrap_with_constr(1, lower_bound.to_plutus_data()),
// Closure
true.to_plutus_data(),
],
), //UpperBound
wrap_multiple_with_constr(
0,
vec![
// PosInf
empty_constr(2),
// Closure
true.to_plutus_data(),
],
),
],
)
}
TimeRange {
lower_bound: None,
upper_bound: Some(upper_bound),
} => {
wrap_multiple_with_constr(
0,
vec![
// LowerBound
wrap_multiple_with_constr(
0,
vec![
// NegInf
empty_constr(0),
// Closure
true.to_plutus_data(),
],
),
//UpperBound
wrap_multiple_with_constr(
0,
vec![
// Finite
wrap_with_constr(1, upper_bound.to_plutus_data()),
// Closure
true.to_plutus_data(),
],
),
],
)
}
TimeRange {
lower_bound: Some(lower_bound),
upper_bound: Some(upper_bound),
} => {
wrap_multiple_with_constr(
0,
vec![
// LowerBound
wrap_multiple_with_constr(
0,
vec![
// Finite
wrap_with_constr(1, lower_bound.to_plutus_data()),
// Closure
true.to_plutus_data(),
],
),
//UpperBound
wrap_multiple_with_constr(
0,
vec![
// Finite
wrap_with_constr(1, upper_bound.to_plutus_data()),
// Closure
false.to_plutus_data(),
],
),
],
)
}
TimeRange {
lower_bound: None,
upper_bound: None,
} => {
wrap_multiple_with_constr(
0,
vec![
// LowerBound
wrap_multiple_with_constr(
0,
vec![
// NegInf
empty_constr(0),
// Closure
true.to_plutus_data(),
],
),
//UpperBound
wrap_multiple_with_constr(
0,
vec![
// PosInf
empty_constr(2),
// Closure
true.to_plutus_data(),
],
),
],
)
}
}
}
}
impl ToPlutusData for TxInInfo {
fn to_plutus_data(&self) -> PlutusData {
wrap_multiple_with_constr(
0,
vec![
self.out_ref.to_plutus_data(),
self.resolved.to_plutus_data(),
],
)
}
}
impl ToPlutusData for ScriptPurpose {
fn to_plutus_data(&self) -> PlutusData {
match self {
ScriptPurpose::Minting(policy_id) => wrap_with_constr(0, policy_id.to_plutus_data()),
ScriptPurpose::Spending(out_ref) => wrap_with_constr(1, out_ref.to_plutus_data()),
ScriptPurpose::Rewarding(stake_credential) => {
wrap_with_constr(2, stake_credential.to_plutus_data())
}
ScriptPurpose::Certifying(dcert) => wrap_with_constr(3, dcert.to_plutus_data()),
}
}
}
impl ToPlutusData for TxInfo {
fn to_plutus_data(&self) -> PlutusData {
match self {
TxInfo::V1(tx_info) => wrap_multiple_with_constr(
0,
vec![
tx_info.inputs.to_plutus_data(),
tx_info.outputs.to_plutus_data(),
tx_info.fee.to_plutus_data(),
tx_info.mint.to_plutus_data(),
tx_info.dcert.to_plutus_data(),
tx_info.wdrl.to_plutus_data(),
tx_info.valid_range.to_plutus_data(),
tx_info.signatories.to_plutus_data(),
tx_info.data.to_plutus_data(),
wrap_with_constr(0, tx_info.id.to_plutus_data()),
],
),
TxInfo::V2(tx_info) => wrap_multiple_with_constr(
0,
vec![
tx_info.inputs.to_plutus_data(),
tx_info.reference_inputs.to_plutus_data(),
tx_info.outputs.to_plutus_data(),
tx_info.fee.to_plutus_data(),
tx_info.mint.to_plutus_data(),
tx_info.dcert.to_plutus_data(),
tx_info.wdrl.to_plutus_data(),
tx_info.valid_range.to_plutus_data(),
tx_info.signatories.to_plutus_data(),
tx_info.redeemers.to_plutus_data(),
tx_info.data.to_plutus_data(),
wrap_with_constr(0, tx_info.id.to_plutus_data()),
],
),
}
}
}
impl ToPlutusData for ScriptContext {
fn to_plutus_data(&self) -> PlutusData {
wrap_multiple_with_constr(
0,
vec![self.tx_info.to_plutus_data(), self.purpose.to_plutus_data()],
)
}
}
impl ToPlutusData for bool {
fn to_plutus_data(&self) -> PlutusData {
match self {
false => empty_constr(0),
true => empty_constr(1),
}
}
}