diff --git a/lib/fortuna/utils.ak b/lib/fortuna/utils.ak index e31d761..9fe2fc4 100644 --- a/lib/fortuna/utils.ak +++ b/lib/fortuna/utils.ak @@ -8,7 +8,7 @@ use aiken/transaction.{ use aiken/transaction/credential.{ScriptCredential} use aiken/transaction/value.{AssetName, PolicyId, Value} -pub fn find_input_resolved( +pub fn resolve_output_reference( inputs: List, output_ref: OutputReference, ) -> Output { @@ -17,7 +17,7 @@ pub fn find_input_resolved( if input.output_reference == output_ref { input.output } else { - find_input_resolved(inputs, output_ref) + resolve_output_reference(inputs, output_ref) } } diff --git a/validators/hard_fork.ak b/validators/hard_fork.ak index 947c2d1..a973ecc 100644 --- a/validators/hard_fork.ak +++ b/validators/hard_fork.ak @@ -1,11 +1,15 @@ use aiken/builtin use aiken/dict -use aiken/hash -use aiken/transaction.{InlineDatum, - OutputReference, ScriptContext, Transaction} as tx -use aiken/transaction/credential.{Inline, ScriptCredential} -use aiken/transaction/value +use aiken/hash.{blake2b_256, sha2_256} +use aiken/interval.{Finite, Interval, IntervalBound} +use aiken/list +use aiken/transaction.{ + InlineDatum, Mint, Output, OutputReference, ScriptContext, Spend, Transaction, +} as tx +use aiken/transaction/credential.{Address, Inline, ScriptCredential} +use aiken/transaction/value.{tokens} use fortuna.{master_token_name, token_name} +use fortuna/parameters.{epoch_number, halving_number, initial_payout} use fortuna/types.{State} use fortuna/utils.{list_at, quantity_of} @@ -23,15 +27,254 @@ type Miner { type MineAction { nonce: ByteArray, miner: Miner, + nft_input_ref: Option, } -validator(fortuna_v1_hash: ByteArray, fork_hash: ByteArray) { +type TargetState { + nonce: ByteArray, + epoch_time: Int, + block_number: Int, + current_hash: ByteArray, + leading_zeros: Int, + difficulty_number: Int, + miner: ByteArray, +} + +validator(init_utxo_ref: OutputReference, fork_hash: ByteArray) { fn tuna(redeemer: TunaAction, ctx: ScriptContext) -> Bool { - todo + when redeemer is { + Genesis -> { + // This time genesis mints "lord tuna" and hands it over to the hard fork contract + // Then after the hard fork "lord tuna" is handed over to this miner contract + // This allows us to maintain the miner contract with the same logic as the old one + let _x = 1 + todo + } + Mine -> { + expect ScriptContext { transaction: tx, purpose: Mint(own_policy) } = + ctx + + let own_credential = ScriptCredential(own_policy) + let own_address = + Address { payment_credential: own_credential, stake_credential: None } + + // Mint(0) Mine requirement: Contract has one spend input with the policy as the payment credential + list.any(tx.inputs, fn(input) { input.output.address == own_address })? + } + Redeem -> todo + } } fn mine(datum: State, redeemer: MineAction, ctx: ScriptContext) -> Bool { - todo + // Access transaction information + let State { + block_number, + current_hash, + leading_zeros, + difficulty_number, + epoch_time, + current_posix_time, + interlink, + .. + } = datum + + let ScriptContext { transaction, purpose } = ctx + + expect Spend(own_reference) = purpose + + let Transaction { inputs, outputs, mint, validity_range, .. } = transaction + + let mint = value.from_minted_value(mint) + + let own_input = fortuna.own_validator_input_utxo(inputs, own_reference) + + let Output { address: in_address, value: in_value, .. } = own_input + + let credential = in_address.payment_credential + + expect ScriptCredential(own_validator_hash) = credential + + let MineAction { nonce, miner, nft_input_ref } = redeemer + + // Spend(0) requirement: Contract has only one output going back to itself + expect [own_output] = + list.filter(outputs, fn(output: Output) { output.address == in_address }) + + let Output { datum: out_datum, value: out_value, .. } = own_output + + // Time Range Span is 3 minutes or less + let Interval { + upper_bound: IntervalBound { + bound_type: upper_range, + is_inclusive: upper_is_inclusive, + }, + lower_bound: IntervalBound { + bound_type: lower_range, + is_inclusive: lower_is_inclusive, + }, + } = validity_range + + // We have a constant expectation of the transaction time range + expect Finite(upper_range) = upper_range + expect Finite(lower_range) = lower_range + let averaged_current_time = ( upper_range - lower_range ) / 2 + lower_range + + // Posix time is in milliseconds + // Spend(1) requirement: Time range span is 3 minutes or less and inclusive + expect and { + !upper_is_inclusive?, + lower_is_inclusive?, + (upper_range - lower_range <= 180000)?, + } + // + // In case you are wondering here is what enables pools + // A miner can be a pkh or an nft + // Nfts can come from any input, even validators + // So any validator logic can be enforced to run along with fortuna + expect + when miner is { + Pkh(signer) -> list.has(transaction.extra_signatories, signer) + Nft(nft_policy, nft_name) -> { + expect Some(input_ref) = nft_input_ref + // + let quantity = + utils.resolve_output_reference(inputs, input_ref).value + |> quantity_of(nft_policy, nft_name) + // + // Spend(2) requirement: Input has nft + quantity == 1 + } + } + // + // Target state now includes a miner credential + let target = + TargetState { + nonce, + epoch_time, + block_number, + current_hash, + leading_zeros, + difficulty_number, + miner: blake2b_256(builtin.serialise_data(miner)), + } + + let found_bytearray = + target + |> builtin.serialise_data() + |> sha2_256() + |> sha2_256() + + let (found_difficulty_number, found_leading_zeros) = + fortuna.format_found_bytearray(found_bytearray) + + // Mining Difficulty Met + // Spend(2) requirement: Found difficulty is less than or equal to the current difficulty + // We do this by checking the leading zeros and the difficulty number + expect or { + (found_leading_zeros > leading_zeros)?, + and { + (found_leading_zeros == leading_zeros)?, + (found_difficulty_number < difficulty_number)?, + }, + } + // + // Spend(3) requirement: Input has master token + expect + (quantity_of(in_value, own_validator_hash, fortuna.master_token_name) == 1)? + // + // Spend(4) requirement: Only one type of token minted under the validator policy + expect [(token_name, quantity)] = + mint + |> tokens(own_validator_hash) + |> dict.to_list + + let halving_exponent = block_number / halving_number + + let expected_quantity = + if halving_exponent > 29 { + 0 + } else { + initial_payout / fortuna.two_exponential(halving_exponent) + } + + // Spend(5) requirement: Minted token is the correct name and amount + expect and { + (token_name == fortuna.token_name)?, + (quantity == expected_quantity)?, + } + // + // Spend(6) requirement: Output has only master token and ada + expect + fortuna.value_has_only_master_and_lovelace(out_value, own_validator_hash)? + // Check output datum contains correct epoch time, block number, hash, and leading zeros + // Check for every divisible by 2016 block: + // - Epoch time resets + // - leading zeros is adjusted based on percent of hardcoded target time for 2016 blocks vs epoch time + expect InlineDatum(output_datum) = out_datum + // Spend(7) requirement: Expect Output Datum to be of type State + expect State { + block_number: out_block_number, + current_hash: out_current_hash, + leading_zeros: out_leading_zeros, + difficulty_number: out_difficulty_number, + epoch_time: out_epoch_time, + current_posix_time: out_current_posix_time, + interlink: out_interlink, + extra, + }: State = output_datum + + // Spend(8) requirement: Check output has correct difficulty number, leading zeros, and epoch time + expect + if block_number % epoch_number == 0 && block_number > 0 { + // use total epoch time with target epoch time to get difficulty adjustment ratio + // ratio maxes out at 4/1 and mins to 1/4 + let total_epoch_time = + epoch_time + averaged_current_time - current_posix_time + let (adjustment_numerator, adjustment_denominator) = + fortuna.get_difficulty_adjustment(total_epoch_time) + // Now use ratio to find new leading zeros difficulty + let (new_difficulty, new_leading_zeroes) = + fortuna.get_new_difficulty( + difficulty_number, + leading_zeros, + adjustment_numerator, + adjustment_denominator, + ) + // + and { + (new_leading_zeroes == out_leading_zeros)?, + (new_difficulty == out_difficulty_number)?, + (0 == out_epoch_time)?, + } + } else { + let new_epoch_time = + epoch_time + averaged_current_time - current_posix_time + // + and { + (leading_zeros == out_leading_zeros)?, + (difficulty_number == out_difficulty_number)?, + (new_epoch_time == out_epoch_time)?, + } + } + // + // Spend(9) requirement: Output posix time is the averaged current time + expect (out_current_posix_time == averaged_current_time)? + // + // Spend(10) requirement: Output block number is the input block number + 1 + // Spend(11) requirement: Output current hash is the target hash + expect + (block_number + 1 == out_block_number && out_current_hash == found_bytearray)? + //Spend(12) requirement: Check output extra field is within a certain size + expect (builtin.length_of_bytearray(builtin.serialise_data(extra)) <= 512)? + // Spend(13) requirement: Check output interlink is correct + (fortuna.calculate_interlink( + interlink, + found_bytearray, + found_leading_zeros, + found_difficulty_number, + difficulty_number, + leading_zeros, + ) == out_interlink)? } } @@ -42,11 +285,7 @@ type HardForkStatus { } type ForkDatum { - HardForkState { - status: HardForkStatus, - miner_support: Int, - fortuna_v1_height: Int, - } + HardForkState { status: HardForkStatus, fortuna_v1_height: Int } MinerGlobalLockState { miner_locked_tuna: Int } GlobalLockState { locked_tuna: Int } NftState { nft_key: ByteArray } @@ -86,7 +325,7 @@ const hard_fork_state_token: ByteArray = "hfs" // maybe we should do time instead of block height? validator( - utxo_ref: OutputReference, + init_utxo_ref: OutputReference, fortuna_v1_hash: ByteArray, block_height: Int, ) { @@ -121,7 +360,10 @@ validator( lock_state_ref, } -> { let hard_fork_state_input = - utils.find_input_resolved(reference_inputs, hard_fork_state_ref) + utils.resolve_output_reference( + reference_inputs, + hard_fork_state_ref, + ) expect utils.value_has_nft_and_lovelace( @@ -186,7 +428,7 @@ validator( expect InlineDatum(out_lock_datum) = lock_output.datum let lock_input = - utils.find_input_resolved(inputs, lock_state_ref) + utils.resolve_output_reference(inputs, lock_state_ref) when lock_type is { // In miner lock we also check for the presence of the fortuna v1 script @@ -215,7 +457,10 @@ validator( }: ForkDatum = out_lock_datum let tuna_v1_input = - utils.find_input_resolved(inputs, fortuna_v1_input_ref) + utils.resolve_output_reference( + inputs, + fortuna_v1_input_ref, + ) expect quantity_of( @@ -273,7 +518,7 @@ validator( let Transaction { inputs, withdrawals, .. } = transaction - let own_input = utils.find_input_resolved(inputs, own_ref) + let own_input = utils.resolve_output_reference(inputs, own_ref) let own_withdrawal = Inline(own_input.address.payment_credential)