diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..161a60f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +build +.VSCodeCounter +plutus.json diff --git a/README.md b/README.md index 9e34bea..b56b059 100644 --- a/README.md +++ b/README.md @@ -1 +1,13 @@ -# aiken-linked-list \ No newline at end of file +# aiken-linked-list + +## Building + +```sh +aiken build +``` + +To run all tests, simply do: + +```sh +aiken check +``` diff --git a/aiken.lock b/aiken.lock new file mode 100644 index 0000000..0a72eb3 --- /dev/null +++ b/aiken.lock @@ -0,0 +1,15 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "1.7.0" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "1.7.0" +requirements = [] +source = "github" + +[etags] diff --git a/aiken.toml b/aiken.toml new file mode 100644 index 0000000..f82ae48 --- /dev/null +++ b/aiken.toml @@ -0,0 +1,14 @@ +name = "aiken-lang/aiken-linked-list" +version = "0.0.0" +license = "Apache-2.0" +description = "Aiken contracts for project 'aiken-lang/aiken-linked-list'" + +[repository] +user = "aiken-lang" +project = "aiken-linked-list" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "1.7.0" +source = "github" diff --git a/lib/linkedlist/constants.ak b/lib/linkedlist/constants.ak new file mode 100644 index 0000000..32b2f1f --- /dev/null +++ b/lib/linkedlist/constants.ak @@ -0,0 +1 @@ +pub const origin_node_token_name = "FSN" diff --git a/lib/linkedlist/linked_list.ak b/lib/linkedlist/linked_list.ak new file mode 100644 index 0000000..029d351 --- /dev/null +++ b/lib/linkedlist/linked_list.ak @@ -0,0 +1,110 @@ +use aiken/bytearray +use aiken/interval +use aiken/list +use aiken/transaction.{Output} +use aiken/transaction/value.{lovelace_of} +use linkedlist/constants +use linkedlist/types.{Common, Config, POSIXTime, PubKeyHash, SetNode} +use linkedlist/utils + +pub fn init(common: Common) -> Bool { + let must_spend_nodes = list.length(common.node_inputs) > 0 + let must_exactly_one_node_outputs = list.length(common.node_outputs) == 1 + let must_mint_correctly = + utils.validate_mint( + common.mint, + common.own_cs, + constants.origin_node_token_name, + 1, + ) + must_spend_nodes? && must_exactly_one_node_outputs? && must_mint_correctly? +} + +pub fn deinit(common: Common) -> Bool { + let must_spend_exactly_one_node_input = list.length(common.node_inputs) == 1 + let must_not_produce_node_output = list.length(common.node_outputs) == 0 + let must_burn_correctly = + utils.validate_mint( + common.mint, + common.own_cs, + constants.origin_node_token_name, + -1, + ) + must_spend_exactly_one_node_input? && must_not_produce_node_output? && must_burn_correctly? +} + +pub fn insert(common: Common, insert_key: PubKeyHash, node: SetNode) -> Bool { + let must_cover_inserting_key = utils.cover_key(node, insert_key) + expect [covering_node] = common.node_inputs + let prev_node_datum = utils.as_predecessor_of(node, insert_key) + let node_datum = utils.as_successor_of(insert_key, node) + let must_has_datum_in_output = + list.any( + common.node_outputs, + fn(node_pair) { node_datum == node_pair.node }, + ) + let must_correct_node_output = + list.any( + common.node_outputs, + fn(node_pair) { + covering_node.val == node_pair.val && prev_node_datum == node_pair.node + }, + ) + + let must_mint_correct = + utils.validate_mint( + common.mint, + common.own_cs, + bytearray.concat(constants.origin_node_token_name, insert_key), + 1, + ) + must_cover_inserting_key? && must_has_datum_in_output? && must_correct_node_output? && must_mint_correct? +} + +pub fn remove( + common: Common, + range: POSIXTime, + disc_config: Config, + outs: List, + sigs: List, + remove_key: PubKeyHash, + node: SetNode, +) -> Bool { + let must_cover_remove_key = utils.cover_key(node, remove_key) + let prev_node_datum = utils.as_predecessor_of(node, remove_key) + let node_datum = utils.as_successor_of(remove_key, node) + let must_spend_two_nodes = list.length(common.node_inputs) == 2 + expect Some(stay_node) = + list.find(common.node_inputs, fn(input) { prev_node_datum == input.node }) + expect Some(remove_node) = + list.find(common.node_inputs, fn(input) { node_datum == input.node }) + let remove_token_name = + bytearray.concat(constants.origin_node_token_name, remove_key) + let must_correct_node_output = + list.any( + common.node_outputs, + fn(node_pair) { stay_node.val == node_pair.val && node == node_pair.node }, + ) + let must_mint_correct = + utils.validate_mint(common.mint, common.own_cs, remove_token_name, -1) + let must_sign_by_user = list.has(sigs, remove_key) + let own_input_lovelace = lovelace_of(remove_node.val) + let own_input_fee = utils.div_ceil(own_input_lovelace, 4) + let disc_deadline = disc_config.deadline + let must_satisfy_removal_broke_phase_rules = + if + interval.is_entirely_after(interval.after(disc_deadline - 8_640_000), range){ + + True + } else { + list.any( + outs, + fn(out) { + out.address == disc_config.penalty_address && own_input_fee < lovelace_of( + out.value, + ) + }, + ) + } + must_cover_remove_key? && must_spend_two_nodes? && must_correct_node_output? && must_mint_correct? && must_sign_by_user? && must_satisfy_removal_broke_phase_rules? +} diff --git a/lib/linkedlist/types.ak b/lib/linkedlist/types.ak new file mode 100644 index 0000000..0b92a87 --- /dev/null +++ b/lib/linkedlist/types.ak @@ -0,0 +1,51 @@ +use aiken/hash.{Blake2b_224, Hash} +use aiken/transaction.{OutputReference} +use aiken/transaction/credential.{Address, VerificationKey} +use aiken/transaction/value.{AssetName, PolicyId, Value} + +/// A number of milliseconds since 00:00:00 UTC on 1 January 1970. +pub type POSIXTime = + Int + +pub type AssetClass { + policy_id: PolicyId, + asset_name: AssetName, +} + +pub type PubKeyHash = + Hash + +pub type Config { + init_utxo: OutputReference, + deadline: POSIXTime, + penalty_address: Address, +} + +pub type NodeKey { + Key { key: PubKeyHash } + Empty +} + +pub type SetNode { + key: NodeKey, + next: NodeKey, +} + +pub type NodePair { + val: Value, + node: SetNode, +} + +pub type Common { + own_cs: PolicyId, + mint: Value, + node_inputs: List, + node_outputs: List, +} + +pub type NodeAction { + Init + Deinit + Insert + Remove +} diff --git a/lib/linkedlist/utils.ak b/lib/linkedlist/utils.ak new file mode 100644 index 0000000..cae5eca --- /dev/null +++ b/lib/linkedlist/utils.ak @@ -0,0 +1,45 @@ +use aiken/bytearray +use aiken/dict +use aiken/transaction/value.{Value} +use linkedlist/types.{Empty, Key, PubKeyHash, SetNode} + +pub fn validate_mint( + mints: Value, + expected_minting_policy: ByteArray, + expected_minting_name: ByteArray, + expected_minting_amt: Int, +) -> Bool { + let mints_policy = dict.to_list(value.tokens(mints, expected_minting_policy)) + mints_policy == [(expected_minting_name, expected_minting_amt)] +} + +pub fn cover_key(node: SetNode, insert_key: PubKeyHash) -> Bool { + let less_than_key = + when node.key is { + Empty -> True + Key(key) -> bytearray.compare(key, insert_key) == Less + } + let more_than_key = + when node.next is { + Empty -> True + Key(key) -> bytearray.compare(key, insert_key) == Greater + } + less_than_key? && more_than_key? +} + +pub fn as_predecessor_of(node: SetNode, next_key: PubKeyHash) -> SetNode { + SetNode { key: node.key, next: Key(next_key) } +} + +pub fn as_successor_of(prev_key: PubKeyHash, node: SetNode) -> SetNode { + SetNode { key: Key(prev_key), next: node.next } +} + +pub fn div_ceil(a, b: Int) -> Int { + let div = a / b + let rem = a % b + when rem is { + 0 -> div + _ -> div + 1 + } +}