diff --git a/lib/linkedlist/constants.ak b/lib/linkedlist/constants.ak index 32b2f1f..5871f7a 100644 --- a/lib/linkedlist/constants.ak +++ b/lib/linkedlist/constants.ak @@ -1 +1,3 @@ pub const origin_node_token_name = "FSN" + +pub const set_node_prefix = "FSN" diff --git a/lib/linkedlist/linked_list.ak b/lib/linkedlist/linked_list.ak index 227d5aa..fad36cd 100644 --- a/lib/linkedlist/linked_list.ak +++ b/lib/linkedlist/linked_list.ak @@ -1,5 +1,5 @@ use aiken/bytearray -use aiken/interval +use aiken/interval.{Interval} use aiken/list use aiken/transaction.{Output} use aiken/transaction/value.{lovelace_of} @@ -8,7 +8,7 @@ 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_spend_nodes = list.length(common.node_inputs) == 0 let must_exactly_one_node_output = list.length(common.node_outputs) == 1 let must_mint_correctly = utils.validate_mint( @@ -63,7 +63,7 @@ pub fn insert(common: Common, insert_key: PubKeyHash, node: SetNode) -> Bool { pub fn remove( common: Common, - range: POSIXTime, + range: Interval, disc_config: Config, outs: List, sigs: List, @@ -92,9 +92,7 @@ pub fn remove( 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){ - + if interval.is_entirely_before(range, disc_deadline) { True } else { list.any( diff --git a/lib/linkedlist/types.ak b/lib/linkedlist/types.ak index 0b92a87..b3d9c9b 100644 --- a/lib/linkedlist/types.ak +++ b/lib/linkedlist/types.ak @@ -46,6 +46,6 @@ pub type Common { pub type NodeAction { Init Deinit - Insert - Remove + Insert { key_to_insert: PubKeyHash, covering_node: SetNode } + Remove { key_to_remove: PubKeyHash, covering_node: SetNode } } diff --git a/lib/linkedlist/utils.ak b/lib/linkedlist/utils.ak index cae5eca..062a91b 100644 --- a/lib/linkedlist/utils.ak +++ b/lib/linkedlist/utils.ak @@ -1,7 +1,17 @@ use aiken/bytearray -use aiken/dict -use aiken/transaction/value.{Value} -use linkedlist/types.{Empty, Key, PubKeyHash, SetNode} +use aiken/dict.{has_key} +use aiken/interval.{Interval} +use aiken/list +use aiken/transaction.{ + InlineDatum, Input, Mint, Output, ScriptContext, Transaction, +} +use aiken/transaction/value.{ + AssetName, PolicyId, Value, flatten, from_minted_value, to_dict, tokens, +} +use linkedlist/constants.{set_node_prefix} +use linkedlist/types.{ + Common, Empty, Key, NodePair, POSIXTime, PubKeyHash, SetNode, +} pub fn validate_mint( mints: Value, @@ -43,3 +53,146 @@ pub fn div_ceil(a, b: Int) -> Int { _ -> div + 1 } } + +pub fn make_common( + ctx: ScriptContext, +) -> (Common, List, List, List, Interval) { + expect ScriptContext { + transaction: Transaction { + inputs, + outputs, + mint, + validity_range, + extra_signatories, + .. + }, + purpose: Mint(own_cs), + } = ctx + let in_outputs = get_outputs(inputs) + let from_node_val = only_at_node_val(in_outputs, own_cs) + let to_node_val = only_at_node_val(outputs, own_cs) + expect Some(head) = list.head(list.concat(from_node_val, to_node_val)) + let Output { address: node_address, .. } = head + expect + from_node_val + |> list.concat(to_node_val) + |> list.reduce( + True, + fn(acc, cur_node) { + let Output { address: cur_address, .. } = cur_node + and { + cur_address == node_address, + acc, + } + }, + ) + let node_inputs = list.map(from_node_val, node_input_utxo_datum_unsafe) + let node_outputs = + list.map(to_node_val, fn(node) { parse_node_output_utxo(own_cs, node) }) + let common = + Common { own_cs, mint: from_minted_value(mint), node_inputs, node_outputs } + (common, inputs, outputs, extra_signatories, validity_range) +} + +// Checks if a Currency Symbol is held within a Value +pub fn has_data_cs(cs: PolicyId, value: Value) -> Bool { + value + |> to_dict() + |> has_key(cs) +} + +pub fn get_outputs(inputs: List) -> List { + list.map( + inputs, + fn(input) { + let Input { output, .. } = input + output + }, + ) +} + +pub fn only_at_node_val(outputs: List, cs: PolicyId) -> List { + outputs + |> list.filter( + fn(output) { + let Output { value, .. } = output + has_data_cs(cs, value) + }, + ) +} + +pub fn node_input_utxo_datum_unsafe(output: Output) -> NodePair { + expect Output { value, datum: InlineDatum(raw_node), .. } = output + expect node: SetNode = raw_node + NodePair { val: value, node } +} + +pub fn parse_node_output_utxo(cs: PolicyId, output: Output) -> NodePair { + expect Output { value, datum: InlineDatum(raw_node), .. } = output + expect node: SetNode = raw_node + expect [(tn, amount)] = + value + |> tokens(cs) + |> dict.to_list() + expect amount == 1 + let node_key = parse_node_key(tn) + let datum_key = + when node.key is { + Empty -> None + Key(key) -> Some(key) + } + expect node_key == datum_key + expect list.length(flatten(value)) == 2 + expect valid_node(node) + expect find_cs_by_token_prefix(value, set_node_prefix) == [cs] + NodePair { val: value, node } +} + +pub fn parse_node_key(tn: AssetName) -> Option { + let prefix_length = bytearray.length(set_node_prefix) + let tn_length = bytearray.length(tn) + let key = bytearray.drop(tn, prefix_length) + expect set_node_prefix == bytearray.take(tn, prefix_length) + when prefix_length < tn_length is { + True -> Some(key) + False -> None + } +} + +pub fn valid_node(node: SetNode) -> Bool { + when node.key is { + Empty -> True + Key(node_key) -> + when node.next is { + Empty -> True + Key(next_key) -> bytearray.compare(node_key, next_key) == Less + } + } +} + +pub fn find_cs_by_token_prefix( + value: Value, + prefix: ByteArray, +) -> List { + value + |> flatten + |> list.filter_map( + fn(input: (PolicyId, ByteArray, Int)) -> Option { + let (cs, tn, _amt) = input + if is_prefix_of(prefix, tn) { + Some(cs) + } else { + None + } + }, + ) +} + +pub fn is_prefix_of(prefix: ByteArray, src: ByteArray) -> Bool { + let prefix_length = bytearray.length(prefix) + let src_length = bytearray.length(src) + when prefix_length <= src_length is { + True -> bytearray.take(src, prefix_length) == prefix + False -> False + } +} diff --git a/validators/sample.ak b/validators/sample.ak new file mode 100644 index 0000000..052fd78 --- /dev/null +++ b/validators/sample.ak @@ -0,0 +1,392 @@ +use aiken/bytearray +use aiken/dict +use aiken/interval.{Finite, Interval, IntervalBound, is_entirely_before} +use aiken/list +use aiken/transaction.{ + InlineDatum, Input, Mint, NoDatum, Output, OutputReference, ScriptContext, + Transaction, TransactionId, +} +use aiken/transaction/credential.{Address} +use aiken/transaction/value +use linkedlist/constants.{origin_node_token_name} +use linkedlist/linked_list.{deinit, init, insert, remove} +use linkedlist/types.{ + Config, Deinit, Empty, Init, Insert, Key, NodeAction, Remove, SetNode, +} +use linkedlist/utils + +validator { + fn mint_validator(cfg: Config, redeemer: NodeAction, ctx: ScriptContext) { + let (common, inputs, outputs, sigs, vrange) = utils.make_common(ctx) + when redeemer is { + Init -> { + expect + list.any( + inputs, + fn(input) { cfg.init_utxo == input.output_reference }, + ) + init(common) + } + Deinit -> deinit(common) + Insert { key_to_insert, covering_node } -> { + expect is_entirely_before(vrange, cfg.deadline) + expect list.any(sigs, fn(sig) { sig == key_to_insert }) + insert(common, key_to_insert, covering_node) + } + Remove { key_to_remove, covering_node } -> { + expect is_entirely_before(vrange, cfg.deadline) + remove(common, vrange, cfg, outputs, sigs, key_to_remove, covering_node) + } + } + } +} + +test mint_validator_init() { + let own_cs = #"746fa3ba2daded6ab9ccc1e39d3835aa1dfcb9b5a54acc2ebe6b79a4" + let init_output_ref = + OutputReference { + transaction_id: TransactionId { + hash: #"2c6dbc95c1e96349c4131a9d19b029362542b31ffd2340ea85dd8f28e271ff6d", + }, + output_index: 1, + } + let config = + Config { + init_utxo: init_output_ref, + deadline: 86_400_000, + penalty_address: Address(credential.ScriptCredential("P"), None), + } + let redeemer = Init + let minted_value = value.add(value.zero(), own_cs, origin_node_token_name, 1) + let head_output = + Output { + address: Address(credential.ScriptCredential("B"), None), + value: value.add( + minted_value, + value.ada_policy_id, + value.ada_asset_name, + 4_000_000, + ), + datum: InlineDatum(SetNode { key: Empty, next: Empty }), + reference_script: None, + } + let context = + ScriptContext { + purpose: Mint(own_cs), + transaction: Transaction { + inputs: [ + Input { + output_reference: init_output_ref, + output: Output { + address: Address(credential.ScriptCredential("C"), None), + value: value.from_lovelace(4_000_000), + datum: NoDatum, + reference_script: None, + }, + }, + ], + reference_inputs: [], + outputs: [head_output], + fee: value.zero(), + mint: value.to_minted_value(minted_value), + certificates: [], + withdrawals: dict.new(), + validity_range: interval.everything(), + extra_signatories: [], + redeemers: dict.new(), + datums: dict.new(), + id: TransactionId { + hash: #"2c6dbc95c1e96349c4131a9d19b029362542b31ffd2340ea85dd8f28e271ff6d", + }, + }, + } + + mint_validator(config, redeemer, context) +} + +test mint_validator_deinit() { + let own_cs = #"746fa3ba2daded6ab9ccc1e39d3835aa1dfcb9b5a54acc2ebe6b79a4" + let init_output_ref = + OutputReference { + transaction_id: TransactionId { + hash: #"2c6dbc95c1e96349c4131a9d19b029362542b31ffd2340ea85dd8f28e271ff6d", + }, + output_index: 1, + } + let config = + Config { + init_utxo: init_output_ref, + deadline: 86_400_000, + penalty_address: Address(credential.ScriptCredential("P"), None), + } + let redeemer = Deinit + let own_cs_value = value.add(value.zero(), own_cs, origin_node_token_name, -1) + let burn_value = value.add(value.zero(), own_cs, origin_node_token_name, -1) + let in_output = + Output { + address: Address(credential.ScriptCredential("B"), None), + value: value.add( + own_cs_value, + value.ada_policy_id, + value.ada_asset_name, + 4_000_000, + ), + datum: InlineDatum(SetNode { key: Empty, next: Empty }), + reference_script: None, + } + let context = + ScriptContext { + purpose: Mint(own_cs), + transaction: Transaction { + inputs: [Input { output_reference: init_output_ref, output: in_output }], + reference_inputs: [], + outputs: [], + fee: value.zero(), + mint: value.to_minted_value(burn_value), + certificates: [], + withdrawals: dict.new(), + validity_range: interval.everything(), + extra_signatories: [], + redeemers: dict.new(), + datums: dict.new(), + id: TransactionId { + hash: #"2c6dbc95c1e96349c4131a9d19b029362542b31ffd2340ea85dd8f28e271ff6d", + }, + }, + } + + mint_validator(config, redeemer, context) +} + +test mint_validator_insert() { + let own_cs = #"746fa3ba2daded6ab9ccc1e39d3835aa1dfcb9b5a54acc2ebe6b79a4" + let init_output_ref = + OutputReference { + transaction_id: TransactionId { + hash: #"2c6dbc95c1e96349c4131a9d19b029362542b31ffd2340ea85dd8f28e271ff6d", + }, + output_index: 1, + } + let config = + Config { + init_utxo: init_output_ref, + deadline: 86_400_000, + penalty_address: Address(credential.ScriptCredential("P"), None), + } + let user1_pkh = + bytearray.from_string( + @"a65ca58a4e9c755fa830173d2a5caed458ac0c73f97db7faae2e7e3b", + ) + let covering_tn = + "FSNa65ca58a4e9c755fa830173d2a5caed458ac0c73f97db7faae2e7e3b" + let user2_pkh = + bytearray.from_string( + @"e18d73505be6420225ed2a42c8e975e4c6f9148ab38e951ea2572e54", + ) + let insert_tn = "FSNe18d73505be6420225ed2a42c8e975e4c6f9148ab38e951ea2572e54" + let covering_minted_value = value.add(value.zero(), own_cs, covering_tn, 1) + let covering_node_value = + value.add( + covering_minted_value, + value.ada_policy_id, + value.ada_asset_name, + 9_000_000, + ) + let covering_node = SetNode { key: Key { key: user1_pkh }, next: Empty } + let covering_output = + Output { + address: Address(credential.ScriptCredential("I"), None), + value: covering_node_value, + datum: InlineDatum(covering_node), + reference_script: None, + } + let covering_output_ref = + OutputReference { + transaction_id: TransactionId { hash: #"" }, + output_index: 1, + } + let out_prev_node = + SetNode { key: covering_node.key, next: Key { key: user2_pkh } } + + let out_prev_node_output = + Output { + address: Address(credential.ScriptCredential("I"), None), + value: covering_node_value, + datum: InlineDatum(out_prev_node), + reference_script: None, + } + let out_node = + SetNode { key: Key { key: user2_pkh }, next: covering_node.next } + let insert_minted_value = value.add(value.zero(), own_cs, insert_tn, 1) + let out_node_output = + Output { + address: Address(credential.ScriptCredential("I"), None), + value: value.add( + insert_minted_value, + value.ada_policy_id, + value.ada_asset_name, + 9_000_000, + ), + datum: InlineDatum(out_node), + reference_script: None, + } + let redeemer = Insert { key_to_insert: user2_pkh, covering_node } + let insert_timerange = + Interval { + lower_bound: IntervalBound { + bound_type: Finite(1000), + is_inclusive: False, + }, + upper_bound: IntervalBound { + bound_type: Finite(2000), + is_inclusive: False, + }, + } + let context = + ScriptContext { + purpose: Mint(own_cs), + transaction: Transaction { + inputs: [ + Input { + output_reference: covering_output_ref, + output: covering_output, + }, + ], + reference_inputs: [], + outputs: [out_prev_node_output, out_node_output], + fee: value.zero(), + mint: value.to_minted_value(insert_minted_value), + certificates: [], + withdrawals: dict.new(), + validity_range: insert_timerange, + extra_signatories: [user2_pkh], + redeemers: dict.new(), + datums: dict.new(), + id: TransactionId { + hash: #"2c6dbc95c1e96349c4131a9d19b029362542b31ffd2340ea85dd8f28e271ff6d", + }, + }, + } + mint_validator(config, redeemer, context) +} + +test mint_validator_remove() { + let own_cs = #"746fa3ba2daded6ab9ccc1e39d3835aa1dfcb9b5a54acc2ebe6b79a4" + let init_output_ref = + OutputReference { + transaction_id: TransactionId { + hash: #"2c6dbc95c1e96349c4131a9d19b029362542b31ffd2340ea85dd8f28e271ff6d", + }, + output_index: 1, + } + let config = + Config { + init_utxo: init_output_ref, + deadline: 86_400_000, + penalty_address: Address(credential.ScriptCredential("P"), None), + } + let user1_pkh = + bytearray.from_string( + @"a65ca58a4e9c755fa830173d2a5caed458ac0c73f97db7faae2e7e3b", + ) + let covering_tn = + "FSNa65ca58a4e9c755fa830173d2a5caed458ac0c73f97db7faae2e7e3b" + let user2_pkh = + bytearray.from_string( + @"e18d73505be6420225ed2a42c8e975e4c6f9148ab38e951ea2572e54", + ) + let user2_tn = "FSNe18d73505be6420225ed2a42c8e975e4c6f9148ab38e951ea2572e54" + let covering_minted_value = value.add(value.zero(), own_cs, covering_tn, 1) + let covering_node_value = + value.add( + covering_minted_value, + value.ada_policy_id, + value.ada_asset_name, + 9_000_000, + ) + let covering_node = + SetNode { key: Key { key: user1_pkh }, next: Key { key: user2_pkh } } + let covering_output = + Output { + address: Address(credential.ScriptCredential("I"), None), + value: covering_node_value, + datum: InlineDatum(covering_node), + reference_script: None, + } + let covering_output_ref = + OutputReference { + transaction_id: TransactionId { hash: #"" }, + output_index: 1, + } + let remove_output_ref = + OutputReference { + transaction_id: TransactionId { hash: #"" }, + output_index: 1, + } + let remove_node = SetNode { key: Key { key: user2_pkh }, next: Empty } + let user2_value = value.add(value.zero(), own_cs, user2_tn, 1) + let remove_node_output = + Output { + address: Address(credential.ScriptCredential("I"), None), + value: value.add( + user2_value, + value.ada_policy_id, + value.ada_asset_name, + 9_000_000, + ), + datum: InlineDatum(remove_node), + reference_script: None, + } + let output_node = SetNode { key: covering_node.key, next: remove_node.next } + let out_node_output = + Output { + address: Address(credential.ScriptCredential("I"), None), + value: covering_node_value, + datum: InlineDatum(output_node), + reference_script: None, + } + let remove_burn_value = value.add(value.zero(), own_cs, user2_tn, -1) + let cover_node = SetNode { key: Key { key: user1_pkh }, next: Empty } + let redeemer = Remove { key_to_remove: user2_pkh, covering_node: cover_node } + let remove_timerange = + Interval { + lower_bound: IntervalBound { + bound_type: Finite(1000), + is_inclusive: False, + }, + upper_bound: IntervalBound { + bound_type: Finite(2000), + is_inclusive: False, + }, + } + let context = + ScriptContext { + purpose: Mint(own_cs), + transaction: Transaction { + inputs: [ + Input { + output_reference: remove_output_ref, + output: remove_node_output, + }, + Input { + output_reference: covering_output_ref, + output: covering_output, + }, + ], + reference_inputs: [], + outputs: [out_node_output], + fee: value.zero(), + mint: value.to_minted_value(remove_burn_value), + certificates: [], + withdrawals: dict.new(), + validity_range: remove_timerange, + extra_signatories: [user2_pkh], + redeemers: dict.new(), + datums: dict.new(), + id: TransactionId { + hash: #"2c6dbc95c1e96349c4131a9d19b029362542b31ffd2340ea85dd8f28e271ff6d", + }, + }, + } + mint_validator(config, redeemer, context) +}