Merge pull request #335 from aiken-lang/blueprint-parameters

Blueprint parameters
This commit is contained in:
Matthias Benkort 2023-02-07 11:43:01 +01:00 committed by GitHub
commit 8feaefe073
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 298 additions and 44 deletions

View File

@ -27,6 +27,7 @@ pub enum Error {
source_code: NamedSource, source_code: NamedSource,
return_type: Arc<Type>, return_type: Arc<Type>,
}, },
#[error("A {} validator requires at least {} arguments.", name.purple().bold(), at_least.to_string().purple().bold())] #[error("A {} validator requires at least {} arguments.", name.purple().bold(), at_least.to_string().purple().bold())]
#[diagnostic(code("aiken::blueprint::invalid::arity"))] #[diagnostic(code("aiken::blueprint::invalid::arity"))]
WrongValidatorArity { WrongValidatorArity {
@ -37,12 +38,13 @@ pub enum Error {
#[source_code] #[source_code]
source_code: NamedSource, source_code: NamedSource,
}, },
#[error("{}", error)] #[error("{}", error)]
#[diagnostic(help("{}", error.help()))] #[diagnostic(help("{}", error.help()))]
#[diagnostic(code("aiken::blueprint::interface"))] #[diagnostic(code("aiken::blueprint::interface"))]
Schema { Schema {
error: schema::Error, error: schema::Error,
#[label("invalid contract's boundary")] #[label("invalid validator's boundary")]
location: Span, location: Span,
#[source_code] #[source_code]
source_code: NamedSource, source_code: NamedSource,
@ -52,6 +54,15 @@ pub enum Error {
#[diagnostic(code("aiken::blueprint::missing"))] #[diagnostic(code("aiken::blueprint::missing"))]
#[diagnostic(help("Did you forget to {build} the project?", build = "build".purple().bold()))] #[diagnostic(help("Did you forget to {build} the project?", build = "build".purple().bold()))]
InvalidOrMissingFile, InvalidOrMissingFile,
#[error("I didn't find any parameters to apply in the given validator.")]
#[diagnostic(code("aiken::blueprint::apply::no_parameters"))]
NoParametersToApply,
#[error("I couldn't compute the address of the given validator because it's parameterized by {} parameter(s)!", format!("{n}").purple())]
#[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 parmeters first.", keyword_spend = "spend".purple()))]
ParameterizedValidator { n: usize },
} }
pub fn assert_return_bool(module: &CheckedModule, def: &TypedFunction) -> Result<(), Error> { pub fn assert_return_bool(module: &CheckedModule, def: &TypedFunction) -> Result<(), Error> {

View File

@ -7,12 +7,12 @@ use aiken_lang::uplc::CodeGenerator;
use error::Error; use error::Error;
use schema::Schema; use schema::Schema;
use std::fmt::{self, Debug, Display}; use std::fmt::{self, Debug, Display};
use validator::Validator; use validator::{Purpose, Validator};
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Blueprint<T> { pub struct Blueprint<T: Default> {
pub preamble: Preamble, pub preamble: Preamble,
pub validators: Vec<validator::Validator<T>>, pub validators: Vec<Validator<T>>,
} }
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
@ -25,6 +25,12 @@ pub struct Preamble {
pub license: Option<String>, pub license: Option<String>,
} }
#[derive(Debug, PartialEq, Clone)]
pub enum LookupResult<'a, T> {
One(&'a T),
Many,
}
impl Blueprint<Schema> { impl Blueprint<Schema> {
pub fn new( pub fn new(
config: &Config, config: &Config,
@ -47,6 +53,59 @@ impl Blueprint<Schema> {
} }
} }
impl<T> Blueprint<T>
where
T: Clone + Default,
{
pub fn lookup(
&self,
title: Option<&String>,
purpose: Option<&Purpose>,
) -> Option<LookupResult<Validator<T>>> {
let mut validator = None;
for v in self.validators.iter() {
let match_title = Some(&v.title) == title.or(Some(&v.title));
let match_purpose = Some(&v.purpose) == purpose.or(Some(&v.purpose));
if match_title && match_purpose {
validator = Some(if validator.is_none() {
LookupResult::One(v)
} else {
LookupResult::Many
})
}
}
validator
}
pub fn with_validator<F, A, E>(
&self,
title: Option<&String>,
purpose: Option<&Purpose>,
when_missing: fn(Vec<(String, Purpose)>) -> E,
when_too_many: fn(Vec<(String, Purpose)>) -> E,
action: F,
) -> Result<A, E>
where
F: Fn(Validator<T>) -> Result<A, E>,
{
match self.lookup(title, purpose) {
Some(LookupResult::One(validator)) => action(validator.to_owned()),
Some(LookupResult::Many) => Err(when_too_many(
self.validators
.iter()
.map(|v| (v.title.clone(), v.purpose.clone()))
.collect(),
)),
None => Err(when_missing(
self.validators
.iter()
.map(|v| (v.title.clone(), v.purpose.clone()))
.collect(),
)),
}
}
}
impl Display for Blueprint<Schema> { impl Display for Blueprint<Schema> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?; let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?;

View File

@ -302,6 +302,12 @@ impl Data {
} }
} }
impl Default for Schema {
fn default() -> Self {
Schema::Unit
}
}
impl Display for Schema { impl Display for Schema {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?; let s = serde_json::to_string_pretty(self).map_err(|_| fmt::Error)?;

View File

@ -10,7 +10,7 @@ use std::{
collections::HashMap, collections::HashMap,
fmt::{self, Display}, fmt::{self, Display},
}; };
use uplc::ast::{DeBruijn, Program}; use uplc::ast::{DeBruijn, Program, Term};
#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub struct Validator<T> { pub struct Validator<T> {
@ -21,6 +21,9 @@ pub struct Validator<T> {
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub datum: Option<Annotated<T>>, pub datum: Option<Annotated<T>>,
pub redeemer: Annotated<T>, pub redeemer: Annotated<T>,
#[serde(skip_serializing_if = "Vec::is_empty")]
#[serde(default)]
pub parameters: Vec<Annotated<T>>,
#[serde(flatten)] #[serde(flatten)]
pub program: Program<DeBruijn>, pub program: Program<DeBruijn>,
} }
@ -58,12 +61,39 @@ impl Validator<Schema> {
assert_min_arity(validator, def, purpose.min_arity())?; assert_min_arity(validator, def, purpose.min_arity())?;
let mut args = def.arguments.iter().rev(); let mut args = def.arguments.iter().rev();
let (_, redeemer, datum) = (args.next(), args.next().unwrap(), args.next()); let (_, redeemer) = (args.next(), args.next().unwrap());
let datum = if purpose.min_arity() > 2 {
args.next()
} else {
None
};
Ok(Validator { Ok(Validator {
title: validator.name.clone(), title: validator.name.clone(),
description: None, description: None,
purpose, purpose,
parameters: args
.rev()
.map(|param| {
let annotation =
Annotated::from_type(modules.into(), &param.tipo, &HashMap::new()).map_err(
|error| Error::Schema {
error,
location: param.location,
source_code: NamedSource::new(
validator.input_path.display().to_string(),
validator.code.clone(),
),
},
);
annotation.map(|mut annotation| {
annotation.title = annotation
.title
.or_else(|| Some(param.arg_name.get_label()));
annotation
})
})
.collect::<Result<_, _>>()?,
datum: datum datum: datum
.map(|datum| { .map(|datum| {
Annotated::from_type(modules.into(), &datum.tipo, &HashMap::new()).map_err( Annotated::from_type(modules.into(), &datum.tipo, &HashMap::new()).map_err(
@ -95,6 +125,25 @@ impl Validator<Schema> {
} }
} }
impl<T> Validator<T>
where
T: Clone,
{
pub fn apply(self, 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.
Ok(Self {
program: self.program.apply_term(arg),
parameters: tail.to_vec(),
..self
})
}
}
}
}
impl Purpose { impl Purpose {
pub fn min_arity(&self) -> u8 { pub fn min_arity(&self) -> u8 {
match self { match self {
@ -272,6 +321,32 @@ mod test {
); );
} }
#[test]
fn validator_mint_parameterized() {
assert_validator(
r#"
fn mint(utxo_ref: Int, redeemer: Data, ctx: Data) {
True
}
"#,
json!({
"title": "test_module",
"purpose": "mint",
"hash": "455f24922a520c59499fdafad95e1272fab81a99452f6b9545f95337",
"parameters": [{
"title": "utxo_ref",
"dataType": "integer"
}],
"redeemer": {
"title": "Data",
"description": "Any Plutus data."
},
"compiledCode": "4d01000022253335734944526161"
}),
);
}
#[test] #[test]
fn validator_spend() { fn validator_spend() {
assert_validator( assert_validator(

View File

@ -37,7 +37,7 @@ use std::{
}; };
use telemetry::EventListener; use telemetry::EventListener;
use uplc::{ use uplc::{
ast::{Constant, Term}, ast::{Constant, DeBruijn, Term},
machine::cost_model::ExBudget, machine::cost_model::ExBudget,
}; };
@ -283,8 +283,8 @@ where
pub fn address( pub fn address(
&self, &self,
with_title: Option<&String>, title: Option<&String>,
with_purpose: Option<&validator::Purpose>, purpose: Option<&validator::Purpose>,
stake_address: Option<&String>, stake_address: Option<&String>,
) -> Result<ShelleyAddress, Error> { ) -> Result<ShelleyAddress, Error> {
// Parse stake address // Parse stake address
@ -306,44 +306,64 @@ where
}; };
// Read blueprint // Read blueprint
let filepath = self.blueprint_path(); let blueprint = File::open(self.blueprint_path())
let blueprint = .map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
File::open(filepath).map_err(|_| blueprint::error::Error::InvalidOrMissingFile)?;
let blueprint: Blueprint<serde_json::Value> = let blueprint: Blueprint<serde_json::Value> =
serde_json::from_reader(BufReader::new(blueprint))?; serde_json::from_reader(BufReader::new(blueprint))?;
// Find validator's program // Calculate the address
let mut program = None; let when_too_many =
for v in blueprint.validators.iter() { |known_validators| Error::MoreThanOneValidatorFound { known_validators };
if Some(&v.title) == with_title.or(Some(&v.title)) let when_missing = |known_validators| Error::NoValidatorNotFound { known_validators };
&& Some(&v.purpose) == with_purpose.or(Some(&v.purpose)) blueprint.with_validator(title, purpose, when_too_many, when_missing, |validator| {
{ let n = validator.parameters.len();
program = Some(if program.is_none() { if n > 0 {
Ok(v.program.clone()) Err(blueprint::error::Error::ParameterizedValidator { n }.into())
} else { } else {
Err(Error::MoreThanOneValidatorFound { Ok(validator
known_validators: blueprint .program
.validators .address(Network::Testnet, delegation_part.to_owned()))
.iter()
.map(|v| (v.title.clone(), v.purpose.clone()))
.collect(),
})
})
} }
})
} }
// Print the address pub fn apply_parameter(
match program { &self,
Some(Ok(program)) => Ok(program.address(Network::Testnet, delegation_part)), title: Option<&String>,
Some(Err(e)) => Err(e), purpose: Option<&validator::Purpose>,
None => Err(Error::NoValidatorNotFound { param: &Term<DeBruijn>,
known_validators: blueprint ) -> Result<Blueprint<serde_json::Value>, 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::from_reader(BufReader::new(blueprint))?;
// Apply parameters
let when_too_many =
|known_validators| Error::MoreThanOneValidatorFound { known_validators };
let when_missing = |known_validators| Error::NoValidatorNotFound { known_validators };
let applied_validator =
blueprint.with_validator(title, purpose, when_too_many, when_missing, |validator| {
validator.apply(param).map_err(|e| e.into())
})?;
// Overwrite validator
blueprint.validators = blueprint
.validators .validators
.iter() .into_iter()
.map(|v| (v.title.clone(), v.purpose.clone())) .map(|validator| {
.collect(), let same_title = validator.title == applied_validator.title;
}), let same_purpose = validator.purpose == applied_validator.purpose;
if same_title && same_purpose {
applied_validator.to_owned()
} else {
validator
} }
})
.collect();
Ok(blueprint)
} }
fn compile_deps(&mut self) -> Result<(), Error> { fn compile_deps(&mut self) -> Result<(), Error> {

View File

@ -0,0 +1,59 @@
use crate::with_project;
use aiken_lang::VALIDATOR_NAMES;
use aiken_project::error::Error;
use miette::IntoDiagnostic;
use std::{fs, path::PathBuf};
use uplc::{
ast::{DeBruijn, Term},
parser,
};
#[derive(clap::Args)]
#[clap(setting(clap::AppSettings::DeriveDisplayOrder))]
/// Apply a parameter to a parameterized validator.
pub struct Args {
/// Path to project
directory: Option<PathBuf>,
/// Name of the validator's module within the project. Optional if there's only one validator.
#[clap(short, long)]
validator: Option<String>,
/// Purpose of the validator within the module. Optional if there's only one validator.
#[clap(short, long, possible_values=&VALIDATOR_NAMES)]
purpose: Option<String>,
/// The parameter, using high-level UPLC-syntax
parameter: String,
}
pub fn exec(
Args {
directory,
validator,
purpose,
parameter,
}: Args,
) -> miette::Result<()> {
let term: Term<DeBruijn> = parser::term(&parameter)
.into_diagnostic()?
.try_into()
.into_diagnostic()?;
with_project(directory, |p| {
let blueprint = p.apply_parameter(
validator.as_ref(),
purpose
.as_ref()
.map(|p| p.clone().try_into().unwrap())
.as_ref(),
&term,
)?;
let json = serde_json::to_string_pretty(&blueprint).unwrap();
fs::write(p.blueprint_path(), json).map_err(|error| Error::FileIo {
error,
path: p.blueprint_path(),
})
})
}

View File

@ -0,0 +1,19 @@
pub mod address;
pub mod apply;
use clap::Subcommand;
/// Commands for working with Plutus blueprints
#[derive(Subcommand)]
#[clap(setting(clap::AppSettings::DeriveDisplayOrder))]
pub enum Cmd {
Address(address::Args),
Apply(apply::Args),
}
pub fn exec(cmd: Cmd) -> miette::Result<()> {
match cmd {
Cmd::Address(args) => address::exec(args),
Cmd::Apply(args) => apply::exec(args),
}
}

View File

@ -1,4 +1,4 @@
pub mod address; pub mod blueprint;
pub mod build; pub mod build;
pub mod check; pub mod check;
pub mod docs; pub mod docs;

View File

@ -1,5 +1,6 @@
use aiken::cmd::{ use aiken::cmd::{
address, build, check, docs, fmt, lsp, new, blueprint::{self, address},
build, check, docs, fmt, lsp, new,
packages::{self, add}, packages::{self, add},
tx, uplc, tx, uplc,
}; };
@ -19,6 +20,9 @@ pub enum Cmd {
Docs(docs::Args), Docs(docs::Args),
Add(add::Args), Add(add::Args),
#[clap(subcommand)]
Blueprint(blueprint::Cmd),
#[clap(subcommand)] #[clap(subcommand)]
Packages(packages::Cmd), Packages(packages::Cmd),
@ -48,6 +52,7 @@ fn main() -> miette::Result<()> {
Cmd::Check(args) => check::exec(args), Cmd::Check(args) => check::exec(args),
Cmd::Docs(args) => docs::exec(args), Cmd::Docs(args) => docs::exec(args),
Cmd::Add(args) => add::exec(args), Cmd::Add(args) => add::exec(args),
Cmd::Blueprint(args) => blueprint::exec(args),
Cmd::Packages(args) => packages::exec(args), Cmd::Packages(args) => packages::exec(args),
Cmd::Lsp(args) => lsp::exec(args), Cmd::Lsp(args) => lsp::exec(args),
Cmd::Tx(sub_cmd) => tx::exec(sub_cmd), Cmd::Tx(sub_cmd) => tx::exec(sub_cmd),