Skip to content

Commit

Permalink
Improve smart contract interpreter(#628)
Browse files Browse the repository at this point in the history
To simplify the interpreter, it is now built of two interpreters:
* Add Condition's interpreter
* Add Action's interpreter

Hence, the validation is easier to handle and to customize. 

This PR brings other improvements:
* Update interpreters
* Uniform the binary/hex handling in smart contract
* Fix SC call inputs serialization
* Clean inputs ETS table for SC calls
* Clear pending transactions table

Co-authored-by: Neylix <julien.leclerc05@protonmail.com>
  • Loading branch information
samuelmanzanera and Neylix committed Dec 2, 2022
1 parent 5fa619b commit 0d5fbd6
Show file tree
Hide file tree
Showing 34 changed files with 2,596 additions and 2,217 deletions.
2 changes: 0 additions & 2 deletions .dialyzer_ignore.exs

This file was deleted.

2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ jobs:
mkdir -p priv/plts
mix dialyzer --plt
- name: Run dialyzer
run: mix dialyzer --no-check
run: mix dialyzer --no-check --ignore-exit-status
4 changes: 4 additions & 0 deletions lib/archethic/account/mem_tables_loader.ex
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ defmodule Archethic.Account.MemTablesLoader do
}
} ->
TokenLedger.add_unspent_output(address, unspent_output)

_ ->
# Ignore smart contract calls
:ignore
end)
end

Expand Down
44 changes: 23 additions & 21 deletions lib/archethic/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ defmodule Archethic.Contracts do
"""

alias __MODULE__.Contract
alias __MODULE__.Contract.Conditions
alias __MODULE__.Contract.Constants
alias __MODULE__.Contract.Trigger
alias __MODULE__.ContractConditions, as: Conditions
alias __MODULE__.ContractConstants, as: Constants
alias __MODULE__.ConditionInterpreter
alias __MODULE__.Interpreter
alias __MODULE__.Loader
alias __MODULE__.TransactionLookup
Expand Down Expand Up @@ -59,9 +59,8 @@ defmodule Archethic.Contracts do
contract: nil,
transaction: nil
},
triggers: [
%Trigger{
actions: {:__block__, [], [
triggers: %{
{:datetime, ~U[2020-09-25 13:18:43Z]} => {:__block__, [], [
{
:=,
[line: 7],
Expand All @@ -79,11 +78,9 @@ defmodule Archethic.Contracts do
]
}
]},
opts: [at: ~U[2020-09-25 13:18:43Z]],
type: :datetime
}
]
}}
}
}
"""
@spec parse(binary()) :: {:ok, Contract.t()} | {:error, binary()}
def parse(contract_code) when is_binary(contract_code) do
Expand All @@ -100,11 +97,10 @@ defmodule Archethic.Contracts do
})

cond do
Enum.any?(triggers, &(&1.type == :transaction)) and
Conditions.empty?(transaction_conditions) ->
Map.has_key?(triggers, :transaction) and Conditions.empty?(transaction_conditions) ->
{:error, "missing transaction conditions"}

Enum.any?(triggers, &(&1.type == :oracle)) and Conditions.empty?(oracle_conditions) ->
Map.has_key?(triggers, :oracle) and Conditions.empty?(oracle_conditions) ->
{:error, "missing oracle conditions"}

true ->
Expand Down Expand Up @@ -168,18 +164,20 @@ defmodule Archethic.Contracts do
end

defp validate_conditions(inherit_conditions, constants) do
if Interpreter.valid_conditions?(inherit_conditions, constants) do
if ConditionInterpreter.valid_conditions?(inherit_conditions, constants) do
:ok
else
Logger.error("Inherit constraints not respected")
{:error, :invalid_inherit_constraints}
end
end

defp validate_triggers([], _, _), do: :ok
defp validate_triggers(triggers, _next_tx, _date) when map_size(triggers) == 0, do: :ok

defp validate_triggers(triggers, next_tx, date) do
if Enum.any?(triggers, &valid_from_trigger?(&1, next_tx, date)) do
if Enum.any?(triggers, fn {trigger_type, _} ->
valid_from_trigger?(trigger_type, next_tx, date)
end) do
:ok
else
Logger.error("Transaction not processed by a valid smart contract trigger")
Expand All @@ -188,7 +186,7 @@ defmodule Archethic.Contracts do
end

defp valid_from_trigger?(
%Trigger{type: :datetime, opts: [at: datetime]},
{:datetime, datetime},
%Transaction{},
validation_date = %DateTime{}
) do
Expand All @@ -198,21 +196,25 @@ defmodule Archethic.Contracts do
end

defp valid_from_trigger?(
%Trigger{type: :interval, opts: [at: interval]},
{:interval, interval},
%Transaction{},
validation_date = %DateTime{}
) do
interval
|> CronParser.parse!(true)
|> CronParser.parse!()
|> CronDateChecker.matches_date?(DateTime.to_naive(validation_date))
end

defp valid_from_trigger?(%Trigger{type: :transaction}, _, _), do: true
defp valid_from_trigger?(_, _, _), do: true

@doc """
List the address of the transaction which has contacted a smart contract
"""
@spec list_contract_transactions(binary()) :: list({binary(), DateTime.t()})
@spec list_contract_transactions(contract_address :: binary()) ::
list(
{transaction_address :: binary(), transaction_timestamp :: DateTime.t(),
protocol_version :: non_neg_integer()}
)
defdelegate list_contract_transactions(address),
to: TransactionLookup,
as: :list_contract_transactions
Expand Down
54 changes: 16 additions & 38 deletions lib/archethic/contracts/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ defmodule Archethic.Contracts.Contract do
Represents a smart contract
"""

alias Archethic.Contracts.Contract.Conditions
alias Archethic.Contracts.Contract.Constants
alias Archethic.Contracts.Contract.Trigger
alias Archethic.Contracts.ContractConditions, as: Conditions
alias Archethic.Contracts.ContractConstants, as: Constants

alias Archethic.Contracts.Interpreter

Expand All @@ -14,7 +13,7 @@ defmodule Archethic.Contracts.Contract do
alias Archethic.TransactionChain.Transaction
alias Archethic.TransactionChain.TransactionData

defstruct triggers: [],
defstruct triggers: %{},
conditions: %{
transaction: %Conditions{},
inherit: %Conditions{},
Expand All @@ -23,12 +22,15 @@ defmodule Archethic.Contracts.Contract do
constants: %Constants{},
next_transaction: %Transaction{data: %TransactionData{}}

@type trigger_type() :: :datetime | :interval | :transaction
@type trigger_type() ::
{:datetime, DateTime.t()} | {:interval, String.t()} | :transaction | :oracle
@type condition() :: :transaction | :inherit | :oracle
@type origin_family :: SharedSecrets.origin_family()

@type t() :: %__MODULE__{
triggers: list(Trigger.t()),
triggers: %{
trigger_type() => Macro.t()
},
conditions: %{
transaction: Conditions.t(),
inherit: Conditions.t(),
Expand All @@ -55,48 +57,24 @@ defmodule Archethic.Contracts.Contract do
@doc """
Add a trigger to the contract
"""
@spec add_trigger(t(), Trigger.type(), Keyword.t(), Macro.t()) :: t()
@spec add_trigger(map(), trigger_type(), Macro.t()) :: t()
def add_trigger(
contract = %__MODULE__{},
:datetime,
opts = [at: _datetime = %DateTime{}],
type,
actions
) do
do_add_trigger(contract, %Trigger{type: :datetime, opts: opts, actions: actions})
end

def add_trigger(
contract = %__MODULE__{},
:interval,
opts = [at: interval],
actions
)
when is_binary(interval) do
do_add_trigger(contract, %Trigger{type: :interval, opts: opts, actions: actions})
end

def add_trigger(contract = %__MODULE__{}, :transaction, _, actions) do
do_add_trigger(contract, %Trigger{type: :transaction, actions: actions})
end

def add_trigger(contract = %__MODULE__{}, :oracle, _, actions) do
do_add_trigger(contract, %Trigger{type: :oracle, actions: actions})
end

defp do_add_trigger(contract, trigger = %Trigger{}) do
Map.update!(contract, :triggers, &(&1 ++ [trigger]))
Map.update!(contract, :triggers, &Map.put(&1, type, actions))
end

@doc """
Add a condition to the contract
"""
@spec add_condition(t(), condition(), any()) :: t()
@spec add_condition(map(), condition(), Conditions.t()) :: t()
def add_condition(
contract = %__MODULE__{conditions: conditions},
contract = %__MODULE__{},
condition_name,
condition
)
when condition_name in [:transaction, :inherit, :oracle] do
%{contract | conditions: Map.put(conditions, condition_name, condition)}
conditions = %Conditions{}
) do
Map.update!(contract, :conditions, &Map.put(&1, condition_name, conditions))
end
end
5 changes: 4 additions & 1 deletion lib/archethic/contracts/contract/conditions.ex
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
defmodule Archethic.Contracts.Contract.Conditions do
defmodule Archethic.Contracts.ContractConditions do
@moduledoc """
Represents the smart contract conditions
"""

defstruct [
:address,
:type,
:content,
:code,
Expand All @@ -20,6 +21,7 @@ defmodule Archethic.Contracts.Contract.Conditions do
alias Archethic.TransactionChain.Transaction

@type t :: %__MODULE__{
address: binary() | Macro.t() | nil,
type: Transaction.transaction_type() | nil,
content: binary() | Macro.t() | nil,
code: binary() | Macro.t() | nil,
Expand All @@ -33,6 +35,7 @@ defmodule Archethic.Contracts.Contract.Conditions do
}

def empty?(%__MODULE__{
address: nil,
type: nil,
content: nil,
code: nil,
Expand Down
67 changes: 65 additions & 2 deletions lib/archethic/contracts/contract/constants.ex
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
defmodule Archethic.Contracts.Contract.Constants do
defmodule Archethic.Contracts.ContractConstants do
@moduledoc """
Represents the smart contract constants and bindings
"""

defstruct [:contract, :transaction]

@type t :: %__MODULE__{
contract: map(),
contract: map() | nil,
transaction: map() | nil
}

Expand Down Expand Up @@ -153,4 +153,67 @@ defmodule Archethic.Contracts.Contract.Constants do
previous_public_key: Map.get(constants, "previous_public_key")
}
end

@doc """
Stringify binary transaction values
"""
@spec stringify(map()) :: map()
def stringify(constants = %{}) do
%{
"address" => apply_not_nil(constants, "address", &Base.encode16/1),
"type" => Map.get(constants, "type"),
"content" => Map.get(constants, "content"),
"code" => Map.get(constants, "code"),
"authorized_keys" =>
apply_not_nil(constants, "authorized_keys", fn authorized_keys ->
authorized_keys
|> Enum.map(fn {public_key, encrypted_secret_key} ->
{Base.encode16(public_key), Base.encode16(encrypted_secret_key)}
end)
|> Enum.into(%{})
end),
"authorized_public_keys" =>
apply_not_nil(constants, "authorized_public_keys", fn public_keys ->
Enum.map(public_keys, &Base.encode16/1)
end),
"secrets" =>
apply_not_nil(constants, "secrets", fn secrets ->
Enum.map(secrets, &Base.encode16/1)
end),
"previous_public_key" => apply_not_nil(constants, "previous_public_key", &Base.encode16/1),
"recipients" =>
apply_not_nil(constants, "recipients", fn recipients ->
Enum.map(recipients, &Base.encode16/1)
end),
"uco_transfers" =>
apply_not_nil(constants, "uco_transfers", fn transfers ->
transfers
|> Enum.map(fn {to, amount} ->
{Base.encode16(to), amount}
end)
|> Enum.into(%{})
end),
"token_transfers" =>
apply_not_nil(constants, "token_transfers", fn transfers ->
transfers
|> Enum.map(fn {to, transfers} ->
{Base.encode16(to),
Enum.map(transfers, fn transfer ->
Map.update!(transfer, "token_address", &Base.encode16/1)
end)}
end)
end),
"timestamp" => Map.get(constants, "timestamp")
}
end

defp apply_not_nil(map, key, fun) do
case Map.get(map, key) do
nil ->
nil

val ->
fun.(val)
end
end
end
19 changes: 0 additions & 19 deletions lib/archethic/contracts/contract/trigger.ex

This file was deleted.

Loading

0 comments on commit 0d5fbd6

Please sign in to comment.