diff --git a/config/config.exs b/config/config.exs index 908d339c9f..0bd7d32cb4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -60,11 +60,12 @@ config :archethic, :mut_dir, "data" config :archethic, :marker, "-=%=-=%=-=%=-" # size represents in bytes binary +# 3 MB config :archethic, :transaction_data_content_max_size, 3_145_728 # size represents in bytes binary -# 24KB Max -config :archethic, :transaction_data_code_max_size, 24576 +# 256 KB +config :archethic, :transaction_data_code_max_size, 262_144 config :archethic, Archethic.Crypto, supported_curves: [ diff --git a/config/test.exs b/config/test.exs index 2635946205..246a9b92fa 100755 --- a/config/test.exs +++ b/config/test.exs @@ -224,3 +224,5 @@ config :archethic, Archethic.Contracts.Interpreter.Library.Common.HttpImpl, config :archethic, Archethic.P2P.Message.GetUnspentOutputs, threshold: 1_000 config :archethic, Archethic.P2P.Message.ValidateSmartContractCall, timeout: 50 + +config :archethic, Archethic.Contracts.Wasm.IO, MockWasmIO diff --git a/lib/archethic/contracts.ex b/lib/archethic/contracts.ex index bcf6a91dbc..8a70216af3 100644 --- a/lib/archethic/contracts.ex +++ b/lib/archethic/contracts.ex @@ -4,9 +4,8 @@ defmodule Archethic.Contracts do Each smart contract is register and supervised as long running process to interact with later on. """ - alias __MODULE__.Conditions - alias __MODULE__.Constants - alias __MODULE__.Contract + alias __MODULE__.Interpreter.Conditions, as: ConditionsInterpreter + alias __MODULE__.Interpreter.Constants, as: ConstantsInterpreter alias __MODULE__.Contract.ActionWithoutTransaction alias __MODULE__.Contract.ActionWithTransaction alias __MODULE__.Contract.ConditionRejected @@ -14,7 +13,16 @@ defmodule Archethic.Contracts do alias __MODULE__.Contract.State alias __MODULE__.Interpreter alias __MODULE__.Interpreter.Library + alias __MODULE__.Interpreter.Contract, as: InterpretedContract alias __MODULE__.Loader + + alias __MODULE__.WasmContract + alias __MODULE__.WasmModule + alias __MODULE__.WasmSpec + alias __MODULE__.Wasm.ReadResult + alias __MODULE__.Wasm.UpdateResult + + alias Archethic alias Archethic.Crypto alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.Transaction.ValidationStamp @@ -23,6 +31,7 @@ defmodule Archethic.Contracts do alias Archethic.TransactionChain.TransactionData alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.TransactionData.Ownership alias Archethic.Utils alias Archethic.UTXO @@ -46,25 +55,35 @@ defmodule Archethic.Contracts do @doc """ Parse a smart contract code and return a contract struct """ - @spec parse(binary()) :: {:ok, Contract.t()} | {:error, binary()} - defdelegate parse(contract_code), - to: Interpreter + @spec parse(binary()) :: + {:ok, InterpretedContract.t() | WasmContract.t()} | {:error, String.t()} + def parse(contract_code) do + case Jason.decode(contract_code) do + {:ok, contract_json} -> + WasmContract.parse(contract_json) + + _ -> + Interpreter.parse(contract_code) + end + end @doc """ Same a `parse/1` but raise if the contract is not valid """ - @spec parse!(binary()) :: Contract.t() + @spec parse!(binary()) :: InterpretedContract.t() | WasmContract.t() def parse!(contract_code) when is_binary(contract_code) do - {:ok, contract} = parse(contract_code) - contract + case parse(contract_code) do + {:ok, contract} -> contract + {:error, reason} -> raise reason + end end @doc """ Execute the contract trigger. """ @spec execute_trigger( - trigger :: Contract.trigger_type(), - contract :: Contract.t(), + trigger :: InterpretedContract.trigger_type() | WasmContract.trigger_type(), + contract :: InterpretedContract.t() | WasmContract.t(), maybe_trigger_tx :: nil | Transaction.t(), maybe_recipient :: nil | Recipient.t(), inputs :: list(UnspentOutput.t()), @@ -74,14 +93,82 @@ defmodule Archethic.Contracts do | {:error, Failure.t()} def execute_trigger( trigger_type, - contract = %Contract{ + contract, + maybe_trigger_tx, + maybe_recipient, + inputs, + opts \\ [] + ) + + def execute_trigger( + {:transaction, "upgrade", _}, + %WasmContract{ + transaction: contract_tx, + state: state, + module: %WasmModule{spec: %WasmSpec{upgrade_opts: upgrade_opts}} + }, + trigger_tx, + %Recipient{args: args}, + _inputs, + _opts + ) do + case upgrade_opts do + nil -> + {:error, :upgrade_not_supported} + + %WasmSpec.UpgradeOpts{from: from} -> + {:ok, genesis_address} = + trigger_tx |> Transaction.previous_address() |> Archethic.fetch_genesis_address() + + if genesis_address == from do + with {:ok, %{"bytecode" => new_code, "manifest" => manifest}} <- + WasmSpec.cast_wasm_input(args, %{"bytecode" => "string", "manifest" => "map"}), + {:ok, new_code_bytes} <- Base.decode16(new_code, case: :mixed), + {:ok, new_module} <- + WasmModule.parse(:zlib.unzip(new_code_bytes), WasmSpec.from_manifest(manifest)) do + upgrade_state = + if "onUpgrade" in WasmModule.list_exported_functions_name(new_module) do + case WasmModule.execute(new_module, "onUpgrade", state: state) do + {:ok, %UpdateResult{state: migrated_state}} -> + migrated_state + + _ -> + state + end + else + state + end + + {:ok, + %UpdateResult{ + state: upgrade_state, + transaction: %{ + type: :contract, + data: %{ + contract: %{bytecode: new_code_bytes, manifest: manifest} + } + } + }} + else + _ -> {:error, :invalid_upgrade_params} + end + else + {:error, :upgrade_not_authorized} + end + end + |> cast_trigger_result(state, contract_tx) + end + + def execute_trigger( + trigger_type, + contract = %{ transaction: contract_tx = %Transaction{address: contract_address}, state: state }, maybe_trigger_tx, maybe_recipient, inputs, - opts \\ [] + opts ) do # TODO: trigger_tx & recipient should be transformed into recipient here # TODO: rescue should be done in here as well @@ -106,14 +193,20 @@ defmodule Archethic.Contracts do Keyword.fetch!(opts, :time_now), inputs_digest(inputs)} fn -> - Interpreter.execute_trigger( - trigger_type, - contract, - maybe_trigger_tx, - maybe_recipient, - inputs, - opts - ) + case contract do + %WasmContract{} -> + exec_wasm(contract, trigger_type, maybe_trigger_tx, maybe_recipient, inputs, opts) + + _ -> + Interpreter.execute_trigger( + trigger_type, + contract, + maybe_trigger_tx, + maybe_recipient, + inputs, + opts + ) + end end |> cache_interpreter_execute(key, timeout_err_msg: "Trigger's execution timed-out", @@ -122,6 +215,83 @@ defmodule Archethic.Contracts do |> cast_trigger_result(state, contract_tx) end + defp exec_wasm( + %WasmContract{ + state: state, + module: module = %WasmModule{spec: %WasmSpec{triggers: triggers}}, + transaction: contract_tx + }, + trigger_type, + maybe_trigger_tx, + maybe_recipient, + inputs, + _opts + ) do + trigger = + Enum.find(triggers, fn %WasmSpec.Trigger{type: type, name: fn_name} -> + case trigger_type do + {:transaction, action_name, _} when action_name != nil -> + type == :transaction and fn_name == action_name + + trigger -> + type == trigger + end + end) + + if trigger != nil do + %WasmSpec.Trigger{input: input} = trigger + + with {:ok, args} <- maybe_recipient_arg(maybe_recipient), + {:ok, args} <- WasmSpec.cast_wasm_input(args, input) do + # FIXME: remove the fetch genesis address when it will be integrated in the transaction's structure + + maybe_trigger_tx = + case maybe_trigger_tx do + nil -> + nil + + tx -> + Map.put( + tx, + :genesis, + tx + |> Transaction.previous_address() + |> Archethic.fetch_genesis_address() + |> elem(1) + ) + end + + WasmModule.execute( + module, + trigger.name, + transaction: maybe_trigger_tx, + state: state, + balance: UTXO.get_balance(inputs), + arguments: args, + contract: + Map.put( + contract_tx, + :genesis, + contract_tx + |> Transaction.previous_address() + |> Archethic.fetch_genesis_address() + |> elem(1) + ), + encrypted_seed: get_encrypted_seed(contract_tx) + ) + else + {:error, reason} -> + {:error, reason} + end + else + {:error, :trigger_not_exists} + end + end + + defp maybe_recipient_arg(nil), do: {:ok, nil} + defp maybe_recipient_arg(%Recipient{args: args}) when is_map(args), do: {:ok, args} + defp maybe_recipient_arg(_), do: {:error, "Wasm contract support only arguments as map"} + defp time_now({:transaction, _, _}, %Transaction{ validation_stamp: %ValidationStamp{timestamp: timestamp} }) do @@ -162,6 +332,56 @@ defmodule Archethic.Contracts do end end + defp cast_trigger_result( + {:ok, %UpdateResult{transaction: next_tx, state: next_state}}, + prev_state, + contract_tx = %Transaction{data: %TransactionData{contract: contract}} + ) do + next_tx = + if next_tx != nil do + if next_tx.data.contract == nil do + next_tx + |> put_in([Access.key(:data, %{}), :contract], contract) + |> Transaction.cast() + else + Transaction.cast(next_tx) + end + end + + if State.empty?(next_state) do + cast_valid_trigger_result({:ok, next_tx, next_state, []}, prev_state, contract_tx, nil) + else + encoded_state = State.serialize(next_state) + + if State.valid_size?(encoded_state) do + cast_valid_trigger_result( + {:ok, next_tx, next_state, []}, + prev_state, + contract_tx, + encoded_state + ) + else + {:error, + %Failure{ + logs: [], + error: :state_exceed_threshold, + stacktrace: [], + user_friendly_error: "Execution was successful but the state exceed the threshold" + }} + end + end + end + + defp cast_trigger_result({:ok, nil}, _, _), + do: + {:error, + %Failure{ + logs: [], + error: :invalid_trigger_output, + stacktrace: [], + user_friendly_error: "Trigger must return either a new transaction or a new state" + }} + defp cast_trigger_result(err = {:error, %Failure{}}, _, _), do: err defp cast_trigger_result({:error, :trigger_not_exists}, _, _) do @@ -178,6 +398,26 @@ defmodule Archethic.Contracts do {:error, raise_to_failure(err, stacktrace)} end + defp cast_trigger_result({:error, reason}, _, _) when is_binary(reason) do + {:error, + %Failure{ + error: :execution_raise, + user_friendly_error: reason, + stacktrace: [], + logs: [] + }} + end + + defp cast_trigger_result({:error, %{"message" => message}}, _, _) do + {:error, + %Failure{ + error: :contract_throw, + user_friendly_error: message, + stacktrace: [], + logs: [] + }} + end + # No output transaction, no state update defp cast_valid_trigger_result({:ok, nil, next_state, logs}, previous_state, _, encoded_state) when next_state == previous_state do @@ -202,15 +442,71 @@ defmodule Archethic.Contracts do Execute contract's function """ @spec execute_function( - contract :: Contract.t(), + contract :: InterpretedContract.t() | WasmContract.t(), function_name :: String.t(), - args_values :: list(), + args_values :: list() | map(), inputs :: list(UnspentOutput.t()) ) :: {:ok, value :: any(), logs :: list(String.t())} | {:error, Failure.t()} def execute_function( - contract = %Contract{ + %WasmContract{ + module: module = %WasmModule{spec: spec}, + state: state, + transaction: contract_tx + }, + function_name, + args_values, + inputs + ) + when is_map(args_values) do + case WasmSpec.get_function_spec(spec, function_name) do + {:error, :function_does_not_exist} -> + {:error, + %Failure{ + error: :function_does_not_exist, + user_friendly_error: "#{function_name} is not exposed as public function", + stacktrace: [], + logs: [] + }} + + {:ok, %WasmSpec.Function{input: input}} -> + with {:ok, arg} <- WasmSpec.cast_wasm_input(args_values, input), + {:ok, %ReadResult{value: value}} <- + WasmModule.execute(module, function_name, + state: state, + balance: UTXO.get_balance(inputs), + arguments: arg, + encrypted_seed: get_encrypted_seed(contract_tx), + contract: + Map.put( + contract_tx, + :genesis, + Archethic.fetch_genesis_address(contract_tx.address) |> elem(1) + ) + ) do + {:ok, value, []} + else + {:error, reason} -> + {:error, + %Failure{ + user_friendly_error: "#{inspect(reason)}", + error: :invalid_function_call + }} + end + end + end + + def execute_function(%WasmContract{}, _function_name, _args_values, _input), + do: + {:error, + %Failure{ + user_friendly_error: "Wasm contract support only arguments as map", + error: :invalid_function_call + }} + + def execute_function( + contract = %InterpretedContract{ transaction: contract_tx, version: contract_version, state: state @@ -237,13 +533,13 @@ defmodule Archethic.Contracts do {:ok, function} -> contract_constants = contract_tx - |> Constants.from_transaction(contract_version) - |> Constants.set_balance(inputs) + |> ConstantsInterpreter.from_transaction(contract_version) + |> ConstantsInterpreter.set_balance(inputs) constants = %{ "contract" => contract_constants, :time_now => DateTime.utc_now() |> DateTime.to_unix(), - :encrypted_seed => Contract.get_encrypted_seed(contract), + :encrypted_seed => get_encrypted_seed(contract_tx), :state => state } @@ -348,8 +644,8 @@ defmodule Archethic.Contracts do The transaction and datetime depends on the condition. """ @spec execute_condition( - condition_type :: Contract.condition_type(), - contract :: Contract.t(), + condition_type :: InterpretedContract.condition_type(), + contract :: InterpretedContract.t() | WasmContract.t(), incoming_transaction :: Transaction.t(), maybe_recipient :: nil | Recipient.t(), validation_time :: DateTime.t(), @@ -358,12 +654,81 @@ defmodule Archethic.Contracts do ) :: {:ok, logs :: list(String.t())} | {:error, ConditionRejected.t() | Failure.t()} def execute_condition( condition_key, - contract = %Contract{conditions: conditions}, - transaction = %Transaction{}, + contract, + transaction, maybe_recipient, datetime, inputs, opts \\ [] + ) + + def execute_condition( + :inherit, + %WasmContract{module: module}, + transaction = %Transaction{ + validation_stamp: %ValidationStamp{ + ledger_operations: %LedgerOperations{ + unspent_outputs: next_unspent_outputs + } + } + }, + _maybe_recipient, + _datetime, + inputs, + _opts + ) do + if "onInherit" in WasmModule.list_exported_functions_name(module) do + next_state = + case Enum.find(next_unspent_outputs, &(&1.type == :state)) do + nil -> + %{} + + %UnspentOutput{encoded_payload: encoded_payload} -> + {state, _} = State.deserialize(encoded_payload) + state + end + + case WasmModule.execute(module, "onInherit", + state: next_state, + balance: UTXO.get_balance(inputs), + transaction: transaction + ) do + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, + %Failure{ + error: :invalid_inherit_condition, + user_friendly_error: "#{inspect(reason)}", + logs: [], + stacktrace: [] + }} + end + else + {:ok, []} + end + end + + def execute_condition( + _condition_key, + %WasmContract{}, + _transaction, + _recipient, + _datetime, + _inputs, + _opts + ), + do: {:ok, []} + + def execute_condition( + condition_key, + contract = %InterpretedContract{conditions: conditions}, + transaction = %Transaction{}, + maybe_recipient, + datetime, + inputs, + opts ) do conditions |> Map.get(condition_key) @@ -391,9 +756,9 @@ defmodule Archethic.Contracts do end defp do_execute_condition( - %Conditions{args: args, subjects: subjects}, + %ConditionsInterpreter{args: args, subjects: subjects}, condition_key, - contract = %Contract{ + contract = %InterpretedContract{ version: version, transaction: %Transaction{address: contract_address} }, @@ -438,12 +803,19 @@ defmodule Archethic.Contracts do @doc """ Returns a contract instance from a transaction """ - @spec from_transaction(Transaction.t()) :: {:ok, Contract.t()} | {:error, String.t()} - defdelegate from_transaction(tx), to: Contract, as: :from_transaction + @spec from_transaction(Transaction.t()) :: + {:ok, InterpretedContract.t() | WasmContract.t()} | {:error, String.t()} + def from_transaction(tx = %Transaction{data: %TransactionData{code: code}}) do + if code != "" do + InterpretedContract.from_transaction(tx) + else + WasmContract.from_transaction(tx) + end + end defp get_condition_constants( :inherit, - contract = %Contract{ + %InterpretedContract{ transaction: contract_tx, functions: functions, version: contract_version, @@ -472,27 +844,27 @@ defmodule Archethic.Contracts do next_constants = transaction - |> Constants.from_transaction(contract_version) - |> Constants.set_balance(new_inputs) + |> ConstantsInterpreter.from_transaction(contract_version) + |> ConstantsInterpreter.set_balance(new_inputs) previous_contract_constants = contract_tx - |> Constants.from_transaction(contract_version) - |> Constants.set_balance(inputs) + |> ConstantsInterpreter.from_transaction(contract_version) + |> ConstantsInterpreter.set_balance(inputs) %{ "previous" => previous_contract_constants, "next" => next_constants, :time_now => DateTime.to_unix(datetime), :functions => functions, - :encrypted_seed => Contract.get_encrypted_seed(contract), + :encrypted_seed => get_encrypted_seed(contract_tx), :state => state } end defp get_condition_constants( _, - contract = %Contract{ + %InterpretedContract{ transaction: contract_tx, functions: functions, version: contract_version, @@ -504,27 +876,37 @@ defmodule Archethic.Contracts do ) do contract_constants = contract_tx - |> Constants.from_transaction(contract_version) - |> Constants.set_balance(inputs) + |> ConstantsInterpreter.from_transaction(contract_version) + |> ConstantsInterpreter.set_balance(inputs) %{ - "transaction" => Constants.from_transaction(transaction, contract_version), + "transaction" => ConstantsInterpreter.from_transaction(transaction, contract_version), "contract" => contract_constants, :time_now => DateTime.to_unix(datetime), :functions => functions, - :encrypted_seed => Contract.get_encrypted_seed(contract), + :encrypted_seed => get_encrypted_seed(contract_tx), :state => state } end # create a new transaction with the same code - defp generate_next_tx(%Transaction{data: %TransactionData{code: code}}) do - %Transaction{ - type: :contract, - data: %TransactionData{ - code: code + defp generate_next_tx(%Transaction{data: %TransactionData{code: code, contract: contract}}) do + if code != "" do + %Transaction{ + version: 3, + type: :contract, + data: %TransactionData{ + code: code + } } - } + else + %Transaction{ + type: :contract, + data: %TransactionData{ + contract: contract + } + } + end end defp raise_to_failure( @@ -604,4 +986,132 @@ defmodule Archethic.Contracts do |> :erlang.list_to_binary() |> then(fn binary -> :crypto.hash(:sha256, binary) end) end + + @doc """ + Add seed ownership to transaction (on contract version != 0) + Sign a next transaction in the contract chain + """ + @spec sign_next_transaction( + contract :: InterpretedContract.t() | WasmContract.t(), + next_tx :: Transaction.t(), + index :: non_neg_integer() + ) :: {:ok, Transaction.t()} | {:error, :decryption_failed} + def sign_next_transaction( + %{ + transaction: + prev_tx = %Transaction{previous_public_key: previous_public_key, address: address} + }, + %Transaction{version: version, type: next_type, data: next_data}, + index + ) do + case get_contract_seed(prev_tx) do + {:ok, contract_seed} -> + ownership = create_new_seed_ownership(contract_seed) + next_data = Map.update(next_data, :ownerships, [ownership], &[ownership | &1]) + + signed_tx = + Transaction.new( + next_type, + next_data, + contract_seed, + index, + curve: Crypto.get_public_key_curve(previous_public_key), + origin: Crypto.get_public_key_origin(previous_public_key), + version: version + ) + + {:ok, signed_tx} + + error -> + Logger.debug("Cannot decrypt the transaction seed", contract: Base.encode16(address)) + error + end + end + + defp create_new_seed_ownership(seed) do + storage_nonce_pub_key = Crypto.storage_nonce_public_key() + + aes_key = :crypto.strong_rand_bytes(32) + secret = Crypto.aes_encrypt(seed, aes_key) + encrypted_key = Crypto.ec_encrypt(aes_key, storage_nonce_pub_key) + + %Ownership{secret: secret, authorized_keys: %{storage_nonce_pub_key => encrypted_key}} + end + + @doc """ + Remove the seed ownership of a contract transaction + """ + @spec remove_seed_ownership(tx :: Transaction.t()) :: Transaction.t() + def remove_seed_ownership(tx) do + storage_nonce_public_key = Crypto.storage_nonce_public_key() + + update_in(tx, [Access.key!(:data), Access.key!(:ownerships)], fn ownerships -> + case Enum.find_index( + ownerships, + &Ownership.authorized_public_key?(&1, storage_nonce_public_key) + ) do + nil -> ownerships + index -> List.delete_at(ownerships, index) + end + end) + end + + @doc """ + Same as remove_seed_ownership but raise if no ownership matches contract seed + """ + @spec remove_seed_ownership!(tx :: Transaction.t()) :: Transaction.t() + def remove_seed_ownership!(tx) do + case remove_seed_ownership(tx) do + ^tx -> raise "Contract does not have seed ownership" + tx -> tx + end + end + + @doc """ + Determines if a contract has any triggers + """ + @spec contains_trigger?(InterpretedContract.t() | WasmContract.t()) :: boolean() + def contains_trigger?(contract = %InterpretedContract{}), + do: InterpretedContract.contains_trigger?(contract) + + def contains_trigger?(contract = %WasmContract{}), do: WasmContract.contains_trigger?(contract) + + @doc """ + Return the ownership related to the storage nonce public key + """ + @spec get_seed_ownership(Transaction.t()) :: Ownership.t() | nil + def get_seed_ownership(%Transaction{data: %TransactionData{ownerships: ownerships}}) do + storage_nonce_public_key = Crypto.storage_nonce_public_key() + Enum.find(ownerships, &Ownership.authorized_public_key?(&1, storage_nonce_public_key)) + end + + @doc """ + Return the encrypted seed and encrypted aes key + """ + @spec get_encrypted_seed(Transaction.t()) :: {binary(), binary()} | nil + def get_encrypted_seed(tx = %Transaction{}) do + case get_seed_ownership(tx) do + %Ownership{secret: secret, authorized_keys: authorized_keys} -> + storage_nonce_public_key = Crypto.storage_nonce_public_key() + encrypted_key = Map.get(authorized_keys, storage_nonce_public_key) + + {secret, encrypted_key} + + nil -> + nil + end + end + + @doc """ + Try to find the contract's seed in the transaction's ownerships + """ + @spec get_contract_seed(Transaction.t()) :: {:ok, binary()} | {:error, :decryption_failed} + def get_contract_seed(tx = %Transaction{}) do + {secret, encrypted_key} = get_encrypted_seed(tx) + + case Crypto.ec_decrypt_with_storage_nonce(encrypted_key) do + {:ok, aes_key} -> Crypto.aes_decrypt(secret, aes_key) + {:error, :decryption_failed} -> {:error, :decryption_failed} + end + end end diff --git a/lib/archethic/contracts/contract.ex b/lib/archethic/contracts/contract.ex deleted file mode 100644 index ba6f49f2ac..0000000000 --- a/lib/archethic/contracts/contract.ex +++ /dev/null @@ -1,300 +0,0 @@ -defmodule Archethic.Contracts.Contract do - @moduledoc """ - Represents a smart contract - """ - - alias __MODULE__.State - alias Archethic.Contracts.Conditions - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects - alias Archethic.Contracts.Interpreter - alias Archethic.Crypto - alias Archethic.TransactionChain.Transaction - alias Archethic.TransactionChain.TransactionData - alias Archethic.TransactionChain.TransactionData.Ownership - alias Archethic.TransactionChain.TransactionData.Recipient - alias Archethic.TransactionChain.Transaction.ValidationStamp - alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations - alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput - - require Logger - - defstruct triggers: %{}, - functions: %{}, - version: 0, - conditions: %{}, - state: %{}, - transaction: %Transaction{} - - @type trigger_type() :: - :oracle - | {:transaction, nil, nil} - | {:transaction, String.t(), list(String.t())} - | {:datetime, DateTime.t()} - | {:interval, String.t()} - - @type condition_type() :: - :oracle - | :inherit - | {:transaction, nil, nil} - | {:transaction, String.t(), list(String.t())} - - @type trigger_key() :: - :oracle - | trigger_recipient() - | {:datetime, DateTime.t()} - | {:interval, String.t()} - - @type trigger_recipient :: {:transaction, nil | String.t(), nil | non_neg_integer()} - - @type condition_key() :: - :oracle - | :inherit - | {:transaction, nil, nil} - | {:transaction, String.t(), non_neg_integer()} - - @type t() :: %__MODULE__{ - triggers: %{trigger_key() => %{args: list(binary()), ast: Macro.t()}}, - version: integer(), - conditions: %{condition_key() => Conditions.t()}, - state: State.t(), - transaction: Transaction.t() - } - - @doc """ - Create a contract from a transaction. Same `from_transaction/1` but throws if the contract's code is invalid - """ - @spec from_transaction!(Transaction.t()) :: t() - def from_transaction!(tx) do - {:ok, contract} = from_transaction(tx) - contract - end - - @doc """ - Create a contract from a transaction - """ - @spec from_transaction(Transaction.t()) :: {:ok, t()} | {:error, String.t()} - def from_transaction(tx = %Transaction{data: %TransactionData{code: code}}) do - case Interpreter.parse(code) do - {:ok, contract} -> - state = get_state_from_tx(tx) - contract = contract |> Map.put(:transaction, tx) |> Map.put(:state, state) - {:ok, contract} - - {:error, reason} -> - {:error, reason} - end - end - - defp get_state_from_tx(%Transaction{ - validation_stamp: %ValidationStamp{ - ledger_operations: %LedgerOperations{unspent_outputs: utxos} - } - }) do - case Enum.find(utxos, &(&1.type == :state)) do - %UnspentOutput{encoded_payload: encoded_state} -> - {state, _rest} = State.deserialize(encoded_state) - state - - nil -> - State.empty() - end - end - - @doc """ - Return true if the contract contains at least one trigger - """ - @spec contains_trigger?(contract :: t()) :: boolean() - def contains_trigger?(%__MODULE__{triggers: triggers}) do - non_empty_triggers = - Enum.reject(triggers, fn {_, %{ast: ast}} -> ast == {:__block__, [], []} end) - - length(non_empty_triggers) > 0 - end - - @doc """ - Add a trigger to the contract - """ - @spec add_trigger(t(), trigger_type(), any()) :: t() - def add_trigger(contract, type, actions) do - trigger_key = get_key(type) - actions = get_actions(type, actions) - - Map.update!(contract, :triggers, &Map.put(&1, trigger_key, actions)) - end - - @doc """ - Add a condition to the contract - """ - @spec add_condition(map(), condition_type(), ConditionsSubjects.t()) :: t() - def add_condition(contract, condition_type, conditions) do - condition_key = get_key(condition_type) - conditions = get_conditions(condition_type, conditions) - - Map.update!(contract, :conditions, &Map.put(&1, condition_key, conditions)) - end - - defp get_key({:transaction, action, args}) when is_list(args), - do: {:transaction, action, length(args)} - - defp get_key(key), do: key - - defp get_conditions({:transaction, _action, args}, conditions) when is_list(args), - do: %Conditions{args: args, subjects: conditions} - - defp get_conditions(_, conditions), do: %Conditions{subjects: conditions} - - defp get_actions({:transaction, _action, args}, ast) when is_list(args), - do: %{args: args, ast: ast} - - defp get_actions(_, conditions), do: %{args: [], ast: conditions} - - @doc """ - Add a public or private function to the contract - """ - @spec add_function( - contract :: t(), - function_name :: binary(), - ast :: any(), - args :: list(), - visibility :: atom() - ) :: t() - def add_function( - contract = %__MODULE__{}, - function_name, - ast, - args, - visibility - ) do - Map.update!( - contract, - :functions, - &Map.put(&1, {function_name, length(args)}, %{args: args, ast: ast, visibility: visibility}) - ) - end - - @doc """ - Return the args names for this recipient or nil - """ - @spec get_trigger_for_recipient(Recipient.t()) :: trigger_key() - def get_trigger_for_recipient(%Recipient{action: nil, args: nil}), do: {:transaction, nil, nil} - - def get_trigger_for_recipient(%Recipient{action: action, args: args_values}), - do: {:transaction, action, length(args_values)} - - @doc """ - Add seed ownership to transaction (on contract version != 0) - Sign a next transaction in the contract chain - """ - @spec sign_next_transaction( - contract :: t(), - next_tx :: Transaction.t(), - index :: non_neg_integer() - ) :: {:ok, Transaction.t()} | {:error, :decryption_failed} - def sign_next_transaction( - %__MODULE__{ - transaction: - prev_tx = %Transaction{previous_public_key: previous_public_key, address: address} - }, - %Transaction{type: next_type, data: next_data}, - index - ) do - case get_contract_seed(prev_tx) do - {:ok, contract_seed} -> - ownership = create_new_seed_ownership(contract_seed) - next_data = Map.update(next_data, :ownerships, [ownership], &[ownership | &1]) - - signed_tx = - Transaction.new( - next_type, - next_data, - contract_seed, - index, - Crypto.get_public_key_curve(previous_public_key), - Crypto.get_public_key_origin(previous_public_key) - ) - - {:ok, signed_tx} - - error -> - Logger.debug("Cannot decrypt the transaction seed", contract: Base.encode16(address)) - error - end - end - - defp get_seed_ownership( - %Transaction{data: %TransactionData{ownerships: ownerships}}, - storage_nonce_public_key - ) do - Enum.find(ownerships, &Ownership.authorized_public_key?(&1, storage_nonce_public_key)) - end - - defp get_contract_seed(tx) do - storage_nonce_public_key = Crypto.storage_nonce_public_key() - - ownership = %Ownership{secret: secret} = get_seed_ownership(tx, storage_nonce_public_key) - - encrypted_key = Ownership.get_encrypted_key(ownership, storage_nonce_public_key) - - case Crypto.ec_decrypt_with_storage_nonce(encrypted_key) do - {:ok, aes_key} -> Crypto.aes_decrypt(secret, aes_key) - {:error, :decryption_failed} -> {:error, :decryption_failed} - end - end - - defp create_new_seed_ownership(seed) do - storage_nonce_pub_key = Crypto.storage_nonce_public_key() - - aes_key = :crypto.strong_rand_bytes(32) - secret = Crypto.aes_encrypt(seed, aes_key) - encrypted_key = Crypto.ec_encrypt(aes_key, storage_nonce_pub_key) - - %Ownership{secret: secret, authorized_keys: %{storage_nonce_pub_key => encrypted_key}} - end - - @doc """ - Remove the seed ownership of a contract transaction - """ - @spec remove_seed_ownership(tx :: Transaction.t()) :: Transaction.t() - def remove_seed_ownership(tx) do - storage_nonce_public_key = Crypto.storage_nonce_public_key() - - update_in(tx, [Access.key!(:data), Access.key!(:ownerships)], fn ownerships -> - case Enum.find_index( - ownerships, - &Ownership.authorized_public_key?(&1, storage_nonce_public_key) - ) do - nil -> ownerships - index -> List.delete_at(ownerships, index) - end - end) - end - - @doc """ - Same as remove_seed_ownership but raise if no ownership matches contract seed - """ - @spec remove_seed_ownership!(tx :: Transaction.t()) :: Transaction.t() - def remove_seed_ownership!(tx) do - case remove_seed_ownership(tx) do - ^tx -> raise "Contract does not have seed ownership" - tx -> tx - end - end - - @doc """ - Return the encrypted seed and encrypted aes key - """ - @spec get_encrypted_seed(contract :: t()) :: {binary(), binary()} | nil - def get_encrypted_seed(%__MODULE__{transaction: tx}) do - storage_nonce_public_key = Crypto.storage_nonce_public_key() - - case get_seed_ownership(tx, storage_nonce_public_key) do - %Ownership{secret: secret, authorized_keys: authorized_keys} -> - encrypted_key = Map.get(authorized_keys, storage_nonce_public_key) - - {secret, encrypted_key} - - nil -> - nil - end - end -end diff --git a/lib/archethic/contracts/contract/context.ex b/lib/archethic/contracts/contract/context.ex index 7c71f08108..afd2d894d0 100644 --- a/lib/archethic/contracts/contract/context.ex +++ b/lib/archethic/contracts/contract/context.ex @@ -11,8 +11,7 @@ defmodule Archethic.Contracts.Contract.Context do alias Archethic.Crypto alias Archethic.Utils alias Archethic.Utils.VarInt - alias Archethic.TransactionChain.Transaction - alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.TransactionData.VersionedRecipient alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput @@ -33,7 +32,7 @@ defmodule Archethic.Contracts.Contract.Context do """ @type trigger :: {:oracle, Crypto.prepended_hash()} - | {:transaction, Crypto.prepended_hash(), Recipient.t()} + | {:transaction, Crypto.prepended_hash(), VersionedRecipient.t()} | {:datetime, DateTime.t()} | {:interval, String.t(), DateTime.t()} @@ -52,14 +51,9 @@ defmodule Archethic.Contracts.Contract.Context do inputs: inputs }) do inputs_bin = - inputs - |> Enum.map(&VersionedUnspentOutput.serialize/1) - |> :erlang.list_to_bitstring() + inputs |> Enum.map(&VersionedUnspentOutput.serialize/1) |> :erlang.list_to_bitstring() - inputs_len_bin = - inputs - |> length() - |> VarInt.from_value() + inputs_len_bin = inputs |> length() |> VarInt.from_value() <> @@ -104,8 +98,7 @@ defmodule Archethic.Contracts.Contract.Context do end defp serialize_trigger({:transaction, address, recipient}) do - tx_version = Transaction.version() - recipient_bin = Recipient.serialize(recipient, tx_version) + recipient_bin = VersionedRecipient.serialize(recipient) <<4::8, address::binary, recipient_bin::bitstring>> end @@ -126,10 +119,8 @@ defmodule Archethic.Contracts.Contract.Context do end defp deserialize_trigger(<<4::8, rest::bitstring>>) do - tx_version = Transaction.version() - {tx_address, rest} = Utils.deserialize_address(rest) - {recipient, rest} = Recipient.deserialize(rest, tx_version) + {recipient, rest} = VersionedRecipient.deserialize(rest) {{:transaction, tx_address, recipient}, rest} end diff --git a/lib/archethic/contracts/interpreter.ex b/lib/archethic/contracts/interpreter.ex index 91cf61ba75..8d50a9361b 100644 --- a/lib/archethic/contracts/interpreter.ex +++ b/lib/archethic/contracts/interpreter.ex @@ -10,9 +10,10 @@ defmodule Archethic.Contracts.Interpreter do alias __MODULE__.FunctionKeys alias __MODULE__.Legacy alias __MODULE__.Scope - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects - alias Archethic.Contracts.Constants - alias Archethic.Contracts.Contract + alias Archethic.Contracts + alias Archethic.Contracts.Interpreter.Conditions.Subjects, as: ConditionsSubjects + alias Archethic.Contracts.Interpreter.Constants + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Contract.State alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData.Recipient @@ -137,7 +138,7 @@ defmodule Archethic.Contracts.Interpreter do def execute_trigger( trigger_key, - contract = %Contract{ + %Contract{ transaction: contract_tx, version: version, triggers: triggers, @@ -177,7 +178,7 @@ defmodule Archethic.Contracts.Interpreter do "contract" => contract_constants, :time_now => timestamp_now, :functions => functions, - :encrypted_seed => Contract.get_encrypted_seed(contract), + :encrypted_seed => Contracts.get_encrypted_seed(contract_tx), :state => state, :logs => [] }) diff --git a/lib/archethic/contracts/interpreter/action_interpreter.ex b/lib/archethic/contracts/interpreter/action_interpreter.ex index e4ab45677b..cb9d1ae599 100644 --- a/lib/archethic/contracts/interpreter/action_interpreter.ex +++ b/lib/archethic/contracts/interpreter/action_interpreter.ex @@ -1,8 +1,8 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do @moduledoc false - alias Archethic.Contracts.Contract alias Archethic.Contracts.Contract.State + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Interpreter.ASTHelper, as: AST alias Archethic.Contracts.Interpreter.CommonInterpreter alias Archethic.Contracts.Interpreter.FunctionKeys @@ -49,7 +49,13 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do :ok = Macro.validate(ast) # initiate a transaction that will be used by the "Contract" module - initial_next_tx = %Transaction{type: :contract, data: %TransactionData{code: code}} + # Keep version 3 as it is the last version to support code. + # Upgrade to further version is done throught Contract.set_contract function + initial_next_tx = %Transaction{ + version: 3, + type: :contract, + data: %TransactionData{code: code} + } constants = constants @@ -61,14 +67,9 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreter do state = Scope.read_global([:state]) # return a next transaction only if it has been modified - if Scope.read_global([:next_transaction_changed]) do - { - Scope.read_global([:next_transaction]), - state - } - else - {nil, state} - end + if Scope.read_global([:next_transaction_changed]), + do: {Scope.read_global([:next_transaction]), state}, + else: {nil, state} end # ---------------------------------------------------------------------- diff --git a/lib/archethic/contracts/interpreter/condition_interpreter.ex b/lib/archethic/contracts/interpreter/condition_interpreter.ex index 6de1a05571..4cee1cc456 100644 --- a/lib/archethic/contracts/interpreter/condition_interpreter.ex +++ b/lib/archethic/contracts/interpreter/condition_interpreter.ex @@ -1,8 +1,8 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreter do @moduledoc false - alias Archethic.Contracts.Contract - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects + alias Archethic.Contracts.Interpreter.Contract, as: Contract + alias Archethic.Contracts.Interpreter.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter.ASTHelper, as: AST alias Archethic.Contracts.Interpreter.CommonInterpreter alias Archethic.Contracts.Interpreter.FunctionKeys diff --git a/lib/archethic/contracts/interpreter/condition_validator.ex b/lib/archethic/contracts/interpreter/condition_validator.ex index 74062e2b96..6fc72bf375 100644 --- a/lib/archethic/contracts/interpreter/condition_validator.ex +++ b/lib/archethic/contracts/interpreter/condition_validator.ex @@ -4,8 +4,8 @@ defmodule Archethic.Contracts.Interpreter.ConditionValidator do The difference is where the scope is stored (process dict VS global variable) """ - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter.Scope require Logger diff --git a/lib/archethic/contracts/conditions.ex b/lib/archethic/contracts/interpreter/conditions.ex similarity index 96% rename from lib/archethic/contracts/conditions.ex rename to lib/archethic/contracts/interpreter/conditions.ex index 26d06e751c..fdc28c7ee7 100644 --- a/lib/archethic/contracts/conditions.ex +++ b/lib/archethic/contracts/interpreter/conditions.ex @@ -1,4 +1,4 @@ -defmodule Archethic.Contracts.Conditions do +defmodule Archethic.Contracts.Interpreter.Conditions do @moduledoc """ Represents the smart contract conditions """ diff --git a/lib/archethic/contracts/constants.ex b/lib/archethic/contracts/interpreter/constants.ex similarity index 97% rename from lib/archethic/contracts/constants.ex rename to lib/archethic/contracts/interpreter/constants.ex index ef92a52615..135d13eb1f 100644 --- a/lib/archethic/contracts/constants.ex +++ b/lib/archethic/contracts/interpreter/constants.ex @@ -1,10 +1,9 @@ -defmodule Archethic.Contracts.Constants do +defmodule Archethic.Contracts.Interpreter.Constants do @moduledoc """ Represents the smart contract constants and bindings """ - alias Archethic.Contracts.Contract - + alias Archethic.Contracts alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData alias Archethic.TransactionChain.TransactionData.Ledger @@ -32,7 +31,7 @@ defmodule Archethic.Contracts.Constants do ) :: map() def from_contract_transaction(contract_tx, contract_version \\ 1), - do: contract_tx |> Contract.remove_seed_ownership() |> from_transaction(contract_version) + do: contract_tx |> Contracts.remove_seed_ownership() |> from_transaction(contract_version) @doc """ Extract constants from a transaction into a map diff --git a/lib/archethic/contracts/interpreter/contract.ex b/lib/archethic/contracts/interpreter/contract.ex new file mode 100644 index 0000000000..e965d6aab3 --- /dev/null +++ b/lib/archethic/contracts/interpreter/contract.ex @@ -0,0 +1,165 @@ +defmodule Archethic.Contracts.Interpreter.Contract do + @moduledoc """ + Represents a smart contract + """ + + alias Archethic.Contracts.Contract.State + alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Conditions + alias Archethic.Contracts.Interpreter.Conditions.Subjects, as: ConditionsSubjects + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.Transaction.ValidationStamp + alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations + alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput + + require Logger + + defstruct triggers: %{}, + functions: %{}, + version: 0, + conditions: %{}, + state: %{}, + transaction: %Transaction{} + + @type trigger_type() :: + :oracle + | {:transaction, nil, nil} + | {:transaction, String.t(), list(String.t())} + | {:datetime, DateTime.t()} + | {:interval, String.t()} + + @type condition_type() :: + :oracle + | :inherit + | {:transaction, nil, nil} + | {:transaction, String.t(), list(String.t())} + + @type condition_key() :: + :oracle + | :inherit + | Recipient.trigger_key() + + @type t() :: %__MODULE__{ + triggers: %{Recipient.trigger_key() => %{args: list(binary()), ast: Macro.t()}}, + version: integer(), + conditions: %{condition_key() => Conditions.t()}, + state: State.t(), + transaction: Transaction.t() + } + + @doc """ + Create a contract from a transaction. Same `from_transaction/1` but throws if the contract's code is invalid + """ + @spec from_transaction!(Transaction.t()) :: t() + def from_transaction!(tx) do + {:ok, contract} = from_transaction(tx) + contract + end + + @doc """ + Create a contract from a transaction + """ + @spec from_transaction(Transaction.t()) :: {:ok, t()} | {:error, String.t()} + def from_transaction(tx = %Transaction{data: %TransactionData{code: code}}) do + case Interpreter.parse(code) do + {:ok, contract} -> + state = get_state_from_tx(tx) + contract = contract |> Map.put(:transaction, tx) |> Map.put(:state, state) + {:ok, contract} + + {:error, reason} -> + {:error, reason} + end + end + + defp get_state_from_tx(%Transaction{ + validation_stamp: %ValidationStamp{ + ledger_operations: %LedgerOperations{unspent_outputs: utxos} + } + }) do + case Enum.find(utxos, &(&1.type == :state)) do + %UnspentOutput{encoded_payload: encoded_state} -> + {state, _rest} = State.deserialize(encoded_state) + state + + nil -> + State.empty() + end + end + + defp get_state_from_tx(_), do: State.empty() + + @doc """ + Return true if the contract contains at least one trigger + """ + @spec contains_trigger?(contract :: t()) :: boolean() + def contains_trigger?(%__MODULE__{triggers: triggers}) do + non_empty_triggers = + Enum.reject(triggers, fn {_, %{ast: ast}} -> ast == {:__block__, [], []} end) + + length(non_empty_triggers) > 0 + end + + @doc """ + Add a trigger to the contract + """ + @spec add_trigger(t(), trigger_type(), any()) :: t() + def add_trigger(contract, type, actions) do + trigger_key = get_key(type) + actions = get_actions(type, actions) + + Map.update!(contract, :triggers, &Map.put(&1, trigger_key, actions)) + end + + @doc """ + Add a condition to the contract + """ + @spec add_condition(map(), condition_type(), ConditionsSubjects.t()) :: t() + def add_condition(contract, condition_type, conditions) do + condition_key = get_key(condition_type) + conditions = get_conditions(condition_type, conditions) + + Map.update!(contract, :conditions, &Map.put(&1, condition_key, conditions)) + end + + defp get_key({:transaction, action, args}) when is_list(args), + do: {:transaction, action, length(args)} + + defp get_key(key), do: key + + defp get_conditions({:transaction, _action, args}, conditions) when is_list(args), + do: %Conditions{args: args, subjects: conditions} + + defp get_conditions(_, conditions), do: %Conditions{subjects: conditions} + + defp get_actions({:transaction, _action, args}, ast) when is_list(args), + do: %{args: args, ast: ast} + + defp get_actions(_, conditions), do: %{args: [], ast: conditions} + + @doc """ + Add a public or private function to the contract + """ + @spec add_function( + contract :: t(), + function_name :: binary(), + ast :: any(), + args :: list(), + visibility :: atom() + ) :: t() + def add_function( + contract = %__MODULE__{}, + function_name, + ast, + args, + visibility + ) do + Map.update!( + contract, + :functions, + &Map.put(&1, {function_name, length(args)}, %{args: args, ast: ast, visibility: visibility}) + ) + end +end diff --git a/lib/archethic/contracts/interpreter/legacy.ex b/lib/archethic/contracts/interpreter/legacy.ex index 3d751334e4..9192652fde 100644 --- a/lib/archethic/contracts/interpreter/legacy.ex +++ b/lib/archethic/contracts/interpreter/legacy.ex @@ -6,8 +6,9 @@ defmodule Archethic.Contracts.Interpreter.Legacy do alias __MODULE__.ActionInterpreter alias __MODULE__.ConditionInterpreter - alias Archethic.Contracts.Contract - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects + alias Archethic.Contracts + alias Archethic.Contracts.Interpreter.Contract + alias Archethic.Contracts.Interpreter.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter alias Archethic.TransactionChain.Transaction @@ -17,471 +18,6 @@ defmodule Archethic.Contracts.Interpreter.Legacy do Parse a smart contract code and return the filtered AST representation. The parser uses a whitelist of instructions, the rest will be rejected - - ## Examples - - iex> {:ok, ast} = Interpreter.sanitize_code(" - ...> condition transaction: [ - ...> content: regex_match?(\"^Mr.Y|Mr.X{1}$\"), - ...> origin_family: biometric - ...> ] - ...> - ...> condition inherit: [ - ...> content: regex_match?(\"hello\") - ...> ] - ...> - ...> condition oracle: [ - ...> content: json_path_extract(\"$.uco.eur\") > 1 - ...> ] - ...> - ...> actions triggered_by: datetime, at: 1603270603 do - ...> new_content = \"Sent #{1_040_000_000}\" - ...> set_type transfer - ...> set_content new_content - ...> add_uco_transfer to: \"22368B50D3B2976787CFCC27508A8E8C67483219825F998FC9D6908D54D0FE10\", amount: 1_040_000_000 - ...> end - ...> - ...> actions triggered_by: oracle do - ...> set_content \"uco price changed\" - ...> end - ...> ") - ...> Legacy.parse(ast) - { - :ok, - %Archethic.Contracts.Contract{ - conditions: %{ - {:transaction, nil, nil} => %Archethic.Contracts.Conditions{ - args: [], - subjects: %Archethic.Contracts.Conditions.Subjects{ - address: nil, - authorized_keys: nil, - code: nil, - content: { - :==, - [line: 3], - [ - true, - { - { - :., - [line: 3], - [ - { - :__aliases__, - [alias: Archethic.Contracts.Interpreter.Legacy.Library], - [:Library] - }, - :regex_match? - ] - }, - [line: 3], - [ - { - :get_in, - [line: 3], - [{:scope, [line: 3], nil}, ["transaction", "content"]] - }, - "^Mr.Y|Mr.X{1}$" - ] - } - ] - }, - origin_family: :biometric, - previous_public_key: nil, - secrets: nil, - timestamp: nil, - token_transfers: nil, - type: nil, - uco_transfers: nil - } - }, - inherit: %Archethic.Contracts.Conditions{ - args: [], - subjects: %Archethic.Contracts.Conditions.Subjects{ - address: nil, - authorized_keys: nil, - code: nil, - content: { - :==, - [line: 8], - [ - true, - { - { - :., - [line: 8], - [ - { - :__aliases__, - [alias: Archethic.Contracts.Interpreter.Legacy.Library], - [:Library] - }, - :regex_match? - ] - }, - [line: 8], - [ - { - :get_in, - [line: 8], - [{:scope, [line: 8], nil}, ["next", "content"]] - }, - "hello" - ] - } - ] - }, - origin_family: :all, - previous_public_key: nil, - secrets: nil, - timestamp: nil, - token_transfers: nil, - type: nil, - uco_transfers: nil - } - }, - oracle: %Archethic.Contracts.Conditions{ - args: [], - subjects: %Archethic.Contracts.Conditions.Subjects{ - address: nil, - authorized_keys: nil, - code: nil, - content: { - :>, - [line: 12], - [ - { - { - :., - [line: 12], - [ - { - :__aliases__, - [alias: Archethic.Contracts.Interpreter.Legacy.Library], - [:Library] - }, - :json_path_extract - ] - }, - [line: 12], - [ - { - :get_in, - [line: 12], - [{:scope, [line: 12], nil}, ["transaction", "content"]] - }, - "$.uco.eur" - ] - }, - 1 - ] - }, - origin_family: :all, - previous_public_key: nil, - secrets: nil, - timestamp: nil, - token_transfers: nil, - type: nil, - uco_transfers: nil - } - } - }, - functions: %{}, - triggers: %{ - :oracle => %{ - args: [], - ast: { - :__block__, - [], - [ - { - :=, - [line: 23], - [ - {:scope, [line: 23], nil}, - { - :update_in, - [line: 23], - [ - {:scope, [line: 23], nil}, - ["next_transaction"], - { - :&, - [line: 23], - [ - { - { - :., - [line: 23], - [ - { - :__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.Legacy.TransactionStatements - ], - [:TransactionStatements] - }, - :set_content - ] - }, - [line: 23], - [{:&, [line: 23], [1]}, "uco price changed"] - } - ] - } - ] - } - ] - }, - { - { - :., - [], - [{:__aliases__, [alias: false], [:Function]}, :identity] - }, - [], - [{:scope, [], nil}] - } - ] - } - }, - {:datetime, ~U[2020-10-21 08:56:43Z]} => %{ - args: [], - ast: { - :__block__, - [], - [ - { - :=, - [line: 16], - [ - {:scope, [line: 16], nil}, - { - { - :., - [line: 16], - [{:__aliases__, [line: 16], [:Map]}, :put] - }, - [line: 16], - [ - {:scope, [line: 16], nil}, - "new_content", - "Sent 1040000000" - ] - } - ] - }, - { - :__block__, - [], - [ - { - :=, - [line: 17], - [ - {:scope, [line: 17], nil}, - { - :update_in, - [line: 17], - [ - {:scope, [line: 17], nil}, - ["next_transaction"], - { - :&, - [line: 17], - [ - { - { - :., - [line: 17], - [ - { - :__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.Legacy.TransactionStatements - ], - [:TransactionStatements] - }, - :set_type - ] - }, - [line: 17], - [{:&, [line: 17], [1]}, "transfer"] - } - ] - } - ] - } - ] - }, - { - { - :., - [], - [{:__aliases__, [alias: false], [:Function]}, :identity] - }, - [], - [{:scope, [], nil}] - } - ] - }, - { - :__block__, - [], - [ - { - :=, - [line: 18], - [ - {:scope, [line: 18], nil}, - { - :update_in, - [line: 18], - [ - {:scope, [line: 18], nil}, - ["next_transaction"], - { - :&, - [line: 18], - [ - { - { - :., - [line: 18], - [ - { - :__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.Legacy.TransactionStatements - ], - [:TransactionStatements] - }, - :set_content - ] - }, - [line: 18], - [ - {:&, [line: 18], [1]}, - { - :get_in, - [line: 18], - [ - {:scope, [line: 18], nil}, - ["new_content"] - ] - } - ] - } - ] - } - ] - } - ] - }, - { - { - :., - [], - [{:__aliases__, [alias: false], [:Function]}, :identity] - }, - [], - [{:scope, [], nil}] - } - ] - }, - { - :__block__, - [], - [ - { - :=, - [line: 19], - [ - {:scope, [line: 19], nil}, - { - :update_in, - [line: 19], - [ - {:scope, [line: 19], nil}, - ["next_transaction"], - { - :&, - [line: 19], - [ - { - { - :., - [line: 19], - [ - { - :__aliases__, - [ - alias: - Archethic.Contracts.Interpreter.Legacy.TransactionStatements - ], - [:TransactionStatements] - }, - :add_uco_transfer - ] - }, - [line: 19], - [ - {:&, [line: 19], [1]}, - [ - { - "to", - "22368B50D3B2976787CFCC27508A8E8C67483219825F998FC9D6908D54D0FE10" - }, - {"amount", 1_040_000_000} - ] - ] - } - ] - } - ] - } - ] - }, - { - { - :., - [], - [{:__aliases__, [alias: false], [:Function]}, :identity] - }, - [], - [{:scope, [], nil}] - } - ] - } - ] - } - } - }, - version: 0 - } - } - - Returns an error when there are invalid trigger options - - iex> {:ok, ast} = Interpreter.sanitize_code(" - ...> actions triggered_by: datetime, at: 0000000 do - ...> end - ...> ") - ...> Legacy.parse(ast) - {:error, "invalid datetime's trigger"} - - Returns an error when a invalid term is provided - - iex> {:ok, ast} = Interpreter.sanitize_code(" - ...> actions triggered_by: transaction do - ...> System.user_home - ...> end - ...> ") - ...> Legacy.parse(ast) - {:error, "unexpected term - System - L3"} """ @spec parse(ast :: Macro.t()) :: {:ok, Contract.t()} | {:error, reason :: binary()} def parse(ast) do @@ -607,7 +143,7 @@ defmodule Archethic.Contracts.Interpreter.Legacy do } ) do %Transaction{data: %TransactionData{ownerships: previous_ownerships}} = - Contract.remove_seed_ownership!(prev_tx) + Contracts.remove_seed_ownership!(prev_tx) put_in( acc, diff --git a/lib/archethic/contracts/interpreter/legacy/action_interpreter.ex b/lib/archethic/contracts/interpreter/legacy/action_interpreter.ex index b4747ac3f4..faf70ec2ea 100644 --- a/lib/archethic/contracts/interpreter/legacy/action_interpreter.ex +++ b/lib/archethic/contracts/interpreter/legacy/action_interpreter.ex @@ -1,7 +1,7 @@ defmodule Archethic.Contracts.Interpreter.Legacy.ActionInterpreter do @moduledoc false - alias Archethic.Contracts.Contract alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Interpreter.Legacy.TransactionStatements alias Archethic.Contracts.Interpreter.Legacy.UtilsInterpreter @@ -349,6 +349,7 @@ defmodule Archethic.Contracts.Interpreter.Legacy.ActionInterpreter do Code.eval_quoted(code, scope: Map.put(constants, "next_transaction", %Transaction{ + version: 3, data: %TransactionData{} }) ) diff --git a/lib/archethic/contracts/interpreter/legacy/condition_interpreter.ex b/lib/archethic/contracts/interpreter/legacy/condition_interpreter.ex index a6713349d3..b733ab4ee2 100644 --- a/lib/archethic/contracts/interpreter/legacy/condition_interpreter.ex +++ b/lib/archethic/contracts/interpreter/legacy/condition_interpreter.ex @@ -1,8 +1,8 @@ defmodule Archethic.Contracts.Interpreter.Legacy.ConditionInterpreter do @moduledoc false - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter.Legacy.Library alias Archethic.Contracts.Interpreter.Legacy.UtilsInterpreter diff --git a/lib/archethic/contracts/interpreter/library/common/chain_impl.ex b/lib/archethic/contracts/interpreter/library/common/chain_impl.ex index 2408199dbb..9ac5d49c3e 100644 --- a/lib/archethic/contracts/interpreter/library/common/chain_impl.ex +++ b/lib/archethic/contracts/interpreter/library/common/chain_impl.ex @@ -5,7 +5,7 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.ChainImpl do alias Archethic.Contracts.Interpreter.Library alias Archethic.Contracts.Interpreter.Library.Common.Chain alias Archethic.Contracts.Interpreter.Legacy.UtilsInterpreter - alias Archethic.Contracts.Constants + alias Archethic.Contracts.Interpreter.Constants alias Archethic.Crypto diff --git a/lib/archethic/contracts/interpreter/library/common/contract.ex b/lib/archethic/contracts/interpreter/library/common/contract.ex index 522ff3779f..2df1bd28cf 100644 --- a/lib/archethic/contracts/interpreter/library/common/contract.ex +++ b/lib/archethic/contracts/interpreter/library/common/contract.ex @@ -14,6 +14,7 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.Contract do alias Archethic.Tag alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData.Contract alias Archethic.TransactionChain.TransactionData.Recipient alias Archethic.Utils @@ -131,6 +132,26 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.Contract do TransactionStatements.add_ownerships(next_tx, casted_args) end + @tag [:write_contract] + @spec set_contract(Transaction.t(), map()) :: Transaction.t() + def set_contract(next_tx, %{"bytecode" => bytecode, "manifest" => manifest}) do + bytecode = + case Base.decode16(bytecode, case: :mixed) do + {:ok, bytecode} -> bytecode + _ -> raise Library.Error, message: "Contract bytecode should be hexadecimal" + end + + if not is_map(manifest), + do: raise(Library.Error, message: "Contract manifest should be a map") + + contract = %Contract{bytecode: bytecode, manifest: manifest} + + next_tx + |> put_in([Access.key!(:data), Access.key!(:code)], "") + |> put_in([Access.key!(:data), Access.key!(:contract)], contract) + |> Map.put(:version, Transaction.version()) + end + @tag [:io] @spec call_function(address :: binary(), function :: binary(), args :: list()) :: any() def call_function(address, function, args) do @@ -188,6 +209,10 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.Contract do AST.is_list?(first) || AST.is_variable_or_function_call?(first) end + def check_types(:set_contract, [first]) do + AST.is_map?(first) || AST.is_variable_or_function_call?(first) + end + def check_types(:call_function, [first, second, third]) do (AST.is_binary?(first) || AST.is_variable_or_function_call?(first)) and (AST.is_binary?(second) || AST.is_variable_or_function_call?(second)) and diff --git a/lib/archethic/contracts/interpreter/library/common/contract_impl.ex b/lib/archethic/contracts/interpreter/library/common/contract_impl.ex index a7b28bf279..6af8a2abcd 100644 --- a/lib/archethic/contracts/interpreter/library/common/contract_impl.ex +++ b/lib/archethic/contracts/interpreter/library/common/contract_impl.ex @@ -7,6 +7,9 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.ContractImpl do alias Archethic.Contracts.Contract.Failure alias Archethic.Contracts.Interpreter.Legacy.UtilsInterpreter alias Archethic.Contracts.Interpreter.Library + alias Archethic.Contracts.WasmContract + alias Archethic.Contracts.WasmModule + alias Archethic.Contracts.WasmSpec use Archethic.Tag @@ -31,9 +34,9 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.ContractImpl do {:ok, genesis_address} <- Archethic.fetch_genesis_address(address), {:ok, contract} <- Contracts.from_transaction(tx), unspent_outputs = Archethic.get_unspent_outputs(genesis_address), - {:ok, value, _logs} <- + {:ok, output, _logs} <- Contracts.execute_function(contract, function, args, unspent_outputs) do - value + cast_function_output(function, contract, output) else {:error, reason} -> raise Library.Error, message: error_to_message(reason) end @@ -46,4 +49,11 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.ContractImpl do defp error_to_message(reason) do "Contract.call_function failed with #{inspect(reason)}" end + + defp cast_function_output(function, %WasmContract{module: %WasmModule{spec: spec}}, output) do + {:ok, function_spec} = WasmSpec.get_function_spec(spec, function) + WasmSpec.cast_wasm_output(output, function_spec) + end + + defp cast_function_output(_, _, output), do: output end diff --git a/lib/archethic/contracts/loader.ex b/lib/archethic/contracts/loader.ex index cccae38427..4a3f7d7c44 100644 --- a/lib/archethic/contracts/loader.ex +++ b/lib/archethic/contracts/loader.ex @@ -4,7 +4,7 @@ defmodule Archethic.Contracts.Loader do alias Archethic.ContractRegistry alias Archethic.ContractSupervisor - alias Archethic.Contracts.Contract + alias Archethic.Contracts alias Archethic.Contracts.Worker alias Archethic.Crypto @@ -53,8 +53,9 @@ defmodule Archethic.Contracts.Loader do genesis_addresses |> Stream.map(fn genesis -> {genesis, TransactionChain.get_last_transaction(genesis)} end) |> Stream.reject(fn - {_, {:ok, %Transaction{type: type, data: %TransactionData{code: code}}}} -> - Transaction.network_type?(type) or code == "" + {_, + {:ok, %Transaction{type: type, data: %TransactionData{code: code, contract: contract}}}} -> + Transaction.network_type?(type) or (code == "" and contract == nil) {_, {:error, _}} -> true @@ -235,7 +236,7 @@ defmodule Archethic.Contracts.Loader do tx = %Transaction{ address: address, type: type, - data: %TransactionData{code: code}, + data: %TransactionData{code: code, contract: contract}, validation_stamp: %ValidationStamp{ ledger_operations: %LedgerOperations{consumed_inputs: consumed_inputs} } @@ -245,22 +246,23 @@ defmodule Archethic.Contracts.Loader do authorized_nodes, execute_contract? ) - when code != "" do + when code != "" or contract != nil do remove_invalid_input(genesis_address, consumed_inputs) with true <- Election.chain_storage_node?(genesis_address, node_key, authorized_nodes), - {:ok, contract} <- Contract.from_transaction(tx), - true <- Contract.contains_trigger?(contract) do - if worker_exists?(genesis_address), - do: Worker.set_contract(genesis_address, contract, execute_contract?), - else: new_contract(genesis_address, contract) - - Logger.info("Smart contract loaded", - transaction_address: Base.encode16(address), - transaction_type: type - ) - else - _ -> stop_contract(genesis_address) + {:ok, contract} <- Contracts.from_transaction(tx) do + if Contracts.contains_trigger?(contract) do + if worker_exists?(genesis_address), + do: Worker.set_contract(genesis_address, contract, execute_contract?), + else: new_contract(genesis_address, contract) + + Logger.info("Smart contract loaded", + transaction_address: Base.encode16(address), + transaction_type: type + ) + else + stop_contract(genesis_address) + end end end diff --git a/lib/archethic/contracts/wasm/contract.ex b/lib/archethic/contracts/wasm/contract.ex new file mode 100644 index 0000000000..07212ea41e --- /dev/null +++ b/lib/archethic/contracts/wasm/contract.ex @@ -0,0 +1,109 @@ +defmodule Archethic.Contracts.WasmContract do + @moduledoc """ + Represents a smart contract using WebAssembly + """ + + alias Archethic.Contracts.Contract.State + alias Archethic.Contracts.WasmModule + alias Archethic.Contracts.WasmSpec + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData + alias Archethic.TransactionChain.TransactionData.Contract + alias Archethic.TransactionChain.Transaction.ValidationStamp + alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations + alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput + + require Logger + + defstruct version: 1, + module: nil, + state: %{}, + transaction: %Transaction{} + + @type trigger_type() :: + :oracle + | {:transaction, String.t(), nil} + | {:datetime, DateTime.t()} + | {:interval, String.t()} + + @type trigger_key() :: + :oracle + | trigger_recipient() + | {:datetime, DateTime.t()} + | {:interval, String.t()} + + @type trigger_recipient :: {:transaction, nil | String.t(), nil | non_neg_integer()} + + @type t() :: %__MODULE__{ + version: integer(), + module: nil | WasmModule.t(), + state: State.t(), + transaction: Transaction.t() + } + + @doc """ + Create a contract from a transaction. Same `from_transaction/1` but throws if the contract's code is invalid + """ + @spec from_transaction!(Transaction.t()) :: t() + def from_transaction!(tx = %Transaction{}) do + case from_transaction(tx) do + {:ok, contract} -> contract + {:error, reason} -> raise reason + end + end + + @doc """ + Parse smart contract json block and return a contract struct + """ + @spec parse(Contract.t()) :: {:ok, t()} | {:error, String.t()} + def parse(%Contract{manifest: manifest, bytecode: bytecode}) do + uncompressed_bytes = :zlib.unzip(bytecode) + spec = WasmSpec.from_manifest(manifest) + + case WasmModule.parse(uncompressed_bytes, spec) do + {:ok, module} -> {:ok, %__MODULE__{module: module}} + {:error, reason} -> {:error, "#{inspect(reason)}"} + end + end + + @doc """ + Create a contract from a transaction + """ + @spec from_transaction(Transaction.t()) :: {:ok, t()} | {:error, String.t()} + def from_transaction(%Transaction{data: %TransactionData{contract: nil}}), + do: {:error, "No contract to parse"} + + def from_transaction(tx = %Transaction{data: %TransactionData{contract: contract}}) do + case parse(contract) do + {:ok, wasm_contract} -> + {:ok, %__MODULE__{wasm_contract | state: get_state_from_tx(tx), transaction: tx}} + + {:error, _} = e -> + e + end + end + + defp get_state_from_tx(%Transaction{ + validation_stamp: %ValidationStamp{ + ledger_operations: %LedgerOperations{unspent_outputs: utxos} + } + }) do + case Enum.find(utxos, &(&1.type == :state)) do + %UnspentOutput{encoded_payload: encoded_state} -> + {state, _rest} = State.deserialize(encoded_state) + state + + nil -> + State.empty() + end + end + + defp get_state_from_tx(_), do: State.empty() + + @doc """ + Return true if the contract contains at least one trigger + """ + @spec contains_trigger?(contract :: t()) :: boolean() + def contains_trigger?(%__MODULE__{module: %WasmModule{spec: %WasmSpec{triggers: triggers}}}), + do: length(triggers) > 0 +end diff --git a/lib/archethic/contracts/wasm/imports.ex b/lib/archethic/contracts/wasm/imports.ex new file mode 100644 index 0000000000..e9620738d7 --- /dev/null +++ b/lib/archethic/contracts/wasm/imports.ex @@ -0,0 +1,100 @@ +defmodule Archethic.Contracts.WasmImports do + @moduledoc """ + Handle all the import callback for the WebAssembly smart contract modules + """ + + alias Archethic.Contracts.WasmMemory + alias Archethic.Contracts.Wasm.IO, as: WasmIO + alias Archethic.Utils + + import Bitwise + + @doc """ + Log a message coming from WASM module + """ + @spec log(offset :: pos_integer(), length :: pos_integer(), wasm_memory_pid :: pid()) :: :ok + def log(offset, length, io_mem_pid) do + log_msg = WasmMemory.read(io_mem_pid, offset, length) + IO.puts("WASM log => #{log_msg}") + end + + @doc """ + Store a byte coming from WASM module at the given offset into the WASM's shared memory + """ + @spec store_u8(offset :: pos_integer(), value :: pos_integer(), wasm_memory_pid :: pid()) :: :ok + def store_u8(offset, value, io_mem_pid), do: WasmMemory.store_u8(io_mem_pid, offset, value) + + @doc """ + Return a byte at the given offset from the WASM's shared memory + """ + @spec load_u8(offset :: pos_integer(), wasm_memory_pid :: pid()) :: pos_integer() + def load_u8(offset, io_mem_pid) do + <> = WasmMemory.read(io_mem_pid, offset, 1) + byte + end + + @doc """ + Return the size of the of the initial loaded input registed in the WASM's shared memory + """ + @spec input_size(wasm_memory_pid :: pid()) :: pos_integer() + def input_size(io_mem_pid), do: WasmMemory.input_size(io_mem_pid) + + @doc """ + Extends the WASM's shared memory of the given size and return the offset of allocation + """ + @spec alloc(size :: pos_integer(), wasm_memory_pid :: pid()) :: pos_integer() + def alloc(size, io_mem_pid), do: WasmMemory.alloc(io_mem_pid, size) + + @doc """ + Set in the WASM's shared memory the WASM's output registed at the given memory location + """ + @spec set_output(offset :: pos_integer(), length :: pos_integer(), wasm_memory_pid :: pid()) :: + :ok + def set_output(offset, length, io_mem_pid), + do: WasmMemory.set_output(io_mem_pid, offset, length) + + @doc """ + Set in the WASM's shared memory the WASM's error registed at the given memory location + """ + @spec set_error(offset :: pos_integer(), length :: pos_integer(), wasm_memory_pid :: pid()) :: + :ok + def set_error(offset, length, io_mem_pid), + do: WasmMemory.set_error(io_mem_pid, offset, length) + + @doc """ + Query the node for some I/O function + """ + def jsonrpc(offset, length, io_mem_pid) do + contract_seed = WasmMemory.read_contract_seed(io_mem_pid) + + encoded_response = + WasmMemory.read(io_mem_pid, offset, length) + |> Jason.decode!() + |> WasmIO.request(seed: contract_seed) + |> Utils.bin2hex() + |> Jason.encode!() + + size = byte_size(encoded_response) + offset = WasmMemory.alloc(io_mem_pid, size) + + encoded_response + |> :erlang.binary_to_list() + |> Enum.with_index() + |> Enum.each(fn {byte, i} -> + WasmMemory.store_u8(io_mem_pid, offset + i, byte) + end) + + combine_number(offset, size) + end + + defp combine_number(a, b) do + a <<< 32 ||| b + end + + # defp decombine_number(n) do + # a = n >>> 32 + # u32_mask = 2 ** 32 - 1 + # b = n &&& u32_mask + # {a, b} + # end +end diff --git a/lib/archethic/contracts/wasm/io.ex b/lib/archethic/contracts/wasm/io.ex new file mode 100644 index 0000000000..99ac5de848 --- /dev/null +++ b/lib/archethic/contracts/wasm/io.ex @@ -0,0 +1,19 @@ +defmodule Archethic.Contracts.Wasm.IO do + @moduledoc """ + Query some data of the blockchain from the SC + """ + alias Archethic.Contracts.Wasm.Result + + defmodule Request do + @moduledoc false + + @type t :: %{ + method: String.t(), + params: term() + } + defstruct [:method, :params] + end + + use Knigge, otp_app: :archethic, default: __MODULE__.JSONRPCImpl + @callback request(request :: Request.t(), opts :: Keyword.t()) :: Result.t() +end diff --git a/lib/archethic/contracts/wasm/io/jsonrpc_impl.ex b/lib/archethic/contracts/wasm/io/jsonrpc_impl.ex new file mode 100644 index 0000000000..6202f44b7a --- /dev/null +++ b/lib/archethic/contracts/wasm/io/jsonrpc_impl.ex @@ -0,0 +1,371 @@ +defmodule Archethic.Contracts.Wasm.IO.JSONRPCImpl do + @moduledoc """ + Implementation of IO functions via JSONRPC serialization + """ + alias Archethic.Contracts.Wasm.IO, as: WasmIO + alias Archethic.Contracts.Wasm.Result + alias Archethic.Contracts.Interpreter.Library + alias Archethic.Crypto + alias Archethic.TransactionChain.Transaction + + ######################################################### + # CHAIN + ######################################################### + + @spec request(req :: WasmIO.Request.t(), opts :: Keyword.t()) :: Result.t() + def request(%{"method" => "getBalance", "params" => %{"hex" => address}}, _opts) do + address + |> Base.decode16!(case: :mixed) + |> Archethic.get_balance() + |> transform_balance() + |> Result.wrap_ok() + end + + def request(%{"method" => "getGenesisAddress", "params" => %{"hex" => address}}, _opts) do + address + |> Base.decode16!(case: :mixed) + |> Library.Common.Chain.get_genesis_address() + |> transform_hex() + |> Result.wrap_ok() + rescue + _ -> + Result.wrap_error("network issue") + end + + def request(%{"method" => "getFirstTransactionAddress", "params" => %{"hex" => address}}, _opts) do + case address + |> Base.decode16!(case: :mixed) + |> Library.Common.Chain.get_first_transaction_address() do + nil -> + Result.wrap_error("not found") + + first_address -> + first_address + |> transform_hex() + |> Result.wrap_ok() + end + end + + def request(%{"method" => "getLastAddress", "params" => %{"hex" => address}}, _opts) do + Library.Common.Chain.get_last_address(address) + |> transform_hex() + |> Result.wrap_ok() + rescue + _ -> + Result.wrap_error("not found") + end + + def request( + %{"method" => "getPreviousAddress", "params" => %{"hex" => previous_public_key}}, + _opts + ) do + previous_public_key + |> String.upcase() + |> Library.Common.Chain.get_previous_address() + |> transform_hex() + |> Result.wrap_ok() + rescue + _ -> + Result.wrap_error("invalid previous public key") + end + + def request(%{"method" => "getGenesisPublicKey", "params" => %{"hex" => public_key}}, _opts) do + case public_key + |> Base.decode16!(case: :mixed) + |> Library.Common.Chain.get_genesis_public_key() do + nil -> + Result.wrap_error("not found") + + genesis_public_key -> + genesis_public_key + |> transform_hex() + |> Result.wrap_ok() + end + end + + def request(%{"method" => "getTransaction", "params" => %{"hex" => address}}, _opts) do + case address + |> Base.decode16!(case: :mixed) + |> Archethic.search_transaction() do + {:ok, tx} -> + tx + |> transform_transaction() + |> Result.wrap_ok() + + _ -> + Result.wrap_error("not found") + end + end + + def request(%{"method" => "getLastTransaction", "params" => %{"hex" => address}}, _opts) do + case address + |> Base.decode16!(case: :mixed) + |> Archethic.get_last_transaction() do + {:ok, tx} -> + tx + |> transform_transaction() + |> Result.wrap_ok() + + _ -> + Result.wrap_error("not found") + end + end + + ######################################################### + # CONTRACT + ######################################################### + def request( + %{ + "method" => "callFunction", + "params" => %{ + "address" => %{"hex" => address}, + "functionName" => function_name, + "args" => args + } + }, + _opts + ) do + args = + if args == nil do + [] + else + args + end + + address + |> Base.decode16!(case: :mixed) + |> Library.Common.Contract.call_function(function_name, args) + |> Result.wrap_ok() + rescue + e in Library.Error -> + Result.wrap_error(e.message) + end + + ######################################################### + # CRYPTO + ######################################################### + def request( + %{ + "method" => "hmacWithStorageNonce", + "params" => %{ + "data" => %{"hex" => data}, + "hashFunction" => hash_function + } + }, + opts + ) do + case Keyword.get(opts, :encrypted_contract_seed) do + nil -> + Result.wrap_error("Missing contract seed") + + {encrypted_seed, encrypted_key} -> + with {:ok, aes_key} <- Crypto.ec_decrypt_with_storage_nonce(encrypted_key), + {:ok, seed} <- Crypto.aes_decrypt(encrypted_seed, aes_key) do + key = :crypto.hash(:sha256, Crypto.storage_nonce() <> seed) + + # TODO: KECCAK256 + # TODO: BLAKE2B + case [{0, :sha256}, {1, :sha512}, {2, :sha3_256}, {3, :sha3_512}] + |> Map.new() + |> Map.get(hash_function) do + nil -> + Result.wrap_error("Invalid hash function") + + _ -> + :crypto.mac(:hmac, hash_function, key, data |> Base.decode16!()) + |> transform_hex() + |> Result.wrap_ok() + end + else + _ -> Result.wrap_error("Unable to decrypt seed for hmacWithStorageNonce") + end + end + end + + def request( + %{ + "method" => "signWithRecovery", + "params" => %{"hex" => data} + }, + opts + ) do + case Keyword.get(opts, :encrypted_contract_seed) do + nil -> + Result.wrap_error("Missing contract seed") + + {encrypted_seed, encrypted_key} -> + with {:ok, aes_key} <- Crypto.ec_decrypt_with_storage_nonce(encrypted_key), + {:ok, seed} <- Crypto.aes_decrypt(encrypted_seed, aes_key) do + data = Base.decode16!(data, case: :mixed) + + {_pub, <<_::16, priv::binary>>} = Crypto.derive_keypair(seed, 0, :secp256k1) + + case ExSecp256k1.sign(data, priv) do + {:ok, {r, s, v}} -> + Result.wrap_ok(%{ + "r" => transform_hex(r), + "s" => transform_hex(s), + "v" => v + }) + + {:error, err} -> + err |> inspect() |> Result.wrap_error() + end + else + _ -> Result.wrap_error("Unable to decrypt seed for signWithRecovery") + end + end + end + + def request( + %{ + "method" => "decryptWithStorageNonce", + "params" => %{"hex" => data} + }, + _opts + ) do + data + |> Base.decode16!(case: :mixed) + |> Library.Common.Crypto.decrypt_with_storage_nonce() + |> transform_hex() + |> Result.wrap_ok() + rescue + _ -> Result.wrap_error("decryption failed") + end + + ######################################################### + # HTTP + ######################################################### + def request( + %{ + "method" => "request", + "params" => %{ + "body" => body, + "headers" => headers, + "method" => method, + "uri" => uri + } + }, + _opts + ) do + method = + case method do + 0 -> "GET" + 1 -> "PUT" + 2 -> "POST" + 3 -> "PATCH" + 4 -> "DELETE" + end + + headers = + Enum.reduce(headers, %{}, fn %{"key" => key, "value" => value}, acc -> + Map.put(acc, key, value) + end) + + Library.Common.Http.request(uri, method, headers, body, true) + |> Result.wrap_ok() + rescue + e in Library.Error -> + Result.wrap_error(e.message) + end + + def request( + %{ + "method" => "requestMany", + "params" => reqs + }, + _opts + ) do + reqs = + Enum.map(reqs, fn %{ + "body" => body, + "headers" => headers, + "method" => method, + "uri" => uri + } -> + method = + case method do + 0 -> "GET" + 1 -> "PUT" + 2 -> "POST" + 3 -> "PATCH" + 4 -> "DELETE" + end + + headers = + Enum.reduce(headers, %{}, fn %{"key" => key, "value" => value}, acc -> + Map.put(acc, key, value) + end) + + %{ + "body" => body, + "headers" => headers, + "method" => method, + "url" => uri + } + end) + + Library.Common.Http.request_many(reqs, true) + |> Result.wrap_ok() + rescue + e in Library.Error -> + Result.wrap_error(e.message) + end + + defp transform_balance(%{uco: uco, token: token}) do + %{ + "uco" => uco, + "token" => + Enum.map(token, fn {{address, token_id}, amount} -> + %{ + "tokenAddress" => transform_hex(address), + "tokenId" => token_id, + "amount" => amount + } + end) + } + end + + # todo: lots of fields missing + defp transform_transaction(%Transaction{type: type, data: data}) do + %{ + "type" => type, + "data" => %{ + "content" => data.content, + "code" => data.code, + "ledger" => %{ + "uco" => %{ + "transfers" => + Enum.map(data.ledger.uco.transfers, fn t -> + %{ + "to" => transform_hex(t.to), + "amount" => t.amount + } + end) + }, + "token" => %{ + "transfers" => + Enum.map(data.ledger.token.transfers, fn t -> + %{ + "to" => transform_hex(t.to), + "amount" => t.amount, + "tokenAddress" => transform_hex(t.token_address), + "tokenId" => t.token_id + } + end) + } + } + } + } + end + + # the hex "struct" used by the smart contract language to represent Address, PublicKey etc. + defp transform_hex(bin) do + # because library functions returns hex directly + if String.printable?(bin) && String.match?(bin, ~r/^[[:xdigit:]]+$/) do + %{"hex" => bin} + else + %{"hex" => bin |> Base.encode16()} + end + end +end diff --git a/lib/archethic/contracts/wasm/memory.ex b/lib/archethic/contracts/wasm/memory.ex new file mode 100644 index 0000000000..91acf3b3d7 --- /dev/null +++ b/lib/archethic/contracts/wasm/memory.ex @@ -0,0 +1,169 @@ +defmodule Archethic.Contracts.WasmMemory do + @moduledoc """ + Represents the WASM's shared memory used to perform I/O for the WebAssembly module + """ + + use GenServer + + @vsn 1 + + def start_link(encrypted_contract_seed) do + GenServer.start_link(__MODULE__, [encrypted_contract_seed]) + end + + @doc """ + Set the input in the shared memory to be retrieved later + """ + @spec set_input(GenServer.server(), binary()) :: :ok + def set_input(server, input) do + GenServer.cast(server, {:set_input, input}) + end + + @doc """ + Returns the size of the input stored in the memory state + """ + @spec input_size(GenServer.server()) :: pos_integer() + def input_size(server) do + GenServer.call(server, :input_size) + end + + @doc """ + Extends the shared memory and return the offset of the allocation + """ + @spec alloc(GenServer.server(), size :: pos_integer()) :: offset :: pos_integer() + def alloc(server, size) do + GenServer.call(server, {:alloc, size}) + end + + @doc """ + Store the data in shared memory at the offset's position + """ + @spec store_u8(GenServer.server(), offset :: pos_integer(), data :: pos_integer()) :: :ok + def store_u8(server, offset, data) do + GenServer.cast(server, {:store_u8, offset, data}) + end + + @doc """ + Set the output read from the offset of the shared memory to be used later + """ + @spec set_output(GenServer.server(), offset :: pos_integer(), length :: pos_integer()) :: :ok + def set_output(server, offset, length) do + GenServer.cast(server, {:set_output, offset, length}) + end + + @doc """ + Retrieve the output registed by `set_output/3` + """ + @spec get_output(GenServer.server()) :: binary() | nil + def get_output(server) do + GenServer.call(server, :get_output) + end + + @doc """ + Set the error read from the offset of the shared memory to be used later + """ + @spec set_error(GenServer.server(), offset :: pos_integer(), length :: pos_integer()) :: :ok + def set_error(server, offset, length) do + GenServer.cast(server, {:set_error, offset, length}) + end + + @doc """ + Retrieve the error registed by `set_output/3` + """ + @spec get_error(GenServer.server()) :: binary() | nil + def get_error(server) do + GenServer.call(server, :get_error) + end + + @doc """ + Read the memory at the given offset for the given length + """ + @spec read(GenServer.server(), offset :: pos_integer(), length :: pos_integer()) :: binary() + def read(server, offset, length) do + GenServer.call(server, {:read, offset, length}) + end + + @doc """ + Read the contract seed + """ + @spec read_contract_seed(GenServer.server()) :: binary() + def read_contract_seed(server) do + GenServer.call(server, :read_seed) + end + + def init([encrypted_contract_seed]) do + {:ok, + %{ + encrypted_contract_seed: encrypted_contract_seed, + input: <<>>, + buffer: <<>>, + buffer_offset: 0 + }} + end + + def handle_cast({:set_input, input}, state = %{buffer: buffer, buffer_offset: buffer_offset}) do + input_size = byte_size(input) + extended_output = <> + + {:noreply, + %{state | buffer: extended_output, buffer_offset: buffer_offset + input_size, input: input}} + end + + def handle_cast({:store_u8, 0, data}, state = %{buffer: <<_::8, remaining::binary>>}) do + {:noreply, %{state | buffer: <>}} + end + + def handle_cast( + {:store_u8, offset, data}, + state = %{buffer: buffer} + ) do + offset_size = offset * 8 + <> = buffer + + {:noreply, + %{ + state + | buffer: <> + }} + end + + def handle_cast({:set_output, offset, length}, state = %{buffer: buffer}) do + {:noreply, Map.put(state, :output, :erlang.binary_part(buffer, offset, length))} + end + + def handle_cast({:set_error, offset, length}, state = %{buffer: buffer}) do + err_payload = :erlang.binary_part(buffer, offset, length) + {:noreply, Map.put(state, :error, err_payload)} + end + + def handle_call( + {:alloc, size}, + _from, + state = %{buffer: buffer, buffer_offset: buffer_offset} + ) do + extended_output = <> + + {:reply, buffer_offset, + %{state | buffer: extended_output, buffer_offset: buffer_offset + size}} + end + + def handle_call(:input_size, _from, state = %{input: input}) do + {:reply, byte_size(input), state} + end + + def handle_call(:get_output, _from, state) do + {:reply, Map.get(state, :output), state} + end + + def handle_call(:get_error, _from, state) do + {:reply, Map.get(state, :error), state} + end + + def handle_call({:read, offset, length}, _from, state = %{buffer: buffer}) do + {:reply, :erlang.binary_part(buffer, offset, length), state} + end + + def handle_call(:read_seed, _from, state = %{encrypted_contract_seed: seed}) do + {:reply, seed, state} + end +end diff --git a/lib/archethic/contracts/wasm/module.ex b/lib/archethic/contracts/wasm/module.ex new file mode 100644 index 0000000000..644dcf7d78 --- /dev/null +++ b/lib/archethic/contracts/wasm/module.ex @@ -0,0 +1,303 @@ +defmodule Archethic.Contracts.WasmModule do + @moduledoc false + alias Archethic.Contracts.WasmResult + alias Archethic.Contracts.WasmSpec + alias Archethic.Contracts.Wasm.ReadResult + alias Archethic.Contracts.Wasm.UpdateResult + alias Archethic.Contracts.WasmMemory + alias Archethic.Contracts.WasmImports + + alias Archethic.TransactionChain.Transaction + + @reserved_functions ["onInit", "onUpgrade", "onInherit"] + + defstruct [:module, :store, :spec] + + @type t() :: %__MODULE__{ + module: Wasmex.Module.t(), + store: Wasmex.StoreOrCaller.t(), + spec: WasmSpec.t() | nil + } + + @type execution_opts :: [ + now: DateTime.t(), + state: map(), + transaction: map(), + contract: map(), + balance: %{ + uco: pos_integer(), + tokens: + list(%{ + token_address: String.t(), + token_id: pos_integer(), + amount: pos_integer() + }) + }, + encrypted_seed: {encrypted_seed :: binary(), encrypted_key :: binary()} + ] + + @doc """ + Parse wasm module and perform some checks + """ + @spec parse(bytes :: binary(), spec :: WasmSpec.t()) :: {:ok, t()} | {:error, any()} + def parse(bytes, spec = %WasmSpec{}) when is_binary(bytes) do + {:ok, engine} = + Wasmex.Engine.new(Wasmex.EngineConfig.consume_fuel(%Wasmex.EngineConfig{}, true)) + + {:ok, store} = + Wasmex.Store.new( + %Wasmex.StoreLimits{}, + engine + ) + + # TODO: define a minimum limit + initial_gas_alloc = 100_000_000 + + # Add fuel max limit + Wasmex.StoreOrCaller.set_fuel(store, initial_gas_alloc) + + with {:ok, module} <- Wasmex.Module.compile(store, bytes), + wrap_module = %__MODULE__{module: module, store: store}, + :ok <- check_module_imports(wrap_module), + :ok <- check_module_exports(wrap_module), + :ok <- check_spec_exports(wrap_module, spec) do + {:ok, %{wrap_module | spec: spec}} + end + end + + defp check_module_imports(%__MODULE__{module: module}) do + required_imports = + [ + "archethic/env::alloc", + "archethic/env::input_size", + "archethic/env::load_u8", + "archethic/env::set_error", + "archethic/env::set_output", + "archethic/env::store_u8" + ] + |> MapSet.new() + + allowed_imports = + [ + "archethic/env::log", + "archethic/env::jsonrpc" + ] + |> MapSet.new() + + imported_functions = + module + |> Wasmex.Module.imports() + |> Enum.flat_map(fn {namespace, functions} -> + Enum.map(functions, fn {fun_name, _} -> "#{namespace}::#{fun_name}" end) + end) + |> MapSet.new() + + if MapSet.subset?(required_imports, imported_functions) and + MapSet.subset?(MapSet.difference(imported_functions, required_imports), allowed_imports) do + :ok + else + {:error, "wasm's module imported functions are not the expected ones"} + end + end + + defp check_module_exports(%__MODULE__{module: module}) do + exported_functions = exported_functions(module) + + if Enum.all?(exported_functions, &match?({_, {:fn, [], []}}, &1)) do + :ok + else + {:error, "exported function shouldn't have input/output variables"} + end + end + + defp check_spec_exports(module, spec) do + exported_functions = list_exported_functions_name(module) + spec_functions = WasmSpec.function_names(spec) + + with :ok <- validate_existing_spec_functions(spec_functions, exported_functions) do + validate_exported_functions_in_spec(spec_functions, exported_functions) + end + end + + defp validate_existing_spec_functions(spec_functions, exported_functions) do + case spec_functions + |> MapSet.new() + |> MapSet.difference(MapSet.new(exported_functions)) + |> MapSet.to_list() do + [] -> + :ok + + difference -> + {:error, + "Contract doesn't have functions: #{Enum.join(difference, ",")} as mentioned in the spec"} + end + end + + defp validate_exported_functions_in_spec( + spec_functions, + exported_functions + ) do + case exported_functions + |> MapSet.new() + |> MapSet.difference(MapSet.new(spec_functions)) + |> MapSet.reject(&(&1 in @reserved_functions)) + |> MapSet.to_list() do + [] -> + :ok + + difference -> + {:error, "Spec doesn't reference the functions: #{Enum.join(difference, ",")}"} + end + end + + @spec list_exported_functions_name(t()) :: list(String.t()) + def list_exported_functions_name(%__MODULE__{module: module}) do + module + |> exported_functions() + |> Enum.map(fn {name, _} -> name end) + end + + defp exported_functions(module) do + module + |> Wasmex.Module.exports() + |> Enum.filter(&match?({_, {:fn, _, _}}, &1)) + |> Enum.into(%{}) + end + + @spec execute(module :: t(), functionName :: binary(), opts :: execution_opts()) :: + {:ok, ReadResult.t() | UpdateResult.t()} | {:error, any()} + def execute(%__MODULE__{module: module, store: store}, function_name, opts \\ []) + when is_binary(function_name) do + input = + %{ + state: Keyword.get(opts, :state, %{}), + transaction: opts |> Keyword.get(:transaction) |> cast_transaction(), + arguments: Keyword.get(opts, :arguments), + balance: Keyword.get(opts, :balance, %{uco: 0, tokens: []}), + contract: opts |> Keyword.get(:contract) |> cast_transaction() + } + |> Jason.encode!() + + {:ok, io_mem_pid} = WasmMemory.start_link(Keyword.get(opts, :encrypted_seed)) + WasmMemory.set_input(io_mem_pid, input) + + with {:ok, instance_pid} <- + Wasmex.start_link(%{module: module, store: store, imports: imports(io_mem_pid)}), + {:ok, _} <- Wasmex.call_function(instance_pid, function_name, []) do + output = WasmMemory.get_output(io_mem_pid) + cast_output(output) + else + {:error, _} = e -> + case WasmMemory.get_error(io_mem_pid) do + nil -> + e + + custom_error -> + {:error, Jason.decode!(custom_error)} + end + end + end + + defp imports(io_mem_pid) do + %{ + "archethic/env" => %{ + log: + {:fn, [:i64, :i64], [], + fn _context, offset, length -> WasmImports.log(offset, length, io_mem_pid) end}, + alloc: + {:fn, [:i64], [:i64], fn _context, size -> WasmImports.alloc(size, io_mem_pid) end}, + input_size: {:fn, [], [:i64], fn _context -> WasmImports.input_size(io_mem_pid) end}, + load_u8: + {:fn, [:i64], [:i32], + fn _context, offset -> WasmImports.load_u8(offset, io_mem_pid) end}, + set_output: + {:fn, [:i64, :i64], [], + fn _context, offset, length -> WasmImports.set_output(offset, length, io_mem_pid) end}, + set_error: + {:fn, [:i64, :i64], [], + fn _context, offset, length -> WasmImports.set_error(offset, length, io_mem_pid) end}, + store_u8: + {:fn, [:i64, :i32], [], + fn _context, offset, value -> WasmImports.store_u8(offset, value, io_mem_pid) end}, + jsonrpc: + {:fn, [:i64, :i64], [:i64], + fn _context, offset, length -> WasmImports.jsonrpc(offset, length, io_mem_pid) end} + } + } + end + + defp cast_output(nil), do: {:ok, WasmResult.cast(nil)} + + defp cast_output(output) do + with {:ok, json} <- Jason.decode(output) do + {:ok, WasmResult.cast(json)} + end + end + + defp cast_transaction(nil), do: nil + + defp cast_transaction(tx) do + tx + |> Transaction.to_map() + |> Map.put(:genesis, Map.get(tx, :genesis)) + |> wrap_binary() + end + + defp wrap_binary(%{ + address: address, + type: type, + previous_public_key: previous_public_key, + data: %{ + content: content, + ledger: %{uco: %{transfers: uco_transfers}, token: %{transfers: token_transfers}}, + ownerships: ownerships, + action_recipients: recipients + }, + genesis: genesis + }) do + %{ + address: %{hex: Base.encode16(address)}, + genesis: %{hex: Base.encode16(genesis)}, + type: type, + previous_public_key: %{hex: Base.encode16(previous_public_key)}, + data: %{ + content: content, + # FIXME: find a better way to avoid timeout + code: "", + ledger: %{ + uco: %{ + transfers: + Enum.map(uco_transfers, fn transfer -> + Map.update!(transfer, :to, &%{hex: Base.encode16(&1)}) + end) + }, + token: %{ + transfers: + Enum.map(token_transfers, fn transfer -> + transfer + |> Map.update!(:to, &%{hex: Base.encode16(&1)}) + |> Map.update!(:token_address, &%{hex: Base.encode16(&1)}) + end) + } + }, + ownerships: + Enum.map(ownerships, fn %{secret: secret, authorized_keys: authorized_keys} -> + %{ + secret: %{hex: Base.encode16(secret)}, + authorized_keys: + Enum.map(authorized_keys, fn {public_key, encrypted_key} -> + %{ + public_key: %{hex: Base.encode16(public_key)}, + encrypted_secret_key: %{hex: Base.encode16(encrypted_key)} + } + end) + } + end), + recipients: + Enum.map(recipients, fn recipient -> + Map.update!(recipient, :address, &%{hex: Base.encode16(&1)}) + end) + } + } + end +end diff --git a/lib/archethic/contracts/wasm/result.ex b/lib/archethic/contracts/wasm/result.ex new file mode 100644 index 0000000000..c5ea4f0e31 --- /dev/null +++ b/lib/archethic/contracts/wasm/result.ex @@ -0,0 +1,70 @@ +defmodule Archethic.Contracts.Wasm.Result do + @moduledoc """ + Represents a result which mutate the transaction or state + """ + @type t :: %__MODULE__{ + ok: %{value: term()} | nil, + error: String.t() | nil + } + + @derive Jason.Encoder + defstruct [:ok, :error] + + def wrap_ok(value) do + %__MODULE__{ok: %{value: value}} + end + + def wrap_error(message) do + %__MODULE__{error: message} + end +end + +defmodule Archethic.Contracts.Wasm.UpdateResult do + @moduledoc """ + Represents a result which mutate the transaction or state + """ + @type t :: %__MODULE__{ + state: map(), + transaction: map() + } + defstruct [:state, :transaction] +end + +defmodule Archethic.Contracts.Wasm.ReadResult do + @moduledoc """ + Represents a result which doesn't mutate + """ + @type t :: %__MODULE__{ + value: any() + } + defstruct [:value] +end + +defmodule Archethic.Contracts.WasmResult do + @moduledoc """ + Represents a WebAssembly module return + """ + alias Archethic.Contracts.Wasm.UpdateResult + alias Archethic.Contracts.Wasm.ReadResult + + alias Archethic.Utils + + @doc """ + Cast JSON WebAssembly result in `UpdateResult` or `ReadResult` + """ + @spec cast(map() | nil) :: UpdateResult.t() | ReadResult.t() + def cast(result) when is_map_key(result, "state") or is_map_key(result, "transaction") do + %UpdateResult{ + state: Map.get(result, "state") |> cast_state(), + transaction: result |> Map.get("transaction") |> cast_transaction() + } + end + + def cast(result), do: %ReadResult{value: result} + + defp cast_state(nil), do: %{} + defp cast_state(state), do: state + + defp cast_transaction(nil), do: nil + defp cast_transaction(tx) when is_map(tx), do: Utils.atomize_keys(tx) +end diff --git a/lib/archethic/contracts/wasm/spec.ex b/lib/archethic/contracts/wasm/spec.ex new file mode 100644 index 0000000000..d840ebde18 --- /dev/null +++ b/lib/archethic/contracts/wasm/spec.ex @@ -0,0 +1,249 @@ +defmodule Archethic.Contracts.WasmSpec do + @moduledoc """ + Represents a WASM Smart Contract Specification + """ + + alias __MODULE__.Function + alias __MODULE__.Trigger + alias __MODULE__.UpgradeOpts + alias Archethic.TransactionChain.Transaction + + @type t :: %__MODULE__{ + version: pos_integer(), + triggers: list(Trigger.t()), + public_functions: list(Function.t()), + upgrade_opts: nil | UpgradeOpts.t() + } + defstruct [:version, triggers: [], public_functions: [], upgrade_opts: nil] + + def from_manifest( + manifest = %{ + "abi" => %{ + "functions" => functions + } + } + ) do + version = Map.get(manifest, "version", 1) + upgrade_opts = Map.get(manifest, "upgradeOpts") + + Enum.reduce( + functions, + %__MODULE__{ + version: version, + upgrade_opts: UpgradeOpts.cast(upgrade_opts), + triggers: [], + public_functions: [] + }, + fn + {name, function_abi = %{"type" => "action"}}, acc -> + Map.update!(acc, :triggers, &[Trigger.cast(name, function_abi) | &1]) + + {name, function_abi = %{"type" => "publicFunction"}}, acc -> + Map.update!(acc, :public_functions, &[Function.cast(name, function_abi) | &1]) + end + ) + end + + @doc """ + Retrieve function spec + """ + @spec get_function_spec(spec :: t(), function_name :: String.t()) :: + {:ok, Function.t()} | {:error, :function_does_not_exist} + def get_function_spec(%__MODULE__{public_functions: functions}, function_name) do + case Enum.find(functions, &(Map.get(&1, :name) == function_name)) do + nil -> {:error, :function_does_not_exist} + spec -> {:ok, spec} + end + end + + @doc """ + Return the function exposed in the spec + """ + @spec function_names(t()) :: list(String.t()) + def function_names(%__MODULE__{triggers: triggers, public_functions: public_functions}) do + Enum.map(triggers, & &1.name) ++ Enum.map(public_functions, & &1.name) + end + + @spec cast_wasm_input(any(), any()) :: {:ok, any()} | {:error, :invalid_input_type} + def cast_wasm_input(nil, _), do: {:ok, nil} + + def cast_wasm_input(value, input) + when input in ["Address", "Hex", "PublicKey"] and is_binary(value) do + case Base.decode16(value, case: :mixed) do + {:ok, _} -> {:ok, %{"hex" => value}} + _ -> {:error, :invalid_hex} + end + end + + def cast_wasm_input(value, input) + when input in ["i8"] and is_integer(value) and value > -128 and value < 127, + do: {:ok, value} + + def cast_wasm_input(value, input) + when input in ["u8"] and is_integer(value) and value > 0 and value < 256, + do: {:ok, value} + + def cast_wasm_input(value, input) + when input in ["i16"] and is_integer(value) and value > -32_768 and value < 32_767, + do: value + + def cast_wasm_input(value, input) + when input in ["u16"] and is_integer(value) and value > 0 and value < 65535, + do: {:ok, value} + + def cast_wasm_input(value, input) + when input in ["i32"] and is_integer(value) and value > -2_147_483_648 and + value < 2_147_483_647, + do: value + + def cast_wasm_input(value, input) + when input in ["u32"] and is_integer(value) and value > 0 and value < 4_294_967_295, + do: {:ok, value} + + def cast_wasm_input(value, input) + when input in ["i64"] and is_integer(value) and value > -9_223_372_036_854_775_808 and + value < 9_223_372_036_854_775_807, + do: {:ok, value} + + def cast_wasm_input(value, input) + when input in ["u64"] and is_integer(value) and value > 0 and + value < 18_446_744_073_709_551_615, + do: {:ok, value} + + def cast_wasm_input(value, "string") when is_binary(value), do: {:ok, value} + def cast_wasm_input(value, "map") when is_map(value), do: {:ok, value} + + def cast_wasm_input(value, [input]) when is_list(value) do + %{value: value, error: error} = + Enum.reduce_while(value, %{value: [], error: nil}, fn val, acc -> + case cast_wasm_input(val, input) do + {:ok, value} -> + {:cont, Map.update!(acc, :value, &(&1 ++ [value]))} + + {:error, reason} -> + {:halt, %{acc | error: reason}} + end + end) + + case error do + nil -> {:ok, value} + reason -> {:error, reason} + end + end + + def cast_wasm_input(value, input) when is_map(value) do + %{value: value, error: error} = + Enum.reduce_while(value, %{value: %{}, error: nil}, fn {k, v}, acc -> + case cast_wasm_input(v, Map.get(input, k)) do + {:ok, value} -> + {:cont, put_in(acc, [:value, k], value)} + + {:error, reason} -> + {:halt, %{acc | error: reason}} + end + end) + + case error do + nil -> {:ok, value} + reason -> {:error, reason} + end + end + + def cast_wasm_input(_val, _type) do + {:error, :invalid_input_type} + end + + @spec cast_wasm_output(result_value :: any(), manifest_output_type :: any()) :: any() + def cast_wasm_output(%{"hex" => value}, output) + when output in ["Address", "Hex", "PublicKey"] do + Base.decode16!(value, case: :mixed) + end + + def cast_wasm_output( + %{ + "address" => %{"hex" => address}, + "type" => type, + "data" => %{ + "content" => content, + "ledger" => %{ + "uco" => %{"transfers" => uco_transfers}, + "token" => %{"transfers" => token_transfers} + }, + "recipients" => recipients + # "ownerships" => ownerships + } + }, + "Transaction" + ) do + %{ + address: Base.decode16!(address, case: :mixed), + type: type, + data: %{ + content: content, + ledger: %{ + uco: %{ + transfers: + Enum.map( + uco_transfers, + fn %{"to" => %{"hex" => to}, "amount" => amount} -> + %{to: Base.decode16!(to, case: :mixed), amount: amount} + end + ) + }, + token: %{ + transfers: + Enum.map( + token_transfers, + fn transfer = %{ + "to" => %{"hex" => to}, + "amount" => amount, + "token_address" => %{"hex" => token_address} + } -> + %{ + to: Base.decode16!(to, case: :mixed), + amount: amount, + token_address: Base.decode16!(token_address, case: :mixed), + token_id: Map.get(transfer, "token_id", 0) + } + end + ) + } + }, + recipients: + Enum.map(recipients, fn %{ + "address" => %{"hex" => address}, + "action" => action, + "args" => args + } -> + %{address: Base.decode16!(address, case: :mixed), action: action, args: args} + end) + } + } + |> Transaction.cast() + end + + def cast_wasm_output(map, output) when is_map(map) do + Enum.map(map, fn + {k, _v = %{"hex" => value}} -> + case Map.get(output, k) do + type when type in ["Address", "PublicKey", "Hex"] -> + {k, Base.decode16!(value, case: :mixed)} + + type -> + {k, cast_wasm_output(value, type)} + end + + {k, v} -> + case Map.get(output, k) do + nil -> {k, v} + output -> {k, cast_wasm_output(v, output)} + end + end) + |> Enum.into(%{}) + end + + def cast_wasm_output(list, [output]) when is_list(list), + do: Enum.map(list, &cast_wasm_output(&1, output)) + + def cast_wasm_output(value, _output), do: value +end diff --git a/lib/archethic/contracts/wasm/spec/function.ex b/lib/archethic/contracts/wasm/spec/function.ex new file mode 100644 index 0000000000..d0f979dfed --- /dev/null +++ b/lib/archethic/contracts/wasm/spec/function.ex @@ -0,0 +1,20 @@ +defmodule Archethic.Contracts.WasmSpec.Function do + @moduledoc false + + defstruct [:name, :input, :output] + + @type t() :: %__MODULE__{ + name: String.t(), + input: map(), + output: String.t() | map() + } + + @spec cast(String.t(), map()) :: t() + def cast(name, abi) do + %__MODULE__{ + name: name, + input: Map.get(abi, "input", %{}), + output: Map.get(abi, "output", "") + } + end +end diff --git a/lib/archethic/contracts/wasm/spec/trigger.ex b/lib/archethic/contracts/wasm/spec/trigger.ex new file mode 100644 index 0000000000..e4cbddb55d --- /dev/null +++ b/lib/archethic/contracts/wasm/spec/trigger.ex @@ -0,0 +1,36 @@ +defmodule Archethic.Contracts.WasmSpec.Trigger do + @moduledoc false + + defstruct [:name, :input, :type] + + @type t() :: %__MODULE__{ + name: String.t(), + input: map(), + type: :transaction | :oracle | {:interval, String.t()} | {:datetime, DateTime.t()} + } + + @spec cast(String.t(), map()) :: t() + def cast(name, abi) do + %__MODULE__{ + name: name, + type: get_trigger_type(abi), + input: Map.get(abi, "input", %{}) + } + end + + defp get_trigger_type(abi) do + case Map.get(abi, "triggerType") do + "transaction" -> + :transaction + + "oracle" -> + :oracle + + "interval" -> + {:interval, Map.get(abi, "triggerArgument")} + + "datetime" -> + {:datetime, Map.get(abi, "triggerArgument")} + end + end +end diff --git a/lib/archethic/contracts/wasm/spec/upgrade.ex b/lib/archethic/contracts/wasm/spec/upgrade.ex new file mode 100644 index 0000000000..906fdefece --- /dev/null +++ b/lib/archethic/contracts/wasm/spec/upgrade.ex @@ -0,0 +1,14 @@ +defmodule Archethic.Contracts.WasmSpec.UpgradeOpts do + @moduledoc false + + @type t() :: %__MODULE__{ + from: binary() + } + defstruct [:from] + + def cast(%{"from" => from}) do + %__MODULE__{from: Base.decode16!(from, case: :mixed)} + end + + def cast(nil), do: nil +end diff --git a/lib/archethic/contracts/worker.ex b/lib/archethic/contracts/worker.ex index dab12e3d9a..1c42478f9e 100644 --- a/lib/archethic/contracts/worker.ex +++ b/lib/archethic/contracts/worker.ex @@ -3,10 +3,14 @@ defmodule Archethic.Contracts.Worker do alias Archethic.ContractRegistry alias Archethic.Contracts - alias Archethic.Contracts.Contract + alias Archethic.Contracts.Interpreter.Contract, as: InterpretedContract + alias Archethic.Contracts.WasmSpec + alias Archethic.Contracts.WasmContract + alias Archethic.Contracts.WasmModule alias Archethic.Contracts.Contract.ActionWithoutTransaction alias Archethic.Contracts.Contract.ActionWithTransaction alias Archethic.Contracts.Contract.Failure + alias Archethic.Contracts.Contract.Context alias Archethic.Contracts.Loader alias Archethic.Crypto alias Archethic.Election @@ -15,6 +19,8 @@ defmodule Archethic.Contracts.Worker do alias Archethic.PubSub alias Archethic.TransactionChain alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.TransactionData.VersionedRecipient alias Archethic.TransactionChain.Transaction.ValidationStamp alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.VersionedUnspentOutput @@ -50,7 +56,7 @@ defmodule Archethic.Contracts.Worker do """ @spec set_contract( genesis_address :: Crypto.prepended_hash(), - contract :: Contract.t(), + contract :: InterpretedContract.t() | WasmContract.t(), execute_contract? :: boolean() ) :: :ok def set_contract(genesis_address, contract, execute_contract?) do @@ -82,9 +88,9 @@ defmodule Archethic.Contracts.Worker do :internal, :start_schedulers, _state, - data = %{contract: %Contract{triggers: triggers}} + data = %{contract: contract} ) do - triggers_type = Map.keys(triggers) + triggers_type = get_contract_trigger_types(contract) # stop all existing timers data = cancel_schedulers(data) @@ -148,9 +154,16 @@ defmodule Archethic.Contracts.Worker do :cast, :reparse_contract, _, - data = %{contract: %Contract{transaction: contract_tx}} + data = %{contract: contract = %{transaction: contract_tx}} ) do - {:keep_state, %{data | contract: Contract.from_transaction!(contract_tx)}} + case contract do + # We need to reparse the contract to update the parsed contract in the state (triggers, etc.) + %InterpretedContract{} -> + {:keep_state, %{data | contract: InterpretedContract.from_transaction!(contract_tx)}} + + _ -> + :keep_state_and_data + end end # TRIGGER: DATETIME or INTERVAL @@ -215,7 +228,7 @@ defmodule Archethic.Contracts.Worker do {:EXIT, _pid, _}, _state, data = %{ - contract: %Contract{transaction: %Transaction{address: contract_address}}, + contract: %{transaction: %Transaction{address: contract_address}}, genesis_address: genesis_address } ) do @@ -238,8 +251,17 @@ defmodule Archethic.Contracts.Worker do {:via, Registry, {ContractRegistry, address}} end + defp get_contract_trigger_types(%InterpretedContract{triggers: triggers}), + do: Map.keys(triggers) + + defp get_contract_trigger_types(%WasmContract{ + module: %WasmModule{spec: %WasmSpec{triggers: triggers}} + }) do + Enum.map(triggers, & &1.type) + end + defp get_next_trigger(%{ - contract: %Contract{transaction: %Transaction{address: contract_address}}, + contract: %{transaction: %Transaction{address: contract_address}}, genesis_address: genesis_address, self_triggers: [] }) do @@ -272,7 +294,7 @@ defmodule Archethic.Contracts.Worker do trigger_type = {:interval, interval}, data = %{ genesis_address: genesis_address, - contract: contract = %Contract{triggers: triggers} + contract: contract } ) do unspent_outputs = fetch_unspent_outputs(genesis_address) @@ -283,7 +305,9 @@ defmodule Archethic.Contracts.Worker do {:ok, new_data} _ -> - interval_timer = schedule_trigger({:interval, interval}, Map.keys(triggers)) + interval_timer = + schedule_trigger({:interval, interval}, get_contract_trigger_types(contract)) + new_data = put_in(data, [:timers, trigger_type], interval_timer) {:error, new_data} end @@ -298,20 +322,20 @@ defmodule Archethic.Contracts.Worker do unspent_outputs = fetch_unspent_outputs(genesis_address) with {:ok, oracle_tx} <- TransactionChain.get_transaction(tx_address), - {:ok, _logs} <- - Contracts.execute_condition( - :oracle, + :ok <- + execute_trigger( contract, + :oracle, oracle_tx, nil, trigger_datetime, - VersionedUnspentOutput.unwrap_unspent_outputs(unspent_outputs) - ), - :ok <- - execute_contract(contract, :oracle, oracle_tx, nil, genesis_address, unspent_outputs) do + unspent_outputs, + genesis_address + ) do {:ok, data} else - _ -> {:error, data} + _ -> + {:error, data} end end @@ -323,40 +347,83 @@ defmodule Archethic.Contracts.Worker do }, recipient}, data = %{ genesis_address: genesis_address, - contract: contract = %Contract{transaction: %Transaction{address: contract_address}} + contract: contract = %{transaction: %Transaction{address: contract_address}} } ) do - trigger = Contract.get_trigger_for_recipient(recipient) + trigger = Recipient.get_trigger(recipient) unspent_outputs = fetch_unspent_outputs(genesis_address) - with {:ok, _logs} <- - Contracts.execute_condition( - trigger, - contract, - trigger_tx, - recipient, - timestamp, - VersionedUnspentOutput.unwrap_unspent_outputs(unspent_outputs) - ), - :ok <- - execute_contract( - contract, - trigger, - trigger_tx, - recipient, - genesis_address, - unspent_outputs - ) do - {:ok, Map.put(data, :last_call_processed, from)} - else + case execute_trigger( + contract, + trigger, + trigger_tx, + recipient, + timestamp, + unspent_outputs, + genesis_address + ) do + :ok -> + {:ok, Map.put(data, :last_call_processed, from)} + _ -> Loader.invalidate_call(genesis_address, contract_address, from) {:error, data} end end + defp execute_trigger( + contract = %InterpretedContract{}, + trigger, + trigger_tx, + recipient, + timestamp, + unspent_outputs, + genesis_address + ) do + case Contracts.execute_condition( + trigger, + contract, + trigger_tx, + recipient, + timestamp, + VersionedUnspentOutput.unwrap_unspent_outputs(unspent_outputs) + ) do + {:ok, _logs} -> + execute_contract( + contract, + trigger, + trigger_tx, + recipient, + genesis_address, + unspent_outputs + ) + + {:error, _} = e -> + e + end + end + + defp execute_trigger( + contract = %WasmContract{}, + trigger, + trigger_tx, + recipient, + _timestamp, + unspent_outputs, + genesis_address + ) do + execute_contract( + contract, + trigger, + trigger_tx, + recipient, + genesis_address, + unspent_outputs + ) + end + defp execute_contract( - contract = %Contract{transaction: %Transaction{address: contract_address}}, + contract = %{transaction: %Transaction{address: contract_address}}, trigger, maybe_trigger_tx, maybe_recipient, @@ -375,7 +442,7 @@ defmodule Archethic.Contracts.Worker do VersionedUnspentOutput.unwrap_unspent_outputs(unspent_outputs) ), index = TransactionChain.get_size(contract_address), - {:ok, next_tx} <- Contract.sign_next_transaction(contract, next_tx, index), + {:ok, next_tx} <- Contracts.sign_next_transaction(contract, next_tx, index), contract_context <- get_contract_context(trigger, maybe_trigger_tx, maybe_recipient, unspent_outputs), :ok <- send_transaction(contract_context, next_tx, contract_genesis_address) do @@ -390,14 +457,14 @@ defmodule Archethic.Contracts.Worker do Logger.debug("Contract execution failed: #{inspect(reason)}", meta) :error - _ -> - Logger.debug("Contract execution failed", meta) + {:error, reason} -> + Logger.debug("Contract execution failed: #{inspect(reason)}", meta) :error end end defp get_contract_context(:oracle, %Transaction{address: address}, _, unspent_outputs) do - %Contract.Context{ + %Context{ status: :tx_output, trigger: {:oracle, address}, timestamp: DateTime.utc_now(), @@ -408,7 +475,7 @@ defmodule Archethic.Contracts.Worker do defp get_contract_context({:interval, interval}, _, _, unspent_outputs) do interval_datetime = Utils.get_current_time_for_interval(interval) - %Contract.Context{ + %Context{ status: :tx_output, trigger: {:interval, interval, interval_datetime}, timestamp: DateTime.utc_now(), @@ -417,7 +484,7 @@ defmodule Archethic.Contracts.Worker do end defp get_contract_context(trigger = {:datetime, _}, _, _, unspent_outputs) do - %Contract.Context{ + %Context{ status: :tx_output, trigger: trigger, timestamp: DateTime.utc_now(), @@ -427,14 +494,16 @@ defmodule Archethic.Contracts.Worker do defp get_contract_context( {:transaction, _, _}, - %Transaction{address: address}, + %Transaction{address: address, version: tx_version}, recipient, unspent_outputs ) do + versioned_recipient = VersionedRecipient.wrap_recipient(recipient, tx_version) + # In a next issue, we'll have different status such as :no_output and :failure - %Contract.Context{ + %Context{ status: :tx_output, - trigger: {:transaction, address, recipient}, + trigger: {:transaction, address, versioned_recipient}, timestamp: DateTime.utc_now(), inputs: unspent_outputs } @@ -542,6 +611,6 @@ defmodule Archethic.Contracts.Worker do address |> TransactionChain.fetch_unspent_outputs(nodes) |> Enum.to_list() - |> Contract.Context.filter_inputs() + |> Context.filter_inputs() end end diff --git a/lib/archethic/crypto.ex b/lib/archethic/crypto.ex index 828eb4eaab..4c058bfea0 100755 --- a/lib/archethic/crypto.ex +++ b/lib/archethic/crypto.ex @@ -395,8 +395,10 @@ defmodule Archethic.Crypto do defp do_generate_deterministic_keypair(:bls, origin, seed) do private_key = :crypto.hash(:sha512, seed) + {:ok, public_key} = BlsEx.get_public_key(private_key) + keypair = { - BlsEx.get_public_key(private_key), + public_key, private_key } @@ -442,7 +444,7 @@ defmodule Archethic.Crypto do end defp do_sign(:ed25519, data, key), do: Ed25519.sign(key, data) - defp do_sign(:bls, data, key), do: BlsEx.sign(key, data) + defp do_sign(:bls, data, key), do: BlsEx.sign!(key, data) defp do_sign(curve, data, key), do: ECDSA.sign(curve, key, data) @doc """ @@ -576,7 +578,7 @@ defmodule Archethic.Crypto do end defp do_verify?(:ed25519, key, data, sig), do: Ed25519.verify?(key, data, sig) - defp do_verify?(:bls, key, data, sig), do: BlsEx.verify_signature(key, data, sig) + defp do_verify?(:bls, key, data, sig), do: BlsEx.verify_signature?(key, data, sig) defp do_verify?(curve, key, data, sig), do: ECDSA.verify?(curve, key, data, sig) @doc """ @@ -1436,7 +1438,7 @@ defmodule Archethic.Crypto do """ @spec aggregate_signatures(signatures :: list(binary()), public_keys :: list(key())) :: binary() def aggregate_signatures(signatures, public_keys) do - BlsEx.aggregate_signatures( + BlsEx.aggregate_signatures!( signatures, Enum.map(public_keys, fn <<_::8, _::8, public_key::binary>> -> public_key end) ) @@ -1449,7 +1451,7 @@ defmodule Archethic.Crypto do def aggregate_mining_public_keys(public_keys) do public_keys |> Enum.map(fn <<_::8, _::8, public_key::binary>> -> public_key end) - |> BlsEx.aggregate_public_keys() + |> BlsEx.aggregate_public_keys!() |> ID.prepend_key(:bls) end end diff --git a/lib/archethic/db/embedded_impl/encoding.ex b/lib/archethic/db/embedded_impl/encoding.ex index 4cde7efddd..c6d7ea98f7 100644 --- a/lib/archethic/db/embedded_impl/encoding.ex +++ b/lib/archethic/db/embedded_impl/encoding.ex @@ -3,8 +3,10 @@ defmodule Archethic.DB.EmbeddedImpl.Encoding do Handle the encoding and decoding of the transaction and its fields """ + alias Archethic.Utils.TypedEncoding alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData + alias Archethic.TransactionChain.TransactionData.Contract alias Archethic.TransactionChain.TransactionData.Recipient alias Archethic.TransactionChain.TransactionData.Ledger alias Archethic.TransactionChain.TransactionData.Ownership @@ -34,6 +36,7 @@ defmodule Archethic.DB.EmbeddedImpl.Encoding do data: %TransactionData{ content: content, code: code, + contract: contract, ownerships: ownerships, ledger: %Ledger{uco: uco_ledger, token: token_ledger}, recipients: recipients @@ -111,12 +114,23 @@ defmodule Archethic.DB.EmbeddedImpl.Encoding do |> length() |> VarInt.from_value() + contract_binary = + case contract do + nil -> + <<>> + + %Contract{bytecode: bytecode, manifest: manifest} -> + < TypedEncoding.serialize(:compact) |> :zlib.zip()::binary>> + end + encoding = [ {"address", address}, {"type", <>}, {"data.content", content}, {"data.code", TransactionData.compress_code(code)}, + {"data.contract", contract_binary}, {"data.ledger.uco", UCOLedger.serialize(uco_ledger, tx_version)}, {"data.ledger.token", TokenLedger.serialize(token_ledger, tx_version)}, {"data.ownerships", <>}, @@ -166,6 +180,28 @@ defmodule Archethic.DB.EmbeddedImpl.Encoding do put_in(acc, [Access.key(:data, %{}), :code], code) end + def decode(_version, "data.contract", <<>>, acc), + do: put_in(acc, [Access.key(:data, %{}), :contract], nil) + + def decode( + _version, + "data.contract", + <>, + acc + ) do + manifest = + contract_manifest_compressed + |> :zlib.unzip() + |> TypedEncoding.deserialize(:compact) + |> elem(0) + + put_in(acc, [Access.key(:data, %{}), :contract], %Contract{ + bytecode: contract_bytecode, + manifest: manifest + }) + end + def decode(tx_version, "data.ownerships", <>, acc) do {nb, rest} = VarInt.get_value(rest) ownerships = deserialize_ownerships(rest, nb, [], tx_version) diff --git a/lib/archethic/election.ex b/lib/archethic/election.ex index a752d368f9..60d616c9b2 100755 --- a/lib/archethic/election.ex +++ b/lib/archethic/election.ex @@ -68,6 +68,7 @@ defmodule Archethic.Election do ## Examples iex> %Transaction{ + ...> version: 3, ...> address: ...> <<0, 120, 195, 32, 77, 84, 215, 196, 116, 215, 56, 141, 40, 54, 226, 48, 66, 254, ...> 119, 11, 73, 77, 243, 125, 62, 94, 133, 67, 9, 253, 45, 134, 89>>, diff --git a/lib/archethic/mining/pending_transaction_validation.ex b/lib/archethic/mining/pending_transaction_validation.ex index 20ae219215..6ac6ef14a7 100644 --- a/lib/archethic/mining/pending_transaction_validation.ex +++ b/lib/archethic/mining/pending_transaction_validation.ex @@ -2,7 +2,6 @@ defmodule Archethic.Mining.PendingTransactionValidation do @moduledoc false alias Archethic.Contracts - alias Archethic.Contracts.Contract alias Archethic.Crypto @@ -29,6 +28,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do alias Archethic.TransactionChain alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData + alias Archethic.TransactionChain.TransactionData.Contract alias Archethic.TransactionChain.TransactionData.Ledger alias Archethic.TransactionChain.TransactionData.Recipient alias Archethic.TransactionChain.TransactionData.Ownership @@ -69,6 +69,19 @@ defmodule Archethic.Mining.PendingTransactionValidation do @tx_max_size Application.compile_env!(:archethic, :transaction_data_content_max_size) + @prod? Mix.env() == :prod + + @doc """ + Ensure transaction version is allowed + Used to differentiate mainnet / testnet network + """ + @spec(validate_transaction_version(transaction :: Transaction.t()) :: :ok, {:error, String.t()}) + def validate_transaction_version(%Transaction{version: version}) do + if @prod? and System.get_env("ARCHETHIC_NETWORK_TYPE") != "testnet" and version >= 4, + do: {:error, "Transaction V4 are not yet supported on mainnet"}, + else: :ok + end + @doc """ Ensure transaction size does not exceed the limit size """ @@ -129,41 +142,61 @@ defmodule Archethic.Mining.PendingTransactionValidation do Ensure contract is valid (size, code, ownerships) """ @spec validate_contract(transaction :: Transaction.t()) :: :ok | {:error, String.t()} - def validate_contract(%Transaction{data: %TransactionData{code: ""}}), do: :ok + def validate_contract(%Transaction{data: %TransactionData{code: "", contract: nil}}), do: :ok + + def validate_contract(%Transaction{version: version, data: %TransactionData{code: code}}) + when code != "" and version >= 4, + do: {:error, "Invalid transaction, from v4 code is deprecated"} def validate_contract(%Transaction{ - data: %TransactionData{code: code, ownerships: ownerships} - }) do - with :ok <- validate_code_size(code), - {:ok, contract} <- parse_contract(code) do + version: version, + data: %TransactionData{contract: %Contract{}} + }) + when version <= 3, + do: {:error, "Invalid transaction, before v3 contract is not allowed"} + + def validate_contract( + tx = %Transaction{ + data: %TransactionData{code: code, contract: contract, ownerships: ownerships} + } + ) do + with :ok <- validate_code_size(code, contract), + {:ok, contract} <- parse_contract(tx) do validate_contract_ownership(contract, ownerships) end end - defp validate_code_size(code) do - if TransactionData.code_size_valid?(code), + defp validate_code_size(code, _contract) when code != "" do + if TransactionData.code_size_valid?(code, false), + do: :ok, + else: {:error, "Invalid transaction, code exceed max size"} + end + + defp validate_code_size(_code, %Contract{bytecode: bytecode}) do + if TransactionData.code_size_valid?(bytecode), do: :ok, else: {:error, "Invalid transaction, code exceed max size"} end - defp parse_contract(code) do - case Contracts.parse(code) do + defp parse_contract(tx) do + case Contracts.from_transaction(tx) do {:ok, contract} -> {:ok, contract} {:error, reason} -> {:error, "Smart contract invalid #{inspect(reason)}"} end end defp validate_contract_ownership(contract, ownerships) do - with true <- Contract.contains_trigger?(contract), - false <- - Enum.any?( - ownerships, - &Ownership.authorized_public_key?(&1, Crypto.storage_nonce_public_key()) - ) do - {:error, "Requires storage nonce public key as authorized public keys"} - else - _ -> :ok - end + if Contracts.contains_trigger?(contract), + do: ensure_ownership_in_contract(ownerships), + else: :ok + end + + defp ensure_ownership_in_contract(ownerships) do + storage_nonce = Crypto.storage_nonce_public_key() + + if Enum.any?(ownerships, &Ownership.authorized_public_key?(&1, storage_nonce)), + do: :ok, + else: {:error, "Requires storage nonce public key as authorized public keys"} end @doc """ @@ -670,8 +703,12 @@ defmodule Archethic.Mining.PendingTransactionValidation do end end - def validate_type_rules(%Transaction{type: :contract, data: %TransactionData{code: ""}}, _), - do: {:error, "Invalid contract type transaction - code is empty"} + def validate_type_rules( + %Transaction{type: :contract, data: %TransactionData{code: code, contract: contract}}, + _ + ) + when code == "" and contract == nil, + do: {:error, "Invalid contract type transaction - contract's code is empty"} def validate_type_rules( %Transaction{type: :data, data: %TransactionData{content: "", ownerships: []}}, diff --git a/lib/archethic/mining/proof_of_work.ex b/lib/archethic/mining/proof_of_work.ex index a3d39e7ec7..e300c2e5d3 100644 --- a/lib/archethic/mining/proof_of_work.ex +++ b/lib/archethic/mining/proof_of_work.ex @@ -10,9 +10,8 @@ defmodule Archethic.Mining.ProofOfWork do """ alias Archethic.Contracts - alias Archethic.Contracts.Contract - alias Archethic.Contracts.Conditions - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects + alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Contract, as: InterpretedContract alias Archethic.Crypto @@ -118,12 +117,18 @@ defmodule Archethic.Mining.ProofOfWork do when code != "" do case Contracts.parse(code) do {:ok, - %Contract{ - conditions: %{inherit: %Conditions{subjects: %ConditionsSubjects{origin_family: family}}} + %InterpretedContract{ + conditions: %{ + inherit: %Interpreter.Conditions{ + subjects: %Interpreter.Conditions.Subjects{origin_family: family} + } + } }} when family != :all -> SharedSecrets.list_origin_public_keys(family) + # TODO: support WasmContract inherit conditions + _ -> do_list_origin_public_keys_candidates(tx) end diff --git a/lib/archethic/mining/smart_contract_validation.ex b/lib/archethic/mining/smart_contract_validation.ex index d8ef792119..0c52ef4cba 100644 --- a/lib/archethic/mining/smart_contract_validation.ex +++ b/lib/archethic/mining/smart_contract_validation.ex @@ -5,7 +5,12 @@ defmodule Archethic.Mining.SmartContractValidation do alias Archethic.Contracts alias Archethic.Contracts.Contract.State - alias Archethic.Contracts.Contract + alias Archethic.Contracts.Contract.Context + alias Archethic.Contracts.Interpreter.Contract, as: InterpretedContract + alias Archethic.Contracts.WasmContract + alias Archethic.Contracts.WasmModule + alias Archethic.Contracts.Wasm.ReadResult + alias Archethic.Contracts.Contract.Failure alias Archethic.Contracts.Contract.ActionWithTransaction alias Archethic.Crypto @@ -24,6 +29,7 @@ defmodule Archethic.Mining.SmartContractValidation do alias Archethic.TransactionChain.TransactionData alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.TransactionData.VersionedRecipient alias Crontab.CronExpression.Parser, as: CronParser alias Crontab.DateChecker, as: CronDateChecker @@ -166,14 +172,14 @@ defmodule Archethic.Mining.SmartContractValidation do It also return the result because it's need to extract the state """ @spec validate_contract_execution( - contract_context :: Contract.Context.t(), + contract_context :: Context.t(), prev_tx :: Transaction.t(), genesis_address :: Crypto.prepended_hash(), next_tx :: Transaction.t(), chain_unspent_outputs :: list(VersionedUnspentOutput.t()) ) :: {:ok, State.encoded() | nil} | {:error, Error.t()} def validate_contract_execution( - contract_context = %Contract.Context{ + contract_context = %Context{ status: status, trigger: trigger, timestamp: timestamp @@ -201,13 +207,83 @@ defmodule Archethic.Mining.SmartContractValidation do _chain_unspent_outputs ) when code != "" do - # only contract without triggers (with only conditions) are allowed to NOT have a Contract.Context - if prev_tx |> Contract.from_transaction!() |> Contract.contains_trigger?(), + # only contract without triggers (with only conditions) are allowed to NOT have a context + + {:ok, contract} = InterpretedContract.from_transaction(prev_tx) + + if InterpretedContract.contains_trigger?(contract), do: {:error, Error.new(:invalid_contract_execution, "Contract has not been triggered")}, else: {:ok, nil} end - def validate_contract_execution(_, _, _, _, _), do: {:ok, nil} + def validate_contract_execution( + _contract_context = nil, + prev_tx = %Transaction{data: %TransactionData{contract: contract}}, + _genesis_address, + _next_tx = %Transaction{}, + _chain_unspent_outputs + ) + when contract != nil do + # only contract without triggers are allowed to NOT have a Context + + contract = WasmContract.from_transaction!(prev_tx) + + if WasmContract.contains_trigger?(contract) do + {:error, Error.new(:invalid_contract_execution, "Contract has not been triggered")} + else + {:ok, nil} + end + end + + def validate_contract_execution( + _contract_context, + nil, + _genesis_address, + next_tx = %Transaction{data: %TransactionData{contract: contract}}, + _chain_unspent_outputs + ) + when contract != nil do + case WasmContract.from_transaction(next_tx) do + {:ok, %WasmContract{module: module}} -> + if "onInit" in WasmModule.list_exported_functions_name(module) do + wasm_init_state(module, next_tx) + else + {:ok, nil} + end + + _ -> + {:ok, nil} + end + end + + def validate_contract_execution( + _contract_context, + _prev_tx, + _genesis_address, + _next_tx, + _chain_unspent_outputs + ), + do: {:ok, nil} + + defp wasm_init_state(module = %WasmModule{}, next_tx = %Transaction{}) do + # FIXME when genesis will be integrated in transaction's structure + next_tx = Map.put(next_tx, :genesis, Transaction.previous_address(next_tx)) + + case WasmModule.execute(module, "onInit", transaction: next_tx) do + {:ok, %ReadResult{value: state}} when is_map(state) -> + if State.empty?(state) do + {:ok, nil} + else + {:ok, State.serialize(state)} + end + + {:ok, %ReadResult{}} -> + {:ok, nil} + + {:error, e} -> + {:error, Error.new(:invalid_contract_execution, "onInit call failed #{inspect(e)}")} + end + end @doc """ Validate contract inherit constraint @@ -218,14 +294,16 @@ defmodule Archethic.Mining.SmartContractValidation do contract_inputs :: list(VersionedUnspentOutput.t()) ) :: :ok | {:error, Error.t()} def validate_inherit_condition( - prev_tx = %Transaction{data: %TransactionData{code: code}}, + prev_tx = %Transaction{data: %TransactionData{code: code, contract: contract}}, next_tx = %Transaction{validation_stamp: %ValidationStamp{timestamp: validation_time}}, contract_inputs ) - when code != "" do + when code != "" or contract != nil do + {:ok, contract} = Contracts.from_transaction(prev_tx) + case Contracts.execute_condition( :inherit, - Contract.from_transaction!(prev_tx), + contract, next_tx, nil, validation_time, @@ -281,14 +359,16 @@ defmodule Archethic.Mining.SmartContractValidation do # TODO: instead of address we could have a transaction_summary with proof of validation/replication # TODO: to avoid downloading the tx - defp validate_trigger({:transaction, address, recipient}, _, contract_genesis_address, inputs) do + defp validate_trigger( + {:transaction, address, versioned_recipient}, + _, + contract_genesis_address, + inputs + ) do storage_nodes = Election.storage_nodes(address, P2P.authorized_and_available_nodes()) + recipient = VersionedRecipient.unwrap_recipient(versioned_recipient) - with true <- - Enum.any?( - inputs, - &(&1.type == :call and &1.from == address) - ), + with true <- Enum.any?(inputs, &(&1.type == :call and &1.from == address)), {:ok, tx} <- TransactionChain.fetch_transaction(address, storage_nodes, acceptance_resolver: :accept_transaction @@ -333,16 +413,16 @@ defmodule Archethic.Mining.SmartContractValidation do end end - defp trigger_to_recipient({:transaction, _, recipient}), do: recipient - defp trigger_to_recipient(_), do: nil + defp trigger_to_recipient({:transaction, _, versioned_recipient}), + do: VersionedRecipient.unwrap_recipient(versioned_recipient) + defp trigger_to_recipient(_), do: nil defp trigger_to_trigger_type({:oracle, _}), do: :oracle defp trigger_to_trigger_type({:datetime, datetime}), do: {:datetime, datetime} defp trigger_to_trigger_type({:interval, cron, _datetime}), do: {:interval, cron} - defp trigger_to_trigger_type({:transaction, _, recipient = %Recipient{}}) do - Contract.get_trigger_for_recipient(recipient) - end + defp trigger_to_trigger_type({:transaction, _, versioned_recipient = %VersionedRecipient{}}), + do: VersionedRecipient.unwrap_recipient(versioned_recipient) |> Recipient.get_trigger() # In the case of a trigger interval, # because of the delay between execution and validation, @@ -358,7 +438,7 @@ defmodule Archethic.Mining.SmartContractValidation do end defp parse_contract(prev_tx) do - case Contract.from_transaction(prev_tx) do + case Contracts.from_transaction(prev_tx) do {:ok, contract} -> {:ok, contract} @@ -374,7 +454,7 @@ defmodule Archethic.Mining.SmartContractValidation do end defp execute_trigger( - %Contract.Context{trigger: trigger, inputs: contract_inputs}, + %Context{trigger: trigger, inputs: contract_inputs}, contract, maybe_trigger_tx ) do @@ -405,7 +485,7 @@ defmodule Archethic.Mining.SmartContractValidation do _status = :tx_output ) do same_payload? = - next_tx |> Contract.remove_seed_ownership() |> Transaction.same_payload?(expected_next_tx) + next_tx |> Contracts.remove_seed_ownership() |> Transaction.same_payload?(expected_next_tx) if same_payload? do {:ok, encoded_state} diff --git a/lib/archethic/mining/validation_context.ex b/lib/archethic/mining/validation_context.ex index 8b1a127dd9..49b8d66de4 100644 --- a/lib/archethic/mining/validation_context.ex +++ b/lib/archethic/mining/validation_context.ex @@ -301,7 +301,8 @@ defmodule Archethic.Mining.ValidationContext do ) do start = System.monotonic_time() - with :ok <- PendingTransactionValidation.validate_previous_public_key(tx), + with :ok <- PendingTransactionValidation.validate_transaction_version(tx), + :ok <- PendingTransactionValidation.validate_previous_public_key(tx), :ok <- PendingTransactionValidation.validate_previous_signature(tx), :ok <- PendingTransactionValidation.validate_size(tx), :ok <- PendingTransactionValidation.validate_contract(tx), diff --git a/lib/archethic/p2p/message/new_transaction.ex b/lib/archethic/p2p/message/new_transaction.ex index d68cb07eee..589bb8ffed 100644 --- a/lib/archethic/p2p/message/new_transaction.ex +++ b/lib/archethic/p2p/message/new_transaction.ex @@ -46,11 +46,8 @@ defmodule Archethic.P2P.Message.NewTransaction do }) do serialized_contract_context = case contract_context do - nil -> - <<0::8>> - - _ -> - <<1::8, Contract.Context.serialize(contract_context)::bitstring>> + nil -> <<0::8>> + _ -> <<1::8, Contract.Context.serialize(contract_context)::bitstring>> end <> -> - {nil, rest} - - <<1::8, rest::bitstring>> -> - Contract.Context.deserialize(rest) + <<0::8, rest::bitstring>> -> {nil, rest} + <<1::8, rest::bitstring>> -> Contract.Context.deserialize(rest) end {%__MODULE__{ diff --git a/lib/archethic/p2p/message/validate_smart_contract_call.ex b/lib/archethic/p2p/message/validate_smart_contract_call.ex index 6adb3b6bd2..8e400a85ad 100644 --- a/lib/archethic/p2p/message/validate_smart_contract_call.ex +++ b/lib/archethic/p2p/message/validate_smart_contract_call.ex @@ -6,9 +6,8 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do @enforce_keys [:recipient, :transaction, :timestamp] defstruct [:recipient, :transaction, :timestamp] - alias Archethic.Contracts.Contract.ActionWithoutTransaction alias Archethic.Contracts - alias Archethic.Contracts.Contract + alias Archethic.Contracts.Contract.ActionWithoutTransaction alias Archethic.Contracts.Contract.Context alias Archethic.Contracts.Contract.Failure alias Archethic.Contracts.Contract.ConditionRejected @@ -44,13 +43,12 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do @spec serialize(t()) :: bitstring() def serialize(%__MODULE__{ recipient: recipient, - transaction: tx = %Transaction{}, + transaction: tx = %Transaction{version: tx_version}, timestamp: timestamp }) do - tx_version = Transaction.version() recipient_bin = Recipient.serialize(recipient, tx_version) - <> end @@ -59,9 +57,8 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do """ @spec deserialize(bitstring()) :: {t(), bitstring()} def deserialize(data) when is_bitstring(data) do - tx_version = Transaction.version() - {recipient, rest} = Recipient.deserialize(data, tx_version) - {tx, <>} = Transaction.deserialize(rest) + {tx = %Transaction{version: tx_version}, rest} = Transaction.deserialize(data) + {recipient, <>} = Recipient.deserialize(rest, tx_version) { %__MODULE__{ @@ -124,7 +121,7 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do case get_last_transaction(recipient_address) do {:ok, contract_tx = %Transaction{validation_stamp: %ValidationStamp{timestamp: timestamp}}} -> with {:ok, contract} <- parse_contract(contract_tx), - trigger = Contract.get_trigger_for_recipient(recipient), + trigger = Recipient.get_trigger(recipient), :ok <- execute_condition( trigger, @@ -154,7 +151,7 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do ) do %SmartContractCallValidation{ status: :ok, - fee: calculate_fee(execution_result, datetime), + fee: calculate_fee(execution_result, contract, datetime), last_chain_sync_date: timestamp } else @@ -214,12 +211,12 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do end defp sign_next_transaction( - contract = %Contract{transaction: %Transaction{address: contract_address}}, + contract = %{transaction: %Transaction{address: contract_address}}, res = %ActionWithTransaction{next_tx: next_tx} ) do index = TransactionChain.get_size(contract_address) - case Contract.sign_next_transaction(contract, next_tx, index) do + case Contracts.sign_next_transaction(contract, next_tx, index) do {:ok, tx} -> {:ok, %ActionWithTransaction{res | next_tx: tx}} _ -> {:error, :parsing_error, "Unable to sign contract transaction"} end @@ -255,19 +252,26 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCall do defp calculate_fee( %ActionWithTransaction{next_tx: next_tx, encoded_state: encoded_state}, + contract, timestamp ) do - previous_usd_price = - timestamp - |> OracleChain.get_last_scheduling_date() - |> OracleChain.get_uco_price() - |> Keyword.fetch!(:usd) - - # Here we use a nil contract_context as we return the fees the user has to pay for the contract - Mining.get_transaction_fee(next_tx, nil, previous_usd_price, timestamp, encoded_state) + case sign_next_transaction(contract, next_tx) do + {:ok, tx} -> + previous_usd_price = + timestamp + |> OracleChain.get_last_scheduling_date() + |> OracleChain.get_uco_price() + |> Keyword.fetch!(:usd) + + # Here we use a nil contract_context as we return the fees the user has to pay for the contract + Mining.get_transaction_fee(tx, nil, previous_usd_price, timestamp, encoded_state) + + _ -> + 0 + end end - defp calculate_fee(_, _), do: 0 + defp calculate_fee(_, _, _), do: 0 defp enough_funds_to_send?( %ActionWithTransaction{next_tx: tx}, diff --git a/lib/archethic/p2p/message/validate_transaction.ex b/lib/archethic/p2p/message/validate_transaction.ex index 3d667fc935..e206c58be8 100644 --- a/lib/archethic/p2p/message/validate_transaction.ex +++ b/lib/archethic/p2p/message/validate_transaction.ex @@ -51,14 +51,9 @@ defmodule Archethic.P2P.Message.ValidateTransaction do def serialize(%__MODULE__{transaction: tx, contract_context: contract_context, inputs: inputs}) do inputs_bin = - inputs - |> Enum.map(&VersionedUnspentOutput.serialize/1) - |> :erlang.list_to_bitstring() + inputs |> Enum.map(&VersionedUnspentOutput.serialize/1) |> :erlang.list_to_bitstring() - inputs_size = - inputs - |> length() - |> Utils.VarInt.from_value() + inputs_size = inputs |> length() |> Utils.VarInt.from_value() < Crypto.hash() end + @doc """ + Determines if a chain is valid according to : + - the proof of integrity + - the chained public keys and addresses + - the timestamping + + ## Examples + + iex> tx2 = %Transaction{ + ...> address: + ...> <<0, 0, 61, 7, 130, 64, 140, 226, 192, 8, 238, 88, 226, 106, 137, 45, 69, 113, 239, + ...> 240, 45, 55, 225, 169, 170, 121, 238, 136, 192, 161, 252, 33, 71, 3>>, + ...> type: :transfer, + ...> data: %TransactionData{}, + ...> previous_public_key: + ...> <<0, 0, 96, 233, 188, 240, 217, 251, 22, 2, 210, 59, 170, 25, 33, 61, 124, 135, 138, + ...> 65, 189, 207, 253, 84, 254, 193, 42, 130, 170, 159, 34, 72, 52, 162>>, + ...> previous_signature: + ...> <<232, 186, 237, 220, 71, 212, 177, 17, 156, 167, 145, 125, 92, 70, 213, 120, 216, + ...> 215, 255, 158, 104, 117, 162, 18, 142, 75, 73, 205, 71, 7, 141, 90, 178, 239, 212, + ...> 227, 167, 161, 155, 143, 43, 50, 6, 7, 97, 130, 134, 174, 7, 235, 183, 88, 165, + ...> 197, 25, 219, 84, 232, 135, 42, 112, 58, 181, 13>>, + ...> origin_signature: + ...> <<232, 186, 237, 220, 71, 212, 177, 17, 156, 167, 145, 125, 92, 70, 213, 120, 216, + ...> 215, 255, 158, 104, 117, 162, 18, 142, 75, 73, 205, 71, 7, 141, 90, 178, 239, 212, + ...> 227, 167, 161, 155, 143, 43, 50, 6, 7, 97, 130, 134, 174, 7, 235, 183, 88, 165, + ...> 197, 25, 219, 84, 232, 135, 42, 112, 58, 181, 13>> + ...> } + ...> + ...> tx1 = %Transaction{ + ...> address: + ...> <<0, 0, 109, 140, 2, 60, 50, 109, 201, 126, 206, 164, 10, 86, 225, 58, 136, 241, 118, + ...> 74, 3, 215, 6, 106, 165, 24, 51, 192, 212, 58, 143, 33, 68, 2>>, + ...> type: :transfer, + ...> data: %TransactionData{}, + ...> previous_public_key: + ...> <<0, 0, 221, 228, 196, 111, 16, 222, 0, 119, 32, 150, 228, 25, 206, 79, 37, 213, 8, + ...> 130, 22, 212, 99, 55, 72, 11, 248, 250, 11, 140, 137, 167, 118, 253>>, + ...> previous_signature: + ...> <<232, 186, 237, 220, 71, 212, 177, 17, 156, 167, 145, 125, 92, 70, 213, 120, 216, + ...> 215, 255, 158, 104, 117, 162, 18, 142, 75, 73, 205, 71, 7, 141, 90, 178, 239, 212, + ...> 227, 167, 161, 155, 143, 43, 50, 6, 7, 97, 130, 134, 174, 7, 235, 183, 88, 165, + ...> 197, 25, 219, 84, 232, 135, 42, 112, 58, 181, 13>>, + ...> origin_signature: + ...> <<232, 186, 237, 220, 71, 212, 177, 17, 156, 167, 145, 125, 92, 70, 213, 120, 216, + ...> 215, 255, 158, 104, 117, 162, 18, 142, 75, 73, 205, 71, 7, 141, 90, 178, 239, 212, + ...> 227, 167, 161, 155, 143, 43, 50, 6, 7, 97, 130, 134, 174, 7, 235, 183, 88, 165, + ...> 197, 25, 219, 84, 232, 135, 42, 112, 58, 181, 13>> + ...> } + ...> + ...> tx1 = %{ + ...> tx1 + ...> | validation_stamp: %ValidationStamp{ + ...> proof_of_integrity: TransactionChain.proof_of_integrity([tx1]), + ...> timestamp: ~U[2022-09-10 10:00:00Z] + ...> } + ...> } + ...> + ...> tx2 = %{ + ...> tx2 + ...> | validation_stamp: %ValidationStamp{ + ...> proof_of_integrity: TransactionChain.proof_of_integrity([tx2, tx1]), + ...> timestamp: ~U[2022-12-10 10:00:00Z] + ...> } + ...> } + ...> + ...> TransactionChain.valid?([tx2, tx1]) + true + """ + @spec valid?([Transaction.t(), ...]) :: boolean + def valid?([ + tx = %Transaction{validation_stamp: %ValidationStamp{proof_of_integrity: poi}}, + nil + ]) do + if poi == proof_of_integrity([tx]) do + true + else + Logger.error("Invalid proof of integrity", + transaction_address: Base.encode16(tx.address), + transaction_type: tx.type + ) + + false + end + end + + def valid?([ + last_tx = %Transaction{ + previous_public_key: previous_public_key, + validation_stamp: %ValidationStamp{timestamp: timestamp, proof_of_integrity: poi} + }, + prev_tx = %Transaction{ + address: previous_address, + validation_stamp: %ValidationStamp{ + timestamp: previous_timestamp + } + } + | _ + ]) do + cond do + proof_of_integrity([Transaction.to_pending(last_tx), prev_tx]) != poi -> + Logger.error("Invalid proof of integrity", + transaction_address: Base.encode16(last_tx.address), + transaction_type: last_tx.type + ) + + false + + Crypto.derive_address(previous_public_key) != previous_address -> + Logger.error("Invalid previous public key", + transaction_type: last_tx.type, + transaction_address: Base.encode16(last_tx.address) + ) + + false + + DateTime.diff(timestamp, previous_timestamp) < 0 -> + Logger.error("Invalid timestamp", + transaction_type: last_tx.type, + transaction_address: Base.encode16(last_tx.address) + ) + + false + + true -> + true + end + end + @doc """ By checking at the proof of integrity (determined by the coordinator) we can ensure a transaction is not the first (because the poi contains the hash of the previous if any) @@ -1268,11 +1397,11 @@ defmodule Archethic.TransactionChain do poi == proof_of_integrity([tx]) end - @doc """ - Load the transaction into the TransactionChain context filling the memory tables - """ - @spec load_transaction(Transaction.t()) :: :ok - defdelegate load_transaction(tx), to: MemTablesLoader + # @doc """ + # Load the transaction into the TransactionChain context filling the memory tables + # """ + # @spec load_transaction(Transaction.t()) :: :ok + # defdelegate load_transaction(tx), to: MemTablesLoader @doc """ Return the list inputs for a given transaction diff --git a/lib/archethic/transaction_chain/mem_tables_loader.ex b/lib/archethic/transaction_chain/mem_tables_loader.ex index 75001e9cd5..cefaa2d264 100644 --- a/lib/archethic/transaction_chain/mem_tables_loader.ex +++ b/lib/archethic/transaction_chain/mem_tables_loader.ex @@ -1,95 +1,95 @@ -defmodule Archethic.TransactionChain.MemTablesLoader do - @moduledoc false - - use GenServer - @vsn 1 - - alias Archethic.Contracts.Contract - alias Archethic.Contracts.Conditions - - alias Archethic.DB - - alias Archethic.TransactionChain.MemTables.PendingLedger - alias Archethic.TransactionChain.Transaction - alias Archethic.TransactionChain.TransactionData - - require Logger - - @query_fields [ - :address, - :type, - :previous_public_key, - data: [:code, :recipients], - validation_stamp: [:timestamp] - ] - - @excluded_types [ - :node, - :node_shared_secrets, - :oracle, - :oracle_summary, - :node_rewards, - :mint_rewards, - :origin - ] - - def start_link(args \\ []) do - GenServer.start_link(__MODULE__, args, name: __MODULE__) - end - - def init(_args) do - DB.list_transactions(@query_fields) - |> Stream.reject(&(&1.type in @excluded_types)) - |> Stream.each(&load_transaction/1) - |> Stream.run() - - {:ok, []} - end - - @doc """ - Ingest the transaction into the memory tables - """ - @spec load_transaction(Transaction.t()) :: :ok - def load_transaction(tx = %Transaction{address: tx_address, type: tx_type}) do - :ok = handle_pending_transaction(tx) - :ok = handle_transaction_recipients(tx) - - Logger.info("Loaded into in memory transactionchain tables", - transaction_address: Base.encode16(tx_address), - transaction_type: tx_type - ) - end - - defp handle_pending_transaction(%Transaction{address: address, type: :code_proposal}) do - PendingLedger.add_address(address) - end - - defp handle_pending_transaction(%Transaction{data: %TransactionData{code: ""}}), do: :ok - - defp handle_pending_transaction(tx = %Transaction{address: address}) do - %Contract{conditions: conditions} = Contract.from_transaction!(tx) - - # TODO: handle {:transaction, action, args_names} - case Map.get(conditions, {:transaction, nil, nil}) do - nil -> - :ok - - transaction_conditions -> - # TODO: improve the criteria of pending detection - if Conditions.empty?(transaction_conditions) do - :ok - else - PendingLedger.add_address(address) - end - end - end - - defp handle_transaction_recipients(%Transaction{ - address: address, - data: %TransactionData{recipients: recipients} - }) do - recipients - |> Enum.map(& &1.address) - |> Enum.each(&PendingLedger.add_signature(&1, address)) - end -end +# defmodule Archethic.TransactionChain.MemTablesLoader do +# @moduledoc false + +# use GenServer +# @vsn 1 + +# alias Archethic.Contracts.Contract +# alias Archethic.Contracts.Conditions + +# alias Archethic.DB + +# alias Archethic.TransactionChain.MemTables.PendingLedger +# alias Archethic.TransactionChain.Transaction +# alias Archethic.TransactionChain.TransactionData + +# require Logger + +# @query_fields [ +# :address, +# :type, +# :previous_public_key, +# data: [:code, :recipients], +# validation_stamp: [:timestamp] +# ] + +# @excluded_types [ +# :node, +# :node_shared_secrets, +# :oracle, +# :oracle_summary, +# :node_rewards, +# :mint_rewards, +# :origin +# ] + +# def start_link(args \\ []) do +# GenServer.start_link(__MODULE__, args, name: __MODULE__) +# end + +# def init(_args) do +# DB.list_transactions(@query_fields) +# |> Stream.reject(&(&1.type in @excluded_types)) +# |> Stream.each(&load_transaction/1) +# |> Stream.run() + +# {:ok, []} +# end + +# @doc """ +# Ingest the transaction into the memory tables +# """ +# @spec load_transaction(Transaction.t()) :: :ok +# def load_transaction(tx = %Transaction{address: tx_address, type: tx_type}) do +# # :ok = handle_pending_transaction(tx) +# :ok = handle_transaction_recipients(tx) + +# Logger.info("Loaded into in memory transactionchain tables", +# transaction_address: Base.encode16(tx_address), +# transaction_type: tx_type +# ) +# end + +# defp handle_pending_transaction(%Transaction{address: address, type: :code_proposal}) do +# PendingLedger.add_address(address) +# end + +# defp handle_pending_transaction(%Transaction{data: %TransactionData{code: ""}}), do: :ok + +# defp handle_pending_transaction(tx = %Transaction{address: address}) do +# %Contract{conditions: conditions} = Contract.from_transaction!(tx) + +# # TODO: handle {:transaction, action, args_names} +# case Map.get(conditions, {:transaction, nil, nil}) do +# nil -> +# :ok + +# transaction_conditions -> +# # TODO: improve the criteria of pending detection +# if Conditions.empty?(transaction_conditions) do +# :ok +# else +# PendingLedger.add_address(address) +# end +# end +# end + +# defp handle_transaction_recipients(%Transaction{ +# address: address, +# data: %TransactionData{recipients: recipients} +# }) do +# recipients +# |> Enum.map(& &1.address) +# |> Enum.each(&PendingLedger.add_signature(&1, address)) +# end +# end diff --git a/lib/archethic/transaction_chain/supervisor.ex b/lib/archethic/transaction_chain/supervisor.ex index 11501c4462..12c94bdc3b 100644 --- a/lib/archethic/transaction_chain/supervisor.ex +++ b/lib/archethic/transaction_chain/supervisor.ex @@ -6,7 +6,7 @@ defmodule Archethic.TransactionChain.Supervisor do alias Archethic.TransactionChain.DBLedger.Supervisor, as: DBLedgerSupervisor alias Archethic.TransactionChain.MemTables.KOLedger alias Archethic.TransactionChain.MemTables.PendingLedger - alias Archethic.TransactionChain.MemTablesLoader + # alias Archethic.TransactionChain.MemTablesLoader alias Archethic.Utils @@ -15,7 +15,12 @@ defmodule Archethic.TransactionChain.Supervisor do end def init(_args) do - optional_children = [PendingLedger, KOLedger, MemTablesLoader, DBLedgerSupervisor] + optional_children = [ + PendingLedger, + KOLedger, + # MemTablesLoader, + DBLedgerSupervisor + ] children = Utils.configurable_children(optional_children) diff --git a/lib/archethic/transaction_chain/transaction.ex b/lib/archethic/transaction_chain/transaction.ex index 21ab56c575..cc974d4de8 100755 --- a/lib/archethic/transaction_chain/transaction.ex +++ b/lib/archethic/transaction_chain/transaction.ex @@ -36,7 +36,7 @@ defmodule Archethic.TransactionChain.Transaction do @unit_uco 100_000_000 - @version 3 + @version 4 defstruct [ :address, @@ -135,7 +135,9 @@ defmodule Archethic.TransactionChain.Transaction do def new(type, data = %TransactionData{}) do {previous_public_key, next_public_key} = get_transaction_public_keys(type) + # TODO: update network chain to use WASM contract to update version %__MODULE__{ + version: 3, address: Crypto.derive_address(next_public_key), type: type, data: data, @@ -150,7 +152,9 @@ defmodule Archethic.TransactionChain.Transaction do def new(type, data = %TransactionData{}, index) do {previous_public_key, next_public_key} = get_transaction_public_keys(type, index) + # TODO: update network chain to use WASM contract to update version %__MODULE__{ + version: 3, address: Crypto.derive_address(next_public_key), type: type, data: data, @@ -170,18 +174,25 @@ defmodule Archethic.TransactionChain.Transaction do data :: TransactionData.t(), seed :: binary(), index :: non_neg_integer(), - curve :: Crypto.supported_curve(), - origin :: Crypto.supported_origin() + opts :: [ + curve: Crypto.supported_curve(), + origin: Crypto.supported_origin(), + version: pos_integer() + ] ) :: t() def new( type, data = %TransactionData{}, seed, index, - curve \\ Crypto.default_curve(), - origin \\ :software + opts \\ [] ) when type in @transaction_types and is_binary(seed) and is_integer(index) and index >= 0 do + curve = Keyword.get(opts, :curve, Crypto.default_curve()) + origin = Keyword.get(opts, :origin, :software) + # TODO: update network chain to use WASM contract to update version + version = Keyword.get(opts, :version, 3) + {previous_public_key, previous_private_key} = Crypto.derive_keypair(seed, index, curve, origin) @@ -191,7 +202,8 @@ defmodule Archethic.TransactionChain.Transaction do address: Crypto.derive_address(next_public_key), type: type, data: data, - previous_public_key: previous_public_key + previous_public_key: previous_public_key, + version: version } |> previous_sign_transaction_with_key(previous_private_key) |> origin_sign_transaction() @@ -201,20 +213,26 @@ defmodule Archethic.TransactionChain.Transaction do Create transaction with the direct use of private and public keys """ @spec new_with_keys( - transaction_type(), - TransactionData.t(), - Crypto.key(), - Crypto.key(), - Crypto.key() + type :: transaction_type(), + data :: TransactionData.t(), + previous_private_key :: Crypto.key(), + previous_public_key :: Crypto.key(), + next_public_key :: Crypto.key(), + opts :: Keyword.t() ) :: t() def new_with_keys( type, data = %TransactionData{}, previous_private_key, previous_public_key, - next_public_key + next_public_key, + opts \\ [] ) do + # TODO: update network chain to use WASM contract to update version + version = Keyword.get(opts, :version, 3) + %__MODULE__{ + version: version, address: Crypto.derive_address(next_public_key), type: type, data: data, diff --git a/lib/archethic/transaction_chain/transaction/data.ex b/lib/archethic/transaction_chain/transaction/data.ex index e0a408ad4b..bc78b62155 100755 --- a/lib/archethic/transaction_chain/transaction/data.ex +++ b/lib/archethic/transaction_chain/transaction/data.ex @@ -5,19 +5,26 @@ defmodule Archethic.TransactionChain.TransactionData do alias Archethic.TransactionChain.Transaction + alias __MODULE__.Contract alias __MODULE__.Ledger alias __MODULE__.Ownership alias __MODULE__.Recipient alias Archethic.Utils.VarInt - defstruct recipients: [], ledger: %Ledger{}, code: "", ownerships: [], content: "" + defstruct recipients: [], + ledger: %Ledger{}, + code: "", + ownerships: [], + content: "", + contract: nil @typedoc """ Transaction data is composed from: - Recipients: list of recipients for smart contract interactions - Ledger: Movement operations on UCO TOKEN or Stock ledger - Code: Contains the smart contract code including triggers, conditions and actions + - Contract: Contains the webassembly smart contract code and its manifest - Ownerships: List of the authorizations and delegations to proof ownership of secrets - Content: Free content to store any data as binary """ @@ -25,6 +32,7 @@ defmodule Archethic.TransactionChain.TransactionData do recipients: list(Recipient.t()), ledger: Ledger.t(), code: binary(), + contract: nil | Contract.t(), ownerships: list(Ownership.t()), content: binary() } @@ -45,9 +53,13 @@ defmodule Archethic.TransactionChain.TransactionData do :zlib.unzip(code) end - @spec code_size_valid?(String.t()) :: bool() - def code_size_valid?(code) do - compress_code(code) |> byte_size() < @code_max_size + @spec code_size_valid?(code :: binary(), compressed :: boolean()) :: boolean() + def code_size_valid?(code, compressed? \\ true) do + if compressed? do + code |> byte_size() < @code_max_size + else + compress_code(code) |> byte_size() < @code_max_size + end end @doc """ @@ -59,8 +71,7 @@ defmodule Archethic.TransactionChain.TransactionData do serialization_mode :: Transaction.serialization_mode() ) :: bitstring() def serialize( - %__MODULE__{ - code: code, + data = %__MODULE__{ content: content, ownerships: ownerships, ledger: ledger, @@ -82,23 +93,31 @@ defmodule Archethic.TransactionChain.TransactionData do encoded_ownership_len = length(ownerships) |> VarInt.from_value() encoded_recipients_len = length(recipients) |> VarInt.from_value() - code = - case mode do - :compact -> - # used when msg passing - compress_code(code) + smart_contract_binary = serialize_contract(data, tx_version, mode) - :extended -> - # used when signing - code - end - - <> end + defp serialize_contract(%__MODULE__{code: code}, tx_version, mode) when tx_version <= 3 do + code = + case mode do + # used when msg passing + :compact -> compress_code(code) + # used when signing + :extended -> code + end + + <> + end + + defp serialize_contract(%__MODULE__{contract: nil}, _, _), do: <<0::8>> + + defp serialize_contract(%__MODULE__{contract: contract}, tx_version, mode), + do: <<1::8, Contract.serialize(contract, tx_version, mode)::bitstring>> + @doc """ Deserialize encoded transaction data """ @@ -107,12 +126,12 @@ defmodule Archethic.TransactionChain.TransactionData do tx_version :: pos_integer(), serialization_mode :: Transaction.serialization_mode() ) :: {t(), bitstring()} - def deserialize( - <>, - tx_version, - serialization_mode \\ :compact - ) do + def deserialize(bin, tx_version, serialization_mode \\ :compact) + + def deserialize(bin, tx_version, serialization_mode) do + {tx_data, <>} = + deserialize_contract(bin, tx_version, serialization_mode) + {nb_ownerships, rest} = VarInt.get_value(rest) {ownerships, rest} = reduce_ownerships(rest, nb_ownerships, [], tx_version) @@ -123,29 +142,35 @@ defmodule Archethic.TransactionChain.TransactionData do {recipients, rest} = reduce_recipients(rest, nb_recipients, [], tx_version, serialization_mode) - # no need to check for serialization_mode because we never deserialize(:extended) - code = - try do - decompress_code(code) - rescue - _ -> - # may happen during upgrade when a V node send msg to a V+1 node (V=version) - # try/rescue can be removed on next release - code - end - { %__MODULE__{ - code: code, - content: content, - ownerships: ownerships, - ledger: ledger, - recipients: recipients + tx_data + | content: content, + ownerships: ownerships, + ledger: ledger, + recipients: recipients }, rest } end + defp deserialize_contract( + <>, + tx_version, + mode + ) + when tx_version <= 3 do + code = if mode == :extended, do: code, else: decompress_code(code) + {%__MODULE__{code: code}, rest} + end + + defp deserialize_contract(<<0::8, rest::bitstring>>, _, _), do: {%__MODULE__{}, rest} + + defp deserialize_contract(<<1::8, rest::bitstring>>, tx_version, mode) do + {contract, rest} = Contract.deserialize(rest, tx_version, mode) + {%__MODULE__{contract: contract}, rest} + end + defp reduce_ownerships(rest, 0, _acc, _version), do: {[], rest} defp reduce_ownerships(rest, nb_ownerships, acc, _version) when nb_ownerships == length(acc), @@ -170,17 +195,12 @@ defmodule Archethic.TransactionChain.TransactionData do @spec cast(map()) :: t() def cast(data = %{}) do code = Map.get(data, :code, "") - - code = - if String.printable?(code) do - code - else - decompress_code(code) - end + code = if String.printable?(code), do: code, else: decompress_code(code) %__MODULE__{ content: Map.get(data, :content, ""), code: code, + contract: Map.get(data, :contract) |> Contract.cast(), ledger: Map.get(data, :ledger, %Ledger{}) |> Ledger.cast(), ownerships: Map.get(data, :ownerships, []) |> Enum.map(&Ownership.cast/1), recipients: Map.get(data, :recipients, []) |> Enum.map(&Recipient.cast/1) @@ -192,6 +212,7 @@ defmodule Archethic.TransactionChain.TransactionData do %{ content: "", code: "", + contract: nil, ledger: Ledger.to_map(nil), ownerships: [], recipients: [] @@ -201,6 +222,7 @@ defmodule Archethic.TransactionChain.TransactionData do def to_map(%__MODULE__{ content: content, code: code, + contract: contract, ledger: ledger, ownerships: ownerships, recipients: recipients @@ -208,6 +230,7 @@ defmodule Archethic.TransactionChain.TransactionData do %{ content: content, code: code, + contract: Contract.to_map(contract), ledger: Ledger.to_map(ledger), ownerships: Enum.map(ownerships, &Ownership.to_map/1), recipients: Enum.map(recipients, &Recipient.to_address/1), diff --git a/lib/archethic/transaction_chain/transaction/data/contract.ex b/lib/archethic/transaction_chain/transaction/data/contract.ex new file mode 100644 index 0000000000..3314c010a1 --- /dev/null +++ b/lib/archethic/transaction_chain/transaction/data/contract.ex @@ -0,0 +1,77 @@ +defmodule Archethic.TransactionChain.TransactionData.Contract do + @moduledoc """ + Represents a smart contract defnition + + - bytecode: the byte code of the compilied wasm code + - manifest: the description of the contract functions + """ + alias Archethic.TransactionChain.Transaction + alias Archethic.Utils.TypedEncoding + + @enforce_keys [:bytecode, :manifest] + defstruct [:bytecode, :manifest] + + @type t :: %__MODULE__{ + bytecode: binary(), + manifest: map() + } + + @doc """ + Serialize a contract + """ + @spec serialize( + recipient :: t(), + version :: pos_integer(), + serialization_mode :: Transaction.serialization_mode() + ) :: bitstring() + def serialize(recipient, version, serialization_mode \\ :compact) + + def serialize(%__MODULE__{bytecode: bytecode, manifest: manifest}, _version, serialization_mode) do + <> + end + + @doc """ + Deserialize a contract + """ + @spec deserialize( + rest :: bitstring(), + version :: pos_integer(), + serialization_mode :: Transaction.serialization_mode() + ) :: {t(), bitstring()} + def deserialize(binary, version, serialization_mode \\ :compact) + + def deserialize( + <>, + _, + serialization_mode + ) do + {manifest, rest} = TypedEncoding.deserialize(rest, serialization_mode) + + {%__MODULE__{bytecode: bytecode, manifest: manifest}, rest} + end + + @doc false + @spec cast(contract :: nil | map()) :: nil | t() + def cast(nil), do: nil + + def cast(%{bytecode: bytecode, manifest: manifest}), + do: %__MODULE__{bytecode: bytecode, manifest: manifest} + + @doc false + @spec to_map(contract :: nil | t()) :: nil | map() + def to_map(nil), do: nil + + def to_map(%__MODULE__{bytecode: bytecode, manifest: manifest}) do + %{"functions" => functions, "state" => state} = Map.get(manifest, "abi") + %{"from" => from} = Map.get(manifest, "upgradeOpts") + + %{ + bytecode: Base.encode16(bytecode), + manifest: %{ + abi: %{functions: functions, state: state}, + upgrade_opts: %{from: from} + } + } + end +end diff --git a/lib/archethic/transaction_chain/transaction/data/recipient.ex b/lib/archethic/transaction_chain/transaction/data/recipient.ex index e900d0ef62..f38094b6f1 100644 --- a/lib/archethic/transaction_chain/transaction/data/recipient.ex +++ b/lib/archethic/transaction_chain/transaction/data/recipient.ex @@ -7,7 +7,7 @@ defmodule Archethic.TransactionChain.TransactionData.Recipient do alias Archethic.Crypto alias Archethic.TransactionChain.Transaction alias Archethic.Utils - alias __MODULE__.ArgumentsEncoding + alias Archethic.Utils.TypedEncoding defstruct [:address, :action, :args] @@ -17,16 +17,9 @@ defmodule Archethic.TransactionChain.TransactionData.Recipient do @type t :: %__MODULE__{ address: Crypto.prepended_hash(), action: String.t() | nil, - args: list(any()) | nil + args: list(any()) | map() | nil } - @doc """ - Return wether this is a named action call or not - """ - @spec is_named_action?(recipient :: t()) :: boolean() - def is_named_action?(%__MODULE__{action: nil, args: nil}), do: false - def is_named_action?(%__MODULE__{}), do: true - @doc """ Serialize a recipient """ @@ -47,39 +40,28 @@ defmodule Archethic.TransactionChain.TransactionData.Recipient do def serialize( %__MODULE__{address: address, action: action, args: args}, - _version = 2, - _serialization_mode + version, + serialization_mode ) do - # action is stored on 8 bytes which means 255 characters - # we force that in the interpreters (action & condition) - action_bytes = byte_size(action) + serialized_args = serialize_args(args, version, serialization_mode) - serialized_args = Jason.encode!(args) - - args_bytes = - serialized_args - |> byte_size() - |> Utils.VarInt.from_value() - - <<@named_action::8, address::binary, action_bytes::8, action::binary, args_bytes::binary, + <<@named_action::8, address::binary, byte_size(action)::8, action::binary, serialized_args::bitstring>> end - def serialize( - %__MODULE__{address: address, action: action, args: args}, - _version = 3, - serialization_mode - ) do - # action is stored on 8 bytes which means 255 characters - # we force that in the interpreters (action & condition) - action_bytes = byte_size(action) - - serialized_args = ArgumentsEncoding.serialize(args, serialization_mode) + defp serialize_args(args, _version = 2, _) do + serialized_args = Jason.encode!(args) + args_bytes = serialized_args |> byte_size() |> Utils.VarInt.from_value() + <> + end - <<@named_action::8, address::binary, action_bytes::8, action::binary, - serialized_args::bitstring>> + defp serialize_args(args, _version = 3, mode) when is_list(args) do + bin = args |> Enum.map(&TypedEncoding.serialize(&1, mode)) |> :erlang.list_to_bitstring() + <> end + defp serialize_args(args, _, mode) when is_map(args), do: TypedEncoding.serialize(args, mode) + @doc """ Deserialize a recipient """ @@ -97,45 +79,37 @@ defmodule Archethic.TransactionChain.TransactionData.Recipient do def deserialize(<<@unnamed_action::8, rest::bitstring>>, _version, _serialization_mode) do {address, rest} = Utils.deserialize_address(rest) - - { - %__MODULE__{address: address}, - rest - } + {%__MODULE__{address: address}, rest} end - def deserialize(<<@named_action::8, rest::bitstring>>, _version = 2, _serialization_mode) do + def deserialize(<<@named_action::8, rest::bitstring>>, version, serialization_mode) do {address, <>} = Utils.deserialize_address(rest) <> = rest + {args, rest} = deserialize_args(rest, version, serialization_mode) + + {%__MODULE__{address: address, action: action, args: args}, rest} + end + defp deserialize_args(rest, _version = 2, _) do {args_bytes, rest} = Utils.VarInt.get_value(rest) <> = rest - - { - %__MODULE__{ - address: address, - action: action, - args: Jason.decode!(args) - }, - rest - } + {Jason.decode!(args), rest} end - def deserialize(<<@named_action::8, rest::bitstring>>, _version = 3, serialization_mode) do - {address, <>} = Utils.deserialize_address(rest) - <> = rest - {args, rest} = ArgumentsEncoding.deserialize(rest, serialization_mode) - - { - %__MODULE__{ - address: address, - action: action, - args: args - }, - rest - } + defp deserialize_args(<<0::8, rest::bitstring>>, _version = 3, _), do: {[], rest} + + defp deserialize_args(<>, _version = 3, mode) do + {args, rest} = + Enum.reduce(1..nb_args, {[], rest}, fn _, {args, rest} -> + {arg, rest} = TypedEncoding.deserialize(rest, mode) + {[arg | args], rest} + end) + + {Enum.reverse(args), rest} end + defp deserialize_args(rest, _, mode), do: TypedEncoding.deserialize(rest, mode) + @doc false @spec cast(recipient :: binary() | map()) :: t() def cast(recipient) when is_binary(recipient), do: %__MODULE__{address: recipient} @@ -153,4 +127,20 @@ defmodule Archethic.TransactionChain.TransactionData.Recipient do @spec to_address(recipient :: t()) :: list(binary()) def to_address(%{address: address}), do: address + + @type trigger_key :: {:transaction, nil | String.t(), nil | non_neg_integer()} + + @doc """ + Return the args names for this recipient or nil + """ + @spec get_trigger(t()) :: trigger_key() + def get_trigger(%__MODULE__{action: nil, args: nil}), do: {:transaction, nil, nil} + + def get_trigger(%__MODULE__{action: action, args: args_values}) + when is_list(args_values), + do: {:transaction, action, length(args_values)} + + def get_trigger(%__MODULE__{action: action, args: args_values}) + when is_map(args_values), + do: {:transaction, action, map_size(args_values)} end diff --git a/lib/archethic/transaction_chain/transaction/data/recipient/arguments_encoding.ex b/lib/archethic/transaction_chain/transaction/data/recipient/arguments_encoding.ex deleted file mode 100644 index 4dfe25bf88..0000000000 --- a/lib/archethic/transaction_chain/transaction/data/recipient/arguments_encoding.ex +++ /dev/null @@ -1,36 +0,0 @@ -defmodule Archethic.TransactionChain.TransactionData.Recipient.ArgumentsEncoding do - @moduledoc """ - Handle encoding of recipients arguments - """ - - alias Archethic.TransactionChain.Transaction - alias Archethic.Utils.TypedEncoding - - @spec serialize(args :: list(TypedEncoding.arg()), mode :: Transaction.serialization_mode()) :: - bitstring() - def serialize(args, mode) do - bin = - args - |> Enum.map(&TypedEncoding.serialize(&1, mode)) - |> :erlang.list_to_bitstring() - - <> - end - - @spec deserialize(binary :: bitstring(), mode :: Transaction.serialization_mode()) :: - {list(TypedEncoding.arg()), bitstring()} - def deserialize(<>, mode) do - do_deserialize(rest, nb_args, [], mode) - end - - defp do_deserialize(<<>>, _nb_args, acc, _mode), do: {Enum.reverse(acc), <<>>} - - defp do_deserialize(rest, nb_args, acc, _mode) when length(acc) == nb_args do - {Enum.reverse(acc), rest} - end - - defp do_deserialize(binary, nb_args, acc, mode) do - {arg, rest} = TypedEncoding.deserialize(binary, mode) - do_deserialize(rest, nb_args, [arg | acc], mode) - end -end diff --git a/lib/archethic/transaction_chain/transaction/data/versioned_recipient.ex b/lib/archethic/transaction_chain/transaction/data/versioned_recipient.ex new file mode 100644 index 0000000000..38927599db --- /dev/null +++ b/lib/archethic/transaction_chain/transaction/data/versioned_recipient.ex @@ -0,0 +1,58 @@ +defmodule Archethic.TransactionChain.TransactionData.VersionedRecipient do + @moduledoc """ + Wrap Recipient struct with the transaction version. + Usefull when recipient is used alone without link with its transaction + """ + alias Archethic.Crypto + alias Archethic.TransactionChain.Transaction + alias Archethic.TransactionChain.TransactionData.Recipient + + defstruct [:address, :action, :args, :tx_version] + + @type t :: %__MODULE__{ + address: Crypto.prepended_hash(), + action: String.t() | nil, + args: list(any()) | map() | nil, + tx_version: non_neg_integer() + } + + @doc """ + Serialize a versioned recipient + """ + @spec serialize( + versioned_recipient :: t(), + serialization_mode :: Transaction.serialization_mode() + ) :: bitstring() + def serialize(recipient, serialization_mode \\ :compact) + + def serialize(verisioned_recipient = %__MODULE__{tx_version: version}, serialization_mode) do + recipient = unwrap_recipient(verisioned_recipient) + <> + end + + @doc """ + Deserialize a recipient + """ + @spec deserialize(rest :: bitstring(), serialization_mode :: Transaction.serialization_mode()) :: + {t(), bitstring()} + def deserialize(binary, serialization_mode \\ :compact) + + def deserialize(<>, serialization_mode) do + {recipient, rest} = Recipient.deserialize(rest, version, serialization_mode) + {wrap_recipient(recipient, version), rest} + end + + @doc """ + Wrap a recipient into a versioned recipient + """ + @spec wrap_recipient(recipient :: Recipient.t(), tx_version :: non_neg_integer()) :: t() + def wrap_recipient(%Recipient{address: address, action: action, args: args}, tx_version), + do: %__MODULE__{address: address, action: action, args: args, tx_version: tx_version} + + @doc """ + Unwrap a versioned recipient into a recipient + """ + @spec unwrap_recipient(versioned_recipient :: t()) :: Recipient.t() + def unwrap_recipient(%__MODULE__{address: address, action: action, args: args}), + do: %Recipient{address: address, action: action, args: args} +end diff --git a/lib/archethic/utils.ex b/lib/archethic/utils.ex index 52d2ad0058..28121474a3 100644 --- a/lib/archethic/utils.ex +++ b/lib/archethic/utils.ex @@ -276,9 +276,7 @@ defmodule Archethic.Utils do atomize_keys(map, nest_dot?, to_snake_case?) end - def atomize_keys(struct = %{__struct__: _}, _, _) do - struct - end + def atomize_keys(map, _, _) when is_struct(map), do: map def atomize_keys(map = %{}, nest_dot?, to_snake_case?) do map @@ -320,9 +318,7 @@ defmodule Archethic.Utils do atomize_keys(rest, nest_dot?, to_snake_case?) end - def atomize_keys(not_a_map, _, _) do - not_a_map - end + def atomize_keys(not_a_map, _, _), do: not_a_map defp nested_path(_keys, acc \\ []) @@ -1329,4 +1325,57 @@ defmodule Archethic.Utils do {Enum.reverse(items), more?, offset} end + + @doc """ + Replace bitstring by hex + """ + @spec bin2hex(any()) :: any() + def bin2hex(data = %DateTime{}), do: data + + def bin2hex(data = %{__struct__: struct}) do + data + |> Map.from_struct() + |> bin2hex() + |> Map.put(:__struct__, struct) + end + + def bin2hex(data) when is_map(data) do + Enum.reduce(data, %{}, fn {key, value}, acc -> + Map.put(acc, bin2hex(key), bin2hex(value)) + end) + end + + def bin2hex(data) when is_list(data) do + Enum.map(data, &bin2hex/1) + end + + def bin2hex(data) when is_binary(data) do + if String.printable?(data) do + data + else + Base.encode16(data) + end + end + + def bin2hex(data), do: data + + def hex2bin(params, opts \\ []) + + def hex2bin(params, opts) when is_map(params) do + keys_to_base_decode = Keyword.get(opts, :keys_to_base_decode, []) + + params + |> Map.keys() + |> Enum.reduce(params, fn key, acc -> + if key in keys_to_base_decode and is_binary(Map.get(acc, key)) do + Map.update!(acc, key, &Base.decode16!(&1, case: :mixed)) + else + Map.update!(acc, key, &hex2bin(&1, opts)) + end + end) + end + + def hex2bin(params, opts) when is_list(params), do: Enum.map(params, &hex2bin(&1, opts)) + + def hex2bin(params, _opts), do: params end diff --git a/lib/archethic/utils/regression/api.ex b/lib/archethic/utils/regression/api.ex index b0bfdc8727..867ce64152 100644 --- a/lib/archethic/utils/regression/api.ex +++ b/lib/archethic/utils/regression/api.ex @@ -184,6 +184,7 @@ defmodule Archethic.Utils.Regression.Api do tx = %Transaction{ + version: Keyword.get(opts, :version, Transaction.version()), address: Crypto.derive_address(next_public_key), type: tx_type, data: transaction_data, diff --git a/lib/archethic/utils/regression/playbooks/smart_contract.ex b/lib/archethic/utils/regression/playbooks/smart_contract.ex index 8555b50099..6677261b72 100644 --- a/lib/archethic/utils/regression/playbooks/smart_contract.ex +++ b/lib/archethic/utils/regression/playbooks/smart_contract.ex @@ -80,13 +80,11 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract do ] } + # Code is supported until version 3 + opts = if data.code != "", do: [version: 3], else: [] + {:ok, address} = - Api.send_transaction_with_await_replication( - seed, - :contract, - data, - endpoint - ) + Api.send_transaction_with_await_replication(seed, :contract, data, endpoint, opts) Logger.debug("DEPLOY: Deployed at #{Base.encode16(address)}") @@ -117,6 +115,12 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract do nil end + # Recipient with list is supported until version 3 + opts = + if opts |> Keyword.get(:recipients, []) |> Enum.any?(&is_list(&1.args)), + do: Keyword.update(opts, :version, 3, & &1), + else: opts + res = Api.send_transaction_with_await_replication( trigger_seed, diff --git a/lib/archethic/utils/regression/playbooks/smart_contract/counter.ex b/lib/archethic/utils/regression/playbooks/smart_contract/counter.ex index 81d33ebdb9..df136f1593 100644 --- a/lib/archethic/utils/regression/playbooks/smart_contract/counter.ex +++ b/lib/archethic/utils/regression/playbooks/smart_contract/counter.ex @@ -43,7 +43,8 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.Counter do Enum.map(1..nb_transactions, fn i -> Task.async(fn -> SmartContract.trigger(Enum.at(triggers_seeds, i - 1), contract_address, endpoint, - await_timeout: 60_000 + await_timeout: 60_000, + version: 3 ) end) end) diff --git a/lib/archethic/utils/regression/playbooks/smart_contract/deterministic_balance.ex b/lib/archethic/utils/regression/playbooks/smart_contract/deterministic_balance.ex index c5c784f9b4..3fdd6c75e5 100644 --- a/lib/archethic/utils/regression/playbooks/smart_contract/deterministic_balance.ex +++ b/lib/archethic/utils/regression/playbooks/smart_contract/deterministic_balance.ex @@ -55,7 +55,8 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.DeterministicBalance fn seed -> SmartContract.trigger(seed, contract_address, endpoint, await_timeout: 60_000, - ledger: ledger + ledger: ledger, + version: 3 ) end, max_concurrency: length(triggers_seeds), diff --git a/lib/archethic/utils/regression/playbooks/smart_contract/dex.ex b/lib/archethic/utils/regression/playbooks/smart_contract/dex.ex index a6b09d0052..3c53377503 100644 --- a/lib/archethic/utils/regression/playbooks/smart_contract/dex.ex +++ b/lib/archethic/utils/regression/playbooks/smart_contract/dex.ex @@ -361,7 +361,7 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.Dex do end defp trigger(seed, contract_address, opts, endpoint) do - opts = opts |> Keyword.merge(await_timeout: 60_000) + opts = opts |> Keyword.merge(await_timeout: 60_000) |> Keyword.put(:version, 3) SmartContract.trigger(seed, contract_address, endpoint, opts) end @@ -402,7 +402,7 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.Dex do lp_token_to_mint = get_lp_token_to_mint(token1_amount, token2_amount) - # Handle invalid values and refund user + # Handle invalid values and refund user valid_amounts? = final_amounts.token1 > 0 && final_amounts.token2 > 0 valid_liquidity? = lp_token_to_mint > 0 diff --git a/lib/archethic/utils/regression/playbooks/smart_contract/legacy.ex b/lib/archethic/utils/regression/playbooks/smart_contract/legacy.ex index 1f81aea0f0..fee1833b8a 100644 --- a/lib/archethic/utils/regression/playbooks/smart_contract/legacy.ex +++ b/lib/archethic/utils/regression/playbooks/smart_contract/legacy.ex @@ -46,7 +46,10 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.Legacy do balance = Api.get_uco_balance(recipient_address, endpoint) - SmartContract.trigger(trigger_seed, contract_address, endpoint, content: "CLOSE_CONTRACT") + SmartContract.trigger(trigger_seed, contract_address, endpoint, + content: "CLOSE_CONTRACT", + version: 3 + ) # there's a slight change there will be 1 more tick due to playbook code if balance in [ticks_count * amount_to_send, (1 + ticks_count) * amount_to_send] do diff --git a/lib/archethic/utils/regression/playbooks/smart_contract/throw.ex b/lib/archethic/utils/regression/playbooks/smart_contract/throw.ex index 926b729a08..6d9207a92e 100644 --- a/lib/archethic/utils/regression/playbooks/smart_contract/throw.ex +++ b/lib/archethic/utils/regression/playbooks/smart_contract/throw.ex @@ -35,7 +35,8 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.Throw do recipients: [ %Recipient{address: contract_address, action: "action", args: ["Hello"]} ], - wait: true + wait: true, + version: 3 ) do {:ok, _} -> last_tx = Api.get_last_transaction(contract_address, endpoint) @@ -61,7 +62,8 @@ defmodule Archethic.Utils.Regression.Playbook.SmartContract.Throw do case SmartContract.trigger(trigger_seed, contract_address, endpoint, recipients: [ %Recipient{address: contract_address, action: "action", args: ["Invalid"]} - ] + ], + version: 3 ) do {:ok, _} -> Logger.error( diff --git a/lib/archethic_web/api/ecto_schemas/function_call_payload.ex b/lib/archethic_web/api/ecto_schemas/function_call_payload.ex index ee132870c9..b79978e2b9 100644 --- a/lib/archethic_web/api/ecto_schemas/function_call_payload.ex +++ b/lib/archethic_web/api/ecto_schemas/function_call_payload.ex @@ -1,6 +1,7 @@ defmodule ArchethicWeb.API.FunctionCallPayload do @moduledoc false alias ArchethicWeb.API.Types.Address + alias ArchethicWeb.API.Types.RecipientArgType use Ecto.Schema import Ecto.Changeset @@ -8,7 +9,7 @@ defmodule ArchethicWeb.API.FunctionCallPayload do embedded_schema do field(:contract, Address) field(:function, :string) - field(:args, {:array, :any}) + field(:args, RecipientArgType) field(:resolve_last, :boolean) end diff --git a/lib/archethic_web/api/ecto_schemas/transaction_payload.ex b/lib/archethic_web/api/ecto_schemas/transaction_payload.ex index 749d39bc84..73d3da2c1c 100644 --- a/lib/archethic_web/api/ecto_schemas/transaction_payload.ex +++ b/lib/archethic_web/api/ecto_schemas/transaction_payload.ex @@ -88,32 +88,46 @@ defmodule ArchethicWeb.API.TransactionPayload do defp format_change(_, value), do: value defp validate_data(changeset = %Ecto.Changeset{}, params) do - validate_change(changeset, :data, fn _, data_changeset -> - case data_changeset.valid? do - true -> validate_recipient_format(changeset, params) - false -> data_changeset.errors + validate_change( + changeset, + :data, + fn + _, %Ecto.Changeset{valid?: false, errors: errors} -> errors + _, _ -> validate_specific_rules(changeset, params) end - end) + ) end - defp validate_recipient_format(changeset, params) do + defp validate_specific_rules(changeset, params) do version = get_field(changeset, :version) - recipients = get_in(params, ["data", "recipients"]) - if not is_nil(version) and not is_nil(recipients) do - case version do - 1 -> - if Enum.any?(recipients, &is_map/1), - do: [recipents: "Transaction V1 cannot use named action recipients"], - else: [] + [] |> validate_recipient_format(version, params) |> validate_contract_data(version, params) + end + + defp validate_recipient_format(errors, version, %{"data" => %{"recipients" => recipients}}) + when is_list(recipients) and is_integer(version) do + cond do + version == 1 and Enum.any?(recipients, &is_map/1) -> + Keyword.put(errors, :recipents, "Transaction V1 cannot use named action recipients") - _ -> - if Enum.any?(recipients, &is_binary/1), - do: [recipents: "From V2, transaction must use named action recipients"], - else: [] - end - else - [] + version >= 2 and Enum.any?(recipients, &is_binary/1) -> + Keyword.put(errors, :recipents, "From V2, transaction must use named action recipients") + + version >= 4 and Enum.any?(recipients, &is_list(Map.get(&1, "args"))) -> + Keyword.put(errors, :recipents, "From V4, recipient arguments must be a map") + + true -> + errors end end + + defp validate_recipient_format(errors, _, _), do: errors + + defp validate_contract_data(errors, version, params) do + code = get_in(params, ["data", "code"]) + + if is_integer(version) and version >= 4 and is_binary(code) and code != "", + do: Keyword.put(errors, :code, "From v4, code is deprecated"), + else: errors + end end diff --git a/lib/archethic_web/api/ecto_schemas/types/recipient_arg_type.ex b/lib/archethic_web/api/ecto_schemas/types/recipient_arg_type.ex new file mode 100644 index 0000000000..b5932f33eb --- /dev/null +++ b/lib/archethic_web/api/ecto_schemas/types/recipient_arg_type.ex @@ -0,0 +1,32 @@ +defmodule ArchethicWeb.API.Types.RecipientArgType do + @moduledoc false + + use Ecto.Type + + def type, do: :any + + # Handle casting from external input (e.g., from forms or APIs) + def cast(value) when is_map(value) do + {:ok, value} + end + + def cast(value) when is_list(value) do + {:ok, value} + end + + def cast(_), do: :error + + def load(data) do + {:ok, data} + end + + def dump(data) when is_map(data) do + {:ok, data} + end + + def dump(data) when is_list(data) do + {:ok, data} + end + + def dump(_), do: :error +end diff --git a/lib/archethic_web/api/ecto_schemas/types/recipient_list.ex b/lib/archethic_web/api/ecto_schemas/types/recipient_list.ex index 3e70ab3ed0..ffc3b95954 100644 --- a/lib/archethic_web/api/ecto_schemas/types/recipient_list.ex +++ b/lib/archethic_web/api/ecto_schemas/types/recipient_list.ex @@ -66,6 +66,10 @@ defmodule ArchethicWeb.API.Types.RecipientList do defp valid_action_and_args?(_action = nil, _args = nil), do: true defp valid_action_and_args?(_action = "", _args), do: false - defp valid_action_and_args?(action, args) when is_binary(action) and is_list(args), do: true + + defp valid_action_and_args?(action, args) + when is_binary(action) and (is_list(args) or is_map(args)), + do: true + defp valid_action_and_args?(_, _), do: false end diff --git a/lib/archethic_web/api/graphql/schema.ex b/lib/archethic_web/api/graphql/schema.ex index f5dea43ffa..b0ef067860 100644 --- a/lib/archethic_web/api/graphql/schema.ex +++ b/lib/archethic_web/api/graphql/schema.ex @@ -9,6 +9,7 @@ defmodule ArchethicWeb.API.GraphQL.Schema do alias __MODULE__.P2PType alias __MODULE__.Resolver alias __MODULE__.SharedSecretsType + alias __MODULE__.JsonType alias __MODULE__.TransactionType alias __MODULE__.IntegerType alias __MODULE__.AddressType @@ -37,6 +38,7 @@ defmodule ArchethicWeb.API.GraphQL.Schema do import_types(OracleData) import_types(Version) import_types(BeaconChainSummary) + import_types(JsonType) query do @desc """ diff --git a/lib/archethic_web/api/graphql/schema/json_type.ex b/lib/archethic_web/api/graphql/schema/json_type.ex new file mode 100644 index 0000000000..ee58ba445b --- /dev/null +++ b/lib/archethic_web/api/graphql/schema/json_type.ex @@ -0,0 +1,34 @@ +defmodule ArchethicWeb.API.GraphQL.Schema.JsonType do + @moduledoc """ + The Json scalar type allows arbitrary JSON values to be passed in and out. + """ + use Absinthe.Schema.Notation + + scalar :json, name: "Json" do + description(""" + The `Json` scalar type represents arbitrary json string data, represented as UTF-8 + character sequences. The Json type is most often used to represent a free-form + human-readable json string. + """) + + serialize(&encode/1) + parse(&decode/1) + end + + @spec decode(Absinthe.Blueprint.Input.String.t()) :: {:ok, term()} | :error + @spec decode(Absinthe.Blueprint.Input.Null.t()) :: {:ok, nil} + defp decode(%Absinthe.Blueprint.Input.String{value: value}) do + case Jason.decode(value) do + {:ok, result} -> {:ok, result} + _ -> :error + end + end + + defp decode(%Absinthe.Blueprint.Input.Null{}) do + {:ok, nil} + end + + defp decode(_), do: :error + + defp encode(value), do: value +end diff --git a/lib/archethic_web/api/graphql/schema/transaction_type.ex b/lib/archethic_web/api/graphql/schema/transaction_type.ex index 3082b0912f..1ac6a0409e 100644 --- a/lib/archethic_web/api/graphql/schema/transaction_type.ex +++ b/lib/archethic_web/api/graphql/schema/transaction_type.ex @@ -56,13 +56,37 @@ defmodule ArchethicWeb.API.GraphQL.Schema.TransactionType do """ object :data do field(:ledger, :ledger) - field(:code, :string) + field(:code, :content) + field(:contract, :contract) field(:content, :content) field(:ownerships, list_of(:ownership)) field(:recipients, list_of(:address)) field(:action_recipients, list_of(:recipient)) end + @desc "[Contract] represents web assembly smart contract" + object :contract do + field(:bytecode, :hex) + field(:manifest, :contract_manifest) + end + + @desc "[ContractManifest] represents metadata of WebAssembly contract" + object :contract_manifest do + field(:abi, :contract_abi) + field(:upgrade_opts, :contract_upgrade_opts) + end + + @desc "[ContractABI] represents typing information for functions and state" + object :contract_abi do + field(:state, :json) + field(:functions, :json) + end + + @desc "[ContractUpgradeOpts] represents information about contract upgradability" + object :contract_upgrade_opts do + field(:from, :string) + end + @desc "[Ledger] represents the ledger operations to perform" object :ledger do field(:uco, :uco_ledger) diff --git a/lib/archethic_web/api/jsonrpc/error.ex b/lib/archethic_web/api/jsonrpc/error.ex index 74c5ca9490..e8faf16913 100644 --- a/lib/archethic_web/api/jsonrpc/error.ex +++ b/lib/archethic_web/api/jsonrpc/error.ex @@ -35,6 +35,7 @@ defmodule ArchethicWeb.API.JsonRPC.Error do defp get_custom_code(:transaction_exists), do: 122 # Smart Contract context + defp get_custom_code(:invalid_function_call), do: 202 defp get_custom_code(:contract_failure), do: 203 defp get_custom_code(:no_recipients), do: 204 defp get_custom_code(:invalid_transaction_constraints), do: 206 diff --git a/lib/archethic_web/api/jsonrpc/methods/call_contract_function.ex b/lib/archethic_web/api/jsonrpc/methods/call_contract_function.ex index 602ed7ad29..dc60245675 100644 --- a/lib/archethic_web/api/jsonrpc/methods/call_contract_function.ex +++ b/lib/archethic_web/api/jsonrpc/methods/call_contract_function.ex @@ -3,7 +3,9 @@ defmodule ArchethicWeb.API.JsonRPC.Method.CallContractFunction do JsonRPC method to call a public function """ + alias Archethic.Contracts.WasmContract alias Archethic.Contracts + alias Archethic.Contracts.Contract.Failure alias ArchethicWeb.API.FunctionCallPayload alias ArchethicWeb.API.JsonRPC.Method @@ -48,7 +50,12 @@ defmodule ArchethicWeb.API.JsonRPC.Method.CallContractFunction do {:ok, contract} <- Contracts.from_transaction(contract_tx), {:ok, inputs} <- get_inputs(contract_address, resolve?), {:ok, value, _logs} <- - Contracts.execute_function(contract, function_name, args, inputs) do + Contracts.execute_function( + contract, + function_name, + format_args(contract, args), + inputs + ) do {:ok, value} else {:error, reason} -> @@ -56,6 +63,9 @@ defmodule ArchethicWeb.API.JsonRPC.Method.CallContractFunction do end end + defp format_args(%WasmContract{}, []), do: %{} + defp format_args(_, args), do: args + defp get_transaction(contract_address, _resolve? = true), do: Archethic.get_last_transaction(contract_address) diff --git a/lib/archethic_web/api/jsonrpc/methods/simulate_contract_execution.ex b/lib/archethic_web/api/jsonrpc/methods/simulate_contract_execution.ex index 26a0e78bd4..9a40a5c1db 100644 --- a/lib/archethic_web/api/jsonrpc/methods/simulate_contract_execution.ex +++ b/lib/archethic_web/api/jsonrpc/methods/simulate_contract_execution.ex @@ -4,7 +4,6 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do """ alias Archethic.Contracts - alias Archethic.Contracts.Contract alias Archethic.Contracts.Contract.ActionWithoutTransaction alias Archethic.Contracts.Contract.ActionWithTransaction alias Archethic.Contracts.Contract.Failure @@ -81,7 +80,7 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do {:ok, contract} <- validate_and_parse_contract_tx(contract_tx), {:ok, genesis_address} <- Archethic.fetch_genesis_address(contract_tx.address), inputs = Archethic.get_unspent_outputs(genesis_address), - trigger <- Contract.get_trigger_for_recipient(recipient), + trigger <- Recipient.get_trigger(recipient), :ok <- validate_contract_condition( trigger, @@ -94,7 +93,7 @@ defmodule ArchethicWeb.API.JsonRPC.Method.SimulateContractExecution do {:ok, next_tx} <- validate_and_execute_trigger(trigger, contract, trigger_tx, recipient, inputs), # Here the index to sign transaction is not accurate has we are in simulation - {:ok, next_tx} <- Contract.sign_next_transaction(contract, next_tx, 0) do + {:ok, next_tx} <- Contracts.sign_next_transaction(contract, next_tx, 0) do validate_contract_condition(:inherit, contract, next_tx, nil, timestamp, inputs) end end diff --git a/lib/archethic_web/api/jsonrpc/schemas/transaction.ex b/lib/archethic_web/api/jsonrpc/schemas/transaction.ex index 50a1ed67f8..cafb37e98f 100644 --- a/lib/archethic_web/api/jsonrpc/schemas/transaction.ex +++ b/lib/archethic_web/api/jsonrpc/schemas/transaction.ex @@ -32,6 +32,7 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchema do @spec validate(params :: map()) :: :ok | {:error, map()} | :error def validate(params) when is_map(params) do with :ok <- ExJsonSchema.Validator.validate(@transaction_schema, params), + :ok <- validate_contract_version(params), :ok <- validate_code_size(params), :ok <- validate_recipients_version(params) do :ok @@ -42,55 +43,47 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchema do def validate(_), do: :error - defp validate_code_size(params) do - case get_in(params, ["data", "code"]) do - nil -> - :ok + defp validate_contract_version(%{"version" => version, "data" => %{"code" => code}}) + when code != "" and version >= 4, + do: {:error, [{"From v4, code is deprecated", "#/data/code"}]} - "" -> - :ok + defp validate_contract_version(%{"version" => version, "data" => %{"contract" => contract}}) + when contract != nil and version <= 3, + do: {:error, [{"Before V4, contract is not allowed", "#/data/contract"}]} - code -> - if TransactionData.code_size_valid?(code) do - :ok - else - {:error, - [ - {"Invalid transaction, code exceed max size.", "#/data/code"} - ]} - end - end + defp validate_contract_version(_), do: :ok + + defp validate_code_size(%{"data" => %{"code" => code}}) + when is_binary(code) and code != "" do + if TransactionData.code_size_valid?(code, false), + do: :ok, + else: {:error, [{"Invalid transaction, code exceed max size.", "#/data/code"}]} end - defp validate_recipients_version(params = %{"version" => 1}) do - case get_in(params, ["data", "recipients"]) do - nil -> - :ok + defp validate_code_size(_), do: :ok - recipients -> - if Enum.any?(recipients, &is_map/1) do - {:error, [{"Transaction V1 cannot use named action recipients", "#/data/recipients"}]} - else - :ok - end - end - end + defp validate_recipients_version(%{ + "version" => version, + "data" => %{"recipients" => recipients} + }) + when is_list(recipients) do + cond do + version == 1 and Enum.any?(recipients, &is_map/1) -> + {:error, [{"Transaction V1 cannot use named action recipients", "#/data/recipients"}]} - defp validate_recipients_version(params) do - case get_in(params, ["data", "recipients"]) do - nil -> - :ok + version >= 2 and Enum.any?(recipients, &is_binary/1) -> + {:error, [{"From V2, transaction must use named action recipients", "#/data/recipients"}]} - recipients -> - if Enum.any?(recipients, &is_binary/1) do - {:error, - [{"From V2, transaction must use named action recipients", "#/data/recipients"}]} - else - :ok - end + version >= 4 and Enum.any?(recipients, &is_list(Map.get(&1, "args"))) -> + {:error, [{"From V4, recipient arguments must be a map", "#/data/recipients"}]} + + true -> + :ok end end + defp validate_recipients_version(_), do: :ok + defp format_errors(errors), do: Enum.reduce(errors, %{}, fn {details, field}, acc -> Map.put(acc, field, details) end) @@ -101,12 +94,14 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchema do def to_transaction(params) do # Remove recipient args to not convert them to atom {original_recipients, params} = remove_recipient_args(params) + {origin_contract, params} = remove_contract(params) params |> Utils.atomize_keys(to_snake_case?: true) - |> decode_hex() + |> Utils.hex2bin(keys_to_base_decode: @keys_to_base_decode) |> format_ownerships() |> put_original_recipients_args(original_recipients) + |> put_origin_contract(origin_contract) |> Transaction.cast() end @@ -119,6 +114,7 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchema do updated_recipients = Enum.map(recipients, fn recipient = %{"args" => args} when is_list(args) -> Map.put(recipient, "args", []) + recipient = %{"args" => args} when is_map(args) -> Map.put(recipient, "args", %{}) recipient -> recipient end) @@ -143,21 +139,20 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchema do end) end - defp decode_hex(params) when is_map(params) do - params - |> Map.keys() - |> Enum.reduce(params, fn - key, acc when key in @keys_to_base_decode -> - Map.update!(acc, key, &Base.decode16!(&1, case: :mixed)) - - key, acc -> - Map.update!(acc, key, &decode_hex/1) + defp remove_contract(params) do + get_and_update_in(params, ["data", "contract"], fn + nil -> {nil, nil} + contract -> {contract, nil} end) end - defp decode_hex(params) when is_list(params), do: Enum.map(params, &decode_hex/1) + defp put_origin_contract(params, nil), do: params - defp decode_hex(params), do: params + defp put_origin_contract(params, %{"bytecode" => bytecode, "manifest" => manifest}) do + update_in(params, [:data, :contract], fn _ -> + %{bytecode: Base.decode16!(bytecode, case: :mixed), manifest: manifest} + end) + end defp format_ownerships(params) do update_in(params, [:data, :ownerships], fn diff --git a/lib/archethic_web/explorer/controllers/faucet_controller.ex b/lib/archethic_web/explorer/controllers/faucet_controller.ex index 9151c61ec2..2e79910bc5 100644 --- a/lib/archethic_web/explorer/controllers/faucet_controller.ex +++ b/lib/archethic_web/explorer/controllers/faucet_controller.ex @@ -111,7 +111,7 @@ defmodule ArchethicWeb.Explorer.FaucetController do }, @pool_seed, transaction_index, - curve + curve: curve ) tx_address = tx.address diff --git a/lib/archethic_web/explorer/live/transaction_details_live.html.heex b/lib/archethic_web/explorer/live/transaction_details_live.html.heex index ae326d6702..73688944df 100644 --- a/lib/archethic_web/explorer/live/transaction_details_live.html.heex +++ b/lib/archethic_web/explorer/live/transaction_details_live.html.heex @@ -93,33 +93,93 @@ <%!-------------------------------- CODE --------------------------------%> -
- <% code_bytes = byte_size(TransactionData.compress_code(@transaction.data.code)) %> -
-

Code (<%= format_bytes(code_bytes) %>)

- <%= if code_bytes > 0 do %> -

- - - -

- <% end %> + <%= if @transaction.version < 4 do %> +
+ <% code_bytes = byte_size(TransactionData.compress_code(@transaction.data.code)) %> +
+

Code (<%= format_bytes(code_bytes) %>)

+ <%= if code_bytes > 0 do %> +

+ + + + +

+ <% end %> +
+
+ <%= if code_bytes == 0 do %> +
+ <% else %> +
+
<%= @transaction.data.code %>
+
+ <% end %> +
-
- <%= if code_bytes == 0 do %> -
- <% else %> -
-
<%= @transaction.data.code %>
-
- <% end %> + <% else %> + <%!-------------------------------- CONTRACT --------------------------------%> +
+ <% code_bytes = + if @transaction.data.contract != nil, + do: byte_size(@transaction.data.contract.bytecode), + else: 0 %> +
+

Contract (<%= format_bytes(code_bytes) %>)

+ <%= if code_bytes > 0 do %> +

+ + + + +

+ <% end %> +
+
+ <%= if code_bytes == 0 do %> +
+ <% else %> +
+
<%= Base.encode16(@transaction.data.contract.bytecode, case: :lower) %>
+
+ <% end %> +
-
+ + <%= if @transaction.data.contract != nil do %> +
+
+

Contract manifest

+

+ + + + +

+
+
+
+
<%= Jason.encode!(@transaction.data.contract.manifest, pretty: true) %>
+
+
+
+ <% end %> + <% end %> <%!-------------------------------- CONTENT --------------------------------%>
@@ -185,7 +245,7 @@ x-show="show" class="language-json" phx-hook="CodeViewer" - > + > <%= print_state(state_utxo) %> <% end %> diff --git a/mix.exs b/mix.exs index 41c118601a..4c3bc65a7c 100644 --- a/mix.exs +++ b/mix.exs @@ -117,23 +117,28 @@ defmodule Archethic.MixProject do {:knigge, "~> 1.4"}, {:ex_json_schema, "~> 0.9", override: true}, {:pathex, "~> 2.4"}, - {:easy_ssl, "~> 1.3"}, - {:castore, "~> 1.0", override: true}, {:floki, "~> 0.33"}, {:ex_cldr, "~> 2.7"}, {:ex_cldr_numbers, "~> 2.29"}, {:git_diff, "~> 0.6.4"}, {:decimal, "~> 2.0"}, - {:plug_crypto, "~> 1.2"}, {:ex_abi, "0.6.1"}, + # Crypto + {:easy_ssl, "~> 1.3"}, + {:castore, "~> 1.0", override: true}, + {:plug_crypto, "~> 1.2"}, + {:ex_keccak, "~> 0.7.3"}, + {:ex_secp256k1, "~> 0.7.2"}, + {:bls_ex, "~> 0.1"}, + # Numbering {:nx, "~> 0.5"}, {:exla, "~> 0.5"}, - {:ex_keccak, "0.7.1"}, - {:ex_secp256k1, "~> 0.7.2"}, {:nimble_csv, "~> 1.1", only: :test, runtime: false}, - {:bls_ex, "~> 0.1"} + + # WASM + {:wasmex, "~> 0.9"} ] end diff --git a/mix.lock b/mix.lock index bd31bf2c2a..af4a5a225e 100644 --- a/mix.lock +++ b/mix.lock @@ -7,9 +7,9 @@ "benchee_html": {:hex, :benchee_html, "1.0.0", "5b4d24effebd060f466fb460ec06576e7b34a00fc26b234fe4f12c4f05c95947", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:benchee_json, "~> 1.0", [hex: :benchee_json, repo: "hexpm", optional: false]}], "hexpm", "5280af9aac432ff5ca4216d03e8a93f32209510e925b60e7f27c33796f69e699"}, "benchee_json": {:hex, :benchee_json, "1.0.0", "cc661f4454d5995c08fe10dd1f2f72f229c8f0fb1c96f6b327a8c8fc96a91fe5", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "da05d813f9123505f870344d68fb7c86a4f0f9074df7d7b7e2bb011a63ec231c"}, "blankable": {:hex, :blankable, "1.0.0", "89ab564a63c55af117e115144e3b3b57eb53ad43ba0f15553357eb283e0ed425", [:mix], [], "hexpm", "7cf11aac0e44f4eedbee0c15c1d37d94c090cb72a8d9fddf9f7aec30f9278899"}, - "bls_ex": {:hex, :bls_ex, "0.1.0", "53c0b3a28936114bb18aa62300c6ddcb5a2a5bad89587d23c3914abd1ccab1fd", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.4", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "4f37db50b065fd71cb3911fc255c2dc29879f6729efdd09e03c36f8c6de63ac5"}, + "bls_ex": {:hex, :bls_ex, "0.1.3", "fd885eb51fac96e0ffa757151f883fb1adb1ce52770d3f897bc6ad7322b5dd4e", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.4", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8bf496400cf253b65acaa7dc9d6b224bf40e03601bab27940b1fff6ca813d3e9"}, "bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"}, - "castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"}, + "castore": {:hex, :castore, "1.0.10", "43bbeeac820f16c89f79721af1b3e092399b3a1ecc8df1a472738fd853574911", [:mix], [], "hexpm", "1b0b7ea14d889d9ea21202c43a4fa015eb913021cb535e8ed91946f4b77a8848"}, "cldr_utils": {:hex, :cldr_utils, "2.21.0", "1bdbb8de3870ab4831f11f877b40cce838a03bf7da272430c232c19726d53f14", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "26f56101663f5aca4e727e0eb983b578ba5b170e2f12e8456df9995809a7a93b"}, "complex": {:hex, :complex, "0.5.0", "af2d2331ff6170b61bb738695e481b27a66780e18763e066ee2cd863d0b1dd92", [:mix], [], "hexpm", "2683bd3c184466cfb94fad74cbfddfaa94b860e27ad4ca1bffe3bff169d91ef1"}, "cors_plug": {:hex, :cors_plug, "3.0.3", "7c3ac52b39624bc616db2e937c282f3f623f25f8d550068b6710e58d04a0e330", [:mix], [{:plug, "~> 1.13", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3f2d759e8c272ed3835fab2ef11b46bddab8c1ab9528167bd463b6452edf830d"}, @@ -19,9 +19,9 @@ "credo": {:hex, :credo, "1.6.7", "323f5734350fd23a456f2688b9430e7d517afb313fbd38671b8a4449798a7854", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "41e110bfb007f7eda7f897c10bf019ceab9a0b269ce79f015d54b0dcf4fc7dd3"}, "crontab": {:hex, :crontab, "1.1.13", "3bad04f050b9f7f1c237809e42223999c150656a6b2afbbfef597d56df2144c5", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "d67441bec989640e3afb94e123f45a2bc42d76e02988c9613885dc3d01cf7085"}, "dart_sass": {:hex, :dart_sass, "0.5.1", "d45f20a8e324313689fb83287d4702352793ce8c9644bc254155d12656ade8b6", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "24f8a1c67e8b5267c51a33cbe6c0b5ebf12c2c83ace88b5ac04947d676b4ec81"}, - "decimal": {:hex, :decimal, "2.0.0", "a78296e617b0f5dd4c6caf57c714431347912ffb1d0842e998e9792b5642d697", [:mix], [], "hexpm", "34666e9c55dea81013e77d9d87370fe6cb6291d1ef32f46a1600230b1d44f577"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, + "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "digital_token": {:hex, :digital_token, "0.4.0", "2ad6894d4a40be8b2890aad286ecd5745fa473fa5699d80361a8c94428edcd1f", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a178edf61d1fee5bb3c34e14b0f4ee21809ee87cade8738f87337e59e5e66e26"}, "distillery": {:git, "https://github.com/archethic-foundation/distillery.git", "eb279d7e01f4e7cb2f77d96dfa2b8dd7c760e661", []}, "doctest_formatter": {:hex, :doctest_formatter, "0.2.1", "61d5674463ae0bf3249f58ae965c33f24e304662b03e7ebc1d46ceab584b545c", [:mix], [], "hexpm", "b2dad6d81b36800a4fd22888adcfdd97c955d9acf20d57cedc92c638125925a6"}, @@ -30,7 +30,7 @@ "easy_ssl": {:hex, :easy_ssl, "1.3.0", "472256942d9dd37652a558a789a8d1cccc27e7f46352e32667d1ca46bb9e22e5", [:mix], [], "hexpm", "ce8fcb7661442713a94853282b56cee0b90c52b983a83aa6af24686d301808e1"}, "ecto": {:hex, :ecto, "3.9.4", "3ee68e25dbe0c36f980f1ba5dd41ee0d3eb0873bccae8aeaf1a2647242bffa35", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "de5f988c142a3aa4ec18b85a4ec34a2390b65b24f02385c1144252ff6ff8ee75"}, "elixir_make": {:hex, :elixir_make, "0.7.5", "784cc00f5fa24239067cc04d449437dcc5f59353c44eb08f188b2b146568738a", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "c3d63e8d5c92fa3880d89ecd41de59473fa2e83eeb68148155e25e8b95aa2887"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, "esbuild": {:hex, :esbuild, "0.6.0", "9ba6ead054abd43cb3d7b14946a0cdd1493698ccd8e054e0e5d6286d7f0f509c", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "30f9a05d4a5bab0d3e37398f312f80864e1ee1a081ca09149d06d474318fd040"}, "ex_abi": {:hex, :ex_abi, "0.6.1", "b3dfc1f81e88c5927ac7ab18b7b743772323d151be6febc08c2a79372ce58842", [:mix], [{:ex_keccak, "~> 0.7.1", [hex: :ex_keccak, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "23f446e19b81428bb1c56de88a595b6aa45a423a7e4a97952c44bef50a66d921"}, "ex_cldr": {:hex, :ex_cldr, "2.34.1", "b4e32d9fb4f7d49211faa45e8a871afff5c5eb3c2f0763b5dd49e3c7df16a0dd", [:mix], [{:cldr_utils, "~> 2.19", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:gettext, "~> 0.19", [hex: :gettext, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: true]}], "hexpm", "bd635b9e76271baa5db67a7c224be485f31cea8f7c05d6b0daeefa9d04cf76c0"}, @@ -38,33 +38,36 @@ "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.29.0", "ce20899a734ac33cf088c57685035ca1626d5564f50e0ac4d3da24202b112d5a", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "134e5e82d3894f0e06f096124c24f4ef1f54f1874c35714d52aa7d370a25c306"}, "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"}, "ex_json_schema": {:hex, :ex_json_schema, "0.9.2", "c9a42e04e70cd70eb11a8903a22e8ec344df16edef4cb8e6ec84ed0caffc9f0f", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "4854329cb352b6c01c4c4b8dbfb3be14dc5bea19ea13e0eafade4ff22ba55224"}, - "ex_keccak": {:hex, :ex_keccak, "0.7.1", "0169f4b0c5073c5df61581d6282b12f1a1b764dcfcda4eeb1c819b5194c9ced0", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6.1", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "c18c19f66b6545b4b46b0c71c0cc0079de84e30b26365a92961e91697e8724ed"}, + "ex_keccak": {:hex, :ex_keccak, "0.7.5", "f3b733173510d48ae9a1ea1de415e694b2651f35c787e63f33b5ed0013fbfd35", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.7", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8a5e1cb7f96fff5e480ff6a121477b90c4fd8c150984086dffd98819f5d83763"}, "ex_secp256k1": {:hex, :ex_secp256k1, "0.7.2", "33398c172813b90fab9ab75c12b98d16cfab472c6dcbde832b13c45ce1c01947", [:mix], [{:rustler, ">= 0.0.0", [hex: :rustler, repo: "hexpm", optional: true]}, {:rustler_precompiled, "~> 0.6", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "f3b1bf56e6992e28b9d86e3bf741a4aca3e641052eb47d13ae4f5f4d4944bdaf"}, "exjsonpath": {:hex, :exjsonpath, "0.9.0", "87e593eb0deb53aa0688ca9f9edc9fb3456aca83c82245f83201ea04d696feba", [:mix], [], "hexpm", "8d7a8e9ba784e1f7a67c6f1074a3ac91a3a79a45969514ee5d95cea5bf749627"}, "exla": {:hex, :exla, "0.5.1", "8832aa299fe06ed9b772e004760b7c97e9d8dcbe40e9a4bfcbbe10b320b9c342", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:nx, "~> 0.5.1", [hex: :nx, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:xla, "~> 0.4.4", [hex: :xla, repo: "hexpm", optional: false]}], "hexpm", "48a990dbaf02bf5f288aa1360b5237c2f55db8bf52d4f63072f2b6a15d4e8375"}, "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "finch": {:hex, :finch, "0.19.0", "c644641491ea854fc5c1bbaef36bfc764e3f08e7185e1f084e35e0672241b76d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "fc5324ce209125d1e2fa0fcd2634601c52a787aff1cd33ee833664a5af4ea2b6"}, "floki": {:hex, :floki, "0.34.0", "002d0cc194b48794d74711731db004fafeb328fe676976f160685262d43706a8", [:mix], [], "hexpm", "9c3a9f43f40dde00332a589bd9d389b90c1f518aef500364d00636acc5ebc99c"}, "gen_state_machine": {:hex, :gen_state_machine, "3.0.0", "1e57f86a494e5c6b14137ebef26a7eb342b3b0070c7135f2d6768ed3f6b6cdff", [:mix], [], "hexpm", "0a59652574bebceb7309f6b749d2a41b45fdeda8dbb4da0791e355dd19f0ed15"}, "git_diff": {:hex, :git_diff, "0.6.4", "ec53ebf8bf83b4527d938d6433e3686b47a3e2a23135a21038f76736c16bb6e0", [:mix], [], "hexpm", "9e05563c136c91e960a306fd296156b2e8d74e294ae60961e69a36e118023a5f"}, "git_hooks": {:hex, :git_hooks, "0.7.3", "09489e94d88dfc767662e22aff2b6208bd7cf555a19dd0e1477cca4683ce0701", [:mix], [{:blankable, "~> 1.0.0", [hex: :blankable, repo: "hexpm", optional: false]}, {:recase, "~> 0.7.0", [hex: :recase, repo: "hexpm", optional: false]}], "hexpm", "d6ddedeb4d3a8602bc3f84e087a38f6150a86d9e790628ed8bc70e6d90681659"}, "gnuplot": {:hex, :gnuplot, "1.22.270", "541da1d4be2acbcb2a53105a7c2f31d91127811858b522c1a2290ed5844b9624", [:mix], [{:ex_doc, "~> 0.28", [hex: :ex_doc, repo: "hexpm", optional: false]}], "hexpm", "2870982804ac79a93eebf31c07a507a1825bc23c782907da48d653cf970a4d41"}, - "hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"}, + "hpax": {:hex, :hpax, "1.0.1", "c857057f89e8bd71d97d9042e009df2a42705d6d690d54eca84c8b29af0787b0", [:mix], [], "hexpm", "4e2d5a4f76ae1e3048f35ae7adb1641c36265510a2d4638157fbcb53dda38445"}, "inet_cidr": {:hex, :erl_cidr, "1.2.0", "9205ffb290c0de8d2b82147976602fbf5bfa6d594834e60556afaf3b82856b95", [:rebar3], [], "hexpm", "3505f5dfac7d862806c7051a3dd475363a45bccf39ca1faee8eda6a6b33cf335"}, "inet_ext": {:hex, :inet_ext, "1.0.0", "40a82557082827a2dc403ee7007bb389869f347465fb9d25d0abf0769b247c34", [:rebar3], [{:inet_cidr, "~> 1.0.2", [hex: :erl_cidr, repo: "hexpm", optional: false]}], "hexpm", "62a8aad524b798de3e1617e2ccfad212cb4cce971574f1adc260441ed99a250c"}, - "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "knigge": {:hex, :knigge, "1.4.1", "8258067fc0a1b73730c9136757b6fc8848c19cebae3a9d29212c2683a3b0fa77", [:mix], [{:bunt, "~> 0.2", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm", "55cbff4648eac4d3a9068e248d27028a66db32a51fcc227da82ca16a60947e10"}, "logger_file_backend": {:hex, :logger_file_backend, "0.0.13", "df07b14970e9ac1f57362985d76e6f24e3e1ab05c248055b7d223976881977c2", [:mix], [], "hexpm", "71a453a7e6e899ae4549fb147b1c6621f4233f8f48f58ca10a64ec67b6c50018"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, "makeup_elixir": {:hex, :makeup_elixir, "0.16.0", "f8c570a0d33f8039513fbccaf7108c5d750f47d8defd44088371191b76492b0b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "28b2cbdc13960a46ae9a8858c4bebdec3c9a6d7b4b9e7f4ed1502f8159f338e7"}, "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, "meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"}, - "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, - "mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, + "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, "mmdb2_decoder": {:hex, :mmdb2_decoder, "3.0.1", "78e3aedde88035c6873ada5ceaf41b7f15a6259ed034e0eaca72ccfa937798f0", [:mix], [], "hexpm", "316af0f388fac824782d944f54efe78e7c9691bbbdb0afd5cccdd0510adf559d"}, "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, "mox": {:hex, :mox, "1.0.2", "dc2057289ac478b35760ba74165b4b3f402f68803dd5aecd3bfd19c183815d64", [:mix], [], "hexpm", "f9864921b3aaf763c8741b5b8e6f908f44566f1e427b2630e89e9a73b981fef2"}, "nimble_csv": {:hex, :nimble_csv, "1.2.0", "4e26385d260c61eba9d4412c71cea34421f296d5353f914afe3f2e71cce97722", [:mix], [], "hexpm", "d0628117fcc2148178b034044c55359b26966c6eaa8e2ce15777be3bbc91b12a"}, + "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, + "nimble_pool": {:hex, :nimble_pool, "1.1.0", "bf9c29fbdcba3564a8b800d1eeb5a3c58f36e1e11d7b7fb2e084a643f645f06b", [:mix], [], "hexpm", "af2e4e6b34197db81f7aad230c1118eac993acc0dae6bc83bac0126d4ae0813a"}, "nx": {:hex, :nx, "0.5.1", "118134b8c97c2a8f86c87aa8434994c1cbbe139a306b89cca04e08dd46228067", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ceb8fbbe19b3c4252a7188d8b0e059fac9da0f4a4f3bb770fc665fdd0b29f0c5"}, "observer_cli": {:hex, :observer_cli, "1.7.4", "3c1bfb6d91bf68f6a3d15f46ae20da0f7740d363ee5bc041191ce8722a6c4fae", [:mix, :rebar3], [{:recon, "~> 2.5.1", [hex: :recon, repo: "hexpm", optional: false]}], "hexpm", "50de6d95d814f447458bd5d72666a74624eddb0ef98bdcee61a0153aae0865ff"}, "pathex": {:hex, :pathex, "2.5.0", "350ed75b41dd7c579843bc6052463d36d9a35362f5430ff3ad12a13c6a783ce6", [:mix], [], "hexpm", "031a2063c59eae2f697373f41814e9d9076105ab2173bd3a88fbe8789fdb434b"}, @@ -75,24 +78,28 @@ "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, "phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"}, "phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"}, - "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, + "plug": {:hex, :plug, "1.16.1", "40c74619c12f82736d2214557dedec2e9762029b2438d6d175c5074c933edc9d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a13ff6b9006b03d7e33874945b2755253841b238c34071ed85b0e86057f8cddc"}, "plug_attack": {:hex, :plug_attack, "0.4.3", "88e6c464d68b1491aa083a0347d59d58ba71a7e591a7f8e1b675e8c7792a0ba8", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9ed6fb8a6f613a36040f2875130a21187126c5625092f24bc851f7f12a8cbdc1"}, "plug_cowboy": {:hex, :plug_cowboy, "2.6.0", "d1cf12ff96a1ca4f52207c5271a6c351a4733f413803488d75b70ccf44aebec2", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "073cf20b753ce6682ed72905cd62a2d4bd9bad1bf9f7feb02a1b8e525bd94fa6"}, - "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.5", "918772575e48e81e455818229bf719d4ab4181fcbf7f85b68a35620f78d89ced", [:mix], [], "hexpm", "26549a1d6345e2172eb1c233866756ae44a9609bd33ee6f99147ab3fd87fd842"}, "ranch": {:hex, :ranch, "2.1.0", "2261f9ed9574dcfcc444106b9f6da155e6e540b2f82ba3d42b339b93673b72a3", [:make, :rebar3], [], "hexpm", "244ee3fa2a6175270d8e1fc59024fd9dbc76294a321057de8f803b1479e76916"}, "rand_compat": {:hex, :rand_compat, "0.0.3", "011646bc1f0b0c432fe101b816f25b9bbb74a085713cee1dafd2d62e9415ead3", [:rebar3], [], "hexpm", "cdf7be2b17308ec245b912c45fe55741f93b6e4f1a24ba6074f7137b0cc09bf4"}, "recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"}, "recon": {:hex, :recon, "2.5.3", "739107b9050ea683c30e96de050bc59248fd27ec147696f79a8797ff9fa17153", [:mix, :rebar3], [], "hexpm", "6c6683f46fd4a1dfd98404b9f78dcabc7fcd8826613a89dcb984727a8c3099d7"}, + "req": {:hex, :req, "0.5.8", "50d8d65279d6e343a5e46980ac2a70e97136182950833a1968b371e753f6a662", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "d7fc5898a566477e174f26887821a3c5082b243885520ee4b45555f5d53f40ef"}, "retry": {:hex, :retry, "0.17.0", "2582b6371155b6c1abdb95e5d35e82c0a3947be61ae8eb72085f1582ef47b652", [:mix], [], "hexpm", "27ab3fd96fc58c05b0a411abb6d150de8b6fe97a8327519171597498a6694024"}, - "rustler_precompiled": {:hex, :rustler_precompiled, "0.6.3", "f838d94bc35e1844973ee7266127b156fdc962e9e8b7ff666c8fb4fed7964d23", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "e18ecca3669a7454b3a2be75ae6c3ef01d550bc9a8cf5fbddcfff843b881d7c6"}, + "rustler": {:hex, :rustler, "0.35.0", "1e2e379e1150fab9982454973c74ac9899bd0377b3882166ee04127ea613b2d9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "a176bea1bb6711474f9dfad282066f2b7392e246459bf4e29dfff6d828779fdf"}, + "rustler_precompiled": {:hex, :rustler_precompiled, "0.8.2", "5f25cbe220a8fac3e7ad62e6f950fcdca5a5a5f8501835d2823e8c74bf4268d5", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:rustler, "~> 0.23", [hex: :rustler, repo: "hexpm", optional: true]}], "hexpm", "63d1bd5f8e23096d1ff851839923162096364bac8656a4a3c00d1fff8e83ee0a"}, "sizeable": {:hex, :sizeable, "1.0.2", "625fe06a5dad188b52121a140286f1a6ae1adf350a942cf419499ecd8a11ee29", [:mix], [], "hexpm", "4bab548e6dfba777b400ca50830a9e3a4128e73df77ab1582540cf5860601762"}, "sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "stream_data": {:hex, :stream_data, "0.6.0", "e87a9a79d7ec23d10ff83eb025141ef4915eeb09d4491f79e52f2562b73e5f47", [:mix], [], "hexpm", "b92b5031b650ca480ced047578f1d57ea6dd563f5b57464ad274718c9c29501c"}, - "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, + "telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"}, "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, "telemetry_metrics_prometheus_core": {:hex, :telemetry_metrics_prometheus_core, "1.1.0", "4e15f6d7dbedb3a4e3aed2262b7e1407f166fcb9c30ca3f96635dfbbef99965c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "0dd10e7fe8070095df063798f82709b0a1224c31b8baf6278b423898d591a069"}, "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, + "toml": {:hex, :toml, "0.7.0", "fbcd773caa937d0c7a02c301a1feea25612720ac3fa1ccb8bfd9d30d822911de", [:mix], [], "hexpm", "0690246a2478c1defd100b0c9b89b4ea280a22be9a7b313a8a058a2408a2fa70"}, + "wasmex": {:hex, :wasmex, "0.9.2", "4fa00ef2ee975cc443aee93a4db3aaa435155b806993717da15c1dd6bda048bc", [:mix], [{:rustler, "~> 0.35.0", [hex: :rustler, repo: "hexpm", optional: false]}, {:rustler_precompiled, "~> 0.8", [hex: :rustler_precompiled, repo: "hexpm", optional: false]}], "hexpm", "8d201669e237e6eddd40bec6c1d8b9a488024ea19df365b0ed6d77bf95fd01b4"}, "websockex": {:hex, :websockex, "0.4.3", "92b7905769c79c6480c02daacaca2ddd49de936d912976a4d3c923723b647bf0", [:mix], [], "hexpm", "95f2e7072b85a3a4cc385602d42115b73ce0b74a9121d0d6dbbf557645ac53e4"}, "xla": {:hex, :xla, "0.4.4", "c3a8ed1f579bda949df505e49ff65415c8281d991fbd6ae1d8f3c5d0fd155f54", [:make, :mix], [{:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "484f3f9011db3c9f1ff1e98eecefd382f3882a07ada540fd58803db1d2dab671"}, } diff --git a/priv/json-schemas/schemas/object/contract.json b/priv/json-schemas/schemas/object/contract.json new file mode 100644 index 0000000000..1ef77222cf --- /dev/null +++ b/priv/json-schemas/schemas/object/contract.json @@ -0,0 +1,85 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "bytecode": { + "$ref": "file://schemas/base/hexadecimal.json", + "description": "Contract's bytecode in hexadecimal", + "maxLength": 524288 + }, + "manifest": { + "type": "object", + "description": "Metadata about the smart contract", + "properties": { + "abi": { + "type": "object", + "description": "Define functions and types of the contract", + "properties": { + "state": { + "type": "object", + "description": "Define the types of the contract's state" + }, + "functions": { + "type": "object", + "description": "Define the list of public functions and triggers of the contract", + "additionalProperties": { + "type": "object", + "properties": { + "type": { + "enum": [ + "action", + "publicFunction" + ], + "description": "Transaction's type" + }, + "triggerType": { + "enum": [ + "transaction", + "oracle", + "interval", + "datetime" + ], + "description": "Define the type of the trigger of the contract action" + }, + "triggerArgument": { + "type": "string", + "description": "Define the argument for specific trigger such as interval & datetime" + }, + "input": { + "type": [ + "string", + "object" + ], + "description": "Define the types of the input(s)" + }, + "output": { + "type": [ + "string", + "object" + ], + "description": "Define the types of the output(s)" + } + }, + "required": [ + "type" + ] + } + } + }, + "required": [ + "state", + "functions" + ] + } + }, + "required": [ + "abi" + ] + } + }, + "required": [ + "bytecode", + "manifest" + ], + "additionalProperties": false +} diff --git a/priv/json-schemas/schemas/object/recipient.json b/priv/json-schemas/schemas/object/recipient.json index 9d94886f53..117e9d03b2 100644 --- a/priv/json-schemas/schemas/object/recipient.json +++ b/priv/json-schemas/schemas/object/recipient.json @@ -35,7 +35,7 @@ "else": { "properties": { "args": { - "type": "array", + "type": ["array", "object"], "items": { "type": [ "array", diff --git a/priv/json-schemas/schemas/object/transaction_data.json b/priv/json-schemas/schemas/object/transaction_data.json index 60a21c4c32..86538463b1 100644 --- a/priv/json-schemas/schemas/object/transaction_data.json +++ b/priv/json-schemas/schemas/object/transaction_data.json @@ -4,7 +4,11 @@ "properties": { "code": { "type": "string", - "description": "Transaction's smart contract code" + "description": "Transaction's smart contract code (deprecated)" + }, + "contract": { + "$ref": "file://schemas/object/contract.json", + "description": "Transaction's contract definition for WebAssembly" }, "content": { "type": "string", diff --git a/priv/json-schemas/transaction.json b/priv/json-schemas/transaction.json index dbecee618c..188bb4611f 100644 --- a/priv/json-schemas/transaction.json +++ b/priv/json-schemas/transaction.json @@ -5,7 +5,7 @@ "version": { "type": "integer", "minimum": 1, - "maximum": 3, + "maximum": 4, "description": "Transaction's version" }, "address": { diff --git a/test/archethic/contracts/contract/context_test.exs b/test/archethic/contracts/contract/context_test.exs index 9792fb1e00..db8211d090 100644 --- a/test/archethic/contracts/contract/context_test.exs +++ b/test/archethic/contracts/contract/context_test.exs @@ -5,6 +5,7 @@ defmodule Archethic.Contracts.Contract.ContextTest do alias Archethic.Contracts.Contract.Context alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.TransactionData.VersionedRecipient alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.UnspentOutput @@ -52,13 +53,13 @@ defmodule Archethic.Contracts.Contract.ContextTest do test "trigger=transaction" do now = DateTime.utc_now() + recipient = + %Recipient{address: random_address()} + |> VersionedRecipient.wrap_recipient(current_transaction_version()) + ctx = %Context{ status: :tx_output, - trigger: - {:transaction, random_address(), - %Recipient{ - address: random_address() - }}, + trigger: {:transaction, random_address(), recipient}, timestamp: now |> DateTime.truncate(:millisecond) } @@ -68,15 +69,17 @@ defmodule Archethic.Contracts.Contract.ContextTest do test "trigger=transaction (named action)" do now = DateTime.utc_now() + recipient = + %Recipient{ + address: random_address(), + action: "add", + args: %{"a" => 1, "b" => 2, "c" => 3, "d" => 4, "e" => 5} + } + |> VersionedRecipient.wrap_recipient(current_transaction_version()) + ctx = %Context{ status: :tx_output, - trigger: - {:transaction, random_address(), - %Recipient{ - address: random_address(), - action: "add", - args: [1, 2, 3, 4, 5] - }}, + trigger: {:transaction, random_address(), recipient}, timestamp: now |> DateTime.truncate(:millisecond) } diff --git a/test/archethic/contracts/interpreter/action_interpreter_test.exs b/test/archethic/contracts/interpreter/action_interpreter_test.exs index f2fc5e3d25..537cbc0968 100644 --- a/test/archethic/contracts/interpreter/action_interpreter_test.exs +++ b/test/archethic/contracts/interpreter/action_interpreter_test.exs @@ -4,8 +4,8 @@ defmodule Archethic.Contracts.Interpreter.ActionInterpreterTest do import ArchethicCase - alias Archethic.Contracts.Constants alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Constants alias Archethic.Contracts.Interpreter.ActionInterpreter alias Archethic.Contracts.Interpreter.FunctionKeys diff --git a/test/archethic/contracts/interpreter/condition_interpreter_test.exs b/test/archethic/contracts/interpreter/condition_interpreter_test.exs index 6473cdfa86..302f8ec00c 100644 --- a/test/archethic/contracts/interpreter/condition_interpreter_test.exs +++ b/test/archethic/contracts/interpreter/condition_interpreter_test.exs @@ -1,8 +1,8 @@ defmodule Archethic.Contracts.Interpreter.ConditionInterpreterTest do use ArchethicCase - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter.ConditionInterpreter alias Archethic.Contracts.Interpreter.FunctionKeys diff --git a/test/archethic/contracts/interpreter/condition_validator_test.exs b/test/archethic/contracts/interpreter/condition_validator_test.exs index f7ded65e5e..550b493d25 100644 --- a/test/archethic/contracts/interpreter/condition_validator_test.exs +++ b/test/archethic/contracts/interpreter/condition_validator_test.exs @@ -2,8 +2,8 @@ defmodule Archethic.Contracts.Interpreter.ConditionValidatorTest do use ArchethicCase alias Archethic.ContractFactory - alias Archethic.Contracts.Constants alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Constants alias Archethic.Contracts.Interpreter.ConditionInterpreter alias Archethic.Contracts.Interpreter.ConditionValidator alias Archethic.Contracts.Interpreter.Library diff --git a/test/archethic/contracts/constants_test.exs b/test/archethic/contracts/interpreter/constants_test.exs similarity index 97% rename from test/archethic/contracts/constants_test.exs rename to test/archethic/contracts/interpreter/constants_test.exs index 17f2e300ae..d5cd7d4722 100644 --- a/test/archethic/contracts/constants_test.exs +++ b/test/archethic/contracts/interpreter/constants_test.exs @@ -1,9 +1,9 @@ -defmodule Archethic.Contracts.ConstantsTest do +defmodule Archethic.Contracts.Interpreter.ConstantsTest do use ArchethicCase import ArchethicCase - alias Archethic.Contracts.Constants + alias Archethic.Contracts.Interpreter.Constants alias Archethic.Reward.MemTables.RewardTokens diff --git a/test/archethic/contracts/contract_test.exs b/test/archethic/contracts/interpreter/contract_test.exs similarity index 86% rename from test/archethic/contracts/contract_test.exs rename to test/archethic/contracts/interpreter/contract_test.exs index 42cde13988..a919084730 100644 --- a/test/archethic/contracts/contract_test.exs +++ b/test/archethic/contracts/interpreter/contract_test.exs @@ -1,34 +1,34 @@ -defmodule Archethic.Contracts.ContractTest do +defmodule Archethic.Contracts.Interpreter.ContractTest do use ArchethicCase - import ArchethicCase + # import ArchethicCase alias Archethic.ContractFactory alias Archethic.Contracts - alias Archethic.Contracts.Contract alias Archethic.Contracts.Contract.ActionWithTransaction alias Archethic.Contracts.Contract.State alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Crypto alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData alias Archethic.TransactionChain.TransactionData.Ownership - alias Archethic.TransactionChain.TransactionData.Recipient - - describe "get_trigger_for_recipient/2" do - test "should return trigger" do - assert {:transaction, "vote", 1} = - Contract.get_trigger_for_recipient(%Recipient{ - address: random_address(), - action: "vote", - args: ["Julio"] - }) - end - - test "should return {:transaction, nil, nil} when no action nor args" do - assert {:transaction, nil, nil} == - Contract.get_trigger_for_recipient(%Recipient{address: random_address()}) - end - end + # alias Archethic.TransactionChain.TransactionData.Recipient + + # describe "get_trigger_for_recipient/2" do + # test "should return trigger" do + # assert {:transaction, "vote", 1} = + # Contract.get_trigger_for_recipient(%Recipient{ + # address: random_address(), + # action: "vote", + # args: ["Julio"] + # }) + # end + + # test "should return {:transaction, nil, nil} when no action nor args" do + # assert {:transaction, nil, nil} == + # Contract.get_trigger_for_recipient(%Recipient{address: random_address()}) + # end + # end describe "from_transaction/1" do test "should return Contract with contract_tx filled" do @@ -102,7 +102,7 @@ defmodule Archethic.Contracts.ContractTest do assert {:ok, signed_tx = %Transaction{previous_public_key: ^pub, previous_signature: signature}} = - Contract.sign_next_transaction(contract, next_tx, 1) + Contracts.sign_next_transaction(contract, next_tx, 1) tx_payload = Transaction.extract_for_previous_signature(signed_tx) |> Transaction.serialize(:extended) @@ -140,7 +140,7 @@ defmodule Archethic.Contracts.ContractTest do assert %Transaction{data: %TransactionData{ownerships: []}} = next_tx assert {:ok, %Transaction{data: %TransactionData{ownerships: [new_ownership]}}} = - Contract.sign_next_transaction(contract, next_tx, 1) + Contracts.sign_next_transaction(contract, next_tx, 1) assert new_ownership != ownerships assert Ownership.authorized_public_key?(new_ownership, storage_nonce_public_key) diff --git a/test/archethic/contracts/interpreter/legacy/condition_interpreter_test.exs b/test/archethic/contracts/interpreter/legacy/condition_interpreter_test.exs index a5008cf272..e99d6c5f2a 100644 --- a/test/archethic/contracts/interpreter/legacy/condition_interpreter_test.exs +++ b/test/archethic/contracts/interpreter/legacy/condition_interpreter_test.exs @@ -1,7 +1,7 @@ defmodule Archethic.Contracts.Interpreter.Legacy.ConditionInterpreterTest do use ArchethicCase - alias Archethic.Contracts.Conditions.Subjects, as: ConditionsSubjects + alias Archethic.Contracts.Interpreter.Conditions.Subjects, as: ConditionsSubjects alias Archethic.Contracts.Interpreter.Legacy.ConditionInterpreter alias Archethic.Contracts.Interpreter diff --git a/test/archethic/contracts/interpreter/library/common/chain_test.exs b/test/archethic/contracts/interpreter/library/common/chain_test.exs index 2c0db924f4..abc81ef46a 100644 --- a/test/archethic/contracts/interpreter/library/common/chain_test.exs +++ b/test/archethic/contracts/interpreter/library/common/chain_test.exs @@ -7,7 +7,7 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.ChainTest do use ArchethicCase import ArchethicCase - alias Archethic.Contracts.Constants + alias Archethic.Contracts.Interpreter.Constants alias Archethic.Contracts.Interpreter.Library alias Archethic.Contracts.Interpreter.Library.Common.Chain diff --git a/test/archethic/contracts/interpreter/library/common/crypto_test.exs b/test/archethic/contracts/interpreter/library/common/crypto_test.exs index aa9cb2258d..3f40553681 100644 --- a/test/archethic/contracts/interpreter/library/common/crypto_test.exs +++ b/test/archethic/contracts/interpreter/library/common/crypto_test.exs @@ -6,8 +6,8 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.CryptoTest do use ArchethicCase - alias Archethic.Contracts.Contract alias Archethic.Contracts.Interpreter + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Interpreter.Library alias Archethic.Contracts.Interpreter.Library.Common.Crypto diff --git a/test/archethic/contracts/interpreter/library/common/http_impl_test.exs b/test/archethic/contracts/interpreter/library/common/http_impl_test.exs index 8d625414f4..cba3b8e07a 100644 --- a/test/archethic/contracts/interpreter/library/common/http_impl_test.exs +++ b/test/archethic/contracts/interpreter/library/common/http_impl_test.exs @@ -224,8 +224,8 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.HttpImplTest do describe "request_many/2" do test "should return a status -4001 for timeout" do assert [ - %{"status" => 200}, - %{"status" => -4001} + %{"status" => -4001}, + %{"status" => 200} ] = HttpImpl.request_many( [ @@ -234,6 +234,7 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.HttpImplTest do ], false ) + |> Enum.sort_by(fn %{"status" => status} -> status end) end test "should return a status -4004 for non https" do diff --git a/test/archethic/contracts/interpreter/library/common/time_test.exs b/test/archethic/contracts/interpreter/library/common/time_test.exs index c58917edb3..b3fd0c1270 100644 --- a/test/archethic/contracts/interpreter/library/common/time_test.exs +++ b/test/archethic/contracts/interpreter/library/common/time_test.exs @@ -3,9 +3,9 @@ defmodule Archethic.Contracts.Interpreter.Library.Common.TimeTest do alias Archethic.ContractFactory alias Archethic.Contracts - alias Archethic.Contracts.Contract alias Archethic.Contracts.Contract.ActionWithTransaction alias Archethic.Contracts.Contract.ConditionRejected + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Interpreter.Library.Common.Time alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData diff --git a/test/archethic/contracts/interpreter_test.exs b/test/archethic/contracts/interpreter_test.exs index 82fb93ce69..0edcd4d5fd 100644 --- a/test/archethic/contracts/interpreter_test.exs +++ b/test/archethic/contracts/interpreter_test.exs @@ -4,9 +4,9 @@ defmodule Archethic.Contracts.InterpreterTest do import ArchethicCase alias Archethic.ContractFactory - alias Archethic.Contracts.Conditions - alias Archethic.Contracts.Constants - alias Archethic.Contracts.Contract + alias Archethic.Contracts.Interpreter.Conditions + alias Archethic.Contracts.Interpreter.Constants + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Contract.State alias Archethic.Contracts.Interpreter alias Archethic.Contracts.Interpreter.Library @@ -576,7 +576,7 @@ defmodule Archethic.Contracts.InterpreterTest do %Contract{ conditions: %{ - {:transaction, nil, nil} => %Archethic.Contracts.Conditions{subjects: subjects} + {:transaction, nil, nil} => %Conditions{subjects: subjects} } } = Contract.from_transaction!(contract_tx) @@ -645,7 +645,7 @@ defmodule Archethic.Contracts.InterpreterTest do %Contract{ functions: functions, conditions: %{ - {:transaction, nil, nil} => %Archethic.Contracts.Conditions{subjects: subjects} + {:transaction, nil, nil} => %Conditions{subjects: subjects} } } = Contract.from_transaction!(contract_tx) @@ -1093,9 +1093,13 @@ defmodule Archethic.Contracts.InterpreterTest do recipients = [recipient, %Recipient{address: random_address()}] trigger_tx = - TransactionFactory.create_valid_transaction([], type: :data, recipients: recipients) + TransactionFactory.create_valid_transaction([], + type: :data, + recipients: recipients, + version: 3 + ) - trigger_key = Contract.get_trigger_for_recipient(recipient) + trigger_key = Recipient.get_trigger(recipient) assert {:ok, %Transaction{data: %TransactionData{content: "Dr. Who?"}}, _state, _logs} = Interpreter.execute_trigger( @@ -1159,9 +1163,13 @@ defmodule Archethic.Contracts.InterpreterTest do recipient = %Recipient{address: contract_tx.address, action: "add", args: [1, 2]} trigger_tx = - TransactionFactory.create_valid_transaction([], type: :data, recipients: [recipient]) + TransactionFactory.create_valid_transaction([], + type: :data, + recipients: [recipient], + version: 3 + ) - trigger_key = Contract.get_trigger_for_recipient(recipient) + trigger_key = Recipient.get_trigger(recipient) assert {:ok, %Transaction{data: %TransactionData{content: "3"}}, _state, _logs} = Interpreter.execute_trigger( @@ -1187,9 +1195,13 @@ defmodule Archethic.Contracts.InterpreterTest do recipient = %Recipient{address: contract_tx.address, action: "add", args: [1, 2]} trigger_tx = - TransactionFactory.create_valid_transaction([], type: :data, recipients: [recipient]) + TransactionFactory.create_valid_transaction([], + type: :data, + recipients: [recipient], + version: 3 + ) - trigger_key = Contract.get_trigger_for_recipient(recipient) + trigger_key = Recipient.get_trigger(recipient) assert {:ok, %Transaction{data: %TransactionData{content: "3"}}, _state, _logs} = Interpreter.execute_trigger( diff --git a/test/archethic/contracts/loader_test.exs b/test/archethic/contracts/loader_test.exs index f5aa3380be..65452ab291 100644 --- a/test/archethic/contracts/loader_test.exs +++ b/test/archethic/contracts/loader_test.exs @@ -5,9 +5,10 @@ defmodule Archethic.Contracts.LoaderTest do alias Archethic.ContractRegistry alias Archethic.ContractSupervisor - alias Archethic.Contracts.Contract + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Loader alias Archethic.Contracts.Worker + alias Archethic.Contracts.Contract.Context alias Archethic.Crypto @@ -143,7 +144,8 @@ defmodule Archethic.Contracts.LoaderTest do seed: random_seed(), recipients: [ %Recipient{address: contract_genesis, action: "test", args: []} - ] + ], + version: 3 ) trigger_genesis = Transaction.previous_address(trigger_tx) @@ -333,7 +335,7 @@ defmodule Archethic.Contracts.LoaderTest do %UnspentOutput{from: trigger_tx1.address, type: :call, timestamp: DateTime.utc_now()} ] - contract_context = %Contract.Context{ + contract_context = %Context{ trigger: {:transaction, trigger_tx1.address, recipient}, timestamp: DateTime.utc_now(), status: :tx_output, diff --git a/test/archethic/contracts/worker_test.exs b/test/archethic/contracts/worker_test.exs index 85e653f367..0ad7d8ff26 100644 --- a/test/archethic/contracts/worker_test.exs +++ b/test/archethic/contracts/worker_test.exs @@ -8,10 +8,12 @@ defmodule Archethic.Contracts.WorkerTest do alias Archethic.PubSub alias Archethic.ContractSupervisor - alias Archethic.Contracts.Contract + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Worker alias Archethic.P2P.Message.StartMining + alias Archethic.P2P.Message.GetUnspentOutputs + alias Archethic.P2P.Message.UnspentOutputList alias Archethic.TransactionChain.Transaction alias Archethic.TransactionChain.TransactionData @@ -57,6 +59,11 @@ defmodule Archethic.Contracts.WorkerTest do MockDB |> stub(:chain_size, fn _ -> 1 end) + MockClient + |> stub(:send_message, fn _, %GetUnspentOutputs{}, _ -> + {:ok, %UnspentOutputList{unspent_outputs: []}} + end) + transaction_seed = :crypto.strong_rand_bytes(32) {first_pub, _} = Crypto.derive_keypair(transaction_seed, 1) @@ -219,7 +226,8 @@ defmodule Archethic.Contracts.WorkerTest do seed: random_seed(), recipients: [ %Recipient{address: contract_genesis, action: "test", args: []} - ] + ], + version: 3 ) trigger_genesis = Transaction.previous_address(trigger_tx) @@ -266,7 +274,8 @@ defmodule Archethic.Contracts.WorkerTest do seed: random_seed(), recipients: [ %Recipient{address: contract_genesis, action: "test", args: []} - ] + ], + version: 3 ) trigger_genesis = Transaction.previous_address(trigger_tx) @@ -322,7 +331,8 @@ defmodule Archethic.Contracts.WorkerTest do seed: random_seed(), recipients: [ %Recipient{address: contract_genesis, action: "test", args: []} - ] + ], + version: 3 ) trigger_genesis = Transaction.previous_address(trigger_tx) @@ -386,7 +396,8 @@ defmodule Archethic.Contracts.WorkerTest do seed: random_seed(), recipients: [ %Recipient{address: contract_genesis, action: "test", args: []} - ] + ], + version: 3 ) trigger_genesis = Transaction.previous_address(trigger_tx) @@ -439,7 +450,10 @@ defmodule Archethic.Contracts.WorkerTest do trigger_tx = %Transaction{address: trigger_address} = - TransactionFactory.create_valid_transaction([], recipients: [%Recipient{address: genesis}]) + TransactionFactory.create_valid_transaction([], + recipients: [%Recipient{address: genesis}], + version: 3 + ) MockDB |> stub(:get_last_chain_address, fn address -> {address, DateTime.utc_now()} end) @@ -488,7 +502,10 @@ defmodule Archethic.Contracts.WorkerTest do trigger_tx = %Transaction{address: trigger_address} = - TransactionFactory.create_valid_transaction([], recipients: [%Recipient{address: genesis}]) + TransactionFactory.create_valid_transaction([], + recipients: [%Recipient{address: genesis}], + version: 3 + ) MockDB |> stub(:get_last_chain_address, fn address -> {address, DateTime.utc_now()} end) @@ -553,7 +570,8 @@ defmodule Archethic.Contracts.WorkerTest do %Transaction{address: trigger_address} = TransactionFactory.create_valid_transaction([], recipients: [%Recipient{address: genesis}], - content: "Mr.X" + content: "Mr.X", + version: 3 ) MockDB @@ -680,7 +698,11 @@ defmodule Archethic.Contracts.WorkerTest do trigger_tx = %Transaction{address: trigger_tx_address} = - TransactionFactory.create_valid_transaction([], ledger: ledger, recipients: [recipient]) + TransactionFactory.create_valid_transaction([], + ledger: ledger, + recipients: [recipient], + version: 3 + ) MockDB |> stub(:get_last_chain_address, fn address -> {address, DateTime.utc_now()} end) @@ -736,7 +758,11 @@ defmodule Archethic.Contracts.WorkerTest do trigger_tx = %Transaction{address: trigger_tx_address} = - TransactionFactory.create_valid_transaction([], type: :data, recipients: [recipient]) + TransactionFactory.create_valid_transaction([], + type: :data, + recipients: [recipient], + version: 3 + ) MockDB |> stub(:get_last_chain_address, fn address -> {address, DateTime.utc_now()} end) @@ -785,7 +811,11 @@ defmodule Archethic.Contracts.WorkerTest do trigger_tx = %Transaction{address: trigger_tx_address} = - TransactionFactory.create_valid_transaction([], ledger: ledger, recipients: [recipient]) + TransactionFactory.create_valid_transaction([], + ledger: ledger, + recipients: [recipient], + version: 3 + ) MockDB |> stub(:get_last_chain_address, fn address -> {address, DateTime.utc_now()} end) @@ -846,7 +876,8 @@ defmodule Archethic.Contracts.WorkerTest do TransactionFactory.create_valid_transaction([], content: "0", recipients: [recipient], - seed: random_seed() + seed: random_seed(), + version: 3 ) valid_trigger_tx = @@ -854,7 +885,8 @@ defmodule Archethic.Contracts.WorkerTest do TransactionFactory.create_valid_transaction([], content: "2", recipients: [recipient], - seed: random_seed() + seed: random_seed(), + version: 3 ) MockDB diff --git a/test/archethic/contracts_test.exs b/test/archethic/contracts_test.exs index 6c128a552d..a323c278eb 100644 --- a/test/archethic/contracts_test.exs +++ b/test/archethic/contracts_test.exs @@ -2,7 +2,7 @@ defmodule Archethic.ContractsTest do use ArchethicCase alias Archethic.Contracts - alias Archethic.Contracts.Contract + alias Archethic.Contracts.Interpreter.Contract alias Archethic.Contracts.Contract.ActionWithTransaction alias Archethic.Contracts.Contract.ActionWithoutTransaction alias Archethic.Contracts.Contract.ConditionRejected @@ -542,7 +542,7 @@ defmodule Archethic.ContractsTest do trigger_tx = TransactionFactory.create_valid_transaction() recipient = %Recipient{address: contract_tx.address, action: "vote", args: ["Juliette"]} - condition_key = Contract.get_trigger_for_recipient(recipient) + condition_key = Recipient.get_trigger(recipient) assert {:ok, _} = Contracts.execute_condition( @@ -576,7 +576,7 @@ defmodule Archethic.ContractsTest do ) recipient = %Recipient{address: contract_tx.address, action: "vote", args: ["Jules"]} - condition_key = Contract.get_trigger_for_recipient(recipient) + condition_key = Recipient.get_trigger(recipient) assert {:ok, _} = Contracts.execute_condition( @@ -610,7 +610,7 @@ defmodule Archethic.ContractsTest do ) recipient = %Recipient{address: contract_tx.address, action: "vote", args: ["Jules"]} - condition_key = Contract.get_trigger_for_recipient(recipient) + condition_key = Recipient.get_trigger(recipient) assert {:error, %ConditionRejected{}} = Contracts.execute_condition( diff --git a/test/archethic/mining/pending_transaction_validation_test.exs b/test/archethic/mining/pending_transaction_validation_test.exs index 84ca849a01..a9e1fe1cd4 100644 --- a/test/archethic/mining/pending_transaction_validation_test.exs +++ b/test/archethic/mining/pending_transaction_validation_test.exs @@ -151,7 +151,7 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do tx = TransactionFactory.create_non_valided_transaction( type: :data, - content: :crypto.strong_rand_bytes(3_145_711) + content: :crypto.strong_rand_bytes(3_145_700) ) assert :ok = PendingTransactionValidation.validate_size(tx) @@ -365,8 +365,8 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do end describe "Contract" do - test "should return error when code is empty" do - assert {:error, "Invalid contract type transaction - code is empty"} = + test "should return error when code or contract is empty" do + assert {:error, "Invalid contract type transaction - contract's code is empty"} = ContractFactory.create_valid_contract_tx("") |> PendingTransactionValidation.validate_type_rules(DateTime.utc_now()) end @@ -1531,7 +1531,7 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do type: :keychain_access, content: "", ownerships: ownerships, - recipients: [%Recipient{address: random_address(), action: "do_something", args: []}] + recipients: [%Recipient{address: random_address(), action: "do_something", args: %{}}] ) assert {:error, "Invalid Keychain Access transaction"} = diff --git a/test/archethic/mining/smart_contract_validation_test.exs b/test/archethic/mining/smart_contract_validation_test.exs index f5440b7e96..09f902eba3 100644 --- a/test/archethic/mining/smart_contract_validation_test.exs +++ b/test/archethic/mining/smart_contract_validation_test.exs @@ -18,6 +18,7 @@ defmodule Archethic.Mining.SmartContractValidationTest do alias Archethic.TransactionChain.Transaction.ValidationStamp.LedgerOperations.VersionedUnspentOutput alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.TransactionData.VersionedRecipient alias Archethic.TransactionFactory @@ -660,7 +661,7 @@ defmodule Archethic.Mining.SmartContractValidationTest do trigger_tx = %Transaction{address: trigger_address} = - TransactionFactory.create_valid_transaction([], recipients: [recipient]) + TransactionFactory.create_valid_transaction([], recipients: [recipient], version: 3) unspent_outputs = [%UnspentOutput{from: trigger_address, type: :call}] @@ -678,8 +679,10 @@ defmodule Archethic.Mining.SmartContractValidationTest do {:ok, trigger_tx} end) + v_recipient = VersionedRecipient.wrap_recipient(recipient, 3) + contract_context = %Contract.Context{ - trigger: {:transaction, trigger_address, recipient}, + trigger: {:transaction, trigger_address, v_recipient}, status: :tx_output, timestamp: trigger_tx.validation_stamp.timestamp, inputs: [] @@ -711,7 +714,7 @@ defmodule Archethic.Mining.SmartContractValidationTest do trigger_tx = %Transaction{address: trigger_address} = - TransactionFactory.create_valid_transaction([], recipients: [recipient]) + TransactionFactory.create_valid_transaction([], recipients: [recipient], version: 3) next_contract_tx = ContractFactory.create_next_contract_tx(prev_contract_tx, content: "content") @@ -721,8 +724,10 @@ defmodule Archethic.Mining.SmartContractValidationTest do {:ok, trigger_tx} end) + v_recipient = VersionedRecipient.wrap_recipient(recipient, 3) + contract_context = %Contract.Context{ - trigger: {:transaction, trigger_address, recipient}, + trigger: {:transaction, trigger_address, v_recipient}, status: :tx_output, timestamp: trigger_tx.validation_stamp.timestamp } @@ -753,7 +758,7 @@ defmodule Archethic.Mining.SmartContractValidationTest do trigger_tx = %Transaction{address: trigger_address} = - TransactionFactory.create_valid_transaction([], recipients: [recipient]) + TransactionFactory.create_valid_transaction([], recipients: [recipient], version: 3) unspent_outputs = [%UnspentOutput{from: trigger_address, type: :call}] @@ -771,8 +776,11 @@ defmodule Archethic.Mining.SmartContractValidationTest do {:ok, trigger_tx} end) + v_recipient = + %Recipient{recipient | action: "otter"} |> VersionedRecipient.wrap_recipient(3) + contract_context = %Contract.Context{ - trigger: {:transaction, trigger_address, %Recipient{recipient | action: "otter"}}, + trigger: {:transaction, trigger_address, v_recipient}, status: :tx_output, timestamp: trigger_tx.validation_stamp.timestamp, inputs: v_unspent_outputs diff --git a/test/archethic/p2p/message/validate_smart_contract_call_test.exs b/test/archethic/p2p/message/validate_smart_contract_call_test.exs index ea33fb81d2..305cbb73e7 100644 --- a/test/archethic/p2p/message/validate_smart_contract_call_test.exs +++ b/test/archethic/p2p/message/validate_smart_contract_call_test.exs @@ -50,7 +50,7 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCallTest do test "should work with named action" do msg = %ValidateSmartContractCall{ - recipient: %Recipient{address: random_address(), action: "do_it", args: []}, + recipient: %Recipient{address: random_address(), action: "do_it", args: %{}}, transaction: Archethic.TransactionFactory.create_valid_transaction(), timestamp: DateTime.utc_now() |> DateTime.truncate(:millisecond) } @@ -207,7 +207,7 @@ defmodule Archethic.P2P.Message.ValidateSmartContractCallTest do recipient: %Recipient{ address: "@SC1_for_contract_with_named_action_and_valid_message", action: "upgrade", - args: [] + args: %{} }, transaction: incoming_tx, timestamp: DateTime.utc_now() diff --git a/test/archethic/replication/transaction_validator_test.exs b/test/archethic/replication/transaction_validator_test.exs index 92788327d9..7f3015ec90 100644 --- a/test/archethic/replication/transaction_validator_test.exs +++ b/test/archethic/replication/transaction_validator_test.exs @@ -26,6 +26,7 @@ defmodule Archethic.Replication.TransactionValidatorTest do alias Archethic.ContractFactory alias Archethic.TransactionChain.TransactionData.Recipient + alias Archethic.TransactionChain.TransactionData.VersionedRecipient alias Archethic.TransactionFactory import ArchethicCase @@ -309,7 +310,7 @@ defmodule Archethic.Replication.TransactionValidatorTest do contract_genesis = contract_seed |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address() - recipient = %Recipient{action: "test", args: [], address: contract_genesis} + recipient = %Recipient{action: "test", args: %{}, address: contract_genesis} trigger_tx = %Transaction{address: trigger_address} = @@ -333,8 +334,10 @@ defmodule Archethic.Replication.TransactionValidatorTest do v_unspent_outputs = VersionedUnspentOutput.wrap_unspent_outputs(unspent_outputs, current_protocol_version()) + v_recipient = VersionedRecipient.wrap_recipient(recipient, current_transaction_version()) + contract_context = %Contract.Context{ - trigger: {:transaction, trigger_address, recipient}, + trigger: {:transaction, trigger_address, v_recipient}, status: :tx_output, timestamp: DateTime.utc_now(), inputs: Contract.Context.filter_inputs(v_unspent_outputs) @@ -435,7 +438,7 @@ defmodule Archethic.Replication.TransactionValidatorTest do contract_genesis = contract_seed |> Crypto.derive_keypair(0) |> elem(0) |> Crypto.derive_address() - recipient = %Recipient{action: "test", args: [], address: contract_genesis} + recipient = %Recipient{action: "test", args: %{}, address: contract_genesis} trigger_tx = %Transaction{address: trigger_address} = diff --git a/test/archethic/transaction_chain/mem_tables_loader_test.exs b/test/archethic/transaction_chain/mem_tables_loader_test.exs index ea16e0e328..eb1d330190 100644 --- a/test/archethic/transaction_chain/mem_tables_loader_test.exs +++ b/test/archethic/transaction_chain/mem_tables_loader_test.exs @@ -1,138 +1,138 @@ -defmodule Archethic.TransactionChain.MemTablesLoaderTest do - use ArchethicCase - - alias Archethic.Crypto - - alias Archethic.TransactionChain.MemTables.PendingLedger - alias Archethic.TransactionChain.MemTablesLoader - alias Archethic.TransactionChain.Transaction - alias Archethic.TransactionChain.Transaction.ValidationStamp - alias Archethic.TransactionChain.TransactionData - alias Archethic.TransactionChain.TransactionData.Recipient - - alias Archethic.ContractFactory - - import Mox - - describe "load_transaction/1" do - test "should track pending transaction when a code proposal transaction is loaded" do - assert :ok = - %Transaction{ - address: "@CodeProp1", - previous_public_key: "CodeProp0", - data: %TransactionData{}, - type: :code_proposal, - validation_stamp: %ValidationStamp{ - timestamp: DateTime.utc_now() - } - } - |> MemTablesLoader.load_transaction() - - assert ["@CodeProp1"] == PendingLedger.get_signatures("@CodeProp1") - end - - test "should track pending transaction when a smart contract requires conditions is loaded" do - code = """ - condition inherit: [] - - condition transaction: [ - content: regex_match?(\"hello\") - ] - - actions triggered_by: transaction do end - """ - - seed = :crypto.strong_rand_bytes(32) - - tx = - %Transaction{address: address} = - ContractFactory.create_valid_contract_tx(code, seed: seed) - - assert :ok == MemTablesLoader.load_transaction(tx) - - assert [address] == PendingLedger.get_signatures(address) - end - - test "should track recipients to add signature to pending transaction" do - assert :ok = - %Transaction{ - address: "@CodeProp1", - previous_public_key: "CodeProp0", - data: %TransactionData{}, - type: :code_proposal, - validation_stamp: %ValidationStamp{ - timestamp: DateTime.utc_now() - } - } - |> MemTablesLoader.load_transaction() - - assert :ok = - %Transaction{ - address: "@CodeApproval1", - previous_public_key: "CodeApproval0", - data: %TransactionData{ - recipients: [%Recipient{address: "@CodeProp1"}] - }, - type: :code_approval, - validation_stamp: %ValidationStamp{ - timestamp: DateTime.utc_now() - } - } - |> MemTablesLoader.load_transaction() - - assert ["@CodeProp1", "@CodeApproval1"] = PendingLedger.get_signatures("@CodeProp1") - end - end - - describe "start_link/1" do - test "should load from database the transaction to index" do - MockDB - |> stub(:list_transactions, fn _ -> - [ - %Transaction{ - address: Crypto.hash("Alice2"), - previous_public_key: "Alice1", - data: %TransactionData{}, - type: :transfer, - validation_stamp: %ValidationStamp{ - timestamp: DateTime.utc_now() - } - }, - %Transaction{ - address: Crypto.hash("Alice1"), - previous_public_key: "Alice0", - data: %TransactionData{}, - type: :transfer, - validation_stamp: %ValidationStamp{ - timestamp: DateTime.utc_now() |> DateTime.add(-10) - } - }, - %Transaction{ - address: "@CodeProp1", - previous_public_key: "CodeProp0", - data: %TransactionData{}, - type: :code_proposal, - validation_stamp: %ValidationStamp{ - timestamp: DateTime.utc_now() - } - }, - %Transaction{ - address: "@CodeApproval1", - previous_public_key: "CodeApproval0", - data: %TransactionData{ - recipients: [%Recipient{address: "@CodeProp1"}] - }, - type: :code_approval, - validation_stamp: %ValidationStamp{ - timestamp: DateTime.utc_now() - } - } - ] - end) - - assert {:ok, _} = MemTablesLoader.start_link() - - assert ["@CodeProp1", "@CodeApproval1"] == PendingLedger.get_signatures("@CodeProp1") - end - end -end +# defmodule Archethic.TransactionChain.MemTablesLoaderTest do +# use ArchethicCase + +# alias Archethic.Crypto + +# alias Archethic.TransactionChain.MemTables.PendingLedger +# alias Archethic.TransactionChain.MemTablesLoader +# alias Archethic.TransactionChain.Transaction +# alias Archethic.TransactionChain.Transaction.ValidationStamp +# alias Archethic.TransactionChain.TransactionData +# alias Archethic.TransactionChain.TransactionData.Recipient + +# alias Archethic.ContractFactory + +# import Mox + +# describe "load_transaction/1" do +# test "should track pending transaction when a code proposal transaction is loaded" do +# assert :ok = +# %Transaction{ +# address: "@CodeProp1", +# previous_public_key: "CodeProp0", +# data: %TransactionData{}, +# type: :code_proposal, +# validation_stamp: %ValidationStamp{ +# timestamp: DateTime.utc_now() +# } +# } +# |> MemTablesLoader.load_transaction() + +# assert ["@CodeProp1"] == PendingLedger.get_signatures("@CodeProp1") +# end + +# test "should track pending transaction when a smart contract requires conditions is loaded" do +# code = """ +# condition inherit: [] + +# condition transaction: [ +# content: regex_match?(\"hello\") +# ] + +# actions triggered_by: transaction do end +# """ + +# seed = :crypto.strong_rand_bytes(32) + +# tx = +# %Transaction{address: address} = +# ContractFactory.create_valid_contract_tx(code, seed: seed) + +# assert :ok == MemTablesLoader.load_transaction(tx) + +# assert [address] == PendingLedger.get_signatures(address) +# end + +# test "should track recipients to add signature to pending transaction" do +# assert :ok = +# %Transaction{ +# address: "@CodeProp1", +# previous_public_key: "CodeProp0", +# data: %TransactionData{}, +# type: :code_proposal, +# validation_stamp: %ValidationStamp{ +# timestamp: DateTime.utc_now() +# } +# } +# |> MemTablesLoader.load_transaction() + +# assert :ok = +# %Transaction{ +# address: "@CodeApproval1", +# previous_public_key: "CodeApproval0", +# data: %TransactionData{ +# recipients: [%Recipient{address: "@CodeProp1"}] +# }, +# type: :code_approval, +# validation_stamp: %ValidationStamp{ +# timestamp: DateTime.utc_now() +# } +# } +# |> MemTablesLoader.load_transaction() + +# assert ["@CodeProp1", "@CodeApproval1"] = PendingLedger.get_signatures("@CodeProp1") +# end +# end + +# describe "start_link/1" do +# test "should load from database the transaction to index" do +# MockDB +# |> stub(:list_transactions, fn _ -> +# [ +# %Transaction{ +# address: Crypto.hash("Alice2"), +# previous_public_key: "Alice1", +# data: %TransactionData{}, +# type: :transfer, +# validation_stamp: %ValidationStamp{ +# timestamp: DateTime.utc_now() +# } +# }, +# %Transaction{ +# address: Crypto.hash("Alice1"), +# previous_public_key: "Alice0", +# data: %TransactionData{}, +# type: :transfer, +# validation_stamp: %ValidationStamp{ +# timestamp: DateTime.utc_now() |> DateTime.add(-10) +# } +# }, +# %Transaction{ +# address: "@CodeProp1", +# previous_public_key: "CodeProp0", +# data: %TransactionData{}, +# type: :code_proposal, +# validation_stamp: %ValidationStamp{ +# timestamp: DateTime.utc_now() +# } +# }, +# %Transaction{ +# address: "@CodeApproval1", +# previous_public_key: "CodeApproval0", +# data: %TransactionData{ +# recipients: [%Recipient{address: "@CodeProp1"}] +# }, +# type: :code_approval, +# validation_stamp: %ValidationStamp{ +# timestamp: DateTime.utc_now() +# } +# } +# ] +# end) + +# assert {:ok, _} = MemTablesLoader.start_link() + +# assert ["@CodeProp1", "@CodeApproval1"] == PendingLedger.get_signatures("@CodeProp1") +# end +# end +# end diff --git a/test/archethic/transaction_chain/transaction_data/recipient_test.exs b/test/archethic/transaction_chain/transaction_data/recipient_test.exs index 03d6e0bba1..08e0799a3d 100644 --- a/test/archethic/transaction_chain/transaction_data/recipient_test.exs +++ b/test/archethic/transaction_chain/transaction_data/recipient_test.exs @@ -1,10 +1,11 @@ defmodule Archethic.TransactionChain.TransactionData.RecipientTest do @moduledoc false - alias Archethic.TransactionChain.TransactionData.Recipient - use ArchethicCase + use ExUnitProperties import ArchethicCase + alias Archethic.TransactionChain.TransactionData.Recipient + describe "serialize/deserialize v1" do test "should work on unnamed action" do recipient = %Recipient{ @@ -42,4 +43,62 @@ defmodule Archethic.TransactionChain.TransactionData.RecipientTest do assert {^recipient, <<>>} = recipient |> Recipient.serialize(2) |> Recipient.deserialize(2) end end + + describe "serialize/deserialize v3" do + property "symmetric encoding/decoding of recipients arguments as list" do + check all( + args <- + StreamData.list_of( + StreamData.one_of([ + StreamData.integer(), + StreamData.string(:alphanumeric), + StreamData.boolean(), + StreamData.constant(nil) + ]) + ) + ) do + recipient = %Recipient{address: random_address(), action: "action mane", args: args} + + assert {^recipient, <<>>} = + recipient + |> Recipient.serialize(3, :compact) + |> Recipient.deserialize(3, :compact) + + assert {^recipient, <<>>} = + recipient + |> Recipient.serialize(3, :extended) + |> Recipient.deserialize(3, :extended) + end + end + end + + describe "serialize/deserialize v4" do + property "symmetric encoding/decoding of recipients arguments as map" do + check all( + args <- + StreamData.map_of( + StreamData.string(:alphanumeric), + StreamData.one_of([ + StreamData.integer(), + StreamData.string(:alphanumeric), + StreamData.boolean(), + StreamData.constant(nil) + ]) + ) + ) do + version = current_transaction_version() + recipient = %Recipient{address: random_address(), action: "action mane", args: args} + + assert {^recipient, <<>>} = + recipient + |> Recipient.serialize(version, :compact) + |> Recipient.deserialize(version, :compact) + + assert {^recipient, <<>>} = + recipient + |> Recipient.serialize(version, :extended) + |> Recipient.deserialize(version, :extended) + end + end + end end diff --git a/test/archethic/transaction_chain/transaction_data/recipients/arguments_encoding_test.exs b/test/archethic/transaction_chain/transaction_data/recipients/arguments_encoding_test.exs deleted file mode 100644 index 7056683e5b..0000000000 --- a/test/archethic/transaction_chain/transaction_data/recipients/arguments_encoding_test.exs +++ /dev/null @@ -1,30 +0,0 @@ -defmodule Archethic.TransactionChain.TransactionData.Recipient.ArgumentsEncodingTest do - use ExUnit.Case - use ExUnitProperties - - alias Archethic.TransactionChain.TransactionData.Recipient.ArgumentsEncoding - - property "symmetric encoding/decoding of recipients arguments" do - check all( - args <- - StreamData.list_of( - StreamData.one_of([ - StreamData.integer(), - StreamData.string(:alphanumeric), - StreamData.boolean(), - StreamData.constant(nil) - ]) - ) - ) do - assert {^args, ""} = - args - |> ArgumentsEncoding.serialize(:compact) - |> ArgumentsEncoding.deserialize(:compact) - - assert {^args, ""} = - args - |> ArgumentsEncoding.serialize(:extended) - |> ArgumentsEncoding.deserialize(:extended) - end - end -end diff --git a/test/archethic/transaction_chain/transaction_data_test.exs b/test/archethic/transaction_chain/transaction_data_test.exs index 203a8ed4e0..d476aa3dc0 100644 --- a/test/archethic/transaction_chain/transaction_data_test.exs +++ b/test/archethic/transaction_chain/transaction_data_test.exs @@ -3,6 +3,7 @@ defmodule Archethic.TransactionChain.TransactionDataTest do alias Archethic.Crypto alias Archethic.TransactionChain.TransactionData + alias Archethic.TransactionChain.TransactionData.Contract alias Archethic.TransactionChain.TransactionData.Ownership alias Archethic.TransactionChain.TransactionData.Ledger alias Archethic.TransactionChain.TransactionData.UCOLedger @@ -126,49 +127,103 @@ defmodule Archethic.TransactionChain.TransactionDataTest do property "symmetric serialization/deserialization of transaction data" do check all( code <- StreamData.binary(), + contract <- gen_contract(), content <- StreamData.binary(), secret <- StreamData.binary(min_length: 1), authorized_public_keys <- StreamData.list_of(gen_authorized_public_key(), min_length: 1), transfers <- StreamData.list_of(uco_transfer_gen()), - recipients <- StreamData.list_of(recipient_gen()) + recipients_list <- StreamData.list_of(recipient_gen_list()), + recipients_map <- StreamData.list_of(recipient_gen_map()) ) do - {tx_data, _} = - %TransactionData{ - code: code, - content: content, - ownerships: [ - Ownership.new( - secret, - :crypto.strong_rand_bytes(32), - authorized_public_keys - ) - ], - ledger: %Ledger{ - uco: %UCOLedger{ - transfers: transfers - } - }, - recipients: recipients - } - |> TransactionData.serialize(current_transaction_version()) - |> TransactionData.deserialize(current_transaction_version()) + tx_data_v3 = %TransactionData{ + code: code, + content: content, + ownerships: [ + Ownership.new(secret, :crypto.strong_rand_bytes(32), authorized_public_keys) + ], + ledger: %Ledger{uco: %UCOLedger{transfers: transfers}}, + recipients: recipients_list + } - assert tx_data.code == code - assert tx_data.content == content - assert List.first(tx_data.ownerships).secret == secret + assert {tx_data_v3, <<>>} == + tx_data_v3 + |> TransactionData.serialize(3) + |> TransactionData.deserialize(3) - assert Enum.all?( - Ownership.list_authorized_public_keys(List.first(tx_data.ownerships)), - &(&1 in authorized_public_keys) - ) + tx_data_v4 = %TransactionData{ + contract: contract, + content: content, + ownerships: [ + Ownership.new(secret, :crypto.strong_rand_bytes(32), authorized_public_keys) + ], + ledger: %Ledger{uco: %UCOLedger{transfers: transfers}}, + recipients: recipients_map + } - assert tx_data.recipients == recipients - assert tx_data.ledger.uco.transfers == transfers + version = current_transaction_version() + + assert {tx_data_v4, <<>>} == + tx_data_v4 + |> TransactionData.serialize(version) + |> TransactionData.deserialize(version) end end end + defp gen_contract() do + gen all( + bytecode <- StreamData.binary(min_length: 1, max_length: 2_000), + functions <- StreamData.list_of(gen_contract_manifest_function(), max_length: 5), + state <- + StreamData.map_of( + StreamData.string(:alphanumeric), + StreamData.one_of([StreamData.constant("u32"), StreamData.constant("string")]), + max_length: 5 + ) + ) do + %Contract{ + bytecode: bytecode, + manifest: %{ + "abi" => %{ + "state" => state, + "functions" => Enum.into(functions, %{}) + } + } + } + end + end + + defp gen_contract_manifest_function() do + gen all( + name <- StreamData.string(:alphanumeric), + input <- + StreamData.map_of( + StreamData.string(:alphanumeric), + StreamData.one_of([StreamData.constant("u32"), StreamData.constant("string")]), + max_length: 3 + ), + output <- + StreamData.map_of( + StreamData.string(:alphanumeric), + StreamData.one_of([StreamData.constant("u32"), StreamData.constant("string")]), + length: 1 + ), + type <- + StreamData.one_of([ + StreamData.constant("action"), + StreamData.constant("publicFunction") + ]) + ) do + {name, + %{ + "type" => type, + "input" => input, + "output" => output + }} + end + end + defp uco_transfer_gen() do gen all( to <- StreamData.binary(length: 32), @@ -186,7 +241,27 @@ defmodule Archethic.TransactionChain.TransactionDataTest do end) end - defp recipient_gen() do + defp recipient_gen_map() do + gen all( + address <- StreamData.binary(length: 32), + action <- StreamData.string(:alphanumeric, min_length: 1), + args <- + StreamData.map_of( + StreamData.string(:alphanumeric), + StreamData.one_of([ + StreamData.integer(), + StreamData.string(:alphanumeric), + StreamData.boolean(), + StreamData.constant(nil) + ]), + max_length: 3 + ) + ) do + %Recipient{address: <<0::8, 0::8, address::binary>>, action: action, args: args} + end + end + + defp recipient_gen_list() do gen all( address <- StreamData.binary(length: 32), action <- StreamData.string(:alphanumeric, min_length: 1), diff --git a/test/archethic_web/api/ecto_schemas/transaction_payload_test.exs b/test/archethic_web/api/ecto_schemas/transaction_payload_test.exs index 8928c0441c..146f243c54 100644 --- a/test/archethic_web/api/ecto_schemas/transaction_payload_test.exs +++ b/test/archethic_web/api/ecto_schemas/transaction_payload_test.exs @@ -604,7 +604,7 @@ defmodule ArchethicWeb.API.TransactionPayloadTest do test "should accept recipients both named & unnamed" do map = %{ - "version" => current_transaction_version(), + "version" => 3, "address" => Base.encode16(random_address()), "type" => "transfer", "timestamp" => DateTime.utc_now() |> DateTime.to_unix(:millisecond), diff --git a/test/archethic_web/api/jsonrpc/methods/estimate_transaction_fee_test.exs b/test/archethic_web/api/jsonrpc/methods/estimate_transaction_fee_test.exs index da6b5a2f3c..fe1c44dc74 100644 --- a/test/archethic_web/api/jsonrpc/methods/estimate_transaction_fee_test.exs +++ b/test/archethic_web/api/jsonrpc/methods/estimate_transaction_fee_test.exs @@ -57,7 +57,7 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.EstimateTransactionFeeTest do assert {:ok, %{ - "fee" => 5_000_080, + "fee" => 5_000_064, "rates" => %{ "eur" => 0.2, "usd" => 0.2 diff --git a/test/archethic_web/api/jsonrpc/methods/simulate_contract_execution_test.exs b/test/archethic_web/api/jsonrpc/methods/simulate_contract_execution_test.exs index f90c5b2c70..4ee4b5c6c6 100644 --- a/test/archethic_web/api/jsonrpc/methods/simulate_contract_execution_test.exs +++ b/test/archethic_web/api/jsonrpc/methods/simulate_contract_execution_test.exs @@ -103,7 +103,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do trigger_tx = TransactionFactory.create_non_valided_transaction( recipients: [%Recipient{address: old_contract_address}], - content: "test content" + content: "test content", + version: 3 ) assert {:ok, [%{"valid" => true, "recipient_address" => ^old_contract_address_hex}]} = @@ -144,7 +145,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do trigger_tx = TransactionFactory.create_non_valided_transaction( recipients: [%Recipient{address: old_contract_address, action: "vote", args: ["Jonas"]}], - content: "test content" + content: "test content", + version: 3 ) assert {:ok, [%{"valid" => true, "recipient_address" => ^old_contract_address_hex}]} = @@ -187,7 +189,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do recipients: [ %Recipient{address: old_contract_address, action: "non_existing_action", args: [1, 2]} ], - content: "test content" + content: "test content", + version: 3 ) assert {:ok, [%{"valid" => false, "recipient_address" => ^old_contract_address_hex}]} = @@ -230,7 +233,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do recipients: [ %Recipient{address: old_contract_address, action: "vote", args: ["Jose", "Monica"]} ], - content: "test content" + content: "test content", + version: 3 ) assert {:ok, [%{"valid" => false, "recipient_address" => ^old_contract_address_hex}]} = @@ -273,7 +277,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do trigger_tx = TransactionFactory.create_non_valided_transaction( recipients: [%Recipient{address: old_contract_address, action: "vote", args: ["Lance"]}], - content: "test content" + content: "test content", + version: 3 ) assert {:ok, [%{"valid" => false, "recipient_address" => ^old_contract_address_hex}]} = @@ -319,7 +324,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do trigger_tx = TransactionFactory.create_non_valided_transaction( content: "test", - recipients: [%Recipient{address: contract_address}] + recipients: [%Recipient{address: contract_address}], + version: 3 ) assert {:ok, @@ -366,7 +372,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do trigger_tx = TransactionFactory.create_non_valided_transaction( content: "test", - recipients: [%Recipient{address: contract_address}] + recipients: [%Recipient{address: contract_address}], + version: 3 ) assert {:ok, @@ -403,7 +410,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do trigger_tx = TransactionFactory.create_non_valided_transaction( content: "test", - recipients: [%Recipient{address: contract_address}] + recipients: [%Recipient{address: contract_address}], + version: 3 ) assert {:ok, @@ -452,7 +460,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do trigger_tx = TransactionFactory.create_non_valided_transaction( content: "test", - recipients: [%Recipient{address: contract_address}] + recipients: [%Recipient{address: contract_address}], + version: 3 ) assert {:ok, @@ -523,7 +532,8 @@ defmodule ArchethicWeb.API.JsonRPC.Methods.SimulateContractExecutionTest do recipients: [ %Recipient{address: contract_address1}, %Recipient{address: contract_address2} - ] + ], + version: 3 ) assert {:ok, diff --git a/test/archethic_web/api/jsonrpc/schemas/transaction_test.exs b/test/archethic_web/api/jsonrpc/schemas/transaction_test.exs index e93a0f0f70..7eb3454f39 100644 --- a/test/archethic_web/api/jsonrpc/schemas/transaction_test.exs +++ b/test/archethic_web/api/jsonrpc/schemas/transaction_test.exs @@ -120,7 +120,7 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchemaTest do test "should return an error if the code length is more than limit" do map = %{ - "version" => current_transaction_version(), + "version" => 3, "address" => Base.encode16(random_address()), "type" => "transfer", "previousPublicKey" => Base.encode16(random_address()), @@ -505,7 +505,7 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchemaTest do test "should accept recipients both named & unnamed" do map = %{ - "version" => current_transaction_version(), + "version" => 3, "address" => Base.encode16(random_address()), "type" => "transfer", "previousPublicKey" => Base.encode16(random_address()), @@ -526,6 +526,29 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchemaTest do } assert :ok = TransactionSchema.validate(map) + + map = %{ + "version" => current_transaction_version(), + "address" => Base.encode16(random_address()), + "type" => "transfer", + "previousPublicKey" => Base.encode16(random_address()), + "previousSignature" => Base.encode16(:crypto.strong_rand_bytes(64)), + "originSignature" => Base.encode16(:crypto.strong_rand_bytes(64)), + "data" => %{ + "recipients" => [ + %{ + "address" => Base.encode16(random_address()) + }, + %{ + "address" => Base.encode16(random_address()), + "action" => "something", + "args" => %{} + } + ] + } + } + + assert :ok = TransactionSchema.validate(map) end test "should return an error if the recipients are more that 255" do @@ -619,6 +642,20 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchemaTest do "previousSignature" => Base.encode16(previous_signature), "originSignature" => Base.encode16(origin_signature), "data" => %{ + "contract" => %{ + "bytecode" => :crypto.strong_rand_bytes(32) |> Base.encode16(), + "manifest" => %{ + "abi" => %{ + "state" => %{}, + "functions" => %{ + "inc" => %{ + "type" => "action", + "triggerType" => "transaction" + } + } + } + } + }, "ledger" => %{ "uco" => %{ "transfers" => [ @@ -648,18 +685,21 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchemaTest do } } + :abi + :functions + assert %Transaction{ - version: transaction_version, - address: address, + version: ^transaction_version, + address: ^address, type: :transfer, - previous_public_key: previous_public_key, - previous_signature: previous_signature, - origin_signature: origin_signature, + previous_public_key: ^previous_public_key, + previous_signature: ^previous_signature, + origin_signature: ^origin_signature, data: %TransactionData{ recipients: [ - %Recipient{address: recipient, action: nil, args: nil}, + %Recipient{address: ^recipient, action: nil, args: nil}, %Recipient{ - address: recipient2_address, + address: ^recipient2_address, action: "something", args: [1, 2, 3] } @@ -667,20 +707,34 @@ defmodule ArchethicWeb.API.JsonRPC.TransactionSchemaTest do ledger: %Ledger{ uco: %UCOLedger{ transfers: [ - %UCOTransfer{to: uco_to, amount: 1_020_000_000} + %UCOTransfer{to: ^uco_to, amount: 1_020_000_000} ] } }, ownerships: [ %Ownership{ - secret: secret, + secret: ^secret, authorized_keys: %{ - authorized_public_key => encrypted_key + ^authorized_public_key => ^encrypted_key + } + } + ], + contract: %{ + bytecode: _, + manifest: %{ + "abi" => %{ + "functions" => %{ + "inc" => %{ + "type" => "action", + "triggerType" => "transaction" + } + }, + "state" => %{} } } - ] + } } - } == TransactionSchema.to_transaction(map) + } = TransactionSchema.to_transaction(map) end end end diff --git a/test/archethic_web/api/rest/controllers/transaction_controller_test.exs b/test/archethic_web/api/rest/controllers/transaction_controller_test.exs index 56b9b6066e..150a56babc 100644 --- a/test/archethic_web/api/rest/controllers/transaction_controller_test.exs +++ b/test/archethic_web/api/rest/controllers/transaction_controller_test.exs @@ -70,7 +70,7 @@ defmodule ArchethicWeb.API.REST.TransactionControllerTest do }) assert %{ - "fee" => 6_500_289, + "fee" => 6_500_275, "rates" => %{ "eur" => 0.2, "usd" => 0.2 @@ -158,7 +158,7 @@ defmodule ArchethicWeb.API.REST.TransactionControllerTest do "previousSignature" => "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", "type" => "transfer", - "version" => current_transaction_version() + "version" => 3 } conn = post(conn, "/api/transaction/contract/simulator", new_tx) @@ -262,7 +262,7 @@ defmodule ArchethicWeb.API.REST.TransactionControllerTest do "previousSignature" => "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", "type" => "transfer", - "version" => current_transaction_version() + "version" => 3 } conn = post(conn, "/api/transaction/contract/simulator", new_tx) @@ -342,7 +342,7 @@ defmodule ArchethicWeb.API.REST.TransactionControllerTest do "previousSignature" => "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", "type" => "transfer", - "version" => current_transaction_version() + "version" => 3 } conn = post(conn, "/api/transaction/contract/simulator", new_tx) @@ -395,7 +395,7 @@ defmodule ArchethicWeb.API.REST.TransactionControllerTest do "previousSignature" => "9b209dd92c6caffbb5c39d12263f05baebc9fe3c36cb0f4dde04c96f1237b75a3a2973405c6d9d5e65d8a970a37bafea57b919febad46b0cceb04a7ffa4b6b00", "type" => "transfer", - "version" => current_transaction_version() + "version" => 3 } conn = post(conn, "/api/transaction/contract/simulator", new_tx) diff --git a/test/archethic_web/explorer/controllers/faucet_controller_test.exs b/test/archethic_web/explorer/controllers/faucet_controller_test.exs index 9d44d17d0f..ae36cd1df9 100644 --- a/test/archethic_web/explorer/controllers/faucet_controller_test.exs +++ b/test/archethic_web/explorer/controllers/faucet_controller_test.exs @@ -63,7 +63,7 @@ defmodule ArchethicWeb.Explorer.FaucetControllerTest do }, @pool_seed, 0, - Crypto.default_curve() + curve: Crypto.default_curve() ) MockClient @@ -124,7 +124,7 @@ defmodule ArchethicWeb.Explorer.FaucetControllerTest do }, @pool_seed, 0, - Crypto.default_curve() + curve: Crypto.default_curve() ) MockClient diff --git a/test/support/contract_factory.ex b/test/support/contract_factory.ex index 76f931e959..7805c988fb 100644 --- a/test/support/contract_factory.ex +++ b/test/support/contract_factory.ex @@ -3,7 +3,7 @@ defmodule Archethic.ContractFactory do alias Archethic.Crypto - alias Archethic.Contracts.Constants + alias Archethic.Contracts.Interpreter.Constants alias Archethic.TransactionFactory alias Archethic.TransactionChain.Transaction @@ -73,6 +73,7 @@ defmodule Archethic.ContractFactory do Keyword.update(opts, :type, :contract, & &1) |> Keyword.put(:ownerships, [contract_seed_ownership | ownerships]) |> Keyword.put(:code, code) + |> Keyword.put(:version, 3) inputs = Keyword.get(opts, :inputs, [ diff --git a/test/support/transaction_factory.ex b/test/support/transaction_factory.ex index 92da4b8893..84d93b51ad 100644 --- a/test/support/transaction_factory.ex +++ b/test/support/transaction_factory.ex @@ -34,6 +34,7 @@ defmodule Archethic.TransactionFactory do ledger = Keyword.get(opts, :ledger, %Ledger{}) recipients = Keyword.get(opts, :recipients, []) ownerships = Keyword.get(opts, :ownerships, []) + version = Keyword.get(opts, :version, current_transaction_version()) Transaction.new( type, @@ -45,7 +46,8 @@ defmodule Archethic.TransactionFactory do ownerships: ownerships }, seed, - index + index, + version: version ) end @@ -72,6 +74,7 @@ defmodule Archethic.TransactionFactory do prev_tx = Keyword.get(opts, :prev_tx) protocol_version = Keyword.get(opts, :protocol_version, current_protocol_version()) contract_context = Keyword.get(opts, :contract_context, nil) + version = Keyword.get(opts, :version, current_transaction_version()) timestamp = Keyword.get(opts, :timestamp, DateTime.utc_now()) |> DateTime.truncate(:millisecond) @@ -89,7 +92,8 @@ defmodule Archethic.TransactionFactory do recipients: recipients }, seed, - index + index, + version: version ) fee = Fee.calculate(tx, nil, 0.07, timestamp, encoded_state, 0, current_protocol_version()) diff --git a/test/test_helper.exs b/test/test_helper.exs index 618d791678..1c425c39eb 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -41,3 +41,5 @@ Mox.defmock(MockNATDiscovery, for: Archethic.Networking.IPLookup.Impl) Mox.defmock(MockUCOPrice, for: Archethic.OracleChain.Services.Impl) Mox.defmock(MockUCOProvider1, for: Archethic.OracleChain.Services.UCOPrice.Providers.Impl) Mox.defmock(MockUCOProvider2, for: Archethic.OracleChain.Services.UCOPrice.Providers.Impl) + +Mox.defmock(MockWasmIO, for: Archethic.Contracts.Wasm.IO)