EEVM.Database behaviour (eevm v0.1.0)

Copy Markdown View Source

World state: a read-only backend plus eevm's own write overlay.

It inspects as its backend and a count of pending writes, such as #EEVM.Database<EEVM.Database.InMemory, pending writes to 2 accounts and 1 storage slot>; read state with this module's functions, list the writes with changes/1, or pass structs: false to inspect/2 to see every field.

A backend implements this behaviour's read callbacks, account/2, code/2, storage/3, and block_hash/2. eevm never writes to it during execution: every balance, nonce, code, and storage write lands in the overlay this struct carries. Because a %EEVM.Database{} is an immutable value, reverting a call frame is just dropping its copy.

When a transaction or block finishes, changes/1 returns the overlay as an EEVM.StateChanges. EEVM.transact/4 and EEVM.Block.execute/4 hand it to the backend's commit/2 and return the committed database. When commit/2 returns {:error, :unsupported}, the result keeps the overlay and the caller applies result.changes itself. EEVM.run/2 never commits.

EEVM.StateRoot also needs to enumerate the backend, through accounts/1 and storage_slots/2. A backend that can't, such as one over JSON-RPC, returns {:error, :unsupported} from them and still executes transactions; the EIP-7610 check that a CREATE target has no storage then sees only the overlay.

Every callback is required, so what a backend can do is stated in its code: eevm never asks the code server which functions a module exports. use EEVM.Database declares the behaviour and defines commit/2, accounts/1, and storage_slots/2 to return {:error, :unsupported} until the module overrides them, so a read-only backend implements only the four reads.

EEVM.Database.InMemory implements every callback. This read-only backend serves a fixed map of accounts, so a call's writes stay in the overlay and commit/1 reports that the backend can't commit:

iex> defmodule MyApp.FixedDatabase do
...>   use EEVM.Database
...>   @impl true
...>   def account(accounts, address), do: {:ok, Map.get(accounts, address)}
...>   @impl true
...>   def code(_accounts, _code_hash), do: {:ok, <<>>}
...>   @impl true
...>   def storage(_accounts, _address, _slot), do: {:ok, 0}
...>   @impl true
...>   def block_hash(_accounts, _number), do: {:ok, nil}
...> end
iex> db = EEVM.Database.new(MyApp.FixedDatabase, %{0xA11CE => %{balance: 100, nonce: 0, code: <<>>}})
iex> {:ok, result} = EEVM.call(db, 0xB0B, <<>>, caller: 0xA11CE, value: 40)
iex> {EEVM.Database.get_balance(result.db, 0xA11CE), EEVM.Database.get_balance(result.db, 0xB0B)}
{60, 40}
iex> EEVM.Database.commit(result.db)
{:error, :commit_not_supported}

Read errors

A callback that returns {:error, reason} aborts execution: the entry point (EEVM.run/2, EEVM.transact/4, EEVM.Block.execute/4, or EEVM.Block.build/4) returns {:error, {:database, reason}}. Called directly, the functions in this module raise EEVM.Database.ReadError.

Deprecated backends

These still work for one minor version:

Summary

Types

An account record: balance, nonce, and code.

An address: a 160-bit integer.

An account as a backend returns it. code may be included to save a code/2 call; when it is, code_hash may be left out.

t()

Callbacks

Returns the account at address, or nil when it doesn't exist.

Enumerates every address with an account or storage, or returns {:error, :unsupported}. Only state roots need it.

Returns the hash of block number, or nil when unknown. BLOCKHASH asks only for blocks the EEVM.BlockEnv doesn't already carry.

Returns the code whose keccak256 hash is code_hash.

Applies a finished set of changes and returns the new backend state. A read-only backend returns {:error, :unsupported}.

Returns the storage word at slot of address (0 when unset).

Enumerates {slot, value} for every set slot of address, or returns {:error, :unsupported}. Only state roots and the EIP-7610 CREATE check need it.

Functions

Declares the EEVM.Database behaviour and defines commit/2, accounts/1, and storage_slots/2 to return {:error, :unsupported}.

Returns every address with an account or storage, overlay included. Needs the backend's accounts/1.

Returns whether address is empty: zero balance and nonce, and no code (EIP-161).

Returns whether an account exists at address.

Returns the hash of block number from the backend, or nil when unknown.

Returns the overlay's writes as an EEVM.StateChanges.

Commits the overlay to the backend with its commit/2 and returns the database with an empty overlay. Returns {:error, :commit_not_supported} when the backend's commit/2 returns {:error, :unsupported}.

Deletes the account at address and all its storage.

Returns %{balance, nonce, code} for address, or nil if it has no account.

Returns the balance of address in wei (0 if it has no account).

Returns the code at address, or <<>>.

Returns the keccak256 hash of the code at address; nil if it has no account.

Returns the nonce of address (0 if it has no account).

Increments the nonce of address.

Wraps a backend module and its state, with an empty overlay.

Replaces the account record at address; missing fields default to zero and no code.

Sets the code at address.

Sets the balance of address.

Sets the nonce of address.

Returns the addresses with storage. Deprecated: use account_addresses/1.

Returns the storage word at key of address (0 when unset).

Returns {slot, value} for every set storage slot of address, overlay included. Needs the backend's storage_slots/2.

Stores a 256-bit word at key of address.

Moves amount wei from from to to.

Types

account()

@type account() :: %{
  optional(:balance) => non_neg_integer(),
  optional(:nonce) => non_neg_integer(),
  optional(:code) => binary()
}

An account record: balance, nonce, and code.

address()

@type address() :: non_neg_integer()

An address: a 160-bit integer.

backend_account()

@type backend_account() :: %{
  :balance => non_neg_integer(),
  :nonce => non_neg_integer(),
  optional(:code_hash) => <<_::256>>,
  optional(:code) => binary()
}

An account as a backend returns it. code may be included to save a code/2 call; when it is, code_hash may be left out.

t()

@type t() :: %EEVM.Database{
  accounts: %{required(address()) => overlay_account() | nil},
  destroyed: MapSet.t(address()),
  impl: module(),
  state: term(),
  storage: %{
    required(address()) => %{required(non_neg_integer()) => non_neg_integer()}
  },
  tries: EEVM.StateRoot.tries() | nil
}

Callbacks

account(state, address)

@callback account(state :: term(), address()) ::
  {:ok, backend_account() | nil} | {:error, term()}

Returns the account at address, or nil when it doesn't exist.

accounts(state)

@callback accounts(state :: term()) ::
  {:ok, Enumerable.t()} | {:error, :unsupported | term()}

Enumerates every address with an account or storage, or returns {:error, :unsupported}. Only state roots need it.

block_hash(state, number)

@callback block_hash(state :: term(), number :: non_neg_integer()) ::
  {:ok, <<_::256>> | nil} | {:error, term()}

Returns the hash of block number, or nil when unknown. BLOCKHASH asks only for blocks the EEVM.BlockEnv doesn't already carry.

code(state, code_hash)

@callback code(state :: term(), code_hash :: <<_::256>>) ::
  {:ok, binary()} | {:error, term()}

Returns the code whose keccak256 hash is code_hash.

commit(state, t)

@callback commit(state :: term(), EEVM.StateChanges.t()) ::
  {:ok, term()} | {:error, :unsupported | term()}

Applies a finished set of changes and returns the new backend state. A read-only backend returns {:error, :unsupported}.

storage(state, address, slot)

@callback storage(state :: term(), address(), slot :: non_neg_integer()) ::
  {:ok, non_neg_integer()} | {:error, term()}

Returns the storage word at slot of address (0 when unset).

storage_slots(state, address)

@callback storage_slots(state :: term(), address()) ::
  {:ok, Enumerable.t()} | {:error, :unsupported | term()}

Enumerates {slot, value} for every set slot of address, or returns {:error, :unsupported}. Only state roots and the EIP-7610 CREATE check need it.

Functions

__using__(opts)

(macro)

Declares the EEVM.Database behaviour and defines commit/2, accounts/1, and storage_slots/2 to return {:error, :unsupported}.

A module that uses it implements only the read callbacks, account/2, code/2, storage/3, and block_hash/2, and overrides a default by defining the callback itself:

defmodule MyApp.RpcDatabase do
  use EEVM.Database

  @impl true
  def account(state, address), do: ...
  # code/2, storage/3, and block_hash/2 likewise; no commit/2,
  # accounts/1, or storage_slots/2.
end

The defaults are ordinary compiled functions, so the module still states what it supports in code. @behaviour EEVM.Database with all seven callbacks keeps working unchanged. The EEVM.Database module docs run a complete read-only adapter.

account_addresses(db)

@spec account_addresses(t()) :: [address()]

Returns every address with an account or storage, overlay included. Needs the backend's accounts/1.

account_empty?(db, address)

@spec account_empty?(t(), address()) :: boolean()

Returns whether address is empty: zero balance and nonce, and no code (EIP-161).

account_exists?(db, address)

@spec account_exists?(t(), address()) :: boolean()

Returns whether an account exists at address.

block_hash(database, number)

@spec block_hash(t(), non_neg_integer()) :: <<_::256>> | nil

Returns the hash of block number from the backend, or nil when unknown.

changes(db)

@spec changes(t()) :: EEVM.StateChanges.t()

Returns the overlay's writes as an EEVM.StateChanges.

Example

iex> db = EEVM.Database.InMemory.new() |> EEVM.Database.set_balance(1, 5)
iex> %{1 => change} = EEVM.Database.changes(db).accounts
iex> {change.balance, change.nonce, change.destroyed?}
{5, 0, false}

commit(db)

@spec commit(t()) :: {:ok, t()} | {:error, term()}

Commits the overlay to the backend with its commit/2 and returns the database with an empty overlay. Returns {:error, :commit_not_supported} when the backend's commit/2 returns {:error, :unsupported}.

delete_account(db, address)

@spec delete_account(t(), address()) :: t()

Deletes the account at address and all its storage.

get_account(db, address)

@spec get_account(t(), address()) :: account() | nil

Returns %{balance, nonce, code} for address, or nil if it has no account.

get_balance(db, address)

@spec get_balance(t(), address()) :: non_neg_integer()

Returns the balance of address in wei (0 if it has no account).

get_code(db, address)

@spec get_code(t(), address()) :: binary()

Returns the code at address, or <<>>.

get_code_hash(db, address)

@spec get_code_hash(t(), address()) :: <<_::256>> | nil

Returns the keccak256 hash of the code at address; nil if it has no account.

get_nonce(db, address)

@spec get_nonce(t(), address()) :: non_neg_integer()

Returns the nonce of address (0 if it has no account).

increment_nonce(db, address)

@spec increment_nonce(t(), address()) :: t()

Increments the nonce of address.

new(impl, state)

@spec new(module(), term()) :: t()

Wraps a backend module and its state, with an empty overlay.

A module that implements only the deprecated pre-1.0 callbacks is wrapped in EEVM.Database.Legacy.

Example

iex> db = EEVM.Database.new(EEVM.Database.InMemory, EEVM.Database.InMemory.init())
iex> EEVM.Database.get_balance(db, 0x1234)
0

put_account(db, address, account)

@spec put_account(t(), address(), account()) :: t()

Replaces the account record at address; missing fields default to zero and no code.

put_code(db, address, code)

@spec put_code(t(), address(), binary()) :: t()

Sets the code at address.

set_balance(db, address, balance)

@spec set_balance(t(), address(), non_neg_integer()) :: t()

Sets the balance of address.

set_nonce(db, address, nonce)

@spec set_nonce(t(), address(), non_neg_integer()) :: t()

Sets the nonce of address.

storage_addresses(db)

This function is deprecated. Use EEVM.Database.account_addresses/1.
@spec storage_addresses(t()) :: [address()]

Returns the addresses with storage. Deprecated: use account_addresses/1.

storage_load(db, address, key)

@spec storage_load(t(), address(), non_neg_integer()) :: non_neg_integer()

Returns the storage word at key of address (0 when unset).

storage_slots(db, address)

@spec storage_slots(t(), address()) :: [{non_neg_integer(), non_neg_integer()}]

Returns {slot, value} for every set storage slot of address, overlay included. Needs the backend's storage_slots/2.

storage_store(db, address, key, value)

@spec storage_store(t(), address(), non_neg_integer(), non_neg_integer()) :: t()

Stores a 256-bit word at key of address.

transfer(db, from, to, amount)

@spec transfer(t(), address(), address(), non_neg_integer()) ::
  {:ok, t()} | {:error, :insufficient_balance}

Moves amount wei from from to to.