Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Contract execution return explicit errors instead of contract_failure #1267

Merged
merged 2 commits into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading