From 8725d25143b674cf6a4cd27357b8abfd8dd96f59 Mon Sep 17 00:00:00 2001 From: Yan Guiborat Date: Fri, 31 Mar 2023 16:41:29 +0200 Subject: [PATCH] Pr 57 validate smart contract calls (#802) * Validate Smart Contracts implementation before the SC merge * prettify contract error * simulate contract function return the next_transaction * add more unit tests * better spec * move simulate into interpreter and refactor the worker * use existing TaskSupervisor * LINT: split Archethic.parse_and_execute_contract_at into 2 functions * LINT: refactor worker for explicit workflow * LINT: split execute_trigger_and_validate_inherit_condition into 2 functions * add an error when missing recipients * simulate contract will fetch the latest contract of the chain * LINT: use get_last_transaction/1 --------- Co-authored-by: Bastien CHAMAGNE --- lib/archethic.ex | 35 +- lib/archethic/contracts.ex | 129 +----- lib/archethic/contracts/interpreter.ex | 330 +++++++++++++- .../interpreter/condition_validator.ex | 8 +- .../legacy/condition_interpreter.ex | 3 +- lib/archethic/contracts/worker.ex | 305 ++++--------- .../controllers/api/transaction_controller.ex | 150 +++++++ lib/archethic_web/explorer_router.ex | 6 + test/archethic/contracts/interpreter_test.exs | 268 +++++++++++ test/archethic/contracts/worker_test.exs | 16 + test/archethic/contracts_test.exs | 2 + .../transaction_chain/transaction_test.exs | 1 - .../api/transaction_controller_test.exs | 416 ++++++++++++++++++ 13 files changed, 1318 insertions(+), 351 deletions(-) diff --git a/lib/archethic.ex b/lib/archethic.ex index d94ee27b9..909e4c634 100644 --- a/lib/archethic.ex +++ b/lib/archethic.ex @@ -6,12 +6,18 @@ defmodule Archethic do alias Archethic.SharedSecrets alias __MODULE__.{Account, BeaconChain, Crypto, Election, P2P, P2P.Node, P2P.Message} alias __MODULE__.{SelfRepair, TransactionChain} + alias __MODULE__.Contracts.Interpreter + alias __MODULE__.Contracts.Contract alias Message.{NewTransaction, NotFound, StartMining} alias Message.{Balance, GetBalance, GetTransactionSummary} alias Message.{StartMining, Ok, TransactionSummaryMessage} - alias TransactionChain.{Transaction, TransactionInput, TransactionSummary} + alias TransactionChain.{ + Transaction, + TransactionInput, + TransactionSummary + } require Logger @@ -302,6 +308,33 @@ defmodule Archethic do end end + @doc """ + Parse the given transaction and return a contract if successful + """ + @spec parse_contract(Transaction.t()) :: {:ok, Contract.t()} | {:error, String.t()} + defdelegate parse_contract(contract_tx), + to: Interpreter, + as: :parse_transaction + + @doc """ + Execute the contract in the given transaction. + We assume the contract is parse-able. + """ + @spec execute_contract( + Contract.trigger_type(), + Contract.t(), + nil | Transaction.t() + ) :: + {:ok, nil | Transaction.t()} + | {:error, + :invalid_triggers_execution + | :invalid_transaction_constraints + | :invalid_oracle_constraints + | :invalid_inherit_constraints} + defdelegate execute_contract(trigger_type, contract, maybe_tx), + to: Interpreter, + as: :execute + @doc """ Retrieve the number of transaction in a transaction chain from the closest nodes """ diff --git a/lib/archethic/contracts.ex b/lib/archethic/contracts.ex index 4a593b41f..c9152fb79 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -5,7 +5,6 @@ defmodule Archethic.Contracts do """ alias __MODULE__.Contract - alias __MODULE__.ContractConditions, as: Conditions alias __MODULE__.ContractConstants, as: Constants alias __MODULE__.Interpreter alias __MODULE__.Loader @@ -18,134 +17,18 @@ defmodule Archethic.Contracts do alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData + alias Archethic.Contracts.ContractConstants, as: Constants + require Logger @extended_mode? Mix.env() != :prod - @doc ~S""" - Parse a smart contract code and return its representation - - ## Examples - - iex> " - ...> condition inherit: [ - ...> content: regex_match?(\"^(Mr.X: ){1}([0-9]+), (Mr.Y: ){1}([0-9])+$\"), - ...> origin_family: biometric - ...> ] - ...> - ...> actions triggered_by: datetime, at: 1601039923 do - ...> set_type hosting - ...> set_content \"Mr.X: 10, Mr.Y: 8\" - ...> end - ...> " - ...> |> Contracts.parse() - { - :ok, - %Archethic.Contracts.Contract{ - conditions: %{ - inherit: %Archethic.Contracts.ContractConditions{ - address: nil, - authorized_keys: nil, - code: nil, - content: { - :==, - [line: 2], - [true, {{:., [line: 2], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.Legacy.Library], [:Library]}, :regex_match?]}, [line: 2], [{:get_in, [line: 2], [{:scope, [line: 2], nil}, ["next", "content"]]}, "^(Mr.X: ){1}([0-9]+), (Mr.Y: ){1}([0-9])+$"]}] - }, - origin_family: :biometric, - previous_public_key: nil, - secrets: nil, - timestamp: nil, - token_transfers: nil, - type: nil, - uco_transfers: nil - }, - oracle: %Archethic.Contracts.ContractConditions{address: nil, authorized_keys: nil, code: nil, content: nil, origin_family: :all, previous_public_key: nil, secrets: nil, timestamp: nil, token_transfers: nil, type: nil, uco_transfers: nil}, - transaction: %Archethic.Contracts.ContractConditions{address: nil, authorized_keys: nil, code: nil, content: nil, origin_family: :all, previous_public_key: nil, secrets: nil, timestamp: nil, token_transfers: nil, type: nil, uco_transfers: nil} - }, - constants: %Archethic.Contracts.ContractConstants{contract: nil, transaction: nil}, - next_transaction: %Archethic.TransactionChain.Transaction{ - address: nil, - cross_validation_stamps: [], - data: %Archethic.TransactionChain.TransactionData{ - code: "", - content: "", - ledger: %Archethic.TransactionChain.TransactionData.Ledger{token: %Archethic.TransactionChain.TransactionData.TokenLedger{transfers: []}, uco: %Archethic.TransactionChain.TransactionData.UCOLedger{transfers: []}}, - ownerships: [], - recipients: [] - }, - origin_signature: nil, - previous_public_key: nil, - previous_signature: nil, - type: nil, - validation_stamp: nil, - version: 1 - }, - triggers: %{ - {:datetime, ~U[2020-09-25 13:18:43Z]} => { - :__block__, - [], - [ - { - :__block__, - [], - [ - { - :=, - [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.Legacy.TransactionStatements], [:TransactionStatements]}, :set_type]}, [line: 7], [{:&, [line: 7], [1]}, "hosting"]}]}]}] - }, - {{:., [], [{:__aliases__, [alias: false], [:Function]}, :identity]}, [], [{:scope, [], nil}]} - ] - }, - { - :__block__, - [], - [ - { - :=, - [line: 8], - [{:scope, [line: 8], nil}, {:update_in, [line: 8], [{:scope, [line: 8], nil}, ["next_transaction"], {:&, [line: 8], [{{:., [line: 8], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.Legacy.TransactionStatements], [:TransactionStatements]}, :set_content]}, [line: 8], [{:&, [line: 8], [1]}, "Mr.X: 10, Mr.Y: 8"]}]}]}] - }, - {{:., [], [{:__aliases__, [alias: false], [:Function]}, :identity]}, [], [{:scope, [], nil}]} - ] - } - ] - } - }, - version: 0 - } - } + @doc """ + Parse a smart contract code and return a contract struct """ @spec parse(binary()) :: {:ok, Contract.t()} | {:error, binary()} - def parse(contract_code) when is_binary(contract_code) do - start = System.monotonic_time() - - case Interpreter.parse(contract_code) do - {:ok, - contract = %Contract{ - triggers: triggers, - conditions: %{transaction: transaction_conditions, oracle: oracle_conditions} - }} -> - :telemetry.execute([:archethic, :contract, :parsing], %{ - duration: System.monotonic_time() - start - }) - - cond do - Map.has_key?(triggers, :transaction) and Conditions.empty?(transaction_conditions) -> - {:error, "missing transaction conditions"} - - Map.has_key?(triggers, :oracle) and Conditions.empty?(oracle_conditions) -> - {:error, "missing oracle conditions"} - - true -> - {:ok, contract} - end - - {:error, _} = e -> - e - end - end + defdelegate parse(contract_code), + to: Interpreter @doc """ Same a `parse/1` but raise if the contract is not valid diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index fb5503279..5e39faef6 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -10,26 +10,61 @@ defmodule Archethic.Contracts.Interpreter do alias Archethic.Contracts.Contract alias Archethic.Contracts.ContractConditions, as: Conditions + alias Archethic.Contracts.ContractConstants, as: Constants alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData @type version() :: integer() + @type execute_opts :: [skip_inherit_check?: boolean()] @doc """ Dispatch through the correct interpreter. This return a filled contract structure or an human-readable error. """ @spec parse(code :: binary()) :: {:ok, Contract.t()} | {:error, String.t()} + def parse(""), do: {:error, "Not a contract"} + def parse(code) when is_binary(code) do - case sanitize_code(code) do - {:ok, block} -> - case block do - {:__block__, [], [{:@, _, [{{:atom, "version"}, _, [version]}]} | rest]} -> - parse_contract(version, rest) - - _ -> - Legacy.parse(block) - end + start = System.monotonic_time() + + result = + case sanitize_code(code) do + {:ok, block} -> + case block do + {:__block__, [], [{:@, _, [{{:atom, "version"}, _, [version]}]} | rest]} -> + parse_contract(version, rest) + |> check_contract_blocks() + + _ -> + Legacy.parse(block) + |> check_contract_blocks() + end + + {:error, {[line: line, column: column], _msg_info, _token}} -> + {:error, "Parse error at line #{line} column #{column}"} + end + + :telemetry.execute([:archethic, :contract, :parsing], %{ + duration: System.monotonic_time() - start + }) + + result + end + + @doc """ + Parse a transaction and return a contract. + This return a filled contract structure or an human-readable error. + """ + @spec parse_transaction(Transaction.t()) :: {:ok, Contract.t()} | {:error, String.t()} + def parse_transaction(contract_tx = %Transaction{data: %TransactionData{code: code}}) do + case parse(code) do + {:ok, contract} -> + {:ok, + %Contract{ + contract + | constants: %Constants{contract: Constants.from_transaction(contract_tx)} + }} {:error, reason} -> {:error, reason} @@ -63,15 +98,60 @@ defmodule Archethic.Contracts.Interpreter do Execute the trigger/action code. May return a new transaction or nil """ - @spec execute_trigger(version(), Macro.t(), map()) :: Transaction.t() | nil - def execute_trigger(0, ast, constants) do + @spec execute_trigger_code(version(), Macro.t(), map()) :: Transaction.t() | nil + def execute_trigger_code(0, ast, constants) do Legacy.execute_trigger(ast, constants) end - def execute_trigger(1, ast, constants) do + def execute_trigger_code(1, ast, constants) do ActionInterpreter.execute(ast, constants) end + @doc """ + Execution the given contract's trigger. + Checks all conditions block. + + `maybe_tx` must be + - the incoming transaction when trigger_type: transaction + - the oracle transaction when trigger_type: oracle + - nil for the other trigger_types + + /!\ The transaction returned is not complete, only the `type` and `data` are filled-in. + """ + @spec execute( + Contract.trigger_type(), + Contract.t(), + nil | Transaction.t(), + execute_opts() + ) :: + {:ok, nil | Transaction.t()} + | {:error, + :invalid_triggers_execution + | :invalid_transaction_constraints + | :invalid_oracle_constraints + | :invalid_inherit_constraints} + def execute( + trigger_type, + contract = %Contract{triggers: triggers}, + maybe_tx, + opts \\ [] + ) do + case triggers[trigger_type] do + nil -> + {:error, :invalid_triggers_execution} + + trigger_code -> + do_execute( + trigger_type, + trigger_code, + contract, + maybe_tx, + contract, + opts + ) + end + end + @doc """ Format an error message from the failing ast node @@ -136,6 +216,205 @@ defmodule Archethic.Contracts.Interpreter do # | .__/|_| |_| \_/ \__,_|\__\___| # |_| # ------------------------------------------------------------ + + defp do_execute( + :transaction, + trigger_code, + contract, + incoming_tx = %Transaction{}, + %Contract{ + version: version, + conditions: conditions, + constants: %Constants{ + contract: contract_constants + } + }, + opts + ) do + constants = %{ + "transaction" => Constants.from_transaction(incoming_tx), + "contract" => contract_constants + } + + if valid_conditions?(version, conditions.transaction, constants) do + case execute_trigger(version, trigger_code, contract, incoming_tx) do + nil -> + {:ok, nil} + + next_tx -> + if valid_inherit_condition?(contract, next_tx, opts) do + {:ok, next_tx} + else + {:error, :invalid_inherit_constraints} + end + end + else + {:error, :invalid_transaction_constraints} + end + end + + defp do_execute( + :oracle, + trigger_code, + contract, + oracle_tx = %Transaction{}, + %Contract{ + version: version, + conditions: conditions, + constants: %Constants{ + contract: contract_constants + } + }, + opts + ) do + constants = %{ + "transaction" => Constants.from_transaction(oracle_tx), + "contract" => contract_constants + } + + if valid_conditions?(version, conditions.oracle, constants) do + case execute_trigger(version, trigger_code, contract, oracle_tx) do + nil -> + {:ok, nil} + + next_tx -> + if valid_inherit_condition?(contract, next_tx, opts) do + {:ok, next_tx} + else + {:error, :invalid_inherit_constraints} + end + end + else + {:error, :invalid_oracle_constraints} + end + end + + defp do_execute( + _trigger_type, + trigger_code, + contract, + nil, + %Contract{version: version}, + opts + ) do + case execute_trigger(version, trigger_code, contract) do + nil -> + {:ok, nil} + + next_tx -> + if valid_inherit_condition?(contract, next_tx, opts) do + {:ok, next_tx} + else + {:error, :invalid_inherit_constraints} + end + end + end + + defp execute_trigger( + version, + trigger_code, + contract, + maybe_tx \\ nil + ) do + constants_trigger = %{ + "transaction" => + case maybe_tx do + nil -> nil + tx -> Constants.from_transaction(tx) + end, + "contract" => contract.constants.contract + } + + case execute_trigger_code(version, trigger_code, constants_trigger) do + nil -> + # contract did not produce a next_tx + nil + + next_tx_to_prepare -> + # contract produce a next_tx but we need to feed previous values to it + chain_transaction( + Constants.to_transaction(contract.constants.contract), + next_tx_to_prepare + ) + end + end + + defp valid_inherit_condition?( + %Contract{ + version: version, + conditions: %{inherit: condition_inherit}, + constants: %{contract: contract_constants} + }, + next_tx, + opts + ) do + if Keyword.get(opts, :skip_inherit_check?, false) do + true + else + constants_inherit = %{ + "previous" => contract_constants, + "next" => Constants.from_transaction(next_tx) + } + + valid_conditions?(version, condition_inherit, constants_inherit) + end + end + + # ----------------------------------------- + # chain next tx + # ----------------------------------------- + defp chain_transaction(previous_transaction, next_transaction) do + %{next_transaction: next_tx} = + %{next_transaction: next_transaction, previous_transaction: previous_transaction} + |> chain_type() + |> chain_code() + |> chain_ownerships() + + next_tx + end + + defp chain_type( + acc = %{ + next_transaction: %Transaction{type: nil}, + previous_transaction: _ + } + ) do + put_in(acc, [:next_transaction, Access.key(:type)], :contract) + end + + defp chain_type(acc), do: acc + + defp chain_code( + acc = %{ + next_transaction: %Transaction{data: %TransactionData{code: ""}}, + previous_transaction: %Transaction{data: %TransactionData{code: previous_code}} + } + ) do + put_in(acc, [:next_transaction, Access.key(:data, %{}), Access.key(:code)], previous_code) + end + + defp chain_code(acc), do: acc + + defp chain_ownerships( + acc = %{ + next_transaction: %Transaction{data: %TransactionData{ownerships: []}}, + previous_transaction: %Transaction{ + data: %TransactionData{ownerships: previous_ownerships} + } + } + ) do + put_in( + acc, + [:next_transaction, Access.key(:data, %{}), Access.key(:ownerships)], + previous_ownerships + ) + end + + defp chain_ownerships(acc), do: acc + + # ----------------------------------------- + # format errors + # ----------------------------------------- defp do_format_error_reason(message, cause, metadata) do message = prepare_message(message) @@ -156,6 +435,9 @@ defmodule Archethic.Contracts.Interpreter do defp metadata_to_string(line: line), do: "L#{line}" defp metadata_to_string(_), do: "" + # ----------------------------------------- + # parsing + # ----------------------------------------- defp atom_encoder(atom, _) do if atom in ["if"] do {:ok, String.to_atom(atom)} @@ -211,4 +493,28 @@ defmodule Archethic.Contracts.Interpreter do end defp parse_ast(ast, _), do: {:error, ast, "unexpected term"} + + # ----------------------------------------- + # contract validation + # ----------------------------------------- + + defp check_contract_blocks({:error, reason}), do: {:error, reason} + + defp check_contract_blocks( + {:ok, contract = %Contract{triggers: triggers, conditions: conditions}} + ) do + cond do + Map.has_key?(triggers, :transaction) and !Map.has_key?(conditions, :transaction) -> + {:error, "missing transaction conditions"} + + Map.has_key?(triggers, :oracle) and !Map.has_key?(conditions, :oracle) -> + {:error, "missing oracle conditions"} + + !Map.has_key?(conditions, :inherit) -> + {:error, "missing inherit conditions"} + + true -> + {:ok, contract} + end + end end diff --git a/lib/archethic/contracts/interpreter/condition_validator.ex b/lib/archethic/contracts/interpreter/condition_validator.ex index 05e997f8a..d01792693 100644 --- a/lib/archethic/contracts/interpreter/condition_validator.ex +++ b/lib/archethic/contracts/interpreter/condition_validator.ex @@ -78,9 +78,11 @@ defmodule Archethic.Contracts.Interpreter.ConditionValidator do {"timestamp", true} end - defp validate_condition({"type", nil}, %{"next" => %{"type" => "transfer"}}) do - # Skip the verification when it's the default type - {"type", true} + defp validate_condition({"type", nil}, %{ + "previous" => %{"type" => previous_type}, + "next" => %{"type" => next_type} + }) do + {"type", previous_type == next_type} end defp validate_condition({"content", nil}, %{"next" => %{"content" => ""}}) do diff --git a/lib/archethic/contracts/interpreter/legacy/condition_interpreter.ex b/lib/archethic/contracts/interpreter/legacy/condition_interpreter.ex index 192039037..ee4cd2bb5 100644 --- a/lib/archethic/contracts/interpreter/legacy/condition_interpreter.ex +++ b/lib/archethic/contracts/interpreter/legacy/condition_interpreter.ex @@ -556,7 +556,8 @@ defmodule Archethic.Contracts.Interpreter.Legacy.ConditionInterpreter do {"timestamp", true} end - defp validate_condition({"type", nil}, %{"next" => %{"type" => "transfer"}}) do + defp validate_condition({"type", nil}, %{"next" => %{"type" => type}}) + when type in ["transfer", "contract"] do # Skip the verification when it's the default type {"type", true} end diff --git a/lib/archethic/contracts/worker.ex b/lib/archethic/contracts/worker.ex index 8c4d075d1..ae5614ca8 100644 --- a/lib/archethic/contracts/worker.ex +++ b/lib/archethic/contracts/worker.ex @@ -5,7 +5,6 @@ defmodule Archethic.Contracts.Worker do alias Archethic.ContractRegistry alias Archethic.Contracts.Contract - alias Archethic.Contracts.ContractConditions, as: Conditions alias Archethic.Contracts.ContractConstants, as: Constants alias Archethic.Contracts.Interpreter @@ -75,190 +74,103 @@ defmodule Archethic.Contracts.Worker do {:noreply, new_state} end + # TRIGGER: TRANSACTION def handle_cast( {:execute, incoming_tx = %Transaction{}}, - state = %{ - contract: %Contract{ - version: version, - triggers: triggers, - constants: %Constants{ - contract: contract_constants = %{"address" => contract_address} - }, - conditions: %{transaction: transaction_condition} - } - } + state = %{contract: contract} ) do - Logger.info("Execute contract transaction actions", - transaction_address: Base.encode16(incoming_tx.address), - transaction_type: incoming_tx.type, - contract: Base.encode16(contract_address) - ) - - if Map.has_key?(triggers, :transaction) do - constants = %{ - "contract" => contract_constants, - "transaction" => Constants.from_transaction(incoming_tx) - } - - contract_transaction = Constants.to_transaction(contract_constants) - - with true <- Interpreter.valid_conditions?(version, transaction_condition, constants), - next_tx = %Transaction{} <- - Interpreter.execute_trigger(version, Map.fetch!(triggers, :transaction), constants), - {:ok, next_tx} <- chain_transaction(next_tx, contract_transaction) do - handle_new_transaction(next_tx) - {:noreply, state} - else - nil -> - # returned by Interpreter.execute_trigger when contract did not create a next tx - {:noreply, state} - - false -> - Logger.debug("Incoming transaction didn't match the condition", - transaction_address: Base.encode16(incoming_tx.address), - transaction_type: incoming_tx.type, - contract: Base.encode16(contract_address) - ) - - {:noreply, state} - - {:error, :transaction_seed_decryption} -> - {:noreply, state} - end + contract_tx = Constants.to_transaction(contract.constants.contract) + + meta = log_metadata(contract_tx, incoming_tx) + Logger.debug("Contract execution started", meta) + + with true <- enough_funds?(contract_tx.address), + {:ok, next_tx = %Transaction{}} <- + Interpreter.execute(:transaction, contract, incoming_tx, skip_inherit_check?: true), + {:ok, next_tx} <- chain_transaction(next_tx, contract_tx), + :ok <- ensure_enough_funds(next_tx, contract_tx.address), + :ok <- handle_new_transaction(next_tx) do + Logger.debug("Contract execution success", meta) else - Logger.debug("No transaction trigger", - transaction_address: Base.encode16(incoming_tx.address), - transaction_type: incoming_tx.type, - contract: Base.encode16(contract_address) - ) - - {:noreply, state} + _ -> + Logger.debug("Contract execution failed", meta) end + + {:noreply, state} end + # TRIGGER: DATETIME def handle_info( - {:trigger, {:datetime, timestamp}}, - state = %{ - contract: %Contract{ - version: version, - triggers: triggers, - constants: %Constants{contract: contract_constants = %{"address" => address}} - } - } + {:trigger, trigger_type = {:datetime, _}}, + state = %{contract: contract} ) do - Logger.info("Execute contract datetime trigger actions", - contract: Base.encode16(address) - ) - - constants = %{ - "contract" => contract_constants - } - - contract_tx = Constants.to_transaction(contract_constants) - - with next_tx = %Transaction{} <- - Interpreter.execute_trigger( - version, - Map.fetch!(triggers, {:datetime, timestamp}), - constants - ), - {:ok, next_tx} <- chain_transaction(next_tx, contract_tx) do - handle_new_transaction(next_tx) + contract_tx = Constants.to_transaction(contract.constants.contract) + + meta = log_metadata(contract_tx) + Logger.debug("Contract execution started", meta) + + with true <- enough_funds?(contract_tx.address), + {:ok, next_tx = %Transaction{}} <- + Interpreter.execute(trigger_type, contract, nil, skip_inherit_check?: true), + {:ok, next_tx} <- chain_transaction(next_tx, contract_tx), + :ok <- ensure_enough_funds(next_tx, contract_tx.address), + :ok <- handle_new_transaction(next_tx) do + Logger.debug("Contract execution success", meta) + else + _ -> + Logger.debug("Contract execution failed", meta) end - {:noreply, Map.update!(state, :timers, &Map.delete(&1, {:datetime, timestamp}))} + {:noreply, Map.update!(state, :timers, &Map.delete(&1, trigger_type))} end + # TRIGGER: INTERVAL def handle_info( - {:trigger, {:interval, interval}}, - state = %{ - contract: %Contract{ - version: version, - triggers: triggers, - constants: %Constants{ - contract: contract_constants = %{"address" => address} - } - } - } + {:trigger, trigger_type = {:interval, interval}}, + state = %{contract: contract} ) do - Logger.info("Execute contract interval trigger actions", - contract: Base.encode16(address) - ) - - # Schedule the next interval trigger - interval_timer = schedule_trigger({:interval, interval}) - - constants = %{ - "contract" => contract_constants - } - - contract_transaction = Constants.to_transaction(contract_constants) - - with true <- enough_funds?(address), - next_tx = %Transaction{} <- - Interpreter.execute_trigger( - version, - 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) + contract_tx = Constants.to_transaction(contract.constants.contract) + + meta = log_metadata(contract_tx) + Logger.debug("Contract execution started", meta) + + with true <- enough_funds?(contract_tx.address), + {:ok, next_tx = %Transaction{}} <- + Interpreter.execute(trigger_type, contract, nil, skip_inherit_check?: true), + {:ok, next_tx} <- chain_transaction(next_tx, contract_tx), + :ok <- ensure_enough_funds(next_tx, contract_tx.address), + :ok <- handle_new_transaction(next_tx) do + Logger.debug("Contract execution success", meta) + else + _ -> + Logger.debug("Contract execution failed", meta) end + interval_timer = schedule_trigger({:interval, interval}) {:noreply, put_in(state, [:timers, :interval], interval_timer)} end + # TRIGGER: ORACLE def handle_info( {:new_transaction, tx_address, :oracle, _timestamp}, - state = %{ - contract: %Contract{ - version: version, - triggers: triggers, - constants: %Constants{contract: contract_constants = %{"address" => address}}, - conditions: %{oracle: oracle_condition} - } - } + state = %{contract: contract} ) do - Logger.info("Execute contract oracle trigger actions", contract: Base.encode16(address)) - - with true <- enough_funds?(address), - true <- Map.has_key?(triggers, :oracle) do - {:ok, tx} = TransactionChain.get_transaction(tx_address) - - constants = %{ - "contract" => contract_constants, - "transaction" => Constants.from_transaction(tx) - } - - contract_transaction = Constants.to_transaction(contract_constants) - - if Conditions.empty?(oracle_condition) do - with next_tx = %Transaction{} <- - Interpreter.execute_trigger(version, 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?(version, oracle_condition, constants), - next_tx = %Transaction{} <- - Interpreter.execute_trigger(version, Map.fetch!(triggers, :oracle), constants), - {:ok, next_tx} <- chain_transaction(next_tx, contract_transaction), - :ok <- ensure_enough_funds(next_tx, address) do - handle_new_transaction(next_tx) - else - nil -> - # returned by Interpreter.execute_trigger when contract did not create a next tx - :ok - - false -> - Logger.info("Invalid oracle conditions", contract: Base.encode16(address)) - - {:error, e} -> - Logger.info("#{inspect(e)}", contract: Base.encode16(address)) - end - end + contract_tx = Constants.to_transaction(contract.constants.contract) + {:ok, oracle_tx} = TransactionChain.get_transaction(tx_address) + + meta = log_metadata(contract_tx, oracle_tx) + Logger.debug("Contract execution started", meta) + + with true <- enough_funds?(contract_tx.address), + {:ok, next_tx = %Transaction{}} <- + Interpreter.execute(:oracle, contract, oracle_tx, skip_inherit_check?: true), + {:ok, next_tx} <- chain_transaction(next_tx, contract_tx), + :ok <- ensure_enough_funds(next_tx, contract_tx.address), + :ok <- handle_new_transaction(next_tx) do + Logger.debug("Contract execution success", meta) + else + _ -> + Logger.debug("Contract execution failed", meta) end {:noreply, state} @@ -268,6 +180,7 @@ defmodule Archethic.Contracts.Worker do {:noreply, state} end + # ---------------------------------------------- defp via_tuple(address) do {:via, Registry, {ContractRegistry, address}} end @@ -330,18 +243,15 @@ defmodule Archethic.Contracts.Worker do end defp chain_transaction( - next_tx, + _next_tx = %Transaction{ + type: new_type, + data: new_data + }, prev_tx = %Transaction{ address: address, previous_public_key: previous_public_key } ) do - %{next_transaction: %Transaction{type: new_type, data: new_data}} = - %{next_transaction: next_tx, previous_transaction: prev_tx} - |> chain_type() - |> chain_code() - |> chain_ownerships() - case get_transaction_seed(prev_tx) do {:ok, transaction_seed} -> length = TransactionChain.size(address) @@ -357,7 +267,7 @@ defmodule Archethic.Contracts.Worker do _ -> Logger.info("Cannot decrypt the transaction seed", contract: Base.encode16(address)) - {:error, :transaction_seed_decryption} + :error end end @@ -380,45 +290,6 @@ defmodule Archethic.Contracts.Worker do end end - defp chain_type( - acc = %{ - next_transaction: %Transaction{type: nil}, - previous_transaction: _ - } - ) do - put_in(acc, [:next_transaction, Access.key(:type)], :contract) - end - - defp chain_type(acc), do: acc - - defp chain_code( - acc = %{ - next_transaction: %Transaction{data: %TransactionData{code: ""}}, - previous_transaction: %Transaction{data: %TransactionData{code: previous_code}} - } - ) do - put_in(acc, [:next_transaction, Access.key(:data, %{}), Access.key(:code)], previous_code) - end - - defp chain_code(acc), do: acc - - defp chain_ownerships( - acc = %{ - next_transaction: %Transaction{data: %TransactionData{ownerships: []}}, - previous_transaction: %Transaction{ - data: %TransactionData{ownerships: previous_ownerships} - } - } - ) do - put_in( - acc, - [:next_transaction, Access.key(:data, %{}), Access.key(:ownerships)], - previous_ownerships - ) - end - - defp chain_ownerships(acc), do: acc - defp enough_funds?(contract_address) do case Account.get_balance(contract_address) do %{uco: uco_balance} when uco_balance > 0 -> @@ -469,7 +340,7 @@ defmodule Archethic.Contracts.Worker do f_token_address == t_token_address and f_token_id == t_token_id end) - balance > t_amount + balance >= t_amount end) do :ok else @@ -482,4 +353,18 @@ defmodule Archethic.Contracts.Worker do {:error, :not_enough_funds} end end + + defp log_metadata(contract_tx), do: log_metadata(contract_tx, nil) + + defp log_metadata(contract_tx, nil) do + [contract: Base.encode16(contract_tx.address)] + end + + defp log_metadata(contract_tx, %Transaction{type: type, address: address}) do + [ + transaction_address: Base.encode16(address), + transaction_type: type, + contract: Base.encode16(contract_tx.address) + ] + end end diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index 40a610335..b8d2da34a 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -143,4 +143,154 @@ defmodule ArchethicWeb.API.TransactionController do |> render("400.json", changeset: changeset) end end + + @doc """ + This controller, Fetch the recipients contract and simulate the transaction, managing possible + exits from contract execution + """ + def simulate_contract_execution( + conn, + params = %{} + ) do + case TransactionPayload.changeset(params) do + changeset = %{valid?: true} -> + tx = + %Transaction{data: %TransactionData{recipients: recipients}} = + changeset + |> TransactionPayload.to_map() + |> Transaction.cast() + + results = + Task.Supervisor.async_stream_nolink( + Archethic.TaskSupervisor, + recipients, + &fetch_recipient_tx_and_simulate(&1, tx), + on_timeout: :kill_task, + timeout: 5000 + ) + |> Stream.zip(recipients) + |> Stream.map(fn + {{:ok, :ok}, recipient} -> + %{ + "valid" => true, + "recipient_address" => Base.encode16(recipient) + } + + {{:ok, {:error, reason}}, recipient} -> + %{ + "valid" => false, + "reason" => reason, + "recipient_address" => Base.encode16(recipient) + } + + {{:exit, :timeout}, recipient} -> + %{ + "valid" => false, + "reason" => "A contract timed out", + "recipient_address" => Base.encode16(recipient) + } + + {{:exit, reason}, recipient} -> + %{ + "valid" => false, + "reason" => format_exit_reason(reason), + "recipient_address" => Base.encode16(recipient) + } + end) + |> Enum.to_list() + + case results do + [] -> + conn + |> put_status(:ok) + |> json([ + %{"valid" => false, "reason" => "There are no recipients in the transaction"} + ]) + + _ -> + conn + |> put_status(:ok) + |> json(results) + end + + changeset -> + error_details = + Ecto.Changeset.traverse_errors(changeset, &ArchethicWeb.ErrorHelpers.translate_error/1) + + json_body = + Map.merge(error_details, %{"valid" => false, "reason" => "Query validation failled"}) + + conn + |> put_status(:ok) + |> json([json_body]) + end + end + + defp fetch_recipient_tx_and_simulate(recipient_address, tx) do + with {:ok, contract_tx} <- Archethic.get_last_transaction(recipient_address), + {:ok, contract} <- Archethic.parse_contract(contract_tx), + {:ok, _} <- Archethic.execute_contract(:transaction, contract, tx) do + :ok + else + # search_transaction errors + {:error, :transaction_not_exists} -> + {:error, "There is no transaction at recipient address."} + + {:error, :transaction_invalid} -> + {:error, "The transaction is marked as invalid."} + + {:error, :network_issue} -> + {:error, "Network issue, please try again later."} + + # execute_contract errors + {:error, :invalid_triggers_execution} -> + {:error, "Contract does not have a `actions triggered_by: transaction` block."} + + {:error, :invalid_transaction_constraints} -> + {:error, + "Contract refused incoming transaction. Check the `condition transaction` block."} + + {:error, :invalid_oracle_constraints} -> + {:error, "Contract refused incoming transaction. Check the `condition oracle` block."} + + {:error, :invalid_inherit_constraints} -> + {:error, "Contract refused outcoming transaction. Check the `condition inherit` block."} + + # parse_contract errors + {:error, reason} when is_binary(reason) -> + {:error, reason} + end + end + + defp format_exit_reason({error, stacktrace}) do + formatted_error = + case error do + atom when is_atom(atom) -> + # ex: :badarith + inspect(atom) + + {atom, _} when is_atom(atom) -> + # ex: :badmatch + inspect(atom) + + _ -> + "unknown error" + end + + Enum.reduce_while( + stacktrace, + "A contract exited with error: #{formatted_error}", + fn + {:elixir_eval, _, _, [file: 'nofile', line: line]}, acc -> + {:halt, acc <> " (line: #{line})"} + + _, acc -> + {:cont, acc} + end + ) + end + + defp format_exit_reason(_) do + "A contract exited with an unknown error" + end end diff --git a/lib/archethic_web/explorer_router.ex b/lib/archethic_web/explorer_router.ex index 7c71e0f07..bf710ca2c 100644 --- a/lib/archethic_web/explorer_router.ex +++ b/lib/archethic_web/explorer_router.ex @@ -90,6 +90,12 @@ defmodule ArchethicWeb.ExplorerRouter do post("/transaction", ArchethicWeb.API.TransactionController, :new) post("/transaction_fee", ArchethicWeb.API.TransactionController, :transaction_fee) + post( + "/transaction/contract/simulator", + ArchethicWeb.API.TransactionController, + :simulate_contract_execution + ) + forward( "/graphiql", Absinthe.Plug.GraphiQL, diff --git a/test/archethic/contracts/interpreter_test.exs b/test/archethic/contracts/interpreter_test.exs index 9ded1dfc6..f75be3533 100644 --- a/test/archethic/contracts/interpreter_test.exs +++ b/test/archethic/contracts/interpreter_test.exs @@ -6,6 +6,9 @@ defmodule Archethic.Contracts.InterpreterTest do alias Archethic.Contracts.Interpreter alias Archethic.ContractFactory + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + doctest Interpreter describe "strict versionning" do @@ -104,4 +107,269 @@ defmodule Archethic.Contracts.InterpreterTest do |> Interpreter.parse() end end + + describe "execute/3" do + test "should return a transaction if the contract is correct and there was a Contract.* call" do + code = """ + @version 1 + condition inherit: [ + content: "hello" + ] + + condition transaction: [] + + actions triggered_by: transaction do + Contract.set_content "hello" + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{} + } + + assert {:ok, %Transaction{}} = + Interpreter.execute( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) + end + + test "should return nil when the contract is correct but no Contract.* call" do + code = """ + @version 1 + condition inherit: [ + content: "hello" + ] + + condition transaction: [] + + actions triggered_by: transaction do + if false do + Contract.set_content "hello" + end + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{} + } + + assert {:ok, nil} = + Interpreter.execute( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) + end + + test "should return inherit constraints error when condition inherit fails" do + code = """ + @version 1 + condition inherit: [ + content: "hello", + type: "data" + ] + + condition transaction: [] + + actions triggered_by: transaction do + Contract.set_content "hello" + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{} + } + + assert match?( + {:error, :invalid_inherit_constraints}, + Interpreter.execute( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) + ) + end + + test "should return transaction constraints error when condition inherit fails" do + code = """ + @version 1 + condition inherit: [ + content: true + ] + + condition transaction: [ + type: "data" + ] + + actions triggered_by: transaction do + Contract.set_content "hello" + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{} + } + + assert match?( + {:error, :invalid_transaction_constraints}, + Interpreter.execute( + :transaction, + Contract.from_transaction!(contract_tx), + incoming_tx + ) + ) + end + + test "should return oracle constraints error when condition oracle fails" do + code = """ + @version 1 + condition inherit: [ + content: true + ] + + condition oracle: [ + type: "oracle", + address: false + ] + + actions triggered_by: oracle do + Contract.set_content "hello" + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + oracle_tx = %Transaction{ + type: :oracle, + data: %TransactionData{} + } + + assert match?( + {:error, :invalid_oracle_constraints}, + Interpreter.execute( + :oracle, + Contract.from_transaction!(contract_tx), + oracle_tx + ) + ) + end + + test "should be able to simulate a trigger: datetime" do + code = """ + @version 1 + condition inherit: [ + content: "hello" + ] + + actions triggered_by: datetime, at: 1678984136 do + Contract.set_content "hello" + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + assert {:ok, %Transaction{}} = + Interpreter.execute( + {:datetime, ~U[2023-03-16 16:28:56Z]}, + Contract.from_transaction!(contract_tx), + nil + ) + end + + test "should be able to simulate a trigger: interval" do + code = """ + @version 1 + condition inherit: [ + content: "hello" + ] + + actions triggered_by: interval, at: "* * * * *" do + Contract.set_content "hello" + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + assert {:ok, %Transaction{}} = + Interpreter.execute( + {:interval, "* * * * *"}, + Contract.from_transaction!(contract_tx), + nil + ) + end + + test "should be able to simulate a trigger: oracle" do + code = """ + @version 1 + condition inherit: [ + content: "hello" + ] + + condition oracle: [] + + actions triggered_by: oracle do + Contract.set_content "hello" + end + """ + + contract_tx = %Transaction{ + type: :contract, + data: %TransactionData{ + code: code + } + } + + assert {:ok, %Transaction{}} = + Interpreter.execute(:oracle, Contract.from_transaction!(contract_tx), nil) + end + end end diff --git a/test/archethic/contracts/worker_test.exs b/test/archethic/contracts/worker_test.exs index 869ac2572..70dcb4fbf 100644 --- a/test/archethic/contracts/worker_test.exs +++ b/test/archethic/contracts/worker_test.exs @@ -246,6 +246,8 @@ defmodule Archethic.Contracts.WorkerTest do uco_transfers: %{ "#{address}" => 1_040_000_000} ] + condition transaction: [] + actions triggered_by: transaction do set_type transfer add_uco_transfer to: "#{address}", amount: 1_040_000_000 @@ -482,6 +484,20 @@ defmodule Archethic.Contracts.WorkerTest do test "ICO crowdsale", %{ constants: constants = %{"address" => contract_address} } do + # the contract need uco to be executed + Archethic.Account.MemTables.TokenLedger.add_unspent_output( + contract_address, + %VersionedUnspentOutput{ + unspent_output: %UnspentOutput{ + from: "@Bob3", + amount: 100_000_000 * 10_000, + type: {:token, contract_address, 0}, + timestamp: DateTime.utc_now() |> DateTime.truncate(:millisecond) + }, + protocol_version: ArchethicCase.current_protocol_version() + } + ) + code = """ # Ensure the next transaction will be a transfer diff --git a/test/archethic/contracts_test.exs b/test/archethic/contracts_test.exs index b0b077424..4296c2f41 100644 --- a/test/archethic/contracts_test.exs +++ b/test/archethic/contracts_test.exs @@ -90,6 +90,8 @@ defmodule Archethic.ContractsTest do type: transfer ] + condition transaction: [] + actions triggered_by: transaction do add_uco_transfer to: \"3265CCD78CD74984FAB3CC6984D30C8C82044EBBAB1A4FFFB683BDB2D8C5BCF9\", amount: 1000000000 set_content "hello" diff --git a/test/archethic/transaction_chain/transaction_test.exs b/test/archethic/transaction_chain/transaction_test.exs index 78d80c5ce..1b53ea6d6 100644 --- a/test/archethic/transaction_chain/transaction_test.exs +++ b/test/archethic/transaction_chain/transaction_test.exs @@ -5,7 +5,6 @@ defmodule Archethic.TransactionChain.TransactionTest do import ArchethicCase, only: [current_transaction_version: 0, current_protocol_version: 0] alias Archethic.Crypto - alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.Transaction.CrossValidationStamp # alias Archethic.TransactionChain.Transaction.ValidationStamp diff --git a/test/archethic_web/controllers/api/transaction_controller_test.exs b/test/archethic_web/controllers/api/transaction_controller_test.exs index 8206b47fc..f50207132 100644 --- a/test/archethic_web/controllers/api/transaction_controller_test.exs +++ b/test/archethic_web/controllers/api/transaction_controller_test.exs @@ -8,6 +8,13 @@ defmodule ArchethicWeb.API.TransactionControllerTest do alias Archethic.P2P.Node alias Archethic.Crypto + alias Archethic.TransactionChain.Transaction + alias Archethic.P2P.Message.GetTransaction + alias Archethic.P2P.Message.GetLastTransactionAddress + alias Archethic.P2P.Message.LastTransactionAddress + alias Archethic.TransactionChain.TransactionData + import Mox + setup do P2P.add_and_connect_node(%Node{ ip: {127, 0, 0, 1}, @@ -99,4 +106,413 @@ defmodule ArchethicWeb.API.TransactionControllerTest do } = json_response(conn, 400) end end + + describe "simulate_contract_execution/2" do + test "should validate the latest contract from the chain", %{conn: conn} do + code = """ + condition inherit: [ + type: transfer, + content: true, + uco_transfers: true + ] + + condition transaction: [ + uco_transfers: size() > 0 + ] + + actions triggered_by: transaction do + set_type transfer + add_uco_transfer to: "000030831178cd6a49fe446778455a7a980729a293bfa16b0a1d2743935db210da76", amount: 1337 + end + """ + + # test + old_contract_address = <<0::16, :crypto.strong_rand_bytes(32)::binary>> + old_contract_address_hex = Base.encode16(old_contract_address) + last_contract_address = <<0::16, :crypto.strong_rand_bytes(32)::binary>> + last_contract_address_hex = Base.encode16(last_contract_address) + + contract_tx = %Transaction{ + address: last_contract_address_hex, + type: :transfer, + data: %TransactionData{ + code: code, + content: "hello" + }, + version: 1 + } + + MockClient + |> stub(:send_message, fn + _, %GetTransaction{address: ^last_contract_address}, _ -> + {:ok, contract_tx} + + _, %GetLastTransactionAddress{address: ^old_contract_address}, _ -> + {:ok, %LastTransactionAddress{address: last_contract_address}} + end) + + new_tx = %{ + "address" => "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + "data" => %{ + "content" => "", + "recipients" => [old_contract_address_hex] + }, + "originSignature" => + "3045022024f8d254671af93f8b9c11b5a2781a4a7535d2e89bad69d6b1f142f8f4bcf489022100c364e10f5f846b2534a7ace4aeaa1b6c8cb674f842b9f8bc78225dfa61cabec6", + "previousPublicKey" => + "000071e1b5d4b89eddf2322c69bbf1c5591f7361b24cb3c4c464f6b5eb688fe50f7a", + "previousSignature" => + "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", + "type" => "transfer", + "version" => 1 + } + + conn = post(conn, "/api/transaction/contract/simulator", new_tx) + + assert( + match?( + [ + %{ + "valid" => true, + "recipient_address" => ^old_contract_address_hex + } + ], + json_response(conn, 200) + ) + ) + end + + test "should indicate faillure when asked to validate an invalid contract", %{conn: conn} do + code = """ + condition inherit: [ + type: transfer, + content: false, + uco_transfers: true + ] + + condition transaction: [ + uco_transfers: size() > 0 + ] + + actions triggered_by: transaction do + set_type transfer + add_uco_transfer to: "000030831178cd6a49fe446778455a7a980729a293bfa16b0a1d2743935db210da76", amount: 1337 + end + """ + + previous_tx = %Transaction{ + address: "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + type: :transfer, + data: %TransactionData{ + code: code, + content: "hello" + }, + version: 1 + } + + MockClient + |> stub(:send_message, fn _, %GetTransaction{address: _}, _ -> + {:ok, previous_tx} + end) + + new_tx = %{ + "address" => "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + "data" => %{ + "code" => code, + "content" => "0000", + "recipients" => ["00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67"] + }, + "originSignature" => + "3045022024f8d254671af93f8b9c11b5a2781a4a7535d2e89bad69d6b1f142f8f4bcf489022100c364e10f5f846b2534a7ace4aeaa1b6c8cb674f842b9f8bc78225dfa61cabec6", + "previousPublicKey" => + "000071e1b5d4b89eddf2322c69bbf1c5591f7361b24cb3c4c464f6b5eb688fe50f7a", + "previousSignature" => + "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", + "type" => "transfer", + "version" => 1 + } + + conn = post(conn, "/api/transaction/contract/simulator", new_tx) + + assert(match?([%{"valid" => false}], json_response(conn, 200))) + end + + test "should indicate when body fails changeset validation", %{conn: conn} do + code = """ + condition inherit: [ + type: transfer, + content: "hello", + uco_transfers: true + ] + + condition transaction: [ + uco_transfers: size() > 0 + ] + + actions triggered_by: transaction do + set_type transfer + add_uco_transfer to: "000030831178cd6a49fe446778455a7a980729a293bfa16b0a1d2743935db210da76", amount: 1337 + end + """ + + previous_tx = %Transaction{ + address: "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + type: :transfer, + data: %TransactionData{ + code: code, + content: "hello" + }, + version: 1 + } + + MockClient + |> stub(:send_message, fn _, %GetTransaction{address: _}, _ -> + {:ok, previous_tx} + end) + + new_tx = %{ + "address" => "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + "data" => %{ + "code" => code, + ## Next line is the invalid part + "content" => "hola", + "recipients" => ["00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67"] + }, + "originSignature" => + "3045022024f8d254671af93f8b9c11b5a2781a4a7535d2e89bad69d6b1f142f8f4bcf489022100c364e10f5f846b2534a7ace4aeaa1b6c8cb674f842b9f8bc78225dfa61cabec6", + "previousPublicKey" => + "000071e1b5d4b89eddf2322c69bbf1c5591f7361b24cb3c4c464f6b5eb688fe50f7a", + "previousSignature" => + "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", + "type" => "transfer", + "version" => 1 + } + + conn = post(conn, "/api/transaction/contract/simulator", new_tx) + + assert(match?([%{"valid" => false}], json_response(conn, 200))) + end + + test "should indicate faillure when failling parsing of contracts", %{conn: conn} do + ## SC is missing the "inherit" keyword + code = """ + condition : [ + type: transfer, + content: true, + uco_transfers: true + ] + + condition transaction: [ + uco_transfers: size() > 0 + ] + + actions triggered_by: transaction do + set_type transfer + add_uco_transfer to: "000030831178cd6a49fe446778455a7a980729a293bfa16b0a1d2743935db210da76", amount: 1337 + end + """ + + previous_tx = %Transaction{ + address: "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + type: :transfer, + data: %TransactionData{ + code: code + }, + version: 1 + } + + MockClient + |> stub(:send_message, fn _, %GetTransaction{address: _}, _ -> + {:ok, previous_tx} + end) + + new_tx = %{ + "address" => "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + "data" => %{ + "code" => code, + "recipients" => ["00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67"] + }, + "originSignature" => + "3045022024f8d254671af93f8b9c11b5a2781a4a7535d2e89bad69d6b1f142f8f4bcf489022100c364e10f5f846b2534a7ace4aeaa1b6c8cb674f842b9f8bc78225dfa61cabec6", + "previousPublicKey" => + "000071e1b5d4b89eddf2322c69bbf1c5591f7361b24cb3c4c464f6b5eb688fe50f7a", + "previousSignature" => + "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", + "type" => "transfer", + "version" => 1 + } + + conn = post(conn, "/api/transaction/contract/simulator", new_tx) + + assert(match?([%{"valid" => false}], json_response(conn, 200))) + end + + test "Assert empty contract are not simulated and return negative answer", %{conn: conn} do + code = "" + + previous_tx = %Transaction{ + address: "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + type: :transfer, + data: %TransactionData{ + code: code + }, + version: 1 + } + + MockClient + |> stub(:send_message, fn _, %GetTransaction{address: _}, _ -> + {:ok, previous_tx} + end) + + new_tx = %{ + "address" => "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + "data" => %{ + "code" => code, + "recipients" => [ + "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67" + ] + }, + "originSignature" => + "3045022024f8d254671af93f8b9c11b5a2781a4a7535d2e89bad69d6b1f142f8f4bcf489022100c364e10f5f846b2534a7ace4aeaa1b6c8cb674f842b9f8bc78225dfa61cabec6", + "previousPublicKey" => + "000071e1b5d4b89eddf2322c69bbf1c5591f7361b24cb3c4c464f6b5eb688fe50f7a", + "previousSignature" => + "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", + "type" => "transfer", + "version" => 1 + } + + conn = post(conn, "/api/transaction/contract/simulator", new_tx) + assert(match?([%{"valid" => false}], json_response(conn, 200))) + end + + test "should return error answer when asked to validate a crashing contract", %{ + conn: conn + } do + code = """ + condition inherit: [ + content: true + ] + + condition transaction: [ + uco_transfers: size() > 0 + ] + + actions triggered_by: transaction do + set_content 10 / 0 + end + """ + + previous_tx1 = %Transaction{ + address: "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + type: :transfer, + data: %TransactionData{ + code: code, + content: "hello" + }, + version: 1 + } + + previous_tx2 = %Transaction{ + address: "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d66", + type: :transfer, + data: %TransactionData{ + code: code, + content: "hello" + }, + version: 1 + } + + MockClient + |> stub(:send_message, fn + ## These decoded addresses are matching the ones in the code above + _, + %GetTransaction{ + address: + <<0, 0, 158, 5, 158, 129, 113, 100, 59, 149, 146, 132, 254, 84, 41, 9, 243, 179, 33, + 152, 184, 252, 37, 179, 229, 4, 71, 88, 155, 132, 52, 28, 29, 103>> + }, + _ -> + {:ok, previous_tx1} + + _, + %GetTransaction{ + address: + <<0, 0, 158, 5, 158, 129, 113, 100, 59, 149, 146, 132, 254, 84, 41, 9, 243, 179, 33, + 152, 184, 252, 37, 179, 229, 4, 71, 88, 155, 132, 52, 28, 29, 102>> + }, + _ -> + {:ok, previous_tx2} + end) + + new_tx = %{ + "address" => "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + "data" => %{ + "code" => code, + "content" => "0000", + "recipients" => [ + "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d66", + "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67" + ] + }, + "originSignature" => + "3045022024f8d254671af93f8b9c11b5a2781a4a7535d2e89bad69d6b1f142f8f4bcf489022100c364e10f5f846b2534a7ace4aeaa1b6c8cb674f842b9f8bc78225dfa61cabec6", + "previousPublicKey" => + "000071e1b5d4b89eddf2322c69bbf1c5591f7361b24cb3c4c464f6b5eb688fe50f7a", + "previousSignature" => + "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", + "type" => "transfer", + "version" => 1 + } + + conn = post(conn, "/api/transaction/contract/simulator", new_tx) + + assert( + match?( + [ + %{ + "valid" => false + }, + %{ + "valid" => false + } + ], + json_response(conn, 200) + ) + ) + end + + test "should return an error if there is no recipients", %{conn: conn} do + new_tx = %{ + "address" => "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", + "data" => %{ + "code" => "", + "content" => "0000", + "recipients" => [] + }, + "originSignature" => + "3045022024f8d254671af93f8b9c11b5a2781a4a7535d2e89bad69d6b1f142f8f4bcf489022100c364e10f5f846b2534a7ace4aeaa1b6c8cb674f842b9f8bc78225dfa61cabec6", + "previousPublicKey" => + "000071e1b5d4b89eddf2322c69bbf1c5591f7361b24cb3c4c464f6b5eb688fe50f7a", + "previousSignature" => + "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", + "type" => "transfer", + "version" => 1 + } + + conn = post(conn, "/api/transaction/contract/simulator", new_tx) + + assert( + match?( + [ + %{ + "valid" => false, + "reason" => "There are no recipients in the transaction" + } + ], + json_response(conn, 200) + ) + ) + end + end end