Skip to content

Commit

Permalink
Invalid Recipient Execution now is a tuple with its reason
Browse files Browse the repository at this point in the history
  • Loading branch information
bchamagne committed Dec 7, 2023
1 parent a01cf9c commit 4a568c3
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 71 deletions.
7 changes: 6 additions & 1 deletion lib/archethic/contracts/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,12 @@ defmodule Archethic.Contracts.Interpreter do
) ::
{:ok, nil | Transaction.t(), State.t(), logs :: list(String.t())}
| {:error, err :: String.t()}
| {:error, err :: Exception.t(), stacktrace :: term(), logs :: list(String.t())}
| {
:error,
err :: Exception.t() | String.t(),
stacktrace :: term(),
logs :: list(String.t())
}

def execute_trigger(
trigger_key,
Expand Down
4 changes: 3 additions & 1 deletion lib/archethic/contracts/interpreter/condition_validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ defmodule Archethic.Contracts.Interpreter.ConditionValidator do
Determines if the conditions of a contract are valid from the given constants
"""
@spec execute_condition(Macro.t() | ConditionsSubjects.t(), map()) ::
{:ok, list(String.t())} | {:error, String.t(), list(String.t())}
{:ok, list(String.t())}
| {:error, subject :: String.t(), logs :: list(String.t())}
| {:error, subject :: String.t(), message :: String.t(), logs :: list(String.t())}
def execute_condition(condition_or_ast, constants = %{}) do
case condition_or_ast do
%ConditionsSubjects{} ->
Expand Down
3 changes: 3 additions & 0 deletions lib/archethic/mining/distributed_workflow.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1158,6 +1158,9 @@ defmodule Archethic.Mining.DistributedWorkflow do
:invalid_proof_of_work ->
{:invalid_transaction, "Invalid origin signature"}

{:invalid_recipients_execution, reason} ->
{:invalid_transaction, "Invalid recipient execution: #{reason}"}

reason ->
{:network_issue, reason |> Atom.to_string() |> String.replace("_", " ")}
end
Expand Down
28 changes: 16 additions & 12 deletions lib/archethic/mining/smart_contract_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ defmodule Archethic.Mining.SmartContractValidation do
recipients :: list(Recipient.t()),
transaction :: Transaction.t(),
validation_time :: DateTime.t()
) :: {true, fee :: non_neg_integer()} | {false, 0}
) :: {:ok, fee :: non_neg_integer()} | {:error, String.t()}
def validate_contract_calls(
recipients,
transaction = %Transaction{},
Expand All @@ -49,9 +49,13 @@ defmodule Archethic.Mining.SmartContractValidation do
ordered: false,
on_timeout: :kill_task
)
|> Enum.reduce_while({true, 0}, fn
{:ok, {_valid? = true, fee}}, {true, total_fee} -> {:cont, {true, total_fee + fee}}
_, _ -> {:halt, {false, 0}}
|> Enum.reduce_while(0, fn
{:ok, %SmartContractCallValidation{valid?: true, fee: fee}}, acc -> {:cont, acc + fee}
{:ok, %SmartContractCallValidation{valid?: false, reason: reason}}, _acc -> {:halt, reason}
end)
|> then(fn
n when is_integer(n) -> {:ok, n}
reason when is_binary(reason) -> {:error, reason}
end)
end

Expand Down Expand Up @@ -216,13 +220,10 @@ defmodule Archethic.Mining.SmartContractValidation do
) do
storage_nodes = Election.chain_storage_nodes(address, P2P.authorized_and_available_nodes())

# We are strict on the results to achieve atomic commitment
conflicts_resolver = fn results ->
if Enum.any?(results, &(&1.valid? == false)) do
%SmartContractCallValidation{valid?: false}
else
%SmartContractCallValidation{valid?: true}
end
# keep the first invalid result if any
# otherwise take the first
Enum.find(results, List.first(results), &(not &1.valid?))
end

case P2P.quorum_read(
Expand All @@ -235,8 +236,11 @@ defmodule Archethic.Mining.SmartContractValidation do
conflicts_resolver,
@timeout
) do
{:ok, %SmartContractCallValidation{valid?: valid?, fee: fee}} -> {valid?, fee}
_ -> {false, 0}
{:ok, call_validation = %SmartContractCallValidation{}} ->
call_validation

{:error, :network_issue} ->
%SmartContractCallValidation{valid?: false, fee: 0, reason: "Network issue"}
end
end
end
3 changes: 3 additions & 0 deletions lib/archethic/mining/standalone_workflow.ex
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ defmodule Archethic.Mining.StandaloneWorkflow do
:invalid_proof_of_work ->
{:invalid_transaction, "Invalid origin signature"}

{:invalid_recipients_execution, reason} ->
{:invalid_transaction, "Invalid recipient execution: #{reason}"}

reason ->
{:network_issue, reason |> Atom.to_string() |> String.replace("_", " ")}
end
Expand Down
39 changes: 26 additions & 13 deletions lib/archethic/mining/validation_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -732,9 +732,15 @@ defmodule Archethic.Mining.ValidationContext do

resolved_recipients = resolved_recipients(recipients, resolved_addresses)

{valid_contract_recipients?, contract_recipients_fee} =
validate_contract_recipients_result =
validate_contract_recipients(tx, resolved_recipients, validation_time)

contract_recipients_fee =
case validate_contract_recipients_result do
{:ok, fee} -> fee
_ -> 0
end

fee =
calculate_fee(
tx,
Expand Down Expand Up @@ -765,7 +771,7 @@ defmodule Archethic.Mining.ValidationContext do
resolved_recipients,
validation_time,
valid_contract_execution?,
valid_contract_recipients?
validate_contract_recipients_result
)
}
|> ValidationStamp.sign()
Expand Down Expand Up @@ -848,7 +854,8 @@ defmodule Archethic.Mining.ValidationContext do
resolved_recipients :: list(Recipient.t()),
validation_time :: DateTime.t(),
valid_contract_execution? :: boolean(),
valid_contract_recipients? :: boolean()
validate_contract_recipients_result ::
{:ok, fee :: non_neg_integer()} | {:error, String.t()}
) :: nil | ValidationStamp.error()
defp get_validation_error(
prev_tx,
Expand All @@ -858,7 +865,7 @@ defmodule Archethic.Mining.ValidationContext do
resolved_recipients,
validation_time,
valid_contract_execution?,
valid_contract_recipients?
validate_contract_recipients_result
) do
cond do
not valid_pending_transaction? ->
Expand All @@ -876,11 +883,11 @@ defmodule Archethic.Mining.ValidationContext do
not valid_contract_recipients_distinct?(resolved_recipients) ->
:recipients_not_distinct

not valid_contract_recipients? ->
:invalid_recipients_execution

true ->
nil
case validate_contract_recipients_result do
{:ok, _} -> nil
{:error, reason} -> {:invalid_recipients_execution, reason}
end
end
end

Expand All @@ -890,7 +897,7 @@ defmodule Archethic.Mining.ValidationContext do
resolved_recipients_addresses == Enum.uniq(resolved_recipients_addresses)
end

defp validate_contract_recipients(_tx, [], _validation_time), do: {true, 0}
defp validate_contract_recipients(_tx, [], _validation_time), do: {:ok, 0}

defp validate_contract_recipients(tx, resolved_recipients, validation_time),
do: SmartContractValidation.validate_contract_calls(resolved_recipients, tx, validation_time)
Expand Down Expand Up @@ -1104,9 +1111,15 @@ defmodule Archethic.Mining.ValidationContext do
{valid_contract_execution?, next_state} =
SmartContractValidation.valid_contract_execution?(contract_context, prev_tx, tx)

{valid_contract_recipients?, contract_recipients_fee} =
validate_contract_recipients_result =
validate_contract_recipients(tx, resolved_recipients, validation_time)

contract_recipients_fee =
case validate_contract_recipients_result do
{:ok, fee} -> fee
_ -> 0
end

fee =
calculate_fee(
tx,
Expand Down Expand Up @@ -1134,7 +1147,7 @@ defmodule Archethic.Mining.ValidationContext do
stamp,
context,
valid_contract_execution?,
valid_contract_recipients?,
validate_contract_recipients_result,
sufficient_funds?,
resolved_recipients
)
Expand Down Expand Up @@ -1204,7 +1217,7 @@ defmodule Archethic.Mining.ValidationContext do
validation_time: validation_time
},
valid_contract_execution?,
valid_contract_recipients?,
validate_contract_recipients_result,
sufficient_funds?,
resolved_recipients
) do
Expand All @@ -1217,7 +1230,7 @@ defmodule Archethic.Mining.ValidationContext do
resolved_recipients,
validation_time,
valid_contract_execution?,
valid_contract_recipients?
validate_contract_recipients_result
)

error == expected_error
Expand Down
4 changes: 2 additions & 2 deletions lib/archethic/replication/transaction_validator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ defmodule Archethic.Replication.TransactionValidator do
|> SmartContractValidation.validate_contract_calls(tx, timestamp)

case res do
{true, fees} -> {:ok, fees}
{false, _} -> {:error, :invalid_recipients_execution}
{:ok, fees} -> {:ok, fees}
{:error, reason} -> {:error, {:invalid_recipients_execution, reason}}
end
end

Expand Down
54 changes: 33 additions & 21 deletions lib/archethic/transaction_chain/transaction/validation_stamp.ex
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do
| :invalid_inherit_constraints
| :insufficient_funds
| :invalid_contract_execution
| :invalid_recipients_execution
| {:invalid_recipients_execution, String.t()}
| :recipients_not_distinct

@typedoc """
Expand Down Expand Up @@ -180,7 +180,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do
<<version::32, DateTime.to_unix(timestamp, :millisecond)::64, pow::binary, poi::binary,
poe::binary, LedgerOperations.serialize(ledger_operations, version)::bitstring,
encoded_recipients_len::binary, :erlang.list_to_binary(recipients)::binary,
serialize_error(error)::8>>
serialize_error(error)::binary>>
end

def serialize(%__MODULE__{
Expand All @@ -207,7 +207,7 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do
<<version::32, DateTime.to_unix(timestamp, :millisecond)::64, pow::binary, poi::binary,
poe::binary, LedgerOperations.serialize(ledger_operations, version)::bitstring,
encoded_recipients_len::binary, :erlang.list_to_binary(recipients)::binary,
serialize_error(error)::8, byte_size(signature)::8, signature::binary>>
serialize_error(error)::binary, byte_size(signature)::8, signature::binary>>
end

@doc """
Expand Down Expand Up @@ -271,10 +271,9 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do

{recipients_length, rest} = rest |> VarInt.get_value()

{recipients, <<error_byte::8, rest::bitstring>>} =
Utils.deserialize_addresses(rest, recipients_length, [])
{recipients, rest} = Utils.deserialize_addresses(rest, recipients_length, [])

error = deserialize_error(error_byte)
{error, rest} = deserialize_error(rest)

<<signature_size::8, signature::binary-size(signature_size), rest::bitstring>> = rest

Expand Down Expand Up @@ -371,19 +370,32 @@ defmodule Archethic.TransactionChain.Transaction.ValidationStamp do
}
end

defp serialize_error(nil), do: 0
defp serialize_error(:invalid_pending_transaction), do: 1
defp serialize_error(:invalid_inherit_constraints), do: 2
defp serialize_error(:insufficient_funds), do: 3
defp serialize_error(:invalid_contract_execution), do: 4
defp serialize_error(:invalid_recipients_execution), do: 5
defp serialize_error(:recipients_not_distinct), do: 6

defp deserialize_error(0), do: nil
defp deserialize_error(1), do: :invalid_pending_transaction
defp deserialize_error(2), do: :invalid_inherit_constraints
defp deserialize_error(3), do: :insufficient_funds
defp deserialize_error(4), do: :invalid_contract_execution
defp deserialize_error(5), do: :invalid_recipients_execution
defp deserialize_error(6), do: :recipients_not_distinct
defp serialize_error(nil), do: <<0::8>>
defp serialize_error(:invalid_pending_transaction), do: <<1::8>>
defp serialize_error(:invalid_inherit_constraints), do: <<2::8>>
defp serialize_error(:insufficient_funds), do: <<3::8>>
defp serialize_error(:invalid_contract_execution), do: <<4::8>>

defp serialize_error({:invalid_recipients_execution, reason}) do
reason_length = byte_size(reason)

reason_length_serialized = VarInt.from_value(reason_length)
<<5::8, reason_length_serialized::binary, reason::binary>>
end

defp serialize_error(:recipients_not_distinct), do: <<6::8>>

defp deserialize_error(<<0::8, rest::bitstring>>), do: {nil, rest}
defp deserialize_error(<<1::8, rest::bitstring>>), do: {:invalid_pending_transaction, rest}
defp deserialize_error(<<2::8, rest::bitstring>>), do: {:invalid_inherit_constraints, rest}
defp deserialize_error(<<3::8, rest::bitstring>>), do: {:insufficient_funds, rest}
defp deserialize_error(<<4::8, rest::bitstring>>), do: {:invalid_contract_execution, rest}

defp deserialize_error(<<5::8, rest::bitstring>>) do
{reason_length, rest} = VarInt.get_value(rest)
<<reason::binary-size(reason_length), rest::bitstring>> = rest
{{:invalid_recipients_execution, reason}, rest}
end

defp deserialize_error(<<6::8, rest::bitstring>>), do: {:recipients_not_distinct, rest}
end
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,11 @@ defmodule ArchethicWeb.API.JsonRPC.Method.EstimateTransactionFee do

resolved_recipients = resolve_recipient_addresses(tx, timestamp)

{_valid?, recipients_fee} =
SmartContractValidation.validate_contract_calls(resolved_recipients, tx, timestamp)
recipients_fee =
case SmartContractValidation.validate_contract_calls(resolved_recipients, tx, timestamp) do
{:ok, fees} -> fees
_ -> 0
end

# not possible to have a contract's state here
fee = Mining.get_transaction_fee(tx, nil, uco_usd, timestamp, nil, recipients_fee)
Expand Down
Loading

0 comments on commit 4a568c3

Please sign in to comment.