Skip to content

Commit

Permalink
16 implement spacetimescript validator checks (#27)
Browse files Browse the repository at this point in the history
* completed spacetime validator.

* spacetime `MoveShip` redeemer tests.

* moved tests constants.

* fixed spacetime validator.

* spacetime validator tests.
  • Loading branch information
franciscojoray authored Apr 8, 2024
1 parent 1dfaa64 commit ee1e8a2
Show file tree
Hide file tree
Showing 8 changed files with 1,152 additions and 65 deletions.
5 changes: 3 additions & 2 deletions onchain/docs/mvp-design/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Pays the min ada locked in the `ShipState` UTxO back to the ship owner and burns
* The `PelletState` value doesn't change.

### SpaceTimeScript validator:
* Params: `AdminToken`, Asteria validator address and pellet validator address.
* Params: `AdminToken`, Asteria validator address, pellet validator address, max distance allowed per movement, fuel required per step and ship's fuel capacity.

#### *MoveShip Redeemer (includes delta_x and delta_y displacements)*
* `ShipToken` is present.
Expand All @@ -144,6 +144,7 @@ Pays the min ada locked in the `ShipState` UTxO back to the ship owner and burns
#### *GatherFuel Redeemer (includes gathering amount)*
* there is a single `ShipState` input.
* there is a single `ShipState` output.
* `PilotToken` is present.
* there is a `PelletState` input with the same x and y datum coordinates as the `ShipState` UTxO.
* the amount specified plus the fuel before charging does not exceed `MAX_SHIP_FUEL` capacity.
* the amount specified is added to the output `ShipState` fuel datum field, and the other fields remain unchanged.
Expand All @@ -159,7 +160,7 @@ Pays the min ada locked in the `ShipState` UTxO back to the ship owner and burns
#### *Quit Redeemer*
* there is a single `ShipState` input.
* the `PilotToken` is present in an input.
* no `ShipState` output.
* `ShipToken` is burnt.

### Ship minting policy or "ShipyardPolicy":
* Params: `SpaceTimeScript` validator address, Asteria validator address, ship's initial fuel and minimum initial distance from Asteria.
Expand Down
23 changes: 23 additions & 0 deletions onchain/src/lib/asteria/test_mock.ak
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
pub const shipyard_policy = #"c000"

pub const admin_policy = #"c111"

pub const admin_token_name = "admin"

pub const transaction_id_1 = #"a111"

pub const transaction_id_2 = #"a222"

pub const transaction_id_3 = #"a333"

pub const ship_credential = #"0000"

pub const pellet_credential = #"1111"

pub const asteria_credential = #"2222"

pub const pilot_credential = #"3333"

pub const ship_token_name = "SHIP7"

pub const pilot_token_name = "PILOT7"
13 changes: 13 additions & 0 deletions onchain/src/lib/asteria/utils.ak
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,23 @@ pub fn is_ship_token_in_utxo(utxo: Output, shipyard_policy: PolicyId) -> Bool {
any(token_names, fn(name) { has_prefix("SHIP", name) })
}

pub fn is_pilot_token_in_utxo(
utxo: Output,
shipyard_policy: PolicyId,
pilot_token_name: AssetName,
) -> Bool {
let token_names = keys(tokens(utxo.value, shipyard_policy))
any(token_names, fn(name) { name == pilot_token_name })
}

pub fn has_prefix(prefix: ByteArray, name: AssetName) -> Bool {
take(name, length(prefix)) == prefix
}

pub fn distance(delta_x: Int, delta_y: Int) -> Int {
math.abs(delta_x) + math.abs(delta_y)
}

pub fn required_fuel(distance: Int, fuel_per_step: Int) -> Int {
distance * fuel_per_step
}
32 changes: 25 additions & 7 deletions onchain/src/plutus.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,39 +101,57 @@
{
"title": "spacetime.spend",
"datum": {
"title": "_datum",
"title": "datum",
"schema": {
"$ref": "#/definitions/asteria~1types~1ShipDatum"
}
},
"redeemer": {
"title": "_redeemer",
"title": "redeemer",
"schema": {
"$ref": "#/definitions/asteria~1types~1ShipRedeemer"
}
},
"parameters": [
{
"title": "_pellet_validator_address",
"title": "pellet_validator_address",
"schema": {
"$ref": "#/definitions/aiken~1transaction~1credential~1Address"
}
},
{
"title": "_asteria_validator_address",
"title": "asteria_validator_address",
"schema": {
"$ref": "#/definitions/aiken~1transaction~1credential~1Address"
}
},
{
"title": "_admin_token",
"schema": {
"$ref": "#/definitions/Tuple$ByteArray_ByteArray"
"$ref": "#/definitions/asteria~1types~1AssetClass"
}
},
{
"title": "max_distance",
"schema": {
"$ref": "#/definitions/Int"
}
},
{
"title": "max_ship_fuel",
"schema": {
"$ref": "#/definitions/Int"
}
},
{
"title": "fuel_per_step",
"schema": {
"$ref": "#/definitions/Int"
}
}
],
"compiledCode": "590112010000323232323232222322322322533300a4a229309b2b19299980499b87480000044c8c8c8c94ccc040c04800852616375a602000260200046eb4c038004c02cdd50010a99980499b87480080044c8c94ccc038c04000852616375a601c00260166ea800854ccc024cdc3a40080022a66601860166ea800852616153330093370e90030008a99980618059baa00214985858c024dd5000a99980319b8748000c01cdd50008991919191919191919191919299980a980b8010a4c2c6eb8c054004c054008dd7180980098098011bae30110013011002375a601e002601e0046eb4c034004c034008dd6980580098041baa0011632337606012002601260140026eb00055cd2ab9d5573caae7d5d0aba201",
"hash": "1e7937119c90ff4b7ca39a916afebf155d47a963a5f7ce39702ca280"
"compiledCode": "590718010000323232323232322222322322322323232232323225333013323232323232323232323232323232323232533302530163026375402226464646464a66605a60600042646464646464646464a666066605060686ea8cc0200748cc008c018cc010dd59801981b1baa300330363754002026466e3c0040444c8c8c8c94ccc0dcc0b00104c8c8c8c8c94ccc0fcc1080084c94ccc0f4c0b4c0f8dd5000899191919191919191919192999824181c99980a1bab3017304a3754602e60946ea807c09c09454ccc12001c54ccc12001854ccc12001454ccc12001054ccc12000c54ccc12000840045280a5014a029405280a5014a066e24020108cdc39bad30203048375401266e040ac018cdc39bad30103047375401066e00098030cdc39bad30133046375400e66e0009c030cdc780f9bae3048304930493049304930493045375400c66ebcc044c110dd5180898221baa01930113044375400e66e24004098cdc100081b9919b803001008300100725333040337100029000099b81480000044004c0c8004c108c0fcdd50008b1803981f1baa0011630400013301302223300500101a375a607c607e0046eb4c0f4004c0e4dd50148a99981b98140020991919299981e9820001099299981d9815981e1baa0011323232533303e3033303f3754002264a66607e605e60806ea80044c8c8c8c8c94ccc1114ccc110cdc38139bad30133046375400a266e1c094dd6980798231baa00514a02a6660880062a666088004200229405280a503375e6024608a6ea8c048c114dd500d180918229baa00a3375e00e60306608c6ea0cdc0013805982418221baa0383371266e000980280e94ccc100c0d4c104dd500089919191919191919299982598270010a4c2c6eb8c130004c130008dd6982500098250011bad30480013048002375a608c00260846ea800458c110c104dd50008b180498201baa300d30403754608660806ea800458cc04809c8cdd7980c18201baa300d3040375400207a60600026080607a6ea800458c014c0f0dd50008b181f0009980881011980180080c1bad303c30393754052264a666070605000a2a666070605a60726ea8cc0340888cdd79809981d9baa3008303b375400206e264a66607266ebcdd31981e99bb037500386ea00692f5bded8c06e98cc0f52f7b63001010000010100004bd6f7b63008008a5033710666008600203e02e02a90000b099b88333004300101f017015480008c8cc004004008894ccc0f400452f5bded8c0264646464a66607c66e3d2201000021003133042337606ea4008dd3000998030030019bab303f003375c607a0046082004607e002446600c6014660106eacc01cc0e8dd5001000919b8f33371890001b8d4890453484950000014890453484950002303b303c303c0012223253330383029303937540022900009bad303d303a375400264a666070605260726ea8004530103d87a8000132330010013756607c60766ea8008894ccc0f4004530103d87a80001323232533303d3371e00e6eb8c0f800c4c04ccc104dd4000a5eb804cc014014008dd6981f0011820801181f80099804001801181a1baa0251622323300100100322533303900114a0264a66606e60086eb8c0f0008528899801801800981e0009181b981c000911929998199812181a1baa00114bd6f7b63009bab3038303537540026600600400244646600200200644a66606c002298103d87a8000132323253330363371e00c6eb8c0dc00c4c030cc0e8dd3000a5eb804cc014014008dd5981b801181d001181c0009180100098008009129998188008a5eb804cc0c8c0bcc0cc004cc008008c0d000488c8cc00400400c894ccc0c8004530103d87a8000132325333031300500213007330350024bd70099802002000981b001181a0009ba54800058c0b8004cc0040488cdd7980198159baa00100422323300100100322533302e00114bd700991929998169802801099818801198020020008998020020009819001181800091816000981518139baa01116375c605260540046eb8c0a0004c0a0008dd7181300098130011bad30240013024002375a604400260440046eb4c080004c070dd50081bab301e301f301f0023758603a002603a603a0046eb0c06c004c05cdd5180d001180c980d000980a9baa00114984d958c94ccc048c01c0044c8c8c8c94ccc064c07000852616375a603400260340046eb4c060004c050dd50020a999809180180089919299980b980d0010a4c2c6eb4c060004c050dd50020a99980918010008a99980a980a1baa00414985854ccc048cdc3a400c0022a66602a60286ea80105261616301237540066e1d2004370e9001180080192999806980118071baa0011323232323232323232323232533301c301f002149858dd7180e800980e8011bae301b001301b002375c603200260320046eb4c05c004c05c008dd6980a800980a8011bad3013001300f37540022c6e1d2000375a0026eb4004dd6800ab9a5573aaae7955cfaba05742ae881",
"hash": "e0bddcd5843130044bd19ddc1d04a70001e49bc60309fe49a33c0cf1"
}
],
"definitions": {
Expand Down
140 changes: 128 additions & 12 deletions onchain/src/validators/spacetime.ak
Original file line number Diff line number Diff line change
@@ -1,18 +1,134 @@
use aiken/transaction.{ScriptContext}
use aiken/list
use aiken/transaction.{InlineDatum, ScriptContext, Spend, Transaction}
use aiken/transaction/credential.{Address}
use aiken/transaction/value.{AssetName, PolicyId}
use asteria/types.{ShipDatum, ShipRedeemer}
use aiken/transaction/value.{from_minted_value, quantity_of}
use asteria/types.{
AssetClass, GatherFuel, MineAsteria, MoveShip, PelletDatum, Quit, ShipDatum,
ShipRedeemer,
}
use asteria/utils

validator(
_pellet_validator_address: Address,
_asteria_validator_address: Address,
_admin_token: (PolicyId, AssetName),
pellet_validator_address: Address,
asteria_validator_address: Address,
_admin_token: AssetClass,
max_distance: Int,
max_ship_fuel: Int,
fuel_per_step: Int,
) {
fn spend(
_datum: ShipDatum,
_redeemer: ShipRedeemer,
_ctx: ScriptContext,
) -> Bool {
True
pub fn spend(datum: ShipDatum, redeemer: ShipRedeemer, ctx: ScriptContext) -> Bool {
let ScriptContext { transaction, purpose } = ctx
let Transaction { inputs, outputs, mint, .. } = transaction
let ShipDatum {
fuel,
pos_x,
pos_y,
shipyard_policy,
ship_token_name,
pilot_token_name,
} = datum

expect Spend(ship_ref) = purpose
expect [ship_input] =
list.filter(inputs, fn(input) { input.output_reference == ship_ref })
expect Some(_) =
list.find(
inputs,
fn(input) {
utils.is_pilot_token_in_utxo(
input.output,
shipyard_policy,
pilot_token_name,
)
},
)

when redeemer is {
MoveShip(delta_x, delta_y) -> {
expect [ship_output] =
list.filter(
outputs,
fn(output) { utils.is_ship_token_in_utxo(output, shipyard_policy) },
)
expect InlineDatum(ship_output_datum) = ship_output.datum
expect ship_output_datum: ShipDatum = ship_output_datum

let must_hold_ship_token =
quantity_of(ship_input.output.value, shipyard_policy, ship_token_name) == 1
let distance = utils.distance(delta_x, delta_y)
let required_fuel = utils.required_fuel(distance, fuel_per_step)
let must_have_enough_fuel = required_fuel <= fuel
let must_preserve_ship_value =
ship_input.output.value == ship_output.value
let must_preserve_pilot_token =
pilot_token_name == ship_output_datum.pilot_token_name
let must_update_x = ship_output_datum.pos_x == pos_x + delta_x
let must_update_y = ship_output_datum.pos_y == pos_y + delta_y
let must_update_fuel = ship_output_datum.fuel == fuel - required_fuel
let must_respect_max_distance = distance <= max_distance

and {
must_hold_ship_token,
must_have_enough_fuel,
must_preserve_ship_value,
must_preserve_pilot_token,
must_update_x,
must_update_y,
must_update_fuel,
must_respect_max_distance,
}
}

GatherFuel(amount) -> {
expect [ship_output] =
list.filter(
outputs,
fn(output) { utils.is_ship_token_in_utxo(output, shipyard_policy) },
)
expect InlineDatum(ship_output_datum) = ship_output.datum
expect ship_output_datum: ShipDatum = ship_output_datum
expect Some(pellet_input) =
list.find(
inputs,
fn(input) { input.output.address == pellet_validator_address },
)
expect InlineDatum(pellet_datum) = pellet_input.output.datum
expect pellet_datum: PelletDatum = pellet_datum

let must_have_pellet_position =
pos_x == pellet_datum.pos_x && pos_y == pellet_datum.pos_y
let must_not_exceed_capacity = fuel + amount <= max_ship_fuel
let must_update_datum =
ship_output_datum == ShipDatum { ..datum, fuel: fuel + amount }
let must_preserve_ship_value =
ship_input.output.value == ship_output.value

and {
must_have_pellet_position,
must_not_exceed_capacity,
must_update_datum,
must_preserve_ship_value,
}
}
MineAsteria -> {
expect Some(_) =
list.find(
inputs,
fn(input) { input.output.address == asteria_validator_address },
)
let must_have_asteria_position = (pos_x, pos_y) == (0, 0)
let must_burn_ship_token =
quantity_of(from_minted_value(mint), shipyard_policy, ship_token_name) < 0
and {
must_have_asteria_position,
must_burn_ship_token,
}
}
Quit -> {
let must_burn_ship_token =
quantity_of(from_minted_value(mint), shipyard_policy, ship_token_name) < 0
must_burn_ship_token
}
}
}
}
27 changes: 12 additions & 15 deletions onchain/src/validators/tests/pellet.ak
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use aiken/transaction/credential.{Address, ScriptCredential}
use aiken/transaction/value.{
PolicyId, Value, add, from_lovelace, without_lovelace, zero,
}
use asteria/test_mock as mock
use asteria/test_utils
use asteria/types.{AssetClass, PelletDatum, Provide}
use pellet as pellet_validator
Expand All @@ -31,10 +32,9 @@ type ProvideTestOptions {
}

fn default_provide_test_options() -> ProvideTestOptions {
let shipyard_policy =
#"0298aa99f95e2fe0a0132a6bb794261fb7e7b0d988215da2f2de2005"
let admin_policy = #"0298aa99f95e2fe0a0132a6bb794261fb7e7b0d988215da2f2de2004"
let admin_token_name = "admin"
let shipyard_policy = mock.shipyard_policy
let admin_policy = mock.admin_policy
let admin_token_name = mock.admin_token_name
let value =
from_lovelace(2_000_000)
|> add(admin_policy, admin_token_name, 1)
Expand All @@ -46,21 +46,18 @@ fn default_provide_test_options() -> ProvideTestOptions {
admin_policy,
admin_token_name,
shipyard_policy,
ship_token_name: "SHIP23",
ship_token_name: mock.ship_token_name,
ship_token_amount: 1,
datum_out: PelletDatum { fuel: 25, pos_x: 1, pos_y: 2, shipyard_policy },
}
}

fn provide(options: ProvideTestOptions) -> Bool {
let pellet_addr = #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306513"
let ship_addr = #"6af53ff4f054348ad825c692dd9db8f1760a8e0eacf9af9f99306514"
let ship_addr = mock.ship_credential
let admin_asset =
AssetClass { policy: options.admin_policy, name: options.admin_token_name }
let transaction_id_1 =
#"6dcd4ce23d88e2ee95838f4b759b3456c63d219231a64a3ce6dd2bf72f5c5b6a"
let transaction_id_2 =
#"6dcd4ce23d88e2ee95838f4b759b3456c63d219231a64a3ce6dd2bf72f5c5b6b"
let transaction_id_1 = mock.transaction_id_1
let transaction_id_2 = mock.transaction_id_2
let redeemer = Provide(options.provided_amount)
let datum_in =
PelletDatum {
Expand All @@ -73,7 +70,7 @@ fn provide(options: ProvideTestOptions) -> Bool {
let output = {
let address =
Address {
payment_credential: ScriptCredential(pellet_addr),
payment_credential: ScriptCredential(mock.pellet_credential),
stake_credential: None,
}
let value = options.pellet_input_value
Expand All @@ -96,7 +93,7 @@ fn provide(options: ProvideTestOptions) -> Bool {
let output = {
let address =
Address {
payment_credential: ScriptCredential(ship_addr),
payment_credential: ScriptCredential(mock.ship_credential),
stake_credential: None,
}
let value =
Expand All @@ -118,7 +115,7 @@ fn provide(options: ProvideTestOptions) -> Bool {
let pellet_out = {
let address =
Address {
payment_credential: ScriptCredential(pellet_addr),
payment_credential: ScriptCredential(mock.pellet_credential),
stake_credential: None,
}
let value = options.pellet_output_value
Expand Down Expand Up @@ -230,7 +227,7 @@ test wrong_ship_token_name() fail {
let options =
ProvideTestOptions {
..default_provide_test_options(),
ship_token_name: "SHI23",
ship_token_name: "SHI7",
}
provide(options)
}
Expand Down
Loading

0 comments on commit ee1e8a2

Please sign in to comment.