diff --git a/lib/archethic/bootstrap/network_init.ex b/lib/archethic/bootstrap/network_init.ex index d45e5f6f9..f760c322a 100644 --- a/lib/archethic/bootstrap/network_init.ex +++ b/lib/archethic/bootstrap/network_init.ex @@ -193,9 +193,9 @@ defmodule Archethic.Bootstrap.NetworkInit do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, nil) |> LedgerValidation.mint_token_utxos(tx, timestamp, 1) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx_type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(address, timestamp) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx_type) |> LedgerValidation.to_ledger_operations() validation_stamp = diff --git a/lib/archethic/mining/ledger_validation.ex b/lib/archethic/mining/ledger_validation.ex index 766ff43e5..ee7052d52 100644 --- a/lib/archethic/mining/ledger_validation.ex +++ b/lib/archethic/mining/ledger_validation.ex @@ -5,7 +5,8 @@ defmodule Archethic.Mining.LedgerValidation do @unit_uco 100_000_000 - defstruct transaction_movements: [], + defstruct state: :init, + transaction_movements: [], unspent_outputs: [], fee: 0, consumed_inputs: [], @@ -31,6 +32,25 @@ defmodule Archethic.Mining.LedgerValidation do alias Archethic.TransactionChain.TransactionData + @typedoc """ + LedgerValidation should execute functions in a specific order. + To avoid miss order, state is updated to ensure order is respected + State is updated following + - :init + - :filtered_inputs + - :utxos_minted + - :sufficient_funds_validated + - :inputs_consumed + - :movements_resolved + """ + @type state() :: + :init + | :filtered_inputs + | :utxos_minted + | :sufficient_funds_validated + | :inputs_consumed + | :movements_resolved + @typedoc """ - Transaction movements: represents the pending transaction ledger movements - Unspent outputs: represents the new unspent outputs @@ -38,6 +58,7 @@ defmodule Archethic.Mining.LedgerValidation do - Consumed inputs: represents the list of inputs consumed to produce the unspent outputs """ @type t() :: %__MODULE__{ + state: state(), transaction_movements: list(TransactionMovement.t()), unspent_outputs: list(UnspentOutput.t()), fee: non_neg_integer(), @@ -58,17 +79,20 @@ defmodule Archethic.Mining.LedgerValidation do def burning_address, do: @burning_address @doc """ - Filter inputs that can be used in this transaction + Filter inputs that can be used in this transaction """ @spec filter_usable_inputs( ops :: t(), inputs :: list(VersionedUnspentOutput.t()), contract_context :: ContractContext.t() | nil ) :: t() - def filter_usable_inputs(ops, inputs, nil), do: %__MODULE__{ops | inputs: inputs} + def filter_usable_inputs(ops = %__MODULE__{state: :init}, inputs, nil), + do: %__MODULE__{ops | inputs: inputs} |> next_state() - def filter_usable_inputs(ops, inputs, contract_context), - do: %__MODULE__{ops | inputs: ContractContext.ledger_inputs(contract_context, inputs)} + def filter_usable_inputs(ops = %__MODULE__{state: :init}, inputs, contract_context) do + %__MODULE__{ops | inputs: ContractContext.ledger_inputs(contract_context, inputs)} + |> next_state() + end @doc """ Build some ledger operations from a specific transaction @@ -80,27 +104,30 @@ defmodule Archethic.Mining.LedgerValidation do protocol_version :: non_neg_integer() ) :: t() def mint_token_utxos( - ops, + ops = %__MODULE__{state: :filtered_inputs}, %Transaction{address: address, type: type, data: %TransactionData{content: content}}, timestamp, protocol_version ) when type in [:token, :mint_rewards] and not is_nil(timestamp) do - case Jason.decode(content) do - {:ok, json} -> - minted_utxos = - json - |> create_token_utxos(address, timestamp) - |> VersionedUnspentOutput.wrap_unspent_outputs(protocol_version) - - %__MODULE__{ops | minted_utxos: minted_utxos} + new_ops = + case Jason.decode(content) do + {:ok, json} -> + minted_utxos = + json + |> create_token_utxos(address, timestamp) + |> VersionedUnspentOutput.wrap_unspent_outputs(protocol_version) + + %__MODULE__{ops | minted_utxos: minted_utxos} + + _ -> + ops + end - _ -> - ops - end + next_state(new_ops) end - def mint_token_utxos(ops, _, _, _), do: ops + def mint_token_utxos(ops = %__MODULE__{state: :filtered_inputs}, _, _, _), do: next_state(ops) defp create_token_utxos( %{"token_reference" => token_ref, "supply" => supply}, @@ -186,34 +213,43 @@ defmodule Archethic.Mining.LedgerValidation do @doc """ Build the resolved view of the movement, with the resolved address and convert MUCO movement to UCO movement + + **MUST** be done after sufficient_funds? and consume_inputs """ @spec build_resolved_movements( ops :: t(), - movements :: list(TransactionMovement.t()), resolved_addresses :: %{Crypto.prepended_hash() => Crypto.prepended_hash()}, tx_type :: Transaction.transaction_type() ) :: t() - def build_resolved_movements(ops, movements, resolved_addresses, tx_type) do + def build_resolved_movements( + ops = %__MODULE__{ + state: :inputs_consumed, + transaction_movements: movements + }, + resolved_addresses, + tx_type + ) do resolved_movements = movements |> TransactionMovement.resolve_addresses(resolved_addresses) |> Enum.map(&TransactionMovement.maybe_convert_reward(&1, tx_type)) |> TransactionMovement.aggregate() - %__MODULE__{ops | transaction_movements: resolved_movements} + %__MODULE__{ops | transaction_movements: resolved_movements} |> next_state() end @doc """ Determine if the transaction has enough funds for it's movements """ - @spec validate_sufficient_funds(ops :: t()) :: t() + @spec validate_sufficient_funds(ops :: t(), list(TransactionMovement.t())) :: t() def validate_sufficient_funds( ops = %__MODULE__{ + state: :utxos_minted, fee: fee, inputs: inputs, - minted_utxos: minted_utxos, - transaction_movements: movements - } + minted_utxos: minted_utxos + }, + movements ) do balances = %{uco: uco_balance, token: tokens_balance} = ledger_balances(inputs ++ minted_utxos) @@ -226,8 +262,10 @@ defmodule Archethic.Mining.LedgerValidation do | sufficient_funds?: sufficient_funds?(uco_balance, uco_to_spend, tokens_balance, tokens_to_spend), balances: balances, - amount_to_spend: amount_to_spend + amount_to_spend: amount_to_spend, + transaction_movements: movements } + |> next_state() end defp total_to_spend(fee, movements) do @@ -287,6 +325,7 @@ defmodule Archethic.Mining.LedgerValidation do """ @spec to_ledger_operations(ops :: t()) :: LedgerOperations.t() def to_ledger_operations(%__MODULE__{ + state: :movements_resolved, transaction_movements: movements, unspent_outputs: utxos, fee: fee, @@ -312,10 +351,26 @@ defmodule Archethic.Mining.LedgerValidation do encoded_state :: State.encoded() | nil, contract_context :: ContractContext.t() | nil ) :: t() - def consumed_inputs(ops = %__MODULE__{sufficient_funds?: false}), do: ops + def consume_inputs( + ops, + change_address, + timestamp, + encoded_state \\ nil, + contract_context \\ nil + ) + + def consume_inputs( + ops = %__MODULE__{state: :sufficient_funds_validated, sufficient_funds?: false}, + _, + _, + _, + _ + ), + do: next_state(ops) def consume_inputs( ops = %__MODULE__{ + state: :sufficient_funds_validated, inputs: inputs, minted_utxos: minted_utxos, balances: %{uco: uco_balance, token: tokens_balance}, @@ -323,8 +378,8 @@ defmodule Archethic.Mining.LedgerValidation do }, change_address, timestamp = %DateTime{}, - encoded_state \\ nil, - contract_context \\ nil + encoded_state, + contract_context ) do # Since AEIP-19 we can consume from minted tokens # Sort inputs, to have consistent results across all nodes @@ -369,6 +424,7 @@ defmodule Archethic.Mining.LedgerValidation do | unspent_outputs: new_unspent_outputs, consumed_inputs: versioned_consumed_utxos } + |> next_state() end defp tokens_utxos( @@ -511,4 +567,20 @@ defmodule Archethic.Mining.LedgerValidation do [new_utxo | utxos] end + + defp next_state(ops = %__MODULE__{state: :init}), do: %__MODULE__{ops | state: :filtered_inputs} + + defp next_state(ops = %__MODULE__{state: :filtered_inputs}), + do: %__MODULE__{ops | state: :utxos_minted} + + defp next_state(ops = %__MODULE__{state: :utxos_minted}), + do: %__MODULE__{ops | state: :sufficient_funds_validated} + + defp next_state(ops = %__MODULE__{state: :sufficient_funds_validated}), + do: %__MODULE__{ops | state: :inputs_consumed} + + defp next_state(ops = %__MODULE__{state: :inputs_consumed}), + do: %__MODULE__{ops | state: :movements_resolved} + + defp next_state(ops = %__MODULE__{state: :movements_resolved}), do: ops end diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index de5333249..d2c07df94 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -803,14 +803,14 @@ defmodule Archethic.Mining.ValidationContext do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, validation_time, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx_type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs( address, validation_time, encoded_state, contract_context ) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx_type) case ops do %LedgerValidation{sufficient_funds?: false} -> @@ -1139,7 +1139,9 @@ defmodule Archethic.Mining.ValidationContext do proof_of_integrity: fn -> valid_stamp_proof_of_integrity?(stamp, context) end, proof_of_election: fn -> valid_stamp_proof_of_election?(stamp, context) end, transaction_fee: fn -> valid_stamp_fee?(stamp, fee) end, - transaction_movements: fn -> valid_stamp_transaction_movements?(stamp, context) end, + transaction_movements: fn -> + valid_stamp_transaction_movements?(stamp, ledger_operations) + end, recipients: fn -> valid_stamp_recipients?(stamp, context) end, consumed_inputs: fn -> valid_consumed_inputs?(stamp, ledger_operations) end, unspent_outputs: fn -> valid_stamp_unspent_outputs?(stamp, ledger_operations) end, @@ -1232,25 +1234,11 @@ defmodule Archethic.Mining.ValidationContext do defp valid_stamp_transaction_movements?( %ValidationStamp{ - ledger_operations: - _ops = %LedgerOperations{ - transaction_movements: transaction_movements - } + ledger_operations: %LedgerOperations{transaction_movements: stamp_movements} }, - %__MODULE__{ - transaction: tx = %Transaction{type: tx_type}, - resolved_addresses: resolved_addresses - } + %LedgerOperations{transaction_movements: expected_movements} ) do - movements = Transaction.get_movements(tx) - - %LedgerOperations{transaction_movements: resolved_movements} = - %LedgerValidation{} - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx_type) - |> LedgerValidation.to_ledger_operations() - - length(resolved_movements) == length(transaction_movements) and - Enum.all?(resolved_movements, &(&1 in transaction_movements)) + expected_movements |> MapSet.new() |> MapSet.equal?(MapSet.new(stamp_movements)) end defp valid_consumed_inputs?( diff --git a/lib/archethic/p2p/message/validate_smart_contract_call.ex b/lib/archethic/p2p/message/validate_smart_contract_call.ex index ded9e448a..6adb3b6bd 100644 --- a/lib/archethic/p2p/message/validate_smart_contract_call.ex +++ b/lib/archethic/p2p/message/validate_smart_contract_call.ex @@ -270,20 +270,18 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do defp calculate_fee(_, _), do: 0 defp enough_funds_to_send?( - %ActionWithTransaction{next_tx: tx = %Transaction{type: tx_type}}, + %ActionWithTransaction{next_tx: tx}, inputs, timestamp ) do movements = Transaction.get_movements(tx) protocol_version = Mining.protocol_version() - resolved_addresses = Enum.map(movements, &{&1.to, &1.to}) |> Map.new() %LedgerValidation{sufficient_funds?: sufficient_funds?} = - %LedgerValidation{fee: 0} + %LedgerValidation{} |> LedgerValidation.filter_usable_inputs(inputs, nil) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx_type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) sufficient_funds? end diff --git a/test/archethic/mining/distributed_workflow_test.exs b/test/archethic/mining/distributed_workflow_test.exs index 93de7451c..4fde39277 100644 --- a/test/archethic/mining/distributed_workflow_test.exs +++ b/test/archethic/mining/distributed_workflow_test.exs @@ -1360,9 +1360,9 @@ defmodule Archethic.Mining.DistributedWorkflowTest do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() %ValidationStamp{ diff --git a/test/archethic/mining/ledger_validation_test.exs b/test/archethic/mining/ledger_validation_test.exs index 3250c1ad7..937d2f3d8 100644 --- a/test/archethic/mining/ledger_validation_test.exs +++ b/test/archethic/mining/ledger_validation_test.exs @@ -5,6 +5,8 @@ defmodule Archethic.Mining.LedgerValidationTest do alias Archethic.TransactionFactory + alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations + alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.TransactionMovement alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput @@ -22,13 +24,36 @@ defmodule Archethic.Mining.LedgerValidationTest do end describe "mint_token_utxos/4" do + test "should raise if not in filtered_inputs state" do + tx = TransactionFactory.create_valid_transaction([]) + + assert_raise FunctionClauseError, fn -> + %LedgerValidation{} + |> LedgerValidation.mint_token_utxos(tx, DateTime.utc_now(), current_protocol_version()) + end + end + + test "should update state to utxos_minted" do + tx = TransactionFactory.create_valid_transaction([]) + + assert %LedgerValidation{state: :utxos_minted} = + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos( + tx, + DateTime.utc_now(), + current_protocol_version() + ) + end + test "should return empty list for non token/mint_reward transaction" do types = Archethic.TransactionChain.Transaction.types() -- [:node, :mint_reward] Enum.each(types, fn t -> assert %LedgerValidation{minted_utxos: []} = - LedgerValidation.mint_token_utxos( - %LedgerValidation{}, + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos( TransactionFactory.create_valid_transaction([], type: t), DateTime.utc_now(), current_protocol_version() @@ -38,8 +63,9 @@ defmodule Archethic.Mining.LedgerValidationTest do test "should return empty list if content is invalid" do assert %LedgerValidation{minted_utxos: []} = - LedgerValidation.mint_token_utxos( - %LedgerValidation{}, + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos( TransactionFactory.create_valid_transaction([], type: :token, content: "not a json" @@ -49,8 +75,9 @@ defmodule Archethic.Mining.LedgerValidationTest do ) assert %LedgerValidation{minted_utxos: []} = - LedgerValidation.mint_token_utxos( - %LedgerValidation{}, + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos( TransactionFactory.create_valid_transaction([], type: :token, content: "{}"), DateTime.utc_now(), current_protocol_version() @@ -86,6 +113,7 @@ defmodule Archethic.Mining.LedgerValidationTest do } ] = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) |> Map.fetch!(:minted_utxos) |> VersionedUnspentOutput.unwrap_unspent_outputs() @@ -107,6 +135,7 @@ defmodule Archethic.Mining.LedgerValidationTest do assert %LedgerValidation{minted_utxos: []} = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) tx = @@ -122,6 +151,7 @@ defmodule Archethic.Mining.LedgerValidationTest do assert %LedgerValidation{minted_utxos: []} = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) token_address = random_address() @@ -140,6 +170,7 @@ defmodule Archethic.Mining.LedgerValidationTest do assert %LedgerValidation{minted_utxos: []} = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) end end @@ -173,6 +204,7 @@ defmodule Archethic.Mining.LedgerValidationTest do } ] = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) |> Map.fetch!(:minted_utxos) |> VersionedUnspentOutput.unwrap_unspent_outputs() @@ -216,6 +248,7 @@ defmodule Archethic.Mining.LedgerValidationTest do ] } = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) end @@ -273,6 +306,7 @@ defmodule Archethic.Mining.LedgerValidationTest do assert %LedgerValidation{minted_utxos: ^expected_utxos} = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) end @@ -298,6 +332,7 @@ defmodule Archethic.Mining.LedgerValidationTest do assert %LedgerValidation{minted_utxos: []} = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) end @@ -316,6 +351,7 @@ defmodule Archethic.Mining.LedgerValidationTest do assert %LedgerValidation{minted_utxos: []} = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) tx = @@ -330,6 +366,7 @@ defmodule Archethic.Mining.LedgerValidationTest do assert %LedgerValidation{minted_utxos: []} = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) tx = @@ -344,17 +381,67 @@ defmodule Archethic.Mining.LedgerValidationTest do assert %LedgerValidation{minted_utxos: []} = %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) end end - describe "validate_sufficient_funds/1" do - test "should return insufficient funds when not enough uco" do + describe "validate_sufficient_funds/2" do + setup do + %{tx: TransactionFactory.create_valid_transaction()} + end + + test "should raise if not in minted_utxos state" do + assert_raise FunctionClauseError, fn -> + %LedgerValidation{} |> LedgerValidation.validate_sufficient_funds([]) + end + end + + test "should update state to sufficient_funds_validated", %{tx: tx} do + assert %LedgerValidation{state: :sufficient_funds_validated} = + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos( + tx, + DateTime.utc_now(), + current_protocol_version() + ) + |> LedgerValidation.validate_sufficient_funds([]) + end + + test "should set the movement in the struct", %{tx: tx} do + movements = [ + %TransactionMovement{ + to: "@JeanClaude", + amount: 100_000_000, + type: {:token, "@CharlieToken", 0} + } + ] + + assert %LedgerValidation{transaction_movements: ^movements} = + %LedgerValidation{fee: 1_000} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos( + tx, + DateTime.utc_now(), + current_protocol_version() + ) + |> LedgerValidation.validate_sufficient_funds(movements) + end + + test "should return insufficient funds when not enough uco", %{tx: tx} do assert %LedgerValidation{sufficient_funds?: false} = - %LedgerValidation{fee: 1_000} |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 1_000} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos( + tx, + DateTime.utc_now(), + current_protocol_version() + ) + |> LedgerValidation.validate_sufficient_funds([]) end - test "should return insufficient funds when not enough tokens" do + test "should return insufficient funds when not enough tokens", %{tx: tx} do inputs = [ %UnspentOutput{ from: "@Charlie1", @@ -374,11 +461,30 @@ defmodule Archethic.Mining.LedgerValidationTest do ] assert %LedgerValidation{sufficient_funds?: false} = - %LedgerValidation{fee: 1_000, transaction_movements: movements, inputs: inputs} - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 1_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos( + tx, + DateTime.utc_now(), + current_protocol_version() + ) + |> LedgerValidation.validate_sufficient_funds(movements) end test "should not be able to pay with the same non-fungible token twice" do + tx = + TransactionFactory.create_valid_transaction([], + type: :token, + content: """ + { + "supply": 100000000, + "type": "non-fungible", + "name": "My NFT", + "symbol": "MNFT" + } + """ + ) + inputs = [ %UnspentOutput{ from: "@Charlie1", @@ -393,36 +499,40 @@ defmodule Archethic.Mining.LedgerValidationTest do %TransactionMovement{ to: "@JeanClaude", amount: 100_000_000, - type: {:token, "@Token", 1} + type: {:token, tx.address, 1} }, %TransactionMovement{ to: "@JeanBob", amount: 100_000_000, - type: {:token, "@Token", 1} + type: {:token, tx.address, 1} } ] - minted_utxos = [ - %UnspentOutput{ - from: "@Alice", - amount: 100_000_000, - type: {:token, "@Token", 1}, - timestamp: ~U[2022-10-09 08:39:10.463Z] - } - |> VersionedUnspentOutput.wrap_unspent_output(current_protocol_version()) - ] - assert %LedgerValidation{sufficient_funds?: false} = - %LedgerValidation{ - fee: 1_000, - transaction_movements: movements, - inputs: inputs, - minted_utxos: minted_utxos - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 1_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos( + tx, + DateTime.utc_now(), + current_protocol_version() + ) + |> LedgerValidation.validate_sufficient_funds(movements) end test "should return available balance and amount to spend and return sufficient_funds to true" do + tx = + TransactionFactory.create_valid_transaction([], + type: :token, + content: """ + { + "supply": 100000000, + "type": "non-fungible", + "name": "My NFT", + "symbol": "MNFT" + } + """ + ) + inputs = [ %UnspentOutput{ @@ -450,7 +560,7 @@ defmodule Archethic.Mining.LedgerValidationTest do %TransactionMovement{ to: "@JeanClaude", amount: 100_000_000, - type: {:token, "@Token", 1} + type: {:token, tx.address, 1} }, %TransactionMovement{ to: "@Michel", @@ -464,24 +574,14 @@ defmodule Archethic.Mining.LedgerValidationTest do } ] - minted_utxos = [ - %UnspentOutput{ - from: "@Alice", - amount: 100_000_000, - type: {:token, "@Token", 1}, - timestamp: ~U[2022-10-09 08:39:10.463Z] - } - |> VersionedUnspentOutput.wrap_unspent_output(current_protocol_version()) - ] - expected_balance = %{ uco: 10_000, - token: %{{"@Token1", 0} => 200_100_000, {"@Token", 1} => 100_000_000} + token: %{{"@Token1", 0} => 200_100_000, {tx.address, 1} => 100_000_000} } expected_amount_to_spend = %{ uco: 1456, - token: %{{"@Token1", 0} => 120_000_000, {"@Token", 1} => 100_000_000} + token: %{{"@Token1", 0} => 120_000_000, {tx.address, 1} => 100_000_000} } assert %LedgerValidation{ @@ -489,18 +589,45 @@ defmodule Archethic.Mining.LedgerValidationTest do balances: ^expected_balance, amount_to_spend: ^expected_amount_to_spend } = - %LedgerValidation{ - fee: 1_000, - transaction_movements: movements, - inputs: inputs, - minted_utxos: minted_utxos - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 1_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos( + tx, + DateTime.utc_now(), + current_protocol_version() + ) + |> LedgerValidation.validate_sufficient_funds(movements) end end describe "consume_inputs/4" do - test "When a single unspent output is sufficient to satisfy the transaction movements" do + setup do + %{tx: TransactionFactory.create_valid_transaction()} + end + + test "should raise if not in sufficient_funds_validated state" do + assert_raise FunctionClauseError, fn -> + %LedgerValidation{} + |> LedgerValidation.consume_inputs(random_address(), DateTime.utc_now()) + end + end + + test "should update state to inputs_consumed", %{tx: tx} do + assert %LedgerValidation{state: :inputs_consumed} = + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos( + tx, + DateTime.utc_now(), + current_protocol_version() + ) + |> LedgerValidation.validate_sufficient_funds([]) + |> LedgerValidation.consume_inputs(random_address(), DateTime.utc_now()) + end + + test "When a single unspent output is sufficient to satisfy the transaction movements", %{ + tx: tx + } do timestamp = ~U[2022-10-10 10:44:38.983Z] tx_address = "@Alice2" @@ -540,16 +667,15 @@ defmodule Archethic.Mining.LedgerValidationTest do } ] } = - %LedgerValidation{ - fee: 40_000_000, - transaction_movements: movements, - inputs: inputs - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 40_000_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, timestamp) end - test "When multiple little unspent output are sufficient to satisfy the transaction movements" do + test "When multiple little unspent output are sufficient to satisfy the transaction movements", + %{tx: tx} do tx_address = "@Alice2" timestamp = ~U[2022-10-10 10:44:38.983Z] @@ -628,16 +754,15 @@ defmodule Archethic.Mining.LedgerValidationTest do ], consumed_inputs: ^expected_consumed_inputs } = - %LedgerValidation{ - fee: 40_000_000, - transaction_movements: movements, - inputs: inputs - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 40_000_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, timestamp) end - test "When using Token unspent outputs are sufficient to satisfy the transaction movements" do + test "When using Token unspent outputs are sufficient to satisfy the transaction movements", + %{tx: tx} do tx_address = "@Alice2" timestamp = ~U[2022-10-10 10:44:38.983Z] @@ -701,16 +826,15 @@ defmodule Archethic.Mining.LedgerValidationTest do ], consumed_inputs: ^expected_consumed_inputs } = - %LedgerValidation{ - fee: 40_000_000, - transaction_movements: movements, - inputs: inputs - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 40_000_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, timestamp) end - test "When multiple Token unspent outputs are sufficient to satisfy the transaction movements" do + test "When multiple Token unspent outputs are sufficient to satisfy the transaction movements", + %{tx: tx} do tx_address = "@Alice2" timestamp = ~U[2022-10-10 10:44:38.983Z] @@ -798,16 +922,16 @@ defmodule Archethic.Mining.LedgerValidationTest do ], consumed_inputs: ^expected_consumed_inputs } = - %LedgerValidation{ - fee: 40_000_000, - transaction_movements: movements, - inputs: inputs - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 40_000_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, timestamp) end - test "When non-fungible tokens are used as input but want to consume only a single input" do + test "When non-fungible tokens are used as input but want to consume only a single input", %{ + tx: tx + } do tx_address = "@Alice2" timestamp = ~U[2022-10-10 10:44:38.983Z] @@ -877,17 +1001,28 @@ defmodule Archethic.Mining.LedgerValidationTest do ], consumed_inputs: ^expected_consumed_inputs } = - %LedgerValidation{ - fee: 40_000_000, - transaction_movements: movements, - inputs: inputs - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 40_000_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, timestamp) end test "should be able to pay with the minted fungible tokens" do - tx_address = "@Alice" + tx = + TransactionFactory.create_valid_transaction([], + type: :token, + content: """ + { + "supply": 100000000, + "type": "fungible", + "name": "My NFT", + "symbol": "MNFT" + } + """ + ) + + tx_address = tx.address now = DateTime.utc_now() inputs = [ @@ -904,35 +1039,22 @@ defmodule Archethic.Mining.LedgerValidationTest do %TransactionMovement{ to: "@JeanClaude", amount: 50_000_000, - type: {:token, "@Token", 0} - } - ] - - minted_utxos = [ - %UnspentOutput{ - from: "@Alice", - amount: 100_000_000, - type: {:token, "@Token", 0}, - timestamp: ~U[2022-10-09 08:39:10.463Z] + type: {:token, tx_address, 0} } - |> VersionedUnspentOutput.wrap_unspent_output(current_protocol_version()) ] assert ops_result = - %LedgerValidation{ - fee: 1_000, - transaction_movements: movements, - inputs: inputs, - minted_utxos: minted_utxos - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 1_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, now) assert [ %UnspentOutput{ - from: "@Alice", + from: ^tx_address, amount: 50_000_000, - type: {:token, "@Token", 0}, + type: {:token, ^tx_address, 0}, timestamp: ^now } ] = ops_result.unspent_outputs @@ -949,14 +1071,27 @@ defmodule Archethic.Mining.LedgerValidationTest do %UnspentOutput{ from: ^burn_address, amount: 100_000_000, - type: {:token, "@Token", 0}, - timestamp: ~U[2022-10-09 08:39:10.463Z] + type: {:token, ^tx_address, 0}, + timestamp: ^now } ] = ops_result.consumed_inputs |> VersionedUnspentOutput.unwrap_unspent_outputs() end test "should be able to pay with the minted non-fungible tokens" do - tx_address = "@Alice" + tx = + TransactionFactory.create_valid_transaction([], + type: :token, + content: """ + { + "supply": 100000000, + "type": "non-fungible", + "name": "My NFT", + "symbol": "MNFT" + } + """ + ) + + tx_address = tx.address now = DateTime.utc_now() inputs = [ @@ -973,28 +1108,15 @@ defmodule Archethic.Mining.LedgerValidationTest do %TransactionMovement{ to: "@JeanClaude", amount: 100_000_000, - type: {:token, "@Token", 1} + type: {:token, tx_address, 1} } ] - minted_utxos = [ - %UnspentOutput{ - from: "@Alice", - amount: 100_000_000, - type: {:token, "@Token", 1}, - timestamp: ~U[2022-10-09 08:39:10.463Z] - } - |> VersionedUnspentOutput.wrap_unspent_output(current_protocol_version()) - ] - assert ops_result = - %LedgerValidation{ - fee: 1_000, - transaction_movements: movements, - inputs: inputs, - minted_utxos: minted_utxos - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 1_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, now) assert [] = ops_result.unspent_outputs @@ -1011,14 +1133,34 @@ defmodule Archethic.Mining.LedgerValidationTest do %UnspentOutput{ from: ^burn_address, amount: 100_000_000, - type: {:token, "@Token", 1}, - timestamp: ~U[2022-10-09 08:39:10.463Z] + type: {:token, ^tx_address, 1}, + timestamp: ^now } ] = ops_result.consumed_inputs |> VersionedUnspentOutput.unwrap_unspent_outputs() end test "should be able to pay with the minted non-fungible tokens (collection)" do - tx_address = "@Alice" + tx = + TransactionFactory.create_valid_transaction([], + type: :token, + content: """ + { + "supply": 200000000, + "name": "My NFT", + "type": "non-fungible", + "symbol": "MNFT", + "properties": { + "description": "this property is for all NFT" + }, + "collection": [ + { "image": "link of the 1st NFT image" }, + { "image": "link of the 2nd NFT image" } + ] + } + """ + ) + + tx_address = tx.address now = DateTime.utc_now() inputs = [ @@ -1035,43 +1177,23 @@ defmodule Archethic.Mining.LedgerValidationTest do %TransactionMovement{ to: "@JeanClaude", amount: 100_000_000, - type: {:token, "@Token", 2} + type: {:token, tx_address, 2} } ] - minted_utxos = - [ - %UnspentOutput{ - from: "@Alice", - amount: 100_000_000, - type: {:token, "@Token", 1}, - timestamp: ~U[2022-10-09 08:39:10.463Z] - }, - %UnspentOutput{ - from: "@Alice", - amount: 100_000_000, - type: {:token, "@Token", 2}, - timestamp: ~U[2022-10-09 08:39:10.463Z] - } - ] - |> VersionedUnspentOutput.wrap_unspent_outputs(current_protocol_version()) - assert ops_result = - %LedgerValidation{ - fee: 1_000, - transaction_movements: movements, - inputs: inputs, - minted_utxos: minted_utxos - } - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 1_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, now) assert [ %UnspentOutput{ - from: "@Alice", + from: ^tx_address, amount: 100_000_000, - type: {:token, "@Token", 1}, - timestamp: ~U[2022-10-09 08:39:10.463Z] + type: {:token, ^tx_address, 1}, + timestamp: ^now } ] = ops_result.unspent_outputs @@ -1087,13 +1209,13 @@ defmodule Archethic.Mining.LedgerValidationTest do %UnspentOutput{ from: ^burn_address, amount: 100_000_000, - type: {:token, "@Token", 2}, - timestamp: ~U[2022-10-09 08:39:10.463Z] + type: {:token, ^tx_address, 2}, + timestamp: ^now } ] = ops_result.consumed_inputs |> VersionedUnspentOutput.unwrap_unspent_outputs() end - test "should merge two similar tokens and update the from & timestamp" do + test "should merge two similar tokens and update the from & timestamp", %{tx: tx} do transaction_address = random_address() transaction_timestamp = DateTime.utc_now() @@ -1165,8 +1287,14 @@ defmodule Archethic.Mining.LedgerValidationTest do consumed_inputs: ^expected_consumed_inputs, fee: 40_000_000 } = - %LedgerValidation{fee: 40_000_000, inputs: inputs} - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{fee: 40_000_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos( + tx, + transaction_timestamp, + current_protocol_version() + ) + |> LedgerValidation.validate_sufficient_funds([]) |> LedgerValidation.consume_inputs(transaction_address, transaction_timestamp) tx_address = "@Alice2" @@ -1210,19 +1338,23 @@ defmodule Archethic.Mining.LedgerValidationTest do %VersionedUnspentOutput{unspent_output: %UnspentOutput{from: "@Tom5"}} ] } = - %LedgerValidation{inputs: inputs, transaction_movements: movements} - |> LedgerValidation.validate_sufficient_funds() + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx_address, now) end - test "should consume state if it's not the same" do + test "should consume state if it's not the same", %{tx: tx} do + now = DateTime.utc_now() + inputs = [ %UnspentOutput{ type: :state, from: random_address(), encoded_payload: :crypto.strong_rand_bytes(32), - timestamp: DateTime.utc_now() + timestamp: now } ] |> VersionedUnspentOutput.wrap_unspent_outputs(current_protocol_version()) @@ -1233,9 +1365,11 @@ defmodule Archethic.Mining.LedgerValidationTest do consumed_inputs: ^inputs, unspent_outputs: [%UnspentOutput{type: :state, encoded_payload: ^new_state}] } = - %LedgerValidation{fee: 0, inputs: inputs} - |> LedgerValidation.validate_sufficient_funds() - |> LedgerValidation.consume_inputs("@Alice2", DateTime.utc_now(), new_state, nil) + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds([]) + |> LedgerValidation.consume_inputs("@Alice2", now, new_state, nil) end # test "should not consume state if it's the same" do @@ -1271,24 +1405,28 @@ defmodule Archethic.Mining.LedgerValidationTest do # ) # end - test "should not return any utxo if nothing is spent" do + test "should not return any utxo if nothing is spent", %{tx: tx} do + timestamp = ~U[2022-10-10 10:44:38.983Z] + inputs = [ %UnspentOutput{ from: "@Bob3", amount: 2_000_000_000, type: :UCO, - timestamp: ~U[2022-10-09 08:39:10.463Z] + timestamp: timestamp } |> VersionedUnspentOutput.wrap_unspent_output(current_protocol_version()) ] assert %LedgerValidation{fee: 0, unspent_outputs: [], consumed_inputs: []} = - %LedgerValidation{fee: 0, inputs: inputs} - |> LedgerValidation.validate_sufficient_funds() - |> LedgerValidation.consume_inputs("@Alice2", ~U[2022-10-10 10:44:38.983Z]) + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds([]) + |> LedgerValidation.consume_inputs("@Alice2", timestamp) end - test "should not update utxo if not consumed" do + test "should not update utxo if not consumed", %{tx: tx} do token_address = random_address() utxo_not_used = [ @@ -1335,17 +1473,21 @@ defmodule Archethic.Mining.LedgerValidationTest do } ] + timestamp = ~U[2022-10-10 10:44:38.983Z] + assert %LedgerValidation{fee: 0, unspent_outputs: [], consumed_inputs: consumed_inputs} = - %LedgerValidation{fee: 0, inputs: all_utxos, transaction_movements: movements} - |> LedgerValidation.validate_sufficient_funds() - |> LedgerValidation.consume_inputs(random_address(), ~U[2022-10-10 10:44:38.983Z]) + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs(all_utxos, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) + |> LedgerValidation.consume_inputs(random_address(), timestamp) # order does not matter assert Enum.all?(consumed_inputs, &(&1 in consumed_utxo)) and length(consumed_inputs) == length(consumed_utxo) end - test "should optimize consumed utxo to avoid consolidation" do + test "should optimize consumed utxo to avoid consolidation", %{tx: tx} do optimized_utxo = [ %UnspentOutput{ from: random_address(), @@ -1384,17 +1526,21 @@ defmodule Archethic.Mining.LedgerValidationTest do movements = [%TransactionMovement{to: random_address(), amount: 200_000_000, type: :UCO}] + timestamp = ~U[2022-10-10 10:44:38.983Z] + assert %LedgerValidation{fee: 0, unspent_outputs: [], consumed_inputs: consumed_inputs} = - %LedgerValidation{fee: 0, inputs: all_utxos, transaction_movements: movements} - |> LedgerValidation.validate_sufficient_funds() - |> LedgerValidation.consume_inputs(random_address(), ~U[2022-10-10 10:44:38.983Z]) + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs(all_utxos, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) + |> LedgerValidation.consume_inputs(random_address(), timestamp) # order does not matter assert Enum.all?(consumed_inputs, &(&1 in consumed_utxo)) and length(consumed_inputs) == length(consumed_utxo) end - test "should sort utxo to be consistent across nodes" do + test "should sort utxo to be consistent across nodes", %{tx: tx} do [lower_address, higher_address] = [random_address(), random_address()] |> Enum.sort() optimized_utxo = [ @@ -1435,20 +1581,17 @@ defmodule Archethic.Mining.LedgerValidationTest do movements = [%TransactionMovement{to: random_address(), amount: 310_000_000, type: :UCO}] + timestamp = ~U[2022-10-10 10:44:38.983Z] + Enum.each(1..5, fn _ -> randomized_utxo = Enum.shuffle(all_utxo) assert %LedgerValidation{fee: 0, unspent_outputs: [], consumed_inputs: consumed_inputs} = - %LedgerValidation{ - fee: 0, - inputs: randomized_utxo, - transaction_movements: movements - } - |> LedgerValidation.validate_sufficient_funds() - |> LedgerValidation.consume_inputs( - random_address(), - ~U[2022-10-10 10:44:38.983Z] - ) + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs(randomized_utxo, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) + |> LedgerValidation.consume_inputs(random_address(), timestamp) # order does not matter assert Enum.all?(consumed_inputs, &(&1 in consumed_utxo)) and @@ -1458,7 +1601,31 @@ defmodule Archethic.Mining.LedgerValidationTest do end describe "build_resoved_movements/3" do - test "should resolve, convert reward and aggregate movements" do + setup do + %{tx: TransactionFactory.create_valid_transaction()} + end + + test "should raise if not in inputs_consumed state" do + assert_raise FunctionClauseError, fn -> + %LedgerValidation{} |> LedgerValidation.build_resolved_movements(%{}, :transfer) + end + end + + test "should update state to movements_resolved", %{tx: tx} do + now = DateTime.utc_now() + + assert %LedgerValidation{state: :movements_resolved} = + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds([]) + |> LedgerValidation.consume_inputs(random_address(), now) + |> LedgerValidation.build_resolved_movements(%{}, :transfer) + end + + test "should resolve, convert reward and aggregate movements", %{tx: tx} do + now = DateTime.utc_now() + address1 = random_address() address2 = random_address() @@ -1472,7 +1639,7 @@ defmodule Archethic.Mining.LedgerValidationTest do RewardTokens.add_reward_token_address(reward_token_address) - movement = [ + movements = [ %TransactionMovement{to: address1, amount: 10, type: :UCO}, %TransactionMovement{to: address1, amount: 10, type: {:token, token_address, 0}}, %TransactionMovement{to: address1, amount: 40, type: {:token, token_address, 0}}, @@ -1487,16 +1654,71 @@ defmodule Archethic.Mining.LedgerValidationTest do ] assert %LedgerValidation{transaction_movements: resolved_movements} = - LedgerValidation.build_resolved_movements( - %LedgerValidation{}, - movement, - resolved_addresses, - :transfer - ) + %LedgerValidation{} + |> LedgerValidation.filter_usable_inputs([], nil) + |> LedgerValidation.mint_token_utxos(tx, now, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) + |> LedgerValidation.consume_inputs(random_address(), now) + |> LedgerValidation.build_resolved_movements(resolved_addresses, :transfer) # Order does not matters assert length(expected_resolved_movement) == length(resolved_movements) assert Enum.all?(expected_resolved_movement, &Enum.member?(resolved_movements, &1)) end end + + describe "to_ledger_operations/1" do + setup do + %{tx: TransactionFactory.create_valid_transaction()} + end + + test "should raise if not in inputs_consumed state" do + assert_raise FunctionClauseError, fn -> + %LedgerValidation{} |> LedgerValidation.to_ledger_operations() + end + end + + test "should return LegderOperations struct", %{tx: tx} do + timestamp = ~U[2022-10-10 10:44:38.983Z] + tx_address = "@Alice2" + + inputs = [ + %UnspentOutput{ + from: "@Bob3", + amount: 2_000_000_000, + type: :UCO, + timestamp: ~U[2022-10-09 08:39:10.463Z] + } + |> VersionedUnspentOutput.wrap_unspent_output(current_protocol_version()) + ] + + movements = [ + %TransactionMovement{to: "@Bob4", amount: 1_040_000_000, type: :UCO}, + %TransactionMovement{to: "@Charlie2", amount: 217_000_000, type: :UCO} + ] + + resolved_addresses = Enum.map(movements, &{&1.to, &1.to}) |> Map.new() + + assert %LedgerOperations{ + fee: 40_000_000, + unspent_outputs: [ + %UnspentOutput{ + from: "@Alice2", + amount: 703_000_000, + type: :UCO, + timestamp: ~U[2022-10-10 10:44:38.983Z] + } + ], + consumed_inputs: ^inputs, + transaction_movements: ^movements + } = + %LedgerValidation{fee: 40_000_000} + |> LedgerValidation.filter_usable_inputs(inputs, nil) + |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) + |> LedgerValidation.validate_sufficient_funds(movements) + |> LedgerValidation.consume_inputs(tx_address, timestamp) + |> LedgerValidation.build_resolved_movements(resolved_addresses, :transfer) + |> LedgerValidation.to_ledger_operations() + end + end end diff --git a/test/archethic/mining/validation_context_test.exs b/test/archethic/mining/validation_context_test.exs index 02d9b936e..eac461590 100644 --- a/test/archethic/mining/validation_context_test.exs +++ b/test/archethic/mining/validation_context_test.exs @@ -12,7 +12,7 @@ defmodule Archethic.Mining.ValidationContextTest do alias Archethic.P2P alias Archethic.P2P.Node - + alias Archethic.Reward.MemTables.RewardTokens alias Archethic.SharedSecrets alias Archethic.TransactionChain @@ -30,6 +30,7 @@ defmodule Archethic.Mining.ValidationContextTest do alias Archethic.TransactionChain.TransactionData alias Archethic.TransactionChain.TransactionData.Ledger alias Archethic.TransactionChain.TransactionData.UCOLedger + alias Archethic.TransactionChain.TransactionData.TokenLedger alias Archethic.TransactionChain.TransactionData.Recipient alias Archethic.TransactionFactory @@ -158,6 +159,79 @@ defmodule Archethic.Mining.ValidationContextTest do } } = ValidationContext.create_validation_stamp(validation_context) end + + test "should handle the MUCO correctly" do + timestamp = DateTime.utc_now() |> DateTime.truncate(:millisecond) + transfer_address = random_address() + muco_addr1 = random_address() + muco_addr2 = random_address() + + start_supervised!(RewardTokens) + RewardTokens.add_reward_token_address(muco_addr1) + RewardTokens.add_reward_token_address(muco_addr2) + + utxos = [ + %UnspentOutput{ + from: random_address(), + amount: 204_000_000, + type: :UCO, + timestamp: timestamp + }, + %UnspentOutput{ + from: random_address(), + amount: 10, + type: {:token, muco_addr1, 0}, + timestamp: timestamp + }, + %UnspentOutput{ + from: random_address(), + amount: 10, + type: {:token, muco_addr2, 0}, + timestamp: timestamp + } + ] + + transaction_opts = [ + ledger: %Ledger{ + token: %TokenLedger{ + transfers: [ + %TokenLedger.Transfer{ + token_address: muco_addr1, + token_id: 0, + to: transfer_address, + amount: 10 + }, + %TokenLedger.Transfer{ + token_address: muco_addr2, + token_id: 0, + to: transfer_address, + amount: 10 + } + ] + } + } + ] + + validation_context = + create_context(timestamp, unspent_outputs: utxos, transaction_opts: transaction_opts) + + assert %ValidationContext{validation_stamp: %ValidationStamp{ledger_operations: ops}} = + ValidationContext.create_validation_stamp(validation_context) + + assert [%TransactionMovement{to: ^transfer_address, amount: 20, type: :UCO}] = + ops.transaction_movements + + assert utxos + |> VersionedUnspentOutput.wrap_unspent_outputs(current_protocol_version()) + |> MapSet.new() + |> MapSet.equal?(MapSet.new(ops.consumed_inputs)) + + remaining_uco = 204_000_000 - ops.fee + tx_address = validation_context.transaction.address + + assert [%UnspentOutput{from: ^tx_address, amount: ^remaining_uco, type: :UCO}] = + ops.unspent_outputs + end end describe "cross_validate/1" do @@ -452,7 +526,10 @@ defmodule Archethic.Mining.ValidationContextTest do end end - defp create_context(validation_time \\ DateTime.utc_now() |> DateTime.truncate(:millisecond)) do + defp create_context( + validation_time \\ DateTime.utc_now() |> DateTime.truncate(:millisecond), + opts \\ [] + ) do welcome_node = %Node{ last_public_key: "key1", first_public_key: "key1", @@ -508,26 +585,36 @@ defmodule Archethic.Mining.ValidationContextTest do Enum.each(cross_validation_nodes, &P2P.add_and_connect_node(&1)) Enum.each(previous_storage_nodes, &P2P.add_and_connect_node(&1)) + default_utxo = %UnspentOutput{ + from: "@Alice2", + amount: 204_000_000, + type: :UCO, + timestamp: validation_time + } + unspent_outputs = - [ - %UnspentOutput{ - from: "@Alice2", - amount: 204_000_000, - type: :UCO, - timestamp: validation_time - } - ] + opts + |> Keyword.get(:unspent_outputs, [default_utxo]) |> VersionedUnspentOutput.wrap_unspent_outputs(current_protocol_version()) + tx = + opts + |> Keyword.get(:transaction_opts, []) + |> TransactionFactory.create_non_valided_transaction() + + resolved_addresses = + tx |> Transaction.get_movements() |> Enum.map(&{&1.to, &1.to}) |> Map.new() + %ValidationContext{ - transaction: TransactionFactory.create_non_valided_transaction(), + transaction: tx, previous_storage_nodes: previous_storage_nodes, unspent_outputs: unspent_outputs, aggregated_utxos: unspent_outputs, welcome_node: welcome_node, coordinator_node: coordinator_node, cross_validation_nodes: cross_validation_nodes, - validation_time: validation_time + validation_time: validation_time, + resolved_addresses: resolved_addresses } end @@ -547,9 +634,9 @@ defmodule Archethic.Mining.ValidationContextTest do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() %ValidationStamp{ @@ -579,9 +666,9 @@ defmodule Archethic.Mining.ValidationContextTest do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() %ValidationStamp{ @@ -611,9 +698,9 @@ defmodule Archethic.Mining.ValidationContextTest do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() %ValidationStamp{ @@ -644,9 +731,9 @@ defmodule Archethic.Mining.ValidationContextTest do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() %ValidationStamp{ @@ -737,9 +824,9 @@ defmodule Archethic.Mining.ValidationContextTest do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() %ValidationStamp{ @@ -769,9 +856,9 @@ defmodule Archethic.Mining.ValidationContextTest do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(unspent_outputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, current_protocol_version()) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() |> Map.put( :consumed_inputs, diff --git a/test/support/transaction_factory.ex b/test/support/transaction_factory.ex index daa37e749..2d95d0fd7 100644 --- a/test/support/transaction_factory.ex +++ b/test/support/transaction_factory.ex @@ -101,9 +101,9 @@ defmodule Archethic.TransactionFactory do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(inputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() poi = @@ -160,9 +160,9 @@ defmodule Archethic.TransactionFactory do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(inputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() validation_stamp = @@ -205,9 +205,9 @@ defmodule Archethic.TransactionFactory do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(inputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() validation_stamp = %ValidationStamp{ @@ -253,9 +253,9 @@ defmodule Archethic.TransactionFactory do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(inputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() validation_stamp = %ValidationStamp{ @@ -294,9 +294,9 @@ defmodule Archethic.TransactionFactory do %LedgerValidation{fee: 1_000_000_000} |> LedgerValidation.filter_usable_inputs(inputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() validation_stamp = @@ -338,9 +338,9 @@ defmodule Archethic.TransactionFactory do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(inputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() validation_stamp = @@ -399,9 +399,9 @@ defmodule Archethic.TransactionFactory do %LedgerValidation{fee: fee} |> LedgerValidation.filter_usable_inputs(inputs, contract_context) |> LedgerValidation.mint_token_utxos(tx, timestamp, protocol_version) - |> LedgerValidation.build_resolved_movements(movements, resolved_addresses, tx.type) - |> LedgerValidation.validate_sufficient_funds() + |> LedgerValidation.validate_sufficient_funds(movements) |> LedgerValidation.consume_inputs(tx.address, timestamp, encoded_state, contract_context) + |> LedgerValidation.build_resolved_movements(resolved_addresses, tx.type) |> LedgerValidation.to_ledger_operations() validation_stamp =