diff --git a/lib/archethic/bootstrap/transaction_handler.ex b/lib/archethic/bootstrap/transaction_handler.ex index a01fee61ee..3f05c738e8 100644 --- a/lib/archethic/bootstrap/transaction_handler.ex +++ b/lib/archethic/bootstrap/transaction_handler.ex @@ -22,7 +22,8 @@ defmodule Archethic.Bootstrap.TransactionHandler do @doc """ Send a transaction to the network towards a welcome node """ - @spec send_transaction(Transaction.t(), list(Node.t())) :: :ok | {:error, :network_issue} + @spec send_transaction(Transaction.t(), list(Node.t())) :: + :ok def send_transaction(tx = %Transaction{address: address}, nodes) do Logger.info("Send node transaction...", transaction_address: Base.encode16(address), @@ -32,6 +33,8 @@ defmodule Archethic.Bootstrap.TransactionHandler do do_send_transaction(nodes, tx) end + @spec do_send_transaction(list(Node.t()), Transaction.t()) :: + :ok defp do_send_transaction([node | rest], tx) do case P2P.send_message(node, %NewTransaction{transaction: tx}) do {:ok, %Ok{}} -> @@ -48,7 +51,13 @@ defmodule Archethic.Bootstrap.TransactionHandler do ) |> Enum.reject(&(&1.first_public_key == Crypto.first_node_public_key())) - await_confirmation(tx.address, storage_nodes) + case await_confirmation(tx.address, storage_nodes) do + :ok -> + :ok + + {:error, :network_issue} -> + raise("No node responded with confirmation for new Node tx") + end {:error, _} = e -> Logger.error("Cannot send node transaction - #{inspect(e)}", @@ -61,14 +70,12 @@ defmodule Archethic.Bootstrap.TransactionHandler do defp do_send_transaction([], _), do: {:error, :network_issue} + @spec await_confirmation(tx_address :: binary(), list(Node.t())) :: + :ok | {:error, :network_issue} defp await_confirmation(tx_address, [node | rest]) do case P2P.send_message(node, %GetTransactionSummary{address: tx_address}) do {:ok, - %TransactionSummaryMessage{ - transaction_summary: %TransactionSummary{ - address: ^tx_address - } - }} -> + %TransactionSummaryMessage{transaction_summary: %TransactionSummary{address: ^tx_address}}} -> :ok {:ok, %NotFound{}} -> @@ -84,6 +91,8 @@ defmodule Archethic.Bootstrap.TransactionHandler do end end + defp await_confirmation(_, []), do: {:error, :network_issue} + @doc """ Create a new node transaction """ diff --git a/lib/archethic/contracts/interpreter/action.ex b/lib/archethic/contracts/interpreter/action.ex index 0cb9e9b63e..06c35553ee 100644 --- a/lib/archethic/contracts/interpreter/action.ex +++ b/lib/archethic/contracts/interpreter/action.ex @@ -146,132 +146,109 @@ defmodule Archethic.Contracts.ActionInterpreter do {node, acc} end - # Whitelist the add_uco_transfer function parameters + # Blacklist the add_uco_transfer argument list defp prewalk( node = {{:atom, "to"}, address}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} ) - when is_binary(address) do - {node, acc} - end - - defp prewalk( - node = {{:atom, "to"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} - ) do - {node, acc} + when not is_tuple(address) and not is_binary(address) do + throw({:error, "invalid add_uco_transfer arguments", node}) end defp prewalk( node = {{:atom, "amount"}, amount}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} ) - when is_integer(amount) and amount > 0 do - {node, acc} + when (not is_tuple(amount) and not is_integer(amount)) or amount <= 0 do + throw({:error, "invalid add_uco_transfer arguments", node}) end defp prewalk( - node = {{:atom, "amount"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} - ) do - {node, acc} + node = {{:atom, atom}, _}, + _acc = {:ok, %{scope: {:function, "add_uco_transfer", {:actions, _}}}} + ) + when atom != "to" and atom != "amount" do + throw({:error, "invalid add_uco_transfer arguments", node}) end - # Whitelist the add_token_transfer argument list + # Blacklist the add_token_transfer argument list defp prewalk( node = {{:atom, "to"}, address}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} ) - when is_binary(address) do - {node, acc} - end - - defp prewalk( - node = {{:atom, "to"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} - ) do - {node, acc} + when not is_tuple(address) and not is_binary(address) do + throw({:error, "invalid add_token_transfer arguments", node}) end defp prewalk( node = {{:atom, "amount"}, amount}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} ) - when is_integer(amount) and amount > 0 do - {node, acc} + when (not is_tuple(amount) and not is_integer(amount)) or amount <= 0 do + throw({:error, "invalid add_token_transfer arguments", node}) end defp prewalk( - node = {{:atom, "amount"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} - ) do - {node, acc} - end - - defp prewalk( - node = {{:atom, "token_address"}, token_address}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + node = {{:atom, "token_address"}, address}, + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} ) - when is_binary(token_address) do - {node, acc} + when not is_tuple(address) and not is_binary(address) do + throw({:error, "invalid add_token_transfer arguments", node}) end defp prewalk( - node = {{:atom, "token_address"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} - ) do - {node, acc} - end - - defp prewalk( - node = {{:atom, "token_id"}, token_id}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + node = {{:atom, "token_id"}, id}, + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} ) - when is_integer(token_id) and token_id >= 0 do - {node, acc} + when (not is_tuple(id) and not is_integer(id)) or id < 0 do + throw({:error, "invalid add_token_transfer arguments", node}) end defp prewalk( - node = {{:atom, "token_id"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} - ) do - {node, acc} + node = {{:atom, atom}, _}, + _acc = {:ok, %{scope: {:function, "add_token_transfer", {:actions, _}}}} + ) + when atom != "to" and atom != "amount" and atom != "token_address" and atom != "token_id" do + throw({:error, "invalid add_token_transfer arguments", node}) end - # Whitelist the add_ownership argument list + # Blacklist the add_ownership argument list defp prewalk( node = {{:atom, "secret"}, secret}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + _acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} ) - when is_binary(secret) do - {node, acc} + when not is_tuple(secret) and not is_binary(secret) do + throw({:error, "invalid add_ownership arguments", node}) end defp prewalk( - node = {{:atom, "secret"}, {{:atom, _}, _, _}}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} - ) do - {node, acc} + node = {{:atom, "secret_key"}, secret_key}, + _acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + ) + when not is_tuple(secret_key) and not is_binary(secret_key) do + throw({:error, "invalid add_ownership arguments", node}) end defp prewalk( - node = {{:atom, "secret_key"}, _secret_key}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} - ) do - {node, acc} + node = {{:atom, "authorized_public_keys"}, authorized_public_keys}, + _acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + ) + when not is_tuple(authorized_public_keys) and not is_list(authorized_public_keys) do + throw({:error, "invalid add_ownership arguments", node}) end defp prewalk( - node = {{:atom, "authorized_public_keys"}, authorized_public_keys}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + node = {{:atom, atom}, _}, + _acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} ) - when is_list(authorized_public_keys) do - {node, acc} + when atom != "secret" and atom != "secret_key" and atom != "authorized_public_keys" do + throw({:error, "invalid add_ownership arguments", node}) end + # Whitelist the keywords defp prewalk( - node = {{:atom, "authorized_public_keys"}, {{:atom, _, _}}}, - acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} + node = {{:atom, _}, _}, + acc = {:ok, _} ) do {node, acc} end @@ -341,11 +318,11 @@ defmodule Archethic.Contracts.ActionInterpreter do end @doc """ - Execute actions code and returns a transaction as result + Execute actions code and returns either the next transaction or nil """ - @spec execute(Macro.t(), map()) :: Transaction.t() + @spec execute(Macro.t(), map()) :: Transaction.t() | nil def execute(code, constants \\ %{}) do - {%{"next_transaction" => next_transaction}, _} = + result = Code.eval_quoted(code, scope: Map.put(constants, "next_transaction", %Transaction{ @@ -353,6 +330,12 @@ defmodule Archethic.Contracts.ActionInterpreter do }) ) - next_transaction + case result do + {%{"next_transaction" => next_transaction}, _} -> + next_transaction + + _ -> + nil + end end end diff --git a/lib/archethic/contracts/interpreter/library.ex b/lib/archethic/contracts/interpreter/library.ex index a5795636a1..02491f47b8 100644 --- a/lib/archethic/contracts/interpreter/library.ex +++ b/lib/archethic/contracts/interpreter/library.ex @@ -9,9 +9,13 @@ defmodule Archethic.Contracts.Interpreter.Library do TransactionChain, Contracts.ContractConstants, Contracts.TransactionLookup, - Contracts.Interpreter.Utils + Utils } + alias Archethic.Contracts.Interpreter.Utils, as: SCUtils + + require Logger + @doc """ Match a regex expression @@ -141,7 +145,7 @@ defmodule Archethic.Contracts.Interpreter.Library do :blake2b end - :crypto.hash(algo, Utils.maybe_decode_hex(content)) + :crypto.hash(algo, SCUtils.maybe_decode_hex(content)) |> Base.encode16() end @@ -180,7 +184,7 @@ defmodule Archethic.Contracts.Interpreter.Library do 2 """ @spec size(binary() | list()) :: non_neg_integer() - def size(binary) when is_binary(binary), do: binary |> Utils.maybe_decode_hex() |> byte_size() + def size(binary) when is_binary(binary), do: binary |> SCUtils.maybe_decode_hex() |> byte_size() def size(list) when is_list(list), do: length(list) def size(map) when is_map(map), do: map_size(map) @@ -192,7 +196,7 @@ defmodule Archethic.Contracts.Interpreter.Library do @spec get_calls(binary()) :: list(map()) def get_calls(contract_address) do contract_address - |> Utils.maybe_decode_hex() + |> SCUtils.maybe_decode_hex() |> TransactionLookup.list_contract_transactions() |> Enum.map(fn {address, _, _} -> # TODO: parallelize @@ -206,7 +210,7 @@ defmodule Archethic.Contracts.Interpreter.Library do """ @spec get_genesis_public_key(binary()) :: binary() def get_genesis_public_key(address) do - bin_address = Utils.maybe_decode_hex(address) + bin_address = SCUtils.maybe_decode_hex(address) nodes = Election.chain_storage_nodes(bin_address, P2P.authorized_and_available_nodes()) {:ok, key} = download_first_public_key(nodes, bin_address) Base.encode16(key) @@ -228,13 +232,51 @@ defmodule Archethic.Contracts.Interpreter.Library do @spec timestamp() :: non_neg_integer() def timestamp, do: DateTime.utc_now() |> DateTime.to_unix() + @doc """ + Provide a token id which uniquely identify the token base on it's properties and genesis address. + """ + @spec get_token_id(binary()) :: {:error, binary()} | {:ok, binary()} + def get_token_id(address) do + address = SCUtils.get_address(address, :get_token_id) + t1 = Task.async(fn -> Archethic.fetch_genesis_address_remotely(address) end) + t2 = Task.async(fn -> Archethic.search_transaction(address) end) + + with {:ok, {:ok, genesis_address}} <- Task.yield(t1), + {:ok, {:ok, tx}} <- Task.yield(t2), + {:ok, %{id: id}} <- Utils.get_token_properties(genesis_address, tx) do + id + else + {:ok, {:error, :network_issue}} -> + {:error, "Network issue"} + + {:ok, {:error, :transaction_not_exists}} -> + {:error, "Transaction not exists"} + + {:ok, {:error, :transaction_invalid}} -> + {:error, "Transaction invalid"} + + {:error, :decode_error} -> + {:error, "Error in decoding transaction"} + + {:error, :not_a_token_transaction} -> + {:error, "Transaction is not of type token"} + + {:exit, reason} -> + Logger.debug("Task exited with reason #{inspect(reason)}") + {:error, "Task Exited!"} + + nil -> + {:error, "Task didn't responded within timeout!"} + end + end + @doc """ Get the genesis address of the chain """ @spec get_genesis_address(binary()) :: binary() def get_genesis_address(address) do - addr_bin = Utils.maybe_decode_hex(address) + addr_bin = SCUtils.maybe_decode_hex(address) nodes = Election.chain_storage_nodes(address, P2P.authorized_and_available_nodes()) case TransactionChain.fetch_genesis_address_remotely(addr_bin, nodes) do @@ -249,7 +291,7 @@ defmodule Archethic.Contracts.Interpreter.Library do @spec get_first_transaction_address(address :: binary()) :: binary() def get_first_transaction_address(address) do - addr_bin = Utils.maybe_decode_hex(address) + addr_bin = SCUtils.maybe_decode_hex(address) nodes = Election.chain_storage_nodes(address, P2P.authorized_and_available_nodes()) case TransactionChain.fetch_first_transaction_address_remotely(addr_bin, nodes) do diff --git a/lib/archethic/contracts/interpreter/transaction_statements.ex b/lib/archethic/contracts/interpreter/transaction_statements.ex index bfd06a2050..0db8449e4b 100644 --- a/lib/archethic/contracts/interpreter/transaction_statements.ex +++ b/lib/archethic/contracts/interpreter/transaction_statements.ex @@ -216,4 +216,146 @@ defmodule Archethic.Contracts.Interpreter.TransactionStatements do &[recipient_address | &1] ) end + + @doc """ + Add multiple recipients + + ## Examples + + iex> address1 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address2 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> TransactionStatements.add_recipients(%Transaction{data: %TransactionData{recipients: [address1]}}, [address2]) + %Transaction{ + data: %TransactionData{ + recipients: [address2, address1] + } + } + """ + @spec add_recipients(Transaction.t(), list(binary())) :: Transaction.t() + def add_recipients(tx = %Transaction{}, args) when is_list(args) do + Enum.reduce(args, tx, &add_recipient(&2, &1)) + end + + @doc """ + Add multiple ownerships + + ## Examples + + iex> {pub_key1, _} = Archethic.Crypto.generate_deterministic_keypair("seed") + iex> {pub_key2, _} = Archethic.Crypto.generate_deterministic_keypair("seed2") + iex> %Transaction{ + ...> data: %TransactionData{ + ...> ownerships: [ + ...> %Ownership{ + ...> authorized_keys: %{ + ...> ^pub_key2 => _ + ...> }, + ...> secret: "ENCODED_SECRET2" + ...> }, + ...> %Ownership{ + ...> authorized_keys: %{ + ...> ^pub_key1 => _ + ...> }, + ...> secret: "ENCODED_SECRET1" + ...> } + ...> ] + ...> } + ...> } = TransactionStatements.add_ownerships(%Transaction{data: %TransactionData{}}, [[ + ...> {"secret", "ENCODED_SECRET1"}, + ...> {"secret_key", :crypto.strong_rand_bytes(32)}, + ...> {"authorized_public_keys", [pub_key1]} + ...> ], + ...> [ + ...> {"secret", "ENCODED_SECRET2"}, + ...> {"secret_key", :crypto.strong_rand_bytes(32)}, + ...> {"authorized_public_keys", [pub_key2]} + ...> ] + ...> ]) + """ + @spec add_ownerships(Transaction.t(), list(list())) :: Transaction.t() + def add_ownerships(tx = %Transaction{}, args) when is_list(args) do + Enum.reduce(args, tx, &add_ownership(&2, &1)) + end + + @doc """ + Add multiple token transfers + + ## Examples + + iex> address1 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address2 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address3 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address4 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> %Transaction{ + ...> data: %TransactionData{ + ...> ledger: %Ledger{ + ...> token: %TokenLedger{ + ...> transfers: [ + ...> %TokenTransfer{ + ...> to: ^address3, + ...> amount: 3, + ...> token_address: ^address4, + ...> token_id: 4 + ...> }, + ...> %TokenTransfer{ + ...> to: ^address1, + ...> amount: 1, + ...> token_address: ^address2, + ...> token_id: 2 + ...> } + ...> ] + ...> } + ...> } + ...> } + ...> } = TransactionStatements.add_token_transfers(%Transaction{data: %TransactionData{}}, [[ + ...> {"to", address1}, + ...> {"amount", 1}, + ...> {"token_address", address2}, + ...> {"token_id", 2} + ...> ], + ...> [ + ...> {"to", address3}, + ...> {"amount", 3}, + ...> {"token_address", address4}, + ...> {"token_id", 4} + ...> ]]) + """ + @spec add_token_transfers(Transaction.t(), list(list())) :: Transaction.t() + def add_token_transfers(tx = %Transaction{}, args) when is_list(args) do + Enum.reduce(args, tx, &add_token_transfer(&2, &1)) + end + + @doc """ + Add multiple UCO transfers + + ## Examples + + iex> address1 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> address2 = <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + iex> %Transaction{ + ...> data: %TransactionData{ + ...> ledger: %Ledger{ + ...> uco: %UCOLedger{ + ...> transfers: [ + ...> %UCOTransfer{ + ...> to: ^address2, + ...> amount: 2 + ...> }, + ...> %UCOTransfer{ + ...> to: ^address1, + ...> amount: 1 + ...> } + ...> ] + ...> } + ...> } + ...> } + ...> } = TransactionStatements.add_uco_transfers(%Transaction{data: %TransactionData{}}, [ + ...> [{"to", address1}, {"amount", 1}], + ...> [{"to", address2}, {"amount", 2}] + ...> ]) + """ + @spec add_uco_transfers(Transaction.t(), list(list())) :: Transaction.t() + def add_uco_transfers(tx = %Transaction{}, args) when is_list(args) do + Enum.reduce(args, tx, &add_uco_transfer(&2, &1)) + end end diff --git a/lib/archethic/contracts/interpreter/utils.ex b/lib/archethic/contracts/interpreter/utils.ex index 856cbbb93a..0b9a7c6443 100644 --- a/lib/archethic/contracts/interpreter/utils.ex +++ b/lib/archethic/contracts/interpreter/utils.ex @@ -287,6 +287,15 @@ defmodule Archethic.Contracts.Interpreter.Utils do {node, acc} end + # Whitelist the get_token_id/1 function + def prewalk( + node = {{:atom, "get_token_id"}, _, [_address]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root do + {node, acc} + end + # Whitelist the timestamp/0 function in condition def prewalk(node = {{:atom, "timestamp"}, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root do diff --git a/lib/archethic/contracts/worker.ex b/lib/archethic/contracts/worker.ex index 2cd95bbeb7..292166d74e 100644 --- a/lib/archethic/contracts/worker.ex +++ b/lib/archethic/contracts/worker.ex @@ -103,11 +103,16 @@ defmodule Archethic.Contracts.Worker do contract_transaction = Constants.to_transaction(contract_constants) with true <- ConditionInterpreter.valid_conditions?(transaction_condition, constants), - next_tx <- ActionInterpreter.execute(Map.fetch!(triggers, :transaction), constants), + next_tx = %Transaction{} <- + ActionInterpreter.execute(Map.fetch!(triggers, :transaction), constants), {:ok, next_tx} <- chain_transaction(next_tx, contract_transaction) do handle_new_transaction(next_tx) {:noreply, state} else + nil -> + # returned by ActionInterpreter.execute when contract did not create a next tx + {:noreply, state} + false -> Logger.debug("Incoming transaction didn't match the condition", transaction_address: Base.encode16(incoming_tx.address), @@ -150,7 +155,7 @@ defmodule Archethic.Contracts.Worker do contract_tx = Constants.to_transaction(contract_constants) - with next_tx <- + with next_tx = %Transaction{} <- ActionInterpreter.execute(Map.fetch!(triggers, {:datetime, timestamp}), constants), {:ok, next_tx} <- chain_transaction(next_tx, contract_tx) do handle_new_transaction(next_tx) @@ -184,7 +189,7 @@ defmodule Archethic.Contracts.Worker do contract_transaction = Constants.to_transaction(contract_constants) with true <- enough_funds?(address), - next_tx <- + next_tx = %Transaction{} <- ActionInterpreter.execute(Map.fetch!(triggers, {:interval, interval}), constants), {:ok, next_tx} <- chain_transaction(next_tx, contract_transaction), :ok <- ensure_enough_funds(next_tx, address) do @@ -218,23 +223,29 @@ defmodule Archethic.Contracts.Worker do contract_transaction = Constants.to_transaction(contract_constants) if Conditions.empty?(oracle_condition) do - with next_tx <- ActionInterpreter.execute(Map.fetch!(triggers, :oracle), constants), + with next_tx = %Transaction{} <- + ActionInterpreter.execute(Map.fetch!(triggers, :oracle), constants), {:ok, next_tx} <- chain_transaction(next_tx, contract_transaction), :ok <- ensure_enough_funds(next_tx, address) do handle_new_transaction(next_tx) end else with true <- ConditionInterpreter.valid_conditions?(oracle_condition, constants), - next_tx <- ActionInterpreter.execute(Map.fetch!(triggers, :oracle), constants), + next_tx = %Transaction{} <- + ActionInterpreter.execute(Map.fetch!(triggers, :oracle), constants), {:ok, next_tx} <- chain_transaction(next_tx, contract_transaction), :ok <- ensure_enough_funds(next_tx, address) do handle_new_transaction(next_tx) else + nil -> + # returned by ActionInterpreter.execute when contract did not create a next tx + :ok + false -> - Logger.error("Invalid oracle conditions", contract: Base.encode16(address)) + Logger.info("Invalid oracle conditions", contract: Base.encode16(address)) {:error, e} -> - Logger.error("#{inspect(e)}", contract: Base.encode16(address)) + Logger.info("#{inspect(e)}", contract: Base.encode16(address)) end end end diff --git a/lib/archethic/mining/distributed_workflow.ex b/lib/archethic/mining/distributed_workflow.ex index 8c21e8c227..5415e40cb8 100644 --- a/lib/archethic/mining/distributed_workflow.ex +++ b/lib/archethic/mining/distributed_workflow.ex @@ -775,7 +775,10 @@ defmodule Archethic.Mining.DistributedWorkflow do }) beacon_storage_nodes = ValidationContext.get_beacon_replication_nodes(context) - P2P.broadcast_message(P2P.distinct_nodes([welcome_node | beacon_storage_nodes]), message) + + [welcome_node | beacon_storage_nodes] + |> P2P.distinct_nodes() + |> P2P.broadcast_message(message) validated_tx = ValidationContext.get_validated_transaction(context) diff --git a/lib/archethic/mining/standalone_workflow.ex b/lib/archethic/mining/standalone_workflow.ex index 0dbf630421..feb3249a2f 100644 --- a/lib/archethic/mining/standalone_workflow.ex +++ b/lib/archethic/mining/standalone_workflow.ex @@ -322,6 +322,8 @@ defmodule Archethic.Mining.StandaloneWorkflow do notify_attestation(context) notify_io_nodes(context) notify_previous_chain(context) + + :ok end defp notify_attestation( diff --git a/lib/archethic/p2p.ex b/lib/archethic/p2p.ex index e6fe4c14cc..701d01bb4a 100644 --- a/lib/archethic/p2p.ex +++ b/lib/archethic/p2p.ex @@ -620,6 +620,7 @@ defmodule Archethic.P2P do message :: Message.t(), conflict_resolver :: (list(Message.t()) -> Message.t()), timeout :: non_neg_integer(), + acceptance_resolver :: (Message.t() -> boolean()), consistency_level :: pos_integer() ) :: {:ok, Message.t()} | {:error, :network_issue} @@ -628,24 +629,40 @@ defmodule Archethic.P2P do message, conflict_resolver \\ fn results -> List.first(results) end, timeout \\ 0, + acceptance_resolver \\ fn _ -> true end, consistency_level \\ 3 ) - def quorum_read(nodes, message, conflict_resolver, timeout, consistency_level) do + def quorum_read( + nodes, + message, + conflict_resolver, + timeout, + acceptance_resolver, + consistency_level + ) do nodes |> Enum.filter(&Node.locally_available?/1) |> nearest_nodes() |> unprioritize_node(Crypto.first_node_public_key()) - |> do_quorum_read(message, conflict_resolver, timeout, consistency_level, nil) + |> do_quorum_read( + message, + conflict_resolver, + acceptance_resolver, + timeout, + consistency_level, + nil + ) end - defp do_quorum_read([], _, _, _, _, nil), do: {:error, :network_issue} - defp do_quorum_read([], _, _, _, _, previous_result), do: {:ok, previous_result} + defp do_quorum_read([], _, _, _, _, _, nil), do: {:error, :network_issue} + defp do_quorum_read([], _, _, _, _, _, previous_result), do: {:ok, previous_result} defp do_quorum_read( nodes, message, conflict_resolver, + acceptance_resolver, timeout, consistency_level, previous_result @@ -675,6 +692,7 @@ defmodule Archethic.P2P do rest, message, conflict_resolver, + acceptance_resolver, consistency_level, timeout, previous_result @@ -682,15 +700,52 @@ defmodule Archethic.P2P do 1 -> if previous_result != nil do - do_quorum([previous_result | results], conflict_resolver) + quorum_result = do_quorum([previous_result | results], conflict_resolver) + + if acceptance_resolver.(quorum_result) do + {:ok, quorum_result} + else + do_quorum_read( + rest, + message, + conflict_resolver, + acceptance_resolver, + consistency_level, + timeout, + quorum_result + ) + end else result = List.first(results) - do_quorum_read(rest, message, conflict_resolver, consistency_level - 1, timeout, result) + + do_quorum_read( + rest, + message, + conflict_resolver, + acceptance_resolver, + consistency_level, + timeout, + result + ) end _ -> results = if previous_result != nil, do: [previous_result | results], else: results - do_quorum(results, conflict_resolver) + quorum_result = do_quorum(results, conflict_resolver) + + if acceptance_resolver.(quorum_result) do + {:ok, quorum_result} + else + do_quorum_read( + rest, + message, + conflict_resolver, + acceptance_resolver, + consistency_level, + timeout, + quorum_result + ) + end end end @@ -699,12 +754,11 @@ defmodule Archethic.P2P do # If the results are the same, then we reached consistency if length(distinct_elems) == 1 do - {:ok, List.first(distinct_elems)} + List.first(distinct_elems) else # If the results differ, we can apply a conflict resolver to merge the result into # a consistent response - resolved_result = conflict_resolver.(distinct_elems) - {:ok, resolved_result} + conflict_resolver.(distinct_elems) end end end diff --git a/lib/archethic/p2p/client.ex b/lib/archethic/p2p/client.ex index 53c5f74518..dff8138b37 100644 --- a/lib/archethic/p2p/client.ex +++ b/lib/archethic/p2p/client.ex @@ -18,7 +18,11 @@ defmodule Archethic.P2P.Client do Crypto.key() ) :: Supervisor.on_start() - @callback send_message(Node.t(), Message.request(), timeout()) :: + @callback send_message( + node :: Node.t(), + message :: Message.request(), + timeout :: non_neg_integer() + ) :: {:ok, Message.response()} | {:error, :timeout} | {:error, :closed} diff --git a/lib/archethic/p2p/client/default_impl.ex b/lib/archethic/p2p/client/default_impl.ex index ab19bc33d0..08294d8111 100644 --- a/lib/archethic/p2p/client/default_impl.ex +++ b/lib/archethic/p2p/client/default_impl.ex @@ -59,6 +59,10 @@ defmodule Archethic.P2P.Client.DefaultImpl do @doc """ Send a message to the given node using the right connection bearer """ + @spec send_message(Node.t(), message :: Message.request(), timeout :: non_neg_integer()) :: + {:ok, Message.response()} + | {:error, :timeout} + | {:error, :closed} @impl Client def send_message( %Node{first_public_key: node_public_key}, diff --git a/lib/archethic/self_repair/repair_worker.ex b/lib/archethic/self_repair/repair_worker.ex index 885f768c6a..73cc58c5b5 100644 --- a/lib/archethic/self_repair/repair_worker.ex +++ b/lib/archethic/self_repair/repair_worker.ex @@ -10,6 +10,9 @@ defmodule Archethic.SelfRepair.RepairWorker do SelfRepair } + alias Archethic.P2P.Message + alias Archethic.TransactionChain.Transaction + alias Archethic.SelfRepair.RepairRegistry use GenServer, restart: :transient @@ -126,9 +129,22 @@ defmodule Archethic.SelfRepair.RepairWorker do address: Base.encode16(address) ) + timeout = Message.get_max_timeout() + + acceptance_resolver = fn + {:ok, %Transaction{address: ^address}} -> true + _ -> false + end + with false <- TransactionChain.transaction_exists?(address), storage_nodes <- Election.chain_storage_nodes(address, authorized_nodes), - {:ok, tx} <- TransactionChain.fetch_transaction_remotely(address, storage_nodes) do + {:ok, tx} <- + TransactionChain.fetch_transaction_remotely( + address, + storage_nodes, + timeout, + acceptance_resolver + ) do if storage? do case Replication.validate_and_store_transaction_chain(tx, true, authorized_nodes) do :ok -> SelfRepair.update_last_address(address, authorized_nodes) diff --git a/lib/archethic/self_repair/sync/transaction_handler.ex b/lib/archethic/self_repair/sync/transaction_handler.ex index 81198032bf..0c2534a76f 100644 --- a/lib/archethic/self_repair/sync/transaction_handler.ex +++ b/lib/archethic/self_repair/sync/transaction_handler.ex @@ -7,6 +7,8 @@ defmodule Archethic.SelfRepair.Sync.TransactionHandler do alias Archethic.P2P + alias Archethic.P2P.Message + alias Archethic.Replication alias Archethic.TransactionChain @@ -66,7 +68,19 @@ defmodule Archethic.SelfRepair.Sync.TransactionHandler do |> Election.chain_storage_nodes_with_type(type, node_list) |> Enum.reject(&(&1.first_public_key == Crypto.first_node_public_key())) - case TransactionChain.fetch_transaction_remotely(address, storage_nodes) do + timeout = Message.get_max_timeout() + + acceptance_resolver = fn + {:ok, %Transaction{address: ^address}} -> true + _ -> false + end + + case TransactionChain.fetch_transaction_remotely( + address, + storage_nodes, + timeout, + acceptance_resolver + ) do {:ok, tx = %Transaction{}} -> tx diff --git a/lib/archethic/transaction_chain.ex b/lib/archethic/transaction_chain.ex index 6c58c88d02..41a1f65c36 100644 --- a/lib/archethic/transaction_chain.ex +++ b/lib/archethic/transaction_chain.ex @@ -688,17 +688,23 @@ defmodule Archethic.TransactionChain do @spec fetch_transaction_remotely( address :: Crypto.versioned_hash(), list(Node.t()), - non_neg_integer() + non_neg_integer(), + (Message.t() -> boolean()) ) :: {:ok, Transaction.t()} | {:error, :transaction_not_exists} | {:error, :transaction_invalid} | {:error, :network_issue} - def fetch_transaction_remotely(address, nodes, timeout \\ Message.get_max_timeout()) + def fetch_transaction_remotely( + address, + nodes, + timeout \\ Message.get_max_timeout(), + acceptance_resolver \\ fn _ -> true end + ) - def fetch_transaction_remotely(_, [], _), do: {:error, :transaction_not_exists} + def fetch_transaction_remotely(_, [], _, _), do: {:error, :transaction_not_exists} - def fetch_transaction_remotely(address, nodes, timeout) + def fetch_transaction_remotely(address, nodes, timeout, acceptance_resolver) when is_binary(address) and is_list(nodes) do conflict_resolver = fn results -> # Prioritize transactions results over not found @@ -715,7 +721,8 @@ defmodule Archethic.TransactionChain do nodes, %GetTransaction{address: address}, conflict_resolver, - timeout + timeout, + acceptance_resolver ) do {:ok, %NotFound{}} -> {:error, :transaction_not_exists} diff --git a/lib/archethic/utils.ex b/lib/archethic/utils.ex index 1b3ee0b372..499f3e7f0b 100644 --- a/lib/archethic/utils.ex +++ b/lib/archethic/utils.ex @@ -12,6 +12,9 @@ defmodule Archethic.Utils do alias Archethic.P2P.Node + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + alias Archethic.Reward.Scheduler, as: RewardScheduler import Bitwise @@ -923,4 +926,63 @@ defmodule Archethic.Utils do |> Stream.take_while(&(NaiveDateTime.compare(&1, end_of_month_datetime) in [:lt])) |> Enum.count() end + + @doc """ + get token properties based on the genesis address and the transaction + """ + @spec get_token_properties(binary(), Transaction.t()) :: + {:ok, map()} | {:error, :decode_error} | {:error, :not_a_token_transaction} + def get_token_properties(genesis_address, %Transaction{ + data: %TransactionData{ + content: content, + ownerships: ownerships + }, + type: tx_type + }) + when tx_type in [:token, :mint_rewards] do + case Jason.decode(content) do + {:ok, map} -> + result = %{ + genesis: genesis_address, + name: Map.get(map, "name", ""), + supply: Map.get(map, "supply"), + symbol: Map.get(map, "symbol", ""), + type: Map.get(map, "type"), + decimals: Map.get(map, "decimals", 8), + properties: Map.get(map, "properties", %{}), + collection: Map.get(map, "collection", []), + ownerships: ownerships + } + + token_id = get_token_id(genesis_address, result) + + {:ok, Map.put(result, :id, token_id)} + + _ -> + {:error, :decode_error} + end + end + + def get_token_properties(_, _), do: {:error, :not_a_token_transaction} + + defp get_token_id(genesis_address, %{ + genesis: genesis_address, + name: name, + symbol: symbol, + decimals: decimals, + properties: properties + }) do + data_to_digest = + %{ + genesis_address: Base.encode16(genesis_address), + name: name, + symbol: symbol, + properties: properties, + decimals: decimals + } + |> Jason.encode!() + + :crypto.hash(:sha256, data_to_digest) + |> Base.encode16() + end end diff --git a/lib/archethic_web/graphql_schema/resolver.ex b/lib/archethic_web/graphql_schema/resolver.ex index 485c97d660..43e90ffd73 100644 --- a/lib/archethic_web/graphql_schema/resolver.ex +++ b/lib/archethic_web/graphql_schema/resolver.ex @@ -13,11 +13,12 @@ defmodule ArchethicWeb.GraphQLSchema.Resolver do alias Archethic.TransactionChain alias Archethic.TransactionChain.Transaction - alias Archethic.TransactionChain.TransactionData alias Archethic.TransactionChain.TransactionInput alias Archethic.Mining + alias Archethic.Utils + require Logger @limit_page 10 @@ -51,58 +52,30 @@ defmodule ArchethicWeb.GraphQLSchema.Resolver do def get_token(address) do t1 = Task.async(fn -> Archethic.fetch_genesis_address_remotely(address) end) - t2 = Task.async(fn -> get_transaction_content(address) end) + t2 = Task.async(fn -> Archethic.search_transaction(address) end) with {:ok, {:ok, genesis_address}} <- Task.yield(t1), - {:ok, - {:ok, - definition = %{ - "ownerships" => ownerships, - "supply" => supply, - "type" => type - }}} <- Task.yield(t2) do - properties = Map.get(definition, "properties", %{}) - collection = Map.get(definition, "collection", []) - decimals = Map.get(definition, "decimals", 8) - name = Map.get(definition, "name", "") - symbol = Map.get(definition, "symbol", "") - - data_to_digest = %{ - genesis_address: Base.encode16(genesis_address), - name: name, - symbol: symbol, - properties: properties, - decimals: decimals - } - - token_id = :crypto.hash(:sha256, Jason.encode!(data_to_digest)) |> Base.encode16() - - {:ok, - %{ - genesis: genesis_address, - name: name, - supply: supply, - symbol: symbol, - type: type, - decimals: decimals, - properties: properties, - collection: collection, - ownerships: ownerships, - id: token_id - }} + {:ok, {:ok, tx}} <- Task.yield(t2), + res = {:ok, _get_token_properties} <- Utils.get_token_properties(genesis_address, tx) do + res else {:ok, {:error, :network_issue}} -> {:error, "Network issue"} - {:ok, {:error, :decode_error}} -> + {:ok, {:error, :transaction_not_exists}} -> + {:error, "Transaction not exists"} + + {:ok, {:error, :transaction_invalid}} -> + {:error, "Transaction invalid"} + + {:error, :decode_error} -> {:error, "Error in decoding transaction"} - {:ok, {:error, :transaction_not_found}} -> - {:error, "Transaction does not exist!"} + {:error, :not_a_token_transaction} -> + {:error, "Transaction is not of type token"} {:exit, reason} -> - Logger.debug("Task exited with reason") - Logger.debug(reason) + Logger.debug("Task exited with reason #{inspect(reason)}") {:error, "Task Exited!"} nil -> @@ -110,24 +83,6 @@ defmodule ArchethicWeb.GraphQLSchema.Resolver do end end - defp get_transaction_content(address) do - case Archethic.search_transaction(address) do - {:ok, - %Transaction{data: %TransactionData{content: content, ownerships: ownerships}, type: type}} - when type in [:token, :mint_rewards] -> - case Jason.decode(content) do - {:ok, map} -> - {:ok, map |> Map.put("ownerships", ownerships)} - - _ -> - {:error, :decode_error} - end - - _ -> - {:error, :transaction_not_found} - end - end - def get_inputs(address, paging_offset \\ 0, limit \\ 0) do inputs = address diff --git a/mix.exs b/mix.exs index 8667531243..d16e2d6f70 100644 --- a/mix.exs +++ b/mix.exs @@ -23,7 +23,16 @@ defmodule Archethic.MixProject do # Run "mix help compile.app" to learn about applications. def application do [ - extra_applications: [:logger, :inets, :os_mon, :runtime_tools, :xmerl], + extra_applications: [ + :public_key, + :crypto, + :logger, + :inets, + :os_mon, + :runtime_tools, + :xmerl, + :crypto + ], mod: {Archethic.Application, []} ] end diff --git a/mix.lock b/mix.lock index 58a96ecc89..fed321920a 100644 --- a/mix.lock +++ b/mix.lock @@ -9,7 +9,7 @@ "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, "castore": {:hex, :castore, "1.0.0", "c25cd0794c054ebe6908a86820c8b92b5695814479ec95eeff35192720b71eec", [:mix], [], "hexpm", "577d0e855983a97ca1dfa33cbb8a3b6ece6767397ffb4861514343b078fc284b"}, - "cldr_utils": {:hex, :cldr_utils, "2.19.1", "5a7bcd2f2fd432c548e494e850bba8a9e838f1b10202f682ea1d9809d74eff31", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "fbd10f79363e70f3d893ab21e195f444ca87c2c80120b5911761491da4489620"}, + "cldr_utils": {:hex, :cldr_utils, "2.21.0", "1bdbb8de3870ab4831f11f877b40cce838a03bf7da272430c232c19726d53f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "26f56101663f5aca4e727e0eb983b578ba5b170e2f12e8456df9995809a7a93b"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, @@ -22,14 +22,14 @@ "dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"}, "digital_token": {:hex, :digital_token, "0.4.0", "2ad6894d4a40be8b2890aad286ecd5745fa473fa5699d80361a8c94428edcd1f", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a178edf61d1fee5bb3c34e14b0f4ee21809ee87cade8738f87337e59e5e66e26"}, "distillery": {:git, "https://github.com/archethic-foundation/distillery.git", "67accaa239dcbe14fc312832c83b23eaaeed66ff", []}, - "earmark": {:hex, :earmark, "1.4.34", "d7f89d3bbd7567a0bffc465e0a949f8f8dcbe43909c3acf96f4761a302cea10c", [:mix], [{:earmark_parser, "~> 1.4.29", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "90b106f3dad85b133b10d7d628167c88246123fd1cecb4557d83d21ec9e65504"}, - "earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"}, + "earmark": {:hex, :earmark, "1.4.35", "e067aab15367c6e43230d6a7409c5230403a48b56f7dcefb3abdad75b498289e", [:mix], [{:earmark_parser, "~> 1.4.30", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "ab869cad78ebe64a62d45ee31addc52fb703c5d595868c9aa11ca38766ff9756"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.30", "0b938aa5b9bafd455056440cdaa2a79197ca5e693830b4a982beada840513c5f", [:mix], [], "hexpm", "3b5385c2d36b0473d0b206927b841343d25adb14f95f0110062506b300cd5a1b"}, "easy_ssl": {:hex, :easy_ssl, "1.3.0", "472256942d9dd37652a558a789a8d1cccc27e7f46352e32667d1ca46bb9e22e5", [:mix], [], "hexpm", "ce8fcb7661442713a94853282b56cee0b90c52b983a83aa6af24686d301808e1"}, "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"}, "elixir_make": {:hex, :elixir_make, "0.7.3", "c37fdae1b52d2cc51069713a58c2314877c1ad40800a57efb213f77b078a460d", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "24ada3e3996adbed1fa024ca14995ef2ba3d0d17b678b0f3f2b1f66e6ce2b274"}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"}, - "ex_cldr": {:hex, :ex_cldr, "2.34.0", "50385e1445f33537bea7d24ca7525aa849ccabb3cf88ac41a6d183693116aba7", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:cldr_utils, "~> 2.18", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "d026df7d580424ab8daf0d908fd38d3c12e0a63962b495b7faeabd66ab072588"}, + "ex_cldr": {:hex, :ex_cldr, "2.34.1", "b4e32d9fb4f7d49211faa45e8a871afff5c5eb3c2f0763b5dd49e3c7df16a0dd", [:mix], [{:cldr_utils, "~> 2.19", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "bd635b9e76271baa5db67a7c224be485f31cea8f7c05d6b0daeefa9d04cf76c0"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.14.2", "354b48134faa011d58ae2e89be69b7de607d81fcc74c7ac684c5fb77b20186f5", [:mix], [{:ex_cldr, "~> 2.27", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "c970533103cdc97b1dedc2fead2209c0f5ae0aee0f1e504fdea5be5ee1466cd1"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.29.0", "ce20899a734ac33cf088c57685035ca1626d5564f50e0ac4d3da24202b112d5a", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "134e5e82d3894f0e06f096124c24f4ef1f54f1874c35714d52aa7d370a25c306"}, "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, @@ -63,9 +63,9 @@ "phoenix": {:hex, :phoenix, "1.6.15", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"}, "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, "phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.7.2", "97cc4ff2dba1ebe504db72cb45098cb8e91f11160528b980bd282cc45c73b29c", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.5", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.18.3", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6 or ~> 1.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0e5fdf063c7a3b620c566a30fcf68b7ee02e5e46fe48ee46a6ec3ba382dc05b7"}, - "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.9", "476264587c780ccd01a6ba7bae5d8c24e2dbe6eb9e56bc38df884c01ca47012b", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d0dd1444cad2028872eafcbef80d0382c53540265b971afbe671918e0eafe511"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.18.11", "c50eac83dae6b5488859180422dfb27b2c609de87f4aa5b9c926ecd0501cd44f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76c99a0ffb47cd95bf06a917e74f282a603f3e77b00375f3c2dd95110971b102"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, - "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, "plug_attack": {:hex, :plug_attack, "0.4.3", "88e6c464d68b1491aa083a0347d59d58ba71a7e591a7f8e1b675e8c7792a0ba8", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9ed6fb8a6f613a36040f2875130a21187126c5625092f24bc851f7f12a8cbdc1"}, diff --git a/src/c/nat/miniupnp b/src/c/nat/miniupnp index 98cc9f1b43..72b33d7145 160000 --- a/src/c/nat/miniupnp +++ b/src/c/nat/miniupnp @@ -1 +1 @@ -Subproject commit 98cc9f1b43de0cebfa1f0c3fdbdb4c04c1ee5a28 +Subproject commit 72b33d7145219e58c19dc5e8521a2e5c86c72cb9 diff --git a/test/archethic/contracts/interpreter/action_test.exs b/test/archethic/contracts/interpreter/action_test.exs index 2c59b167c7..5e962d3733 100644 --- a/test/archethic/contracts/interpreter/action_test.exs +++ b/test/archethic/contracts/interpreter/action_test.exs @@ -524,4 +524,139 @@ defmodule Archethic.Contracts.ActionInterpreterTest do } }) end + + describe "blacklist" do + test "should parse when arguments are allowed" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: "ABC123", amount: 64 + add_token_transfer to: "ABC123", amount: 64, token_id: 0, token_address: "012" + add_ownership secret: "ABC123", secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when arguments are variables" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + address = "ABC123" + add_uco_transfer to: address, amount: 64 + add_token_transfer to: address, amount: 64, token_id: 0, token_address: "012" + add_ownership secret: address, secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when arguments are fields" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: transaction.address, amount: 64 + add_token_transfer to: transaction.address, amount: 64, token_id: 0, token_address: "012" + add_ownership secret: transaction.address, secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when arguments are functions" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: regex_extract("@addr", ".*"), amount: 64 + add_token_transfer to: regex_extract("@addr", ".*"), amount: 64, token_id: 0, token_address: "012" + add_ownership secret: regex_extract("@addr", ".*"), secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when arguments are string interpolation" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + name = "sophia" + add_uco_transfer to: "hello #{name}", amount: 64 + add_token_transfer to: "hello #{name}", amount: 64, token_id: 0, token_address: "012" + add_ownership secret: "hello #{name}", secret_key: "s3cr3t", authorized_public_keys: ["ADE459"] + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should parse when building a keyword list" do + assert {:ok, :transaction, _ast} = + ~S""" + actions triggered_by: transaction do + uco_transfer = [to: "ABC123", amount: 33] + add_uco_transfer uco_transfer + + token_transfer = [to: "ABC123", amount: 64, token_id: 0, token_address: "012"] + add_token_transfer token_transfer + + ownership = [secret: "ABC123", secret_key: "s3cr3t", authorized_public_keys: ["ADE459"]] + add_ownership ownership + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should not parse when arguments are not allowed" do + assert {:error, "invalid add_uco_transfer arguments - amount"} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: "abc123", amount: 0 + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + + assert {:error, "invalid add_uco_transfer arguments - hello"} = + ~S""" + actions triggered_by: transaction do + add_uco_transfer to: "abc123", amount: 31, hello: 1 + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + + assert {:error, "invalid add_token_transfer arguments - amount"} = + ~S""" + actions triggered_by: transaction do + add_token_transfer to: "abc123", amount: "thirty one" + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + + assert {:error, "invalid add_ownership arguments - authorized_public_keys"} = + ~S""" + actions triggered_by: transaction do + add_ownership secret: "ABC123", secret_key: "s3cr3t", authorized_public_keys: 42 + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + end end diff --git a/test/archethic/contracts/interpreter/library_test.exs b/test/archethic/contracts/interpreter/library_test.exs index 072b876f50..00ad420165 100644 --- a/test/archethic/contracts/interpreter/library_test.exs +++ b/test/archethic/contracts/interpreter/library_test.exs @@ -1,17 +1,66 @@ defmodule Archethic.Contracts.Interpreter.LibraryTest do use ArchethicCase - alias Archethic.{Contracts.Interpreter.Library, P2P, P2P.Node} + alias Archethic.Contracts.Interpreter.Library - alias P2P.Message.{ - GetFirstTransactionAddress, - FirstTransactionAddress - } + alias Archethic.P2P.Message.GetFirstTransactionAddress + alias Archethic.P2P.Message.FirstTransactionAddress + + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + + alias Archethic.Utils + + alias Archethic.P2P + alias Archethic.P2P.Node doctest Library import Mox + describe "get_token_id\1" do + test "should return token_id given the address of the transaction" do + tx_seed = :crypto.strong_rand_bytes(32) + + tx = + Transaction.new( + :token, + %TransactionData{ + content: + Jason.encode!(%{ + supply: 300_000_000, + name: "MyToken", + type: "non-fungible", + symbol: "MTK", + properties: %{ + global: "property" + }, + collection: [ + %{image: "link", value: "link"}, + %{image: "link", value: "link"}, + %{image: "link", value: "link"} + ] + }) + }, + tx_seed, + 0 + ) + + genesis_address = "@Alice1" + + MockDB + |> stub(:get_transaction, fn _, _, _ -> {:ok, tx} end) + |> stub(:get_genesis_address, fn _ -> genesis_address end) + + {:ok, %{id: token_id}} = Utils.get_token_properties(genesis_address, tx) + + assert token_id == + Library.get_token_id(tx.address) + end + end + + import Mox + test "get_first_transaction_address/1" do P2P.add_and_connect_node(%Node{ ip: {127, 0, 0, 1}, diff --git a/test/archethic/oracle_chain/services/uco_price_test.exs b/test/archethic/oracle_chain/services/uco_price_test.exs index f2f9937d13..e57d916a17 100644 --- a/test/archethic/oracle_chain/services/uco_price_test.exs +++ b/test/archethic/oracle_chain/services/uco_price_test.exs @@ -44,17 +44,17 @@ defmodule Archethic.OracleChain.Services.UCOPriceTest do test "fetch/0 should retrieve some data and build a map with the oracle name in it and keep the precision to 5" do MockUCOPriceProvider1 - |> expect(:fetch, fn pairs -> + |> expect(:fetch, fn _pairs -> {:ok, %{"eur" => [0.123456789], "usd" => [0.123454789]}} end) MockUCOPriceProvider2 - |> expect(:fetch, fn pairs -> + |> expect(:fetch, fn _pairs -> {:ok, %{"eur" => [0.123456789], "usd" => [0.123454789]}} end) MockUCOPriceProvider3 - |> expect(:fetch, fn pairs -> + |> expect(:fetch, fn _pairs -> {:ok, %{"eur" => [0.123456789], "usd" => [0.123454789]}} end) diff --git a/test/archethic/p2p_test.exs b/test/archethic/p2p_test.exs index 45f9ca2282..05ffb16d7d 100644 --- a/test/archethic/p2p_test.exs +++ b/test/archethic/p2p_test.exs @@ -135,6 +135,37 @@ defmodule Archethic.P2PTest do end end) end + + test "should try all nodes and return last message when no response match acceptance resolver", + %{ + nodes: nodes + } do + MockClient + |> expect( + :send_message, + 4, + fn _node, _message, _timeout -> + {:ok, %Transaction{}} + end + ) + |> expect( + :send_message, + 1, + fn _node, _message, _timeout -> + :timer.sleep(200) + {:ok, %NotFound{}} + end + ) + + assert {:ok, %NotFound{}} = + P2P.quorum_read( + nodes, + %GetTransaction{address: ""}, + fn results -> List.last(results) end, + 0, + fn _ -> false end + ) + end end describe "authorized_and_available_nodes/1" do diff --git a/test/archethic/self_repair/sync/transaction_handler_test.exs b/test/archethic/self_repair/sync/transaction_handler_test.exs index eae8a261ee..eff974b793 100644 --- a/test/archethic/self_repair/sync/transaction_handler_test.exs +++ b/test/archethic/self_repair/sync/transaction_handler_test.exs @@ -130,6 +130,79 @@ defmodule Archethic.SelfRepair.Sync.TransactionHandlerTest do ) end + test "download_transaction/2 should download the transaction even after a first failure" do + inputs = [ + %TransactionInput{ + from: "@Alice2", + amount: 1_000_000_000, + type: :UCO, + timestamp: DateTime.utc_now() + } + ] + + tx = TransactionFactory.create_valid_transaction(inputs) + + pb_key1 = Crypto.derive_keypair("key101", 0) |> elem(0) + pb_key2 = Crypto.derive_keypair("key202", 0) |> elem(0) + pb_key3 = Crypto.derive_keypair("key303", 0) |> elem(0) + + nodes = [ + %Node{ + first_public_key: pb_key1, + last_public_key: pb_key1, + authorized?: true, + available?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-10), + geo_patch: "AAA", + network_patch: "AAA", + reward_address: :crypto.strong_rand_bytes(32), + enrollment_date: DateTime.utc_now() + }, + %Node{ + first_public_key: pb_key2, + last_public_key: pb_key2, + authorized?: true, + available?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-10), + geo_patch: "AAA", + network_patch: "AAA", + reward_address: :crypto.strong_rand_bytes(32), + enrollment_date: DateTime.utc_now() + }, + %Node{ + first_public_key: pb_key3, + last_public_key: pb_key3, + authorized?: true, + available?: true, + authorization_date: DateTime.utc_now() |> DateTime.add(-10), + geo_patch: "AAA", + network_patch: "AAA", + reward_address: :crypto.strong_rand_bytes(32), + enrollment_date: DateTime.utc_now() + } + ] + + Enum.each(nodes, &P2P.add_and_connect_node(&1)) + + MockClient + |> expect(:send_message, 4, fn + _, %GetTransaction{}, _ -> + {:error, :network_issue} + end) + |> expect(:send_message, fn + _, %GetTransaction{}, _ -> + {:ok, tx} + end) + + tx_summary = %TransactionSummary{address: "@Alice2", timestamp: DateTime.utc_now()} + + assert ^tx = + TransactionHandler.download_transaction( + tx_summary, + P2P.authorized_and_available_nodes() + ) + end + test "process_transaction/1 should handle the transaction and replicate it" do me = self() diff --git a/test/archethic_web/graphql_schema_test.exs b/test/archethic_web/graphql_schema_test.exs index fedbba6cf8..c53964b257 100644 --- a/test/archethic_web/graphql_schema_test.exs +++ b/test/archethic_web/graphql_schema_test.exs @@ -348,8 +348,8 @@ defmodule ArchethicWeb.GraphQLSchemaTest do _, %GetGenesisAddress{}, _ -> {:ok, %NotFound{}} - _, %GetLastTransactionAddress{address: address}, _ -> - {:ok, %LastTransactionAddress{address: address}} + _, %GetLastTransactionAddress{}, _ -> + {:ok, %LastTransactionAddress{address: last}} end) conn =