diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index d6f542bda..cbc1ab64a 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -587,6 +587,15 @@ defmodule ArchEthic.Contracts.Interpreter do when scope != :root, do: {node, acc} + # Whitelist the get_genesis_address/1 function + defp prewalk( + node = {{:atom, "get_genesis_address"}, _, [_address]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root do + {node, acc} + end + # Whitelist the regex_match?/1 function in the condition defp prewalk( node = {{:atom, "regex_match?"}, _, [_search]}, @@ -631,6 +640,14 @@ defmodule ArchEthic.Contracts.Interpreter do defp prewalk(node = {{:atom, "size"}, _, []}, acc = {:ok, %{scope: :condition}}), do: {node, acc} + # Whitelist the get_genesis_address/0 function in condition + defp prewalk( + node = {{:atom, "get_genesis_address"}, _, []}, + acc = {:ok, %{scope: :condition}} + ) do + {node, acc} + end + # Whitelist the used of functions in the actions defp prewalk(node = {{:atom, fun_name}, _, _}, {:ok, acc = %{scope: :actions}}) when fun_name in @transaction_statements_functions_names, diff --git a/lib/archethic/contracts/interpreter/library.ex b/lib/archethic/contracts/interpreter/library.ex index 654f7b1f2..d6282e362 100644 --- a/lib/archethic/contracts/interpreter/library.ex +++ b/lib/archethic/contracts/interpreter/library.ex @@ -2,6 +2,10 @@ defmodule ArchEthic.Contracts.Interpreter.Library do @moduledoc false alias ArchEthic.Crypto + alias ArchEthic.P2P + alias ArchEthic.P2P.Message.GetFirstAddress + alias ArchEthic.P2P.Message.FirstAddress + alias ArchEthic.Election @doc """ Match a regex expression @@ -79,12 +83,12 @@ defmodule ArchEthic.Contracts.Interpreter.Library do end @doc ~S""" - Match a json path expression + Match a json path expression ## Examples iex> Library.json_path_match?("{\"1622541930\":{\"uco\":{\"eur\":0.176922,\"usd\":0.21642}}}", "$.*.uco.usd") - true + true """ @spec json_path_match?(binary(), binary()) :: boolean() def json_path_match?(text, path) when is_binary(text) and is_binary(path) do @@ -149,4 +153,25 @@ defmodule ArchEthic.Contracts.Interpreter.Library do def size(binary) when is_binary(binary), do: byte_size(binary) def size(list) when is_list(list), do: length(list) def size(map) when is_map(map), do: map_size(map) + + @doc """ + Get the genesis address of the chain + + """ + @spec get_genesis_address(binary()) :: + binary() + def get_genesis_address(address) do + nodes = Election.chain_storage_nodes(address, P2P.available_nodes()) + {:ok, address} = download_first_address(nodes, address) + address + end + + defp download_first_address([node | rest], address) do + case P2P.send_message(node, %GetFirstAddress{address: address}) do + {:ok, %FirstAddress{address: address}} -> {:ok, address} + {:error, _} -> download_first_address(rest, address) + end + end + + defp download_first_address([], _address), do: {:error, :network_issue} end diff --git a/lib/archethic/p2p/message.ex b/lib/archethic/p2p/message.ex index 810170b09..f2c4824d8 100644 --- a/lib/archethic/p2p/message.ex +++ b/lib/archethic/p2p/message.ex @@ -27,6 +27,8 @@ defmodule ArchEthic.P2P.Message do alias __MODULE__.EncryptedStorageNonce alias __MODULE__.Error alias __MODULE__.FirstPublicKey + alias __MODULE__.FirstAddress + alias __MODULE__.GetFirstAddress alias __MODULE__.GetBalance alias __MODULE__.GetBeaconSummaries alias __MODULE__.GetBeaconSummary @@ -114,6 +116,7 @@ defmodule ArchEthic.P2P.Message do | BeaconUpdate.t() | TransactionSummary.t() | ReplicationAttestation.t() + | GetFirstAddress.t() @type response :: Ok.t() @@ -135,6 +138,7 @@ defmodule ArchEthic.P2P.Message do | Error.t() | Summary.t() | BeaconSummaryList.t() + | FirstAddress.t() @doc """ Extract the Message Struct name @@ -349,6 +353,14 @@ defmodule ArchEthic.P2P.Message do <<30::8, ReplicationAttestation.serialize(attestation)::binary>> end + def encode(%GetFirstAddress{address: address}) do + <<31::8, address::binary>> + end + + def encode(%FirstAddress{address: address}) do + <<235::8, address::binary>> + end + def encode(%BeaconUpdate{transaction_attestations: transaction_attestations}) do transaction_attestations_bin = transaction_attestations @@ -788,6 +800,16 @@ defmodule ArchEthic.P2P.Message do ReplicationAttestation.deserialize(rest) end + def decode(<<31::8, rest::bitstring>>) do + {address, rest} = Utils.deserialize_address(rest) + {%GetFirstAddress{address: address}, rest} + end + + def decode(<<235::8, rest::bitstring>>) do + {address, rest} = Utils.deserialize_address(rest) + {%FirstAddress{address: address}, rest} + end + def decode(<<236::8, nb_transaction_attestations::16, rest::bitstring>>) do {transaction_attestations, rest} = Utils.deserialize_transaction_attestations(rest, nb_transaction_attestations, []) @@ -1233,6 +1255,16 @@ defmodule ArchEthic.P2P.Message do } end + def process(%GetFirstAddress{address: address}) do + case TransactionChain.get_first_transaction(address, [:address]) do + {:ok, %Transaction{address: address}} -> + %FirstAddress{address: address} + + {:error, :transaction_not_exists} -> + %NotFound{} + end + end + def process(%GetLastTransactionAddress{address: address, timestamp: timestamp}) do address = TransactionChain.get_last_address(address, timestamp) %LastTransactionAddress{address: address} diff --git a/lib/archethic/p2p/message/first_address.ex b/lib/archethic/p2p/message/first_address.ex new file mode 100644 index 000000000..196fcbd5c --- /dev/null +++ b/lib/archethic/p2p/message/first_address.ex @@ -0,0 +1,11 @@ +defmodule ArchEthic.P2P.Message.FirstAddress do + @moduledoc """ + Represents a message to first address from the transaction chain + """ + @enforce_keys [:address] + defstruct [:address] + + @type t :: %__MODULE__{ + address: binary() + } +end diff --git a/lib/archethic/p2p/message/get_first_address.ex b/lib/archethic/p2p/message/get_first_address.ex new file mode 100644 index 000000000..616f2997b --- /dev/null +++ b/lib/archethic/p2p/message/get_first_address.ex @@ -0,0 +1,12 @@ +defmodule ArchEthic.P2P.Message.GetFirstAddress do + @moduledoc """ + Represents a message to request the first address from a transaction chain + """ + + @enforce_keys [:address] + defstruct [:address] + + @type t() :: %__MODULE__{ + address: binary() + } +end diff --git a/test/archethic/contracts/interpreter/library_test.exs b/test/archethic/contracts/interpreter/library_test.exs index 9eeaf47e4..7c4a04a56 100644 --- a/test/archethic/contracts/interpreter/library_test.exs +++ b/test/archethic/contracts/interpreter/library_test.exs @@ -1,5 +1,5 @@ defmodule ArchEthic.Contracts.Interpreter.LibraryTest do - use ExUnit.Case + use ArchEthicCase alias ArchEthic.Contracts.Interpreter.Library diff --git a/test/archethic/contracts/interpreter_test.exs b/test/archethic/contracts/interpreter_test.exs index 003faef2d..482f6de3c 100644 --- a/test/archethic/contracts/interpreter_test.exs +++ b/test/archethic/contracts/interpreter_test.exs @@ -1,5 +1,5 @@ defmodule ArchEthic.Contracts.InterpreterTest do - use ExUnit.Case + use ArchEthicCase alias ArchEthic.Contracts.Contract alias ArchEthic.Contracts.Contract.Conditions @@ -9,14 +9,16 @@ defmodule ArchEthic.Contracts.InterpreterTest do alias ArchEthic.Contracts.Interpreter alias ArchEthic.Crypto - + alias ArchEthic.P2P + alias ArchEthic.P2P.Node + alias ArchEthic.P2P.Message.FirstAddress alias ArchEthic.TransactionChain.Transaction alias ArchEthic.TransactionChain.TransactionData alias ArchEthic.TransactionChain.TransactionData.Ledger alias ArchEthic.TransactionChain.TransactionData.UCOLedger alias ArchEthic.TransactionChain.TransactionData.UCOLedger.Transfer, as: UCOTransfer - + import Mox doctest Interpreter describe "parse/1" do @@ -273,7 +275,7 @@ defmodule ArchEthic.Contracts.InterpreterTest do add_uco_transfer to: \"7F6661ACE282F947ACA2EF947D01BDDC90C65F09EE828BDADE2E3ED4258470B3\", amount: 1040000000 add_nft_transfer to: \"30670455713E2CBECF94591226A903651ED8625635181DDA236FECC221D1E7E4\", amount: 20000000000, nft: \"AEB4A6F5AB6D82BE223C5867EBA5FE616F52F410DCF83B45AFF158DD40AE8AC3\" set_content \"Receipt\" - add_ownership secret: \"MyEncryptedSecret\", secret_key: \"MySecretKey\", authorized_public_keys: ["70C245E5D970B59DF65638BDD5D963EE22E6D892EA224D8809D0FB75D0B1907A"] + add_ownership secret: \"MyEncryptedSecret\", secret_key: \"MySecretKey\", authorized_public_keys: ["70C245E5D970B59DF65638BDD5D963EE22E6D892EA224D8809D0FB75D0B1907A"] add_recipient \"78273C5CBCEB8617F54380CC2F173DF2404DB676C9F10D546B6F395E6F3BDDEE\" end """ @@ -522,7 +524,7 @@ defmodule ArchEthic.Contracts.InterpreterTest do condition inherit: [ type: transfer, uco_transfers: size() == 1 - # TODO: to provide more security, we should check the destination address is within the previous transaction inputs + # TODO: to provide more security, we should check the destination address is within the previous transaction inputs ] @@ -542,4 +544,75 @@ defmodule ArchEthic.Contracts.InterpreterTest do """ |> Interpreter.parse() end + + describe "get_genesis_address/1" do + setup do + key = <<0::16, :crypto.strong_rand_bytes(32)::binary>> + + P2P.add_and_connect_node(%Node{ + ip: {127, 0, 0, 1}, + port: 3000, + first_public_key: key, + last_public_key: key, + available?: true, + geo_patch: "AAA", + network_patch: "AAA", + authorized?: true, + authorization_date: DateTime.utc_now() + }) + + {:ok, [key: key]} + end + + test "shall get the first address of the chain in the conditions" do + address = "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + b_address = Base.decode16!(address) + + MockClient + |> expect(:send_message, fn _, _, _ -> + {:ok, %FirstAddress{address: b_address}} + end) + + {:ok, %Contract{conditions: %{transaction: conditions}}} = + ~s""" + condition transaction: [ + address: get_genesis_address() == "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + ] + """ + |> Interpreter.parse() + + assert true = + Interpreter.valid_conditions?( + conditions, + %{"transaction" => %{"address" => :crypto.strong_rand_bytes(32)}} + ) + end + + @tag :genesis + test "shall parse get_genesis_address/1 in actions" do + address = "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + b_address = Base.decode16!(address) + + MockClient + |> expect(:send_message, fn _, _, _ -> + {:ok, %FirstAddress{address: b_address}} + end) + + {:ok, contract} = + ~s""" + actions triggered_by: transaction do + address = get_genesis_address "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + if address == "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" do + set_content "yes" + else + set_content "no" + end + end + """ + |> Interpreter.parse() + + assert %Transaction{data: %TransactionData{content: "yes"}} = + Interpreter.execute_actions(contract, :transaction) + end + end end