Skip to content

Commit

Permalink
Validate Smart Contracts implementation before the SC merge
Browse files Browse the repository at this point in the history
  • Loading branch information
netboz authored and bchamagne committed Mar 15, 2023
1 parent 9c935db commit 736c775
Show file tree
Hide file tree
Showing 10 changed files with 755 additions and 29 deletions.
31 changes: 29 additions & 2 deletions lib/archethic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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, TransactionSummaryList}
alias Message.{Balance, GetBalance, GetCurrentSummaries, GetTransactionSummary}
alias Message.{StartMining, Ok, TransactionSummaryMessage}

alias TransactionChain.{Transaction, TransactionInput, TransactionSummary}
alias TransactionChain.{
Transaction,
TransactionInput,
TransactionSummary,
TransactionData
}

require Logger

Expand Down Expand Up @@ -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
"""
Expand Down
133 changes: 133 additions & 0 deletions lib/archethic/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -165,6 +167,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
Expand Down
4 changes: 2 additions & 2 deletions lib/archethic/contracts/interpreter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 23 additions & 23 deletions lib/archethic/contracts/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -380,44 +380,44 @@ 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)],
previous_ownerships
)
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
Expand Down
85 changes: 85 additions & 0 deletions lib/archethic_web/controllers/api/transaction_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions lib/archethic_web/explorer_router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading

0 comments on commit 736c775

Please sign in to comment.