Merge pull request #847 from aiken-lang/forbid-opaque-types
Forbid opaque types in the application binary interface.
This commit is contained in:
commit
bfb4455e0f
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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>",
|
||||||
|
,
|
||||||
|
}
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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!(
|
||||||
|
|
Loading…
Reference in New Issue