Merge pull request #477 from aiken-lang/blueprint-json-deserializers

Blueprint schema validation
This commit is contained in:
Matthias Benkort 2023-04-08 10:26:11 +02:00 committed by GitHub
commit ee8509956d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 2148 additions and 454 deletions

620
Cargo.lock generated vendored

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,10 @@
use std::{cell::RefCell, collections::HashMap, ops::Deref, sync::Arc};
use uplc::{ast::Type as UplcType, builtins::DefaultFunction};
use self::{environment::Environment, pretty::Printer};
use crate::{
ast::{Constant, DefinitionLocation, ModuleKind, Span},
tipo::fields::FieldMap,
};
use self::{environment::Environment, pretty::Printer};
use std::{cell::RefCell, collections::HashMap, ops::Deref, sync::Arc};
use uplc::{ast::Type as UplcType, builtins::DefaultFunction};
mod environment;
pub mod error;

View File

@ -20,6 +20,7 @@ ignore = "0.4.20"
indexmap = "1.9.2"
itertools = "0.10.5"
miette = { version = "5.5.0", features = ["fancy"] }
minicbor = "0.19.1"
owo-colors = { version = "3.5.0", features = ["supports-colors"] }
pallas = "0.18.0"
pallas-traverse = "0.18.0"
@ -37,3 +38,6 @@ toml = "0.7.2"
uplc = { path = '../uplc', version = "0.0.29" }
walkdir = "2.3.2"
zip = "0.6.4"
[dev-dependencies]
proptest = "1.1.0"

View File

@ -1,6 +1,7 @@
use aiken_lang::tipo::{Type, TypeVar};
use serde::{
self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer},
};
use std::{
@ -52,6 +53,12 @@ impl<T> Definitions<T> {
self.inner.remove(reference.as_key());
}
/// Insert a new definition
pub fn insert(&mut self, reference: &Reference, schema: T) {
self.inner
.insert(reference.as_key().to_string(), Some(schema));
}
/// Register a new definition only if it doesn't exist. This uses a strategy of
/// mark-and-insert such that recursive definitions are only built once.
pub fn register<F, E>(
@ -94,14 +101,14 @@ impl Reference {
}
/// Turn a reference into a key suitable for lookup.
fn as_key(&self) -> &str {
pub(crate) fn as_key(&self) -> &str {
self.inner.as_str()
}
/// Turn a reference into a valid JSON pointer. Note that the JSON pointer specification
/// indicates that '/' must be escaped as '~1' in pointer addresses (as they are otherwise
/// treated as path delimiter in pointers paths).
fn as_json_pointer(&self) -> String {
pub(crate) fn as_json_pointer(&self) -> String {
format!("#/definitions/{}", self.as_key().replace('/', "~1"))
}
}
@ -181,3 +188,55 @@ impl Serialize for Reference {
s.end()
}
}
impl<'a> Deserialize<'a> for Reference {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
enum Field {
#[serde(rename = "$ref")]
Ref,
}
const FIELDS: &[&str] = &["$ref"];
struct ReferenceVisitor;
impl<'a> Visitor<'a> for ReferenceVisitor {
type Value = Reference;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Reference")
}
fn visit_map<V>(self, mut map: V) -> Result<Reference, V::Error>
where
V: MapAccess<'a>,
{
let mut inner = None;
while let Some(key) = map.next_key()? {
match key {
Field::Ref => {
if inner.is_some() {
return Err(de::Error::duplicate_field(FIELDS[0]));
}
inner = Some(map.next_value()?);
}
}
}
let inner: String = inner.ok_or_else(|| de::Error::missing_field(FIELDS[0]))?;
match inner.strip_prefix("#/definitions/") {
Some(suffix) => Ok(Reference {
inner: suffix.to_string(),
}),
None => Err(de::Error::custom(
"Invalid reference; only local JSON pointer to #/definitions are allowed.",
)),
}
}
}
deserializer.deserialize_struct("Reference", FIELDS, ReferenceVisitor)
}
}

View File

@ -1,8 +1,13 @@
use super::schema;
use super::{
definitions::Reference,
schema::{self, Schema},
};
use aiken_lang::ast::Span;
use miette::{Diagnostic, NamedSource};
use minicbor as cbor;
use owo_colors::{OwoColorize, Stream::Stdout};
use std::fmt::Debug;
use uplc::ast::Constant;
#[derive(Debug, thiserror::Error, Diagnostic)]
pub enum Error {
@ -37,9 +42,61 @@ pub enum Error {
)]
#[diagnostic(code("aiken::blueprint::address::parameterized"))]
#[diagnostic(help(
"I can only compute addresses of validators that are fully applied. For example, a {keyword_spend} validator must have exactly 3 arguments: a datum, a redeemer and a context. If it has more, they need to be provided beforehand and applied directly in the validator. Applying parameters change the validator's compiled code, and thus the address.\n\nThis is why I need you to apply parameters first.",
keyword_spend = "spend".if_supports_color(Stdout, |s| s.purple())))]
"I can only compute addresses of validators that are fully applied. For example, a {keyword_spend} validator must have exactly {spend_arity} arguments: a datum, a redeemer and a context. If it has more, they need to be provided beforehand and applied directly to the validator.\n\nApplying parameters change the validator's compiled code, and thus the address. This is why I need you to apply parameters first using the {blueprint_apply_command} command.",
keyword_spend = "spend".if_supports_color(Stdout, |s| s.yellow()),
spend_arity = "3".if_supports_color(Stdout, |s| s.yellow()),
blueprint_apply_command = "blueprint apply".if_supports_color(Stdout, |s| s.purple()),
))]
ParameterizedValidator { n: usize },
#[error("I stumble upon something else than a constant when I expected one.")]
#[diagnostic(code("aiken:blueprint::apply::malformed::argument"))]
#[diagnostic(help(
"Parameters applied to blueprints must be constant; they cannot be lambdas or delayed terms."
))]
NonConstantParameter,
#[error("I couldn't find a definition corresponding to a reference.")]
#[diagnostic(code("aiken::blueprint::apply::unknown::reference"))]
#[diagnostic(help(
"While resolving a schema definition, I stumbled upon an unknown reference:\n\n→ {reference}\n\nThis is unfortunate, but signals that either the reference is invalid or that the corresponding schema definition is missing. Double-check the blueprint for that reference or definition.",
reference = reference.as_json_pointer().if_supports_color(Stdout, |s| s.red())
))]
UnresolvedSchemaReference { reference: Reference },
#[error("I caught a parameter application that seems off.")]
#[diagnostic(code("aiken::blueprint::apply::mismatch"))]
#[diagnostic(help(
"When applying parameters to a validator, I control that the shape of the parameter you give me matches what is specified in the blueprint. Unfortunately, it didn't match in this case.\n\nI am looking at the following value:\n\n{term}\n\nbut failed to match it against the specified schema:\n\n{expected}\n\n\nNOTE: this may only represent part of a bigger whole as I am validating the parameter incrementally.",
expected = serde_json::to_string_pretty(&schema).unwrap().if_supports_color(Stdout, |s| s.green()),
term = {
let mut buf = vec![];
match term {
Constant::Data(data) => {
cbor::encode(data, &mut buf).unwrap();
cbor::display(&buf).to_string()
},
_ => term.to_pretty()
}
}.if_supports_color(Stdout, |s| s.red()),
))]
SchemaMismatch { schema: Schema, term: Constant },
#[error(
"I discovered a discrepancy of elements between a given tuple and its declared schema."
)]
#[diagnostic(code("aiken::blueprint::apply::tuple::mismatch"))]
#[diagnostic(help(
"When validating a list-like schema with multiple 'items' schemas, I try to match each element of the instance with each item schema (by their position). Hence, I expect to be as many items in the declared schema ({expected}) than there are items in the instance ({found}).",
expected = expected.if_supports_color(Stdout, |s| s.green()),
found = found.if_supports_color(Stdout, |s| s.red()),
))]
TupleItemsMismatch { expected: usize, found: usize },
#[error("I failed to convert some input into a valid parameter")]
#[diagnostic(code("aiken::blueprint::parse::parameter"))]
#[diagnostic(help("{hint}"))]
MalformedParameter { hint: String },
}
unsafe impl Send for Error {}

View File

@ -1,22 +1,23 @@
pub mod definitions;
pub mod error;
pub mod parameter;
pub mod schema;
pub mod validator;
use crate::{config::Config, module::CheckedModules};
use aiken_lang::gen_uplc::CodeGenerator;
use definitions::{Definitions, Reference};
use definitions::Definitions;
use error::Error;
use schema::{Annotated, Schema};
use std::fmt::Debug;
use validator::Validator;
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Blueprint<R: Default, S: Default> {
pub struct Blueprint {
pub preamble: Preamble,
pub validators: Vec<Validator<R, S>>,
pub validators: Vec<Validator>,
#[serde(skip_serializing_if = "Definitions::is_empty", default)]
pub definitions: Definitions<S>,
pub definitions: Definitions<Annotated<Schema>>,
}
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
@ -48,7 +49,7 @@ pub enum LookupResult<'a, T> {
Many,
}
impl Blueprint<Reference, Annotated<Schema>> {
impl Blueprint {
pub fn new(
config: &Config,
modules: &CheckedModules,
@ -82,12 +83,8 @@ impl Blueprint<Reference, Annotated<Schema>> {
}
}
impl<R, S> Blueprint<R, S>
where
R: Clone + Default,
S: Clone + Default,
{
pub fn lookup(&self, title: Option<&String>) -> Option<LookupResult<Validator<R, S>>> {
impl Blueprint {
pub fn lookup(&self, title: Option<&String>) -> Option<LookupResult<Validator>> {
let mut validator = None;
for v in self.validators.iter() {
@ -112,7 +109,7 @@ where
action: F,
) -> Result<A, E>
where
F: Fn(Validator<R, S>) -> Result<A, E>,
F: Fn(Validator) -> Result<A, E>,
{
match self.lookup(title) {
Some(LookupResult::One(validator)) => action(validator.to_owned()),
@ -146,13 +143,13 @@ impl From<&Config> for Preamble {
mod test {
use super::*;
use aiken_lang::builtins;
use schema::{Data, Items, Schema};
use schema::{Data, Declaration, Items, Schema};
use serde_json::{self, json};
use std::collections::HashMap;
#[test]
fn serialize_no_description() {
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
let blueprint = Blueprint {
preamble: Preamble {
title: "Foo".to_string(),
description: None,
@ -179,7 +176,7 @@ mod test {
#[test]
fn serialize_with_description() {
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
let blueprint = Blueprint {
preamble: Preamble {
title: "Foo".to_string(),
description: Some("Lorem ipsum".to_string()),
@ -222,12 +219,15 @@ mod test {
&HashMap::new(),
|_| Ok(Schema::Data(Data::Bytes).into()),
)?;
Ok(Schema::Data(Data::List(Items::One(Box::new(ref_bytes)))).into())
Ok(
Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_bytes))))
.into(),
)
},
)
.unwrap();
let blueprint: Blueprint<Reference, Annotated<Schema>> = Blueprint {
let blueprint = Blueprint {
preamble: Preamble {
title: "Foo".to_string(),
description: None,

View File

@ -0,0 +1,428 @@
use super::{
definitions::{Definitions, Reference},
error::Error,
schema::{Annotated, Constructor, Data, Declaration, Items, Schema},
};
use std::{iter, ops::Deref};
use uplc::{
ast::{Constant, Data as UplcData, DeBruijn, Term},
PlutusData,
};
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Parameter {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub schema: Reference,
}
impl From<Reference> for Parameter {
fn from(schema: Reference) -> Parameter {
Parameter {
title: None,
schema,
}
}
}
impl Parameter {
pub fn validate(
&self,
definitions: &Definitions<Annotated<Schema>>,
term: &Term<DeBruijn>,
) -> Result<(), Error> {
let schema = &definitions
.lookup(&self.schema)
.map(Ok)
.unwrap_or_else(|| {
Err(Error::UnresolvedSchemaReference {
reference: self.schema.clone(),
})
})?
.annotated;
if let Term::Constant(constant) = term {
validate_schema(schema, definitions, constant)
} else {
Err(Error::NonConstantParameter)
}
}
}
fn mismatch(term: &Constant, schema: Schema) -> Error {
Error::SchemaMismatch {
schema,
term: term.clone(),
}
}
fn validate_schema(
schema: &Schema,
definitions: &Definitions<Annotated<Schema>>,
term: &Constant,
) -> Result<(), Error> {
match schema {
Schema::Data(data) => validate_data(data, definitions, term),
Schema::Unit => expect_unit(term),
Schema::Integer => expect_integer(term),
Schema::Bytes => expect_bytes(term),
Schema::String => expect_string(term),
Schema::Boolean => expect_boolean(term),
Schema::Pair(left, right) => {
let (term_left, term_right) = expect_pair(term)?;
let left =
left.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: left.reference().unwrap().clone(),
})?;
validate_schema(left, definitions, &term_left)?;
let right =
right
.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: right.reference().unwrap().clone(),
})?;
validate_schema(right, definitions, &term_right)?;
Ok(())
}
Schema::List(Items::One(item)) => {
let terms = expect_list(term)?;
let item =
item.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: item.reference().unwrap().clone(),
})?;
for ref term in terms {
validate_schema(item, definitions, term)?;
}
Ok(())
}
Schema::List(Items::Many(items)) => {
let terms = expect_list(term)?;
let items = items
.iter()
.map(|item| {
item.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: item.reference().unwrap().clone(),
})
})
.collect::<Result<Vec<_>, _>>()?;
if terms.len() != items.len() {
return Err(Error::TupleItemsMismatch {
expected: items.len(),
found: terms.len(),
});
}
for (item, ref term) in iter::zip(items, terms) {
validate_schema(item, definitions, term)?;
}
Ok(())
}
}
}
fn validate_data(
data: &Data,
definitions: &Definitions<Annotated<Schema>>,
term: &Constant,
) -> Result<(), Error> {
match data {
Data::Opaque => expect_data(term),
Data::Integer => expect_data_integer(term),
Data::Bytes => expect_data_bytes(term),
Data::List(Items::One(item)) => {
let terms = expect_data_list(term)?;
let item =
item.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: item.reference().unwrap().clone(),
})?;
for ref term in terms {
validate_data(item, definitions, term)?;
}
Ok(())
}
Data::List(Items::Many(items)) => {
let terms = expect_data_list(term)?;
let items = items
.iter()
.map(|item| {
item.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: item.reference().unwrap().clone(),
})
})
.collect::<Result<Vec<_>, _>>()?;
if terms.len() != items.len() {
return Err(Error::TupleItemsMismatch {
expected: items.len(),
found: terms.len(),
});
}
for (item, ref term) in iter::zip(items, terms) {
validate_data(item, definitions, term)?;
}
Ok(())
}
Data::Map(keys, values) => {
let terms = expect_data_map(term)?;
let keys =
keys.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: keys.reference().unwrap().clone(),
})?;
let values =
values
.schema(definitions)
.ok_or_else(|| Error::UnresolvedSchemaReference {
reference: values.reference().unwrap().clone(),
})?;
for (ref k, ref v) in terms {
validate_data(keys, definitions, k)?;
validate_data(values, definitions, v)?;
}
Ok(())
}
Data::AnyOf(constructors) => {
let constructors: Vec<(usize, Vec<&Data>)> = constructors
.iter()
.map(|constructor| {
constructor
.annotated
.fields
.iter()
.map(|field| {
field.annotated.schema(definitions).ok_or_else(|| {
Error::UnresolvedSchemaReference {
reference: field.annotated.reference().unwrap().clone(),
}
})
})
.collect::<Result<_, _>>()
.map(|fields| (constructor.annotated.index, fields))
})
.collect::<Result<_, _>>()?;
for (index, fields_schema) in constructors.iter() {
if let Ok(fields) = expect_data_constr(term, *index) {
if fields_schema.len() != fields.len() {
panic!("fields length different");
}
for (instance, schema) in iter::zip(fields, fields_schema) {
validate_data(schema, definitions, &instance)?;
}
return Ok(());
}
}
Err(mismatch(
term,
Schema::Data(Data::AnyOf(
constructors
.iter()
.map(|(index, fields)| {
Constructor {
index: *index,
fields: fields
.iter()
.map(|_| Declaration::Inline(Box::new(Data::Opaque)).into())
.collect(),
}
.into()
})
.collect(),
)),
))
}
}
}
fn expect_data(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::Data(..)) {
return Ok(());
}
Err(mismatch(term, Schema::Data(Data::Opaque)))
}
fn expect_data_integer(term: &Constant) -> Result<(), Error> {
if let Constant::Data(data) = term {
if matches!(data, PlutusData::BigInt(..)) {
return Ok(());
}
}
Err(mismatch(term, Schema::Data(Data::Integer)))
}
fn expect_data_bytes(term: &Constant) -> Result<(), Error> {
if let Constant::Data(data) = term {
if matches!(data, PlutusData::BoundedBytes(..)) {
return Ok(());
}
}
Err(mismatch(term, Schema::Data(Data::Bytes)))
}
fn expect_data_list(term: &Constant) -> Result<Vec<Constant>, Error> {
if let Constant::Data(PlutusData::Array(elems)) = term {
return Ok(elems
.iter()
.map(|elem| Constant::Data(elem.to_owned()))
.collect());
}
Err(mismatch(
term,
Schema::Data(Data::List(Items::One(Declaration::Inline(Box::new(
Data::Opaque,
))))),
))
}
fn expect_data_map(term: &Constant) -> Result<Vec<(Constant, Constant)>, Error> {
if let Constant::Data(PlutusData::Map(pairs)) = term {
return Ok(pairs
.iter()
.map(|(k, v)| (Constant::Data(k.to_owned()), Constant::Data(v.to_owned())))
.collect());
}
Err(mismatch(
term,
Schema::Data(Data::Map(
Declaration::Inline(Box::new(Data::Opaque)),
Declaration::Inline(Box::new(Data::Opaque)),
)),
))
}
fn expect_data_constr(term: &Constant, index: usize) -> Result<Vec<Constant>, Error> {
if let Constant::Data(PlutusData::Constr(constr)) = term {
if let PlutusData::Constr(expected) = UplcData::constr(index as u64, vec![]) {
if expected.tag == constr.tag && expected.any_constructor == constr.any_constructor {
return Ok(constr
.fields
.iter()
.map(|field| Constant::Data(field.to_owned()))
.collect());
}
}
}
Err(mismatch(
term,
Schema::Data(Data::AnyOf(vec![Constructor {
index,
fields: vec![],
}
.into()])),
))
}
fn expect_unit(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::Unit) {
return Ok(());
}
Err(mismatch(term, Schema::Unit))
}
fn expect_integer(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::Integer(..)) {
return Ok(());
}
Err(mismatch(term, Schema::Integer))
}
fn expect_bytes(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::ByteString(..)) {
return Ok(());
}
Err(mismatch(term, Schema::Bytes))
}
fn expect_string(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::String(..)) {
return Ok(());
}
Err(mismatch(term, Schema::String))
}
fn expect_boolean(term: &Constant) -> Result<(), Error> {
if matches!(term, Constant::Bool(..)) {
return Ok(());
}
Err(mismatch(term, Schema::Boolean))
}
fn expect_pair(term: &Constant) -> Result<(Constant, Constant), Error> {
if let Constant::ProtoPair(_, _, left, right) = term {
return Ok((left.deref().clone(), right.deref().clone()));
}
Err(mismatch(
term,
Schema::Pair(
Declaration::Inline(Box::new(Schema::Data(Data::Opaque))),
Declaration::Inline(Box::new(Schema::Data(Data::Opaque))),
),
))
}
fn expect_list(term: &Constant) -> Result<Vec<Constant>, Error> {
if let Constant::ProtoList(_, elems) = term {
return Ok(elems.to_owned());
}
Err(mismatch(
term,
Schema::List(Items::One(Declaration::Inline(Box::new(Schema::Data(
Data::Opaque,
))))),
))
}

View File

@ -8,10 +8,11 @@ use aiken_lang::{
use owo_colors::{OwoColorize, Stream::Stdout};
use serde::{
self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer},
};
use std::ops::Deref;
use std::{collections::HashMap, sync::Arc};
use serde_json as json;
use std::{collections::HashMap, fmt, ops::Deref, sync::Arc};
// NOTE: Can be anything BUT 0
pub const REDEEMER_DISCRIMINANT: usize = 1;
@ -26,6 +27,50 @@ pub struct Annotated<T> {
pub annotated: T,
}
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Declaration<T> {
Referenced(Reference),
Inline(Box<T>),
}
impl<'a, T> Declaration<T> {
pub fn reference(&'a self) -> Option<&'a Reference> {
match self {
Declaration::Referenced(reference) => Some(reference),
Declaration::Inline(..) => None,
}
}
fn try_schema(
&'a self,
definitions: &'a Definitions<Annotated<Schema>>,
cast: fn(&'a Schema) -> Option<&'a T>,
) -> Option<&'a T> {
match self {
Declaration::Inline(inner) => Some(inner.deref()),
Declaration::Referenced(reference) => definitions
.lookup(reference)
.and_then(|s| cast(&s.annotated)),
}
}
}
impl<'a> Declaration<Data> {
pub fn schema(&'a self, definitions: &'a Definitions<Annotated<Schema>>) -> Option<&'a Data> {
self.try_schema(definitions, |s| match s {
Schema::Data(data) => Some(data),
_ => None,
})
}
}
impl<'a> Declaration<Schema> {
pub fn schema(&'a self, definitions: &'a Definitions<Annotated<Schema>>) -> Option<&'a Schema> {
self.try_schema(definitions, Some)
}
}
/// A schema for low-level UPLC primitives.
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Schema {
@ -34,8 +79,8 @@ pub enum Schema {
Integer,
Bytes,
String,
Pair(Data, Data),
List(Vec<Data>),
Pair(Declaration<Schema>, Declaration<Schema>),
List(Items<Schema>),
Data(Data),
}
@ -44,24 +89,25 @@ pub enum Schema {
pub enum Data {
Integer,
Bytes,
List(Items<Reference>),
Map(Box<Reference>, Box<Reference>),
List(Items<Data>),
Map(Declaration<Data>, Declaration<Data>),
AnyOf(Vec<Annotated<Constructor>>),
Opaque,
}
/// A structure that represents either one or many elements.
#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
pub enum Items<T> {
One(Box<T>),
Many(Vec<T>),
One(Declaration<T>),
Many(Vec<Declaration<T>>),
}
/// Captures a single UPLC constructor with its
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Constructor {
pub index: usize,
pub fields: Vec<Annotated<Reference>>,
pub fields: Vec<Annotated<Declaration<Data>>>,
}
impl<T> From<T> for Annotated<T> {
@ -90,7 +136,7 @@ impl Annotated<Schema> {
description: Some("A redeemer wrapped in an extra constructor to make multi-validator detection possible on-chain.".to_string()),
annotated: Schema::Data(Data::AnyOf(vec![Constructor {
index: REDEEMER_DISCRIMINANT,
fields: vec![schema.into()],
fields: vec![Declaration::Referenced(schema).into()],
}
.into()])),
})
@ -219,7 +265,7 @@ impl Annotated<Schema> {
description: Some("An optional value.".to_string()),
annotated: Constructor {
index: 0,
fields: vec![generic.into()],
fields: vec![Declaration::Referenced(generic).into()],
},
},
Annotated {
@ -260,22 +306,15 @@ impl Annotated<Schema> {
} if xs.len() == 2 => {
definitions.remove(&generic);
Data::Map(
Box::new(
xs.first()
.expect("length (== 2) checked in pattern clause")
.to_owned(),
),
Box::new(
xs.last()
.expect("length (== 2) checked in pattern clause")
.to_owned(),
),
)
}
_ => {
// let inner = schema.clone().into_data(type_info)?.annotated;
Data::List(Items::One(Box::new(generic)))
}
_ => Data::List(Items::One(Declaration::Referenced(generic))),
};
Ok(Schema::Data(data).into())
@ -320,6 +359,7 @@ impl Annotated<Schema> {
.iter()
.map(|elem| {
Annotated::do_from_type(elem, modules, type_parameters, definitions)
.map(Declaration::Referenced)
})
.collect::<Result<Vec<_>, _>>()
.map_err(|e| e.backtrack(type_info))?;
@ -398,7 +438,7 @@ impl Data {
fields.push(Annotated {
title: field.label.clone(),
description: field.doc.clone().map(|s| s.trim().to_string()),
annotated: reference,
annotated: Declaration::Referenced(reference),
});
}
@ -510,6 +550,245 @@ impl Serialize for Schema {
}
}
fn visit_schema<'a, V>(mut map: V) -> Result<Schema, V::Error>
where
V: MapAccess<'a>,
{
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "camelCase")]
enum Field {
DataType,
Items,
Keys,
Values,
Left,
Right,
AnyOf,
OneOf,
}
let mut data_type: Option<String> = None;
let mut items: Option<json::Value> = None; // defer items deserialization to later
let mut keys = None;
let mut left = None;
let mut right = None;
let mut values = None;
let mut any_of = None;
while let Some(key) = map.next_key()? {
match key {
Field::DataType => {
if data_type.is_some() {
return Err(de::Error::duplicate_field("dataType"));
}
data_type = Some(map.next_value()?);
}
Field::Items => {
if items.is_some() {
return Err(de::Error::duplicate_field("items"));
}
items = Some(map.next_value()?);
}
Field::Keys => {
if keys.is_some() {
return Err(de::Error::duplicate_field("keys"));
}
keys = Some(map.next_value()?);
}
Field::Values => {
if values.is_some() {
return Err(de::Error::duplicate_field("values"));
}
values = Some(map.next_value()?);
}
Field::Left => {
if left.is_some() {
return Err(de::Error::duplicate_field("left"));
}
left = Some(map.next_value()?);
}
Field::Right => {
if right.is_some() {
return Err(de::Error::duplicate_field("right"));
}
right = Some(map.next_value()?);
}
Field::AnyOf => {
if any_of.is_some() {
return Err(de::Error::duplicate_field("anyOf/oneOf"));
}
any_of = Some(map.next_value()?);
}
Field::OneOf => {
if any_of.is_some() {
return Err(de::Error::duplicate_field("anyOf/oneOf"));
}
any_of = Some(map.next_value()?);
}
}
}
let expect_data_items = || match &items {
Some(items) => serde_json::from_value::<Items<Data>>(items.clone())
.map_err(|e| de::Error::custom(e.to_string())),
None => Err(de::Error::missing_field("items")),
};
let expect_schema_items = || match &items {
Some(items) => serde_json::from_value::<Items<Schema>>(items.clone())
.map_err(|e| de::Error::custom(e.to_string())),
None => Err(de::Error::missing_field("items")),
};
let expect_no_items = || {
if items.is_some() {
return Err(de::Error::custom(
"unexpected fields 'items' for non-list data-type",
));
}
Ok(())
};
let expect_no_keys = || {
if keys.is_some() {
return Err(de::Error::custom(
"unexpected fields 'keys' for non-map data-type",
));
}
Ok(())
};
let expect_no_values = || {
if values.is_some() {
return Err(de::Error::custom(
"unexpected fields 'values' for non-map data-type",
));
}
Ok(())
};
let expect_no_any_of = || {
if any_of.is_some() {
return Err(de::Error::custom(
"unexpected fields 'anyOf' or 'oneOf'; applicators must singletons",
));
}
Ok(())
};
let expect_no_left_or_right = || {
if left.is_some() || right.is_some() {
return Err(de::Error::custom(
"unexpected field(s) 'left' and/or 'right' for a non-pair data-type",
));
}
Ok(())
};
match data_type {
None => {
expect_no_items()?;
expect_no_keys()?;
expect_no_values()?;
expect_no_left_or_right()?;
match any_of {
None => Ok(Schema::Data(Data::Opaque)),
Some(constructors) => Ok(Schema::Data(Data::AnyOf(constructors))),
}
}
Some(data_type) if data_type == "list" => {
expect_no_keys()?;
expect_no_values()?;
expect_no_any_of()?;
expect_no_left_or_right()?;
let items = expect_data_items()?;
Ok(Schema::Data(Data::List(items)))
}
Some(data_type) if data_type == "#list" => {
expect_no_keys()?;
expect_no_values()?;
expect_no_any_of()?;
expect_no_left_or_right()?;
let items = expect_schema_items()?;
Ok(Schema::List(items))
}
Some(data_type) if data_type == "map" => {
expect_no_items()?;
expect_no_any_of()?;
expect_no_left_or_right()?;
match (keys, values) {
(Some(keys), Some(values)) => Ok(Schema::Data(Data::Map(keys, values))),
(None, _) => Err(de::Error::missing_field("keys")),
(Some(..), None) => Err(de::Error::missing_field("values")),
}
}
Some(data_type) if data_type == "#pair" => {
expect_no_items()?;
expect_no_keys()?;
expect_no_values()?;
expect_no_any_of()?;
match (left, right) {
(Some(left), Some(right)) => Ok(Schema::Pair(left, right)),
(None, _) => Err(de::Error::missing_field("left")),
(Some(..), None) => Err(de::Error::missing_field("right")),
}
}
Some(data_type) => {
expect_no_items()?;
expect_no_keys()?;
expect_no_values()?;
expect_no_any_of()?;
expect_no_left_or_right()?;
if data_type == "bytes" {
Ok(Schema::Data(Data::Bytes))
} else if data_type == "integer" {
Ok(Schema::Data(Data::Integer))
} else if data_type == "#unit" {
Ok(Schema::Unit)
} else if data_type == "#integer" {
Ok(Schema::Integer)
} else if data_type == "#bytes" {
Ok(Schema::Bytes)
} else if data_type == "#boolean" {
Ok(Schema::Boolean)
} else if data_type == "#string" {
Ok(Schema::String)
} else {
Err(de::Error::custom("unknown data-type"))
}
}
}
}
impl<'a> Deserialize<'a> for Schema {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
struct SchemaVisitor;
impl<'a> Visitor<'a> for SchemaVisitor {
type Value = Schema;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Schema")
}
fn visit_map<V>(self, mut map: V) -> Result<Schema, V::Error>
where
V: MapAccess<'a>,
{
visit_schema(&mut map)
}
}
deserializer.deserialize_struct(
"Schema",
&[
"dataType", "items", "keys", "values", "anyOf", "oneOf", "left", "right",
],
SchemaVisitor,
)
}
}
impl Serialize for Data {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
@ -527,13 +806,7 @@ impl Serialize for Data {
s.serialize_field("dataType", "bytes")?;
s.end()
}
Data::List(Items::One(item)) => {
let mut s = serializer.serialize_struct("List", 2)?;
s.serialize_field("dataType", "list")?;
s.serialize_field("items", &item)?;
s.end()
}
Data::List(Items::Many(items)) => {
Data::List(items) => {
let mut s = serializer.serialize_struct("List", 2)?;
s.serialize_field("dataType", "list")?;
s.serialize_field("items", &items)?;
@ -554,6 +827,38 @@ impl Serialize for Data {
}
}
}
impl<'a> Deserialize<'a> for Data {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
struct DataVisitor;
impl<'a> Visitor<'a> for DataVisitor {
type Value = Data;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Data")
}
fn visit_map<V>(self, mut map: V) -> Result<Data, V::Error>
where
V: MapAccess<'a>,
{
let schema = visit_schema(&mut map)?;
match schema {
Schema::Data(data) => Ok(data),
_ => Err(de::Error::custom("not a valid 'data'")),
}
}
}
deserializer.deserialize_struct(
"Data",
&["dataType", "items", "keys", "values", "anyOf", "oneOf"],
DataVisitor,
)
}
}
impl Serialize for Constructor {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut s = serializer.serialize_struct("Constructor", 3)?;
@ -564,6 +869,60 @@ impl Serialize for Constructor {
}
}
impl<'a> Deserialize<'a> for Constructor {
fn deserialize<D: Deserializer<'a>>(deserializer: D) -> Result<Self, D::Error> {
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "camelCase")]
enum Field {
Index,
Fields,
}
const FIELDS: &[&str] = &["index", "fields"];
struct ConstructorVisitor;
impl<'a> Visitor<'a> for ConstructorVisitor {
type Value = Constructor;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Constructor")
}
fn visit_map<V>(self, mut map: V) -> Result<Constructor, V::Error>
where
V: MapAccess<'a>,
{
let mut index = None;
let mut fields = None;
while let Some(key) = map.next_key()? {
match key {
Field::Index => {
if index.is_some() {
return Err(de::Error::duplicate_field(FIELDS[0]));
}
index = Some(map.next_value()?);
}
Field::Fields => {
if fields.is_some() {
return Err(de::Error::duplicate_field(FIELDS[1]));
}
fields = Some(map.next_value()?);
}
}
}
Ok(Constructor {
index: index.ok_or_else(|| de::Error::missing_field(FIELDS[0]))?,
fields: fields.ok_or_else(|| de::Error::missing_field(FIELDS[1]))?,
})
}
}
deserializer.deserialize_struct("Constructor", FIELDS, ConstructorVisitor)
}
}
#[derive(Debug, PartialEq, Clone, thiserror::Error)]
#[error("{}", context)]
pub struct Error {
@ -681,6 +1040,7 @@ Here's the types I followed and that led me to this problem:
#[cfg(test)]
pub mod test {
use super::*;
use proptest::prelude::*;
use serde_json::{self, json, Value};
pub fn assert_json(schema: &impl Serialize, expected: Value) {
@ -712,7 +1072,7 @@ pub mod test {
#[test]
fn serialize_data_list_1() {
let ref_integer = Reference::new("Int");
let schema = Schema::Data(Data::List(Items::One(Box::new(ref_integer))));
let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(ref_integer))));
assert_json(
&schema,
json!({
@ -727,7 +1087,9 @@ pub mod test {
#[test]
fn serialize_data_list_2() {
let ref_list_integer = Reference::new("List$Int");
let schema = Schema::Data(Data::List(Items::One(Box::new(ref_list_integer))));
let schema = Schema::Data(Data::List(Items::One(Declaration::Referenced(
ref_list_integer,
))));
assert_json(
&schema,
json!({
@ -741,9 +1103,9 @@ pub mod test {
#[test]
fn serialize_data_map_1() {
let ref_integer = Reference::new("Int");
let ref_bytes = Reference::new("ByteArray");
let schema = Schema::Data(Data::Map(Box::new(ref_integer), Box::new(ref_bytes)));
let ref_integer = Declaration::Referenced(Reference::new("Int"));
let ref_bytes = Declaration::Referenced(Reference::new("ByteArray"));
let schema = Schema::Data(Data::Map(ref_integer, ref_bytes));
assert_json(
&schema,
json!({
@ -782,12 +1144,12 @@ pub mod test {
let schema = Schema::Data(Data::AnyOf(vec![
Constructor {
index: 0,
fields: vec![Reference::new("Int").into()],
fields: vec![Declaration::Referenced(Reference::new("Int")).into()],
}
.into(),
Constructor {
index: 1,
fields: vec![Reference::new("Bytes").into()],
fields: vec![Declaration::Referenced(Reference::new("Bytes")).into()],
}
.into(),
]));
@ -848,4 +1210,224 @@ pub mod test {
}),
)
}
#[test]
fn deserialize_data_opaque() {
assert_eq!(Data::Opaque, serde_json::from_value(json!({})).unwrap())
}
#[test]
fn deserialize_data_integer() {
assert_eq!(
Data::Integer,
serde_json::from_value(json!({
"dataType": "integer",
}))
.unwrap()
)
}
#[test]
fn deserialize_data_bytes() {
assert_eq!(
Data::Bytes,
serde_json::from_value(json!({
"dataType": "bytes",
}))
.unwrap()
)
}
#[test]
fn deserialize_data_list_one() {
assert_eq!(
Data::List(Items::One(Declaration::Referenced(Reference::new("foo")))),
serde_json::from_value(json!({
"dataType": "list",
"items": { "$ref": "#/definitions/foo" }
}))
.unwrap()
)
}
#[test]
fn deserialize_data_list_many() {
assert_eq!(
Data::List(Items::Many(vec![
Declaration::Referenced(Reference::new("foo")),
Declaration::Referenced(Reference::new("bar"))
])),
serde_json::from_value(json!({
"dataType": "list",
"items": [
{ "$ref": "#/definitions/foo" },
{ "$ref": "#/definitions/bar" }
],
}))
.unwrap()
)
}
#[test]
fn deserialize_data_map() {
assert_eq!(
Data::Map(
Declaration::Referenced(Reference::new("foo")),
Declaration::Referenced(Reference::new("bar"))
),
serde_json::from_value(json!({
"dataType": "map",
"keys": { "$ref": "#/definitions/foo" },
"values": { "$ref": "#/definitions/bar" }
}))
.unwrap()
)
}
#[test]
fn deserialize_any_of() {
assert_eq!(
Data::AnyOf(vec![Constructor {
index: 0,
fields: vec![
Declaration::Referenced(Reference::new("foo")).into(),
Declaration::Referenced(Reference::new("bar")).into()
],
}
.into()]),
serde_json::from_value(json!({
"anyOf": [{
"index": 0,
"fields": [
{
"$ref": "#/definitions/foo",
},
{
"$ref": "#/definitions/bar",
}
]
}]
}))
.unwrap()
)
}
#[test]
fn deserialize_one_of() {
assert_eq!(
Data::AnyOf(vec![Constructor {
index: 0,
fields: vec![
Declaration::Referenced(Reference::new("foo")).into(),
Declaration::Referenced(Reference::new("bar")).into()
],
}
.into()]),
serde_json::from_value(json!({
"oneOf": [{
"index": 0,
"fields": [
{
"$ref": "#/definitions/foo",
},
{
"$ref": "#/definitions/bar",
}
]
}]
}))
.unwrap()
)
}
fn arbitrary_data() -> impl Strategy<Value = Data> {
let leaf = prop_oneof![Just(Data::Opaque), Just(Data::Bytes), Just(Data::Integer)];
leaf.prop_recursive(3, 8, 3, |inner| {
let r = prop_oneof![
".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))),
inner.prop_map(|s| Declaration::Inline(Box::new(s)))
];
let constructor =
(0..3usize, prop::collection::vec(r.clone(), 0..3)).prop_map(|(index, fields)| {
Constructor {
index,
fields: fields.into_iter().map(|f| f.into()).collect(),
}
.into()
});
prop_oneof![
(r.clone(), r.clone()).prop_map(|(k, v)| Data::Map(k, v)),
r.clone().prop_map(|x| Data::List(Items::One(x))),
prop::collection::vec(r, 1..3).prop_map(|xs| Data::List(Items::Many(xs))),
prop::collection::vec(constructor, 1..3).prop_map(Data::AnyOf)
]
})
}
fn arbitrary_schema() -> impl Strategy<Value = Schema> {
prop_compose! {
fn data_strategy()(data in arbitrary_data()) -> Schema {
Schema::Data(data)
}
}
let leaf = prop_oneof![
Just(Schema::Unit),
Just(Schema::Boolean),
Just(Schema::Bytes),
Just(Schema::Integer),
Just(Schema::String),
data_strategy(),
];
leaf.prop_recursive(3, 8, 3, |inner| {
let r = prop_oneof![
".*".prop_map(|s| Declaration::Referenced(Reference::new(&s))),
inner.prop_map(|s| Declaration::Inline(Box::new(s)))
];
prop_oneof![
(r.clone(), r.clone()).prop_map(|(l, r)| Schema::Pair(l, r)),
r.clone().prop_map(|x| Schema::List(Items::One(x))),
prop::collection::vec(r, 1..3).prop_map(|xs| Schema::List(Items::Many(xs))),
]
})
}
proptest! {
#[test]
fn data_serialization_roundtrip(data in arbitrary_data()) {
let json = serde_json::to_value(data);
let pretty = json
.as_ref()
.map(|v| serde_json::to_string_pretty(v).unwrap())
.unwrap_or_else(|_| "invalid".to_string());
assert!(
matches!(
json.and_then(serde_json::from_value::<Data>),
Ok{..}
),
"\ncounterexample: {pretty}\n",
)
}
}
proptest! {
#[test]
fn schema_serialization_roundtrip(schema in arbitrary_schema()) {
let json = serde_json::to_value(schema);
let pretty = json
.as_ref()
.map(|v| serde_json::to_string_pretty(v).unwrap())
.unwrap_or_else(|_| "invalid".to_string());
assert!(
matches!(
json.and_then(serde_json::from_value::<Schema>),
Ok{..}
),
"\ncounterexample: {pretty}\n",
)
}
}
}

View File

@ -1,6 +1,7 @@
use super::{
definitions::{Definitions, Reference},
definitions::Definitions,
error::Error,
parameter::Parameter,
schema::{Annotated, Schema},
};
use crate::module::{CheckedModule, CheckedModules};
@ -13,44 +14,36 @@ use serde;
use uplc::ast::{DeBruijn, Program, Term};
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Validator<R, S> {
pub struct Validator {
pub title: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub datum: Option<Argument<R>>,
pub datum: Option<Parameter>,
pub redeemer: Argument<R>,
pub redeemer: Parameter,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub parameters: Vec<Argument<R>>,
pub parameters: Vec<Parameter>,
#[serde(flatten)]
pub program: Program<DeBruijn>,
#[serde(skip_serializing_if = "Definitions::is_empty")]
#[serde(default)]
pub definitions: Definitions<S>,
pub definitions: Definitions<Annotated<Schema>>,
}
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Argument<T> {
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub schema: T,
}
impl Validator<Reference, Annotated<Schema>> {
impl Validator {
pub fn from_checked_module(
modules: &CheckedModules,
generator: &mut CodeGenerator,
module: &CheckedModule,
def: &TypedValidator,
) -> Vec<Result<Validator<Reference, Annotated<Schema>>, Error>> {
) -> Vec<Result<Validator, Error>> {
let program = generator.generate(def).try_into().unwrap();
let is_multi_validator = def.other_fun.is_some();
@ -85,7 +78,7 @@ impl Validator<Reference, Annotated<Schema>> {
params: &[TypedArg],
func: &TypedFunction,
is_multi_validator: bool,
) -> Result<Validator<Reference, Annotated<Schema>>, Error> {
) -> Result<Validator, Error> {
let mut args = func.arguments.iter().rev();
let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next());
@ -102,7 +95,7 @@ impl Validator<Reference, Annotated<Schema>> {
.iter()
.map(|param| {
Annotated::from_type(modules.into(), &param.tipo, &mut definitions)
.map(|schema| Argument {
.map(|schema| Parameter {
title: Some(param.arg_name.get_label()),
schema,
})
@ -130,7 +123,7 @@ impl Validator<Reference, Annotated<Schema>> {
)
})
.transpose()?
.map(|schema| Argument {
.map(|schema| Parameter {
title: datum.map(|datum| datum.arg_name.get_label()),
schema,
}),
@ -143,7 +136,7 @@ impl Validator<Reference, Annotated<Schema>> {
module.code.clone(),
),
})
.map(|schema| Argument {
.map(|schema| Parameter {
title: Some(redeemer.arg_name.get_label()),
schema: match datum {
Some(..) if is_multi_validator => Annotated::as_wrapped_redeemer(
@ -160,16 +153,16 @@ impl Validator<Reference, Annotated<Schema>> {
}
}
impl<R, S> Validator<R, S>
where
S: Clone,
R: Clone,
{
pub fn apply(self, arg: &Term<DeBruijn>) -> Result<Self, Error> {
impl Validator {
pub fn apply(
self,
definitions: &Definitions<Annotated<Schema>>,
arg: &Term<DeBruijn>,
) -> Result<Self, Error> {
match self.parameters.split_first() {
None => Err(Error::NoParametersToApply),
Some((_, tail)) => {
// TODO: Ideally, we should control that the applied term matches its schema.
Some((head, tail)) => {
head.validate(definitions, arg)?;
Ok(Self {
program: self.program.apply_term(arg),
parameters: tail.to_vec(),
@ -182,7 +175,14 @@ where
#[cfg(test)]
mod test {
use super::*;
use super::{
super::{
definitions::{Definitions, Reference},
error::Error,
schema::{Annotated, Constructor, Data, Declaration, Items, Schema},
},
*,
};
use crate::{module::ParsedModule, PackageName};
use aiken_lang::{
self,
@ -197,6 +197,7 @@ mod test {
use indexmap::IndexMap;
use serde_json::{self, json};
use std::{collections::HashMap, path::PathBuf};
use uplc::ast as uplc;
// TODO: Possible refactor this out of the module and have it used by `Project`. The idea would
// be to make this struct below the actual project, and wrap it in another metadata struct
@ -318,6 +319,69 @@ mod test {
assert_json_eq!(serde_json::to_value(validator).unwrap(), expected);
}
fn fixture_definitions() -> Definitions<Annotated<Schema>> {
let mut definitions = Definitions::new();
// #/definitions/Int
//
// {
// "dataType": "integer"
// }
definitions
.register::<_, Error>(&builtins::int(), &HashMap::new(), |_| {
Ok(Schema::Data(Data::Integer).into())
})
.unwrap();
// #/definitions/ByteArray
//
// {
// "dataType": "bytes"
// }
definitions
.register::<_, Error>(&builtins::byte_array(), &HashMap::new(), |_| {
Ok(Schema::Data(Data::Bytes).into())
})
.unwrap();
// #/definitions/Bool
//
// {
// "anyOf": [
// {
// "dataType": "constructor",
// "index": 0,
// "fields": []
// },
// {
// "dataType": "constructor",
// "index": 1,
// "fields": []
// },
// ]
// }
definitions.insert(
&Reference::new("Bool"),
Schema::Data(Data::AnyOf(vec![
// False
Constructor {
index: 0,
fields: vec![],
}
.into(),
// True
Constructor {
index: 1,
fields: vec![],
}
.into(),
]))
.into(),
);
definitions
}
#[test]
fn mint_basic() {
assert_validator(
@ -1096,4 +1160,266 @@ mod test {
}),
)
}
#[test]
fn validate_arguments_integer() {
let definitions = fixture_definitions();
let term = Term::data(uplc::Data::integer(42.into()));
let param = Parameter {
title: None,
schema: Reference::new("Int"),
};
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_bytestring() {
let definitions = fixture_definitions();
let term = Term::data(uplc::Data::bytestring(vec![102, 111, 111]));
let param = Parameter {
title: None,
schema: Reference::new("ByteArray"),
};
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_list_inline() {
let schema = Reference::new("List$Int");
// #/definitions/List$Int
//
// {
// "dataType": "list",
// "items": { "dataType": "integer" }
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::List(Items::One(Declaration::Inline(Box::new(
Data::Integer,
)))))
.into(),
);
let term = Term::data(uplc::Data::list(vec![
uplc::Data::integer(42.into()),
uplc::Data::integer(14.into()),
]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_list_ref() {
let schema = Reference::new("List$ByteArray");
// #/definitions/List$ByteArray
//
// {
// "dataType": "list",
// "items": { "$ref": "#/definitions/ByteArray" }
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::List(Items::One(Declaration::Referenced(
Reference::new("ByteArray"),
))))
.into(),
);
let term = Term::data(uplc::Data::list(vec![uplc::Data::bytestring(vec![
102, 111, 111,
])]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_tuple() {
let schema = Reference::new("Tuple$Int_ByteArray");
// #/definitions/Tuple$Int_ByteArray
//
// {
// "dataType": "list",
// "items": [
// { "$ref": "#/definitions/Int" }
// { "$ref": "#/definitions/ByteArray" }
// ]
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::List(Items::Many(vec![
Declaration::Referenced(Reference::new("Int")),
Declaration::Referenced(Reference::new("ByteArray")),
])))
.into(),
);
let term = Term::data(uplc::Data::list(vec![
uplc::Data::integer(42.into()),
uplc::Data::bytestring(vec![102, 111, 111]),
]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_dict() {
let schema = Reference::new("Dict$ByteArray_Int");
// #/definitions/Dict$Int_ByteArray
//
// {
// "dataType": "map",
// "keys": { "dataType": "bytes" },
// "values": { "dataType": "integer" }
// }
let mut definitions = fixture_definitions();
definitions.insert(
&Reference::new("Dict$ByteArray_Int"),
Schema::Data(Data::Map(
Declaration::Inline(Box::new(Data::Bytes)),
Declaration::Inline(Box::new(Data::Integer)),
))
.into(),
);
let term = Term::data(uplc::Data::map(vec![(
uplc::Data::bytestring(vec![102, 111, 111]),
uplc::Data::integer(42.into()),
)]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_constr_nullary() {
let schema = Reference::new("Bool");
let definitions = fixture_definitions();
let term = Term::data(uplc::Data::constr(1, vec![]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_constr_n_ary() {
let schema = Reference::new("Foo");
// #/definitions/Foo
//
// {
// "anyOf": [
// {
// "dataType": "constructor",
// "index": 0,
// "fields": [{
// "$ref": "#/definitions/Bool
// }]
// },
// ]
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::AnyOf(vec![Constructor {
index: 0,
fields: vec![Declaration::Referenced(Reference::new("Bool")).into()],
}
.into()]))
.into(),
);
let term = Term::data(uplc::Data::constr(0, vec![uplc::Data::constr(0, vec![])]));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
#[test]
fn validate_arguments_constr_recursive() {
let schema = Reference::new("LinkedList$Int");
// #/definitions/LinkedList$Int
//
// {
// "anyOf": [
// {
// "dataType": "constructor",
// "index": 0,
// "fields": []
// },
// {
// "dataType": "constructor",
// "index": 1,
// "fields": [{
// "$ref": "#/definitions/Int
// "$ref": "#/definitions/LinkedList$Int
// }]
// },
// ]
// }
let mut definitions = fixture_definitions();
definitions.insert(
&schema,
Schema::Data(Data::AnyOf(vec![
// Empty
Constructor {
index: 0,
fields: vec![],
}
.into(),
// Node
Constructor {
index: 1,
fields: vec![
Declaration::Referenced(Reference::new("Int")).into(),
Declaration::Referenced(Reference::new("LinkedList$Int")).into(),
],
}
.into(),
]))
.into(),
);
let term = Term::data(uplc::Data::constr(
1,
vec![
uplc::Data::integer(14.into()),
uplc::Data::constr(
1,
vec![
uplc::Data::integer(42.into()),
uplc::Data::constr(0, vec![]),
],
),
],
));
let param: Parameter = schema.into();
assert!(matches!(param.validate(&definitions, &term), Ok { .. }))
}
}

View File

@ -12,11 +12,7 @@ pub mod pretty;
pub mod script;
pub mod telemetry;
use crate::blueprint::{
definitions::Reference,
schema::{Annotated, Schema},
Blueprint,
};
use crate::blueprint::Blueprint;
use aiken_lang::{
ast::{Definition, Function, ModuleKind, Tracing, TypedDataType, TypedFunction},
builtins,
@ -218,10 +214,7 @@ where
self.compile(options)
}
pub fn dump_uplc(
&self,
blueprint: &Blueprint<Reference, Annotated<Schema>>,
) -> Result<(), Error> {
pub fn dump_uplc(&self, blueprint: &Blueprint) -> Result<(), Error> {
let dir = self.root.join("artifacts");
self.event_listener
@ -362,8 +355,7 @@ where
// Read blueprint
let blueprint = File::open(self.blueprint_path())
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
let blueprint: Blueprint<serde_json::Value, serde_json::Value> =
serde_json::from_reader(BufReader::new(blueprint))?;
let blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?;
// Calculate the address
let when_too_many =
@ -386,12 +378,11 @@ where
&self,
title: Option<&String>,
param: &Term<DeBruijn>,
) -> Result<Blueprint<serde_json::Value, serde_json::Value>, Error> {
) -> Result<Blueprint, Error> {
// Read blueprint
let blueprint = File::open(self.blueprint_path())
.map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
let mut blueprint: Blueprint<serde_json::Value, serde_json::Value> =
serde_json::from_reader(BufReader::new(blueprint))?;
let mut blueprint: Blueprint = serde_json::from_reader(BufReader::new(blueprint))?;
// Apply parameters
let when_too_many =
@ -400,7 +391,9 @@ where
let applied_validator =
blueprint.with_validator(title, when_too_many, when_missing, |validator| {
validator.apply(param).map_err(|e| e.into())
validator
.apply(&blueprint.definitions, param)
.map_err(|e| e.into())
})?;
// Overwrite validator

View File

@ -1,18 +1,22 @@
use crate::with_project;
use aiken_project::error::Error;
use miette::IntoDiagnostic;
use std::{fs, path::PathBuf};
use uplc::{
ast::{DeBruijn, Term},
parser,
};
use aiken_project::{blueprint, error::Error};
use owo_colors::{OwoColorize, Stream::Stderr};
use std::{fs, path::PathBuf, process, rc::Rc};
use uplc::ast::{Constant, DeBruijn, Term};
/// Apply a parameter to a parameterized validator.
#[derive(clap::Args)]
pub struct Args {
/// The parameter, as a Plutus Data (CBOR, hex-encoded)
parameter: String,
/// Path to project
directory: Option<PathBuf>,
/// Output file. Optional, print on stdout when omitted.
#[clap(short, long)]
out: Option<PathBuf>,
/// Name of the validator's module within the project. Optional if there's only one validator.
#[clap(short, long)]
module: Option<String>,
@ -20,23 +24,58 @@ pub struct Args {
/// Name of the validator within the module. Optional if there's only one validator.
#[clap(short, long)]
validator: Option<String>,
/// The parameter, using high-level UPLC-syntax
parameter: String,
}
pub fn exec(
Args {
parameter,
directory,
out,
module,
validator,
parameter,
}: Args,
) -> miette::Result<()> {
let term: Term<DeBruijn> = parser::term(&parameter)
.into_diagnostic()?
.try_into()
.into_diagnostic()?;
eprintln!(
"{} inputs",
" Parsing"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
);
let bytes = hex::decode(parameter)
.map_err::<Error, _>(|e| {
blueprint::error::Error::MalformedParameter {
hint: format!("Invalid hex-encoded string: {e}"),
}
.into()
})
.unwrap_or_else(|e| {
println!();
e.report();
process::exit(1)
});
let data = uplc::plutus_data(&bytes)
.map_err::<Error, _>(|e| {
blueprint::error::Error::MalformedParameter {
hint: format!("Invalid Plutus data; malformed CBOR encoding: {e}"),
}
.into()
})
.unwrap_or_else(|e| {
println!();
e.report();
process::exit(1)
});
let term: Term<DeBruijn> = Term::Constant(Rc::new(Constant::Data(data)));
eprintln!(
"{} blueprint",
" Analyzing"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
);
with_project(directory, |p| {
let title = module.as_ref().map(|m| {
@ -51,16 +90,35 @@ pub fn exec(
let title = title.as_ref().or(validator.as_ref());
eprintln!(
"{} parameter",
" Applying"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
);
let blueprint = p.apply_parameter(title, &term)?;
let json = serde_json::to_string_pretty(&blueprint).unwrap();
fs::write(p.blueprint_path(), json).map_err(|error| {
Error::FileIo {
match out {
None => {
println!("\n{}\n", json);
Ok(())
}
Some(ref path) => fs::write(path, json).map_err(|error| Error::FileIo {
error,
path: p.blueprint_path(),
}
.into()
})
}),
}?;
eprintln!(
"{}",
" Done"
.if_supports_color(Stderr, |s| s.purple())
.if_supports_color(Stderr, |s| s.bold()),
);
Ok(())
})
}

View File

@ -65,7 +65,7 @@ pub fn exec(
.map_err(|_| BlueprintError::InvalidOrMissingFile)
.into_diagnostic()?;
let blueprint: Blueprint<serde_json::Value, serde_json::Value> =
let blueprint: Blueprint =
serde_json::from_reader(BufReader::new(blueprint)).into_diagnostic()?;
// Perform the conversion

View File

@ -1,23 +1,3 @@
use std::{
fmt::{self, Display},
hash::{self, Hash},
rc::Rc,
};
use num_bigint::BigInt;
use serde::{
self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer},
};
use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_primitives::{
alonzo::PlutusData,
babbage::{self as cardano, Language},
};
use pallas_traverse::ComputeHash;
use crate::{
builtins::DefaultFunction,
debruijn::{self, Converter},
@ -28,6 +8,24 @@ use crate::{
Machine,
},
};
use num_bigint::BigInt;
use num_traits::ToPrimitive;
use pallas_addresses::{Network, ShelleyAddress, ShelleyDelegationPart, ShelleyPaymentPart};
use pallas_primitives::{
alonzo::{self as pallas, Constr, PlutusData},
babbage::{self as cardano, Language},
};
use pallas_traverse::ComputeHash;
use serde::{
self,
de::{self, Deserialize, Deserializer, MapAccess, Visitor},
ser::{Serialize, SerializeStruct, Serializer},
};
use std::{
fmt::{self, Display},
hash::{self, Hash},
rc::Rc,
};
/// This represents a program in Untyped Plutus Core.
/// A program contains a version tuple and a term.
@ -239,6 +237,61 @@ pub enum Constant {
Data(PlutusData),
}
pub struct Data {}
// TODO: See about moving these builders upstream to Pallas?
impl Data {
pub fn integer(i: BigInt) -> PlutusData {
match i.to_i64() {
Some(i) => PlutusData::BigInt(pallas::BigInt::Int(i.into())),
None => {
let (sign, bytes) = i.to_bytes_be();
match sign {
num_bigint::Sign::Minus => {
PlutusData::BigInt(pallas::BigInt::BigNInt(bytes.into()))
}
_ => PlutusData::BigInt(pallas::BigInt::BigUInt(bytes.into())),
}
}
}
}
pub fn bytestring(bytes: Vec<u8>) -> PlutusData {
PlutusData::BoundedBytes(bytes.into())
}
pub fn map(kvs: Vec<(PlutusData, PlutusData)>) -> PlutusData {
PlutusData::Map(kvs.into())
}
pub fn list(xs: Vec<PlutusData>) -> PlutusData {
PlutusData::Array(xs)
}
pub fn constr(ix: u64, fields: Vec<PlutusData>) -> PlutusData {
// NOTE: see https://github.com/input-output-hk/plutus/blob/9538fc9829426b2ecb0628d352e2d7af96ec8204/plutus-core/plutus-core/src/PlutusCore/Data.hs#L139-L155
if ix < 7 {
PlutusData::Constr(Constr {
tag: 121 + ix,
any_constructor: None,
fields,
})
} else if ix < 128 {
PlutusData::Constr(Constr {
tag: 1280 + ix - 7,
any_constructor: None,
fields,
})
} else {
PlutusData::Constr(Constr {
tag: 102,
any_constructor: Some(ix),
fields,
})
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Type {
Bool,

View File

@ -2,13 +2,14 @@ use crate::{
ast::{Constant, Name, Term, Type},
builtins::DefaultFunction,
};
use pallas_primitives::alonzo::PlutusData;
pub const CONSTR_FIELDS_EXPOSER: &str = "__constr_fields_exposer";
pub const CONSTR_INDEX_EXPOSER: &str = "__constr_index_exposer";
pub const CONSTR_GET_FIELD: &str = "__constr_get_field";
pub const EXPECT_ON_LIST: &str = "__expect_on_list";
impl Term<Name> {
impl<T> Term<T> {
pub fn apply(self, arg: Self) -> Self {
Term::Apply {
function: self.into(),
@ -16,13 +17,6 @@ impl Term<Name> {
}
}
pub fn lambda(self, parameter_name: impl ToString) -> Self {
Term::Lambda {
parameter_name: Name::text(parameter_name).into(),
body: self.into(),
}
}
pub fn force(self) -> Self {
Term::Force(self.into())
}
@ -31,10 +25,6 @@ impl Term<Name> {
Term::Delay(self.into())
}
pub fn var(name: impl ToString) -> Self {
Term::Var(Name::text(name).into())
}
pub fn integer(i: num_bigint::BigInt) -> Self {
Term::Constant(Constant::Integer(i).into())
}
@ -55,6 +45,10 @@ impl Term<Name> {
Term::Constant(Constant::Unit.into())
}
pub fn data(d: PlutusData) -> Self {
Term::Constant(Constant::Data(d).into())
}
pub fn empty_list() -> Self {
Term::Constant(Constant::ProtoList(Type::Data, vec![]).into())
}
@ -204,7 +198,7 @@ impl Term<Name> {
.force()
}
pub fn trace(self, msg_term: Term<Name>) -> Self {
pub fn trace(self, msg_term: Self) -> Self {
Term::Builtin(DefaultFunction::Trace)
.force()
.apply(msg_term)
@ -212,41 +206,34 @@ impl Term<Name> {
.force()
}
pub fn assert_on_list(self) -> Term<Name> {
self.lambda(EXPECT_ON_LIST.to_string())
.apply(
Term::var(EXPECT_ON_LIST.to_string()).apply(Term::var(EXPECT_ON_LIST.to_string())),
)
.lambda(EXPECT_ON_LIST.to_string())
.apply(
Term::var("__list_to_check".to_string())
.delayed_choose_list(
Term::unit(),
Term::var("__check_with".to_string())
.apply(
Term::head_list().apply(Term::var("__list_to_check".to_string())),
)
.choose_unit(
Term::var(EXPECT_ON_LIST.to_string())
.apply(Term::var(EXPECT_ON_LIST.to_string()))
.apply(
Term::tail_list()
.apply(Term::var("__list_to_check".to_string())),
)
.apply(Term::var("__check_with".to_string())),
),
)
.lambda("__check_with".to_string())
.lambda("__list_to_check".to_string())
.lambda(EXPECT_ON_LIST),
)
}
pub fn final_wrapper(self: Term<Name>) -> Term<Name> {
pub fn final_wrapper(self) -> Self {
self.delayed_if_else(Term::unit(), Term::Error)
}
pub fn constr_fields_exposer(self: Term<Name>) -> Term<Name> {
pub fn repeat_tail_list(self, repeat: usize) -> Self {
let mut term = self;
for _ in 0..repeat {
term = Term::tail_list().apply(term);
}
term
}
}
impl Term<Name> {
pub fn lambda(self, parameter_name: impl ToString) -> Self {
Term::Lambda {
parameter_name: Name::text(parameter_name).into(),
body: self.into(),
}
}
pub fn var(name: impl ToString) -> Self {
Term::Var(Name::text(name).into())
}
pub fn constr_fields_exposer(self) -> Self {
self.lambda(CONSTR_FIELDS_EXPOSER.to_string()).apply(
Term::snd_pair()
.apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string())))
@ -254,7 +241,7 @@ impl Term<Name> {
)
}
pub fn constr_index_exposer(self: Term<Name>) -> Term<Name> {
pub fn constr_index_exposer(self) -> Self {
self.lambda(CONSTR_INDEX_EXPOSER.to_string()).apply(
Term::fst_pair()
.apply(Term::unconstr_data().apply(Term::var("__constr_var".to_string())))
@ -262,7 +249,7 @@ impl Term<Name> {
)
}
pub fn constr_get_field(self: Term<Name>) -> Term<Name> {
pub fn constr_get_field(self) -> Self {
self.lambda(CONSTR_GET_FIELD.to_string())
.apply(
Term::var(CONSTR_GET_FIELD.to_string())
@ -298,13 +285,33 @@ impl Term<Name> {
)
}
pub fn repeat_tail_list(self: Term<Name>, repeat: usize) -> Term<Name> {
let mut term = self;
for _ in 0..repeat {
term = Term::tail_list().apply(term);
}
term
pub fn assert_on_list(self) -> Self {
self.lambda(EXPECT_ON_LIST.to_string())
.apply(
Term::var(EXPECT_ON_LIST.to_string()).apply(Term::var(EXPECT_ON_LIST.to_string())),
)
.lambda(EXPECT_ON_LIST.to_string())
.apply(
Term::var("__list_to_check".to_string())
.delayed_choose_list(
Term::unit(),
Term::var("__check_with".to_string())
.apply(
Term::head_list().apply(Term::var("__list_to_check".to_string())),
)
.choose_unit(
Term::var(EXPECT_ON_LIST.to_string())
.apply(Term::var(EXPECT_ON_LIST.to_string()))
.apply(
Term::tail_list()
.apply(Term::var("__list_to_check".to_string())),
)
.apply(Term::var("__check_with".to_string())),
),
)
.lambda("__check_with".to_string())
.lambda("__list_to_check".to_string())
.lambda(EXPECT_ON_LIST),
)
}
}