From 84c4ccaf4cc04be551c8e51a7f574eb01fa3c98f Mon Sep 17 00:00:00 2001 From: KtorZ Date: Sun, 3 Mar 2024 13:37:02 +0100 Subject: [PATCH] Forbid opaque types in the application binary interface. We cannot enforce internal invariants on opaque types from only structural checks on Data. Thus, it is forbidden to find an opaque type in an outward-facing interface. Instead, users should rely on intermediate representations and lift them into opaque types using constructors and methods provided by the type (e.g. Dict.from_list, Rational.from_int, Rational.new, ...) --- crates/aiken-project/src/blueprint/schema.rs | 75 ++++++++++--------- ...ests__opaque_singleton_multi_variants.snap | 22 ++++++ ...tor__tests__opaque_singleton_variants.snap | 68 ++++++++++------- .../aiken-project/src/blueprint/validator.rs | 64 ++++++++++------ 4 files changed, 147 insertions(+), 82 deletions(-) create mode 100644 crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap diff --git a/crates/aiken-project/src/blueprint/schema.rs b/crates/aiken-project/src/blueprint/schema.rs index f07e02e7..dc40915c 100644 --- a/crates/aiken-project/src/blueprint/schema.rs +++ b/crates/aiken-project/src/blueprint/schema.rs @@ -1,5 +1,7 @@ -use crate::blueprint::definitions::{Definitions, Reference}; -use crate::CheckedModule; +use crate::{ + blueprint::definitions::{Definitions, Reference}, + CheckedModule, +}; use aiken_lang::{ ast::{Definition, TypedDataType, TypedDefinition}, builtins::wrapped_redeemer, @@ -12,8 +14,7 @@ use serde::{ ser::{Serialize, SerializeStruct, Serializer}, }; use serde_json as json; -use std::rc::Rc; -use std::{collections::HashMap, fmt, ops::Deref}; +use std::{collections::HashMap, fmt, ops::Deref, rc::Rc}; // NOTE: Can be anything BUT 0 pub const REDEEMER_DISCRIMINANT: usize = 1; @@ -385,21 +386,6 @@ impl Annotated { Type::Fn { .. } => unreachable!(), } } - - fn into_data(self, type_info: &Type) -> Result, Error> { - match self { - Annotated { - title, - description, - annotated: Schema::Data(data), - } => Ok(Annotated { - title, - description, - annotated: data, - }), - _ => Err(Error::new(ErrorContext::ExpectedData, type_info)), - } - } } impl Data { @@ -409,28 +395,24 @@ impl Data { type_parameters: &mut HashMap>, definitions: &mut Definitions>, ) -> Result { + if data_type.opaque { + // NOTE: No breadcrumbs here which is *okay*, as the caller will backtrack + // and add the necessary type information. + return Err(Error { + context: ErrorContext::IllegalOpaqueType, + breadcrumbs: vec![], + }); + } + let mut variants = vec![]; - let len_constructors = data_type.constructors.len(); for (index, constructor) in data_type.constructors.iter().enumerate() { let mut fields = vec![]; - let len_arguments = data_type.constructors.len(); for field in constructor.arguments.iter() { let reference = Annotated::do_from_type(&field.tipo, modules, type_parameters, definitions)?; - // NOTE: Opaque data-types with a single variant and a single field are transparent, they - // are erased completely at compilation time. - if data_type.opaque && len_constructors == 1 && len_arguments == 1 { - let schema = definitions - .lookup(&reference) - .expect("Schema definition registered just above") - .clone(); - definitions.remove(&reference); - return Ok(schema.into_data(&field.tipo)?.annotated); - } - fields.push(Annotated { title: field.label.clone(), description: field.doc.clone().map(|s| s.trim().to_string()), @@ -479,7 +461,7 @@ fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option { - return Some(data_type.clone()) + return Some(data_type.clone()); } Definition::Fn { .. } | Definition::Validator { .. } @@ -928,7 +910,9 @@ pub struct Error { #[derive(Debug, PartialEq, Clone, thiserror::Error)] pub enum ErrorContext { - #[error("I failed at my own job and couldn't figure out how to generate a specification for a type.")] + #[error( + "I failed at my own job and couldn't figure out how to generate a specification for a type." + )] UnsupportedType, #[error("I discovered a type hole where I would expect a concrete type.")] @@ -942,6 +926,9 @@ pub enum ErrorContext { #[error("I figured you tried to export a function in your contract's binary interface.")] UnexpectedFunction, + + #[error("I caught an opaque type trying to escape")] + IllegalOpaqueType, } impl Error { @@ -963,6 +950,26 @@ impl Error { pub fn help(&self) -> String { match self.context { + ErrorContext::IllegalOpaqueType => format!( + r#"Opaque types cannot figure anywhere in an outward-facing type like a validator's redeemer or datum. This is because an {opaque} type hides its implementation details, and likely enforce invariants that cannot be expressed only structurally. In particular, the {opaque} type {signature} cannot be safely constructed from any Plutus Data. + +Hence, {opaque} types are forbidden from interface points with the off-chain world. Instead, use an intermediate representation and construct the {opaque} type at runtime using constructors and methods provided for that type (e.g. {Dict}.{from_list}, {Rational}.{new}, ...)."#, + opaque = "opaque".if_supports_color(Stdout, |s| s.purple()), + signature = Error::fmt_breadcrumbs(&[self + .breadcrumbs + .last() + .expect("always at least one breadcrumb") + .to_owned()]), + Dict = "Dict" + .if_supports_color(Stdout, |s| s.bright_blue()) + .if_supports_color(Stdout, |s| s.bold()), + from_list = "from_list".if_supports_color(Stdout, |s| s.blue()), + Rational = "Rational" + .if_supports_color(Stdout, |s| s.bright_blue()) + .if_supports_color(Stdout, |s| s.bold()), + new = "new".if_supports_color(Stdout, |s| s.blue()), + ), + ErrorContext::UnsupportedType => format!( r#"I do not know how to generate a portable Plutus specification for the following type: diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap new file mode 100644 index 00000000..e397e816 --- /dev/null +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_multi_variants.snap @@ -0,0 +1,22 @@ +--- +source: crates/aiken-project/src/blueprint/validator.rs +description: "Code:\n\npub opaque type Rational {\n numerator: Int,\n denominator: Int,\n}\n\nvalidator {\n fn opaque_singleton_multi_variants(redeemer: Rational, ctx: Void) {\n True\n }\n}\n" +--- +Schema { + error: Error { + context: IllegalOpaqueType, + breadcrumbs: [ + App { + public: true, + module: "test_module", + name: "Rational", + args: [], + }, + ], + }, + location: 117..135, + source_code: NamedSource { + name: "", + source: "", + , +} diff --git a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap index 2485eb95..18a09e86 100644 --- a/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap +++ b/crates/aiken-project/src/blueprint/snapshots/aiken_project__blueprint__validator__tests__opaque_singleton_variants.snap @@ -2,32 +2,46 @@ source: crates/aiken-project/src/blueprint/validator.rs description: "Code:\n\npub opaque type Dict {\n inner: List<(ByteArray, value)>\n}\n\ntype UUID { UUID }\n\nvalidator {\n fn opaque_singleton_variants(redeemer: Dict, ctx: Void) {\n True\n }\n}\n" --- -{ - "title": "test_module.opaque_singleton_variants", - "redeemer": { - "title": "redeemer", - "schema": { - "$ref": "#/definitions/test_module~1Dict$test_module~1UUID_Int" - } - }, - "compiledCode": "58f201000032323232323232323223232253330064a22930a99803a4811856616c696461746f722072657475726e65642066616c73650013656323300100100222533300a00114984c8cc00c00cc034008c8c8c94cccccc04000454cc0280205854cc0280205854cc028020584dd68008a998050040b18058011929999998078008a998048038b0a998048038b0a998048038b0a998048038b09bae0013009001300b001533333300a001153300400216137560022a660080042c2a660080042c2a660080042c9211972656465656d65723a20446963743c555549442c20496e743e005734ae7155ceaab9e5573eae855d12ba41", - "hash": "c3f68ad7fb4d6c26e1f19799fe0ded6c9904bf04b924835ddad2abf0", - "definitions": { - "ByteArray": { - "dataType": "bytes" +Schema { + error: Error { + context: IllegalOpaqueType, + breadcrumbs: [ + App { + public: true, + module: "test_module", + name: "Dict", + args: [ + Var { + tipo: RefCell { + value: Link { + tipo: App { + public: false, + module: "test_module", + name: "UUID", + args: [], + }, + }, + }, + }, + Var { + tipo: RefCell { + value: Link { + tipo: App { + public: true, + module: "", + name: "Int", + args: [], + }, + }, + }, + }, + ], + }, + ], }, - "Int": { - "dataType": "integer" - }, - "test_module/Dict$test_module/UUID_Int": { - "title": "Dict", - "dataType": "map", - "keys": { - "$ref": "#/definitions/ByteArray" - }, - "values": { - "$ref": "#/definitions/Int" - } - } - } + location: 137..162, + source_code: NamedSource { + name: "", + source: "", + , } diff --git a/crates/aiken-project/src/blueprint/validator.rs b/crates/aiken-project/src/blueprint/validator.rs index cd891418..990b7de9 100644 --- a/crates/aiken-project/src/blueprint/validator.rs +++ b/crates/aiken-project/src/blueprint/validator.rs @@ -12,8 +12,7 @@ use aiken_lang::{ }; use miette::NamedSource; use serde; -use std::borrow::Borrow; -use std::rc::Rc; +use std::{borrow::Borrow, rc::Rc}; use uplc::{ ast::{Constant, DeBruijn, Program, Term}, PlutusData, @@ -250,17 +249,6 @@ impl Validator { #[cfg(test)] mod tests { - use std::collections::HashMap; - - use aiken_lang::{ - self, - ast::{TraceLevel, Tracing}, - builtins, - }; - use uplc::ast as uplc_ast; - - use crate::tests::TestProject; - use super::{ super::{ definitions::{Definitions, Reference}, @@ -269,6 +257,14 @@ mod tests { }, *, }; + use crate::tests::TestProject; + use aiken_lang::{ + self, + ast::{TraceLevel, Tracing}, + builtins, + }; + use std::collections::HashMap; + use uplc::ast as uplc_ast; macro_rules! assert_validator { ($code:expr) => { @@ -296,15 +292,23 @@ mod tests { let validator = validators .get(0) .unwrap() - .as_ref() - .expect("Failed to create validator blueprint"); + .as_ref(); - insta::with_settings!({ - description => concat!("Code:\n\n", indoc::indoc! { $code }), - omit_expression => true - }, { - insta::assert_json_snapshot!(validator); - }); + match validator { + Err(e) => insta::with_settings!({ + description => concat!("Code:\n\n", indoc::indoc! { $code }), + omit_expression => true + }, { + insta::assert_debug_snapshot!(e); + }), + + Ok(validator) => insta::with_settings!({ + description => concat!("Code:\n\n", indoc::indoc! { $code }), + omit_expression => true + }, { + insta::assert_json_snapshot!(validator); + }), + }; }; } @@ -514,6 +518,24 @@ mod tests { ); } + #[test] + fn opaque_singleton_multi_variants() { + assert_validator!( + r#" + pub opaque type Rational { + numerator: Int, + denominator: Int, + } + + validator { + fn opaque_singleton_multi_variants(redeemer: Rational, ctx: Void) { + True + } + } + "# + ); + } + #[test] fn nested_data() { assert_validator!(