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, ...)
This commit is contained in:
KtorZ 2024-03-03 13:37:02 +01:00
parent 4ff11f4229
commit 84c4ccaf4c
No known key found for this signature in database
GPG Key ID: 33173CB6F77F4277
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",
args: [
Var {
tipo: RefCell {
value: Link {
tipo: App {
public: false,
module: "test_module",
name: "UUID",
args: [],
}, },
"compiledCode": "58f201000032323232323232323223232253330064a22930a99803a4811856616c696461746f722072657475726e65642066616c73650013656323300100100222533300a00114984c8cc00c00cc034008c8c8c94cccccc04000454cc0280205854cc0280205854cc028020584dd68008a998050040b18058011929999998078008a998048038b0a998048038b0a998048038b0a998048038b09bae0013009001300b001533333300a001153300400216137560022a660080042c2a660080042c2a660080042c9211972656465656d65723a20446963743c555549442c20496e743e005734ae7155ceaab9e5573eae855d12ba41",
"hash": "c3f68ad7fb4d6c26e1f19799fe0ded6c9904bf04b924835ddad2abf0",
"definitions": {
"ByteArray": {
"dataType": "bytes"
}, },
"Int": {
"dataType": "integer"
}, },
"test_module/Dict$test_module/UUID_Int": {
"title": "Dict",
"dataType": "map",
"keys": {
"$ref": "#/definitions/ByteArray"
}, },
"values": { Var {
"$ref": "#/definitions/Int" tipo: RefCell {
} value: Link {
} tipo: App {
} public: true,
module: "",
name: "Int",
args: [],
},
},
},
},
],
},
],
},
location: 137..162,
source_code: NamedSource {
name: "",
source: "<redacted>",
,
} }

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 {
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 }), description => concat!("Code:\n\n", indoc::indoc! { $code }),
omit_expression => true omit_expression => true
}, { }, {
insta::assert_json_snapshot!(validator); 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!(