aiken_design_patterns/linked_list

Storing lists in datums is generally impractical, as its growth can lead to unspendable UTxOs due to limited resources available on-chain.

A linked list is a construct for storing an infinitely large array of elements on-chain, such that each element is represented with a UTxO that points to its immediate successor.

Usage Guideline

Contracts using this module must keep the linked list controlled by one dedicated spend script and one list NFT minting policy:

  1. Define the spend script datum as an applied alias of Element<a, b>:
pub type Datum = linked_list.Element<RootType, NodeType>
  1. Ensure that the UTxO produced by init goes to that spend script credential.
  2. Implement list spend scripts so they only succeed through this module’s two spend helpers: spend_for_adding_or_removing_an_element and spend_for_updating_elements_data.
  3. Implement list mint scripts so they only succeed through this module’s provided mint helpers.
  4. Choose root and node NFT names so their namespaces are disjoint: use a non-empty node_key_prefix, non-empty node keys, and a root_key that does not begin with the node prefix. This avoids root/node identity collisions without adding repeated runtime checks to every operation.

These rules preserve the invariant that linked list NFTs cannot leave the list spend script/payment credential.

The guideline above implies five important design constraints:

  1. The elements are required to have no scripts attached to them at mint. The continuations are already provided with too many values. Providing their reference script fields would overwhelm the API even further. So far this doesn’t seem to be a needed feature.
  2. While continued anchor nodes are validated to go to the same address, for new/removed nodes only the payment parts are validated to match the anchors’. This allows customization of staking parts of new nodes, while preserving the staking parts of existing elements.
  3. Extra mints under the list NFT policy beyond the NFT needed for the list structure are unsupported (e.g. CIP-68 tokens are not supported). The provided mint helpers enforce exactly the mint/burn operation needed by the list structure.
  4. List operations assume that the list script/payment credential is dedicated to the linked list. For remove/fold operations, every spent input at that payment credential is expected to be an authentic list element participating in the operation. Non-authentic inputs at the same payment credential are invalid transaction construction; implementations may reject them without trying to ignore or classify them.
  5. The module assumes root and node asset-name namespaces are disjoint. It validates the root key and node prefix at operation boundaries, but does not add extra comparisons to prove namespace disjointness on-chain.

Types

The terminology used here is as follows:

  • Root is the first element of the linked list
  • Node is any other element in a linked list other than the root
  • Element is the umbrella term to refer to either the root or nodes

You probably won’t need to interact with this.

Constructors

  • Element { data: ElementData<root_data, node_data>, link: Link }

Similar to Element above, the API is designed so that you wouldn’t need to work with this datatype directly.

Continuations that provide element data implicitly signal whether it should be coerced into a root_data or a node_data through the (potential) key. That is, if no key is provided, it means the element at hand is a root element.

Constructors

  • Root { data: root_data }
  • Node { data: node_data }

Similar to Eval, but with polymorphic return type. This allows returning custom types with get_element_info.

Alias

ElementEval<a> = fn(PolicyId, RootKey, NodeKeyPrefix, NodeKeyPrefixLength) -> a

Type alias for the continuation function passed to get_element_info.

Alias

ElementInfo<a> = fn(Lovelace, Option<NodeKey>, Data, Link) -> a

This is the datatype for allowing you to provide environment constants. To be a bit nerdy, this is essentially a Reader monad.

To prevent excessive overhead, this function requires the length of the key prefix to be provided explicitly, next to the key prefix itself (otherwise the length should have been validated on-chain).

The length of the RootKey can be 32 bytes at the maximum. On the other hand, the length of any of the NodeKeys can be at most (32 - NodeKeyPrefixLength). “Length” here refers to the number of bytes after the asset name is decoded from any of the encoding schemes (e.g. UTF-8).

Alias

Eval = fn(PolicyId, RootKey, NodeKeyPrefix, NodeKeyPrefixLength) -> Bool

Alias

Link = Option<NodeKey>

Alias

LovelaceChange = Lovelace

Alias

NodeData = Data

Alias for representing the keys of linked list nodes. This must be at least one byte long, and contracts should implement validations on them depending on their needs. The generic module assumes this invariant rather than re-checking it on every operation.

Requiring keys to be unique is the recommended approach (e.g. making sure it’s hash of an input’s out ref). If you do need to allow duplicate keys, proceed with extreme caution as linked list integrity breaks without some other means of preserving it.

As with the general principle of this API, for ordered operations (e.g. insert_ascending), proper ordering is already validated.

Alias

NodeKey = ByteArray

Bytes prefixing all linked list nodes (i.e. all elements except for the root). It should be non-empty and must not prefix RootKey.

Note that the total number of bytes allowed for NFTs of linked list elements can be 32 bytes at most. Therefore, if for example your prefix occupies 4 bytes, your keys can have 28 bytes at most.

Alias

NodeKeyPrefix = ByteArray

A sort of boilerplate to reduce execution budgets. You can use const to leverage Aiken compiler’s optimizations:

pub const node_key_prefix = "🔗"

pub const node_key_prefix_length =
  bytearray.length(node_key_prefix)

Alias

NodeKeyPrefixLength = Int

Alias

RootData = Data

Similar to Eval, but for (init)[#init] and (deinit)[#deinit], which don’t validate node key prefixes.

Alias

RootEval = fn(PolicyId, RootKey) -> Bool

An alias for the asset name of the NFT held in the root element. This can be zero bytes or more (32 bytes at most).

Choose this outside the node namespace: it must not begin with NodeKeyPrefix. The usual low-budget setup is an empty root key, a non-empty node prefix, and non-empty node keys.

Keep in mind that some UTF-8 characters occupy more than 1 byte. For example the tree emoji (🌳) takes up 4 bytes.

Alias

RootKey = AssetName

Functions

Finalization Functions

run_eval_with(
  reader: Eval,
  list_nft_policy_id: PolicyId,
  root_key: RootKey,
  node_key_prefix: NodeKeyPrefix,
  node_key_prefix_length: NodeKeyPrefixLength,
) -> Bool

This typically ends up as boilerplate by defining a helper like this:

use aiken_design_patterns/linked_list

const node_key_prefix = "NODE"

const node_key_prefix_length =
  bytearray.length(node_key_prefix)

pub fn finalize_linked_list(
  eval: linked_list.Eval,
  list_nft_policy_id: PolicyId,
) -> Bool {
  linked_list.run_eval_with(
    eval,
    list_nft_policy_id,
    node_key_prefix,
    node_key_prefix_length,
  )
}

As an example, with this you’ll be able to turn your Eval results to Bools as such:

// In your minting contract:

let linked_list_eval = insert_ascending(...)

expect linked_list_eval |> finalize_linked_list(own_policy_id)

run_root_with(
  reader: RootEval,
  list_nft_policy_id: PolicyId,
  root_key: RootKey,
) -> Bool

run_element_with(
  reader: ElementEval<a>,
  list_nft_policy_id: PolicyId,
  root_key: RootKey,
  node_key_prefix: NodeKeyPrefix,
  node_key_prefix_length: NodeKeyPrefixLength,
) -> a

Initialization and De-initialization

init(
  nonce_validated: Bool,
  produced_element_output: Output,
  tx_mint: Value,
  root_validator: fn(Lovelace, Data) -> Bool,
) -> RootEval

Initialize a linked list, i.e. produce a UTxO which contains some ADA and the authentication NFT, with a Root element data. The asset name of the NFT (i.e. RootKey) is something that must be validated by passing the result of this function to run_root_with.

deinit(
  inputs: List<Input>,
  tx_mint: Value,
  root_validator: fn(Input, Lovelace, Data) -> Bool,
) -> RootEval

Deinitialize an empty list.

Element Addition and Removal

insert_ascending(
  continued_anchor_element_output: Output,
  new_node_output: Output,
  inputs: List<Input>,
  tx_mint: Value,
  additional_validations: fn(
    Input,
    LovelaceChange,
    Option<NodeKey>,
    Data,
    Lovelace,
    NodeKey,
    NodeData,
    Link,
  ) ->
    Bool,
) -> Eval

The anchor element is inferred from inputs by requiring exactly one authentic linked list element input. It is assumed the address from which the anchor element comes from is the destination address for the new node.

The additional_validations argument provides you with 8 values:

  1. Input of the anchor element
  2. Change in Lovelace count from anchor input to continued anchor output
  3. If the anchor element is a node, its key (i.e. asset name without the prefix) is provided
  4. Underlying data of the anchor element – note that this can come from either a Root or a Node
  5. Lovelace count of the new node UTxO
  6. Key (i.e. asset name without the prefix) of the new node
  7. Underlying data of the new node – note that this is extracted from a Node
  8. Link of the new node

insert_descending(
  continued_anchor_element_output: Output,
  new_node_output: Output,
  inputs: List<Input>,
  tx_mint: Value,
  additional_validations: fn(
    Input,
    LovelaceChange,
    Option<NodeKey>,
    Data,
    Lovelace,
    NodeKey,
    NodeData,
    Link,
  ) ->
    Bool,
) -> Eval

This is exactly like insert_ascending, with the exception that the inserted key is expected to be less than the anchor’s, and greater than anchor’s link (if any).

append_unordered(
  continued_anchor_element_output: Output,
  new_node_output: Output,
  inputs: List<Input>,
  tx_mint: Value,
  additional_validations: fn(
    Input,
    LovelaceChange,
    Option<NodeKey>,
    Data,
    Lovelace,
    NodeKey,
    NodeData,
  ) ->
    Bool,
) -> Eval

Append a new element at the very end of an unordered list. The anchor element is inferred from inputs by requiring exactly one authentic linked list element input. There is no comparison validation for the new key with respect to the anchor element’s, but the anchor must be terminal.

The additional_validations argument provides you with 7 values:

  1. Input of the anchor element
  2. Change in Lovelace count from anchor input to continued anchor output
  3. If the anchor element is a node, its key (i.e. asset name without the prefix) is provided
  4. Underlying data of the anchor element – note that this can come from either a Root or a Node
  5. Lovelace count of the new node UTxO
  6. Key (i.e. asset name without the prefix) of the new node
  7. Underlying data of the new node – note that this is extracted from a Node

prepend_unordered(
  continued_root_element_output: Output,
  new_node_output: Output,
  inputs: List<Input>,
  tx_mint: Value,
  additional_validations: fn(
    Input,
    LovelaceChange,
    RootData,
    Lovelace,
    NodeKey,
    NodeData,
    Link,
  ) ->
    Bool,
) -> Eval

Prepend a new element at the very start of an unordered list. The root element is inferred from inputs by requiring exactly one authentic linked list element input. There is no validation for the new key to be less than or greater than the root element’s potential link.

The additional_validations argument provides you with 7 values:

  1. Input of the root element
  2. Change in Lovelace count from root input to continued root output
  3. Underlying data of the root
  4. Lovelace count of the new node UTxO
  5. Key (i.e. asset name without the prefix) of the new node
  6. Underlying data of the new node – note that this is extracted from a Node
  7. Link of the new node

remove(
  anchor_input_outref: OutputReference,
  continued_anchor_element_output: Output,
  inputs: List<Input>,
  tx_mint: Value,
  additional_validations: fn(
    Input,
    LovelaceChange,
    Option<NodeKey>,
    Data,
    Input,
    Lovelace,
    NodeKey,
    NodeData,
    Link,
  ) ->
    Bool,
) -> Eval

Remove a node. The anchor element is identified by its output reference, and the anchor and removing node are inferred from the transaction inputs. This operation assumes the list script/payment credential is dedicated to list elements, so the transaction must not include unrelated inputs at the same payment credential.

The additional_validations argument provides you with 9 values:

  1. Input of the anchor element
  2. Change in Lovelace count from anchor input to continued anchor output
  3. If the anchor element is a node, its key (i.e. asset name without the prefix) is provided
  4. Underlying data of the anchor element – note that this can come from either a Root or a Node
  5. Input of the removing node
  6. Lovelace count of the removing node UTxO
  7. Key (i.e. asset name without the prefix) of the removing node
  8. Underlying data of the removing node
  9. Link of the removing node

Fold Functions

fold_from_root(
  anchor_root_input_outref: OutputReference,
  continued_anchor_root_output: Output,
  inputs: List<Input>,
  tx_mint: Value,
  additional_validations: fn(
    Input,
    LovelaceChange,
    RootData,
    Input,
    Lovelace,
    NodeKey,
    NodeData,
    Link,
    RootData,
  ) ->
    Bool,
) -> Eval

Spend a root element and its link, reproduce the root while expecting the linked node to be burnt. The root is identified by its output reference. This traverses the whole inputs list and expects exactly two authentic list inputs at the list payment credential: the root input and the folding node input. This relies on the list spending validators preserving the invariant that list NFTs do not leave their list script/payment credential, and on the list script/payment credential being dedicated to list elements. Unrelated inputs at the same payment credential are intentionally rejected. Additionally, performs an arbitrary validation on keys and underlying data.

This function ensures:

  • Authenticity of the two spent list inputs and root’s continued counterpart
  • The anchor is root, and that it correctly points to the folding node
  • The root is continued at the same address
  • The continued anchor data remains a Root
  • The folding node shares the root’s payment credential
  • Folding node is burnt
  • Root’s key is correct, and node’s key is prefixed as expected

The function you define for additional_validations is provided with 9 values (in order):

  1. Input of the root element
  2. Change in Lovelace count from input root to output
  3. Underlying data of the root
  4. Input of the folding node
  5. Lovelace count of the folding node
  6. Key (i.e. asset name) of the folding node
  7. Underlying data of the folding node
  8. Link of the folding node
  9. Underlying data of the reproduced/continued root

Spending Script Helpers

spend_for_adding_or_removing_an_element(
  list_nft_policy_id: PolicyId,
  tx_mint: Value,
) -> Bool

Simply checks if there are any tokens getting minted/burnt in transaction’s mint field. How you provide list_nft_policy_id is most likely the “coupling” needed between the spending and minting endpoints.

spend_for_updating_elements_data(
  element_input_index: Int,
  continued_element_output_index: Int,
  element_input_outref: OutputReference,
  inputs: List<Input>,
  outputs: List<Output>,
  tx_mint: Value,
  additional_validations: fn(LovelaceChange, Option<NodeKey>, Data, Data, Link) ->
    Bool,
) -> Eval

For spending an individual element within a linked list and reproducing it with updated data, without affecting the structure of the linked list.

The continuation provides you with:

  1. Change in Lovelace count from input to output (i.e. output minus input).
  2. A possible node key (i.e. its asset name without the prefix). It’ll be None if the spent element is root.
  3. Input underlying data.
  4. Updated data in the reproduced element.
  5. Element’s link.

The transaction’s mint is required to ensure no linked list assets are minted or burnt.

Exposed Helpers

get_element_info(
  element_utxo: Output,
  info_validations: ElementInfo<a>,
) -> ElementEval<a>

This should generally not be needed, but in cases where another script wants to validate expenditure from the linked list, this function can help.

After validating the authenticity of the provided element UTxO, it provides the continuation with:

  1. Lovelace count of the element
  2. A possible NodeKey (i.e. it will be None if the element UTxO is root)
  3. Underlying data of the element
  4. Element’s link

Unlike other helpers, this one returns an ElementEval<a>. This is very similar to Eval. However, the type variable a allows the final evaluation to result in any type rather than being restricted to Bool.

Search Document