diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..74b642a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: Tests + +on: + push: + branches: ["main"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - uses: aiken-lang/setup-aiken@v0.1.0 + with: + version: v1.0.24-alpha + + - run: aiken fmt --check + - run: aiken check -D + - run: aiken build diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ff7811b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +# Aiken compilation artifacts +artifacts/ +# Aiken's project working directory +build/ +# Aiken's default documentation export +docs/ 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..d71cf92 --- /dev/null +++ b/aiken.toml @@ -0,0 +1,14 @@ +name = "txpipe/asteria" +version = "0.0.0" +license = "Apache-2.0" +description = "Aiken contracts for project 'txpipe/asteria'" + +[repository] +user = "txpipe" +project = "asteria" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "1.7.0" +source = "github" diff --git a/plutus.json b/plutus.json new file mode 100644 index 0000000..ac64ac5 --- /dev/null +++ b/plutus.json @@ -0,0 +1,108 @@ +{ + "preamble": { + "title": "txpipe/asteria", + "description": "Aiken contracts for project 'txpipe/asteria'", + "version": "0.0.0", + "plutusVersion": "v2", + "compiler": { + "name": "Aiken", + "version": "v1.0.24-alpha+982eff4" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "spacetime.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/spacetime~1Datum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/spacetime~1Redeemer" + } + }, + "parameters": [ + { + "title": "ship_policy_id", + "schema": { + "$ref": "#/definitions/ByteArray" + } + }, + { + "title": "max_speed", + "schema": { + "$ref": "#/definitions/Int" + } + }, + { + "title": "fuel_per_step", + "schema": { + "$ref": "#/definitions/Int" + } + } + ], + "compiledCode": "59035c010000323232323232323232232232232222323232533300f32323232323232323232323232323232323232323253330243370e9001181180309919191919299981499b8748000c0a00044c8c8c8c8c94ccc0b8cdc3a4000605a00226464646464646464a66606c66e1d2004303500113232323232533303b533303b533303b533303b00b100914a020102940400852808008a503371e6eb8c050c0e000c0994ccc0e4cdc39bad300e303700233700046052266e1cdd69802981b80119b8002102714a06058002607800260680022c6002606600c460746076607600266e24cdc101580100c99b8900102c3233700600204460020404a66606466e2000520001337029000000880099b873330043756600a605c600a605c00c0580389001181a00098160008b19803005119b873330023756600660580020540349001111191919299981919b8748008004520001375a606e6060004606000264a66606266e1d200200114c0103d87a8000132323300100100222533303700114c103d87a800013232323253330383371e014004266e9520003303c375000297ae0133006006003375a60720066eb8c0dc008c0ec008c0e4004dd5981b18178011817800991980080080211299981a0008a6103d87a800013232323253330353371e010004266e95200033039374c00297ae01330060060033756606c0066eb8c0d0008c0e0008c0d80048c0c4c0c8004c0bc004c09c00458cc0040208cdd7980198138008021119198008008019129998170008a6103d87a800013232533302d300500213374a90001981880125eb804cc010010004c0c8008c0c00048c0b0004c0a8004c08801858dd61814000981400098138011bac3025001301d0033023001302300230210013019010375a603e002603e0046eb4c074004c074008dd6980d800980d8011bae3019001301100a375a602e002602e0046eb4c054004c0340145261365632533300f3370e9000000899191919299980b180c8010a4c2c6eb4c05c004c05c008dd6980a80098068028b18068021800802119299980719b87480000044c8c8c8c8c8c8c8c94ccc064c07000852616375a603400260340046eb4c060004c060008dd6980b000980b0011bae3014001300c00216300c001375a0026eb4004dd7000918029baa001230033754002ae6955ceaab9e5573eae815d0aba21", + "hash": "4169f9c7110ee486b5e1872d123b10aa4924f38c8a47291b5d8e3422" + } + ], + "definitions": { + "ByteArray": { + "dataType": "bytes" + }, + "Int": { + "dataType": "integer" + }, + "spacetime/Datum": { + "title": "Datum", + "anyOf": [ + { + "title": "Datum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "ship_id", + "$ref": "#/definitions/ByteArray" + }, + { + "title": "pos_x", + "$ref": "#/definitions/Int" + }, + { + "title": "pos_y", + "$ref": "#/definitions/Int" + }, + { + "title": "fuel", + "$ref": "#/definitions/Int" + } + ] + } + ] + }, + "spacetime/Redeemer": { + "title": "Redeemer", + "anyOf": [ + { + "title": "Redeemer", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "delta_x", + "$ref": "#/definitions/Int" + }, + { + "title": "delta_y", + "$ref": "#/definitions/Int" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/validators/spacetime.ak b/validators/spacetime.ak new file mode 100644 index 0000000..880d4d1 --- /dev/null +++ b/validators/spacetime.ak @@ -0,0 +1,71 @@ +use aiken/list +use aiken/math +use aiken/transaction.{InlineDatum, ScriptContext, Spend, Transaction} +use aiken/transaction/value.{PolicyId, quantity_of} + +type Datum { + ship_id: ByteArray, + pos_x: Int, + pos_y: Int, + fuel: Int, +} + +type Redeemer { + delta_x: Int, + delta_y: Int, +} + +fn distance(delta_x: Int, delta_y: Int) -> Int { + math.abs(delta_x) + math.abs(delta_y) +} + +// we need to enforce: +// [x] that the speed (manhattan_distance/slot) doesn't exceed an upper bound +// [ ] that the ship has enough fuel (token) to move the desired delta +// [ ] that the new position of the ship utxo equals the last position + delta +// [ ] that the fuel required for the delta has been burned +// [ ] that the tx is signed by the owner of the ship (token holder) + +validator(ship_policy_id: PolicyId, max_speed: Int, fuel_per_step: Int) { + fn spend(datum: Datum, redeemer: Redeemer, ctx: ScriptContext) -> Bool { + let Redeemer { delta_x, delta_y } = redeemer + + let Datum { ship_id, pos_x, pos_y, fuel } = datum + + let ScriptContext { transaction, purpose } = ctx + + let Transaction { inputs, outputs, .. } = transaction + + expect Spend(utxo_ref) = purpose + + expect Some(own_input) = + list.find(inputs, fn(input) { input.output_reference == utxo_ref }) + + expect Some(own_output) = + list.find( + outputs, + fn(output) { quantity_of(output.value, ship_policy_id, ship_id) == 1 }, + ) + + let must_hold_ship_asset = + quantity_of(own_input.output.value, ship_policy_id, ship_id) == 1 + + let distance = distance(delta_x, delta_y) + + // speed = distance since we consider each block the unit of time + let must_respect_max_speed = distance <= max_speed + + let must_have_enough_fuel = fuel >= fuel_per_step * distance + + expect InlineDatum(out_data) = own_output.datum + + expect out_data: Datum = out_data + + let final_pos_must_respect_movement = + out_data.pos_x == pos_x + delta_x && out_data.pos_y == pos_y + delta_y + + let final_ship_id_must_match = out_data.ship_id == ship_id + + must_hold_ship_asset && must_respect_max_speed && must_have_enough_fuel && final_pos_must_respect_movement && final_ship_id_must_match + } +}