From 508d88035b55731d5fb4590f618a6ddc04e6dbbe Mon Sep 17 00:00:00 2001 From: KtorZ Date: Thu, 8 Aug 2024 18:17:38 +0200 Subject: [PATCH] Fix Plutus v3 validator hash calculation in blueprint. --- crates/aiken-project/src/blueprint/mod.rs | 5 +- .../aiken-project/src/blueprint/validator.rs | 20 +++- crates/aiken-project/src/export.rs | 27 +++-- crates/aiken-project/src/lib.rs | 26 ++--- crates/aiken/src/cmd/blueprint/convert.rs | 2 +- crates/uplc/src/ast.rs | 107 ++++++++++++++++-- 6 files changed, 144 insertions(+), 43 deletions(-) diff --git a/crates/aiken-project/src/blueprint/mod.rs b/crates/aiken-project/src/blueprint/mod.rs index 9ff3daff..86369656 100644 --- a/crates/aiken-project/src/blueprint/mod.rs +++ b/crates/aiken-project/src/blueprint/mod.rs @@ -5,14 +5,13 @@ pub mod parameter; pub mod schema; pub mod validator; -pub use error::Error; - use crate::{ config::{self, Config, PlutusVersion}, module::CheckedModules, }; use aiken_lang::gen_uplc::CodeGenerator; use definitions::Definitions; +pub use error::Error; use schema::{Annotated, Schema}; use std::fmt::Debug; use validator::Validator; @@ -70,7 +69,7 @@ impl Blueprint { let validators: Result, Error> = modules .validators() .flat_map(|(validator, def)| { - Validator::from_checked_module(modules, generator, validator, def) + Validator::from_checked_module(modules, generator, validator, def, &config.plutus) .into_iter() .map(|result| { result.map(|mut schema| { diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index 292728d5..61db8c5f 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -9,13 +9,14 @@ use crate::module::{CheckedModule, CheckedModules}; use aiken_lang::{ ast::{Annotation, TypedArg, TypedFunction, TypedValidator}, gen_uplc::CodeGenerator, + plutus_version::PlutusVersion, tipo::Type, }; use miette::NamedSource; use serde; use std::borrow::Borrow; use uplc::{ - ast::{Constant, DeBruijn, Program}, + ast::{Constant, SerializableProgram}, PlutusData, }; @@ -36,7 +37,7 @@ pub struct Validator { pub parameters: Vec, #[serde(flatten)] - pub program: Program, + pub program: SerializableProgram, #[serde(skip_serializing_if = "Definitions::is_empty")] #[serde(default)] @@ -49,6 +50,7 @@ impl Validator { generator: &mut CodeGenerator, module: &CheckedModule, def: &TypedValidator, + plutus_version: &PlutusVersion, ) -> Vec> { let is_multi_validator = def.other_fun.is_some(); @@ -62,6 +64,7 @@ impl Validator { &def.fun, is_multi_validator, &mut program, + plutus_version, )]; if let Some(ref other_func) = def.other_fun { @@ -73,12 +76,14 @@ impl Validator { other_func, is_multi_validator, &mut program, + plutus_version, )); } validators } + #[allow(clippy::too_many_arguments)] fn create_validator_blueprint( generator: &mut CodeGenerator, modules: &CheckedModules, @@ -87,6 +92,7 @@ impl Validator { func: &TypedFunction, is_multi_validator: bool, program: &mut MemoProgram, + plutus_version: &PlutusVersion, ) -> Result { let mut args = func.arguments.iter().rev(); let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next()); @@ -168,7 +174,11 @@ impl Validator { parameters, datum, redeemer, - program: program.get(generator, def, &module.name), + program: match plutus_version { + PlutusVersion::V1 => SerializableProgram::PlutusV1Program, + PlutusVersion::V2 => SerializableProgram::PlutusV2Program, + PlutusVersion::V3 => SerializableProgram::PlutusV3Program, + }(program.get(generator, def, &module.name)), definitions, }) } @@ -208,7 +218,7 @@ impl Validator { Some((head, tail)) => { head.validate(definitions, &Constant::Data(arg.clone()))?; Ok(Self { - program: self.program.apply_data(arg.clone()), + program: self.program.map(|program| program.apply_data(arg.clone())), parameters: tail.to_vec(), ..self }) @@ -284,7 +294,7 @@ mod tests { .next() .expect("source code did no yield any validator"); - let validators = Validator::from_checked_module(&modules, &mut generator, validator, def); + let validators = Validator::from_checked_module(&modules, &mut generator, validator, def, &PlutusVersion::default()); if validators.len() > 1 { panic!("Multi-validator given to test bench. Don't do that.") diff --git a/crates/aiken-project/src/export.rs b/crates/aiken-project/src/export.rs index f364348d..99d3b3f2 100644 --- a/crates/aiken-project/src/export.rs +++ b/crates/aiken-project/src/export.rs @@ -1,7 +1,3 @@ -use aiken_lang::{ast::TypedFunction, gen_uplc::CodeGenerator}; -use miette::NamedSource; -use uplc::ast::{DeBruijn, Program}; - use crate::{ blueprint::{ self, @@ -11,6 +7,9 @@ use crate::{ }, module::{CheckedModule, CheckedModules}, }; +use aiken_lang::{ast::TypedFunction, gen_uplc::CodeGenerator, plutus_version::PlutusVersion}; +use miette::NamedSource; +use uplc::ast::SerializableProgram; #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] pub struct Export { @@ -24,7 +23,7 @@ pub struct Export { pub parameters: Vec, #[serde(flatten)] - pub program: Program, + pub program: SerializableProgram, #[serde(skip_serializing_if = "Definitions::is_empty")] #[serde(default)] @@ -37,6 +36,7 @@ impl Export { module: &CheckedModule, generator: &mut CodeGenerator, modules: &CheckedModules, + plutus_version: &PlutusVersion, ) -> Result { let mut definitions = Definitions::new(); @@ -64,10 +64,16 @@ impl Export { }) .collect::>()?; - let program = generator - .generate_raw(&func.body, &func.arguments, &module.name) - .to_debruijn() - .unwrap(); + let program = match plutus_version { + PlutusVersion::V1 => SerializableProgram::PlutusV1Program, + PlutusVersion::V2 => SerializableProgram::PlutusV2Program, + PlutusVersion::V3 => SerializableProgram::PlutusV3Program, + }( + generator + .generate_raw(&func.body, &func.arguments, &module.name) + .to_debruijn() + .unwrap(), + ); Ok(Export { name: format!("{}.{}", &module.name, &func.name), @@ -86,6 +92,7 @@ mod tests { use aiken_lang::{ self, ast::{TraceLevel, Tracing}, + plutus_version::PlutusVersion, }; macro_rules! assert_export { @@ -103,7 +110,7 @@ mod tests { .next() .expect("source code did no yield any exports"); - let export = Export::from_function(func, module, &mut generator, &modules); + let export = Export::from_function(func, module, &mut generator, &modules, &PlutusVersion::default()); match export { Err(e) => insta::with_settings!({ diff --git a/crates/aiken-project/src/lib.rs b/crates/aiken-project/src/lib.rs index 83b073dc..2c2df06d 100644 --- a/crates/aiken-project/src/lib.rs +++ b/crates/aiken-project/src/lib.rs @@ -40,7 +40,6 @@ use aiken_lang::{ format::{Formatter, MAX_COLUMNS}, gen_uplc::CodeGenerator, line_numbers::LineNumbers, - plutus_version::PlutusVersion, tipo::{Type, TypeInfo}, IdGenerator, }; @@ -50,8 +49,7 @@ use miette::NamedSource; use options::{CodeGenMode, Options}; use package_name::PackageName; use pallas_addresses::{Address, Network, ShelleyAddress, ShelleyDelegationPart, StakePayload}; -use pallas_primitives::conway::{self as cardano, PolicyId}; -use pallas_traverse::ComputeHash; +use pallas_primitives::conway::PolicyId; use std::{ collections::{BTreeSet, HashMap}, fs::{self, File}, @@ -298,7 +296,7 @@ where let path = dir.clone().join(format!("{}.uplc", validator.title)); let program = &validator.program; - let program: Program = program.try_into().unwrap(); + let program: Program = program.inner().try_into().unwrap(); fs::write(&path, program.to_pretty()).map_err(|error| Error::FileIo { error, path })?; } @@ -490,7 +488,7 @@ where Network::Testnet }; - Ok(validator.program.address( + Ok(validator.program.inner().address( network, delegation_part.to_owned(), &self.config.plutus.into(), @@ -520,15 +518,7 @@ where if n > 0 { Err(blueprint::error::Error::ParameterizedValidator { n }.into()) } else { - let cbor = validator.program.to_cbor().unwrap(); - - let validator_hash = match self.config.plutus { - PlutusVersion::V1 => cardano::PlutusV1Script(cbor.into()).compute_hash(), - PlutusVersion::V2 => cardano::PlutusV2Script(cbor.into()).compute_hash(), - PlutusVersion::V3 => cardano::PlutusV3Script(cbor.into()).compute_hash(), - }; - - Ok(validator_hash) + Ok(validator.program.compiled_code_and_hash().1) } }) } @@ -552,7 +542,13 @@ where .map(|(checked_module, func)| { let mut generator = self.new_generator(tracing); - Export::from_function(func, checked_module, &mut generator, &self.checked_modules) + Export::from_function( + func, + checked_module, + &mut generator, + &self.checked_modules, + &self.config.plutus, + ) }) .transpose()? .ok_or_else(|| Error::ExportNotFound { diff --git a/crates/aiken/src/cmd/blueprint/convert.rs b/crates/aiken/src/cmd/blueprint/convert.rs index 60e70f80..f5512b3a 100644 --- a/crates/aiken/src/cmd/blueprint/convert.rs +++ b/crates/aiken/src/cmd/blueprint/convert.rs @@ -83,7 +83,7 @@ pub fn exec( let result = blueprint.with_validator(title, when_too_many, when_missing, |validator| match to { Format::CardanoCli => { - let cbor_bytes = validator.program.to_cbor().unwrap(); + let cbor_bytes = validator.program.inner().to_cbor().unwrap(); let mut double_cbor_bytes = Vec::new(); diff --git a/crates/uplc/src/ast.rs b/crates/uplc/src/ast.rs index c5db115a..c1359ece 100644 --- a/crates/uplc/src/ast.rs +++ b/crates/uplc/src/ast.rs @@ -111,38 +111,99 @@ where } } -impl Serialize for Program { +#[derive(Debug, Clone, PartialEq)] +pub enum SerializableProgram { + PlutusV1Program(Program), + PlutusV2Program(Program), + PlutusV3Program(Program), +} + +impl SerializableProgram { + pub fn inner(&self) -> &Program { + use SerializableProgram::*; + + match self { + PlutusV1Program(program) => program, + PlutusV2Program(program) => program, + PlutusV3Program(program) => program, + } + } + + pub fn map(self, f: F) -> Self + where + F: FnOnce(Program) -> Program, + { + use SerializableProgram::*; + + match self { + PlutusV1Program(program) => PlutusV1Program(f(program)), + PlutusV2Program(program) => PlutusV2Program(f(program)), + PlutusV3Program(program) => PlutusV3Program(f(program)), + } + } + + pub fn compiled_code_and_hash(&self) -> (String, pallas_crypto::hash::Hash<28>) { + use SerializableProgram::*; + + match self { + PlutusV1Program(pgrm) => { + let cbor = pgrm.to_cbor().unwrap(); + let compiled_code = hex::encode(&cbor); + let hash = conway::PlutusV1Script(cbor.into()).compute_hash(); + (compiled_code, hash) + } + + PlutusV2Program(pgrm) => { + let cbor = pgrm.to_cbor().unwrap(); + let compiled_code = hex::encode(&cbor); + let hash = conway::PlutusV2Script(cbor.into()).compute_hash(); + (compiled_code, hash) + } + + PlutusV3Program(pgrm) => { + let cbor = pgrm.to_cbor().unwrap(); + let compiled_code = hex::encode(&cbor); + let hash = conway::PlutusV3Script(cbor.into()).compute_hash(); + (compiled_code, hash) + } + } + } +} + +impl Serialize for SerializableProgram { fn serialize(&self, serializer: S) -> Result { - let cbor = self.to_cbor().unwrap(); + let (compiled_code, hash) = self.compiled_code_and_hash(); let mut s = serializer.serialize_struct("Program", 2)?; - s.serialize_field("compiledCode", &hex::encode(&cbor))?; - s.serialize_field("hash", &conway::PlutusV2Script(cbor.into()).compute_hash())?; + s.serialize_field("compiledCode", &compiled_code)?; + s.serialize_field("hash", &hash)?; s.end() } } -impl<'a> Deserialize<'a> for Program { +impl<'a> Deserialize<'a> for SerializableProgram { fn deserialize>(deserializer: D) -> Result { #[derive(serde::Deserialize)] #[serde(field_identifier, rename_all = "camelCase")] enum Fields { CompiledCode, + Hash, } struct ProgramVisitor; impl<'a> Visitor<'a> for ProgramVisitor { - type Value = Program; + type Value = SerializableProgram; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("Program") + formatter.write_str("validator") } - fn visit_map(self, mut map: V) -> Result, V::Error> + fn visit_map(self, mut map: V) -> Result where V: MapAccess<'a>, { let mut compiled_code: Option = None; + let mut hash: Option = None; while let Some(key) = map.next_key()? { match key { Fields::CompiledCode => { @@ -151,11 +212,20 @@ impl<'a> Deserialize<'a> for Program { } compiled_code = Some(map.next_value()?); } + + Fields::Hash => { + if hash.is_some() { + return Err(de::Error::duplicate_field("hash")); + } + hash = Some(map.next_value()?); + } } } let compiled_code = compiled_code.ok_or_else(|| de::Error::missing_field("compiledCode"))?; + let hash = hash.ok_or_else(|| de::Error::missing_field("hash"))?; + let mut cbor_buffer = Vec::new(); let mut flat_buffer = Vec::new(); @@ -166,10 +236,29 @@ impl<'a> Deserialize<'a> for Program { &"a base16-encoded CBOR-serialized UPLC program", ) }) + .and_then(|program| { + let cbor = || program.to_cbor().unwrap().into(); + + if conway::PlutusV3Script(cbor()).compute_hash().to_string() == hash { + return Ok(SerializableProgram::PlutusV3Program(program)); + } + + if conway::PlutusV2Script(cbor()).compute_hash().to_string() == hash { + return Ok(SerializableProgram::PlutusV2Program(program)); + } + + if conway::PlutusV1Script(cbor()).compute_hash().to_string() == hash { + return Ok(SerializableProgram::PlutusV1Program(program)); + } + + Err(de::Error::custom( + "hash doesn't match any recognisable Plutus version.", + )) + }) } } - const FIELDS: &[&str] = &["compiledCode"]; + const FIELDS: &[&str] = &["compiledCode", "hash"]; deserializer.deserialize_struct("Program", FIELDS, ProgramVisitor) } }