Skip to content

Commit

Permalink
Update with manifest to generate spec
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelmanzanera committed Sep 11, 2024
1 parent ec32164 commit a8a9432
Show file tree
Hide file tree
Showing 17 changed files with 302 additions and 310 deletions.
95 changes: 61 additions & 34 deletions lib/archethic/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ defmodule Archethic.Contracts do
Each smart contract is register and supervised as long running process to interact with later on.
"""

alias Archethic.Contracts.WasmTrigger
alias __MODULE__.Conditions
alias __MODULE__.Constants
alias __MODULE__.Contract
Expand Down Expand Up @@ -57,14 +56,14 @@ defmodule Archethic.Contracts do
Parse a smart contract code and return a contract struct
"""
@spec parse(binary()) :: {:ok, Contract.t() | WasmContract.t()} | {:error, String.t()}
def parse(contract_code) do
def parse(contract_code, tx_content \\ "") do
if String.printable?(contract_code) do
Interpreter.parse(contract_code)
else
case WasmModule.parse(contract_code) do
{:ok, module} ->
{:ok, %WasmContract{module: module}}

with {:ok, json} <- Jason.decode(tx_content),
{:ok, module} <- WasmModule.parse(contract_code, WasmSpec.from_manifest(json)) do
{:ok, %WasmContract{module: module}}
else
{:error, reason} ->
{:error, "#{inspect(reason)}"}
end
Expand Down Expand Up @@ -128,8 +127,9 @@ defmodule Archethic.Contracts do

if genesis_address == from do
case Enum.at(arg, 0) do
%{"code" => new_code} ->
{:ok, new_module} = WasmModule.parse(Base.decode16!(new_code, case: :mixed))
%{"code" => new_code, "manifest" => manifest} ->
spec = manifest |> Jason.decode!() |> WasmSpec.from_manifest()
{:ok, new_module} = WasmModule.parse(Base.decode16!(new_code, case: :mixed), spec)

upgrade_state =
if "onUpgrade" in WasmModule.list_exported_functions_name(new_module) do
Expand Down Expand Up @@ -230,7 +230,7 @@ defmodule Archethic.Contracts do
_opts
) do
trigger =
Enum.find(triggers, fn %WasmTrigger{type: type, function_name: fn_name} ->
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
Expand All @@ -241,18 +241,25 @@ defmodule Archethic.Contracts do
end)

if trigger != nil do
argument =
with %Recipient{args: args} <- maybe_recipient,
arg when arg != nil <- Enum.at(args, 0) do
arg
else
_ ->
nil
end

argument = case maybe_recipient do
%Recipient{args: args = [_|_]} ->
case Enum.at(args, 0) do
arg when is_map(arg) -> arg
_ ->
%WasmSpec.Trigger{input: input} = trigger

input
|> Map.keys()
|> Enum.with_index()
|> Enum.reduce(%{}, fn {key, index}, acc -> Map.put(acc, key, Enum.at(args, index)) end)
end
_ -> nil
end

WasmModule.execute(
module,
trigger.function_name,
trigger.name,
transaction: maybe_trigger_tx,
state: state,
balance: UTXO.get_balance(inputs),
Expand Down Expand Up @@ -409,23 +416,43 @@ defmodule Archethic.Contracts do
args_values,
inputs
) do
if function_name in functions do
{:ok, %ReadResult{value: value}} =
WasmModule.execute(module, function_name,
state: state,
balance: UTXO.get_balance(inputs),
arguments: Enum.at(args_values, 0)
)
case Enum.find(functions, &(&1.name == function_name)) do
nil ->
{:error,
%Failure{
error: "invalid execution",
user_friendly_error: "#{function_name} is not exposed as public function",
stacktrace: [],
logs: []
}}

{:ok, value, []}
else
{:error,
%Failure{
error: "invalid execution",
user_friendly_error: "#{function_name} is not exposed as public function",
stacktrace: [],
logs: []
}}
%WasmSpec.Function{input: input} ->
arguments =
if length(args_values) > 0 do
case Enum.at(args_values, 0) do
arg when is_map(arg) ->
arg

_ ->
input
|> Map.keys()
|> Enum.with_index()
|> Enum.reduce(%{}, fn {key, index}, acc ->
Map.put(acc, key, Enum.at(args_values, index))
end)
end
else
nil
end

{:ok, %ReadResult{value: value}} =
WasmModule.execute(module, function_name,
state: state,
balance: UTXO.get_balance(inputs),
arguments: arguments
)

{:ok, value, []}
end
end

Expand Down
142 changes: 12 additions & 130 deletions lib/archethic/contracts/wasm/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ defmodule Archethic.Contracts.WasmContract do
"""

alias Archethic.Contracts.Contract.State
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
Expand Down Expand Up @@ -62,17 +60,18 @@ defmodule Archethic.Contracts.WasmContract do
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 WasmModule.parse(code) do
{:ok, module} ->
contract = %__MODULE__{
module: module,
state: get_state_from_tx(tx),
transaction: tx
}

{:ok, contract}

def from_transaction(tx = %Transaction{data: %TransactionData{code: code, content: content}}) do
with {:ok, manifest_json} <- Jason.decode(content),
spec = WasmSpec.from_manifest(manifest_json),
{:ok, module} <- WasmModule.parse(code, spec) do
contract = %__MODULE__{
module: module,
state: get_state_from_tx(tx),
transaction: tx
}

{:ok, contract}
else
{:error, reason} ->
{:error, "#{inspect(reason)}"}
end
Expand Down Expand Up @@ -107,121 +106,4 @@ defmodule Archethic.Contracts.WasmContract do
"""
def get_trigger_for_recipient(%Recipient{action: action, args: _args_values}),
do: {:transaction, action, nil}

@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
29 changes: 11 additions & 18 deletions lib/archethic/contracts/wasm/module.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ defmodule Archethic.Contracts.WasmModule do
alias Archethic.TransactionChain.Transaction
alias Archethic.Utils

@reserved_functions ["spec", "onInit", "onUpgrade"]
@reserved_functions ["onInit", "onUpgrade"]

defstruct [:module, :store, :spec]

Expand Down Expand Up @@ -38,8 +38,8 @@ defmodule Archethic.Contracts.WasmModule do
@doc """
Parse wasm module and perform some checks
"""
@spec parse(bytes :: binary()) :: {:ok, t()} | {:error, any()}
def parse(bytes) when is_binary(bytes) do
@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))

Expand All @@ -59,7 +59,7 @@ defmodule Archethic.Contracts.WasmModule do
wrap_module = %__MODULE__{module: module, store: store},
:ok <- check_module_imports(wrap_module),
:ok <- check_module_exports(wrap_module),
{:ok, spec} <- check_spec_exports(wrap_module) do
:ok <- check_spec_exports(wrap_module, spec) do
{:ok, %{wrap_module | spec: spec}}
end
end
Expand Down Expand Up @@ -102,26 +102,19 @@ defmodule Archethic.Contracts.WasmModule do
defp check_module_exports(%__MODULE__{module: module}) do
exported_functions = exported_functions(module)

if Map.has_key?(exported_functions, "spec") do
if Enum.all?(exported_functions, &match?({_, {:fn, [], []}}, &1)) do
:ok
else
{:error, "exported function shouldn't have input/output variables"}
end
if Enum.all?(exported_functions, &match?({_, {:fn, [], []}}, &1)) do
:ok
else
{:error, "wasm's module doesn't expose spec function"}
{:error, "exported function shouldn't have input/output variables"}
end
end

defp check_spec_exports(module) do
defp check_spec_exports(module, spec) do
exported_functions = list_exported_functions_name(module)
spec_functions = WasmSpec.function_names(spec)

with {:ok, %ReadResult{value: result}} <- execute(module, "spec"),
spec = WasmSpec.cast(result),
spec_functions = WasmSpec.function_names(spec),
:ok <- validate_existing_spec_functions(spec_functions, exported_functions),
:ok <- validate_exported_functions_in_spec(spec_functions, exported_functions) do
{:ok, spec}
with :ok <- validate_existing_spec_functions(spec_functions, exported_functions) do
validate_exported_functions_in_spec(spec_functions, exported_functions)
end
end

Expand Down
Loading

0 comments on commit a8a9432

Please sign in to comment.