From f6e39181baff9dad0de77f1869757e5f8a5c4b06 Mon Sep 17 00:00:00 2001 From: Yan Guiborat Date: Tue, 20 Dec 2022 14:51:14 +0100 Subject: [PATCH 01/13] Validate Smart Contracts implementation before the SC merge --- lib/archethic.ex | 31 +- lib/archethic/contracts.ex | 133 ++++++ lib/archethic/contracts/interpreter.ex | 4 +- .../legacy/condition_interpreter.ex | 3 +- lib/archethic/contracts/worker.ex | 46 +- .../controllers/api/transaction_controller.ex | 85 ++++ lib/archethic_web/explorer_router.ex | 6 + test/archethic/contracts_test.exs | 70 +++ .../transaction_chain/transaction_test.exs | 1 - .../api/transaction_controller_test.exs | 405 ++++++++++++++++++ 10 files changed, 755 insertions(+), 29 deletions(-) diff --git a/lib/archethic.ex b/lib/archethic.ex index d94ee27b9..f3e0d1246 100644 --- a/lib/archethic.ex +++ b/lib/archethic.ex @@ -4,14 +4,19 @@ defmodule Archethic do """ alias Archethic.SharedSecrets - alias __MODULE__.{Account, BeaconChain, Crypto, Election, P2P, P2P.Node, P2P.Message} + alias __MODULE__.{Account, BeaconChain, Contracts, Crypto, Election, P2P, P2P.Node, P2P.Message} alias __MODULE__.{SelfRepair, TransactionChain} 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, + TransactionData + } require Logger @@ -302,6 +307,28 @@ defmodule Archethic do end end + @doc """ + Assert the transaction holds a contract and then simulate its execution. + Return an error if the transaction holds no contract. + """ + @spec simulate_contract_execution(Transaction.t(), Transaction.t()) :: + :ok | {:error, reason :: term()} + def simulate_contract_execution( + prev_tx = %Transaction{data: %TransactionData{code: code}}, + incoming_tx + ) + when code != "" do + Contracts.simulate_contract_execution(prev_tx, incoming_tx, DateTime.utc_now()) + end + + ## Empty contracts are considered invalid + def simulate_contract_execution( + _prev_tx, + _next_tx + ) do + {:error, :no_contract} + end + @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..c44cf0bfb 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -18,6 +18,8 @@ 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 @@ -199,6 +201,137 @@ defmodule Archethic.Contracts do end end + @doc """ + Simulate the execution of the contract hold in prev_tx with the inputs of next_tx, at a certain date + """ + + @spec simulate_contract_execution(Transaction.t(), Transaction.t(), DateTime.t()) :: + :ok | {:error, reason :: term()} + def simulate_contract_execution( + prev_tx = %Transaction{data: %TransactionData{code: code}}, + incoming_tx = %Transaction{}, + date = %DateTime{} + ) do + case Interpreter.parse(code) do + {:ok, + %Contract{ + version: version, + triggers: triggers, + conditions: conditions + }} -> + triggers + |> Enum.find_value(:ok, fn {trigger_type, trigger_code} -> + do_simulate_contract( + version, + trigger_code, + trigger_type, + conditions, + prev_tx, + incoming_tx, + date + ) + end) + + {:error, reason} -> + {:error, reason} + end + end + + defp do_simulate_contract( + version, + trigger_code, + trigger_type, + conditions, + prev_tx, + incoming_tx, + date + ) do + case valid_from_trigger?(trigger_type, incoming_tx, date) do + true -> + case validate_transaction_conditions( + version, + trigger_type, + conditions, + prev_tx, + incoming_tx + ) do + :ok -> + validate_inherit_conditions(version, trigger_code, conditions, prev_tx, incoming_tx) + + {:error, reason} -> + {:error, reason} + end + + false -> + {:error, :invalid_trigger} + end + end + + defp validate_transaction_conditions( + version, + trigger_type, + %{transaction: transaction_conditions}, + prev_tx, + incoming_tx + ) do + case trigger_type do + :transaction -> + constants_prev = %{ + "transaction" => Constants.from_transaction(incoming_tx), + "contract" => Constants.from_transaction(prev_tx) + } + + case Interpreter.valid_conditions?(version, transaction_conditions, constants_prev) do + true -> + :ok + + false -> + {:error, :invalid_transaction_conditions} + end + + _ -> + :ok + end + end + + defp validate_inherit_conditions( + version, + trigger_code, + %{inherit: inherit_conditions}, + prev_tx, + incoming_tx + ) do + prev_constants = %{ + "transaction" => Constants.from_transaction(incoming_tx), + "contract" => Constants.from_transaction(prev_tx) + } + + case Interpreter.execute_trigger(version, trigger_code, prev_constants) do + nil -> + :ok + + next_transaction = %Transaction{} -> + %{next_transaction: next_transaction} = + %{next_transaction: next_transaction, previous_transaction: prev_tx} + |> Worker.chain_type() + |> Worker.chain_code() + |> Worker.chain_ownerships() + + constants_both = %{ + "previous" => Constants.from_transaction(prev_tx), + "next" => Constants.from_transaction(next_transaction) + } + + case Interpreter.valid_conditions?(version, inherit_conditions, constants_both) do + true -> + :ok + + false -> + {:error, :invalid_inherit_conditions} + end + end + end + defp validate_conditions(version, inherit_conditions, constants) do if Interpreter.valid_conditions?(version, inherit_conditions, constants) do :ok diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index fb5503279..347469e42 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -31,8 +31,8 @@ defmodule Archethic.Contracts.Interpreter do Legacy.parse(block) end - {:error, reason} -> - {:error, reason} + {:error, {[line: line, column: column], _msg_info, _token}} -> + {:error, "Parse error at line #{line} column #{column}"} end end 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..79d86414a 100644 --- a/lib/archethic/contracts/worker.ex +++ b/lib/archethic/contracts/worker.ex @@ -380,36 +380,36 @@ defmodule Archethic.Contracts.Worker do end end - defp chain_type( - acc = %{ - next_transaction: %Transaction{type: nil}, - previous_transaction: _ - } - ) do + def 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 + def chain_type(acc), do: acc - defp chain_code( - acc = %{ - next_transaction: %Transaction{data: %TransactionData{code: ""}}, - previous_transaction: %Transaction{data: %TransactionData{code: previous_code}} - } - ) do + def 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 + def chain_code(acc), do: acc - defp chain_ownerships( - acc = %{ - next_transaction: %Transaction{data: %TransactionData{ownerships: []}}, - previous_transaction: %Transaction{ - data: %TransactionData{ownerships: previous_ownerships} - } - } - ) do + def 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)], @@ -417,7 +417,7 @@ defmodule Archethic.Contracts.Worker do ) end - defp chain_ownerships(acc), do: acc + def chain_ownerships(acc), do: acc defp enough_funds?(contract_address) do case Account.get_balance(contract_address) do diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index 40a610335..484abd1b9 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -143,4 +143,89 @@ 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() + + {:ok, sup} = Task.Supervisor.start_link(strategy: :one_for_one) + + results = + Task.Supervisor.async_stream_nolink( + sup, + 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} -> + # TODO: find a way to prettify the error and remove the stacktrace + %{ + "valid" => false, + "reason" => "A contract exited while simulating", + "recipient_address" => Base.encode16(recipient) + } + end) + |> Enum.to_list() + + conn + |> put_status(:ok) + |> json(results) + + 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 + case Archethic.search_transaction(recipient_address) do + {:ok, prev_tx} -> + Archethic.simulate_contract_execution(prev_tx, tx) + + {:error, reason} -> + {:error, reason} + end + 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_test.exs b/test/archethic/contracts_test.exs index b0b077424..40d0fc043 100644 --- a/test/archethic/contracts_test.exs +++ b/test/archethic/contracts_test.exs @@ -314,4 +314,74 @@ defmodule Archethic.ContractsTest do assert true == Contracts.accept_new_contract?(previous_tx, next_tx, time) end end + + describe "simulate_contract_execution?/3" do + test "should return true when simulating execution of a valid contract" do + code = """ + condition inherit: [ + content: "hello" + ] + + actions triggered_by: transaction do + set_content "hello" + end + """ + + previous_tx = %Transaction{ + data: %TransactionData{ + code: code + } + } + + {:ok, time} = DateTime.new(~D[2016-05-24], ~T[13:26:00.000999], "Etc/UTC") + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{ + content: "hello", + code: code + } + } + + assert match?( + :ok, + Contracts.simulate_contract_execution(previous_tx, incoming_tx, time) + ) + end + + test "should return false when simulating execution of a contract where + conditions aren't filled" do + code = """ + condition inherit: [ + content: "hello", + type: data + ] + + actions triggered_by: transaction do + set_content "hello" + end + """ + + previous_tx = %Transaction{ + data: %TransactionData{ + code: code + } + } + + {:ok, time} = DateTime.new(~D[2016-05-24], ~T[13:26:00.000999], "Etc/UTC") + + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{ + content: "hola", + code: code + } + } + + assert match?( + {:error, :invalid_inherit_conditions}, + Contracts.simulate_contract_execution(previous_tx, incoming_tx, time) + ) + end + end end 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..6365fff2a 100644 --- a/test/archethic_web/controllers/api/transaction_controller_test.exs +++ b/test/archethic_web/controllers/api/transaction_controller_test.exs @@ -8,6 +8,11 @@ defmodule ArchethicWeb.API.TransactionControllerTest do alias Archethic.P2P.Node alias Archethic.Crypto + alias Archethic.TransactionChain.Transaction + alias Archethic.P2P.Message.GetTransaction + alias Archethic.TransactionChain.TransactionData + import Mox + setup do P2P.add_and_connect_node(%Node{ ip: {127, 0, 0, 1}, @@ -99,4 +104,404 @@ defmodule ArchethicWeb.API.TransactionControllerTest do } = json_response(conn, 400) end end + + describe "simulate_contract_execution/2" do + test "should return sucessfull answer when asked to validate a valid contract", %{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 + """ + + 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" => %{ + "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" => true, + "address" => "00009E059E8171643B959284FE542909F3B32198B8FC25B3E50447589B84341C1D66" + }, + %{ + "valid" => true, + "address" => "00009E059E8171643B959284FE542909F3B32198B8FC25B3E50447589B84341C1D67" + } + ], + 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 + end end From 1e9e0231767ab0c232a8f3c94c178d79d645aa29 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Wed, 15 Mar 2023 19:01:10 +0100 Subject: [PATCH 02/13] prettify contract error --- .../controllers/api/transaction_controller.ex | 23 ++++++++++++++++--- .../api/transaction_controller_test.exs | 6 +++-- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index 484abd1b9..eb76724a7 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -192,11 +192,10 @@ defmodule ArchethicWeb.API.TransactionController do "recipient_address" => Base.encode16(recipient) } - {{:exit, _reason}, recipient} -> - # TODO: find a way to prettify the error and remove the stacktrace + {{:exit, reason}, recipient} -> %{ "valid" => false, - "reason" => "A contract exited while simulating", + "reason" => format_exit_reason(reason), "recipient_address" => Base.encode16(recipient) } end) @@ -228,4 +227,22 @@ defmodule ArchethicWeb.API.TransactionController do {:error, reason} end end + + defp format_exit_reason({error_atom, stacktrace}) do + Enum.reduce_while( + stacktrace, + "A contract exited with error: #{error_atom}", + 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/test/archethic_web/controllers/api/transaction_controller_test.exs b/test/archethic_web/controllers/api/transaction_controller_test.exs index 6365fff2a..961fccc42 100644 --- a/test/archethic_web/controllers/api/transaction_controller_test.exs +++ b/test/archethic_web/controllers/api/transaction_controller_test.exs @@ -192,11 +192,13 @@ defmodule ArchethicWeb.API.TransactionControllerTest do [ %{ "valid" => true, - "address" => "00009E059E8171643B959284FE542909F3B32198B8FC25B3E50447589B84341C1D66" + "recipient_address" => + "00009E059E8171643B959284FE542909F3B32198B8FC25B3E50447589B84341C1D66" }, %{ "valid" => true, - "address" => "00009E059E8171643B959284FE542909F3B32198B8FC25B3E50447589B84341C1D67" + "recipient_address" => + "00009E059E8171643B959284FE542909F3B32198B8FC25B3E50447589B84341C1D67" } ], json_response(conn, 200) From 8c1ea436df5510eab569cf100a9df84650aefd94 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Thu, 16 Mar 2023 16:50:07 +0100 Subject: [PATCH 03/13] simulate contract function return the next_transaction --- lib/archethic.ex | 35 +- lib/archethic/contracts.ex | 308 +++++------------- lib/archethic/contracts/interpreter.ex | 62 +++- .../interpreter/condition_validator.ex | 8 +- .../controllers/api/transaction_controller.ex | 36 +- test/archethic/contracts/worker_test.exs | 2 + test/archethic/contracts_test.exs | 103 ++++-- 7 files changed, 274 insertions(+), 280 deletions(-) diff --git a/lib/archethic.ex b/lib/archethic.ex index f3e0d1246..ee48f557a 100644 --- a/lib/archethic.ex +++ b/lib/archethic.ex @@ -14,8 +14,7 @@ defmodule Archethic do alias TransactionChain.{ Transaction, TransactionInput, - TransactionSummary, - TransactionData + TransactionSummary } require Logger @@ -308,26 +307,20 @@ defmodule Archethic do end @doc """ - Assert the transaction holds a contract and then simulate its execution. - Return an error if the transaction holds no contract. + Simulate the execution of the given contract's trigger. """ - @spec simulate_contract_execution(Transaction.t(), Transaction.t()) :: - :ok | {:error, reason :: term()} - def simulate_contract_execution( - prev_tx = %Transaction{data: %TransactionData{code: code}}, - incoming_tx - ) - when code != "" do - Contracts.simulate_contract_execution(prev_tx, incoming_tx, DateTime.utc_now()) - end - - ## Empty contracts are considered invalid - def simulate_contract_execution( - _prev_tx, - _next_tx - ) do - {:error, :no_contract} - end + @spec simulate_contract_execution(atom(), Transaction.t(), nil | Transaction.t()) :: + {:ok, nil | Transaction.t()} + | {:error, + :invalid_triggers_execution + | :invalid_transaction_constraints + | :invalid_inherit_constraints} + defdelegate simulate_contract_execution( + trigger_type, + contract_transaction, + incoming_transaction + ), + to: Contracts @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 c44cf0bfb..f3b7b1c97 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 @@ -24,130 +23,12 @@ defmodule Archethic.Contracts do @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 @@ -202,132 +83,119 @@ defmodule Archethic.Contracts do end @doc """ - Simulate the execution of the contract hold in prev_tx with the inputs of next_tx, at a certain date + Simulate the execution of the given contract's trigger. """ - - @spec simulate_contract_execution(Transaction.t(), Transaction.t(), DateTime.t()) :: - :ok | {:error, reason :: term()} + @spec simulate_contract_execution(atom(), Transaction.t(), nil | Transaction.t()) :: + {:ok, nil | Transaction.t()} + | {:error, + :invalid_triggers_execution + | :invalid_transaction_constraints + | :invalid_inherit_constraints} def simulate_contract_execution( - prev_tx = %Transaction{data: %TransactionData{code: code}}, - incoming_tx = %Transaction{}, - date = %DateTime{} + trigger_type, + contract_tx = %Transaction{data: %TransactionData{code: code}}, + maybe_incoming_tx ) do - case Interpreter.parse(code) do - {:ok, - %Contract{ - version: version, - triggers: triggers, - conditions: conditions - }} -> - triggers - |> Enum.find_value(:ok, fn {trigger_type, trigger_code} -> - do_simulate_contract( - version, - trigger_code, - trigger_type, - conditions, - prev_tx, - incoming_tx, - date - ) - end) + # the contract transaction exists, no need to check again for parse error + contract = parse!(code) - {:error, reason} -> - {:error, reason} + case contract.triggers[trigger_type] do + nil -> + {:error, :invalid_triggers_execution} + + trigger_code -> + do_simulate_contract_execution( + trigger_type, + trigger_code, + contract_tx, + maybe_incoming_tx, + contract + ) end end - defp do_simulate_contract( - version, + defp do_simulate_contract_execution( + :transaction, trigger_code, - trigger_type, - conditions, - prev_tx, - incoming_tx, - date + contract_tx, + incoming_tx = %Transaction{}, + %Contract{version: version, conditions: conditions} ) do - case valid_from_trigger?(trigger_type, incoming_tx, date) do - true -> - case validate_transaction_conditions( - version, - trigger_type, - conditions, - prev_tx, - incoming_tx - ) do - :ok -> - validate_inherit_conditions(version, trigger_code, conditions, prev_tx, incoming_tx) - - {:error, reason} -> - {:error, reason} - end + constants = %{ + "transaction" => Constants.from_transaction(incoming_tx), + "contract" => Constants.from_transaction(contract_tx) + } - false -> - {:error, :invalid_trigger} + if Interpreter.valid_conditions?(version, conditions.transaction, constants) do + execute_trigger_and_validate_inherit_condition( + version, + trigger_code, + contract_tx, + incoming_tx, + conditions.inherit + ) + else + {:error, :invalid_transaction_constraints} end end - defp validate_transaction_conditions( - version, - trigger_type, - %{transaction: transaction_conditions}, - prev_tx, - incoming_tx + defp do_simulate_contract_execution( + _trigger_type, + trigger_code, + contract_tx, + nil, + %Contract{ + version: version, + conditions: conditions + } ) do - case trigger_type do - :transaction -> - constants_prev = %{ - "transaction" => Constants.from_transaction(incoming_tx), - "contract" => Constants.from_transaction(prev_tx) - } - - case Interpreter.valid_conditions?(version, transaction_conditions, constants_prev) do - true -> - :ok - - false -> - {:error, :invalid_transaction_conditions} - end - - _ -> - :ok - end + execute_trigger_and_validate_inherit_condition( + version, + trigger_code, + contract_tx, + nil, + conditions.inherit + ) end - defp validate_inherit_conditions( + defp execute_trigger_and_validate_inherit_condition( version, trigger_code, - %{inherit: inherit_conditions}, - prev_tx, - incoming_tx + contract_tx, + maybe_incoming_tx, + inherit_condition ) do - prev_constants = %{ - "transaction" => Constants.from_transaction(incoming_tx), - "contract" => Constants.from_transaction(prev_tx) + constants_trigger = %{ + "transaction" => + case maybe_incoming_tx do + nil -> nil + tx -> Constants.from_transaction(tx) + end, + "contract" => Constants.from_transaction(contract_tx) } - case Interpreter.execute_trigger(version, trigger_code, prev_constants) do + case Interpreter.execute_trigger(version, trigger_code, constants_trigger) do nil -> - :ok + # contract did not produce a next_tx + {:ok, nil} - next_transaction = %Transaction{} -> - %{next_transaction: next_transaction} = - %{next_transaction: next_transaction, previous_transaction: prev_tx} + next_tx -> + # contract produce a next_tx but we need to fill it in with the previous values + %{next_transaction: next_tx} = + %{next_transaction: next_tx, previous_transaction: contract_tx} |> Worker.chain_type() |> Worker.chain_code() |> Worker.chain_ownerships() - constants_both = %{ - "previous" => Constants.from_transaction(prev_tx), - "next" => Constants.from_transaction(next_transaction) + constants_inherit = %{ + "previous" => Constants.from_transaction(contract_tx), + "next" => Constants.from_transaction(next_tx) } - case Interpreter.valid_conditions?(version, inherit_conditions, constants_both) do - true -> - :ok - - false -> - {:error, :invalid_inherit_conditions} + if Interpreter.valid_conditions?(version, inherit_condition, constants_inherit) do + {:ok, next_tx} + else + {:error, :invalid_inherit_constraints} end end end diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index 347469e42..4a2ac1a95 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -20,20 +20,33 @@ defmodule Archethic.Contracts.Interpreter do 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 - - {:error, {[line: line, column: column], _msg_info, _token}} -> - {:error, "Parse error at line #{line} column #{column}"} - 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 """ @@ -211,4 +224,27 @@ defmodule Archethic.Contracts.Interpreter do end defp parse_ast(ast, _), do: {:error, ast, "unexpected term"} + + 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?(triggers, :oracle) and Conditions.empty?(conditions.oracle) -> + {:error, "empty 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_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index eb76724a7..df8c1d63b 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -221,17 +221,47 @@ defmodule ArchethicWeb.API.TransactionController do defp fetch_recipient_tx_and_simulate(recipient_address, tx) do case Archethic.search_transaction(recipient_address) do {:ok, prev_tx} -> - Archethic.simulate_contract_execution(prev_tx, tx) + # this endpoint is only used for transaction triggers + case Archethic.simulate_contract_execution(:transaction, prev_tx, tx) do + {:ok, _} -> + # contract may have returned a transaction or not, in both case it's valid + :ok + + {: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 `conditon transaction` block."} + + {:error, :invalid_inherit_constraints} -> + {:error, + "Contract refused outcoming transaction. Check the `condition inherit` block."} + end {:error, reason} -> {:error, reason} end end - defp format_exit_reason({error_atom, stacktrace}) do + 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: #{error_atom}", + "A contract exited with error: #{formatted_error}", fn {:elixir_eval, _, _, [file: 'nofile', line: line]}, acc -> {:halt, acc <> " (line: #{line})"} diff --git a/test/archethic/contracts/worker_test.exs b/test/archethic/contracts/worker_test.exs index 869ac2572..d8347bd52 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 diff --git a/test/archethic/contracts_test.exs b/test/archethic/contracts_test.exs index 40d0fc043..10ae3e7d1 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" @@ -316,71 +318,132 @@ defmodule Archethic.ContractsTest do end describe "simulate_contract_execution?/3" do - test "should return true when simulating execution of a valid contract" 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 - set_content "hello" + Contract.set_content "hello" end """ - previous_tx = %Transaction{ + contract_tx = %Transaction{ + type: :contract, data: %TransactionData{ code: code } } - {:ok, time} = DateTime.new(~D[2016-05-24], ~T[13:26:00.000999], "Etc/UTC") - incoming_tx = %Transaction{ type: :transfer, + data: %TransactionData{} + } + + assert {:ok, %Transaction{}} = + Contracts.simulate_contract_execution(: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{ - content: "hello", code: code } } - assert match?( - :ok, - Contracts.simulate_contract_execution(previous_tx, incoming_tx, time) - ) + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{} + } + + assert {:ok, nil} = + Contracts.simulate_contract_execution(:transaction, contract_tx, incoming_tx) end - test "should return false when simulating execution of a contract where - conditions aren't filled" do + test "should return inherit constraints error when condition inherit fails" do code = """ + @version 1 condition inherit: [ content: "hello", - type: data + type: "data" ] + condition transaction: [] + actions triggered_by: transaction do - set_content "hello" + Contract.set_content "hello" end """ - previous_tx = %Transaction{ + contract_tx = %Transaction{ + type: :contract, data: %TransactionData{ code: code } } - {:ok, time} = DateTime.new(~D[2016-05-24], ~T[13:26:00.000999], "Etc/UTC") - incoming_tx = %Transaction{ type: :transfer, + data: %TransactionData{} + } + + assert match?( + {:error, :invalid_inherit_constraints}, + Contracts.simulate_contract_execution(: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{ - content: "hola", code: code } } + incoming_tx = %Transaction{ + type: :transfer, + data: %TransactionData{} + } + assert match?( - {:error, :invalid_inherit_conditions}, - Contracts.simulate_contract_execution(previous_tx, incoming_tx, time) + {:error, :invalid_transaction_constraints}, + Contracts.simulate_contract_execution(:transaction, contract_tx, incoming_tx) ) end end From a04f07abffde03e3bce12a48193b7b0519e4fa27 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Thu, 16 Mar 2023 17:33:19 +0100 Subject: [PATCH 04/13] add more unit tests --- lib/archethic/contracts/interpreter.ex | 3 - test/archethic/contracts_test.exs | 79 ++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index 4a2ac1a95..78bf14462 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -237,9 +237,6 @@ defmodule Archethic.Contracts.Interpreter do Map.has_key?(triggers, :oracle) and !Map.has_key?(conditions, :oracle) -> {:error, "missing oracle conditions"} - Map.has_key?(triggers, :oracle) and Conditions.empty?(conditions.oracle) -> - {:error, "empty oracle conditions"} - !Map.has_key?(conditions, :inherit) -> {:error, "missing inherit conditions"} diff --git a/test/archethic/contracts_test.exs b/test/archethic/contracts_test.exs index 10ae3e7d1..fc7ec4f9d 100644 --- a/test/archethic/contracts_test.exs +++ b/test/archethic/contracts_test.exs @@ -446,5 +446,84 @@ defmodule Archethic.ContractsTest do Contracts.simulate_contract_execution(:transaction, contract_tx, incoming_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{}} = + Contracts.simulate_contract_execution( + {:datetime, ~U[2023-03-16 16:28:56Z]}, + 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{}} = + Contracts.simulate_contract_execution( + {:interval, "* * * * *"}, + 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{}} = + Contracts.simulate_contract_execution(:oracle, contract_tx, nil) + end end end From c921023ca3fec77f80d697508fbedacb7b0fa2a1 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Thu, 16 Mar 2023 17:37:07 +0100 Subject: [PATCH 05/13] better spec --- lib/archethic/contracts.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/archethic/contracts.ex b/lib/archethic/contracts.ex index f3b7b1c97..2fb7e3af5 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -85,7 +85,11 @@ defmodule Archethic.Contracts do @doc """ Simulate the execution of the given contract's trigger. """ - @spec simulate_contract_execution(atom(), Transaction.t(), nil | Transaction.t()) :: + @spec simulate_contract_execution( + Contract.trigger_type(), + Transaction.t(), + nil | Transaction.t() + ) :: {:ok, nil | Transaction.t()} | {:error, :invalid_triggers_execution From 0bc6cef2ed0cc2736d82fdf4e1ce9b42f337e701 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 17 Mar 2023 14:07:22 +0100 Subject: [PATCH 06/13] move simulate into interpreter and refactor the worker --- lib/archethic.ex | 27 +- lib/archethic/contracts.ex | 122 -------- lib/archethic/contracts/interpreter.ex | 234 ++++++++++++++- lib/archethic/contracts/worker.ex | 280 +++++------------- .../controllers/api/transaction_controller.ex | 9 +- test/archethic/contracts/interpreter_test.exs | 268 +++++++++++++++++ test/archethic/contracts/worker_test.exs | 14 + test/archethic/contracts_test.exs | 210 ------------- 8 files changed, 605 insertions(+), 559 deletions(-) diff --git a/lib/archethic.ex b/lib/archethic.ex index ee48f557a..e0fdc4b06 100644 --- a/lib/archethic.ex +++ b/lib/archethic.ex @@ -4,8 +4,10 @@ defmodule Archethic do """ alias Archethic.SharedSecrets - alias __MODULE__.{Account, BeaconChain, Contracts, Crypto, Election, P2P, P2P.Node, P2P.Message} + 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} @@ -307,20 +309,27 @@ defmodule Archethic do end @doc """ - Simulate the execution of the given contract's trigger. + Parse and execute the contract in the given transaction. + We assume the contract is parse-able. """ - @spec simulate_contract_execution(atom(), Transaction.t(), nil | Transaction.t()) :: + @spec parse_and_execute_contract_at( + Contract.trigger_type(), + Transaction.t(), + nil | Transaction.t() + ) :: {:ok, nil | Transaction.t()} | {:error, :invalid_triggers_execution | :invalid_transaction_constraints + | :invalid_oracle_constraints | :invalid_inherit_constraints} - defdelegate simulate_contract_execution( - trigger_type, - contract_transaction, - incoming_transaction - ), - to: Contracts + def parse_and_execute_contract_at( + trigger_type, + contract_tx, + maybe_tx + ) do + Interpreter.execute(trigger_type, Contract.from_transaction!(contract_tx), maybe_tx) + end @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 2fb7e3af5..c9152fb79 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -82,128 +82,6 @@ defmodule Archethic.Contracts do end end - @doc """ - Simulate the execution of the given contract's trigger. - """ - @spec simulate_contract_execution( - Contract.trigger_type(), - Transaction.t(), - nil | Transaction.t() - ) :: - {:ok, nil | Transaction.t()} - | {:error, - :invalid_triggers_execution - | :invalid_transaction_constraints - | :invalid_inherit_constraints} - def simulate_contract_execution( - trigger_type, - contract_tx = %Transaction{data: %TransactionData{code: code}}, - maybe_incoming_tx - ) do - # the contract transaction exists, no need to check again for parse error - contract = parse!(code) - - case contract.triggers[trigger_type] do - nil -> - {:error, :invalid_triggers_execution} - - trigger_code -> - do_simulate_contract_execution( - trigger_type, - trigger_code, - contract_tx, - maybe_incoming_tx, - contract - ) - end - end - - defp do_simulate_contract_execution( - :transaction, - trigger_code, - contract_tx, - incoming_tx = %Transaction{}, - %Contract{version: version, conditions: conditions} - ) do - constants = %{ - "transaction" => Constants.from_transaction(incoming_tx), - "contract" => Constants.from_transaction(contract_tx) - } - - if Interpreter.valid_conditions?(version, conditions.transaction, constants) do - execute_trigger_and_validate_inherit_condition( - version, - trigger_code, - contract_tx, - incoming_tx, - conditions.inherit - ) - else - {:error, :invalid_transaction_constraints} - end - end - - defp do_simulate_contract_execution( - _trigger_type, - trigger_code, - contract_tx, - nil, - %Contract{ - version: version, - conditions: conditions - } - ) do - execute_trigger_and_validate_inherit_condition( - version, - trigger_code, - contract_tx, - nil, - conditions.inherit - ) - end - - defp execute_trigger_and_validate_inherit_condition( - version, - trigger_code, - contract_tx, - maybe_incoming_tx, - inherit_condition - ) do - constants_trigger = %{ - "transaction" => - case maybe_incoming_tx do - nil -> nil - tx -> Constants.from_transaction(tx) - end, - "contract" => Constants.from_transaction(contract_tx) - } - - case Interpreter.execute_trigger(version, trigger_code, constants_trigger) do - nil -> - # contract did not produce a next_tx - {:ok, nil} - - next_tx -> - # contract produce a next_tx but we need to fill it in with the previous values - %{next_transaction: next_tx} = - %{next_transaction: next_tx, previous_transaction: contract_tx} - |> Worker.chain_type() - |> Worker.chain_code() - |> Worker.chain_ownerships() - - constants_inherit = %{ - "previous" => Constants.from_transaction(contract_tx), - "next" => Constants.from_transaction(next_tx) - } - - if Interpreter.valid_conditions?(version, inherit_condition, constants_inherit) do - {:ok, next_tx} - else - {:error, :invalid_inherit_constraints} - end - end - end - defp validate_conditions(version, inherit_conditions, constants) do if Interpreter.valid_conditions?(version, inherit_conditions, constants) do :ok diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index 78bf14462..cb11b44e5 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -10,10 +10,13 @@ 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. @@ -76,15 +79,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 @@ -149,6 +197,179 @@ defmodule Archethic.Contracts.Interpreter do # | .__/|_| |_| \_/ \__,_|\__\___| # |_| # ------------------------------------------------------------ + + defp do_execute( + :transaction, + trigger_code, + contract, + incoming_tx = %Transaction{}, + %Contract{version: version, conditions: conditions}, + opts + ) do + constants = %{ + "transaction" => Constants.from_transaction(incoming_tx), + "contract" => contract.constants.contract + } + + if valid_conditions?(version, conditions.transaction, constants) do + execute_trigger_and_validate_inherit_condition( + version, + trigger_code, + contract, + incoming_tx, + conditions.inherit, + opts + ) + else + {:error, :invalid_transaction_constraints} + end + end + + defp do_execute( + :oracle, + trigger_code, + contract, + oracle_tx = %Transaction{}, + %Contract{version: version, conditions: conditions}, + opts + ) do + constants = %{ + "transaction" => Constants.from_transaction(oracle_tx), + "contract" => contract.constants.contract + } + + if valid_conditions?(version, conditions.oracle, constants) do + execute_trigger_and_validate_inherit_condition( + version, + trigger_code, + contract, + oracle_tx, + conditions.inherit, + opts + ) + else + {:error, :invalid_oracle_constraints} + end + end + + defp do_execute( + _trigger_type, + trigger_code, + contract, + nil, + %Contract{version: version, conditions: conditions}, + opts + ) do + execute_trigger_and_validate_inherit_condition( + version, + trigger_code, + contract, + nil, + conditions.inherit, + opts + ) + end + + defp execute_trigger_and_validate_inherit_condition( + version, + trigger_code, + contract, + maybe_tx, + inherit_condition, + opts + ) 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 + {:ok, nil} + + next_tx_to_prepare -> + # contract produce a next_tx but we need to feed previous values to it + next_tx = + chain_transaction( + Constants.to_transaction(contract.constants.contract), + next_tx_to_prepare + ) + + constants_inherit = %{ + "previous" => contract.constants.contract, + "next" => Constants.from_transaction(next_tx) + } + + skip_inherit_check? = Keyword.get(opts, :skip_inherit_check?, false) + + if skip_inherit_check? or valid_conditions?(version, inherit_condition, constants_inherit) do + {:ok, next_tx} + else + {:error, :invalid_inherit_constraints} + end + 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) @@ -169,6 +390,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)} @@ -225,6 +449,10 @@ defmodule Archethic.Contracts.Interpreter do defp parse_ast(ast, _), do: {:error, ast, "unexpected term"} + # ----------------------------------------- + # contract validation + # ----------------------------------------- + defp check_contract_blocks({:error, reason}), do: {:error, reason} defp check_contract_blocks( diff --git a/lib/archethic/contracts/worker.ex b/lib/archethic/contracts/worker.ex index 79d86414a..3925aee54 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,191 +74,45 @@ 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) - ) + _ = do_execute_trigger_and_handle_result(:transaction, contract, incoming_tx) - 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 - 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} - 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 - } + _ = do_execute_trigger_and_handle_result(trigger_type, contract) - 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) - 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) - ) + _ = do_execute_trigger_and_handle_result(trigger_type, contract) - # 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) - end - {: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) - } + {:ok, oracle_tx} = TransactionChain.get_transaction(tx_address) - 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 - end + _ = do_execute_trigger_and_handle_result(:oracle, contract, oracle_tx) {:noreply, state} end @@ -268,6 +121,7 @@ defmodule Archethic.Contracts.Worker do {:noreply, state} end + # ---------------------------------------------- defp via_tuple(address) do {:via, Registry, {ContractRegistry, address}} end @@ -330,18 +184,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 +208,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 +231,6 @@ defmodule Archethic.Contracts.Worker do end end - def chain_type( - acc = %{ - next_transaction: %Transaction{type: nil}, - previous_transaction: _ - } - ) do - put_in(acc, [:next_transaction, Access.key(:type)], :contract) - end - - def chain_type(acc), do: acc - - def 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 - - def chain_code(acc), do: acc - - def 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 - - def 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 +281,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 +294,48 @@ defmodule Archethic.Contracts.Worker do {:error, :not_enough_funds} end end + + # no one use the return value + defp do_execute_trigger_and_handle_result( + trigger_type, + contract = %Contract{}, + maybe_tx \\ nil + ) do + contract_tx = Constants.to_transaction(contract.constants.contract) + + if enough_funds?(contract_tx.address) do + log_metadata = + case maybe_tx do + nil -> + [contract: Base.encode16(contract_tx.address)] + + incoming_tx -> + [ + transaction_address: Base.encode16(incoming_tx.address), + transaction_type: incoming_tx.type, + contract: Base.encode16(contract_tx.address) + ] + end + + Logger.info("Execute contract trigger: #{trigger_type}", log_metadata) + + # keep similar behaviour as legacy, we don't check the inherit condition in the worker + case Interpreter.execute(trigger_type, contract, maybe_tx, skip_inherit_check?: true) do + {:ok, nil} -> + :ok + + {:ok, next_tx} -> + with {:ok, next_tx} <- chain_transaction(next_tx, contract_tx), + :ok <- ensure_enough_funds(next_tx, contract_tx.address) do + handle_new_transaction(next_tx) + end + + {:error, reason} -> + Logger.debug("Contract execution failed, reason: #{inspect(reason)}", log_metadata) + :error + end + else + :error + end + end end diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index df8c1d63b..f156beb76 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -220,9 +220,9 @@ defmodule ArchethicWeb.API.TransactionController do defp fetch_recipient_tx_and_simulate(recipient_address, tx) do case Archethic.search_transaction(recipient_address) do - {:ok, prev_tx} -> + {:ok, contract_tx} -> # this endpoint is only used for transaction triggers - case Archethic.simulate_contract_execution(:transaction, prev_tx, tx) do + case Archethic.parse_and_execute_contract_at(:transaction, contract_tx, tx) do {:ok, _} -> # contract may have returned a transaction or not, in both case it's valid :ok @@ -232,7 +232,10 @@ defmodule ArchethicWeb.API.TransactionController do {:error, :invalid_transaction_constraints} -> {:error, - "Contract refused incoming transaction. Check the `conditon transaction` block."} + "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, 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 d8347bd52..70dcb4fbf 100644 --- a/test/archethic/contracts/worker_test.exs +++ b/test/archethic/contracts/worker_test.exs @@ -484,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 fc7ec4f9d..4296c2f41 100644 --- a/test/archethic/contracts_test.exs +++ b/test/archethic/contracts_test.exs @@ -316,214 +316,4 @@ defmodule Archethic.ContractsTest do assert true == Contracts.accept_new_contract?(previous_tx, next_tx, time) end end - - describe "simulate_contract_execution?/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{}} = - Contracts.simulate_contract_execution(: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} = - Contracts.simulate_contract_execution(: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}, - Contracts.simulate_contract_execution(: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}, - Contracts.simulate_contract_execution(:transaction, contract_tx, incoming_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{}} = - Contracts.simulate_contract_execution( - {:datetime, ~U[2023-03-16 16:28:56Z]}, - 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{}} = - Contracts.simulate_contract_execution( - {:interval, "* * * * *"}, - 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{}} = - Contracts.simulate_contract_execution(:oracle, contract_tx, nil) - end - end end From b7bc0a059966f97e46810adc38ac184762643532 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Mon, 27 Mar 2023 14:48:45 +0200 Subject: [PATCH 07/13] use existing TaskSupervisor --- lib/archethic_web/controllers/api/transaction_controller.ex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index f156beb76..5482456f5 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -160,11 +160,9 @@ defmodule ArchethicWeb.API.TransactionController do |> TransactionPayload.to_map() |> Transaction.cast() - {:ok, sup} = Task.Supervisor.start_link(strategy: :one_for_one) - results = Task.Supervisor.async_stream_nolink( - sup, + Archethic.TaskSupervisor, recipients, &fetch_recipient_tx_and_simulate(&1, tx), on_timeout: :kill_task, From 6bfb8e2076d7d5f2eec808a56a4794736a489059 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Mon, 27 Mar 2023 15:29:47 +0200 Subject: [PATCH 08/13] LINT: split Archethic.parse_and_execute_contract_at into 2 functions --- lib/archethic.ex | 24 +++++---- lib/archethic/contracts/interpreter.ex | 19 +++++++ .../controllers/api/transaction_controller.ex | 53 +++++++++++-------- 3 files changed, 63 insertions(+), 33 deletions(-) diff --git a/lib/archethic.ex b/lib/archethic.ex index e0fdc4b06..909e4c634 100644 --- a/lib/archethic.ex +++ b/lib/archethic.ex @@ -309,12 +309,20 @@ defmodule Archethic do end @doc """ - Parse and execute the contract in the given transaction. + 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 parse_and_execute_contract_at( + @spec execute_contract( Contract.trigger_type(), - Transaction.t(), + Contract.t(), nil | Transaction.t() ) :: {:ok, nil | Transaction.t()} @@ -323,13 +331,9 @@ defmodule Archethic do | :invalid_transaction_constraints | :invalid_oracle_constraints | :invalid_inherit_constraints} - def parse_and_execute_contract_at( - trigger_type, - contract_tx, - maybe_tx - ) do - Interpreter.execute(trigger_type, Contract.from_transaction!(contract_tx), maybe_tx) - end + 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/interpreter.ex b/lib/archethic/contracts/interpreter.ex index cb11b44e5..bc9e115db 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -52,6 +52,25 @@ defmodule Archethic.Contracts.Interpreter do 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} + end + end + @doc """ Sanitize code takes care of converting atom to {:atom, bin()}. This way the user cannot create atoms at all. (which is mandatory to avoid atoms-table exhaustion) diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index 5482456f5..b1f1b1018 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -217,30 +217,37 @@ defmodule ArchethicWeb.API.TransactionController do end defp fetch_recipient_tx_and_simulate(recipient_address, tx) do - case Archethic.search_transaction(recipient_address) do - {:ok, contract_tx} -> - # this endpoint is only used for transaction triggers - case Archethic.parse_and_execute_contract_at(:transaction, contract_tx, tx) do - {:ok, _} -> - # contract may have returned a transaction or not, in both case it's valid - :ok - - {: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."} - end + with {:ok, contract_tx} <- Archethic.search_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."} - {:error, reason} -> + # parse_contract errors + {:error, reason} when is_binary(reason) -> {:error, reason} end end From 133e5cef2eeb14810b781d0086745c2ffc23dc9f Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Mon, 27 Mar 2023 17:03:10 +0200 Subject: [PATCH 09/13] LINT: refactor worker for explicit workflow --- lib/archethic/contracts/worker.ex | 117 +++++++++++++++++++----------- 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/lib/archethic/contracts/worker.ex b/lib/archethic/contracts/worker.ex index 3925aee54..ae5614ca8 100644 --- a/lib/archethic/contracts/worker.ex +++ b/lib/archethic/contracts/worker.ex @@ -79,7 +79,22 @@ defmodule Archethic.Contracts.Worker do {:execute, incoming_tx = %Transaction{}}, state = %{contract: contract} ) do - _ = do_execute_trigger_and_handle_result(:transaction, contract, incoming_tx) + 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("Contract execution failed", meta) + end {:noreply, state} end @@ -89,7 +104,22 @@ defmodule Archethic.Contracts.Worker do {:trigger, trigger_type = {:datetime, _}}, state = %{contract: contract} ) do - _ = do_execute_trigger_and_handle_result(trigger_type, contract) + 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, trigger_type))} end @@ -99,7 +129,22 @@ defmodule Archethic.Contracts.Worker do {:trigger, trigger_type = {:interval, interval}}, state = %{contract: contract} ) do - _ = do_execute_trigger_and_handle_result(trigger_type, contract) + 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)} @@ -110,9 +155,23 @@ defmodule Archethic.Contracts.Worker do {:new_transaction, tx_address, :oracle, _timestamp}, state = %{contract: contract} ) do + contract_tx = Constants.to_transaction(contract.constants.contract) {:ok, oracle_tx} = TransactionChain.get_transaction(tx_address) - _ = do_execute_trigger_and_handle_result(:oracle, contract, oracle_tx) + 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} end @@ -295,47 +354,17 @@ defmodule Archethic.Contracts.Worker do end end - # no one use the return value - defp do_execute_trigger_and_handle_result( - trigger_type, - contract = %Contract{}, - maybe_tx \\ nil - ) do - contract_tx = Constants.to_transaction(contract.constants.contract) - - if enough_funds?(contract_tx.address) do - log_metadata = - case maybe_tx do - nil -> - [contract: Base.encode16(contract_tx.address)] - - incoming_tx -> - [ - transaction_address: Base.encode16(incoming_tx.address), - transaction_type: incoming_tx.type, - contract: Base.encode16(contract_tx.address) - ] - end - - Logger.info("Execute contract trigger: #{trigger_type}", log_metadata) - - # keep similar behaviour as legacy, we don't check the inherit condition in the worker - case Interpreter.execute(trigger_type, contract, maybe_tx, skip_inherit_check?: true) do - {:ok, nil} -> - :ok + defp log_metadata(contract_tx), do: log_metadata(contract_tx, nil) - {:ok, next_tx} -> - with {:ok, next_tx} <- chain_transaction(next_tx, contract_tx), - :ok <- ensure_enough_funds(next_tx, contract_tx.address) do - handle_new_transaction(next_tx) - end + defp log_metadata(contract_tx, nil) do + [contract: Base.encode16(contract_tx.address)] + end - {:error, reason} -> - Logger.debug("Contract execution failed, reason: #{inspect(reason)}", log_metadata) - :error - end - else - :error - 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 From d4b9797621abefeb847c8075ba4a053a9d1be5ff Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Mon, 27 Mar 2023 17:53:31 +0200 Subject: [PATCH 10/13] LINT: split execute_trigger_and_validate_inherit_condition into 2 functions --- lib/archethic/contracts/interpreter.ex | 126 +++++++++++++++---------- 1 file changed, 76 insertions(+), 50 deletions(-) diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index bc9e115db..5e39faef6 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -222,23 +222,32 @@ defmodule Archethic.Contracts.Interpreter do trigger_code, contract, incoming_tx = %Transaction{}, - %Contract{version: version, conditions: conditions}, + %Contract{ + version: version, + conditions: conditions, + constants: %Constants{ + contract: contract_constants + } + }, opts ) do constants = %{ "transaction" => Constants.from_transaction(incoming_tx), - "contract" => contract.constants.contract + "contract" => contract_constants } if valid_conditions?(version, conditions.transaction, constants) do - execute_trigger_and_validate_inherit_condition( - version, - trigger_code, - contract, - incoming_tx, - conditions.inherit, - opts - ) + 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 @@ -249,23 +258,32 @@ defmodule Archethic.Contracts.Interpreter do trigger_code, contract, oracle_tx = %Transaction{}, - %Contract{version: version, conditions: conditions}, + %Contract{ + version: version, + conditions: conditions, + constants: %Constants{ + contract: contract_constants + } + }, opts ) do constants = %{ "transaction" => Constants.from_transaction(oracle_tx), - "contract" => contract.constants.contract + "contract" => contract_constants } if valid_conditions?(version, conditions.oracle, constants) do - execute_trigger_and_validate_inherit_condition( - version, - trigger_code, - contract, - oracle_tx, - conditions.inherit, - opts - ) + 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 @@ -276,26 +294,27 @@ defmodule Archethic.Contracts.Interpreter do trigger_code, contract, nil, - %Contract{version: version, conditions: conditions}, + %Contract{version: version}, opts ) do - execute_trigger_and_validate_inherit_condition( - version, - trigger_code, - contract, - nil, - conditions.inherit, - opts - ) + 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_and_validate_inherit_condition( + defp execute_trigger( version, trigger_code, contract, - maybe_tx, - inherit_condition, - opts + maybe_tx \\ nil ) do constants_trigger = %{ "transaction" => @@ -309,28 +328,35 @@ defmodule Archethic.Contracts.Interpreter do case execute_trigger_code(version, trigger_code, constants_trigger) do nil -> # contract did not produce a next_tx - {:ok, nil} + nil next_tx_to_prepare -> # contract produce a next_tx but we need to feed previous values to it - next_tx = - chain_transaction( - Constants.to_transaction(contract.constants.contract), - next_tx_to_prepare - ) - - constants_inherit = %{ - "previous" => contract.constants.contract, - "next" => Constants.from_transaction(next_tx) - } + chain_transaction( + Constants.to_transaction(contract.constants.contract), + next_tx_to_prepare + ) + end + end - skip_inherit_check? = Keyword.get(opts, :skip_inherit_check?, false) + 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) + } - if skip_inherit_check? or valid_conditions?(version, inherit_condition, constants_inherit) do - {:ok, next_tx} - else - {:error, :invalid_inherit_constraints} - end + valid_conditions?(version, condition_inherit, constants_inherit) end end From 70e1b8e6c01c27a7630cc0480b7b6b4bd849b8eb Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 31 Mar 2023 14:34:48 +0200 Subject: [PATCH 11/13] add an error when missing recipients --- .../controllers/api/transaction_controller.ex | 16 +++++++-- .../api/transaction_controller_test.exs | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index b1f1b1018..6933bb78e 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -199,9 +199,19 @@ defmodule ArchethicWeb.API.TransactionController do end) |> Enum.to_list() - conn - |> put_status(:ok) - |> json(results) + 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 = diff --git a/test/archethic_web/controllers/api/transaction_controller_test.exs b/test/archethic_web/controllers/api/transaction_controller_test.exs index 961fccc42..a5f7b7228 100644 --- a/test/archethic_web/controllers/api/transaction_controller_test.exs +++ b/test/archethic_web/controllers/api/transaction_controller_test.exs @@ -505,5 +505,38 @@ defmodule ArchethicWeb.API.TransactionControllerTest do ) ) 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 From a4f1d4f9090c2d7b5977ba8c762e507f099de36e Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 31 Mar 2023 14:54:51 +0200 Subject: [PATCH 12/13] simulate contract will fetch the latest contract of the chain --- .../controllers/api/transaction_controller.ex | 3 +- .../api/transaction_controller_test.exs | 58 ++++++------------- 2 files changed, 19 insertions(+), 42 deletions(-) diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index 6933bb78e..a90b841c5 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -227,7 +227,8 @@ defmodule ArchethicWeb.API.TransactionController do end defp fetch_recipient_tx_and_simulate(recipient_address, tx) do - with {:ok, contract_tx} <- Archethic.search_transaction(recipient_address), + with {:ok, last_address} <- Archethic.get_last_transaction_address(recipient_address), + {:ok, contract_tx} <- Archethic.search_transaction(last_address), {:ok, contract} <- Archethic.parse_contract(contract_tx), {:ok, _} <- Archethic.execute_contract(:transaction, contract, tx) do :ok diff --git a/test/archethic_web/controllers/api/transaction_controller_test.exs b/test/archethic_web/controllers/api/transaction_controller_test.exs index a5f7b7228..f50207132 100644 --- a/test/archethic_web/controllers/api/transaction_controller_test.exs +++ b/test/archethic_web/controllers/api/transaction_controller_test.exs @@ -10,6 +10,8 @@ defmodule ArchethicWeb.API.TransactionControllerTest do 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 @@ -106,7 +108,7 @@ defmodule ArchethicWeb.API.TransactionControllerTest do end describe "simulate_contract_execution/2" do - test "should return sucessfull answer when asked to validate a valid contract", %{conn: conn} do + test "should validate the latest contract from the chain", %{conn: conn} do code = """ condition inherit: [ type: transfer, @@ -124,18 +126,14 @@ defmodule ArchethicWeb.API.TransactionControllerTest do end """ - previous_tx1 = %Transaction{ - address: "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", - type: :transfer, - data: %TransactionData{ - code: code, - content: "hello" - }, - version: 1 - } + # 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) - previous_tx2 = %Transaction{ - address: "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d66", + contract_tx = %Transaction{ + address: last_contract_address_hex, type: :transfer, data: %TransactionData{ code: code, @@ -146,34 +144,18 @@ defmodule ArchethicWeb.API.TransactionControllerTest do 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: ^last_contract_address}, _ -> + {:ok, contract_tx} - _, - %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} + _, %GetLastTransactionAddress{address: ^old_contract_address}, _ -> + {:ok, %LastTransactionAddress{address: last_contract_address}} end) new_tx = %{ "address" => "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67", "data" => %{ - "content" => "0000", - "recipients" => [ - "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d66", - "00009e059e8171643b959284fe542909f3b32198b8fc25b3e50447589b84341c1d67" - ] + "content" => "", + "recipients" => [old_contract_address_hex] }, "originSignature" => "3045022024f8d254671af93f8b9c11b5a2781a4a7535d2e89bad69d6b1f142f8f4bcf489022100c364e10f5f846b2534a7ace4aeaa1b6c8cb674f842b9f8bc78225dfa61cabec6", @@ -192,13 +174,7 @@ defmodule ArchethicWeb.API.TransactionControllerTest do [ %{ "valid" => true, - "recipient_address" => - "00009E059E8171643B959284FE542909F3B32198B8FC25B3E50447589B84341C1D66" - }, - %{ - "valid" => true, - "recipient_address" => - "00009E059E8171643B959284FE542909F3B32198B8FC25B3E50447589B84341C1D67" + "recipient_address" => ^old_contract_address_hex } ], json_response(conn, 200) From 193114ee2174da3f937a952c3a49919c5f19d536 Mon Sep 17 00:00:00 2001 From: Bastien CHAMAGNE Date: Fri, 31 Mar 2023 16:30:46 +0200 Subject: [PATCH 13/13] LINT: use get_last_transaction/1 --- lib/archethic_web/controllers/api/transaction_controller.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/archethic_web/controllers/api/transaction_controller.ex b/lib/archethic_web/controllers/api/transaction_controller.ex index a90b841c5..b8d2da34a 100644 --- a/lib/archethic_web/controllers/api/transaction_controller.ex +++ b/lib/archethic_web/controllers/api/transaction_controller.ex @@ -227,8 +227,7 @@ defmodule ArchethicWeb.API.TransactionController do end defp fetch_recipient_tx_and_simulate(recipient_address, tx) do - with {:ok, last_address} <- Archethic.get_last_transaction_address(recipient_address), - {:ok, contract_tx} <- Archethic.search_transaction(last_address), + 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