From 12c09b0ef05131f1ecfbc9339710b068a6e0675e Mon Sep 17 00:00:00 2001 From: Samuel Manzanera Date: Tue, 18 Oct 2022 23:37:00 +0200 Subject: [PATCH] Update interpreters --- lib/archethic/contracts.ex | 38 +- lib/archethic/contracts/contract.ex | 54 +- .../contracts/contract/conditions.ex | 5 +- lib/archethic/contracts/contract/constants.ex | 4 +- lib/archethic/contracts/contract/trigger.ex | 19 - lib/archethic/contracts/interpreter.ex | 1382 +---------------- lib/archethic/contracts/interpreter/action.ex | 100 +- .../contracts/interpreter/condition.ex | 445 ++++-- lib/archethic/contracts/interpreter/utils.ex | 131 +- lib/archethic/contracts/loader.ex | 38 +- lib/archethic/contracts/worker.ex | 115 +- .../mining/pending_transaction_validation.ex | 2 +- lib/archethic/mining/proof_of_work.ex | 4 +- .../transaction_chain/mem_tables_loader.ex | 4 +- lib/archethic/utils.ex | 4 +- .../contracts/interpreter/action_test.exs | 351 ++++- .../contracts/interpreter/condition_test.exs | 257 ++- test/archethic/contracts/interpreter_test.exs | 607 +------- test/archethic/contracts/loader_test.exs | 7 +- test/archethic/contracts/worker_test.exs | 7 +- test/archethic/contracts_test.exs | 19 +- 21 files changed, 1329 insertions(+), 2264 deletions(-) delete mode 100644 lib/archethic/contracts/contract/trigger.ex diff --git a/lib/archethic/contracts.ex b/lib/archethic/contracts.ex index b9f91cac26..0011f721df 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -5,9 +5,9 @@ defmodule Archethic.Contracts do """ alias __MODULE__.Contract - alias __MODULE__.Contract.Conditions - alias __MODULE__.Contract.Constants - alias __MODULE__.Contract.Trigger + alias __MODULE__.ContractConditions, as: Conditions + alias __MODULE__.ContractConstants, as: Constants + alias __MODULE__.ConditionInterpreter alias __MODULE__.Interpreter alias __MODULE__.Loader alias __MODULE__.TransactionLookup @@ -59,9 +59,8 @@ defmodule Archethic.Contracts do contract: nil, transaction: nil }, - triggers: [ - %Trigger{ - actions: {:__block__, [], [ + triggers: %{ + {:datetime, ~U[2020-09-25 13:18:43Z]} => {:__block__, [], [ { :=, [line: 7], @@ -79,11 +78,9 @@ defmodule Archethic.Contracts do ] } ]}, - opts: [at: ~U[2020-09-25 13:18:43Z]], - type: :datetime } - ] - }} + } + } """ @spec parse(binary()) :: {:ok, Contract.t()} | {:error, binary()} def parse(contract_code) when is_binary(contract_code) do @@ -100,11 +97,10 @@ defmodule Archethic.Contracts do }) cond do - Enum.any?(triggers, &(&1.type == :transaction)) and - Conditions.empty?(transaction_conditions) -> + Map.has_key?(triggers, :transaction) and Conditions.empty?(transaction_conditions) -> {:error, "missing transaction conditions"} - Enum.any?(triggers, &(&1.type == :oracle)) and Conditions.empty?(oracle_conditions) -> + Map.has_key?(triggers, :oracle) and Conditions.empty?(oracle_conditions) -> {:error, "missing oracle conditions"} true -> @@ -168,7 +164,7 @@ defmodule Archethic.Contracts do end defp validate_conditions(inherit_conditions, constants) do - if Interpreter.valid_conditions?(inherit_conditions, constants) do + if ConditionInterpreter.valid_conditions?(inherit_conditions, constants) do :ok else Logger.error("Inherit constraints not respected") @@ -176,10 +172,12 @@ defmodule Archethic.Contracts do end end - defp validate_triggers([], _, _), do: :ok + defp validate_triggers(triggers, _next_tx, _date) when map_size(triggers) == 0, do: :ok defp validate_triggers(triggers, next_tx, date) do - if Enum.any?(triggers, &valid_from_trigger?(&1, next_tx, date)) do + if Enum.any?(triggers, fn {trigger_type, _} -> + valid_from_trigger?(trigger_type, next_tx, date) + end) do :ok else Logger.error("Transaction not processed by a valid smart contract trigger") @@ -188,7 +186,7 @@ defmodule Archethic.Contracts do end defp valid_from_trigger?( - %Trigger{type: :datetime, opts: [at: datetime]}, + {:datetime, datetime}, %Transaction{}, validation_date = %DateTime{} ) do @@ -198,16 +196,16 @@ defmodule Archethic.Contracts do end defp valid_from_trigger?( - %Trigger{type: :interval, opts: [at: interval]}, + {:interval, interval}, %Transaction{}, validation_date = %DateTime{} ) do interval - |> CronParser.parse!(true) + |> CronParser.parse!() |> CronDateChecker.matches_date?(DateTime.to_naive(validation_date)) end - defp valid_from_trigger?(%Trigger{type: :transaction}, _, _), do: true + defp valid_from_trigger?(_, _, _), do: true @doc """ List the address of the transaction which has contacted a smart contract diff --git a/lib/archethic/contracts/contract.ex b/lib/archethic/contracts/contract.ex index c562859e7c..4659439fd5 100644 --- a/lib/archethic/contracts/contract.ex +++ b/lib/archethic/contracts/contract.ex @@ -3,9 +3,8 @@ defmodule Archethic.Contracts.Contract do Represents a smart contract """ - alias Archethic.Contracts.Contract.Conditions - alias Archethic.Contracts.Contract.Constants - alias Archethic.Contracts.Contract.Trigger + alias Archethic.Contracts.ContractConditions, as: Conditions + alias Archethic.Contracts.ContractConstants, as: Constants alias Archethic.Contracts.Interpreter @@ -14,7 +13,7 @@ defmodule Archethic.Contracts.Contract do alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData - defstruct triggers: [], + defstruct triggers: %{}, conditions: %{ transaction: %Conditions{}, inherit: %Conditions{}, @@ -23,12 +22,15 @@ defmodule Archethic.Contracts.Contract do constants: %Constants{}, next_transaction: %Transaction{data: %TransactionData{}} - @type trigger_type() :: :datetime | :interval | :transaction + @type trigger_type() :: + {:datetime, DateTime.t()} | {:interval, String.t()} | :transaction | :oracle @type condition() :: :transaction | :inherit | :oracle @type origin_family :: SharedSecrets.origin_family() @type t() :: %__MODULE__{ - triggers: list(Trigger.t()), + triggers: %{ + trigger_type() => Macro.t() + }, conditions: %{ transaction: Conditions.t(), inherit: Conditions.t(), @@ -55,48 +57,24 @@ defmodule Archethic.Contracts.Contract do @doc """ Add a trigger to the contract """ - @spec add_trigger(t(), Trigger.type(), Keyword.t(), Macro.t()) :: t() + @spec add_trigger(t(), trigger_type(), Macro.t()) :: t() def add_trigger( contract = %__MODULE__{}, - :datetime, - opts = [at: _datetime = %DateTime{}], + type, actions ) do - do_add_trigger(contract, %Trigger{type: :datetime, opts: opts, actions: actions}) - end - - def add_trigger( - contract = %__MODULE__{}, - :interval, - opts = [at: interval], - actions - ) - when is_binary(interval) do - do_add_trigger(contract, %Trigger{type: :interval, opts: opts, actions: actions}) - end - - def add_trigger(contract = %__MODULE__{}, :transaction, _, actions) do - do_add_trigger(contract, %Trigger{type: :transaction, actions: actions}) - end - - def add_trigger(contract = %__MODULE__{}, :oracle, _, actions) do - do_add_trigger(contract, %Trigger{type: :oracle, actions: actions}) - end - - defp do_add_trigger(contract, trigger = %Trigger{}) do - Map.update!(contract, :triggers, &(&1 ++ [trigger])) + Map.update!(contract, :triggers, &Map.put(&1, type, actions)) end @doc """ Add a condition to the contract """ - @spec add_condition(t(), condition(), any()) :: t() + @spec add_condition(t(), condition(), Conditions.t()) :: t() def add_condition( - contract = %__MODULE__{conditions: conditions}, + contract = %__MODULE__{}, condition_name, - condition - ) - when condition_name in [:transaction, :inherit, :oracle] do - %{contract | conditions: Map.put(conditions, condition_name, condition)} + conditions = %Conditions{} + ) do + Map.update!(contract, :conditions, &Map.put(&1, condition_name, conditions)) end end diff --git a/lib/archethic/contracts/contract/conditions.ex b/lib/archethic/contracts/contract/conditions.ex index fb58bb95d4..81cbae15d9 100644 --- a/lib/archethic/contracts/contract/conditions.ex +++ b/lib/archethic/contracts/contract/conditions.ex @@ -1,9 +1,10 @@ -defmodule Archethic.Contracts.Contract.Conditions do +defmodule Archethic.Contracts.ContractConditions do @moduledoc """ Represents the smart contract conditions """ defstruct [ + :address, :type, :content, :code, @@ -20,6 +21,7 @@ defmodule Archethic.Contracts.Contract.Conditions do alias Archethic.TransactionChain.Transaction @type t :: %__MODULE__{ + address: binary() | Macro.t() | nil, type: Transaction.transaction_type() | nil, content: binary() | Macro.t() | nil, code: binary() | Macro.t() | nil, @@ -33,6 +35,7 @@ defmodule Archethic.Contracts.Contract.Conditions do } def empty?(%__MODULE__{ + address: nil, type: nil, content: nil, code: nil, diff --git a/lib/archethic/contracts/contract/constants.ex b/lib/archethic/contracts/contract/constants.ex index 4177a65706..ffa437a278 100644 --- a/lib/archethic/contracts/contract/constants.ex +++ b/lib/archethic/contracts/contract/constants.ex @@ -1,4 +1,4 @@ -defmodule Archethic.Contracts.Contract.Constants do +defmodule Archethic.Contracts.ContractConstants do @moduledoc """ Represents the smart contract constants and bindings """ @@ -6,7 +6,7 @@ defmodule Archethic.Contracts.Contract.Constants do defstruct [:contract, :transaction] @type t :: %__MODULE__{ - contract: map(), + contract: map() | nil, transaction: map() | nil } diff --git a/lib/archethic/contracts/contract/trigger.ex b/lib/archethic/contracts/contract/trigger.ex deleted file mode 100644 index 697268c09b..0000000000 --- a/lib/archethic/contracts/contract/trigger.ex +++ /dev/null @@ -1,19 +0,0 @@ -defmodule Archethic.Contracts.Contract.Trigger do - @moduledoc """ - Represents the smart contract triggers - """ - - defstruct [:type, :actions, opts: []] - - @type timestamp :: non_neg_integer() - @type interval :: binary() - @type address :: binary() - - @type type() :: :datetime | :interval | :transaction | :oracle - - @type t :: %__MODULE__{ - type: type(), - opts: Keyword.t(), - actions: Macro.t() - } -end diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index b5c9c69ddc..082e073418 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -3,51 +3,12 @@ defmodule Archethic.Contracts.Interpreter do require Logger - alias Crontab.CronExpression.Parser, as: CronParser + alias Archethic.Contracts.ActionInterpreter + alias Archethic.Contracts.ConditionInterpreter - alias __MODULE__.Library - alias __MODULE__.TransactionStatements + alias __MODULE__.Utils, as: InterpreterUtils alias Archethic.Contracts.Contract - alias Archethic.Contracts.Contract.Conditions - - alias Archethic.SharedSecrets - - alias Archethic.TransactionChain.Transaction - alias Archethic.TransactionChain.TransactionData - - @library_functions_names Library.__info__(:functions) - |> Enum.map(&Atom.to_string(elem(&1, 0))) - - @transaction_statements_functions_names TransactionStatements.__info__(:functions) - |> Enum.map(&Atom.to_string(elem(&1, 0))) - - @transaction_fields [ - "address", - "type", - "timestamp", - "previous_signature", - "previous_public_key", - "origin_signature", - "content", - "keys", - "code", - "uco_ledger", - "token_ledger", - "uco_transfers", - "token_transfers", - "authorized_public_keys", - "secrets", - "recipients" - ] - - @condition_fields Conditions.__struct__() - |> Map.keys() - |> Enum.reject(&(&1 == :__struct__)) - |> Enum.map(&Atom.to_string/1) - - @transaction_types Transaction.types() |> Enum.map(&Atom.to_string/1) - @origin_families SharedSecrets.list_origin_families() |> Enum.map(&Atom.to_string/1) @doc ~S""" Parse a smart contract code and return the filtered AST representation. @@ -84,7 +45,7 @@ defmodule Archethic.Contracts.Interpreter do {:ok, %Contract{ conditions: %{ - inherit: %Archethic.Contracts.Contract.Conditions{ + inherit: %Archethic.Contracts.ContractConditions{ content: { :==, [line: 7], @@ -101,7 +62,7 @@ defmodule Archethic.Contracts.Interpreter do type: nil, uco_transfers: nil }, - oracle: %Archethic.Contracts.Contract.Conditions{ + oracle: %Archethic.Contracts.ContractConditions{ content: { :>, [line: 11], @@ -118,14 +79,14 @@ defmodule Archethic.Contracts.Interpreter do ] }, authorized_keys: nil, - code: nil, + code: nil, token_transfers: nil, origin_family: :all, previous_public_key: nil, type: nil, uco_transfers: nil }, - transaction: %Archethic.Contracts.Contract.Conditions{ + transaction: %Archethic.Contracts.ContractConditions{ content: { :==, [line: 2], @@ -143,11 +104,10 @@ defmodule Archethic.Contracts.Interpreter do uco_transfers: nil } }, - constants: %Constants{}, + constants: %Archethic.Contracts.ContractConstants{}, next_transaction: %Transaction{ data: %TransactionData{}}, - triggers: [ - %Trigger{ - actions: {:__block__, [], + triggers: %{ + {:datetime, ~U[2020-10-21 08:56:43Z]} => {:__block__, [], [ {:=, [line: 15], [{:scope, [line: 15], nil}, {{:., [line: 15], [{:__aliases__, [line: 15], [:Map]}, :put]}, [line: 15], [{:scope, [line: 15], nil}, "new_content", "Sent 1040000000"]}]}, { @@ -175,74 +135,50 @@ defmodule Archethic.Contracts.Interpreter do ] } ]}, - opts: [at: ~U[2020-10-21 08:56:43Z]], - type: :datetime - }, - %Trigger{actions: { + :oracle => { :=, [line: 22], [ {:scope, [line: 22], nil}, {:update_in, [line: 22], [{:scope, [line: 22], nil}, ["next_transaction"], {:&, [line: 22], [{{:., [line: 22], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.TransactionStatements], [:TransactionStatements]}, :set_content]}, [line: 22], [{:&, [line: 22], [1]}, "uco price changed"]}]}]} ] - }, opts: [], type: :oracle} - ] + } + } } } - # Returns an error when there are invalid trigger options + Returns an error when there are invalid trigger options - # iex> Interpreter.parse(" - # ...> actions triggered_by: datetime, at: 0000000 do - # ...> end - # ...> ") - # {:error, "invalid trigger - invalid datetime - arguments at:0 - L1"} + iex> Interpreter.parse(" + ...> actions triggered_by: datetime, at: 0000000 do + ...> end + ...> ") + {:error, "invalid datetime's trigger"} - # Returns an error when a invalid term is provided + Returns an error when a invalid term is provided - # iex> Interpreter.parse(" - # ...> actions do - # ...> System.user_home - # ...> end - # ...> ") - # {:error, "unexpected token - System - L2"} + iex> Interpreter.parse(" + ...> actions triggered_by: transaction do + ...> System.user_home + ...> end + ...> ") + {:error, "unexpected term - System - L2"} """ @spec parse(code :: binary()) :: {:ok, Contract.t()} | {:error, reason :: binary()} def parse(code) when is_binary(code) do - with {:ok, ast} <- - Code.string_to_quoted(String.trim(code), - static_atoms_encoder: &atom_encoder/2 - ), - {_, {:ok, %{contract: contract}}} <- - Macro.traverse( - ast, - {:ok, %{scope: :root, contract: %Contract{}}}, - &prewalk/2, - &postwalk/2 - ) do + with {:ok, ast} <- sanitize_code(code), + {:ok, contract} <- parse_contract(ast, %Contract{}) do {:ok, contract} else - {_node, {:error, reason}} -> - {:error, format_error_reason(reason)} + {:error, {meta, info, token}} -> + {:error, InterpreterUtils.format_error_reason({token, meta, []}, info)} + + {:error, {:unexpected_term, ast}} -> + {:error, InterpreterUtils.format_error_reason(ast, "unexpected term")} {:error, reason} -> - {:error, format_error_reason(reason)} + {:error, reason} end - catch - {{:error, reason}, {:., metadata, [{:__aliases__, _, atom: cause}, _]}} -> - {:error, format_error_reason({metadata, reason, cause})} - - {{:error, :unexpected_token}, {:atom, key}} -> - {:error, format_error_reason({[], "unexpected_token", key})} - - {{:error, :unexpected_token}, {{:atom, key}, metadata, _}} -> - {:error, format_error_reason({metadata, "unexpected_token", key})} - - {{:error, :unexpected_token}, {{:atom, key}, _}} -> - {:error, format_error_reason({[], "unexpected_token", key})} - - {:error, reason = {_metadata, _message, _cause}} -> - {:error, format_error_reason(reason)} end defp atom_encoder(atom, _) do @@ -253,1244 +189,52 @@ defmodule Archethic.Contracts.Interpreter do end end - defp format_error_reason({metadata, message, cause}) do - message = prepare_message(message) - - [prepare_message(message), cause, metadata_to_string(metadata)] - |> Enum.reject(&(&1 == "")) - |> Enum.join(" - ") - end - - defp prepare_message(message) when is_atom(message) do - message |> Atom.to_string() |> String.replace("_", " ") - end - - defp prepare_message(message) when is_binary(message) do - String.trim_trailing(message, ":") - end - - defp metadata_to_string(line: line, column: column), do: "L#{line}:C#{column}" - defp metadata_to_string(line: line), do: "L#{line}" - defp metadata_to_string(_), do: "" - - # Whitelist operators - defp prewalk(node = {:+, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:-, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:/, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:*, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:>, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:<, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:>=, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:<=, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:|>, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:==, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - # Allow variable assignation inside the actions - defp prewalk(node = {:=, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - # Allow variable as function argument - defp prewalk(node = {{:atom, _}, {{:atom, _varname}, _, _}}, acc = {:ok, %{scope: scope}}) - when scope != :root do - {node, acc} - end - - # Whitelist the use of doted statement - defp prewalk(node = {{:., _, [{_, _, _}, _]}, _, []}, acc = {:ok, %{scope: scope}}) - when scope != :root, - do: {node, acc} - - # # Whitelist the definition of globals in the root - # defp prewalk(node = {:@, _, [{key, _, [val]}]}, acc = {:ok, :root}) - # when is_atom(key) and not is_nil(val), - # do: {node, acc} - - # # Whitelist the use of globals - # defp prewalk(node = {:@, _, [{key, _, nil}]}, acc = {:ok, _}) when is_atom(key), - # do: {node, acc} - - # Whitelist conditional oeprators - defp prewalk(node = {:if, _, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = {:else, _}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = [do: _, else: _], acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = :else, acc = {:ok, %{scope: scope}}) when scope != :root, do: {node, acc} - - defp prewalk(node = {:and, _, _}, acc = {:ok, _}), do: {node, acc} - defp prewalk(node = {:or, _, _}, acc = {:ok, _}), do: {node, acc} - - # Whitelist the in operation - defp prewalk(node = {:in, _, [_, _]}, acc = {:ok, _}), do: {node, acc} - - # Whitelist maps - defp prewalk(node = {:%{}, _, fields}, acc = {:ok, _}) when is_list(fields), do: {node, acc} - - defp prewalk(node = {key, _val}, acc) when is_binary(key) do - {node, acc} - end - - # Whitelist the multiline - defp prewalk(node = {{:__block__, _, _}}, acc = {:ok, _}) do - {node, acc} + @spec sanitize_code(binary()) :: {:ok, Macro.t()} | {:error, any()} + def sanitize_code(code) when is_binary(code) do + code + |> String.trim() + |> Code.string_to_quoted(static_atoms_encoder: &atom_encoder/2) end - defp prewalk(node = {:__block__, _, _}, acc = {:ok, _}) do - {node, acc} + defp parse_contract({:__block__, _, ast}, contract) do + parse_ast_block(ast, contract) end - # Whitelist custom atom - defp prewalk(node = :atom, acc), do: {node, acc} - - # Whitelist the actions DSL - defp prewalk(node = {{:atom, "actions"}, _, _}, {:ok, acc = %{scope: :root}}) do - {node, {:ok, %{acc | scope: :actions}}} + defp parse_contract(ast, contract) do + parse_ast(ast, contract) end - # Whitelist the triggered_by DSL in the actions - defp prewalk( - node = [ - {{:atom, "triggered_by"}, {{:atom, trigger_type}, meta = [line: _], _}} | trigger_opts - ], - acc = {:ok, %{scope: :actions}} - ) - when trigger_type in ["datetime", "interval", "transaction", "oracle"] do - case valid_trigger_opts(trigger_type, trigger_opts) do - :ok -> - {node, acc} + defp parse_ast_block([ast | rest], contract) do + case parse_ast(ast, contract) do + {:ok, contract} -> + parse_ast_block(rest, contract) - {:error, reason} -> - params = Enum.map_join(trigger_opts, ", ", fn {{:atom, k}, v} -> "#{k}:#{v}" end) - - {node, {:error, {meta, "invalid trigger - #{reason}", "arguments #{params}"}}} + {:error, _} = e -> + e end end - defp prewalk( - node = {{:atom, "triggered_by"}, {{:atom, trigger_type}, _, _}}, - acc = {:ok, %{scope: :actions}} - ) - when trigger_type in ["datetime", "interval", "transaction", "oracle"], - do: {node, acc} - - defp prewalk(node = {:atom, trigger_type}, acc = {:ok, %{scope: :actions}}) - when trigger_type in ["datetime", "interval", "transaction", "oracle"], - do: {node, acc} - - # Whitelist triggers 'at' argument - defp prewalk(node = {{:atom, "at"}, _arg}, acc = {:ok, %{scope: :actions}}), do: {node, acc} - - # Whitelist the condition DSL - defp prewalk( - node = {{:atom, "condition"}, _, [{{:atom, condition_name}, [_]}]}, - {:ok, acc = %{scope: :root}} - ) - when condition_name in ["inherit", "transaction", "oracle"] do - {node, {:ok, %{acc | scope: :condition}}} - end - - defp prewalk(node = [{{:atom, condition}, [_ | _]}], acc = {:ok, %{scope: :condition}}) - when condition in ["inherit", "transaction", "oracle"] do - {node, acc} - end - - defp prewalk( - node = {{:atom, "condition"}, _, [[{{:atom, condition_name}, _}]]}, - {:ok, acc = %{scope: :root}} - ) - when condition_name in ["inherit", "transaction", "oracle"] do - {node, {:ok, %{acc | scope: :condition}}} - end - - defp prewalk(node = [{{:atom, _}, _} | _], acc = {:ok, %{scope: :condition}}) do - {node, acc} - end - - defp prewalk(node = {{:atom, condition_name}, _}, acc = {:ok, %{scope: :condition}}) - when condition_name in ["inherit", "transaction", "oracle"] do - {node, acc} - end - - defp prewalk(node = {:atom, condition_name}, acc = {:ok, %{scope: :condition}}) - when condition_name in ["inherit", "transaction", "oracle"], - do: {node, acc} - - defp prewalk(node = {:atom, field_name}, acc = {:ok, %{scope: :condition}}) - when field_name in @condition_fields, - do: {node, acc} - - # Whitelist the usage of map in the conditions - # Example: - # - # ``` - # condition inehrit: [ - # uco_transfers: [%{ to: "address", amount: 1000000 }] - # ] - # ``` - defp prewalk(node = {{:atom, _key}, _val}, acc = {:ok, %{scope: :condition}}) do - {node, acc} - end - - # Whitelist the usage transaction fields - defp prewalk( - node = {:., _, [{{:atom, transaction_ref}, _, nil}, {:atom, type}]}, - acc - ) - when transaction_ref in ["next", "previous", "transaction", "contract"] and - type in @transaction_fields do - {node, acc} - end - - defp prewalk( - node = - {{:atom, _}, {{:., _, [{{:atom, transaction_ref}, _, nil}, {:atom, type}]}, _, _}}, - acc - ) - when transaction_ref in ["next", "previous", "transaction", "contract"] and - type in @transaction_fields do - {node, acc} - end - - # Whitelist the use of list - defp prewalk(node = [{{:atom, _}, _, nil} | _], acc = {:ok, %{scope: scope}}) - when scope != :root do - {node, acc} - end - - # Whitelist access to map field - defp prewalk( - node = {{:., _, [Access, :get]}, _, _}, - acc = {:ok, %{scope: scope}} - ) - when scope != :root do - {node, acc} - end - - defp prewalk(node = {:., _, [Access, :get]}, acc = {:ok, %{scope: scope}}) when scope != :root, - do: {node, acc} - - defp prewalk(node = Access, acc), do: {node, acc} - defp prewalk(node = :get, acc), do: {node, acc} + defp parse_ast_block([], contract), do: {:ok, contract} - # Whitelist condition fields + defp parse_ast(ast = {{:atom, "condition"}, _, _}, contract) do + case ConditionInterpreter.parse(ast) do + {:ok, condition_type, condition} -> + {:ok, Contract.add_condition(contract, condition_type, condition)} - # Whitelist the origin family condition - defp prewalk( - node = {{:atom, "origin_family"}, {{:atom, family}, metadata, nil}}, - acc = {:ok, %{scope: :condition}} - ) do - if family in @origin_families do - {node, acc} - else - {node, {:error, metadata, "unexpected token"}, "invalid origin family"} + {:error, _} = e -> + e end end - defp prewalk(node = {:atom, origin_family}, acc = {:ok, %{scope: :condition}}) - when origin_family in @origin_families, - do: {node, acc} + defp parse_ast(ast = {{:atom, "actions"}, _, _}, contract) do + case ActionInterpreter.parse(ast) do + {:ok, trigger_type, actions} -> + {:ok, Contract.add_trigger(contract, trigger_type, actions)} - # Whitelist the transaction type condition - defp prewalk( - node = {{:atom, "type"}, {{:atom, type}, metadata, nil}}, - acc = {:ok, %{scope: :condition}} - ) do - if type in @transaction_types do - {node, acc} - else - {node, {:error, metadata, "unexpected token"}, "invalid transaction type"} + {:error, _} = e -> + e end end - defp prewalk( - node = {{:atom, field}, _}, - acc = {:ok, %{scope: :condition}} - ) - when field in @condition_fields do - {node, acc} - end - - defp prewalk( - node = {{:atom, field}, [line: _], _}, - acc = {:ok, %{scope: {:function, _}}} - ) - when field in @transaction_fields do - {node, acc} - end - - # Whitelist the size/1 function - defp prewalk( - node = {{:atom, "size"}, _, [_data]}, - acc = {:ok, %{scope: scope}} - ) - when scope != :root do - {node, acc} - end - - # Whitelist the hash/1 function - defp prewalk(node = {{:atom, "hash"}, _, [_data]}, acc = {:ok, %{scope: scope}}) - when scope != :root, - do: {node, acc} - - # Whitelist the regex_match?/2 function - defp prewalk( - node = {{:atom, "regex_match?"}, _, [_input, _search]}, - acc = {:ok, %{scope: scope}} - ) - when scope != :root, - do: {node, acc} - - # Whitelist the regex_extract/2 function - defp prewalk( - node = {{:atom, "regex_extract"}, _, [_input, _search]}, - acc = {:ok, %{scope: scope}} - ) - when scope != :root, - do: {node, acc} - - # Whitelist the json_path_extract/2 function - defp prewalk( - node = {{:atom, "json_path_extract"}, _, [_input, _search]}, - acc = {:ok, %{scope: scope}} - ) - when scope != :root, - do: {node, acc} - - # Whitelist the json_path_match?/2 function - defp prewalk( - node = {{:atom, "json_path_match?"}, _, [_input, _search]}, - acc = {:ok, %{scope: scope}} - ) - 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 timestamp/0 function - defp prewalk(node = {{:atom, "timestamp"}, _, []}, acc = {:ok, %{scope: scope}}) - when scope != :root, - do: {node, acc} - - defp prewalk( - node = {{:atom, "get_genesis_public_key"}, _, [_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]}, - acc = {:ok, %{scope: :condition}} - ) do - {node, acc} - end - - # Whitelist the json_path_extract/1 function in the condition - defp prewalk( - node = {{:atom, "json_path_extract"}, _, [_search]}, - acc = {:ok, %{scope: :condition}} - ) do - {node, acc} - end - - # Whitelist the json_path_match?/1 function in the condition - defp prewalk( - node = {{:atom, "json_path_match?"}, _, [_search]}, - acc = {:ok, %{scope: :condition}} - ) do - {node, acc} - end - - # Whitelist the hash/0 function in the condition - defp prewalk( - node = {{:atom, "hash"}, _, []}, - acc = {:ok, %{scope: :condition}} - ) do - {node, acc} - end - - # Whitelist the in?/1 function in the condition - defp prewalk( - node = {{:atom, "in?"}, _, [_data]}, - acc = {:ok, %{scope: :condition}} - ) do - {node, acc} - end - - # Whitelist the size/0 function in the condition - 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 get_genesis_public_key/0 function in condition - defp prewalk( - node = {{:atom, "get_genesis_public_key"}, _, []}, - acc = {:ok, %{scope: :condition}} - ) do - {node, acc} - end - - # Whitelist the timestamp/0 function in condition - defp prewalk(node = {{:atom, "timestamp"}, _, []}, acc = {:ok, %{scope: :condition}}), - do: {node, acc} - - # 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, - do: {node, {:ok, %{acc | scope: {:function, fun_name, :actions}}}} - - defp prewalk( - node = [{{:atom, _variable_name}, _, nil}], - acc = {:ok, %{scope: {:function, "set_content", :actions}}} - ) do - {node, acc} - end - - # Whitelist the add_uco_transfer argument list - defp prewalk( - node = [{{:atom, "to"}, _to}, {{:atom, "amount"}, _amount}], - acc = {:ok, %{scope: {:function, "add_uco_transfer", :actions}}} - ) do - {node, acc} - end - - defp prewalk( - node = {{:atom, arg}, _}, - acc = {:ok, %{scope: {:function, "add_uco_transfer", :actions}}} - ) - when arg in ["to", "amount"], - do: {node, acc} - - # Whitelist the add_token_tranfser argument list - defp prewalk( - node = [ - {{:atom, "to"}, _to}, - {{:atom, "amount"}, _amount}, - {{:atom, "token_address"}, _token_address}, - {{:atom, "token_id"}, _token_id} - ], - acc = {:ok, %{scope: {:function, "add_token_transfer", :actions}}} - ) do - {node, acc} - end - - defp prewalk( - node = {{:atom, arg}, _}, - acc = {:ok, %{scope: {:function, "add_token_transfer", :actions}}} - ) - when arg in ["to", "amount", "token_address", "token_id"], - do: {node, acc} - - # Whitelist the add_ownership argument list - defp prewalk( - node = [ - {{:atom, "secret"}, _secret}, - {{:atom, "secret_key"}, _secret_key}, - {{:atom, "authorized_public_keys"}, _authorized_public_keys} - ], - acc = {:ok, %{scope: {:function, "add_ownership", :actions}}} - ) do - {node, acc} - end - - defp prewalk( - node = {{:atom, arg}, _}, - acc = {:ok, %{scope: {:function, "add_ownership", :actions}}} - ) - when arg in ["secret", "secret_key", "authorized_public_keys"], - do: {node, acc} - - # Whitelist generics - defp prewalk(true, acc = {:ok, _}), do: {true, acc} - defp prewalk(false, acc = {:ok, _}), do: {false, acc} - defp prewalk(number, acc = {:ok, _}) when is_number(number), do: {number, acc} - defp prewalk(string, acc = {:ok, _}) when is_binary(string), do: {string, acc} - defp prewalk(node = [do: _], acc = {:ok, _}), do: {node, acc} - defp prewalk(node = {:do, _}, acc = {:ok, _}), do: {node, acc} - defp prewalk(node = :do, acc = {:ok, _}), do: {node, acc} - # Literals - defp prewalk(node = {{:atom, key}, _, nil}, acc = {:ok, _}) when is_binary(key), - do: {node, acc} - - defp prewalk(node = {:atom, key}, acc = {:ok, _}) when is_binary(key), do: {node, acc} - - # Whitelist interpolation of strings - defp prewalk( - node = - {:<<>>, _, [{:"::", _, [{{:., _, [Kernel, :to_string]}, _, _}, {:binary, _, nil}]}, _]}, - acc - ) do - {node, acc} - end - - defp prewalk( - node = - {:<<>>, _, - [ - _, - {:"::", _, [{{:., _, [Kernel, :to_string]}, _, _}, _]} - ]}, - acc - ) do - {node, acc} - end - - defp prewalk(node = {:"::", _, [{{:., _, [Kernel, :to_string]}, _, _}, _]}, acc) do - {node, acc} - end - - defp prewalk(node = {{:., _, [Kernel, :to_string]}, _, _}, acc) do - {node, acc} - end - - defp prewalk(node = {:., _, [Kernel, :to_string]}, acc) do - {node, acc} - end - - defp prewalk(node = Kernel, acc), do: {node, acc} - defp prewalk(node = :to_string, acc), do: {node, acc} - defp prewalk(node = {:binary, _, nil}, acc), do: {node, acc} - - defp prewalk(node, acc) when is_list(node) do - {node, acc} - end - - # Blacklist anything else - defp prewalk(node, {:ok, _acc}) do - throw({{:error, :unexpected_token}, node}) - end - - defp prewalk(node, e = {:error, _}), do: {node, e} - - # Reset the scope after actions triggered block ending - defp postwalk( - node = - {{:atom, "actions"}, [line: _], - [[{{:atom, "triggered_by"}, {{:atom, trigger_type}, _, _}} | opts], [do: actions]]}, - {:ok, acc} - ) do - actions = - inject_bindings_and_functions(actions, - bindings: %{ - "contract" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}), - "transaction" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}) - } - ) - - acc = - case trigger_type do - "datetime" -> - [{{:atom, "at"}, timestamp}] = opts - datetime = DateTime.from_unix!(timestamp) - - Map.update!( - acc, - :contract, - &Contract.add_trigger(&1, :datetime, [at: datetime], actions) - ) - - "interval" -> - [{{:atom, "at"}, interval}] = opts - - Map.update!( - acc, - :contract, - &Contract.add_trigger( - &1, - :interval, - [at: interval], - actions - ) - ) - - "transaction" -> - Map.update!(acc, :contract, &Contract.add_trigger(&1, :transaction, [], actions)) - - "oracle" -> - Map.update!(acc, :contract, &Contract.add_trigger(&1, :oracle, [], actions)) - end - - {node, {:ok, %{acc | scope: :root}}} - end - - # Add conditions with brackets - defp postwalk( - node = {{:atom, "condition"}, _, [[{{:atom, condition_name}, conditions}]]}, - {:ok, acc} - ) do - bindings = Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}) - - bindings = - case condition_name do - "inherit" -> - Map.merge(bindings, %{ - "next" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}), - "previous" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}) - }) - - _ -> - Map.merge(bindings, %{ - "contract" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}), - "transaction" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}) - }) - end - - subject_scope = if condition_name == "inherit", do: "next", else: "transaction" - - conditions = - inject_bindings_and_functions(conditions, - bindings: bindings, - subject: subject_scope - ) - - new_acc = - acc - |> Map.update!( - :contract, - &Contract.add_condition( - &1, - String.to_existing_atom(condition_name), - aggregate_conditions(conditions, subject_scope) - ) - ) - |> Map.put(:scope, :root) - - {node, {:ok, new_acc}} - end - - # Add complex conditions with if statements - defp postwalk( - node = - {{:atom, "condition"}, _, - [ - {{:atom, condition_name}, _, - [ - conditions - ]} - ]}, - {:ok, acc} - ) do - subject_scope = if condition_name == "inherit", do: "next", else: "transaction" - - bindings = Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}) - - bindings = - case condition_name do - "inherit" -> - Map.merge(bindings, %{ - "next" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}), - "previous" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}) - }) - - _ -> - Map.merge(bindings, %{ - "contract" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}), - "transaction" => Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}) - }) - end - - conditions = - inject_bindings_and_functions(conditions, - bindings: bindings, - subject: subject_scope - ) - - new_acc = - acc - |> Map.update!( - :contract, - &Contract.add_condition( - &1, - String.to_existing_atom(condition_name), - aggregate_conditions(conditions, subject_scope) - ) - ) - |> Map.put(:scope, :root) - - {node, {:ok, new_acc}} - end - - # Return to the parent scope after parsing the function call - defp postwalk(node = {{:atom, _}, _, _}, {:ok, acc = %{scope: {:function, _, scope}}}) do - {node, {:ok, %{acc | scope: scope}}} - end - - # Convert Access key string to binary - defp postwalk( - {{:., meta1, [Access, :get]}, meta2, - [{{:., meta3, [{subject, meta4, nil}, {:atom, field}]}, meta5, []}, {:atom, key}]}, - acc = {:ok, _} - ) do - { - {{:., meta1, [Access, :get]}, meta2, - [ - {{:., meta3, [{subject, meta4, nil}, String.to_existing_atom(field)]}, meta5, []}, - Base.decode16!(key, case: :mixed) - ]}, - acc - } - end - - # Convert map key to binary - defp postwalk({:%{}, meta, params}, acc = {:ok, _}) do - encoded_params = - Enum.map(params, fn - {{:atom, key}, value} when is_binary(key) -> - case Base.decode16(key, case: :mixed) do - {:ok, bin} -> - {bin, value} - - :error -> - {key, value} - end - - {key, value} -> - {key, value} - end) - - {{:%{}, meta, encoded_params}, acc} - end - - defp postwalk(node, acc), do: {node, acc} - - defp valid_trigger_opts("datetime", [{{:atom, "at"}, timestamp}]) do - if length(Integer.digits(timestamp)) != 10 do - {:error, "invalid datetime"} - else - case DateTime.from_unix(timestamp) do - {:ok, _} -> - :ok - - _ -> - {:error, "invalid datetime"} - end - end - end - - defp valid_trigger_opts("interval", [{{:atom, "at"}, interval}]) do - case CronParser.parse(interval) do - {:ok, _} -> - :ok - - {:error, _} -> - {:error, "invalid interval"} - end - end - - defp valid_trigger_opts("transaction", []), do: :ok - defp valid_trigger_opts("oracle", []), do: :ok - - defp valid_trigger_opts(_, _), do: {:error, "unexpected token"} - - defp aggregate_conditions(conditions, subject_scope) do - Enum.reduce(conditions, %Conditions{}, fn {subject, condition}, acc -> - condition = do_aggregate_condition(condition, subject_scope, subject) - Map.put(acc, String.to_existing_atom(subject), condition) - end) - end - - defp do_aggregate_condition(condition, _, "origin_family"), - do: String.to_existing_atom(condition) - - defp do_aggregate_condition(condition, subject_scope, subject) - when is_binary(condition) or is_number(condition) do - {:==, [], - [ - {:get_in, [], [{:scope, [], nil}, [subject_scope, subject]]}, - condition - ]} - end - - defp do_aggregate_condition(condition, subject_scope, subject) when is_list(condition) do - {:==, [], - [ - {:get_in, [], - [ - {:scope, [], nil}, - [subject_scope, subject] - ]}, - condition - ]} - end - - defp do_aggregate_condition(condition, subject_scope, subject) do - Macro.postwalk(condition, &to_boolean_expression(&1, subject_scope, subject)) - end - - defp to_boolean_expression( - {{:., metadata, [{:__aliases__, _, [:Library]}, fun]}, _, args}, - subject_scope, - subject - ) do - arguments = - if :erlang.function_exported(Library, fun, length(args)) do - # If the number of arguments fullfill the function's arity (without subject) - args - else - [ - {:get_in, metadata, [{:scope, metadata, nil}, [subject_scope, subject]]} | args - ] - end - - if fun |> Atom.to_string() |> String.ends_with?("?") do - {:==, metadata, - [ - true, - {{:., metadata, [{:__aliases__, [alias: Library], [:Library]}, fun]}, metadata, - arguments} - ]} - else - {:==, metadata, - [ - {:get_in, metadata, [{:scope, metadata, nil}, [subject_scope, subject]]}, - {{:., metadata, [{:__aliases__, [alias: Library], [:Library]}, fun]}, metadata, - arguments} - ]} - end - end - - defp to_boolean_expression(condition = {:%{}, _, _}, subject_scope, subject) do - {:==, [], - [ - {:get_in, [], [{:scope, [], nil}, [subject_scope, subject]]}, - condition - ]} - end - - # Flatten comparison operations - defp to_boolean_expression({op, _, [{:==, metadata, [{:get_in, _, _}, comp_a]}, comp_b]}, _, _) - when op in [:==, :>=, :<=] do - {op, metadata, [comp_a, comp_b]} - end - - defp to_boolean_expression(condition, _, _), do: condition - - @doc """ - - ## Examples - - iex> Interpreter.execute_actions(%Contract{ - ...> triggers: [ - ...> %Contract.Trigger{ - ...> type: :transaction, - ...> actions: {:=, [line: 2], - ...> [ - ...> {:scope, [line: 2], nil}, - ...> {:update_in, [line: 2], - ...> [ - ...> {:scope, [line: 2], nil}, - ...> ["next_transaction"], - ...> {:&, [line: 2], - ...> [ - ...> {{:., [line: 2], - ...> [ - ...> {:__aliases__, - ...> [alias: Archethic.Contracts.Interpreter.TransactionStatements], - ...> [:TransactionStatements]}, - ...> :set_type - ...> ]}, [line: 2], [{:&, [line: 2], [1]}, "transfer"]} - ...> ]} - ...> ]} - ...> ]} - ...> } - ...> ] - ...> }, :transaction) - %Transaction{type: :transfer, data: %TransactionData{}} - - iex> Interpreter.execute_actions(%Contract{ - ...> triggers: [ - ...> %Contract.Trigger{ - ...> type: :transaction, - ...> actions: {:__block__, [], [ - ...> { - ...> :=, - ...> [{:line, 2}], - ...> [ - ...> {:scope, [{:line, 2}], nil}, - ...> {:update_in, [line: 2], [ - ...> {:scope, [line: 2], nil}, - ...> ["next_transaction"], - ...> {:&, [line: 2], [ - ...> {{:., [line: 2], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.TransactionStatements], [:TransactionStatements]}, :set_type]}, - ...> [line: 2], - ...> [{:&, [line: 2], [1]}, "transfer"]}] - ...> } - ...> ]} - ...> ] - ...> }, - ...> { - ...> :=, - ...> [line: 3], - ...> [ - ...> {:scope, [line: 3], nil}, - ...> {:update_in, [line: 3], [ - ...> {:scope, [line: 3], nil}, - ...> ["next_transaction"], - ...> {:&, [line: 3], [ - ...> { - ...> {:., [line: 3], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.TransactionStatements], [:TransactionStatements]}, :add_uco_transfer]}, - ...> [line: 3], [ - ...> {:&, [line: 3], [1]}, - ...> [ - ...> {"to", "005220865F2237E3B62FFAA2AB72260A9FA711FBADF7F1DA391AB02B93D9E0D4A3"}, - ...> {"amount", 10.04} - ...> ] - ...> ] - ...> } - ...> ]} - ...> ]}] - ...> } - ...> ]}, - ...> }]}, :transaction) - %Transaction{ - type: :transfer, - data: %TransactionData{ - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: [ - %UCOTransfer{ - to: <<0, 82, 32, 134, 95, 34, 55, 227, 182, 47, 250, 162, 171, 114, 38, 10, 159, 167, 17, 251, 173, 247, 241, 218, 57, 26, 176, 43, 147, 217, 224, 212, 163>>, - amount: 10.04 - } - ] - } - } - } - } - """ - def execute_actions(%Contract{triggers: triggers}, trigger_type, constants \\ %{}) do - %Contract.Trigger{actions: quoted_code} = Enum.find(triggers, &(&1.type == trigger_type)) - - {%{"next_transaction" => next_transaction}, _} = - Code.eval_quoted(quoted_code, - scope: - Map.put(constants, "next_transaction", %Transaction{ - data: %TransactionData{} - }) - ) - - next_transaction - end - - @doc """ - Execute abritary code using some constants as bindings - - ## Examples - - iex> Interpreter.execute({{:., [line: 1], - ...> [ - ...> {:__aliases__, [alias: Archethic.Contracts.Interpreter.Library], - ...> [:Library]}, - ...> :regex_match? - ...> ]}, [line: 1], - ...> [{:get_in, [line: 1], [{:scope, [line: 1], nil}, ["content"]]}, "abc"]}, %{ "content" => "abc"}) - true - - iex> Interpreter.execute({:==, [], - ...> [ - ...> {:get_in, [line: 1], [{:scope, [line: 1], nil}, ["next_transaction", "content"]]}, - ...> {:get_in, [line: 1], [{:scope, [line: 1], nil}, ["previous_transaction", "content"]]}, - ...> ]}, %{ "previous_transaction" => %{"content" => "abc"}, "next_transaction" => %{ "content" => "abc" } }) - true - - iex> Interpreter.execute({{:., [line: 2], - ...> [ - ...> {:__aliases__, [alias: Archethic.Contracts.Interpreter.Library], - ...> [:Library]}, - ...> :hash - ...> ]}, [line: 2], - ...> [{:get_in, [line: 2], [{:scope, [line: 2], nil}, ["content"]]}]}, %{ "content" => "abc" }) - <<186, 120, 22, 191, 143, 1, 207, 234, 65, 65, 64, 222, 93, 174, 34, 35, 176, 3, 97, 163, 150, 23, 122, 156, 180, 16, 255, 97, 242, 0, 21, 173>> - """ - def execute(quoted_code, constants = %{}) do - {res, _} = Code.eval_quoted(quoted_code, scope: constants) - res - end - - defp inject_bindings_and_functions(quoted_code, opts) when is_list(opts) do - bindings = Keyword.get(opts, :bindings, %{}) - subject = Keyword.get(opts, :subject) - - {ast, _} = - Macro.postwalk( - quoted_code, - %{ - bindings: bindings, - library_functions: @library_functions_names, - transaction_statements_functions: @transaction_statements_functions_names, - subject: subject - }, - &do_postwalk_execution/2 - ) - - ast - end - - defp do_postwalk_execution({:=, metadata, [var_name, content]}, acc) do - put_ast = - {{:., metadata, [{:__aliases__, metadata, [:Map]}, :put]}, metadata, - [{:scope, metadata, nil}, var_name, parse_value(content)]} - - { - {:=, metadata, [{:scope, metadata, nil}, put_ast]}, - put_in(acc, [:bindings, var_name], parse_value(content)) - } - end - - defp do_postwalk_execution(_node = {{:atom, atom}, metadata, args}, acc) - when atom in @library_functions_names do - {{{:., metadata, [{:__aliases__, [alias: Library], [:Library]}, String.to_atom(atom)]}, - metadata, args}, acc} - end - - defp do_postwalk_execution(_node = {{:atom, atom}, metadata, args}, acc) - when atom in @transaction_statements_functions_names do - args = - Enum.map(args, fn arg -> - {ast, _} = Macro.postwalk(arg, acc, &do_postwalk_execution/2) - ast - end) - - ast = { - {:., metadata, - [ - {:__aliases__, [alias: TransactionStatements], [:TransactionStatements]}, - String.to_atom(atom) - ]}, - metadata, - [{:&, metadata, [1]} | args] - } - - update_ast = - {:update_in, metadata, - [ - {:scope, metadata, nil}, - ["next_transaction"], - {:&, metadata, - [ - ast - ]} - ]} - - { - {:=, metadata, [{:scope, metadata, nil}, update_ast]}, - acc - } - end - - defp do_postwalk_execution( - _node = {{:atom, atom}, metadata, _args}, - acc = %{bindings: bindings, subject: subject} - ) do - if Map.has_key?(bindings, atom) do - search = - case subject do - nil -> - [atom] - - subject -> - # Do not use the subject when using reserved keyword - if atom in ["contract", "transaction", "next", "previous"] do - [atom] - else - [subject, atom] - end - end - - { - {:get_in, metadata, [{:scope, metadata, nil}, search]}, - acc - } - else - {atom, acc} - end - end - - defp do_postwalk_execution({:., metadata, [parent, {{:atom, field}}]}, acc) do - { - {:get_in, metadata, [{:scope, metadata, nil}, [parent, parse_value(field)]]}, - acc - } - end - - defp do_postwalk_execution( - {:., _, [{:get_in, metadata, [{:scope, _, nil}, access]}, {:atom, field}]}, - acc - ) do - { - {:get_in, metadata, [{:scope, metadata, nil}, access ++ [parse_value(field)]]}, - acc - } - end - - defp do_postwalk_execution( - {:., metadata, - [{:get_in, metadata, [{:scope, metadata, nil}, [parent]]}, {:atom, child}]}, - acc - ) do - {{:get_in, metadata, [{:scope, metadata, nil}, [parent, parse_value(child)]]}, acc} - end - - defp do_postwalk_execution({{:atom, atom}, val}, acc) do - {ast, _} = Macro.postwalk(val, acc, &do_postwalk_execution/2) - {{atom, ast}, acc} - end - - defp do_postwalk_execution({{:get_in, metadata, [{:scope, metadata, nil}, access]}, _, []}, acc) do - { - {:get_in, metadata, [{:scope, metadata, nil}, access]}, - acc - } - end - - defp do_postwalk_execution({:==, _, [{left, _, args_l}, {right, _, args_r}]}, acc) do - {{:==, [], [{left, [], args_l}, {right, [], args_r}]}, acc} - end - - defp do_postwalk_execution({:==, _, [{left, _, args}, right]}, acc) do - {{:==, [], [{left, [], args}, right]}, acc} - end - - defp do_postwalk_execution({node, _, _}, acc) when is_binary(node) do - {parse_value(node), acc} - end - - defp do_postwalk_execution(node, acc), do: {parse_value(node), acc} - - defp parse_value(val) when is_binary(val) do - case Base.decode16(val, case: :mixed) do - {:ok, bin} -> - bin - - _ -> - val - end - end - - defp parse_value(val), do: val - - @spec valid_conditions?(Conditions.t(), map()) :: boolean() - def valid_conditions?(conditions = %Conditions{}, constants = %{}) do - result = - conditions - |> Map.from_struct() - |> Enum.all?(fn {field, condition} -> - case validate_condition({field, condition}, constants) do - {_, true} -> - true - - {_, false} -> - Logger.debug( - "Invalid condition for `#{field}` with the given value: `#{get_in(constants, ["next", field])}` - expected: #{inspect(condition)}" - ) - - false - end - end) - - if result do - result - else - result - end - end - - defp validate_condition({:origin_family, _}, _) do - # Skip the verification - # The Proof of Work algorithm will use this condition to verify the transaction - {:origin_family, true} - end - - defp validate_condition({:previous_public_key, nil}, _) do - # Skip the verification as previous public key change for each transaction - {:previous_public_key, true} - end - - defp validate_condition({:timestamp, nil}, _) do - # Skip the verification as timestamp change for each transaction - {:timestamp, true} - end - - defp validate_condition({:type, nil}, %{"next" => %{"type" => "transfer"}}) do - # Skip the verification when it's the default type - {:type, true} - end - - defp validate_condition({:content, nil}, %{"next" => %{"content" => ""}}) do - # Skip the verification when it's the default type - {:content, true} - end - - # Validation rules for inherit constraints - defp validate_condition({field, nil}, %{"previous" => prev, "next" => next}) do - {field, Map.get(prev, Atom.to_string(field)) == Map.get(next, Atom.to_string(field))} - end - - defp validate_condition({field, condition}, constants = %{"next" => next}) do - result = execute(condition, constants) - - if is_boolean(result) do - {field, result} - else - {field, Map.get(next, Atom.to_string(field)) == result} - end - end - - # Validation rules for incoming transaction - defp validate_condition({field, nil}, %{"transaction" => _}) do - # Skip the validation if no transaction conditions are provided - {field, true} - end - - defp validate_condition( - {field, condition}, - constants = %{"transaction" => transaction} - ) do - result = execute(condition, constants) - - if is_boolean(result) do - {field, result} - else - {field, Map.get(transaction, Atom.to_string(field)) == result} - end - end + defp parse_ast(ast, _), do: {:error, {:unexpected_term, ast}} end diff --git a/lib/archethic/contracts/interpreter/action.ex b/lib/archethic/contracts/interpreter/action.ex index 6bde897138..2fa6248ed5 100644 --- a/lib/archethic/contracts/interpreter/action.ex +++ b/lib/archethic/contracts/interpreter/action.ex @@ -1,22 +1,23 @@ defmodule Archethic.Contracts.ActionInterpreter do @moduledoc false - alias Archethic.Contracts.Interpreter.Library alias Archethic.Contracts.Interpreter.TransactionStatements alias Archethic.Contracts.Interpreter.Utils, as: InterpreterUtils - @transaction_fields InterpreterUtils.transaction_fields() + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + + alias Crontab.CronExpression.Parser, as: CronParser - @library_functions_names Library.__info__(:functions) - |> Enum.map(&Atom.to_string(elem(&1, 0))) + @transaction_fields InterpreterUtils.transaction_fields() @transaction_statements_functions_names TransactionStatements.__info__(:functions) |> Enum.map(&Atom.to_string(elem(&1, 0))) - @type trigger :: :transaction | :interval | :datetime | :oracle + @type trigger :: :transaction | {:interval, String.t()} | {:datetime, DateTime.t()} | :oracle @doc ~S""" - Parse an action block + Parse an action block and return the trigger's type associated with the code to execute ## Examples @@ -37,8 +38,8 @@ defmodule Archethic.Contracts.ActionInterpreter do ...> ]}) {:ok, :transaction, {:=, [line: 2], [{:scope, [line: 2], nil}, {:update_in, [line: 2], [{:scope, [line: 2], nil}, ["next_transaction"], {:&, [line: 2], [{{:., [line: 2], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.TransactionStatements], [:TransactionStatements]}, :add_uco_transfer]}, [line: 2], [{:&, [line: 2], [1]}, [{"to", "0000D574D171A484F8DEAC2D61FC3F7CC984BEB52465D69B3B5F670090742CBF5CC"}, {"amount", 2000000000}]]}]}]}]}} - # Usage with trigger accepting parameters - + Usage with trigger accepting parameters + iex> ActionInterpreter.parse({{:atom, "actions"}, [line: 1], ...> [ ...> [ @@ -52,27 +53,43 @@ defmodule Archethic.Contracts.ActionInterpreter do ...> ] ...> ]}) {:ok, {:datetime, ~U[2014-02-02 02:43:50Z]}, {:=, [line: 2], [{:scope, [line: 2], nil}, {:update_in, [line: 2], [{:scope, [line: 2], nil}, ["next_transaction"], {:&, [line: 2], [{{:., [line: 2], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.TransactionStatements], [:TransactionStatements]}, :add_recipient]}, [line: 2], [{:&, [line: 2], [1]}, "0000D574D171A484F8DEAC2D61FC3F7CC984BEB52465D69B3B5F670090742CBF5CC"]}]}]}]}} + + + Prevent usage of not authorized functions + + iex> ActionInterpreter.parse({{:atom, "actions"}, [line: 1], + ...> [ + ...> [{{:atom, "triggered_by"}, {{:atom, "transaction"}, [line: 1], nil}}], + ...> [ + ...> do: {{:., [line: 2], + ...> [{:__aliases__, [line: 2], [atom: "System"]}, {:atom, "user_home"}]}, + ...> [line: 2], []} + ...> ] + ...> ]} + ...> ) + {:error, "unexpected term - System - L2"} + """ @spec parse(Macro.t()) :: {:ok, trigger(), Macro.t()} def parse(ast) do - try do - {_node, {:ok, trigger, actions}} = - Macro.traverse( - ast, - {:ok, %{scope: :root}}, - &prewalk(&1, &2), - &postwalk/2 - ) - - {:ok, trigger, actions} - catch - {:error, reason, {{:atom, key}, metadata, _}} -> - {:error, InterpreterUtils.format_error_reason({metadata, reason, key})} - - {:error, node = {{:atom, key}, metadata, _}} -> - IO.inspect(node) - {:error, InterpreterUtils.format_error_reason({metadata, "unexpected term", key})} + case Macro.traverse( + ast, + {:ok, %{scope: :root}}, + &prewalk(&1, &2), + &postwalk/2 + ) do + {_node, {:ok, trigger, actions}} -> + {:ok, trigger, actions} + + {node, _} -> + {:error, InterpreterUtils.format_error_reason(node, "unexpected term")} end + catch + {:error, reason, node} -> + {:error, InterpreterUtils.format_error_reason(node, reason)} + + {:error, node} -> + {:error, InterpreterUtils.format_error_reason(node, "unexpected term")} end # Whitelist the actions DSL @@ -95,10 +112,23 @@ defmodule Archethic.Contracts.ActionInterpreter do {node, acc} else _ -> - {node, {:error, "invalid datetime"}} + {node, {:error, "invalid datetime's trigger"}} end end + defp prewalk(node = {{:atom, "at"}, interval}, acc = {:ok, %{scope: {:actions, :interval}}}) do + case CronParser.parse(interval) do + {:ok, _} -> + {node, acc} + + {:error, _} -> + {node, {:error, "invalid interval"}} + end + end + + # Whitelist variable assignation inside the actions + defp prewalk(node = {:=, _, _}, acc = {:ok, %{scope: {:actions, _}}}), do: {node, acc} + # Whitelist the transaction statements functions defp prewalk( node = {{:atom, function}, _, _}, @@ -225,7 +255,7 @@ defmodule Archethic.Contracts.ActionInterpreter do end defp prewalk( - node = {{:atom, "secret_key"}, {{:atom, _}, _, _}}, + node = {{:atom, "secret_key"}, _secret_key}, acc = {:ok, %{scope: {:function, "add_ownership", {:actions, _}}}} ) do {node, acc} @@ -289,4 +319,20 @@ defmodule Archethic.Contracts.ActionInterpreter do defp postwalk(node, acc) do InterpreterUtils.postwalk(node, acc) end + + @doc """ + Execute actions code and returns a transaction as result + """ + @spec execute(Macro.t(), map()) :: Transaction.t() + def execute(code, constants \\ %{}) do + {%{"next_transaction" => next_transaction}, _} = + Code.eval_quoted(code, + scope: + Map.put(constants, "next_transaction", %Transaction{ + data: %TransactionData{} + }) + ) + + next_transaction + end end diff --git a/lib/archethic/contracts/interpreter/condition.ex b/lib/archethic/contracts/interpreter/condition.ex index 3396d46bf7..e9a0130acf 100644 --- a/lib/archethic/contracts/interpreter/condition.ex +++ b/lib/archethic/contracts/interpreter/condition.ex @@ -1,9 +1,10 @@ defmodule Archethic.Contracts.ConditionInterpreter do @moduledoc false - alias Archethic.Contracts.Contract.Conditions + alias Archethic.Contracts.ContractConditions, as: Conditions alias Archethic.Contracts.Interpreter.Library alias Archethic.Contracts.Interpreter.Utils, as: InterpreterUtils + alias Archethic.SharedSecrets @condition_fields Conditions.__struct__() |> Map.keys() @@ -12,23 +13,26 @@ defmodule Archethic.Contracts.ConditionInterpreter do @transaction_fields InterpreterUtils.transaction_fields() - @library_functions_names Library.__info__(:functions) - |> Enum.map(&Atom.to_string(elem(&1, 0))) + @exported_library_functions Library.__info__(:functions) + + @type condition_type :: :transaction | :inherit | :oracle + + require Logger + @doc ~S""" - Parse a condition block + Parse a condition block and returns the right condition's type with a `Archethic.Contracts.Contract.Conditions` struct ## Examples - iex> ConditionInterpreter.parse({{:atom, "condition"}, [line: 1], - ...> [ - ...> [ - ...> {{:atom, "transaction"}, [ - ...> {{:atom, "content"}, "hello"} - ...> ]} - ...> ] - ...> ]}) - {:ok, %{ - transaction: %Conditions{ + iex> ConditionInterpreter.parse({{:atom, "condition"}, [line: 1], + ...> [ + ...> [ + ...> {{:atom, "transaction"}, [ + ...> {{:atom, "content"}, "hello"} + ...> ]} + ...> ] + ...> ]}) + {:ok, :transaction, %Conditions{ content: {:==, [], [ {:get_in, [], [ {:scope, [], nil}, @@ -38,31 +42,29 @@ defmodule Archethic.Contracts.ConditionInterpreter do ]} } } - } - - # Usage of functions in the condition fields - - iex> ConditionInterpreter.parse({{:atom, "condition"}, [line: 1], - ...> [ - ...> [ - ...> {{:atom, "transaction"}, [ - ...> {{:atom, "content"}, {{:atom, "hash"}, [line: 2], - ...> [ - ...> {{:., [line: 2], - ...> [ - ...> { {:atom, "contract"}, [line: 2], - ...> nil}, - ...> {:atom, "code"} - ...> ]}, - ...> [no_parens: true, line: 2], - ...> []} - ...> ]} - ...> }]} - ...> ] - ...> ]}) - { - :ok, %{ - transaction: %Conditions{ + + Usage of functions in the condition fields + + iex> ConditionInterpreter.parse({{:atom, "condition"}, [line: 1], + ...> [ + ...> [ + ...> {{:atom, "transaction"}, [ + ...> {{:atom, "content"}, {{:atom, "hash"}, [line: 2], + ...> [ + ...> {{:., [line: 2], + ...> [ + ...> { {:atom, "contract"}, [line: 2], + ...> nil}, + ...> {:atom, "code"} + ...> ]}, + ...> [no_parens: true, line: 2], + ...> []} + ...> ]} + ...> }]} + ...> ] + ...> ]}) + { + :ok, :transaction, %Conditions{ content: {:==, [line: 2], [ {:get_in, [line: 2], [ {:scope, [line: 2], nil}, @@ -83,64 +85,69 @@ defmodule Archethic.Contracts.ConditionInterpreter do } } } - } - - # Usage with multiple condition fields - - iex> ConditionInterpreter.parse({{:atom, "condition"}, [line: 1], - ...> [ - ...> {{:atom, "transaction"}, [ - ...> {{:atom, "content"}, "hello"}, - ...> {{:atom, "uco_transfers"}, {:%{}, [line: 3], - ...> [ - ...> {"00006B368BE45DACD0CBC0EC5893BDC1079448181AA88A2CBB84AF939912E858843E", - ...> 1000000000} - ...> ]} - ...> } - ...> ]} - ...> ]}) - {:ok, %{ - transaction: %Conditions{ - content: {:==, [], [{:get_in, [], [{:scope, [], nil}, ["transaction", "content"]]}, "hello"]}, - uco_transfers: {:==, [], [ - {:get_in, [], [{:scope, [], nil}, - ["transaction", "uco_transfers"] - ]}, - {:%{}, [line: 3], [{ - <<0, 0, 107, 54, 139, 228, 93, 172, 208, 203, 192, 236, 88, 147, 189, 193, 7, 148, 72, 24, 26, 168, 138, 44, 187, 132, 175, 147, 153, 18, 232, 88, 132, 62>>, 1000000000 - }]} - ]} + + Usage with multiple condition fields + + iex> ConditionInterpreter.parse({{:atom, "condition"}, [line: 1], + ...> [ + ...> {{:atom, "transaction"}, [ + ...> {{:atom, "content"}, "hello"}, + ...> {{:atom, "uco_transfers"}, {:%{}, [line: 3], + ...> [ + ...> {"00006B368BE45DACD0CBC0EC5893BDC1079448181AA88A2CBB84AF939912E858843E", + ...> 1000000000} + ...> ]} + ...> } + ...> ]} + ...> ]}) + {:ok, :transaction, %Conditions{ + content: {:==, [], [{:get_in, [], [{:scope, [], nil}, ["transaction", "content"]]}, "hello"]}, + uco_transfers: {:==, [], [ + {:get_in, [], [{:scope, [], nil}, + ["transaction", "uco_transfers"] + ]}, + {:%{}, [line: 3], [{ + <<0, 0, 107, 54, 139, 228, 93, 172, 208, 203, 192, 236, 88, 147, 189, 193, 7, 148, 72, 24, 26, 168, 138, 44, 187, 132, 175, 147, 153, 18, 232, 88, 132, 62>>, 1000000000 + }]} + ]} + } } - }} + + Usage with origin_family condition + + iex> ConditionInterpreter.parse({{:atom, "condition"}, [line: 1], + ...> [ + ...> [ + ...> {{:atom, "inherit"}, [ + ...> {{:atom, "origin_family"}, {{:atom, "abc"}, + ...> [line: 2], nil}} + ...> ]} + ...> ] + ...> ]}) + {:error, "invalid origin family - L2"} """ + @spec parse(Macro.t()) :: + {:ok, condition_type(), Conditions.t()} | {:error, reason :: String.t()} def parse(ast) do - try do - case Macro.traverse( - ast, - {:ok, %{scope: :root, conditions: %{}}}, - &prewalk(&1, &2), - &postwalk/2 - ) do - {{:atom, key}, :error} -> - {:error, InterpreterUtils.format_error_reason({[], "unexpected term", key})} - - {{{:atom, key}, _}, :error} -> - {:error, InterpreterUtils.format_error_reason({[], "unexpected term", key})} - - {{{:atom, key}, metadata, _}, :error} -> - {:error, InterpreterUtils.format_error_reason({metadata, "unexpected term", key})} - - {{_, metadata, _}, {:error, reason}} -> - {:error, InterpreterUtils.format_error_reason({metadata, "unexpected term", reason})} - - {_node, {:ok, %{conditions: conditions}}} -> - {:ok, conditions} - end - catch - {:error, {{:atom, key}, metadata, _}} -> - {:error, InterpreterUtils.format_error_reason({metadata, "unexpected term", key})} + case Macro.traverse( + ast, + {:ok, %{scope: :root}}, + &prewalk(&1, &2), + &postwalk/2 + ) do + {_node, {:ok, condition_name, conditions}} -> + {:ok, condition_name, conditions} + + {node, _} -> + {:error, InterpreterUtils.format_error_reason(node, "unexpected term")} end + catch + {:error, node} -> + {:error, InterpreterUtils.format_error_reason(node, "unexpected term")} + + {:error, reason, node} -> + {:error, InterpreterUtils.format_error_reason(node, reason)} end # Whitelist the DSL for conditions @@ -166,13 +173,75 @@ defmodule Archethic.Contracts.ConditionInterpreter do {node, {:ok, %{context | scope: {:condition, condition_name, field}}}} end - # Whitelist the library functions in the the field of a condition + # Whitelist the origin family + defp prewalk(node = [{{:atom, "origin_family"}, {{:atom, family}, _, _}}], acc = {:ok, _}) do + families = SharedSecrets.list_origin_families() |> Enum.map(&Atom.to_string/1) + + if family in families do + {node, acc} + else + {node, {:error, "invalid origin family"}} + end + end + + # Whitelist the regex_match?/1 function in the condition defp prewalk( - node = {{:atom, function}, _metadata, _}, - {:ok, context = %{scope: parent_scope = {:condition, _, _}}} - ) - when function in @library_functions_names do - {node, {:ok, %{context | scope: {:function, function, parent_scope}}}} + node = {{:atom, "regex_match?"}, _, [_search]}, + acc = {:ok, %{scope: {:condition, _, _}}} + ) do + {node, acc} + end + + # Whitelist the json_path_extract/1 function in the condition + defp prewalk( + node = {{:atom, "json_path_extract"}, _, [_search]}, + acc = {:ok, %{scope: {:condition, _, _}}} + ) do + {node, acc} + end + + # Whitelist the json_path_match?/1 function in the condition + defp prewalk( + node = {{:atom, "json_path_match?"}, _, [_search]}, + acc = {:ok, %{scope: {:condition, _, _}}} + ) do + {node, acc} + end + + # Whitelist the hash/0 function in the condition + defp prewalk( + node = {{:atom, "hash"}, _, []}, + acc = {:ok, %{scope: {:condition, _, _}}} + ) do + {node, acc} + end + + # Whitelist the in?/1 function in the condition + defp prewalk( + node = {{:atom, "in?"}, _, [_data]}, + acc = {:ok, %{scope: {:condition, _, _}}} + ) do + {node, acc} + end + + # Whitelist the size/0 function in the condition + 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 get_genesis_public_key/0 function in condition + defp prewalk( + node = {{:atom, "get_genesis_public_key"}, _, []}, + acc = {:ok, %{scope: {:condition, _, _}}} + ) do + {node, acc} end # Whitelist usage of maps in the field of a condition @@ -180,6 +249,10 @@ defmodule Archethic.Contracts.ConditionInterpreter do {node, acc} end + defp prewalk(node, {:error, reason}) do + throw({:error, reason, node}) + end + defp prewalk(node, acc) do InterpreterUtils.prewalk(node, acc) end @@ -188,18 +261,34 @@ defmodule Archethic.Contracts.ConditionInterpreter do defp postwalk( node = {{:atom, "condition"}, _, [[{{:atom, condition_name}, conditions}]]}, - {:ok, context = %{conditions: previous_conditions}} + {:ok, _} ) do - new_conditions = new_conditions(condition_name, conditions, previous_conditions) - {node, {:ok, %{context | conditions: new_conditions}}} + conditions = build_conditions(condition_name, conditions) + + acc = + case condition_name do + "transaction" -> {:ok, :transaction, conditions} + "inherit" -> {:ok, :inherit, conditions} + "oracle" -> {:ok, :oracle, conditions} + end + + {node, acc} end defp postwalk( node = {{:atom, "condition"}, _, [{{:atom, condition_name}, conditions}]}, - {:ok, context = %{conditions: previous_conditions}} + {:ok, _} ) do - new_conditions = new_conditions(condition_name, conditions, previous_conditions) - {node, {:ok, %{context | conditions: new_conditions}}} + conditions = build_conditions(condition_name, conditions) + + acc = + case condition_name do + "transaction" -> {:ok, :transaction, conditions} + "inherit" -> {:ok, :inherit, conditions} + "oracle" -> {:ok, :oracle, conditions} + end + + {node, acc} end defp postwalk( @@ -211,10 +300,18 @@ defmodule Archethic.Contracts.ConditionInterpreter do conditions ]} ]}, - {:ok, context = %{scope: %{conditions: previous_conditions}}} + {:ok, _} ) do - new_conditions = new_conditions(condition_name, conditions, previous_conditions) - {node, {:ok, %{context | conditions: new_conditions}}} + conditions = build_conditions(condition_name, conditions) + + acc = + case condition_name do + "transaction" -> {:ok, :transaction, conditions} + "inherit" -> {:ok, :inherit, conditions} + "oracle" -> {:ok, :oracle, conditions} + end + + {node, acc} end defp postwalk( @@ -237,7 +334,7 @@ defmodule Archethic.Contracts.ConditionInterpreter do InterpreterUtils.postwalk(node, acc) end - defp new_conditions(condition_name, conditions, previous_conditions) do + defp build_conditions(condition_name, conditions) do bindings = Enum.map(@transaction_fields, &{&1, ""}) |> Enum.into(%{}) bindings = @@ -257,17 +354,12 @@ defmodule Archethic.Contracts.ConditionInterpreter do subject_scope = if condition_name == "inherit", do: "next", else: "transaction" - conditions = - InterpreterUtils.inject_bindings_and_functions(conditions, - bindings: bindings, - subject: subject_scope - ) - - Map.put( - previous_conditions, - String.to_existing_atom(condition_name), - aggregate_conditions(conditions, subject_scope) + conditions + |> InterpreterUtils.inject_bindings_and_functions( + bindings: bindings, + subject: subject_scope ) + |> aggregate_conditions(subject_scope) end defp aggregate_conditions(conditions, subject_scope) do @@ -310,14 +402,18 @@ defmodule Archethic.Contracts.ConditionInterpreter do subject_scope, subject ) do + arg_length = length(args) + arguments = - if :erlang.function_exported(Library, fun, length(args)) do - # If the number of arguments fullfill the function's arity (without subject) - args - else - [ - {:get_in, metadata, [{:scope, metadata, nil}, [subject_scope, subject]]} | args - ] + case Keyword.get(@exported_library_functions, fun) do + ^arg_length -> + # If the number of arguments fullfill the function's arity (without subject) + args + + _ -> + [ + {:get_in, metadata, [{:scope, metadata, nil}, [subject_scope, subject]]} | args + ] end if fun |> Atom.to_string() |> String.ends_with?("?") do @@ -352,4 +448,103 @@ defmodule Archethic.Contracts.ConditionInterpreter do end defp to_boolean_expression(condition, _, _), do: condition + + @doc """ + Determines if the conditions of a contract are valid from the given constants + """ + @spec valid_conditions?(Conditions.t(), map()) :: boolean() + def valid_conditions?(conditions = %Conditions{}, constants = %{}) do + result = + conditions + |> Map.from_struct() + |> Enum.all?(fn {field, condition} -> + case validate_condition({field, condition}, constants) do + {_, true} -> + true + + {_, false} -> + Logger.debug( + "Invalid condition for `#{field}` with the given value: `#{get_in(constants, ["next", field])}` - expected: #{inspect(condition)}" + ) + + false + end + end) + + if result do + result + else + result + end + end + + defp validate_condition({:origin_family, _}, _) do + # Skip the verification + # The Proof of Work algorithm will use this condition to verify the transaction + {:origin_family, true} + end + + defp validate_condition({:address, nil}, _) do + # Skip the verification as the address changes for each transaction + {:address, true} + end + + defp validate_condition({:previous_public_key, nil}, _) do + # Skip the verification as the previous public key changes for each transaction + {:previous_public_key, true} + end + + defp validate_condition({:timestamp, nil}, _) do + # Skip the verification as timestamp changes for each transaction + {:timestamp, true} + end + + defp validate_condition({:type, nil}, %{"next" => %{"type" => "transfer"}}) do + # Skip the verification when it's the default type + {:type, true} + end + + defp validate_condition({:content, nil}, %{"next" => %{"content" => ""}}) do + # Skip the verification when it's the default type + {:content, true} + end + + # Validation rules for inherit constraints + defp validate_condition({field, nil}, %{"previous" => prev, "next" => next}) do + {field, Map.get(prev, Atom.to_string(field)) == Map.get(next, Atom.to_string(field))} + end + + defp validate_condition({field, condition}, constants = %{"next" => next}) do + result = execute_condition_code(condition, constants) + + if is_boolean(result) do + {field, result} + else + {field, Map.get(next, Atom.to_string(field)) == result} + end + end + + # Validation rules for incoming transaction + defp validate_condition({field, nil}, %{"transaction" => _}) do + # Skip the validation if no transaction conditions are provided + {field, true} + end + + defp validate_condition( + {field, condition}, + constants = %{"transaction" => transaction} + ) do + result = execute_condition_code(condition, constants) + + if is_boolean(result) do + {field, result} + else + {field, Map.get(transaction, Atom.to_string(field)) == result} + end + end + + defp execute_condition_code(quoted_code, constants) do + {res, _} = Code.eval_quoted(quoted_code, scope: constants) + res + end end diff --git a/lib/archethic/contracts/interpreter/utils.ex b/lib/archethic/contracts/interpreter/utils.ex index d01f4cf4fa..4ec31bd47a 100644 --- a/lib/archethic/contracts/interpreter/utils.ex +++ b/lib/archethic/contracts/interpreter/utils.ex @@ -7,9 +7,19 @@ defmodule Archethic.Contracts.Interpreter.Utils do @library_functions_names Library.__info__(:functions) |> Enum.map(&Atom.to_string(elem(&1, 0))) + @library_functions_names_atoms Library.__info__(:functions) + |> Enum.map(&{Atom.to_string(elem(&1, 0)), elem(&1, 0)}) + |> Enum.into(%{}) + @transaction_statements_functions_names TransactionStatements.__info__(:functions) |> Enum.map(&Atom.to_string(elem(&1, 0))) + @transaction_statements_functions_names_atoms TransactionStatements.__info__(:functions) + |> Enum.map( + &{Atom.to_string(elem(&1, 0)), elem(&1, 0)} + ) + |> Enum.into(%{}) + @transaction_fields [ "address", "type", @@ -196,11 +206,84 @@ defmodule Archethic.Contracts.Interpreter.Utils do {node, acc} end + # Whitelist the size/1 function + def prewalk( + node = {{:atom, "size"}, _, [_data]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root do + {node, acc} + end + + # Whitelist the hash/1 function + def prewalk(node = {{:atom, "hash"}, _, [_data]}, acc = {:ok, %{scope: scope}}) + when scope != :root, + do: {node, acc} + + # Whitelist the regex_match?/2 function + def prewalk( + node = {{:atom, "regex_match?"}, _, [_input, _search]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root, + do: {node, acc} + + # Whitelist the regex_extract/2 function + def prewalk( + node = {{:atom, "regex_extract"}, _, [_input, _search]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root, + do: {node, acc} + + # Whitelist the json_path_extract/2 function + def prewalk( + node = {{:atom, "json_path_extract"}, _, [_input, _search]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root, + do: {node, acc} + + # Whitelist the json_path_match?/2 function + def prewalk( + node = {{:atom, "json_path_match?"}, _, [_input, _search]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root, + do: {node, acc} + + # Whitelist the get_genesis_address/1 function + def prewalk( + node = {{:atom, "get_genesis_address"}, _, [_address]}, + acc = {:ok, %{scope: scope}} + ) + when scope != :root do + {node, acc} + end + + # Whitelist the get_genesis_public_key/1 function + def prewalk( + node = {{:atom, "get_genesis_public_key"}, _, [_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 + {node, acc} + end + # Blacklist everything else - def prewalk(node, _), do: throw({:error, node |> IO.inspect()}) + def prewalk(node, _acc) do + throw({:error, node}) + end @spec postwalk(Macro.t(), any()) :: {Macro.t(), any()} - def postwalk(node = {{:atom, _}, _, _}, {:ok, context = %{scope: {:function, _, scope}}}) do + def postwalk(node = {{:atom, fun}, _, _}, {:ok, context = %{scope: {:function, _, scope}}}) + when fun in @library_functions_names or fun in @transaction_statements_functions_names do {node, {:ok, %{context | scope: scope}}} end @@ -241,6 +324,9 @@ defmodule Archethic.Contracts.Interpreter.Utils do def postwalk(node, acc), do: {node, acc} + @doc """ + Inject context variables and functions by transforming the ast + """ @spec inject_bindings_and_functions(Macro.t(), list()) :: Macro.t() def inject_bindings_and_functions(quoted_code, opts) when is_list(opts) do bindings = Keyword.get(opts, :bindings, %{}) @@ -274,8 +360,9 @@ defmodule Archethic.Contracts.Interpreter.Utils do defp do_postwalk_execution(_node = {{:atom, atom}, metadata, args}, acc) when atom in @library_functions_names do - {{{:., metadata, [{:__aliases__, [alias: Library], [:Library]}, String.to_atom(atom)]}, - metadata, args}, acc} + fun = Map.get(@library_functions_names_atoms, atom) + + {{{:., metadata, [{:__aliases__, [alias: Library], [:Library]}, fun]}, metadata, args}, acc} end defp do_postwalk_execution(_node = {{:atom, atom}, metadata, args}, acc) @@ -286,11 +373,13 @@ defmodule Archethic.Contracts.Interpreter.Utils do ast end) + fun = Map.get(@transaction_statements_functions_names_atoms, atom) + ast = { {:., metadata, [ {:__aliases__, [alias: TransactionStatements], [:TransactionStatements]}, - String.to_atom(atom) + fun ]}, metadata, [{:&, metadata, [1]} | args] @@ -404,7 +493,37 @@ defmodule Archethic.Contracts.Interpreter.Utils do defp parse_value(val), do: val - def format_error_reason({metadata, message, cause}) do + @doc """ + Format an error message from the failing ast node + + It returns message with metadata if possible to indicate the line of the error + """ + @spec format_error_reason(Macro.t(), String.t()) :: String.t() + def format_error_reason({:atom, _key}, reason) do + do_format_error_reason(reason, "", []) + end + + def format_error_reason({{:atom, key}, metadata, _}, reason) do + do_format_error_reason(reason, key, metadata) + end + + def format_error_reason({_, metadata, [{:__aliases__, _, [atom: module]} | _]}, reason) do + do_format_error_reason(reason, module, metadata) + end + + def format_error_reason(ast_node = {_, metadata, _}, reason) do + do_format_error_reason(reason, Macro.to_string(ast_node), metadata) + end + + def format_error_reason({{:atom, _}, {_, metadata, _}}, reason) do + do_format_error_reason(reason, "", metadata) + end + + def format_error_reason({{:atom, key}, _}, reason) do + do_format_error_reason(reason, key, []) + end + + defp do_format_error_reason(message, cause, metadata) do message = prepare_message(message) [prepare_message(message), cause, metadata_to_string(metadata)] diff --git a/lib/archethic/contracts/loader.ex b/lib/archethic/contracts/loader.ex index b4686a3751..95b0d20617 100644 --- a/lib/archethic/contracts/loader.ex +++ b/lib/archethic/contracts/loader.ex @@ -61,27 +61,23 @@ defmodule Archethic.Contracts.Loader do when code != "" do stop_contract(Transaction.previous_address(tx)) - case Contracts.parse!(code) do - # Only load smart contract which are expecting interactions - %Contract{triggers: triggers = [_ | _]} -> - triggers = Enum.reject(triggers, &(&1.actions == {:__block__, [], []})) - - # Avoid to load empty smart contract - if length(triggers) > 0 do - {:ok, _} = - DynamicSupervisor.start_child( - ContractSupervisor, - {Worker, Contract.from_transaction!(tx)} - ) - - Logger.info("Smart contract loaded", - transaction_address: Base.encode16(address), - transaction_type: type - ) - end - - _ -> - :ok + %Contract{triggers: triggers} = Contracts.parse!(code) + triggers = Enum.reject(triggers, fn {_, actions} -> actions == {:__block__, [], []} end) + + # Create worker only load smart contract which are expecting interactions and where the actions are not empty + if length(triggers) > 0 do + {:ok, _} = + DynamicSupervisor.start_child( + ContractSupervisor, + {Worker, Contract.from_transaction!(tx)} + ) + + Logger.info("Smart contract loaded", + transaction_address: Base.encode16(address), + transaction_type: type + ) + else + :ok end end diff --git a/lib/archethic/contracts/worker.ex b/lib/archethic/contracts/worker.ex index 3ee05903c5..db827fc802 100644 --- a/lib/archethic/contracts/worker.ex +++ b/lib/archethic/contracts/worker.ex @@ -5,10 +5,10 @@ defmodule Archethic.Contracts.Worker do alias Archethic.ContractRegistry alias Archethic.Contracts.Contract - alias Archethic.Contracts.Contract.Conditions - alias Archethic.Contracts.Contract.Constants - alias Archethic.Contracts.Contract.Trigger - alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.ContractConditions, as: Conditions + alias Archethic.Contracts.ContractConstants, as: Constants + alias Archethic.Contracts.ActionInterpreter + alias Archethic.Contracts.ConditionInterpreter alias Archethic.Crypto @@ -62,10 +62,10 @@ defmodule Archethic.Contracts.Worker do def handle_continue(:start_schedulers, state = %{contract: %Contract{triggers: triggers}}) do new_state = - Enum.reduce(triggers, state, fn trigger = %Trigger{type: type}, acc -> - case schedule_trigger(trigger) do + Enum.reduce(triggers, state, fn {trigger_type, _}, acc -> + case schedule_trigger(trigger_type) do timer when is_reference(timer) -> - Map.update(acc, :timers, %{type => timer}, &Map.put(&1, type, timer)) + Map.update(acc, :timers, %{trigger_type => timer}, &Map.put(&1, trigger_type, timer)) _ -> acc @@ -79,14 +79,13 @@ defmodule Archethic.Contracts.Worker do {:execute, incoming_tx = %Transaction{}}, _from, state = %{ - contract: - contract = %Contract{ - triggers: triggers, - constants: %Constants{ - contract: contract_constants = %{"address" => contract_address} - }, - conditions: %{transaction: transaction_condition} - } + contract: %Contract{ + triggers: triggers, + constants: %Constants{ + contract: contract_constants = %{"address" => contract_address} + }, + conditions: %{transaction: transaction_condition} + } } ) do Logger.info("Execute contract transaction actions", @@ -95,7 +94,7 @@ defmodule Archethic.Contracts.Worker do contract: Base.encode16(contract_address) ) - if Enum.any?(triggers, &(&1.type == :transaction)) do + if Map.has_key?(triggers, :transaction) do constants = %{ "contract" => contract_constants, "transaction" => Constants.from_transaction(incoming_tx) @@ -103,8 +102,8 @@ defmodule Archethic.Contracts.Worker do contract_transaction = Constants.to_transaction(contract_constants) - with true <- Interpreter.valid_conditions?(transaction_condition, constants), - next_tx <- Interpreter.execute_actions(contract, :transaction, constants), + with true <- ConditionInterpreter.valid_conditions?(transaction_condition, constants), + next_tx <- ActionInterpreter.execute(Map.fetch!(triggers, :transaction), constants), {:ok, next_tx} <- chain_transaction(next_tx, contract_transaction) do handle_new_transaction(next_tx) {:reply, :ok, state} @@ -133,13 +132,12 @@ defmodule Archethic.Contracts.Worker do end def handle_info( - %Trigger{type: :datetime}, + {:trigger, {:datetime, timestamp}}, state = %{ - contract: - contract = %Contract{ - constants: %Constants{contract: contract_constants = %{"address" => address}} - }, - timers: %{datetime: timer} + contract: %Contract{ + triggers: triggers, + constants: %Constants{contract: contract_constants = %{"address" => address}} + } } ) do Logger.info("Execute contract datetime trigger actions", @@ -152,24 +150,24 @@ defmodule Archethic.Contracts.Worker do contract_tx = Constants.to_transaction(contract_constants) - with next_tx <- Interpreter.execute_actions(contract, :datetime, constants), + with next_tx <- + ActionInterpreter.execute(Map.fetch!(triggers, {:datetime, timestamp}), constants), {:ok, next_tx} <- chain_transaction(next_tx, contract_tx) do handle_new_transaction(next_tx) end - Process.cancel_timer(timer) - {:noreply, Map.update!(state, :timers, &Map.delete(&1, :datetime))} + {:noreply, Map.update!(state, :timers, &Map.delete(&1, {:datetime, timestamp}))} end def handle_info( - trigger = %Trigger{type: :interval}, + {:trigger, {:interval, interval}}, state = %{ - contract: - contract = %Contract{ - constants: %Constants{ - contract: contract_constants = %{"address" => address} - } + contract: %Contract{ + triggers: triggers, + constants: %Constants{ + contract: contract_constants = %{"address" => address} } + } } ) do Logger.info("Execute contract interval trigger actions", @@ -177,7 +175,7 @@ defmodule Archethic.Contracts.Worker do ) # Schedule the next interval trigger - interval_timer = schedule_trigger(trigger) + interval_timer = schedule_trigger({:interval, interval}) constants = %{ "contract" => contract_constants @@ -186,7 +184,8 @@ defmodule Archethic.Contracts.Worker do contract_transaction = Constants.to_transaction(contract_constants) with true <- enough_funds?(address), - next_tx <- Interpreter.execute_actions(contract, :interval, constants), + next_tx <- + 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 handle_new_transaction(next_tx) @@ -198,18 +197,17 @@ defmodule Archethic.Contracts.Worker do def handle_info( {:new_transaction, tx_address, :oracle, _timestamp}, state = %{ - contract: - contract = %Contract{ - triggers: triggers, - constants: %Constants{contract: contract_constants = %{"address" => address}}, - conditions: %{oracle: oracle_condition} - } + contract: %Contract{ + triggers: triggers, + constants: %Constants{contract: contract_constants = %{"address" => address}}, + conditions: %{oracle: oracle_condition} + } } ) do Logger.info("Execute contract oracle trigger actions", contract: Base.encode16(address)) with true <- enough_funds?(address), - true <- Enum.any?(triggers, &(&1.type == :oracle)) do + true <- Map.has_key?(triggers, :oracle) do {:ok, tx} = TransactionChain.get_transaction(tx_address) constants = %{ @@ -220,14 +218,14 @@ defmodule Archethic.Contracts.Worker do contract_transaction = Constants.to_transaction(contract_constants) if Conditions.empty?(oracle_condition) do - with next_tx <- Interpreter.execute_actions(contract, :oracle, constants), + with next_tx <- 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 <- Interpreter.valid_conditions?(oracle_condition, constants), - next_tx <- Interpreter.execute_actions(contract, :oracle, constants), + with true <- ConditionInterpreter.valid_conditions?(oracle_condition, constants), + next_tx <- 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) @@ -252,31 +250,32 @@ defmodule Archethic.Contracts.Worker do {:via, Registry, {ContractRegistry, address}} end - defp schedule_trigger( - trigger = %Trigger{ - type: :interval, - opts: opts - } - ) do - interval = Keyword.fetch!(opts, :at) - enable_seconds? = Keyword.get(opts, :enable_seconds, false) + # Only used for testing + defp schedule_trigger(trigger = {:interval, {interval, :second}}) do + Process.send_after( + self(), + {:trigger, trigger}, + Utils.time_offset(interval, DateTime.utc_now(), true) * 1000 + ) + end + defp schedule_trigger(trigger = {:interval, interval}) do Process.send_after( self(), - trigger, - Utils.time_offset(interval, DateTime.utc_now(), enable_seconds?) * 1000 + {:trigger, trigger}, + Utils.time_offset(interval, DateTime.utc_now()) * 1000 ) end - defp schedule_trigger(trigger = %Trigger{type: :datetime, opts: [at: datetime = %DateTime{}]}) do + defp schedule_trigger(trigger = {:datetime, datetime = %DateTime{}}) do seconds = DateTime.diff(datetime, DateTime.utc_now()) if seconds > 0 do - Process.send_after(self(), trigger, seconds * 1000) + Process.send_after(self(), {:trigger, trigger}, seconds * 1000) end end - defp schedule_trigger(%Trigger{type: :oracle}) do + defp schedule_trigger(:oracle) do PubSub.register_to_new_transaction_by_type(:oracle) end diff --git a/lib/archethic/mining/pending_transaction_validation.ex b/lib/archethic/mining/pending_transaction_validation.ex index 32ed3f39fc..4f37be2e2c 100644 --- a/lib/archethic/mining/pending_transaction_validation.ex +++ b/lib/archethic/mining/pending_transaction_validation.ex @@ -225,7 +225,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do data: %TransactionData{code: code, ownerships: ownerships} }) do case Contracts.parse(code) do - {:ok, %Contract{triggers: [_ | _]}} -> + {:ok, %Contract{triggers: triggers}} when map_size(triggers) > 0 -> if Enum.any?( ownerships, &Ownership.authorized_public_key?(&1, Crypto.storage_nonce_public_key()) diff --git a/lib/archethic/mining/proof_of_work.ex b/lib/archethic/mining/proof_of_work.ex index ca7c1700c0..17e6785823 100644 --- a/lib/archethic/mining/proof_of_work.ex +++ b/lib/archethic/mining/proof_of_work.ex @@ -11,7 +11,7 @@ defmodule Archethic.Mining.ProofOfWork do alias Archethic.Contracts alias Archethic.Contracts.Contract - alias Archethic.Contracts.Contract.Conditions + alias Archethic.Contracts.ContractConditions alias Archethic.Crypto @@ -118,7 +118,7 @@ defmodule Archethic.Mining.ProofOfWork do def list_origin_public_keys_candidates(tx = %Transaction{data: %TransactionData{code: code}}) when code != "" do case Contracts.parse(code) do - {:ok, %Contract{conditions: %{inherit: %Conditions{origin_family: family}}}} + {:ok, %Contract{conditions: %{inherit: %ContractConditions{origin_family: family}}}} when family != :all -> SharedSecrets.list_origin_public_keys(family) diff --git a/lib/archethic/transaction_chain/mem_tables_loader.ex b/lib/archethic/transaction_chain/mem_tables_loader.ex index 97b7021e36..44e48f890e 100644 --- a/lib/archethic/transaction_chain/mem_tables_loader.ex +++ b/lib/archethic/transaction_chain/mem_tables_loader.ex @@ -5,7 +5,7 @@ defmodule Archethic.TransactionChain.MemTablesLoader do @vsn Mix.Project.config()[:version] alias Archethic.Contracts.Contract - alias Archethic.Contracts.Contract.Conditions + alias Archethic.Contracts.ContractConditions alias Archethic.DB @@ -70,7 +70,7 @@ defmodule Archethic.TransactionChain.MemTablesLoader do %Contract{conditions: %{transaction: transaction_conditions}} = Contract.from_transaction!(tx) # TODO: improve the criteria of pending detection - if Conditions.empty?(transaction_conditions) do + if ContractConditions.empty?(transaction_conditions) do :ok else PendingLedger.add_address(address) diff --git a/lib/archethic/utils.ex b/lib/archethic/utils.ex index 9fb02c89c3..e752dd4a95 100644 --- a/lib/archethic/utils.ex +++ b/lib/archethic/utils.ex @@ -783,10 +783,10 @@ defmodule Archethic.Utils do iex> Utils.previous_date(%Crontab.CronExpression{second: [{:/, :*, 10}], extended: true}, ~U[2022-10-01 01:00:10Z]) ~U[2022-10-01 01:00:00Z] - + iex> Utils.previous_date(%Crontab.CronExpression{second: [{:/, :*, 10}], extended: true}, ~U[2022-10-01 01:00:10.100Z]) ~U[2022-10-01 01:00:10Z] - + iex> Utils.previous_date(%Crontab.CronExpression{second: [{:/, :*, 30}], extended: true}, ~U[2022-10-26 07:38:30.569648Z]) ~U[2022-10-26 07:38:30Z] """ diff --git a/test/archethic/contracts/interpreter/action_test.exs b/test/archethic/contracts/interpreter/action_test.exs index f7cbb89b96..edf36a49ba 100644 --- a/test/archethic/contracts/interpreter/action_test.exs +++ b/test/archethic/contracts/interpreter/action_test.exs @@ -1,7 +1,356 @@ defmodule Archethic.Contracts.ActionInterpreterTest do - use ExUnit.Case + use ArchethicCase alias Archethic.Contracts.ActionInterpreter + alias Archethic.Contracts.Interpreter + + alias Archethic.P2P + alias Archethic.P2P.Node + alias Archethic.P2P.Message.FirstAddress + + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData doctest ActionInterpreter + + import Mox + + test "should parse a contract with some standard functions" do + assert {:ok, :transaction, + { + :__block__, + [], + [ + { + :=, + [line: 2], + [ + {:scope, [line: 2], nil}, + { + :update_in, + [line: 2], + [ + {:scope, [line: 2], nil}, + ["next_transaction"], + {:&, [line: 2], + [ + {{:., [line: 2], + [ + {:__aliases__, + [ + alias: Archethic.Contracts.Interpreter.TransactionStatements + ], [:TransactionStatements]}, + :set_type + ]}, [line: 2], [{:&, [line: 2], [1]}, "transfer"]} + ]} + ] + } + ] + }, + { + :=, + [line: 3], + [ + {:scope, [line: 3], nil}, + { + :update_in, + [line: 3], + [ + {:scope, [line: 3], nil}, + ["next_transaction"], + { + :&, + [line: 3], + [ + { + {:., [line: 3], + [ + {:__aliases__, + [ + alias: Archethic.Contracts.Interpreter.TransactionStatements + ], [:TransactionStatements]}, + :add_uco_transfer + ]}, + [line: 3], + [ + {:&, [line: 3], [1]}, + [ + {"to", + <<127, 102, 97, 172, 226, 130, 249, 71, 172, 162, 239, 148, + 125, 1, 189, 220, 144, 198, 95, 9, 238, 130, 139, 218, 222, + 46, 62, 212, 37, 132, 112, 179>>}, + {"amount", 1_040_000_000} + ] + ] + } + ] + } + ] + } + ] + }, + { + :=, + [line: 4], + [ + {:scope, [line: 4], nil}, + { + :update_in, + [line: 4], + [ + {:scope, [line: 4], nil}, + ["next_transaction"], + { + :&, + [line: 4], + [ + { + {:., [line: 4], + [ + {:__aliases__, + [ + alias: Archethic.Contracts.Interpreter.TransactionStatements + ], [:TransactionStatements]}, + :add_token_transfer + ]}, + [line: 4], + [ + {:&, [line: 4], [1]}, + [ + {"to", + <<48, 103, 4, 85, 113, 62, 44, 190, 207, 148, 89, 18, 38, 169, + 3, 101, 30, 216, 98, 86, 53, 24, 29, 218, 35, 111, 236, 194, + 33, 209, 231, 228>>}, + {"amount", 20_000_000_000}, + {"token_address", + <<174, 180, 166, 245, 171, 109, 130, 190, 34, 60, 88, 103, 235, + 165, 254, 97, 111, 82, 244, 16, 220, 248, 59, 69, 175, 241, + 88, 221, 64, 174, 138, 195>>}, + {"token_id", 0} + ] + ] + } + ] + } + ] + } + ] + }, + { + :=, + [line: 5], + [ + {:scope, [line: 5], nil}, + { + :update_in, + [line: 5], + [ + {:scope, [line: 5], nil}, + ["next_transaction"], + { + :&, + [line: 5], + [ + {{:., [line: 5], + [ + {:__aliases__, + [ + alias: Archethic.Contracts.Interpreter.TransactionStatements + ], [:TransactionStatements]}, + :set_content + ]}, [line: 5], [{:&, [line: 5], [1]}, "Receipt"]} + ] + } + ] + } + ] + }, + { + :=, + [line: 6], + [ + {:scope, [line: 6], nil}, + { + :update_in, + [line: 6], + [ + {:scope, [line: 6], nil}, + ["next_transaction"], + { + :&, + [line: 6], + [ + { + {:., [line: 6], + [ + {:__aliases__, + [ + alias: Archethic.Contracts.Interpreter.TransactionStatements + ], [:TransactionStatements]}, + :add_ownership + ]}, + [line: 6], + [ + {:&, [line: 6], [1]}, + [ + {"secret", "MyEncryptedSecret"}, + {"secret_key", "MySecretKey"}, + {"authorized_public_keys", + [ + <<112, 194, 69, 229, 217, 112, 181, 157, 246, 86, 56, 189, + 213, 217, 99, 238, 34, 230, 216, 146, 234, 34, 77, 136, 9, + 208, 251, 117, 208, 177, 144, 122>> + ]} + ] + ] + } + ] + } + ] + } + ] + }, + { + :=, + [line: 7], + [ + {:scope, [line: 7], nil}, + { + :update_in, + [line: 7], + [ + {:scope, [line: 7], nil}, + ["next_transaction"], + { + :&, + [line: 7], + [ + { + {:., [line: 7], + [ + {:__aliases__, + [ + alias: Archethic.Contracts.Interpreter.TransactionStatements + ], [:TransactionStatements]}, + :add_recipient + ]}, + [line: 7], + [ + {:&, [line: 7], [1]}, + <<120, 39, 60, 92, 188, 235, 134, 23, 245, 67, 128, 204, 47, 23, + 61, 242, 64, 77, 182, 118, 201, 241, 13, 84, 107, 111, 57, 94, + 111, 59, 221, 238>> + ] + } + ] + } + ] + } + ] + } + ] + }} = + """ + actions triggered_by: transaction do + set_type transfer + add_uco_transfer to: \"7F6661ACE282F947ACA2EF947D01BDDC90C65F09EE828BDADE2E3ED4258470B3\", amount: 1040000000 + add_token_transfer to: \"30670455713E2CBECF94591226A903651ED8625635181DDA236FECC221D1E7E4\", amount: 20000000000, token_address: \"AEB4A6F5AB6D82BE223C5867EBA5FE616F52F410DCF83B45AFF158DD40AE8AC3\", token_id: 0 + set_content \"Receipt\" + add_ownership secret: \"MyEncryptedSecret\", secret_key: \"MySecretKey\", authorized_public_keys: ["70C245E5D970B59DF65638BDD5D963EE22E6D892EA224D8809D0FB75D0B1907A"] + add_recipient \"78273C5CBCEB8617F54380CC2F173DF2404DB676C9F10D546B6F395E6F3BDDEE\" + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + end + + test "should evaluate actions based on if statement" do + assert %Transaction{data: %TransactionData{content: "yes"}} = + ~S""" + actions triggered_by: transaction do + if transaction.previous_public_key == "abc" do + set_content "yes" + else + set_content "no" + end + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + |> elem(2) + |> ActionInterpreter.execute(%{"transaction" => %{"previous_public_key" => "abc"}}) + end + + test "should use a variable assignation" do + assert %Transaction{data: %TransactionData{content: "hello"}} = + """ + actions triggered_by: transaction do + new_content = \"hello\" + set_content new_content + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + |> elem(2) + |> ActionInterpreter.execute() + end + + test "should use a interpolation assignation" do + assert %Transaction{data: %TransactionData{content: "hello 4"}} = + ~S""" + actions triggered_by: transaction do + new_content = "hello #{2+2}" + set_content new_content + end + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + |> elem(2) + |> ActionInterpreter.execute() + end + + test "shall use get_genesis_address/1 in actions" 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() + }) + + address = "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + b_address = Base.decode16!(address) + + MockClient + |> expect(:send_message, fn _, _, _ -> + {:ok, %FirstAddress{address: b_address}} + end) + + assert %Transaction{data: %TransactionData{content: "yes"}} = + ~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.sanitize_code() + |> elem(1) + |> ActionInterpreter.parse() + |> elem(2) + |> ActionInterpreter.execute() + end end diff --git a/test/archethic/contracts/interpreter/condition_test.exs b/test/archethic/contracts/interpreter/condition_test.exs index 5c6918fa5c..38657bf3ed 100644 --- a/test/archethic/contracts/interpreter/condition_test.exs +++ b/test/archethic/contracts/interpreter/condition_test.exs @@ -1,8 +1,261 @@ defmodule Archethic.Contracts.ConditionInterpreterTest do - use ExUnit.Case + use ArchethicCase - alias Archethic.Contracts.Contract.Conditions + alias Archethic.Contracts.ContractConditions, as: Conditions alias Archethic.Contracts.ConditionInterpreter + alias Archethic.Contracts.Interpreter + + alias Archethic.P2P + alias Archethic.P2P.Node + alias Archethic.P2P.Message.FirstPublicKey + alias Archethic.P2P.Message.FirstAddress doctest ConditionInterpreter + + import Mox + + test "should parse map based inherit constraints" do + assert {:ok, :inherit, + %Conditions{ + uco_transfers: + {:==, _, + [ + {:get_in, _, + [ + {:scope, _, nil}, + ["next", "uco_transfers"] + ]}, + [ + {:%{}, _, + [ + {"to", + <<127, 102, 97, 172, 226, 130, 249, 71, 172, 162, 239, 148, 125, 1, 189, + 220, 144, 198, 95, 9, 238, 130, 139, 218, 222, 46, 62, 212, 37, 132, + 112, 179>>}, + {"amount", 1_040_000_000} + ]} + ] + ]} + }} = + """ + condition inherit: [ + uco_transfers: [%{ to: "7F6661ACE282F947ACA2EF947D01BDDC90C65F09EE828BDADE2E3ED4258470B3", amount: 1040000000 }] + ] + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + end + + test "should parse multiline inherit constraints" do + assert {:ok, :inherit, + %Conditions{ + uco_transfers: + {:==, _, + [ + {:get_in, _, [{:scope, _, nil}, ["next", "uco_transfers"]]}, + [ + {:%{}, _, + [ + {"to", + <<127, 102, 97, 172, 226, 130, 249, 71, 172, 162, 239, 148, 125, 1, 189, + 220, 144, 198, 95, 9, 238, 130, 139, 218, 222, 46, 62, 212, 37, 132, + 112, 179>>}, + {"amount", 1_040_000_000} + ]} + ] + ]}, + content: {:==, _, [{:get_in, _, [{:scope, _, nil}, ["next", "content"]]}, "hello"]} + }} = + """ + condition inherit: [ + uco_transfers: [%{ to: "7F6661ACE282F947ACA2EF947D01BDDC90C65F09EE828BDADE2E3ED4258470B3", amount: 1040000000 }], + content: "hello" + ] + + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + end + + test "should flatten comparison operators" do + assert {:ok, :inherit, + %Conditions{ + content: + {:>=, [line: 2], + [ + {{:., [line: 2], + [ + {:__aliases__, [alias: Archethic.Contracts.Interpreter.Library], + [:Library]}, + :size + ]}, [line: 2], + [ + {:get_in, [line: 2], [{:scope, [line: 2], nil}, ["next", "content"]]} + ]}, + 10 + ]} + }} = + """ + condition inherit: [ + content: size() >= 10 + ] + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + end + + test "should accept conditional code within condition" do + {:ok, :inherit, %Conditions{}} = + ~S""" + condition inherit: [ + content: if type == transfer do + regex_match?("hello") + else + regex_match?("hi") + end + ] + + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + end + + test "should accept different type of transaction keyword references" do + {:ok, :inherit, %Conditions{}} = + ~S""" + condition inherit: [ + content: if next.type == transfer do + "hi" + else + if previous.type == transfer do + "hi" + else + "hello" + end + end + ] + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + end + + describe "valid_conditions?" do + test "should validate complex if conditions" do + {:ok, :inherit, conditions} = + ~S""" + condition inherit: [ + type: in?([transfer, token]), + content: if type == transfer do + regex_match?("reason transfer: (.*)") + else + regex_match?("reason token creation: (.*)") + end + ] + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + + assert true == + ConditionInterpreter.valid_conditions?(conditions, %{ + "next" => %{"type" => "transfer", "content" => "reason transfer: pay back alice"} + }) + + assert false == + ConditionInterpreter.valid_conditions?(conditions, %{ + "next" => %{"type" => "transfer", "content" => "dummy"} + }) + + assert true == + ConditionInterpreter.valid_conditions?(conditions, %{ + "next" => %{ + "type" => "token", + "content" => "reason token creation: new super token" + } + }) + end + + test "shall get the first address of the chain in the conditions" 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() + }) + + address = "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + b_address = Base.decode16!(address) + + MockClient + |> expect(:send_message, fn _, _, _ -> + {:ok, %FirstAddress{address: b_address}} + end) + + assert true = + ~s""" + condition transaction: [ + address: get_genesis_address() == "64F05F5236088FC64D1BB19BD13BC548F1C49A42432AF02AD9024D8A2990B2B4" + ] + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + |> elem(2) + |> ConditionInterpreter.valid_conditions?(%{ + "transaction" => %{"address" => :crypto.strong_rand_bytes(32)} + }) + end + + test "shall get the first public of the chain in the conditions" 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() + }) + + public_key = "0001DDE54A313E5DCD73E413748CBF6679F07717F8BDC66CBE8F981E1E475A98605C" + b_public_key = Base.decode16!(public_key) + + MockClient + |> expect(:send_message, fn _, _, _ -> + {:ok, %FirstPublicKey{public_key: b_public_key}} + end) + + assert true = + ~s""" + condition transaction: [ + previous_public_key: get_genesis_public_key() == "0001DDE54A313E5DCD73E413748CBF6679F07717F8BDC66CBE8F981E1E475A98605C" + ] + """ + |> Interpreter.sanitize_code() + |> elem(1) + |> ConditionInterpreter.parse() + |> elem(2) + |> ConditionInterpreter.valid_conditions?(%{ + "transaction" => %{ + "previous_public_key" => <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> + } + }) + end + end end diff --git a/test/archethic/contracts/interpreter_test.exs b/test/archethic/contracts/interpreter_test.exs index 2ce64ec026..24bfd2ef46 100644 --- a/test/archethic/contracts/interpreter_test.exs +++ b/test/archethic/contracts/interpreter_test.exs @@ -3,524 +3,31 @@ defmodule Archethic.Contracts.InterpreterTest do use ArchethicCase alias Archethic.Contracts.Contract - alias Archethic.Contracts.Contract.Conditions - alias Archethic.Contracts.Contract.Constants - alias Archethic.Contracts.Contract.Trigger alias Archethic.Contracts.Interpreter - alias Archethic.P2P - alias Archethic.P2P.Node - alias Archethic.P2P.Message.FirstAddress - alias Archethic.P2P.Message.FirstPublicKey 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 - test "should parse a contract with some standard functions" do - assert {:ok, - %Contract{ - triggers: [ - %Trigger{ - type: :transaction, - actions: { - :__block__, - [], - [ - { - :=, - [line: 2], - [ - {:scope, [line: 2], nil}, - { - :update_in, - [line: 2], - [ - {:scope, [line: 2], nil}, - ["next_transaction"], - {:&, [line: 2], - [ - {{:., [line: 2], - [ - {:__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.TransactionStatements - ], [:TransactionStatements]}, - :set_type - ]}, [line: 2], [{:&, [line: 2], [1]}, "transfer"]} - ]} - ] - } - ] - }, - { - :=, - [line: 3], - [ - {:scope, [line: 3], nil}, - { - :update_in, - [line: 3], - [ - {:scope, [line: 3], nil}, - ["next_transaction"], - { - :&, - [line: 3], - [ - { - {:., [line: 3], - [ - {:__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.TransactionStatements - ], [:TransactionStatements]}, - :add_uco_transfer - ]}, - [line: 3], - [ - {:&, [line: 3], [1]}, - [ - {"to", - <<127, 102, 97, 172, 226, 130, 249, 71, 172, 162, 239, - 148, 125, 1, 189, 220, 144, 198, 95, 9, 238, 130, - 139, 218, 222, 46, 62, 212, 37, 132, 112, 179>>}, - {"amount", 1_040_000_000} - ] - ] - } - ] - } - ] - } - ] - }, - { - :=, - [line: 4], - [ - {:scope, [line: 4], nil}, - { - :update_in, - [line: 4], - [ - {:scope, [line: 4], nil}, - ["next_transaction"], - { - :&, - [line: 4], - [ - { - {:., [line: 4], - [ - {:__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.TransactionStatements - ], [:TransactionStatements]}, - :add_token_transfer - ]}, - [line: 4], - [ - {:&, [line: 4], [1]}, - [ - {"to", - <<48, 103, 4, 85, 113, 62, 44, 190, 207, 148, 89, 18, - 38, 169, 3, 101, 30, 216, 98, 86, 53, 24, 29, 218, - 35, 111, 236, 194, 33, 209, 231, 228>>}, - {"amount", 20_000_000_000}, - {"token_address", - <<174, 180, 166, 245, 171, 109, 130, 190, 34, 60, 88, - 103, 235, 165, 254, 97, 111, 82, 244, 16, 220, 248, - 59, 69, 175, 241, 88, 221, 64, 174, 138, 195>>}, - {"token_id", 0} - ] - ] - } - ] - } - ] - } - ] - }, - { - :=, - [line: 5], - [ - {:scope, [line: 5], nil}, - { - :update_in, - [line: 5], - [ - {:scope, [line: 5], nil}, - ["next_transaction"], - { - :&, - [line: 5], - [ - {{:., [line: 5], - [ - {:__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.TransactionStatements - ], [:TransactionStatements]}, - :set_content - ]}, [line: 5], [{:&, [line: 5], [1]}, "Receipt"]} - ] - } - ] - } - ] - }, - { - :=, - [line: 6], - [ - {:scope, [line: 6], nil}, - { - :update_in, - [line: 6], - [ - {:scope, [line: 6], nil}, - ["next_transaction"], - { - :&, - [line: 6], - [ - { - {:., [line: 6], - [ - {:__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.TransactionStatements - ], [:TransactionStatements]}, - :add_ownership - ]}, - [line: 6], - [ - {:&, [line: 6], [1]}, - [ - {"secret", "MyEncryptedSecret"}, - {"secret_key", "MySecretKey"}, - {"authorized_public_keys", - [ - <<112, 194, 69, 229, 217, 112, 181, 157, 246, 86, 56, - 189, 213, 217, 99, 238, 34, 230, 216, 146, 234, 34, - 77, 136, 9, 208, 251, 117, 208, 177, 144, 122>> - ]} - ] - ] - } - ] - } - ] - } - ] - }, - { - :=, - [line: 7], - [ - {:scope, [line: 7], nil}, - { - :update_in, - [line: 7], - [ - {:scope, [line: 7], nil}, - ["next_transaction"], - { - :&, - [line: 7], - [ - { - {:., [line: 7], - [ - {:__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.TransactionStatements - ], [:TransactionStatements]}, - :add_recipient - ]}, - [line: 7], - [ - {:&, [line: 7], [1]}, - <<120, 39, 60, 92, 188, 235, 134, 23, 245, 67, 128, 204, - 47, 23, 61, 242, 64, 77, 182, 118, 201, 241, 13, 84, - 107, 111, 57, 94, 111, 59, 221, 238>> - ] - } - ] - } - ] - } - ] - } - ] - } - } - ] - }} = + test "should return an error if not conditions or triggers are defined" do + assert {:error, _} = """ - actions triggered_by: transaction do - set_type transfer - add_uco_transfer to: \"7F6661ACE282F947ACA2EF947D01BDDC90C65F09EE828BDADE2E3ED4258470B3\", amount: 1040000000 - add_token_transfer to: \"30670455713E2CBECF94591226A903651ED8625635181DDA236FECC221D1E7E4\", amount: 20000000000, token_address: \"AEB4A6F5AB6D82BE223C5867EBA5FE616F52F410DCF83B45AFF158DD40AE8AC3\", token_id: 0 - set_content \"Receipt\" - add_ownership secret: \"MyEncryptedSecret\", secret_key: \"MySecretKey\", authorized_public_keys: ["70C245E5D970B59DF65638BDD5D963EE22E6D892EA224D8809D0FB75D0B1907A"] - add_recipient \"78273C5CBCEB8617F54380CC2F173DF2404DB676C9F10D546B6F395E6F3BDDEE\" - end + abc """ |> Interpreter.parse() - end - test "should parse a contract with some map based inherit constraints" do - assert {:ok, - %Contract{ - conditions: %{ - inherit: %Conditions{ - uco_transfers: - {:==, _, - [ - {:get_in, _, - [ - {:scope, _, nil}, - ["next", "uco_transfers"] - ]}, - [ - {:%{}, _, - [ - {"to", - <<127, 102, 97, 172, 226, 130, 249, 71, 172, 162, 239, 148, 125, 1, - 189, 220, 144, 198, 95, 9, 238, 130, 139, 218, 222, 46, 62, 212, - 37, 132, 112, 179>>}, - {"amount", 1_040_000_000} - ]} - ] - ]} - } - } - }} = + assert {:error, _} = """ - condition inherit: [ - uco_transfers: [%{ to: "7F6661ACE282F947ACA2EF947D01BDDC90C65F09EE828BDADE2E3ED4258470B3", amount: 1040000000 }] - ] - - actions triggered_by: datetime, at: 1102190390 do - set_type transfer - add_uco_transfer to: \"7F6661ACE282F947ACA2EF947D01BDDC90C65F09EE828BDADE2E3ED4258470B3\", amount: 1040000000 - end + condition """ |> Interpreter.parse() end - test "should parse multiline inherit constraints" do - assert {:ok, - %Contract{ - conditions: %{ - inherit: %Conditions{ - uco_transfers: - {:==, _, - [ - {:get_in, _, [{:scope, _, nil}, ["next", "uco_transfers"]]}, - [ - {:%{}, _, - [ - {"to", - <<127, 102, 97, 172, 226, 130, 249, 71, 172, 162, 239, 148, 125, 1, - 189, 220, 144, 198, 95, 9, 238, 130, 139, 218, 222, 46, 62, 212, - 37, 132, 112, 179>>}, - {"amount", 1_040_000_000} - ]} - ] - ]}, - content: - {:==, _, [{:get_in, _, [{:scope, _, nil}, ["next", "content"]]}, "hello"]} - } - } - }} = - """ - condition inherit: [ - uco_transfers: [%{ to: "7F6661ACE282F947ACA2EF947D01BDDC90C65F09EE828BDADE2E3ED4258470B3", amount: 1040000000 }], - content: "hello" - ] - - """ - |> Interpreter.parse() - end - end - - describe "execute_actions/2" do - test "should evaluate actions based on if statement" do - {:ok, contract} = - ~S""" - actions triggered_by: transaction do - if transaction.previous_public_key == "abc" do - set_content "yes" - else - set_content "no" - end - end - """ - |> Interpreter.parse() - - assert %Transaction{data: %TransactionData{content: "yes"}} = - Interpreter.execute_actions(contract, :transaction, %{ - "transaction" => %{"previous_public_key" => "abc"} - }) - end - - test "should use a variable assignation" do - {:ok, contract} = - """ - actions triggered_by: transaction do - new_content = \"hello\" - set_content new_content - end - """ - |> Interpreter.parse() - - assert %Transaction{data: %TransactionData{content: "hello"}} = - Interpreter.execute_actions(contract, :transaction) - end - - test "should use a interpolation assignation" do - {:ok, contract} = - ~S""" - actions triggered_by: transaction do - new_content = "hello #{2+2}" - set_content new_content - end - """ - |> Interpreter.parse() - - assert %Transaction{data: %TransactionData{content: "hello 4"}} = - Interpreter.execute_actions(contract, :transaction) - end - - test "should flatten comparison operators" do - code = """ - condition inherit: [ - content: size() >= 10 - ] - """ - - {:ok, - %Contract{ - conditions: %{ - inherit: %Conditions{ - content: - {:>=, [line: 2], - [ - {{:., [line: 2], - [ - {:__aliases__, [alias: Archethic.Contracts.Interpreter.Library], - [:Library]}, - :size - ]}, [line: 2], - [ - {:get_in, [line: 2], [{:scope, [line: 2], nil}, ["next", "content"]]} - ]}, - 10 - ]} - } - } - }} = Interpreter.parse(code) - end - - test "should accept conditional code within condition" do - {:ok, %Contract{}} = - ~S""" - condition inherit: [ - content: if type == transfer do - regex_match?("hello") - else - regex_match?("hi") - end - ] - - """ - |> Interpreter.parse() - end - - test "should accept different type of transaction keyword references" do - {:ok, %Contract{}} = - ~S""" - condition inherit: [ - content: if next.type == transfer do - "hi" - else - if previous.type == transfer do - "hi" - else - "hello" - end - end - ] - - condition transaction: [ - content: "#{hash(contract.code)} - YES" - ] - """ - |> Interpreter.parse() - end - end - - describe "execute/2" do - test "should execute complex condition with if statements" do - code = ~S""" - condition inherit: [ - type: in?([transfer, token]), - content: if type == transfer do - regex_match?("reason transfer: (.*)") - else - regex_match?("reason token creation: (.*)") - end, - ] - - condition transaction: [ - content: hash(contract.code) - ] - """ - - {:ok, - %Contract{conditions: %{inherit: inherit_conditions, transaction: transaction_conditions}}} = - Interpreter.parse(code) - - assert true == - Interpreter.valid_conditions?(inherit_conditions, %{ - "next" => %{"type" => "transfer", "content" => "reason transfer: pay back alice"} - }) - - assert false == - Interpreter.valid_conditions?(inherit_conditions, %{ - "next" => %{"type" => "transfer", "content" => "dummy"} - }) - - assert true == - Interpreter.valid_conditions?(inherit_conditions, %{ - "next" => %{ - "type" => "token", - "content" => "reason token creation: new super token" - } - }) - - assert true == - Interpreter.valid_conditions?(transaction_conditions, %{ - "transaction" => %{"content" => :crypto.hash(:sha256, code)}, - "contract" => %{"code" => code} - }) + test "should return an error for unexpected term" do + assert {:error, "unexpected term - @1 - L1"} = "@1" |> Interpreter.parse() end end @@ -550,104 +57,4 @@ 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 - - test "shall get the first public of the chain in the conditions" do - public_key = "0001DDE54A313E5DCD73E413748CBF6679F07717F8BDC66CBE8F981E1E475A98605C" - b_public_key = Base.decode16!(public_key) - - MockClient - |> expect(:send_message, fn _, _, _ -> - {:ok, %FirstPublicKey{public_key: b_public_key}} - end) - - {:ok, %Contract{conditions: %{transaction: conditions}}} = - ~s""" - condition transaction: [ - previous_public_key: get_genesis_public_key() == "0001DDE54A313E5DCD73E413748CBF6679F07717F8BDC66CBE8F981E1E475A98605C" - ] - """ - |> Interpreter.parse() - - assert true = - Interpreter.valid_conditions?( - conditions, - %{ - "transaction" => %{ - "previous_public_key" => - <<0::8, 0::8, :crypto.strong_rand_bytes(32)::binary>> - } - } - ) - end - - 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 diff --git a/test/archethic/contracts/loader_test.exs b/test/archethic/contracts/loader_test.exs index 3883db6545..83889ad228 100644 --- a/test/archethic/contracts/loader_test.exs +++ b/test/archethic/contracts/loader_test.exs @@ -3,8 +3,7 @@ defmodule Archethic.Contracts.LoaderTest do alias Archethic.ContractRegistry alias Archethic.Contracts.Contract - alias Archethic.Contracts.Contract.Constants - alias Archethic.Contracts.Contract.Trigger + alias Archethic.Contracts.ContractConstants, as: Constants alias Archethic.Contracts.Loader alias Archethic.Contracts.Worker alias Archethic.ContractSupervisor @@ -55,7 +54,7 @@ defmodule Archethic.Contracts.LoaderTest do assert %{ contract: %Contract{ - triggers: [%Trigger{type: :transaction}], + triggers: %{transaction: _}, constants: %Constants{contract: %{"address" => ^contract_address}} } } = :sys.get_state(pid) @@ -164,7 +163,7 @@ defmodule Archethic.Contracts.LoaderTest do assert %{ contract: %Contract{ - triggers: [%Trigger{type: :transaction}], + triggers: %{transaction: _}, constants: %Constants{contract: %{"address" => ^contract_address}} } } = :sys.get_state(pid) diff --git a/test/archethic/contracts/worker_test.exs b/test/archethic/contracts/worker_test.exs index 660e9d2227..767221b548 100644 --- a/test/archethic/contracts/worker_test.exs +++ b/test/archethic/contracts/worker_test.exs @@ -4,7 +4,7 @@ defmodule Archethic.Contracts.WorkerTest do alias Archethic.Account alias Archethic.Contracts.Contract - alias Archethic.Contracts.Contract.Constants + alias Archethic.Contracts.ContractConstants, as: Constants alias Archethic.Contracts.Interpreter @@ -192,9 +192,10 @@ defmodule Archethic.Contracts.WorkerTest do | constants: %Constants{contract: Map.put(constants, "code", code)} } |> Map.update!(:triggers, fn triggers -> - Enum.map(triggers, fn trigger -> - %{trigger | opts: Keyword.put(trigger.opts, :enable_seconds, true)} + Enum.map(triggers, fn {{:interval, interval}, code} -> + {{:interval, {interval, :second}}, code} end) + |> Enum.into(%{}) end) {:ok, _pid} = Worker.start_link(contract) diff --git a/test/archethic/contracts_test.exs b/test/archethic/contracts_test.exs index e20df4ff67..6050c5e4fd 100644 --- a/test/archethic/contracts_test.exs +++ b/test/archethic/contracts_test.exs @@ -4,9 +4,8 @@ defmodule Archethic.ContractsTest do alias Archethic.Contracts alias Archethic.Contracts.Contract - alias Archethic.Contracts.Contract.Conditions - alias Archethic.Contracts.Contract.Constants - alias Archethic.Contracts.Contract.Trigger + alias Archethic.Contracts.ContractConditions, as: Conditions + alias Archethic.Contracts.ContractConstants, as: Constants alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData @@ -222,7 +221,7 @@ defmodule Archethic.ContractsTest do uco_transfers: %{ "3265CCD78CD74984FAB3CC6984D30C8C82044EBBAB1A4FFFB683BDB2D8C5BCF9" => 1000000000} ] - actions triggered_by: interval, at: "0 * * * * *" do + actions triggered_by: interval, at: "0 * * * *" do add_uco_transfer to: \"3265CCD78CD74984FAB3CC6984D30C8C82044EBBAB1A4FFFB683BDB2D8C5BCF9\", amount: 1000000000 end """ @@ -233,8 +232,6 @@ defmodule Archethic.ContractsTest do } } - {:ok, time} = DateTime.new(~D[2016-05-24], ~T[13:26:20], "Etc/UTC") - next_tx = %Transaction{ data: %TransactionData{ code: code, @@ -253,7 +250,8 @@ defmodule Archethic.ContractsTest do } } - assert false == Contracts.accept_new_contract?(previous_tx, next_tx, time) + assert false == + Contracts.accept_new_contract?(previous_tx, next_tx, ~U[2016-05-24 13:26:20Z]) end test "should return false when the transaction have been triggered by interval and the timestamp does match " do @@ -262,7 +260,7 @@ defmodule Archethic.ContractsTest do uco_transfers: %{ "3265CCD78CD74984FAB3CC6984D30C8C82044EBBAB1A4FFFB683BDB2D8C5BCF9" => 1000000000} ] - actions triggered_by: interval, at: "0 * * * * *" do + actions triggered_by: interval, at: "0 * * * *" do add_uco_transfer to: \"3265CCD78CD74984FAB3CC6984D30C8C82044EBBAB1A4FFFB683BDB2D8C5BCF9\", amount: 1000000000 end """ @@ -273,8 +271,6 @@ defmodule Archethic.ContractsTest do } } - {:ok, time} = DateTime.new(~D[2016-05-24], ~T[13:26:00.000999], "Etc/UTC") - next_tx = %Transaction{ data: %TransactionData{ code: code, @@ -293,7 +289,8 @@ defmodule Archethic.ContractsTest do } } - assert true == Contracts.accept_new_contract?(previous_tx, next_tx, time) + assert true == + Contracts.accept_new_contract?(previous_tx, next_tx, ~U[2016-05-24 13:00:00Z]) end test "should return true when the inherit constraint match and when no trigger is specified" do