Merge pull request #847 from aiken-lang/forbid-opaque-types

Forbid opaque types in the application binary interface.
This commit is contained in:
Matthias Benkort 2024-03-03 15:37:43 +01:00 committed by GitHub
commit bfb4455e0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 147 additions and 82 deletions

View File

@ -1,5 +1,7 @@
use crate::blueprint::definitions::{Definitions, Reference}; use crate::{
use crate::CheckedModule; blueprint::definitions::{Definitions, Reference},
CheckedModule,
};
use aiken_lang::{ use aiken_lang::{
ast::{Definition, TypedDataType, TypedDefinition}, ast::{Definition, TypedDataType, TypedDefinition},
builtins::wrapped_redeemer, builtins::wrapped_redeemer,
@ -12,8 +14,7 @@ use serde::{
ser::{Serialize, SerializeStruct, Serializer}, ser::{Serialize, SerializeStruct, Serializer},
}; };
use serde_json as json; use serde_json as json;
use std::rc::Rc; use std::{collections::HashMap, fmt, ops::Deref, rc::Rc};
use std::{collections::HashMap, fmt, ops::Deref};
// NOTE: Can be anything BUT 0 // NOTE: Can be anything BUT 0
pub const REDEEMER_DISCRIMINANT: usize = 1; pub const REDEEMER_DISCRIMINANT: usize = 1;
@ -385,21 +386,6 @@ impl Annotated<Schema> {
Type::Fn { .. } => unreachable!(), Type::Fn { .. } => unreachable!(),
} }
} }
fn into_data(self, type_info: &Type) -> Result<Annotated<Data>, 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 { impl Data {
@ -409,28 +395,24 @@ impl Data {
type_parameters: &mut HashMap<u64, Rc<Type>>, type_parameters: &mut HashMap<u64, Rc<Type>>,
definitions: &mut Definitions<Annotated<Schema>>, definitions: &mut Definitions<Annotated<Schema>>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
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 mut variants = vec![];
let len_constructors = data_type.constructors.len();
for (index, constructor) in data_type.constructors.iter().enumerate() { for (index, constructor) in data_type.constructors.iter().enumerate() {
let mut fields = vec![]; let mut fields = vec![];
let len_arguments = data_type.constructors.len();
for field in constructor.arguments.iter() { for field in constructor.arguments.iter() {
let reference = let reference =
Annotated::do_from_type(&field.tipo, modules, type_parameters, definitions)?; 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 { fields.push(Annotated {
title: field.label.clone(), title: field.label.clone(),
description: field.doc.clone().map(|s| s.trim().to_string()), description: field.doc.clone().map(|s| s.trim().to_string()),
@ -479,7 +461,7 @@ fn find_data_type(name: &str, definitions: &[TypedDefinition]) -> Option<TypedDa
for def in definitions { for def in definitions {
match def { match def {
Definition::DataType(data_type) if name == data_type.name => { Definition::DataType(data_type) if name == data_type.name => {
return Some(data_type.clone()) return Some(data_type.clone());
} }
Definition::Fn { .. } Definition::Fn { .. }
| Definition::Validator { .. } | Definition::Validator { .. }
@ -928,7 +910,9 @@ pub struct Error {
#[derive(Debug, PartialEq, Clone, thiserror::Error)] #[derive(Debug, PartialEq, Clone, thiserror::Error)]
pub enum ErrorContext { 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, UnsupportedType,
#[error("I discovered a type hole where I would expect a concrete type.")] #[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.")] #[error("I figured you tried to export a function in your contract's binary interface.")]
UnexpectedFunction, UnexpectedFunction,
#[error("I caught an opaque type trying to escape")]
IllegalOpaqueType,
} }
impl Error { impl Error {
@ -963,6 +950,26 @@ impl Error {
pub fn help(&self) -> String { pub fn help(&self) -> String {
match self.context { 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!( ErrorContext::UnsupportedType => format!(
r#"I do not know how to generate a portable Plutus specification for the following type: r#"I do not know how to generate a portable Plutus specification for the following type:

View File

@ -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: "<redacted>",
,
}

View File

@ -2,32 +2,46 @@
source: crates/aiken-project/src/blueprint/validator.rs source: crates/aiken-project/src/blueprint/validator.rs
description: "Code:\n\npub opaque type Dict<key, value> {\n inner: List<(ByteArray, value)>\n}\n\ntype UUID { UUID }\n\nvalidator {\n fn opaque_singleton_variants(redeemer: Dict<UUID, Int>, ctx: Void) {\n True\n }\n}\n" description: "Code:\n\npub opaque type Dict<key, value> {\n inner: List<(ByteArray, value)>\n}\n\ntype UUID { UUID }\n\nvalidator {\n fn opaque_singleton_variants(redeemer: Dict<UUID, Int>, ctx: Void) {\n True\n }\n}\n"
--- ---
{ Schema {
"title": "test_module.opaque_singleton_variants", error: Error {
"redeemer": { context: IllegalOpaqueType,
"title": "redeemer", breadcrumbs: [
"schema": { App {
"$ref": "#/definitions/test_module~1Dict$test_module~1UUID_Int" public: true,
} module: "test_module",
}, name: "Dict",
"compiledCode": "58f201000032323232323232323223232253330064a22930a99803a4811856616c696461746f722072657475726e65642066616c73650013656323300100100222533300a00114984c8cc00c00cc034008c8c8c94cccccc04000454cc0280205854cc0280205854cc028020584dd68008a998050040b18058011929999998078008a998048038b0a998048038b0a998048038b0a998048038b09bae0013009001300b001533333300a001153300400216137560022a660080042c2a660080042c2a660080042c9211972656465656d65723a20446963743c555549442c20496e743e005734ae7155ceaab9e5573eae855d12ba41", args: [
"hash": "c3f68ad7fb4d6c26e1f19799fe0ded6c9904bf04b924835ddad2abf0", Var {
"definitions": { tipo: RefCell {
"ByteArray": { value: Link {
"dataType": "bytes" tipo: App {
public: false,
module: "test_module",
name: "UUID",
args: [],
},
},
},
},
Var {
tipo: RefCell {
value: Link {
tipo: App {
public: true,
module: "",
name: "Int",
args: [],
},
},
},
},
],
},
],
}, },
"Int": { location: 137..162,
"dataType": "integer" source_code: NamedSource {
}, name: "",
"test_module/Dict$test_module/UUID_Int": { source: "<redacted>",
"title": "Dict", ,
"dataType": "map",
"keys": {
"$ref": "#/definitions/ByteArray"
},
"values": {
"$ref": "#/definitions/Int"
}
}
}
} }

View File

@ -12,8 +12,7 @@ use aiken_lang::{
}; };
use miette::NamedSource; use miette::NamedSource;
use serde; use serde;
use std::borrow::Borrow; use std::{borrow::Borrow, rc::Rc};
use std::rc::Rc;
use uplc::{ use uplc::{
ast::{Constant, DeBruijn, Program, Term}, ast::{Constant, DeBruijn, Program, Term},
PlutusData, PlutusData,
@ -250,17 +249,6 @@ impl Validator {
#[cfg(test)] #[cfg(test)]
mod tests { 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::{ use super::{
super::{ super::{
definitions::{Definitions, Reference}, 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 { macro_rules! assert_validator {
($code:expr) => { ($code:expr) => {
@ -296,15 +292,23 @@ mod tests {
let validator = validators let validator = validators
.get(0) .get(0)
.unwrap() .unwrap()
.as_ref() .as_ref();
.expect("Failed to create validator blueprint");
insta::with_settings!({ match validator {
description => concat!("Code:\n\n", indoc::indoc! { $code }), Err(e) => insta::with_settings!({
omit_expression => true description => concat!("Code:\n\n", indoc::indoc! { $code }),
}, { omit_expression => true
insta::assert_json_snapshot!(validator); }, {
}); 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] #[test]
fn nested_data() { fn nested_data() {
assert_validator!( assert_validator!(