Skip to content

Commit

Permalink
Contract execution return explicit errors instead of contract_failure (
Browse files Browse the repository at this point in the history
…#1267)

* Contract execution return explicit errors instead of contract_failure

* fix after rebase
  • Loading branch information
bchamagne committed Sep 13, 2023
1 parent 41a0b6d commit 9fcb9e1
Show file tree
Hide file tree
Showing 9 changed files with 115 additions and 64 deletions.
4 changes: 1 addition & 3 deletions lib/archethic/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@ defmodule Archethic.Contracts do
nil | Transaction.t(),
nil | Recipient.t(),
Keyword.t()
) ::
{:ok, nil | Transaction.t()}
| {:error, :contract_failure | :invalid_triggers_execution}
) :: {:ok, nil | Transaction.t()} | {:error, String.t()}
defdelegate execute_trigger(
trigger_type,
contract,
Expand Down
24 changes: 19 additions & 5 deletions lib/archethic/contracts/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ defmodule Archethic.Contracts.Interpreter do
execute_opts()
) ::
{:ok, nil | Transaction.t()}
| {:error, :contract_failure | :invalid_triggers_execution}
| {:error, String.t()}
def execute_trigger(
trigger_key,
%Contract{
Expand All @@ -141,7 +141,7 @@ defmodule Archethic.Contracts.Interpreter do
) do
case Map.get(triggers, trigger_key) do
nil ->
{:error, :invalid_triggers_execution}
{:error, "Trigger not found on the contract"}

%{args: args, ast: trigger_code} ->
timestamp_now =
Expand Down Expand Up @@ -183,9 +183,7 @@ defmodule Archethic.Contracts.Interpreter do
end
rescue
err ->
Logger.error(Exception.format(:error, err, __STACKTRACE__))
# it's ok to loose the error because it's user-code
{:error, :contract_failure}
{:error, error_to_string(err, __STACKTRACE__)}
end

@doc """
Expand Down Expand Up @@ -503,4 +501,20 @@ defmodule Archethic.Contracts.Interpreter do
end
end
end

defp error_to_string(err, stacktrace) do
case Enum.find_value(stacktrace, fn
{_, _, _, [file: 'nofile', line: line]} ->
line

_ ->
false
end) do
line when is_integer(line) ->
Exception.message(err) <> " - L#{line}"

_ ->
Exception.message(err)
end
end
end
55 changes: 32 additions & 23 deletions lib/archethic/contracts/interpreter/common_interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,11 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
# whitelist assignation & write them to scope
# this is done in the prewalk because it must be done before the "variable are read from scope" step
def prewalk(
_node = {:=, _, [{{:atom, var_name}, _, nil}, value]},
_node = {:=, meta, [{{:atom, var_name}, _, nil}, value]},
acc
) do
new_node =
quote do
quote line: Keyword.fetch!(meta, :line) do
Scope.write_cascade(unquote(var_name), unquote(value))
end

Expand All @@ -167,9 +167,12 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
end

# Dot access non-nested (x.y)
def prewalk(_node = {{:., _, [{{:atom, map_name}, _, nil}, {:atom, key_name}]}, _, _}, acc) do
def prewalk(
_node = {{:., meta, [{{:atom, map_name}, _, nil}, {:atom, key_name}]}, _, _},
acc
) do
new_node =
quote do
quote line: Keyword.fetch!(meta, :line) do
Scope.read(unquote(map_name), unquote(key_name))
end

Expand All @@ -178,9 +181,9 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

# Dot access nested (x.y.z)
# or Module.function().z
def prewalk({{:., _, [first_arg, {:atom, key_name}]}, _, []}, acc) do
def prewalk({{:., meta, [first_arg, {:atom, key_name}]}, _, []}, acc) do
new_node =
quote do
quote line: Keyword.fetch!(meta, :line) do
Map.get(unquote(first_arg), unquote(key_name))
end

Expand All @@ -189,12 +192,12 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

# Map access non-nested (x[y])
def prewalk(
_node = {{:., _, [Access, :get]}, _, [{{:atom, map_name}, _, nil}, accessor]},
_node = {{:., meta, [Access, :get]}, _, [{{:atom, map_name}, _, nil}, accessor]},
acc
) do
# accessor can be a variable, a function call, a dot access, a string
new_node =
quote do
quote line: Keyword.fetch!(meta, :line) do
Scope.read(unquote(map_name), unquote(accessor))
end

Expand All @@ -203,11 +206,11 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

# Map access nested (x[y][z])
def prewalk(
_node = {{:., _, [Access, :get]}, _, [first_arg, accessor]},
_node = {{:., meta, [Access, :get]}, _, [first_arg, accessor]},
acc
) do
new_node =
quote do
quote line: Keyword.fetch!(meta, :line) do
Map.get(unquote(first_arg), unquote(accessor))
end

Expand Down Expand Up @@ -244,8 +247,12 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
# log (not documented, only useful for developer debugging)
# TODO: should be implemented in a module Logger (only available if config allows it)
# will soon be updated to log into the playground console
def prewalk(_node = {{:atom, "log"}, _, [data]}, acc) do
new_node = quote do: apply(IO, :inspect, [unquote(data)])
def prewalk(_node = {{:atom, "log"}, meta, [data]}, acc) do
new_node =
quote line: Keyword.fetch!(meta, :line) do
apply(IO, :inspect, [unquote(data)])
end

{new_node, acc}
end

Expand Down Expand Up @@ -274,20 +281,21 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
# ----------------------------------------------------------------------
# exit block == set parent scope
# we need to return user's last expression and not the result of Scope.leave_scope()
# ps: there is no meta in a :__block__
def postwalk(
_node = {:__block__, meta, expressions},
_node = {:__block__, [], expressions},
acc
) do
{last_expression, expressions} = List.pop_at(expressions, -1)

{:__block__, _meta, new_expressions} =
{:__block__, [], new_expressions} =
quote do
result = unquote(last_expression)
Scope.leave_scope()
result
end

{{:__block__, meta, expressions ++ new_expressions}, acc}
{{:__block__, [], expressions ++ new_expressions}, acc}
end

# Module function call
Expand All @@ -307,7 +315,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

new_node =
if Library.function_tagged_with?(module_name, function_name, :write_contract) do
quote do
quote line: Keyword.fetch!(meta, :line) do
# mark the next_tx as dirty
Scope.update_global([:next_transaction_changed], fn _ -> true end)

Expand All @@ -328,11 +336,11 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do

# variable are read from scope
def postwalk(
_node = {{:atom, var_name}, _, nil},
_node = {{:atom, var_name}, meta, nil},
acc
) do
new_node =
quote do
quote line: Keyword.fetch!(meta, :line) do
Scope.read(unquote(var_name))
end

Expand All @@ -342,7 +350,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
# for var in list
def postwalk(
_node =
{{:atom, "for"}, _,
{{:atom, "for"}, meta,
[
{:%{}, _, [{var_name, list}]},
[do: block]
Expand All @@ -356,7 +364,7 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
# transform the for-loop into Enum.each
# and create a variable in the scope
new_node =
quote do
quote line: Keyword.fetch!(meta, :line) do
Enum.each(unquote(list), fn x ->
Scope.write_at(unquote(var_name), x)

Expand All @@ -367,17 +375,18 @@ defmodule Archethic.Contracts.Interpreter.CommonInterpreter do
{new_node, acc}
end

def postwalk({{:atom, function_name}, _, args}, acc) when is_list(args) do
def postwalk({{:atom, function_name}, meta, args}, acc) when is_list(args) do
new_node =
quote do
quote line: Keyword.fetch!(meta, :line) do
Scope.execute_function_ast(unquote(function_name), unquote(args))
end

{new_node, acc}
end

# BigInt mathematics to avoid floating point issues
def postwalk(_node = {ast, meta, [lhs, rhs]}, acc) when ast in [:*, :/, :+, :-] do
def postwalk(_node = {ast, meta, [lhs, rhs]}, acc)
when ast in [:*, :/, :+, :-] do
new_node =
quote line: Keyword.fetch!(meta, :line) do
AST.decimal_arithmetic(unquote(ast), unquote(lhs), unquote(rhs))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.HttpImpl do
{:ok, {:error, :threshold_reached}} ->
raise Library.Error, message: "Http.fetch/1 response is bigger than threshold"

{:ok, {:error, :not_https}} ->
raise Library.Error, message: "Http.fetch/1 was called with a non https url"

{:ok, {:error, _}} ->
# Mint.HTTP.connect error
# Mint.HTTP.stream error
Expand All @@ -49,7 +52,7 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.HttpImpl do

nil ->
# Task.shutdown
raise Library.Error, message: "Http.fetch/1 timed out for url: #{uri}"
raise Library.Error, message: "Http.fetch/1 timed out"
end
end

Expand Down Expand Up @@ -87,6 +90,10 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.HttpImpl do
raise Library.Error,
message: "Http.fetch_many/1 response is bigger than threshold for url: #{uri}"

{{:ok, {:error, :not_https}}, uri}, _ ->
raise Library.Error,
message: "Http.fetch_many/1 was called with a non https url: #{uri}"

{{:ok, {:error, _}}, uri}, _ ->
# Mint.HTTP.connect error
# Mint.HTTP.stream error
Expand Down
1 change: 0 additions & 1 deletion lib/archethic_web/api/jsonrpc/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ defmodule ArchethicWeb.API.JsonRPC.Error do
# Smart Contract context
defp get_custom_code(:contract_failure), do: 203
defp get_custom_code(:no_recipients), do: 204
defp get_custom_code(:invalid_triggers_execution), do: 205
defp get_custom_code(:invalid_transaction_constraints), do: 206
defp get_custom_code(:invalid_inherit_constraints), do: 207
defp get_custom_code(:parsing_contract), do: 208
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,14 +78,34 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do
) do
with {:ok, contract_tx} <-
Archethic.get_last_transaction(recipient_address),
{:ok, contract} <- Contracts.from_transaction(contract_tx),
{:ok, contract} <- validate_and_parse_contract_tx(contract_tx),
trigger <- Contract.get_trigger_for_recipient(recipient),
:ok <- validate_contract_condition(trigger, contract, trigger_tx, recipient, timestamp),
{:ok, next_tx} <- Contracts.execute_trigger(trigger, contract, trigger_tx, recipient) do
{:ok, next_tx} <- validate_and_execute_trigger(trigger, contract, trigger_tx, recipient) do
validate_contract_condition(:inherit, contract, next_tx, nil, timestamp)
end
end

defp validate_and_parse_contract_tx(tx) do
case Contracts.from_transaction(tx) do
{:ok, contract} ->
{:ok, contract}

{:error, reason} ->
{:error, {:parsing_error, reason}}
end
end

def validate_and_execute_trigger(trigger, contract, trigger_tx, recipient) do
case Contracts.execute_trigger(trigger, contract, trigger_tx, recipient) do
{:ok, nil_or_next_tex} ->
{:ok, nil_or_next_tex}

{:error, reason} ->
{:error, {:execute_error, reason}}
end
end

defp create_valid_response(%Recipient{address: recipient_address}) do
%{"recipient_address" => Base.encode16(recipient_address), "valid" => true}
end
Expand All @@ -107,14 +127,6 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do
defp format_reason(:network_issue),
do: {:internal_error, "Cannot fetch contract transaction"}

defp format_reason(:contract_failure),
do: {:custom_error, :contract_failure, "Contract execution encountered an error"}

defp format_reason(:invalid_triggers_execution),
do:
{:custom_error, :invalid_triggers_execution,
"Contract does not contain a trigger transaction"}

defp format_reason(:invalid_transaction_constraints),
do:
{:custom_error, :invalid_transaction_constraints,
Expand All @@ -128,9 +140,12 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do
defp format_reason(:timeout),
do: {:internal_error, "Timeout while simulating contract execution"}

defp format_reason(reason) when is_binary(reason),
defp format_reason({:parsing_error, reason}) when is_binary(reason),
do: {:custom_error, :parsing_contract, "Error while parsing contract", reason}

defp format_reason({:execute_error, reason}) when is_binary(reason),
do: {:custom_error, :contract_failure, "Error while executing contract", reason}

defp format_reason(_), do: {:internal_error, "Unknown error"}

defp validate_contract_condition(condition_type, contract, tx, recipient, timestamp) do
Expand Down
16 changes: 0 additions & 16 deletions lib/archethic_web/api/rest/controllers/transaction_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -248,22 +248,6 @@ defmodule ArchethicWeb.API.REST.TransactionController do
{:error, "Network issue, please try again later."}

# execute_contract errors
{:error, :contract_failure} ->
{:error, "Contract execution produced an error."}

{:error, :invalid_triggers_execution} ->
{:error, "Contract does not have a `actions triggered_by: transaction` block."}

{:error, :invalid_transaction_constraints} ->
{:error,
"Contract refused incoming transaction. Check the `condition transaction` block."}

{:error, :invalid_oracle_constraints} ->
{:error, "Contract refused incoming transaction. Check the `condition oracle` block."}

{:error, :invalid_inherit_constraints} ->
{:error, "Contract refused outcoming transaction. Check the `condition inherit` block."}

# parse_contract errors
{:error, reason} when is_binary(reason) ->
{:error, reason}
Expand Down
Loading

0 comments on commit 9fcb9e1

Please sign in to comment.