Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add more validation for smart contract wasm #1631

Merged
merged 2 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 14 additions & 26 deletions lib/archethic/contracts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,32 +52,6 @@ defmodule Archethic.Contracts do
end
end

@doc """
Parse a smart contract code and return a contract struct
"""
@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()) :: InterpretedContract.t() | WasmContract.t()
def parse!(contract_code) when is_binary(contract_code) do
case parse(contract_code) do
{:ok, contract} -> contract
{:error, reason} -> raise reason
end
end

@doc """
Execute the contract trigger.
"""
Expand Down Expand Up @@ -814,6 +788,20 @@ defmodule Archethic.Contracts do
end
end

@doc """
Returns a contract instance from a transaction
"""
@spec validate_and_parse_transaction(transaction :: Transaction.t()) ::
{:ok, InterpretedContract.t() | WasmContract.t()} | {:error, String.t()}
def validate_and_parse_transaction(tx = %Transaction{version: version}) when version < 4,
do: InterpretedContract.from_transaction(tx)

def validate_and_parse_transaction(%Transaction{data: %TransactionData{contract: nil}}),
do: {:error, "No contract to parse"}

def validate_and_parse_transaction(%Transaction{data: %TransactionData{contract: contract}}),
do: WasmContract.validate_and_parse(contract)

defp get_condition_constants(
:inherit,
%InterpretedContract{
Expand Down
30 changes: 17 additions & 13 deletions lib/archethic/contracts/wasm/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,19 @@ defmodule Archethic.Contracts.WasmContract do
end

@doc """
Parse smart contract json block and return a contract struct
Validate WASM contract
"""
@spec parse(Contract.t()) :: {:ok, t()} | {:error, String.t()}
def parse(%Contract{manifest: manifest, bytecode: bytecode}) do
@spec validate_and_parse(t()) :: {:ok, t()} | {:error, String.t()}
def validate_and_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)}"}
with :ok <- WasmSpec.validate_manifest(manifest),
spec = WasmSpec.from_manifest(manifest),
{:ok, module} <- WasmModule.parse(uncompressed_bytes, spec) do
{:ok, %__MODULE__{module: module}}
end
rescue
ErlangError -> {:error, "invalid bytecode"}
end

@doc """
Expand All @@ -74,13 +76,15 @@ defmodule Archethic.Contracts.WasmContract do
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}}
{:ok, %__MODULE__{parse(contract) | state: get_state_from_tx(tx), transaction: tx}}
end

{:error, _} = e ->
e
end
defp parse(%Contract{manifest: manifest, bytecode: bytecode}) do
uncompressed_bytes = :zlib.unzip(bytecode)
spec = WasmSpec.from_manifest(manifest)

{:ok, module} = WasmModule.parse(uncompressed_bytes, spec)
%__MODULE__{module: module}
end

defp get_state_from_tx(%Transaction{
Expand Down
31 changes: 31 additions & 0 deletions lib/archethic/contracts/wasm/spec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,31 @@ defmodule Archethic.Contracts.WasmSpec do
}
defstruct [:version, triggers: [], public_functions: [], upgrade_opts: nil]

@contract_schema :archethic
|> Application.app_dir("priv/json-schemas/schemas/object/contract.json")
|> File.read!()
|> Jason.decode!()
|> ExJsonSchema.Schema.resolve()

@doc """
Validate the manifest from the JSON schema specification
"""
@spec validate_manifest(map()) :: :ok | {:error, String.t()}
def validate_manifest(manifest) do
case ExJsonSchema.Validator.validate_fragment(
@contract_schema,
"#/properties/manifest",
manifest
) do
:ok -> :ok
{:error, errors} -> {:error, "invalid manifest - #{inspect(errors)}"}
end
end

@doc """
Cast the JSON manifest into a well-defined structure
"""
@spec from_manifest(map()) :: t()
def from_manifest(
manifest = %{
"abi" => %{
Expand Down Expand Up @@ -64,6 +89,9 @@ defmodule Archethic.Contracts.WasmSpec do
Enum.map(triggers, & &1.name) ++ Enum.map(public_functions, & &1.name)
end

@doc """
Cast an input to the corresponding type from the spec
"""
@spec cast_wasm_input(any(), any()) :: {:ok, any()} | {:error, :invalid_input_type}
def cast_wasm_input(nil, _), do: {:ok, nil}

Expand Down Expand Up @@ -153,6 +181,9 @@ defmodule Archethic.Contracts.WasmSpec do
{:error, :invalid_input_type}
end

@doc """
Cast an output to the corresponding type from the spec
"""
@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
Expand Down
2 changes: 1 addition & 1 deletion lib/archethic/mining/pending_transaction_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end

defp parse_contract(tx) do
case Contracts.from_transaction(tx) do
case Contracts.validate_and_parse_transaction(tx) do
{:ok, contract} -> {:ok, contract}
{:error, reason} -> {:error, "Smart contract invalid #{inspect(reason)}"}
end
Expand Down
8 changes: 3 additions & 5 deletions lib/archethic/mining/proof_of_work.ex
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,10 @@ defmodule Archethic.Mining.ProofOfWork do

Smart contract code can defined which family to use (like security level)
"""
# TODO: support WasmContract inherit conditions
@spec list_origin_public_keys_candidates(Transaction.t()) :: list(Crypto.key())
def list_origin_public_keys_candidates(tx = %Transaction{data: %TransactionData{code: code}})
when code != "" do
case Contracts.parse(code) do
def list_origin_public_keys_candidates(tx = %Transaction{version: version}) when version < 4 do
case Contracts.from_transaction(tx) do
{:ok,
%InterpretedContract{
conditions: %{
Expand All @@ -127,8 +127,6 @@ defmodule Archethic.Mining.ProofOfWork do
when family != :all ->
SharedSecrets.list_origin_public_keys(family)

# TODO: support WasmContract inherit conditions

_ ->
do_list_origin_public_keys_candidates(tx)
end
Expand Down
60 changes: 60 additions & 0 deletions test/archethic/mining/pending_transaction_validation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,66 @@ defmodule Archethic.Mining.PendingTransactionValidationTest do
|> ContractFactory.create_valid_contract_tx()
|> PendingTransactionValidation.validate_contract()
end

test "invalid bytecode" do
assert {:error, "Smart contract invalid \"invalid bytecode\""} =
Transaction.new(
:contract,
%TransactionData{
contract: %Archethic.TransactionChain.TransactionData.Contract{
bytecode: "",
manifest: %{}
}
},
"seed",
0,
version: 4
)
|> PendingTransactionValidation.validate_contract()
end

test "invalid manifest" do
assert {:error,
"Smart contract invalid \"invalid manifest - [{\\\"Required property abi was not present.\\\", \\\"#\\\"}]\""} =
Transaction.new(
:contract,
%TransactionData{
contract: %Archethic.TransactionChain.TransactionData.Contract{
bytecode: :zlib.zip(:crypto.strong_rand_bytes(32)),
manifest: %{
"key" => "value"
}
}
},
"seed",
0,
version: 4
)
|> PendingTransactionValidation.validate_contract()
end

test "invalid wasm module" do
assert {:error,
"Smart contract invalid \"Error while parsing bytes: input bytes aren't valid utf-8.\""} =
Transaction.new(
:contract,
%TransactionData{
contract: %Archethic.TransactionChain.TransactionData.Contract{
bytecode: :zlib.zip(:crypto.strong_rand_bytes(32)),
manifest: %{
"abi" => %{
"functions" => %{},
"state" => %{}
}
}
}
},
"seed",
0,
version: 4
)
|> PendingTransactionValidation.validate_contract()
end
end

describe "Data" do
Expand Down
Loading