Skip to content

Commit

Permalink
Pr 57 validate smart contract calls (#802)
Browse files Browse the repository at this point in the history
* Validate Smart Contracts implementation before the SC merge

* prettify contract error

* simulate contract function return the next_transaction

* add more unit tests

* better spec

* move simulate into interpreter and refactor the worker

* use existing TaskSupervisor

* LINT: split Archethic.parse_and_execute_contract_at into 2 functions

* LINT: refactor worker for explicit workflow

* LINT: split execute_trigger_and_validate_inherit_condition into 2 functions

* add an error when missing recipients

* simulate contract will fetch the latest contract of the chain

* LINT: use get_last_transaction/1

---------

Co-authored-by: Bastien CHAMAGNE <bastien@chamagne.fr>
  • Loading branch information
netboz and bchamagne committed Mar 31, 2023
1 parent 2562a04 commit 8725d25
Show file tree
Hide file tree
Showing 13 changed files with 1,318 additions and 351 deletions.
35 changes: 34 additions & 1 deletion lib/archethic.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,18 @@ defmodule Archethic do
alias Archethic.SharedSecrets
alias __MODULE__.{Account, BeaconChain, Crypto, Election, P2P, P2P.Node, P2P.Message}
alias __MODULE__.{SelfRepair, TransactionChain}
alias __MODULE__.Contracts.Interpreter
alias __MODULE__.Contracts.Contract

alias Message.{NewTransaction, NotFound, StartMining}
alias Message.{Balance, GetBalance, GetTransactionSummary}
alias Message.{StartMining, Ok, TransactionSummaryMessage}

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

require Logger

Expand Down Expand Up @@ -302,6 +308,33 @@ defmodule Archethic do
end
end

@doc """
Parse the given transaction and return a contract if successful
"""
@spec parse_contract(Transaction.t()) :: {:ok, Contract.t()} | {:error, String.t()}
defdelegate parse_contract(contract_tx),
to: Interpreter,
as: :parse_transaction

@doc """
Execute the contract in the given transaction.
We assume the contract is parse-able.
"""
@spec execute_contract(
Contract.trigger_type(),
Contract.t(),
nil | Transaction.t()
) ::
{:ok, nil | Transaction.t()}
| {:error,
:invalid_triggers_execution
| :invalid_transaction_constraints
| :invalid_oracle_constraints
| :invalid_inherit_constraints}
defdelegate execute_contract(trigger_type, contract, maybe_tx),
to: Interpreter,
as: :execute

@doc """
Retrieve the number of transaction in a transaction chain from the closest nodes
"""
Expand Down
129 changes: 6 additions & 123 deletions lib/archethic/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ defmodule Archethic.Contracts do
"""

alias __MODULE__.Contract
alias __MODULE__.ContractConditions, as: Conditions
alias __MODULE__.ContractConstants, as: Constants
alias __MODULE__.Interpreter
alias __MODULE__.Loader
Expand All @@ -18,134 +17,18 @@ 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

@doc ~S"""
Parse a smart contract code and return its representation
## Examples
iex> "
...> condition inherit: [
...> content: regex_match?(\"^(Mr.X: ){1}([0-9]+), (Mr.Y: ){1}([0-9])+$\"),
...> origin_family: biometric
...> ]
...>
...> actions triggered_by: datetime, at: 1601039923 do
...> set_type hosting
...> set_content \"Mr.X: 10, Mr.Y: 8\"
...> end
...> "
...> |> Contracts.parse()
{
:ok,
%Archethic.Contracts.Contract{
conditions: %{
inherit: %Archethic.Contracts.ContractConditions{
address: nil,
authorized_keys: nil,
code: nil,
content: {
:==,
[line: 2],
[true, {{:., [line: 2], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.Legacy.Library], [:Library]}, :regex_match?]}, [line: 2], [{:get_in, [line: 2], [{:scope, [line: 2], nil}, ["next", "content"]]}, "^(Mr.X: ){1}([0-9]+), (Mr.Y: ){1}([0-9])+$"]}]
},
origin_family: :biometric,
previous_public_key: nil,
secrets: nil,
timestamp: nil,
token_transfers: nil,
type: nil,
uco_transfers: nil
},
oracle: %Archethic.Contracts.ContractConditions{address: nil, authorized_keys: nil, code: nil, content: nil, origin_family: :all, previous_public_key: nil, secrets: nil, timestamp: nil, token_transfers: nil, type: nil, uco_transfers: nil},
transaction: %Archethic.Contracts.ContractConditions{address: nil, authorized_keys: nil, code: nil, content: nil, origin_family: :all, previous_public_key: nil, secrets: nil, timestamp: nil, token_transfers: nil, type: nil, uco_transfers: nil}
},
constants: %Archethic.Contracts.ContractConstants{contract: nil, transaction: nil},
next_transaction: %Archethic.TransactionChain.Transaction{
address: nil,
cross_validation_stamps: [],
data: %Archethic.TransactionChain.TransactionData{
code: "",
content: "",
ledger: %Archethic.TransactionChain.TransactionData.Ledger{token: %Archethic.TransactionChain.TransactionData.TokenLedger{transfers: []}, uco: %Archethic.TransactionChain.TransactionData.UCOLedger{transfers: []}},
ownerships: [],
recipients: []
},
origin_signature: nil,
previous_public_key: nil,
previous_signature: nil,
type: nil,
validation_stamp: nil,
version: 1
},
triggers: %{
{:datetime, ~U[2020-09-25 13:18:43Z]} => {
:__block__,
[],
[
{
:__block__,
[],
[
{
:=,
[line: 7],
[{:scope, [line: 7], nil}, {:update_in, [line: 7], [{:scope, [line: 7], nil}, ["next_transaction"], {:&, [line: 7], [{{:., [line: 7], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.Legacy.TransactionStatements], [:TransactionStatements]}, :set_type]}, [line: 7], [{:&, [line: 7], [1]}, "hosting"]}]}]}]
},
{{:., [], [{:__aliases__, [alias: false], [:Function]}, :identity]}, [], [{:scope, [], nil}]}
]
},
{
:__block__,
[],
[
{
:=,
[line: 8],
[{:scope, [line: 8], nil}, {:update_in, [line: 8], [{:scope, [line: 8], nil}, ["next_transaction"], {:&, [line: 8], [{{:., [line: 8], [{:__aliases__, [alias: Archethic.Contracts.Interpreter.Legacy.TransactionStatements], [:TransactionStatements]}, :set_content]}, [line: 8], [{:&, [line: 8], [1]}, "Mr.X: 10, Mr.Y: 8"]}]}]}]
},
{{:., [], [{:__aliases__, [alias: false], [:Function]}, :identity]}, [], [{:scope, [], nil}]}
]
}
]
}
},
version: 0
}
}
@doc """
Parse a smart contract code and return a contract struct
"""
@spec parse(binary()) :: {:ok, Contract.t()} | {:error, binary()}
def parse(contract_code) when is_binary(contract_code) do
start = System.monotonic_time()

case Interpreter.parse(contract_code) do
{:ok,
contract = %Contract{
triggers: triggers,
conditions: %{transaction: transaction_conditions, oracle: oracle_conditions}
}} ->
:telemetry.execute([:archethic, :contract, :parsing], %{
duration: System.monotonic_time() - start
})

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

Map.has_key?(triggers, :oracle) and Conditions.empty?(oracle_conditions) ->
{:error, "missing oracle conditions"}

true ->
{:ok, contract}
end

{:error, _} = e ->
e
end
end
defdelegate parse(contract_code),
to: Interpreter

@doc """
Same a `parse/1` but raise if the contract is not valid
Expand Down
Loading

0 comments on commit 8725d25

Please sign in to comment.