Skip to content

Commit

Permalink
Add more validation for smart contract wasm
Browse files Browse the repository at this point in the history
  • Loading branch information
samuelmanzanera committed Jan 13, 2025
1 parent e2d39e0 commit b989683
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 39 deletions.
26 changes: 0 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
30 changes: 19 additions & 11 deletions lib/archethic/contracts/wasm/contract.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,29 @@ defmodule Archethic.Contracts.WasmContract do
@doc """
Parse smart contract json block and return a contract struct
"""
@spec parse(Contract.t()) :: {:ok, t()} | {:error, String.t()}
@spec parse(Contract.t()) :: 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)}"}
{:ok, module} = WasmModule.parse(uncompressed_bytes, spec)
%__MODULE__{module: module}
end

@doc """
Validate WASM contract
"""
@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)

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 +88,7 @@ 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}}

{:error, _} = e ->
e
end
{:ok, %__MODULE__{parse(contract) | state: get_state_from_tx(tx), transaction: tx}}
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
9 changes: 8 additions & 1 deletion lib/archethic/mining/pending_transaction_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,19 @@ defmodule Archethic.Mining.PendingTransactionValidation do
end

defp parse_contract(tx) do
case Contracts.from_transaction(tx) do
case validate_and_parse_transaction(tx) do
{:ok, contract} -> {:ok, contract}
{:error, reason} -> {:error, "Smart contract invalid #{inspect(reason)}"}
end
end

defp validate_and_parse_transaction(tx = %Transaction{data: %TransactionData{code: code}})
when code != "",
do: Contracts.Interpreter.Contract.from_transaction(tx)

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

defp validate_contract_ownership(contract, ownerships) do
if Contracts.contains_trigger?(contract),
do: ensure_ownership_in_contract(ownerships),
Expand Down
2 changes: 1 addition & 1 deletion lib/archethic/mining/proof_of_work.ex
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ defmodule Archethic.Mining.ProofOfWork do
@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
case Contracts.from_transaction(tx) do
{:ok,
%InterpretedContract{
conditions: %{
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

0 comments on commit b989683

Please sign in to comment.