From d7956a36f63edadc42beba1d2bce7c15752b3ea9 Mon Sep 17 00:00:00 2001 From: Francisco Joray Date: Tue, 9 Apr 2024 02:10:50 -0300 Subject: [PATCH 1/3] completed asteria validator checks. --- onchain/docs/mvp-design/design.md | 5 +- onchain/src/plutus.json | 36 ++++++------- onchain/src/validators/asteria.ak | 89 +++++++++++++++++++++++++++---- 3 files changed, 101 insertions(+), 29 deletions(-) diff --git a/onchain/docs/mvp-design/design.md b/onchain/docs/mvp-design/design.md index 2aa371d..0a05f96 100644 --- a/onchain/docs/mvp-design/design.md +++ b/onchain/docs/mvp-design/design.md @@ -105,16 +105,17 @@ Pays the min ada locked in the `ShipState` UTxO back to the ship owner and burns ## Validators & Minting Policies ### Asteria validator: -* Params: `AdminToken`. +* Params: `AdminToken`, `SHIP_MINT_FEE` and `MAX_ASTERIA_MINING`. #### *AddNewShip Redeemer* * `AsteriaUTxO` output value equals input value plus the `SHIP_MINT_FEE`. * `AdminToken` is in the input. -* datum doesn't change. +* datum `ship_counter` field is incremented by 1. #### *Mine Redeemer* * `ShipToken` is present in some input. * `AsteriaUTxO` output value has at most `MAX_ASTERIA_MINING`% adas less than input value. +* datum doesn't change. ### Pellet validator: * Params: `AdminToken`. diff --git a/onchain/src/plutus.json b/onchain/src/plutus.json index 99d1af4..c491dea 100644 --- a/onchain/src/plutus.json +++ b/onchain/src/plutus.json @@ -14,27 +14,39 @@ { "title": "asteria.spend", "datum": { - "title": "_datum", + "title": "datum", "schema": { "$ref": "#/definitions/asteria~1types~1AsteriaDatum" } }, "redeemer": { - "title": "_redeemer", + "title": "redeemer", "schema": { "$ref": "#/definitions/asteria~1types~1AsteriaRedeemer" } }, "parameters": [ { - "title": "_admin_token", + "title": "admin_token", "schema": { - "$ref": "#/definitions/Tuple$ByteArray_ByteArray" + "$ref": "#/definitions/asteria~1types~1AssetClass" + } + }, + { + "title": "ship_mint_lovelace_fee", + "schema": { + "$ref": "#/definitions/Int" + } + }, + { + "title": "max_asteria_mining", + "schema": { + "$ref": "#/definitions/Int" } } ], - "compiledCode": "5898010000323232323232232232232253330084a229309b2b19299980399b874800000454ccc028c024dd50010a4c2c2a66600e66e1d20020011533300a300937540042930b0b18039baa00153330043370e900018029baa001132323232533300b300d002149858dd7180580098058011bad3009001300637540022c6466ec0c01c004c01cc020004dd6000ab9a5573aaae7955cfaba157441", - "hash": "27cbc8819bede902bc58445bb6fc3d8c91341d0f48f58b5ea7adb72e" + "compiledCode": "5907040100003232323232323222322322323232232322533300e32323232323232325333016300b3017375400e26464646464a66603c60420042646464a6660426048004264a66603e66e1d20043020375400226464646464a666048603a604a6ea80684c8c8c94ccc09ccdc4240006660086eacc030c0a4dd5180618149baa00d375c602060526ea8098dd7180618149baa0261533302700315333027002100114a0294052819b8f375c601660506ea801c044cdc39bad300e3027375400c66e0004520023375e6012604c6ea801cdd319912999813180f8118800899191980080080191299981600089981699bb04c1014000374c00697adef6c60132323232533302d3375e66012911000024c103d879800013303133760981014000374c00e00a2a66605a66e3d22100002132533302e3027302f375400226606466ec1301014000303330303754002008200864a66605ca66606200229445280a6103d87a80001300d33032374c00297ae03233001001002225333032001133033337609801014000375005897adef6c6013232323253330333375e6601e911000024c103d879800013303733760981014000375006000a2a66606666e3d221000021325333034302d3035375400226607066ec1301014000303930363754002008200864a666068605a002298103d87a80001301333038375000297ae03370000206026606e66ec0dd48011ba800133006006003375a60680066eb8c0c8008c0d8008c0d00044cc0c4cdd81ba9002374c0026600c00c0066eacc0b800cdd7181600118180011817000991900119198008008011129998160008a4c264a66605a002293099192999816181298169baa33008375c605a60620086eb8c0b40084cc014014cc0c000800458c0c4008c0bc004c0bc004cc0a8cdd8261014000375004697adef6c60225333026337200040022980103d8798000153330263371e0040022980103d87a800014c103d87b800037566012604c6ea8c024c098dd50050a999812180e98129baa323300100101422533302900114c0103d87a80001323253330283232330010013300200232533302b3020302c3754002297adef6c60137566060605a6ea8004cc020dd5980798161baa300f302c375400802a44a66605c00229404c94ccc0b0cdc7999b8c48000dc6a45045348495000375c606200491104534849500014a2266006006002606200244a66605a002297ae013302e302b302f001330020023030001130073302c0024bd70099802002000981680118158008992999812980f18131baa0011323253330273232323371266e08dd698178009bad302f3030002337046eb4c0bc008dd69817981800098159baa3232300a3302f375066e08dd698180011bad30300013302f375066e08dd6981818188011bad303030310014bd7018161baa32323232300d33032375066e04cdc11bad3033004001337046eb4c0cc00800ccc0c8dd419b820030014bd701bad30323033001302e37546062605c6ea8020dd69818181880098161baa300348008c0acdd5180118021bab300e302b3754601c60566ea803cc0a8dd5180098019bab300d302a37540164600e660586ea0004cc0b130010101004bd7008008a503375e04000e4666006002910100488100165333024301d4832004530103d87a80001533302433710906400a400026006660506006660506ea0cdc0a400003e660506ea0cdc024000906380a5eb812f5c026006660506006660506ea007ccc0a1301021864004bd7025eb8058888c94ccc09cc070c0a0dd50008a400026eb4c0b0c0a4dd5000992999813980e18141baa00114c103d87a8000132330010013756605a60546ea8008894ccc0b0004530103d87a80001323232533302c3371e00e6eb8c0b400c4c02ccc0c0dd4000a5eb804cc014014008dd6981680118180011817000998020018011119198008008019129998148008a60103d87a8000132323253330293371e00c6eb8c0a800c4c020cc0b4dd3000a5eb804cc014014008dd59815001181680118158009ba548000c060004c090c084dd50008b18119812181218101baa0011630220013300400a23375e600c603e6ea8004c018c07cdd51801180f9baa00323021302200116301f0013300100823375e600660386ea800401088c8cc00400400c894ccc07c00452f5c026464a66603c600a00426604400466008008002266008008002604600460420024603a002603660306ea801c58dd7180d180d8011bad30190013015375401a6eb0c05cc060c060008dd6180b00098091baa3015002301430150013010375400229309b2b19299980698030008a99980818079baa00314985854ccc034c00800454ccc040c03cdd50018a4c2c2c601a6ea8008dc3a400460020064a666012600460146ea80044c8c8c8c94ccc040c04c00852616375c602200260220046eb4c03c004c02cdd50008b1b8748000dd68009bad0015734aae7555cf2ab9f5740ae855d11", + "hash": "3a75534efcf27b51da03e46d531facefc6cb531c69c9aee02419faec" }, { "title": "pellet.spend", @@ -184,18 +196,6 @@ } ] }, - "Tuple$ByteArray_ByteArray": { - "title": "Tuple", - "dataType": "list", - "items": [ - { - "$ref": "#/definitions/ByteArray" - }, - { - "$ref": "#/definitions/ByteArray" - } - ] - }, "aiken/transaction/credential/Address": { "title": "Address", "description": "A Cardano `Address` typically holding one or two credential references.\n\n Note that legacy bootstrap addresses (a.k.a. 'Byron addresses') are\n completely excluded from Plutus contexts. Thus, from an on-chain\n perspective only exists addresses of type 00, 01, ..., 07 as detailed\n in [CIP-0019 :: Shelley Addresses](https://github.com/cardano-foundation/CIPs/tree/master/CIP-0019/#shelley-addresses).", diff --git a/onchain/src/validators/asteria.ak b/onchain/src/validators/asteria.ak index 34003d8..438bb99 100644 --- a/onchain/src/validators/asteria.ak +++ b/onchain/src/validators/asteria.ak @@ -1,13 +1,84 @@ -use aiken/transaction.{ScriptContext} -use aiken/transaction/value.{AssetName, PolicyId} -use asteria/types.{AsteriaDatum, AsteriaRedeemer} +use aiken/list +use aiken/math/rational.{compare_with, from_int} +use aiken/transaction.{InlineDatum, ScriptContext, Spend, Transaction} +use aiken/transaction/value.{ada_asset_name, ada_policy_id} +use asteria/types.{AddNewShip, AssetClass, AsteriaDatum, AsteriaRedeemer, Mine} +use asteria/utils -validator(_admin_token: (PolicyId, AssetName)) { - fn spend( - _datum: AsteriaDatum, - _redeemer: AsteriaRedeemer, - _ctx: ScriptContext, +validator( + admin_token: AssetClass, + ship_mint_lovelace_fee: Int, + max_asteria_mining: Int, +) { + pub fn spend( + datum: AsteriaDatum, + redeemer: AsteriaRedeemer, + ctx: ScriptContext, ) -> Bool { - True + let ScriptContext { transaction, purpose } = ctx + let Transaction { inputs, outputs, .. } = transaction + let AsteriaDatum { ship_counter, shipyard_policy } = datum + expect Spend(asteria_ref) = purpose + expect [asteria_input] = + list.filter(inputs, fn(input) { input.output_reference == asteria_ref }) + expect [asteria_output] = + list.filter( + outputs, + fn(output) { output.address == asteria_input.output.address }, + ) + expect InlineDatum(asteria_output_datum) = asteria_output.datum + expect asteria_output_datum: AsteriaDatum = asteria_output_datum + when redeemer is { + AddNewShip -> { + let must_hold_admin_token = + value.quantity_of( + asteria_input.output.value, + admin_token.policy, + admin_token.name, + ) > 0 + let must_add_fee = + asteria_output.value == ( + asteria_input.output.value + |> value.add(ada_policy_id, ada_asset_name, ship_mint_lovelace_fee) + ) + let must_increment_counter = + asteria_output_datum.ship_counter == ship_counter + 1 + let must_preserve_shipyard_policy = + asteria_output_datum.shipyard_policy == shipyard_policy + + and { + must_hold_admin_token, + must_add_fee, + must_increment_counter, + must_preserve_shipyard_policy, + } + } + + Mine -> { + expect Some(_) = + list.find( + inputs, + fn(input) { + utils.is_ship_token_in_utxo(input.output, shipyard_policy) + }, + ) + expect Some(percentage) = rational.new(max_asteria_mining, 100) + let rewards = value.lovelace_of(asteria_input.output.value) + let must_respect_max_mining = + compare_with( + from_int(value.lovelace_of(asteria_output.value)), + >=, + rational.mul( + from_int(rewards), + rational.sub(from_int(1), percentage), + ), + ) + let must_preserve_datum = datum == asteria_output_datum + and { + must_respect_max_mining, + must_preserve_datum, + } + } + } } } From e3ff3e1a005cd6d11aacead227fc47d58cba6850 Mon Sep 17 00:00:00 2001 From: Francisco Joray Date: Tue, 9 Apr 2024 02:11:24 -0300 Subject: [PATCH 2/3] asteria validator tests. --- onchain/src/validators/tests/asteria.ak | 335 ++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 onchain/src/validators/tests/asteria.ak diff --git a/onchain/src/validators/tests/asteria.ak b/onchain/src/validators/tests/asteria.ak new file mode 100644 index 0000000..cdaf84d --- /dev/null +++ b/onchain/src/validators/tests/asteria.ak @@ -0,0 +1,335 @@ +use aiken/bytearray +use aiken/dict +use aiken/interval.{Finite, Interval, IntervalBound} +use aiken/transaction.{ + InlineDatum, Input, NoDatum, Output, OutputReference, ScriptContext, Spend, + Transaction, TransactionId, +} +use aiken/transaction/credential.{Address, ScriptCredential} +use aiken/transaction/value.{ada_asset_name, ada_policy_id} +use asteria +use asteria/test_mock as mock +use asteria/test_utils +use asteria/types.{AddNewShip, AssetClass, AsteriaDatum, Mine} + +// ============================================================================================== +// AddNewShip Tests +// ============================================================================================== + +type AddTestOptions { + paid_fee: Bool, + include_admin_token: Bool, + update_counter: Bool, +} + +fn default_add_options() { + AddTestOptions { + paid_fee: True, + include_admin_token: True, + update_counter: True, + } +} + +fn addNewShip(options: AddTestOptions) -> Bool { + let ship_mint_lovelace_fee = 2_000 + let max_asteria_mining = 40 + let ship_counter = 7 + let admin_token = + AssetClass { policy: mock.admin_policy, name: mock.admin_token_name } + let redeemer = AddNewShip + let asteria_address = + Address { + payment_credential: ScriptCredential(mock.asteria_credential), + stake_credential: None, + } + let asteria_value = + if options.include_admin_token { + value.from_lovelace(10_000_000) + |> value.add(mock.admin_policy, mock.admin_token_name, 1) + } else { + value.from_lovelace(10_000_000) + } + let asteria_datum = + AsteriaDatum { ship_counter, shipyard_policy: mock.shipyard_policy } + let asteria_in = { + let output = + Output { + address: asteria_address, + value: asteria_value, + datum: InlineDatum(asteria_datum), + reference_script: None, + } + let output_reference = + OutputReference { + transaction_id: TransactionId { hash: mock.transaction_id_1 }, + output_index: 0, + } + Input { output_reference, output } + } + let asteria_out = + Output { + address: asteria_address, + value: if options.paid_fee { + asteria_in.output.value + |> value.add(ada_policy_id, ada_asset_name, ship_mint_lovelace_fee) + } else { + asteria_in.output.value + }, + datum: if options.update_counter { + InlineDatum(AsteriaDatum { ..asteria_datum, ship_counter: 8 }) + } else { + InlineDatum(asteria_datum) + }, + reference_script: None, + } + let tx = + Transaction { + inputs: [asteria_in], + reference_inputs: [], + outputs: [asteria_out], + fee: value.from_lovelace(5_000), + mint: value.to_minted_value(value.zero()), + certificates: [], + withdrawals: dict.new(), + validity_range: Interval { + lower_bound: IntervalBound { bound_type: Finite(1), is_inclusive: True }, + upper_bound: IntervalBound { + bound_type: Finite(10), + is_inclusive: True, + }, + }, + extra_signatories: [], + redeemers: dict.new() + |> dict.insert( + key: Spend( + OutputReference { + transaction_id: TransactionId { hash: mock.transaction_id_1 }, + output_index: 0, + }, + ), + value: { + let redeemer_data: Data = redeemer + redeemer_data + }, + compare: test_utils.script_purpose_compare, + ), + datums: dict.new() + |> dict.insert( + mock.transaction_id_1, + { + let datum_data: Data = InlineDatum(asteria_datum) + datum_data + }, + compare: bytearray.compare, + ), + id: TransactionId { hash: mock.transaction_id_3 }, + } + let spend_ctx = + ScriptContext { + transaction: tx, + purpose: Spend( + OutputReference { + transaction_id: TransactionId { hash: mock.transaction_id_1 }, + output_index: 0, + }, + ), + } + let result = + asteria.spend( + admin_token, + ship_mint_lovelace_fee, + max_asteria_mining, + asteria_datum, + redeemer, + spend_ctx, + ) + result +} + +test add_ok() { + addNewShip(default_add_options()) +} + +test add_no_fee_paid() fail { + addNewShip(AddTestOptions { ..default_add_options(), paid_fee: False }) +} + +test add_no_admin_token() fail { + addNewShip( + AddTestOptions { ..default_add_options(), include_admin_token: False }, + ) +} + +test add_counter_not_updated() fail { + addNewShip(AddTestOptions { ..default_add_options(), update_counter: False }) +} + +// ============================================================================================== +// Mine Tests +// ============================================================================================== + +type MineTestOptions { + lovelace_mined: Int, + preserve_counter: Bool, + include_ship_token: Bool, +} + +fn default_mine_options() { + MineTestOptions { + lovelace_mined: 4_000_000, + preserve_counter: True, + include_ship_token: True, + } +} + +fn mine(options: MineTestOptions) -> Bool { + let ship_mint_lovelace_fee = 2_000 + let max_asteria_mining = 40 + let ship_counter = 7 + let admin_token = + AssetClass { policy: mock.admin_policy, name: mock.admin_token_name } + let redeemer = Mine + let asteria_address = + Address { + payment_credential: ScriptCredential(mock.asteria_credential), + stake_credential: None, + } + let asteria_datum = + AsteriaDatum { ship_counter, shipyard_policy: mock.shipyard_policy } + let asteria_in = { + let output = + Output { + address: asteria_address, + value: value.from_lovelace(10_000_000) + |> value.add(mock.admin_policy, mock.admin_token_name, 1), + datum: InlineDatum(asteria_datum), + reference_script: None, + } + let output_reference = + OutputReference { + transaction_id: TransactionId { hash: mock.transaction_id_1 }, + output_index: 0, + } + Input { output_reference, output } + } + let ship_address = + Address { + payment_credential: ScriptCredential(mock.ship_credential), + stake_credential: None, + } + let ship_value = + if options.include_ship_token { + value.from_lovelace(2_000_000) + |> value.add(mock.shipyard_policy, mock.ship_token_name, 1) + } else { + value.from_lovelace(2_000_000) + } + + let ship_in = { + let output = + Output { + address: ship_address, + value: ship_value, + datum: NoDatum, + reference_script: None, + } + let output_reference = + OutputReference { + transaction_id: TransactionId { hash: mock.transaction_id_2 }, + output_index: 0, + } + Input { output_reference, output } + } + let asteria_out = + Output { + address: asteria_address, + value: asteria_in.output.value + |> value.add(ada_policy_id, ada_asset_name, -options.lovelace_mined), + datum: if options.preserve_counter { + InlineDatum(asteria_datum) + } else { + InlineDatum( + AsteriaDatum { ..asteria_datum, ship_counter: ship_counter + 1 }, + ) + }, + reference_script: None, + } + let tx = + Transaction { + inputs: [asteria_in, ship_in], + reference_inputs: [], + outputs: [asteria_out], + fee: value.from_lovelace(5_000), + mint: value.to_minted_value(value.zero()), + certificates: [], + withdrawals: dict.new(), + validity_range: Interval { + lower_bound: IntervalBound { bound_type: Finite(1), is_inclusive: True }, + upper_bound: IntervalBound { + bound_type: Finite(10), + is_inclusive: True, + }, + }, + extra_signatories: [], + redeemers: dict.new() + |> dict.insert( + key: Spend( + OutputReference { + transaction_id: TransactionId { hash: mock.transaction_id_1 }, + output_index: 0, + }, + ), + value: { + let redeemer_data: Data = redeemer + redeemer_data + }, + compare: test_utils.script_purpose_compare, + ), + datums: dict.new() + |> dict.insert( + mock.transaction_id_1, + { + let datum_data: Data = InlineDatum(asteria_datum) + datum_data + }, + compare: bytearray.compare, + ), + id: TransactionId { hash: mock.transaction_id_3 }, + } + let spend_ctx = + ScriptContext { + transaction: tx, + purpose: Spend( + OutputReference { + transaction_id: TransactionId { hash: mock.transaction_id_1 }, + output_index: 0, + }, + ), + } + let result = + asteria.spend( + admin_token, + ship_mint_lovelace_fee, + max_asteria_mining, + asteria_datum, + redeemer, + spend_ctx, + ) + result +} + +test mine_ok() { + mine(default_mine_options()) +} + +test mine_no_ship_token() fail { + mine(MineTestOptions { ..default_mine_options(), include_ship_token: False }) +} + +test mine_exceed_mining() fail { + mine(MineTestOptions { ..default_mine_options(), lovelace_mined: 4_000_001 }) +} + +test mine_alter_counter() fail { + mine(MineTestOptions { ..default_mine_options(), preserve_counter: False }) +} From dc7debf35a4a0ee66d27de67bffde8d2e1b68663 Mon Sep 17 00:00:00 2001 From: Francisco Joray Date: Tue, 9 Apr 2024 02:16:30 -0300 Subject: [PATCH 3/3] fixed typo. --- onchain/src/validators/tests/asteria.ak | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/onchain/src/validators/tests/asteria.ak b/onchain/src/validators/tests/asteria.ak index cdaf84d..2d1f675 100644 --- a/onchain/src/validators/tests/asteria.ak +++ b/onchain/src/validators/tests/asteria.ak @@ -17,14 +17,14 @@ use asteria/types.{AddNewShip, AssetClass, AsteriaDatum, Mine} // ============================================================================================== type AddTestOptions { - paid_fee: Bool, + pay_fee: Bool, include_admin_token: Bool, update_counter: Bool, } fn default_add_options() { AddTestOptions { - paid_fee: True, + pay_fee: True, include_admin_token: True, update_counter: True, } @@ -69,7 +69,7 @@ fn addNewShip(options: AddTestOptions) -> Bool { let asteria_out = Output { address: asteria_address, - value: if options.paid_fee { + value: if options.pay_fee { asteria_in.output.value |> value.add(ada_policy_id, ada_asset_name, ship_mint_lovelace_fee) } else { @@ -151,7 +151,7 @@ test add_ok() { } test add_no_fee_paid() fail { - addNewShip(AddTestOptions { ..default_add_options(), paid_fee: False }) + addNewShip(AddTestOptions { ..default_add_options(), pay_fee: False }) } test add_no_admin_token() fail {