message:
feat: add eth txn hashes to contract events
update code from PR comments
update ethevents migration and db design docs
rootchain/childchain -> root_chain/child_chain per code review comments

diff:
@@ -44,6 +44,7 @@ defmodule OMG.State.Core do
}
@type deposit() :: %{
+ root_chain_txhash: Crypto.hash_t(),
blknum: non_neg_integer(),
currency: Crypto.address_t(),
owner: Crypto.address_t(),

diff:
@@ -263,8 +263,8 @@ defmodule OMG.Eth do
Map.new(Enum.zip(arg_names, function_inputs))
end
- defp common_parse_event(result, %{"blockNumber" => eth_height}) do
- Map.put(result, :eth_height, int_from_hex(eth_height))
+ defp common_parse_event(result, %{"blockNumber" => eth_height, "transactionHash" => root_chain_txhash}) do
+ Map.merge(result, %{eth_height: int_from_hex(eth_height), root_chain_txhash: from_hex(root_chain_txhash)})
end
defp get_signer_passphrase("0x00a329c0648769a73afac7f9381e08fb43dbea72") do

diff:
@@ -326,9 +326,8 @@ defmodule OMG.Eth.RootChain do
decode_exit_started = decode_exit_started(log)
args = [:utxo_pos, :output_tx, :output_tx_inclusion_proof]
types = [:uint192, :bytes, :bytes]
- hash = from_hex(log["transactionHash"])
- transaction_hash = Eth.get_call_data(hash, "startStandardExit", args, types)
- Map.put(decode_exit_started, :call_data, transaction_hash)
+ call_data = Eth.get_call_data(decode_exit_started.root_chain_txhash, "startStandardExit", args, types)
+ Map.put(decode_exit_started, :call_data, call_data)
end)
{:ok, exits}

diff:
defmodule OMG.Watcher.DB.EthEvent do
@moduledoc """
- Ecto schema for events logged by Ethereum: deposits and exits
+ Ecto schema for events logged by Ethereum
"""
use Ecto.Schema
+ import Ecto.Changeset
alias OMG.Crypto
+ alias OMG.Eth.Encoding
alias OMG.Utxo
alias OMG.Watcher.DB
require Utxo
- @primary_key {:hash, :binary, []}
- @derive {Phoenix.Param, key: :hash}
+ @primary_key false
schema "ethevents" do
- field(:blknum, :integer)
- field(:txindex, :integer)
- field(:event_type, OMG.Watcher.DB.Types.AtomType)
+ field(:root_chain_txhash, :binary, primary_key: true)
+ field(:event_type, OMG.Watcher.DB.Types.AtomType, primary_key: true)
- has_one(:created_utxo, DB.TxOutput, foreign_key: :creating_deposit)
- has_one(:exited_utxo, DB.TxOutput, foreign_key: :spending_exit)
+ field(:root_chain_txhash_event, :binary)
+
+ many_to_many(
+ :txoutputs,
+ DB.TxOutput,
+ join_through: "ethevents_txoutputs",
+ join_keys: [root_chain_txhash_event: :root_chain_txhash_event, child_chain_utxohash: :child_chain_utxohash]
+ )
+
+ timestamps([type: :utc_datetime])
end
@doc """
@@ -44,26 +52,38 @@ defmodule OMG.Watcher.DB.EthEvent do
end
@spec insert_deposit!(OMG.State.Core.deposit()) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()}
- defp insert_deposit!(%{blknum: blknum, owner: owner, currency: currency, amount: amount}) do
- {:ok, _} =
- if existing_deposit = DB.Repo.get(__MODULE__, deposit_key(blknum)) != nil,
- do: {:ok, existing_deposit},
- else:
+ defp insert_deposit!(%{root_chain_txhash: root_chain_txhash, blknum: blknum, owner: owner, currency: currency, amount: amount}) do
+ event_type = :deposit
+ position = Utxo.position(blknum, 0, 0)
+ root_chain_txhash_event = generate_root_chain_txhash_event(root_chain_txhash, event_type)
+
+ case get(root_chain_txhash_event) do
+ nil ->
%__MODULE__{
- hash: deposit_key(blknum),
- blknum: blknum,
- txindex: 0,
+ root_chain_txhash_event: root_chain_txhash_event,
+ root_chain_txhash: root_chain_txhash,
event_type: :deposit,
- created_utxo: %DB.TxOutput{
+
+ # a deposit from the root chain will only ever have 1 childchain txoutput object
+ txoutputs: [%DB.TxOutput{
+ child_chain_utxohash: generate_child_chain_utxohash(position),
blknum: blknum,
txindex: 0,
oindex: 0,
owner: owner,
currency: currency,
amount: amount
- }
+ }]
}
|> DB.Repo.insert()
+
+ # an ethevents row just got inserted, now return the ethevent with all populated fields including
+ # those populated by the DB (eg: inserted_at, updated_at, ...)
+ {:ok, get(root_chain_txhash_event)}
+
+ existing_deposit ->
+ {:ok, existing_deposit}
+ end
end
@doc """
@@ -72,44 +92,64 @@ defmodule OMG.Watcher.DB.EthEvent do
@spec insert_exits!([non_neg_integer()]) :: :ok
def insert_exits!(exits) do
exits
- |> Stream.map(&utxo_pos_from_exit_event/1)
+ |> Stream.map(&utxo_exit_from_exit_event/1)
|> Enum.each(&insert_exit!/1)
end
- @spec utxo_pos_from_exit_event(%{call_data: %{utxo_pos: pos_integer()}}) :: Utxo.Position.t()
- defp utxo_pos_from_exit_event(%{call_data: %{utxo_pos: utxo_pos}}), do: Utxo.Position.decode!(utxo_pos)
+ @spec utxo_exit_from_exit_event(%{call_data: %{utxo_pos: pos_integer()}, root_chain_txhash: charlist()})
+ :: %{root_chain_txhash: binary(), decoded_utxo_position: Utxo.Position.t()}
+ defp utxo_exit_from_exit_event(%{call_data: %{utxo_pos: utxo_pos}, root_chain_txhash: root_chain_txhash}) do
+ %{root_chain_txhash: root_chain_txhash, decoded_utxo_position: Utxo.Position.decode!(utxo_pos)}
+ end
- @spec insert_exit!(Utxo.Position.t()) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()}
- defp insert_exit!(Utxo.position(blknum, txindex, _oindex) = position) do
- {:ok, _} =
- if existing_exit = get(exit_key(position)) != nil do
- {:ok, existing_exit}
- else
- utxo = DB.TxOutput.get_by_position(position)
+ @spec insert_exit!(%{root_chain_txhash: binary(), decoded_utxo_position: Utxo.Position.t()})
+ :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()}
+ defp insert_exit!(%{root_chain_txhash: root_chain_txhash, decoded_utxo_position: decoded_utxo_position}) do
+ event_type = :standard_exit
+ root_chain_txhash_event = generate_root_chain_txhash_event(root_chain_txhash, event_type)
- %__MODULE__{
- hash: exit_key(position),
- blknum: blknum,
- txindex: txindex,
- event_type: :exit,
- exited_utxo: utxo
+ case get(root_chain_txhash_event) do
+ nil ->
+ ethevent = %__MODULE__{
+ root_chain_txhash_event: root_chain_txhash_event,
+ root_chain_txhash: root_chain_txhash,
+ event_type: :standard_exit
}
- |> DB.Repo.insert()
+
+ DB.TxOutput.get_by_position(decoded_utxo_position)
+ |> DB.Repo.preload(:ethevents)
+ |> txoutput_changeset(%{child_chain_utxohash: generate_child_chain_utxohash(decoded_utxo_position)}, ethevent)
+ |> DB.Repo.update()
+
+ # a txoutput row just got updated, but we need to return the associated ethevent with all populated
+ # fields including those populated by the DB (eg: inserted_at, updated_at, ...)
+ {:ok, get(root_chain_txhash_event)}
+
+ existing_exit ->
+ {:ok, existing_exit}
end
end
+ def txoutput_changeset(txoutput, params, ethevent) do
+ fields = [:blknum, :txindex, :oindex, :owner, :amount, :currency, :child_chain_utxohash]
+
+ txoutput
+ |> cast(params, fields)
+ |> put_assoc(:ethevents, txoutput.ethevents ++ [ethevent])
+ |> validate_required(fields)
+ end
+
@doc """
- Good candidate for deposit/exit primary key is a pair (Utxo.position, event_type).
- Switching to composite key requires careful consideration of data types and schema change,
- so for now, we'd go with artificial key
+ Generate a unique child_chain_utxohash from the Utxo.position
"""
- @spec generate_unique_key(Utxo.Position.t(), :deposit | :exit) :: OMG.Crypto.hash_t()
- def generate_unique_key(position, type) do
- "<#{position |> Utxo.Position.encode()}:#{type}>" |> Crypto.hash()
+ @spec generate_child_chain_utxohash(Utxo.Position.t()) :: OMG.Crypto.hash_t()
+ def generate_child_chain_utxohash(position) do
+ "<#{position |> Utxo.Position.encode()}>" |> Crypto.hash()
end
- defp deposit_key(blknum), do: generate_unique_key(Utxo.position(blknum, 0, 0), :deposit)
- defp exit_key(position), do: generate_unique_key(position, :exit)
+ defp generate_root_chain_txhash_event(root_chain_txhash, event_type) do
+ Encoding.to_hex(root_chain_txhash) <> Atom.to_string(event_type) |> Crypto.hash()
+ end
- defp get(hash), do: DB.Repo.get(__MODULE__, hash)
+ defp get(root_chain_txhash_event), do: DB.Repo.get_by(__MODULE__, root_chain_txhash_event: root_chain_txhash_event)
end

diff:
@@ -51,12 +51,19 @@ defmodule OMG.Watcher.DB.TxOutput do
field(:currency, :binary)
field(:proof, :binary)
field(:spending_tx_oindex, :integer)
+ field(:child_chain_utxohash, :binary)
belongs_to(:creating_transaction, DB.Transaction, foreign_key: :creating_txhash, references: :txhash, type: :binary)
- belongs_to(:deposit, DB.EthEvent, foreign_key: :creating_deposit, references: :hash, type: :binary)
-
belongs_to(:spending_transaction, DB.Transaction, foreign_key: :spending_txhash, references: :txhash, type: :binary)
- belongs_to(:exit, DB.EthEvent, foreign_key: :spending_exit, references: :hash, type: :binary)
+
+ many_to_many(
+ :ethevents,
+ DB.EthEvent,
+ join_through: "ethevents_txoutputs",
+ join_keys: [child_chain_utxohash: :child_chain_utxohash, root_chain_txhash_event: :root_chain_txhash_event]
+ )
+
+ timestamps([type: :utc_datetime])
end
@spec compose_utxo_exit(Utxo.Position.t()) :: {:ok, exit_t()} | {:error, :utxo_not_found}
@@ -76,9 +83,10 @@ defmodule OMG.Watcher.DB.TxOutput do
query =
from(
txo in __MODULE__,
- where: txo.owner == ^owner and is_nil(txo.spending_txhash) and is_nil(txo.spending_exit),
- order_by: [asc: :blknum, asc: :txindex, asc: :oindex],
- preload: [:creating_transaction, :deposit]
+ preload: [:ethevents],
+ left_join: ethevent in assoc(txo, :ethevents),
+ where: txo.owner == ^owner and is_nil(txo.spending_txhash) and (is_nil(ethevent) or ethevent.event_type != ^:standard_exit),
+ order_by: [asc: :blknum, asc: :txindex, asc: :oindex]
)
Repo.all(query)
@@ -88,10 +96,11 @@ defmodule OMG.Watcher.DB.TxOutput do
def get_balance(owner) do
query =
from(
- t in __MODULE__,
- where: t.owner == ^owner and is_nil(t.spending_txhash) and is_nil(t.spending_exit),
- group_by: t.currency,
- select: {t.currency, sum(t.amount)}
+ txo in __MODULE__,
+ left_join: ethevent in assoc(txo, :ethevents),
+ where: txo.owner == ^owner and is_nil(txo.spending_txhash) and (is_nil(ethevent) or ethevent.event_type != ^:standard_exit),
+ group_by: txo.currency,
+ select: {txo.currency, sum(txo.amount)}
)
Repo.all(query)

diff:
+
+defmodule OMG.Watcher.DB.Repo.Migrations.AlterTxOutputsTableAddRootchainTxnHashDepositAndExitColumns do
+ use Ecto.Migration
+
+ # non-backward compatible migration, thus cannot use `change/0`
+
+ def up do
+ drop constraint(:txoutputs, "txoutputs_creating_deposit_fkey")
+ drop constraint(:txoutputs, "txoutputs_spending_exit_fkey")
+ drop constraint(:ethevents, "ethevents_pkey")
+
+ flush()
+
+ # drop ethevents table and rebuild it as this table is currently unused for all practical purposes.
+ # when getting utxos we filter on txoutputs.creating_deposit is nil and txoutputs.spending is nil and
+ # never query/join with the ethevents table.
+ # dropping is easiest here because we are altering what the primary key is
+
+ drop table(:ethevents)
+
+ create table(:ethevents, primary_key: false) do
+ add(:root_chain_txhash, :binary, primary_key: true)
+ add(:event_type, :string, size: 124, primary_key: true)
+
+ add(:root_chain_txhash_event, :binary)
+
+ timestamps([type: :utc_datetime])
+ end
+
+ create index(:ethevents, :root_chain_txhash)
+ create index(:ethevents, :event_type)
+ create unique_index(:ethevents, :root_chain_txhash_event)
+
+ # how to do this in ecto correctly? do it manually
+ execute("ALTER TABLE ethevents ALTER COLUMN inserted_at SET DEFAULT (now() at time zone 'utc');");
+ execute("ALTER TABLE ethevents ALTER COLUMN updated_at SET DEFAULT (now() at time zone 'utc');");
+
+ alter table(:txoutputs) do
+ add(:child_chain_utxohash, :binary)
+ end
+
+ create unique_index(:txoutputs, :child_chain_utxohash)
+
+ flush()
+
+ # backfill child_chain_utxohash with values from either creating_deposit or spending_exit
+ execute """
+ UPDATE txoutputs as t
+ SET child_chain_utxohash =
+ (SELECT
+ CASE WHEN creating_deposit IS NULL THEN spending_exit
+ WHEN spending_exit IS NULL THEN creating_deposit
+ ELSE creating_deposit || spending_exit
+ END AS txhash
+ FROM txoutputs as t_inner
+ WHERE t.creating_deposit = t_inner.creating_deposit OR t.spending_exit = t_inner.spending_exit);
+ """
+
+ alter table(:txoutputs) do
+ remove(:creating_deposit)
+ remove(:spending_exit)
+
+ timestamps([type: :utc_datetime])
+ end
+
+ # how to do this in ecto correctly? do it manually
+ execute("ALTER TABLE txoutputs ALTER COLUMN inserted_at SET DEFAULT (now() at time zone 'utc');");
+ execute("ALTER TABLE txoutputs ALTER COLUMN updated_at SET DEFAULT (now() at time zone 'utc');");
+
+ create table(:ethevents_txoutputs, primary_key: false) do
+ add(:root_chain_txhash_event, references(:ethevents, column: :root_chain_txhash_event, type: :binary, on_delete: :restrict), primary_key: true)
+ add(:child_chain_utxohash, references(:txoutputs, column: :child_chain_utxohash, type: :binary, on_delete: :restrict), primary_key: true)
+
+ timestamps([type: :utc_datetime])
+ end
+
+ # how to do this in ecto correctly? do it manually
+ execute("ALTER TABLE ethevents_txoutputs ALTER COLUMN inserted_at SET DEFAULT (now() at time zone 'utc');");
+ execute("ALTER TABLE ethevents_txoutputs ALTER COLUMN updated_at SET DEFAULT (now() at time zone 'utc');");
+
+ create index(:ethevents_txoutputs, :root_chain_txhash_event)
+ create index(:ethevents_txoutputs, :child_chain_utxohash)
+ end
+
+ def down do
+ # non-backward compatible migration, thus cannot use `change/0`
+ # no-op
+ end
+end

diff:
@@ -4,6 +4,8 @@ Watcher benefits from two databases approach:
* leveldb - key-value database that child chain state uses for transaction validation
* watcherDb - PostgreSQL database that stores transactions and contains data needed for challenges and exits. It provides user interface with the data of user's concern (e.g: did I pay the electricity bill, who sent me money last week).
+[PostgreSQL Schema Diagram](https://docs.google.com/drawings/d/14_0bfUGGWarndNWwpzA2Nznll4PHLbefvQy05B2LB38/edit?usp=sharing)
+
## The blocks table
Stores data about blocks: hash, timestamp when block was mined, Ethereum height is was published on.
@@ -42,16 +44,32 @@ Stores inputs and outputs of transactions. Utxo is a record in `txoutputs` table
|amount|numeric(81,0)||
|currency|bytea||
|proof|bytea||
+|child_chain_utxohash|bytea|UI|
+|inserted_at|datetime|UTC (w/o TZ)|
+|updated_at|datetime|UTC (w/o TZ)|
## The Ethereum events table
Stores events logged in root contract, such as _deposits_ or _exits_.
|**ethevents**|||
|:-:|:-|:-|
-|hash|bytea|Pk|
-|blknum|bigint||
-|txindex|integer||
-|event_type|varchar(124)||
+|root_chain_txhash|bytea|Pk(root_chain_txhash, event_type)|
+|event_type|varchar(124)|Pk^|
+|root_chain_txhash_event|bytea|UI|
+|inserted_at|datetime|UTC (w/o TZ)|
+|updated_at|datetime|UTC (w/o TZ)|
+
+
+## The ethevents_txoutputs table
+A table for many-to-many relationships between Ethereum events and UTXOs.
+
+|**ethevents_txoutputs**|||
+|:-:|:-|:-|
+|root_chain_txhash_event|bytea|Pk(root_chain_txhash_event, child_chain_utxohash), FK(ethevents, (root_chain_txhash_event))|
+|child_chain_utxohash|bytea|Pk^, FK(txoutputs, (child_chain_utxohash))|
+|inserted_at|datetime|UTC (w/o TZ)|
+|updated_at|datetime|UTC (w/o TZ)|
+
## Examples of queries against the tables
@@ -116,3 +134,22 @@ select t.txhash
from transactions t
where t.blknum == @blknum
```
+
+### 8. get all ethereum events for existing utxos
+```
+SELECT CASE WHEN t.child_chain_utxohash IS NULL THEN NULL
+ WHEN t.child_chain_utxohash IS NOT NULL THEN concat('0x', encode(t.child_chain_utxohash::bytea, 'hex'))
+ END AS child_chain_utxohash,
+ CASE WHEN e.root_chain_txhash_event IS NULL THEN NULL
+ WHEN e.root_chain_txhash_event IS NOT NULL THEN concat('0x', encode(e.root_chain_txhash_event::bytea, 'hex'))
+ END AS root_chain_txhash_event,
+ CASE WHEN e.root_chain_txhash IS NULL THEN NULL
+ WHEN e.root_chain_txhash IS NOT NULL THEN concat('0x', encode(e.root_chain_txhash::bytea, 'hex'))
+ END AS root_chain_txhash,
+ blknum, txindex, oindex,
+ amount,
+ event_type
+FROM txoutputs t
+ LEFT OUTER JOIN ethevents_txoutputs et ON t.child_chain_utxohash = et.child_chain_utxohash
+ LEFT OUTER JOIN ethevents e ON et.root_chain_txhash_event = e.root_chain_txhash_event;
+```


message: change eth_events primary key to (root_chain_txhash, log_index)

change_type: MODIFY

diff:
@@ -45,6 +45,7 @@ defmodule OMG.State.Core do
@type deposit() :: %{
root_chain_txhash: Crypto.hash_t(),
+ log_index: non_neg_integer(),
blknum: non_neg_integer(),
currency: Crypto.address_t(),
owner: Crypto.address_t(),

diff:
@@ -263,8 +263,8 @@ defmodule OMG.Eth do
Map.new(Enum.zip(arg_names, function_inputs))
end
- defp common_parse_event(result, %{"blockNumber" => eth_height, "transactionHash" => root_chain_txhash}) do
- Map.merge(result, %{eth_height: int_from_hex(eth_height), root_chain_txhash: from_hex(root_chain_txhash)})
+ defp common_parse_event(result, %{"blockNumber" => eth_height, "transactionHash" => root_chain_txhash, "logIndex" => log_index}) do
+ Map.merge(result, %{eth_height: int_from_hex(eth_height), root_chain_txhash: from_hex(root_chain_txhash), log_index: int_from_hex(log_index)})
end
defp get_signer_passphrase("0x00a329c0648769a73afac7f9381e08fb43dbea72") do

diff:
@@ -16,9 +16,11 @@ defmodule OMG.Watcher.DB.EthEvent do
@moduledoc """
Ecto schema for events logged by Ethereum
"""
- use Ecto.Schema
+ import Ecto.Query, only: [from: 2]
import Ecto.Changeset
+ use Ecto.Schema
+
alias OMG.Crypto
alias OMG.Eth.Encoding
alias OMG.Utxo
@@ -29,7 +31,9 @@ defmodule OMG.Watcher.DB.EthEvent do
@primary_key false
schema "ethevents" do
field(:root_chain_txhash, :binary, primary_key: true)
- field(:event_type, OMG.Watcher.DB.Types.AtomType, primary_key: true)
+ field(:log_index, :integer, primary_key: true)
+
+ field(:event_type, OMG.Watcher.DB.Types.AtomType)
field(:root_chain_txhash_event, :binary)
@@ -52,17 +56,19 @@ defmodule OMG.Watcher.DB.EthEvent do
end
@spec insert_deposit!(OMG.State.Core.deposit()) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()}
- defp insert_deposit!(%{root_chain_txhash: root_chain_txhash, blknum: blknum, owner: owner, currency: currency, amount: amount}) do
+ defp insert_deposit!(%{root_chain_txhash: root_chain_txhash, log_index: log_index, blknum: blknum, owner: owner,
+ currency: currency, amount: amount}) do
event_type = :deposit
position = Utxo.position(blknum, 0, 0)
- root_chain_txhash_event = generate_root_chain_txhash_event(root_chain_txhash, event_type)
+ root_chain_txhash_event = generate_root_chain_txhash_event(root_chain_txhash, log_index)
case get(root_chain_txhash_event) do
nil ->
%__MODULE__{
root_chain_txhash_event: root_chain_txhash_event,
+ log_index: log_index,
root_chain_txhash: root_chain_txhash,
- event_type: :deposit,
+ event_type: event_type,
# a deposit from the root chain will only ever have 1 childchain txoutput object
txoutputs: [%DB.TxOutput{
@@ -96,28 +102,28 @@ defmodule OMG.Watcher.DB.EthEvent do
|> Enum.each(&insert_exit!/1)
end
- @spec utxo_exit_from_exit_event(%{call_data: %{utxo_pos: pos_integer()}, root_chain_txhash: charlist()})
- :: %{root_chain_txhash: binary(), decoded_utxo_position: Utxo.Position.t()}
- defp utxo_exit_from_exit_event(%{call_data: %{utxo_pos: utxo_pos}, root_chain_txhash: root_chain_txhash}) do
- %{root_chain_txhash: root_chain_txhash, decoded_utxo_position: Utxo.Position.decode!(utxo_pos)}
+ @spec utxo_exit_from_exit_event(%{call_data: %{utxo_pos: pos_integer()}, root_chain_txhash: charlist(), log_index: non_neg_integer()})
+ :: %{root_chain_txhash: binary(), log_index: non_neg_integer(), decoded_utxo_position: Utxo.Position.t()}
+ defp utxo_exit_from_exit_event(%{call_data: %{utxo_pos: utxo_pos}, root_chain_txhash: root_chain_txhash, log_index: log_index}) do
+ %{root_chain_txhash: root_chain_txhash, log_index: log_index, decoded_utxo_position: Utxo.Position.decode!(utxo_pos)}
end
- @spec insert_exit!(%{root_chain_txhash: binary(), decoded_utxo_position: Utxo.Position.t()})
+ @spec insert_exit!(%{root_chain_txhash: binary(), log_index: non_neg_integer(), decoded_utxo_position: Utxo.Position.t()})
:: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()}
- defp insert_exit!(%{root_chain_txhash: root_chain_txhash, decoded_utxo_position: decoded_utxo_position}) do
+ defp insert_exit!(%{root_chain_txhash: root_chain_txhash, log_index: log_index, decoded_utxo_position: decoded_utxo_position}) do
event_type = :standard_exit
- root_chain_txhash_event = generate_root_chain_txhash_event(root_chain_txhash, event_type)
+ root_chain_txhash_event = generate_root_chain_txhash_event(root_chain_txhash, log_index)
case get(root_chain_txhash_event) do
nil ->
ethevent = %__MODULE__{
root_chain_txhash_event: root_chain_txhash_event,
+ log_index: log_index,
root_chain_txhash: root_chain_txhash,
- event_type: :standard_exit
+ event_type: event_type
}
DB.TxOutput.get_by_position(decoded_utxo_position)
- |> DB.Repo.preload(:ethevents)
|> txoutput_changeset(%{child_chain_utxohash: generate_child_chain_utxohash(decoded_utxo_position)}, ethevent)
|> DB.Repo.update()
@@ -147,9 +153,15 @@ defmodule OMG.Watcher.DB.EthEvent do
"<#{position |> Utxo.Position.encode()}>" |> Crypto.hash()
end
- defp generate_root_chain_txhash_event(root_chain_txhash, event_type) do
- Encoding.to_hex(root_chain_txhash) <> Atom.to_string(event_type) |> Crypto.hash()
+ def generate_root_chain_txhash_event(root_chain_txhash, log_index) do
+ Encoding.to_hex(root_chain_txhash) <> Integer.to_string(log_index) |> Crypto.hash()
end
- defp get(root_chain_txhash_event), do: DB.Repo.get_by(__MODULE__, root_chain_txhash_event: root_chain_txhash_event)
+ # preload txoutputs in a single query as there will not be a large number of them
+ def get(root_chain_txhash_event) do
+ DB.Repo.one from ethevent in __MODULE__,
+ where: ethevent.root_chain_txhash_event == ^root_chain_txhash_event,
+ left_join: txoutputs in assoc(ethevent, :txoutputs),
+ preload: [txoutputs: txoutputs]
+ end
end

diff:
@@ -16,6 +16,8 @@ defmodule OMG.Watcher.DB.TxOutput do
@moduledoc """
Ecto schema for transaction's output or input
"""
+ import Ecto.Query, only: [from: 2]
+
use Ecto.Schema
alias OMG.State.Transaction
@@ -74,18 +76,29 @@ defmodule OMG.Watcher.DB.TxOutput do
# TODO: Make use of Block API's block.get when available
do: DB.Transaction.get_by_blknum(blknum) |> Core.compose_output_exit(decoded_utxo_pos)
+ # preload ethevents in a single query as there will not be a large number of them
@spec get_by_position(Utxo.Position.t()) :: map() | nil
def get_by_position(Utxo.position(blknum, txindex, oindex)) do
- Repo.get_by(__MODULE__, blknum: blknum, txindex: txindex, oindex: oindex)
+ DB.Repo.one from txoutput in __MODULE__,
+ preload: [:ethevents],
+ left_join: ethevent in assoc(txoutput, :ethevents),
+ where: txoutput.blknum == ^blknum and txoutput.txindex == ^txindex and txoutput.oindex == ^oindex
end
def get_utxos(owner) do
query =
from(
- txo in __MODULE__,
+ txoutput in __MODULE__,
preload: [:ethevents],
- left_join: ethevent in assoc(txo, :ethevents),
- where: txo.owner == ^owner and is_nil(txo.spending_txhash) and (is_nil(ethevent) or ethevent.event_type != ^:standard_exit),
+ left_join: ethevent in assoc(txoutput, :ethevents),
+ where: txoutput.owner == ^owner
+ and is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment("
+ NOT EXISTS (SELECT 1
+ FROM ethevents_txoutputs AS etfrag
+ JOIN ethevents AS efrag ON
+ etfrag.root_chain_txhash_event=efrag.root_chain_txhash_event
+ AND efrag.event_type NOT IN (?)
+ AND etfrag.child_chain_utxohash = ?)", "standard_exit", txoutput.child_chain_utxohash)),
order_by: [asc: :blknum, asc: :txindex, asc: :oindex]
)
@@ -96,11 +109,17 @@ defmodule OMG.Watcher.DB.TxOutput do
def get_balance(owner) do
query =
from(
- txo in __MODULE__,
- left_join: ethevent in assoc(txo, :ethevents),
- where: txo.owner == ^owner and is_nil(txo.spending_txhash) and (is_nil(ethevent) or ethevent.event_type != ^:standard_exit),
- group_by: txo.currency,
- select: {txo.currency, sum(txo.amount)}
+ txoutput in __MODULE__,
+ left_join: ethevent in assoc(txoutput, :ethevents),
+ where: txoutput.owner == ^owner and is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment("
+ NOT EXISTS (SELECT 1
+ FROM ethevents_txoutputs AS etfrag
+ JOIN ethevents AS efrag ON
+ etfrag.root_chain_txhash_event=efrag.root_chain_txhash_event
+ AND efrag.event_type NOT IN (?)
+ AND etfrag.child_chain_utxohash = ?)", "standard_exit", txoutput.child_chain_utxohash)),
+ group_by: txoutput.currency,
+ select: {txoutput.currency, sum(txoutput.amount)}
)
Repo.all(query)

diff:
@@ -20,7 +20,9 @@ defmodule OMG.Watcher.DB.Repo.Migrations.AlterTxOutputsTableAddRootchainTxnHashD
create table(:ethevents, primary_key: false) do
add(:root_chain_txhash, :binary, primary_key: true)
- add(:event_type, :string, size: 124, primary_key: true)
+ add(:log_index, :int, primary_key: true)
+
+ add(:event_type, :string, size: 124)
add(:root_chain_txhash_event, :binary)
@@ -28,7 +30,7 @@ defmodule OMG.Watcher.DB.Repo.Migrations.AlterTxOutputsTableAddRootchainTxnHashD
end
create index(:ethevents, :root_chain_txhash)
- create index(:ethevents, :event_type)
+ create index(:ethevents, :log_index)
create unique_index(:ethevents, :root_chain_txhash_event)
# how to do this in ecto correctly? do it manually

diff:
@@ -17,7 +17,9 @@ defmodule OMG.Watcher.DB.EthEventTest do
use ExUnit.Case, async: false
use OMG.Fixtures
+ alias OMG.Crypto
alias OMG.Utxo
+ alias OMG.Utxo.Position
alias OMG.Watcher.DB
require Utxo
@@ -26,84 +28,217 @@ defmodule OMG.Watcher.DB.EthEventTest do
@tag fixtures: [:phoenix_ecto_sandbox]
test "insert deposits: creates deposit event and utxo" do
- owner = <<1::160>>
- expected_hash = DB.EthEvent.generate_unique_key(Utxo.position(1, 0, 0), :deposit)
- DB.EthEvent.insert_deposits!([%{blknum: 1, owner: owner, currency: @eth, amount: 1}])
-
- [event] = DB.Repo.all(DB.EthEvent)
- assert %DB.EthEvent{blknum: 1, txindex: 0, event_type: :deposit, hash: ^expected_hash} = event
-
- utxo = DB.TxOutput.get_by_position(Utxo.position(1, 0, 0))
+ expected_root_chain_txnhash = Crypto.hash(<<1::256>>)
+ expected_log_index = 0
+ expected_event_type = :deposit
+
+ root_chain_txhash_event = DB.EthEvent.generate_root_chain_txhash_event(expected_root_chain_txnhash, expected_log_index)
+ expected_child_chain_utxohash = DB.EthEvent.generate_child_chain_utxohash(Utxo.position(1, 0, 0))
+
+ expected_blk_num = 1
+ expected_txindex = 0
+ expected_oindex = 0
+ expected_owner = <<1::160>>
+ expected_currency = @eth
+ expected_amount = 1
+
+ assert :ok =
+ DB.EthEvent.insert_deposits!([%{
+ root_chain_txhash: expected_root_chain_txnhash,
+ log_index: expected_log_index,
+ blknum: expected_blk_num,
+ owner: expected_owner,
+ currency: expected_currency,
+ amount: expected_amount
+ }])
+
+ event = DB.EthEvent.get(root_chain_txhash_event)
+
+ assert %DB.EthEvent{
+ root_chain_txhash: ^expected_root_chain_txnhash,
+ log_index: ^expected_log_index,
+ event_type: ^expected_event_type
+ } = event
+
+ # check ethevent side of relationship
+ assert length(event.txoutputs) == 1
+ assert [%DB.TxOutput{
+ blknum: ^expected_blk_num,
+ txindex: ^expected_txindex,
+ oindex: ^expected_oindex,
+ owner: ^expected_owner,
+ amount: ^expected_amount,
+ currency: ^expected_currency,
+
+ creating_txhash: nil,
+ spending_txhash: nil,
+
+ spending_tx_oindex: nil,
+ proof: nil,
+
+ child_chain_utxohash: ^expected_child_chain_utxohash
+ } | _tail ] = event.txoutputs
+
+ # check txoutput side of relationship
+ txoutput = DB.TxOutput.get_by_position(Utxo.position(expected_blk_num, expected_txindex, expected_oindex))
assert %DB.TxOutput{
- blknum: 1,
- txindex: 0,
- oindex: 0,
- owner: ^owner,
- currency: @eth,
- amount: 1,
- creating_deposit: ^expected_hash
- } = utxo
+ blknum: ^expected_blk_num,
+ txindex: ^expected_txindex,
+ oindex: ^expected_oindex,
+ owner: ^expected_owner,
+ amount: ^expected_amount,
+ currency: ^expected_currency,
+
+ creating_txhash: nil,
+ spending_txhash: nil,
+
+ spending_tx_oindex: nil,
+ proof: nil,
+
+ child_chain_utxohash: ^expected_child_chain_utxohash
+ } = txoutput
+
+ assert length(txoutput.ethevents) == 1
+ assert [%DB.EthEvent{
+ root_chain_txhash: ^expected_root_chain_txnhash,
+ log_index: ^expected_log_index,
+ event_type: ^expected_event_type
+ } | _tail ] = txoutput.ethevents
end
@tag fixtures: [:phoenix_ecto_sandbox, :alice]
test "insert deposits: creates deposits and retrieves them by hash", %{alice: alice} do
- :ok =
- DB.EthEvent.insert_deposits!([
- %{blknum: 1, owner: alice.addr, currency: @eth, amount: 1},
- %{blknum: 1000, owner: alice.addr, currency: @eth, amount: 2},
- %{blknum: 2013, owner: alice.addr, currency: @eth, amount: 3}
- ])
+ expected_event_type = :deposit
+ expected_owner = alice.addr
+ expected_currency = @eth
+ expected_log_index = 0
+ expected_amount = 1
- hash1 = DB.EthEvent.generate_unique_key(Utxo.position(1, 0, 0), :deposit)
+ expected_root_chain_txhash_1 = Crypto.hash(<<2::256>>)
+ expected_root_chain_txhash_event_1 = DB.EthEvent.generate_root_chain_txhash_event(expected_root_chain_txhash_1, expected_log_index)
+ expected_blk_num_1 = 1
- assert %DB.EthEvent{blknum: 1, txindex: 0, event_type: :deposit, hash: ^hash1} = DB.Repo.get(DB.EthEvent, hash1)
+ expected_root_chain_txhash_2 = Crypto.hash(<<3::256>>)
+ expected_root_chain_txhash_event_2 = DB.EthEvent.generate_root_chain_txhash_event(expected_root_chain_txhash_2, expected_log_index)
+ expected_blk_num_2 = 1000
- hash2 = DB.EthEvent.generate_unique_key(Utxo.position(1000, 0, 0), :deposit)
+ expected_root_chain_txhash_3 = Crypto.hash(<<4::256>>)
+ expected_root_chain_txhash_event_3 = DB.EthEvent.generate_root_chain_txhash_event(expected_root_chain_txhash_3, expected_log_index)
+ expected_blk_num_3 = 2000
- assert %DB.EthEvent{blknum: 1000, txindex: 0, event_type: :deposit, hash: ^hash2} = DB.Repo.get(DB.EthEvent, hash2)
-
- hash3 = DB.EthEvent.generate_unique_key(Utxo.position(2013, 0, 0), :deposit)
-
- assert %DB.EthEvent{blknum: 2013, txindex: 0, event_type: :deposit, hash: ^hash3} = DB.Repo.get(DB.EthEvent, hash3)
+ assert :ok =
+ DB.EthEvent.insert_deposits!([
+ %{root_chain_txhash: expected_root_chain_txhash_1, log_index: expected_log_index, blknum: expected_blk_num_1,
+ owner: expected_owner, currency: expected_currency, amount: expected_amount
+ },
+ %{root_chain_txhash: expected_root_chain_txhash_2, log_index: expected_log_index, blknum: expected_blk_num_2,
+ owner: expected_owner, currency: expected_currency, amount: expected_amount
+ },
+ %{root_chain_txhash: expected_root_chain_txhash_3, log_index: expected_log_index, blknum: expected_blk_num_3,
+ owner: expected_owner, currency: expected_currency, amount: expected_amount
+ },
+ ])
- assert [hash1, hash2, hash3] == DB.TxOutput.get_utxos(alice.addr) |> Enum.map(& &1.creating_deposit)
+ assert %DB.EthEvent{
+ root_chain_txhash: ^expected_root_chain_txhash_1,
+ event_type: ^expected_event_type,
+ root_chain_txhash_event: ^expected_root_chain_txhash_event_1}
+ = DB.EthEvent.get(expected_root_chain_txhash_event_1)
+
+ assert %DB.EthEvent{
+ root_chain_txhash: ^expected_root_chain_txhash_2,
+ event_type: ^expected_event_type,
+ root_chain_txhash_event: ^expected_root_chain_txhash_event_2}
+ = DB.EthEvent.get(expected_root_chain_txhash_event_2)
+
+ assert %DB.EthEvent{
+ root_chain_txhash: ^expected_root_chain_txhash_3,
+ event_type: ^expected_event_type,
+ root_chain_txhash_event: ^expected_root_chain_txhash_event_3}
+ = DB.EthEvent.get(expected_root_chain_txhash_event_3)
+
+ assert [^expected_root_chain_txhash_1, ^expected_root_chain_txhash_2, ^expected_root_chain_txhash_3] =
+ DB.TxOutput.get_utxos(alice.addr) |> Enum.map(fn txoutput ->
+ [head | _tail] = txoutput.ethevents
+ head.root_chain_txhash
+ end)
end
- @tag fixtures: [:initial_blocks]
+ # @tag fixtures: [:initial_blocks]
+ @tag fixtures: [:phoenix_ecto_sandbox]
test "insert exits: creates exit event and marks utxo as spent" do
- bobs_deposit_pos = Utxo.position(2, 0, 0)
- bobs_deposit_exit_hash = DB.EthEvent.generate_unique_key(bobs_deposit_pos, :exit)
- alices_utxo_pos = Utxo.position(3000, 1, 1)
- alices_utxo_exit_hash = DB.EthEvent.generate_unique_key(alices_utxo_pos, :exit)
+# bobs_deposit_pos = Utxo.position(2, 0, 0)
+# bobs_deposit_exit_hash = DB.EthEvent.generate_unique_key(bobs_deposit_pos, :exit)
+#
+# alices_utxo_pos = Utxo.position(3000, 1, 1)
+# alices_utxo_exit_hash = DB.EthEvent.generate_unique_key(alices_utxo_pos, :exit)
+#
+# to_insert = prepare_to_insert([bobs_deposit_pos, alices_utxo_pos])
+# :ok = DB.EthEvent.insert_exits!(to_insert)
+#
+# assert %DB.EthEvent{blknum: 2, txindex: 0, event_type: :exit, hash: ^bobs_deposit_exit_hash} =
+# DB.Repo.get(DB.EthEvent, bobs_deposit_exit_hash)
+#
+# assert %DB.EthEvent{blknum: 3000, txindex: 1, event_type: :exit, hash: ^alices_utxo_exit_hash} =
+# DB.Repo.get(DB.EthEvent, alices_utxo_exit_hash)
+#
+# assert %DB.TxOutput{amount: 100, spending_tx_oindex: nil, spending_exit: ^bobs_deposit_exit_hash} =
+# DB.TxOutput.get_by_position(bobs_deposit_pos)
+#
+# assert %DB.TxOutput{amount: 50, spending_tx_oindex: nil, spending_exit: ^alices_utxo_exit_hash} =
+# DB.TxOutput.get_by_position(alices_utxo_pos)
- to_insert = prepare_to_insert([bobs_deposit_pos, alices_utxo_pos])
- :ok = DB.EthEvent.insert_exits!(to_insert)
- assert %DB.EthEvent{blknum: 2, txindex: 0, event_type: :exit, hash: ^bobs_deposit_exit_hash} =
- DB.Repo.get(DB.EthEvent, bobs_deposit_exit_hash)
- assert %DB.EthEvent{blknum: 3000, txindex: 1, event_type: :exit, hash: ^alices_utxo_exit_hash} =
- DB.Repo.get(DB.EthEvent, alices_utxo_exit_hash)
+ expected_owner = <<1::160>>
+ expected_log_index = 0
+ expected_amount = 1
+ expected_currency = @eth
- assert %DB.TxOutput{amount: 100, spending_tx_oindex: nil, spending_exit: ^bobs_deposit_exit_hash} =
- DB.TxOutput.get_by_position(bobs_deposit_pos)
+ expected_blk_num = 3000
+ expected_txindex = 0
+ expected_oindex = 0
- assert %DB.TxOutput{amount: 50, spending_tx_oindex: nil, spending_exit: ^alices_utxo_exit_hash} =
- DB.TxOutput.get_by_position(alices_utxo_pos)
- end
+ expected_utxo_encoded_position = Position.encode(Utxo.position(expected_blk_num, expected_txindex, expected_oindex))
- @tag fixtures: [:alice, :initial_blocks]
- test "Writes of deposits and exits are idempotent", %{alice: alice} do
- # try to insert again existing deposit (from initial_blocks)
- assert :ok = DB.EthEvent.insert_deposits!([%{owner: alice.addr, currency: @eth, amount: 333, blknum: 1}])
+ expected_deposit_root_chain_txhash = Crypto.hash(<<5::256>>)
+ expected_exit_root_chain_txhash = Crypto.hash(<<6::256>>)
- to_insert = prepare_to_insert([Utxo.position(1, 0, 0), Utxo.position(1, 0, 0)])
+ assert :ok =
+ DB.EthEvent.insert_deposits!([
+ %{
+ root_chain_txhash: expected_deposit_root_chain_txhash,
+ log_index: expected_log_index,
+ blknum: expected_blk_num,
+ owner: expected_owner,
+ currency: expected_currency,
+ amount: expected_amount
+ }
+ ])
- assert :ok = DB.EthEvent.insert_exits!(to_insert)
- end
+ assert :ok = DB.EthEvent.insert_exits!([
+ %{
+ call_data: %{utxo_pos: expected_utxo_encoded_position},
+ root_chain_txhash: expected_exit_root_chain_txhash,
+ log_index: expected_log_index
+ }
+ ])
- defp prepare_to_insert(positions),
- do: Enum.map(positions, &%{call_data: %{utxo_pos: Utxo.Position.encode(&1)}})
+ assert length(DB.TxOutput.get_utxos(expected_owner)) == 0
+ end
+#
+# @tag fixtures: [:alice, :initial_blocks]
+# test "Writes of deposits and exits are idempotent", %{alice: alice} do
+# # try to insert again existing deposit (from initial_blocks)
+# assert :ok = DB.EthEvent.insert_deposits!([%{owner: alice.addr, currency: @eth, amount: 333, blknum: 1}])
+#
+# to_insert = prepare_to_insert([Utxo.position(1, 0, 0), Utxo.position(1, 0, 0)])
+#
+# assert :ok = DB.EthEvent.insert_exits!(to_insert)
+# end
+#
+# defp prepare_to_insert(positions),
+# do: Enum.map(positions, &%{call_data: %{utxo_pos: Utxo.Position.encode(&1)}})
end

diff:
@@ -53,8 +53,9 @@ Stores events logged in root contract, such as _deposits_ or _exits_.
|**ethevents**|||
|:-:|:-|:-|
-|root_chain_txhash|bytea|Pk(root_chain_txhash, event_type)|
-|event_type|varchar(124)|Pk^|
+|root_chain_txhash|bytea|Pk(root_chain_txhash, log_index)|
+|event_type|integer|Pk^|
+|event_type|varchar(124)||
|root_chain_txhash_event|bytea|UI|
|inserted_at|datetime|UTC (w/o TZ)|
|updated_at|datetime|UTC (w/o TZ)|
@@ -148,6 +149,7 @@ SELECT CASE WHEN t.child_chain_utxohash IS NULL THEN NULL
END AS root_chain_txhash,
blknum, txindex, oindex,
amount,
+ log_index,
event_type
FROM txoutputs t
LEFT OUTER JOIN ethevents_txoutputs et ON t.child_chain_utxohash = et.child_chain_utxohash

license: Apache License 2.0

repo: omgnetwork/elixir-omg


message: update test and add comments

diff:
@@ -23,6 +23,7 @@ defmodule OMG.EthTest do
"""
alias OMG.Eth
+ alias OMG.Eth.Encoding
use ExUnitFixtures
use ExUnit.Case, async: false
@@ -86,20 +87,28 @@ defmodule OMG.EthTest do
[List.duplicate(zero_in, 4), [[contract.authority_addr, @eth, 1]] ++ List.duplicate(zero_out, 3)]
|> ExRLP.encode()
- {:ok, _} =
+ {:ok, tx_hash} =
Eth.RootChainHelper.deposit(tx, 1, contract.authority_addr, contract.contract_addr)
|> Eth.DevHelpers.transact_sync!()
{:ok, height} = Eth.get_ethereum_height()
authority_addr = contract.authority_addr
- deposits = Eth.RootChain.get_deposits(1, height, contract.contract_addr)
-
- assert {:ok, [%{amount: 1, blknum: 1, owner: ^authority_addr, currency: @eth, eth_height: height, log_index: 0}]} =
- deposits
+ root_chain_txhash = Encoding.from_hex(tx_hash["transactionHash"])
- {:ok, [%{} = deposit]} = deposits
+ deposits = Eth.RootChain.get_deposits(1, height, contract.contract_addr)
- assert Map.has_key?(deposit, :root_chain_txhash)
+ assert {:ok,
+ [
+ %{
+ amount: 1,
+ blknum: 1,
+ owner: ^authority_addr,
+ currency: @eth,
+ eth_height: height,
+ log_index: 0,
+ root_chain_txhash: ^root_chain_txhash
+ }
+ ]} = deposits
end
end

diff:
@@ -94,9 +94,8 @@ defmodule OMG.Watcher.DB.TxOutput do
txoutput in __MODULE__,
preload: [:ethevents],
left_join: ethevent in assoc(txoutput, :ethevents),
- where:
- txoutput.owner == ^owner and
- is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment("
+ # select txoutputs by owner that have neither been spent nor have a corresponding ethevents exit events
+ where: txoutput.owner == ^owner and is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment("
NOT EXISTS (SELECT 1
FROM ethevents_txoutputs AS etfrag
JOIN ethevents AS efrag ON
@@ -115,6 +114,7 @@ defmodule OMG.Watcher.DB.TxOutput do
from(
txoutput in __MODULE__,
left_join: ethevent in assoc(txoutput, :ethevents),
+ # select txoutputs by owner that have neither been spent nor have a corresponding ethevents exit events
where: txoutput.owner == ^owner and is_nil(txoutput.spending_txhash) and (is_nil(ethevent) or fragment("
NOT EXISTS (SELECT 1
FROM ethevents_txoutputs AS etfrag

diff:
@@ -197,7 +197,6 @@ defmodule OMG.Watcher.DB.EthEventTest do
end)
end
- # @tag fixtures: [:initial_blocks]
@tag fixtures: [:phoenix_ecto_sandbox]
test "insert exits: creates exit event and marks utxo as spent" do
expected_owner = <<1::160>>

original_message: update test and add comments


message: relax timestamp not null constraints on txoutputs

change_type: ADD

diff:
+defmodule OMG.Watcher.DB.Repo.Migrations.RelaxTimestampNotNullConstraintsOnTxoutputsTable do
+ use Ecto.Migration
+
+ def change do
+ alter table(:txoutputs) do
+ modify :inserted_at, :utc_datetime, default: fragment("now() at time zone 'utc'"), null: true
+ modify :updated_at, :utc_datetime, default: fragment("now() at time zone 'utc'"), null: true
+ end
+ end
+end


message: change existing broken migration. dont add a new migration

diff:
@@ -62,13 +62,9 @@ defmodule OMG.Watcher.DB.Repo.Migrations.AlterTxOutputsTableAddRootchainTxnHashD
remove(:creating_deposit)
remove(:spending_exit)
- timestamps([type: :utc_datetime])
+ timestamps(type: :utc_datetime, default: fragment("(now() at time zone 'utc')"), null: false)
end
- # how to do this in ecto correctly? do it manually
- execute("ALTER TABLE txoutputs ALTER COLUMN inserted_at SET DEFAULT (now() at time zone 'utc');")
- execute("ALTER TABLE txoutputs ALTER COLUMN updated_at SET DEFAULT (now() at time zone 'utc');")
-
create table(:ethevents_txoutputs, primary_key: false) do
add(:root_chain_txhash_event,
references(:ethevents, column: :root_chain_txhash_event, type: :binary, on_delete: :restrict),

change_type: DELETE

diff:
-defmodule OMG.Watcher.DB.Repo.Migrations.RelaxTimestampNotNullConstraintsOnTxoutputsTable do
- use Ecto.Migration
-
- def change do
- alter table(:txoutputs) do
- modify :inserted_at, :utc_datetime, default: fragment("now() at time zone 'utc'"), null: true
- modify :updated_at, :utc_datetime, default: fragment("now() at time zone 'utc'"), null: true
- end
- end
-end


message: null: true, not null: false

diff:
@@ -62,7 +62,7 @@ defmodule OMG.Watcher.DB.Repo.Migrations.AlterTxOutputsTableAddRootchainTxnHashD
remove(:creating_deposit)
remove(:spending_exit)
- timestamps(type: :utc_datetime, default: fragment("(now() at time zone 'utc')"), null: false)
+ timestamps(type: :utc_datetime, default: fragment("(now() at time zone 'utc')"), null: true)
end
create table(:ethevents_txoutputs, primary_key: false) do

original_message: null: true, not null: false


message: set inserted_at and updated_at null columns to epoch and add not null constraint

diff:
+defmodule OMG.Watcher.Repo.Migrations.SetInsertedAtUpdatedAtToEpoch do
+ use Ecto.Migration
+
+ def change do
+ execute("UPDATE txoutputs SET inserted_at = 'epoch' at time zone 'utc';")
+ execute("UPDATE txoutputs SET updated_at = 'epoch' at time zone 'utc';")
+ execute("ALTER TABLE txoutputs ALTER COLUMN inserted_at SET NOT NULL;")
+ execute("ALTER TABLE txoutputs ALTER COLUMN updated_at SET NOT NULL;")
+ end
+end


message: Add tests for OMG.Block and extract Merkle-related functions

diff:
@@ -18,9 +18,9 @@ defmodule OMG.Block do
"""
alias OMG.Crypto
+ alias OMG.Merkle
alias OMG.State.Transaction
- @transaction_merkle_tree_height 16
@type block_hash_t() :: <<_::256>>
defstruct [:transactions, :hash, :number]
@@ -48,7 +48,7 @@ defmodule OMG.Block do
|> Enum.map(&get_data_per_tx/1)
|> Enum.unzip()
- %__MODULE__{hash: merkle_hash(hashed_txs), transactions: txs_bytes, number: blknum}
+ %__MODULE__{hash: Merkle.hash(hashed_txs), transactions: txs_bytes, number: blknum}
end
@doc """
@@ -84,7 +84,7 @@ defmodule OMG.Block do
|> Enum.map(&get_data_per_tx/1)
|> Enum.unzip()
- create_tx_proof(hashed_txs, txindex)
+ Merkle.create_tx_proof(hashed_txs, txindex)
end
def inclusion_proof(%__MODULE__{transactions: transactions}, txindex), do: inclusion_proof(transactions, txindex)
@@ -96,30 +96,4 @@ defmodule OMG.Block do
end
defp to_recovered_tx(txbytes), do: Transaction.Recovered.recover_from!(txbytes)
-
- @default_leaf <<0>> |> List.duplicate(32) |> Enum.join() |> Crypto.hash()
- # Creates a Merkle proof that transaction under a given transaction index
- # is included in block consisting of hashed transactions
- @spec create_tx_proof(nonempty_list(binary()), non_neg_integer()) :: binary()
- defp create_tx_proof(hashed_txs, txindex),
- do:
- MerkleTree.build(hashed_txs,
- hash_function: &Crypto.hash/1,
- hash_leaves: false,
- height: @transaction_merkle_tree_height,
- default_data_block: @default_leaf
- )
- |> MerkleTree.Proof.prove(txindex)
- |> (& &1.hashes).()
- |> Enum.reverse()
- |> Enum.join()
-
- defp merkle_hash(hashed_txs),
- do:
- MerkleTree.fast_root(hashed_txs,
- hash_function: &Crypto.hash/1,
- hash_leaves: false,
- height: @transaction_merkle_tree_height,
- default_data_block: @default_leaf
- )
end

diff:
+# Copyright 2019 OmiseGO Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule OMG.Merkle do
+ @moduledoc """
+ Encapsulates all the interactions with the MerkleTree library.
+ """
+
+ alias OMG.Crypto
+
+ @transaction_merkle_tree_height 16
+ @default_leaf <<0>> |> List.duplicate(32) |> Enum.join() |> Crypto.hash()
+
+ # Creates a Merkle proof that transaction under a given transaction index
+ # is included in block consisting of hashed transactions
+ @spec create_tx_proof(nonempty_list(binary()), non_neg_integer()) :: binary()
+ def create_tx_proof(hashed_txs, txindex) do
+ build(hashed_txs)
+ |> prove(txindex)
+ |> (& &1.hashes).()
+ |> Enum.reverse()
+ |> Enum.join()
+ end
+
+ @spec hash(nonempty_list(binary())) :: binary()
+ def hash(hashed_txs) do
+ MerkleTree.fast_root(hashed_txs,
+ hash_function: &Crypto.hash/1,
+ hash_leaves: false,
+ height: @transaction_merkle_tree_height,
+ default_data_block: @default_leaf
+ )
+ end
+
+ defp build(hashed_txs) do
+ MerkleTree.build(hashed_txs,
+ hash_function: &Crypto.hash/1,
+ hash_leaves: false,
+ height: @transaction_merkle_tree_height,
+ default_data_block: @default_leaf
+ )
+ end
+
+ defp prove(hash, txindex) do
+ MerkleTree.Proof.prove(hash, txindex)
+ end
+end

diff:
@@ -25,6 +25,198 @@ defmodule OMG.BlockTest do
defp eth, do: OMG.Eth.RootChain.eth_pseudo_address()
+ describe "hashed_txs_at/2" do
+ @tag fixtures: [:stable_alice, :stable_bob]
+ test "returns a block with the list of transactions and a computed merkle root hash", %{
+ stable_alice: alice,
+ stable_bob: bob
+ } do
+ tx_1 = TestHelper.create_recovered([{1, 0, 0, alice}], eth(), [{bob, 100}])
+ tx_2 = TestHelper.create_recovered([{1, 1, 1, alice}], eth(), [{bob, 100}])
+
+ transactions = [tx_1, tx_2]
+
+ assert Block.hashed_txs_at(transactions, 10) == %Block{
+ hash:
+ <<34, 146, 42, 183, 241, 237, 118, 74, 89, 216, 33, 45, 171, 175, 216, 24, 97, 154, 90, 81, 21, 199,
+ 155, 254, 64, 112, 96, 58, 205, 178, 162, 77>>,
+ number: 10,
+ transactions: [
+ tx_1.signed_tx_bytes,
+ tx_2.signed_tx_bytes
+ ]
+ }
+ end
+
+ test "handles an empty list of transactions" do
+ assert Block.hashed_txs_at([], 10) == %Block{
+ hash:
+ <<119, 106, 49, 219, 52, 161, 160, 167, 202, 175, 134, 44, 255, 223, 255, 23, 137, 41, 127, 250, 220,
+ 56, 11, 211, 211, 146, 129, 211, 64, 171, 211, 173>>,
+ number: 10,
+ transactions: []
+ }
+ end
+ end
+
+ describe "to_api_format/1" do
+ test "formats to map for API" do
+ block = %Block{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+
+ assert Block.to_api_format(block) == %{
+ blknum: 10,
+ hash: "hash",
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+ end
+ end
+
+ describe "to_db_value/1" do
+ test "formats to DB format with valid inputs" do
+ block = %Block{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+
+ assert Block.to_db_value(block) == %{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+ end
+
+ test "fails to format when the list of transactions is not a list" do
+ # Not sure how we want to handle this, there is currently no
+ # fallback function in the Block module
+ assert_raise FunctionClauseError, fn ->
+ Block.to_db_value(%Block{
+ hash: "hash",
+ number: 10,
+ transactions: %{}
+ })
+ end
+ end
+
+ test "fails to format when the hash is not a binary" do
+ assert_raise FunctionClauseError, fn ->
+ Block.to_db_value(%Block{
+ hash: 1,
+ number: 10,
+ transactions: []
+ })
+ end
+ end
+
+ test "fails to format when the number is not an integer" do
+ assert_raise FunctionClauseError, fn ->
+ Block.to_db_value(%Block{
+ hash: 1,
+ number: "10",
+ transactions: []
+ })
+ end
+ end
+ end
+
+ describe "from_db_value/1" do
+ test "formats from DB format with valid inputs" do
+ block = %{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+
+ assert Block.from_db_value(block) == %Block{
+ hash: "hash",
+ number: 10,
+ transactions: ["tx_1_bytes", "tx_2_bytes"]
+ }
+ end
+
+ test "fails to format when the list of transactions is not a list" do
+ # Not sure how we want to handle this, there is currently no
+ # fallback function in the Block module
+ assert_raise FunctionClauseError, fn ->
+ Block.from_db_value(%{
+ hash: "hash",
+ number: 10,
+ transactions: %{}
+ })
+ end
+ end
+
+ test "fails to format when the hash is not a binary" do
+ assert_raise FunctionClauseError, fn ->
+ Block.from_db_value(%{
+ hash: 1,
+ number: 10,
+ transactions: []
+ })
+ end
+ end
+
+ test "fails to format when the number is not an integer" do
+ assert_raise FunctionClauseError, fn ->
+ Block.from_db_value(%{
+ hash: 1,
+ number: "10",
+ transactions: []
+ })
+ end
+ end
+ end
+
+ describe "inclusion_proof/2" do
+ @tag fixtures: [:stable_alice]
+ test "calculates the inclusion proof when a list of transactions is given", %{
+ stable_alice: alice
+ } do
+ tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}])
+ tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}])
+
+ encoded_proof =
+ [tx_1, tx_2]
+ |> Block.inclusion_proof(1)
+ |> Base.encode16(case: :lower)
+
+ assert "491b9e07c4997c976c38a91f" <> _ = encoded_proof
+ end
+
+ @tag fixtures: [:stable_alice]
+ test "calculates the inclusion proof when a block is given", %{
+ stable_alice: alice
+ } do
+ tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}])
+ tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}])
+
+ encoded_proof =
+ %Block{transactions: [tx_1, tx_2]}
+ |> Block.inclusion_proof(1)
+ |> Base.encode16(case: :lower)
+
+ assert "491b9e07c4997c976c38a91f" <> _ = encoded_proof
+ end
+
+ test "calculates the inclusion proof when an empty list is given" do
+ encoded_proof =
+ []
+ |> Block.inclusion_proof(1)
+ |> Base.encode16(case: :lower)
+
+ assert "290decd9548b62a8d60345a9" <> _ = encoded_proof
+ end
+
+ test "raises an error when an invalid input is given (map)" do
+ assert_raise FunctionClauseError, fn ->
+ Block.inclusion_proof(%{}, 1)
+ end
+ end
+
@tag fixtures: [:stable_alice, :stable_bob]
test "Block merkle proof smoke test", %{
stable_alice: alice
@@ -50,12 +242,10 @@ defmodule OMG.BlockTest do
test "Block merkle proof smoke test for deposit transactions",
%{alice: alice} do
tx = TestHelper.create_encoded([], eth(), [{alice, 7}])
-
- proof =
- %Block{transactions: [tx]}
- |> Block.inclusion_proof(0)
+ proof = Block.inclusion_proof(%Block{transactions: [tx]}, 0)
assert is_binary(proof)
assert byte_size(proof) == 32 * 16
end
end
+end

diff:
+# Copyright 2019 OmiseGO Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule OMG.MerkleTest do
+ @moduledoc false
+
+ use ExUnitFixtures
+ use ExUnit.Case, async: true
+
+ alias OMG.Merkle
+
+ describe "create_tx_proof/2" do
+ test "creates merkle proofs based on list of values and index" do
+ # We don't want to be testing the underlying library here,
+ # we just want to ensure that our code calling it always
+ # return the same result
+ values = ["abc", "def", "ghi"]
+
+ proof_1 =
+ values
+ |> Merkle.create_tx_proof(1)
+ |> Base.encode16(case: :lower)
+
+ proof_2 =
+ values
+ |> Merkle.create_tx_proof(2)
+ |> Base.encode16(case: :lower)
+
+ assert "6162639621ddbb2d2631190" <> _ = proof_1
+ assert proof_1 != proof_2
+ end
+ end
+
+ describe "hash/1" do
+ test "returns the merkle tree root for a list of transaction" do
+ values = ["abc", "def", "ghi"]
+
+ proof =
+ values
+ |> Merkle.hash()
+ |> Base.encode16(case: :lower)
+
+ assert proof == "5886d20993731f68ff6bc6b175f58aa2bddc09512eabe761aaf62ab57c086066"
+ end
+ end
+end


message: Switch type spec for Merkle functions from nonempty_list() to list()

diff:
@@ -24,7 +24,7 @@ defmodule OMG.Merkle do
# Creates a Merkle proof that transaction under a given transaction index
# is included in block consisting of hashed transactions
- @spec create_tx_proof(nonempty_list(binary()), non_neg_integer()) :: binary()
+ @spec create_tx_proof(list(binary()), non_neg_integer()) :: binary()
def create_tx_proof(hashed_txs, txindex) do
build(hashed_txs)
|> prove(txindex)
@@ -33,7 +33,7 @@ defmodule OMG.Merkle do
|> Enum.join()
end
- @spec hash(nonempty_list(binary())) :: binary()
+ @spec hash(list(binary())) :: binary()
def hash(hashed_txs) do
MerkleTree.fast_root(hashed_txs,
hash_function: &Crypto.hash/1,


message: Refactor asserts for merkle proofs to byte_size checks

diff:
@@ -172,19 +172,23 @@ defmodule OMG.BlockTest do
end
describe "inclusion_proof/2" do
+ # The tests below checks merkle proof normally tested via speaking to the contract
+ # (integration tests) against a fixed binary. The motivation for having such
+ # test is a quick test of whether the merkle proving didn't change.
@tag fixtures: [:stable_alice]
test "calculates the inclusion proof when a list of transactions is given", %{
stable_alice: alice
} do
+ # odd number of transactions, just in case
tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}])
tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}])
+ tx_3 = TestHelper.create_encoded([{1, 0, 1, alice}], eth(), [{alice, 2}])
- encoded_proof =
- [tx_1, tx_2]
- |> Block.inclusion_proof(1)
- |> Base.encode16(case: :lower)
+ proof = Block.inclusion_proof([tx_1, tx_2, tx_3], 1)
- assert "491b9e07c4997c976c38a91f" <> _ = encoded_proof
+ assert "491b9e07c4997c976c38a91f" <> _ = Base.encode16(proof, case: :lower)
+ assert is_binary(proof)
+ assert byte_size(proof) == 32 * 16
end
@tag fixtures: [:stable_alice]
@@ -193,22 +197,30 @@ defmodule OMG.BlockTest do
} do
tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}])
tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}])
+ proof = Block.inclusion_proof(%Block{transactions: [tx_1, tx_2]}, 1)
+
+ assert is_binary(proof)
+ assert byte_size(proof) == 32 * 16
+ end
+
+ @tag fixtures: [:stable_alice]
+ test "calculating a proof via a block or a list of transactions return the same result", %{
+ stable_alice: alice
+ } do
+ tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}])
+ tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}])
- encoded_proof =
- %Block{transactions: [tx_1, tx_2]}
- |> Block.inclusion_proof(1)
- |> Base.encode16(case: :lower)
+ block_proof = Block.inclusion_proof(%Block{transactions: [tx_1, tx_2]}, 1)
+ transactions_proof = Block.inclusion_proof([tx_1, tx_2], 1)
- assert "491b9e07c4997c976c38a91f" <> _ = encoded_proof
+ assert block_proof == transactions_proof
end
test "calculates the inclusion proof when an empty list is given" do
- encoded_proof =
- []
- |> Block.inclusion_proof(1)
- |> Base.encode16(case: :lower)
+ proof = Block.inclusion_proof([], 1)
- assert "290decd9548b62a8d60345a9" <> _ = encoded_proof
+ assert is_binary(proof)
+ assert byte_size(proof) == 32 * 16
end
test "raises an error when an invalid input is given (map)" do
@@ -217,29 +229,8 @@ defmodule OMG.BlockTest do
end
end
- @tag fixtures: [:stable_alice, :stable_bob]
- test "Block merkle proof smoke test", %{
- stable_alice: alice
- } do
- # this checks merkle proof normally tested via speaking to the contract (integration tests) against
- # a fixed binary. The motivation for having such test is a quick test of whether the merkle proving didn't change
-
- # odd number of transactions, just in case
- tx_1 = TestHelper.create_encoded([{1, 0, 0, alice}], eth(), [{alice, 7}])
- tx_2 = TestHelper.create_encoded([{1, 1, 0, alice}], eth(), [{alice, 2}])
- tx_3 = TestHelper.create_encoded([{1, 0, 1, alice}], eth(), [{alice, 2}])
-
- txs = [tx_1, tx_2, tx_3]
- assert Block.inclusion_proof(txs, 1) == Block.inclusion_proof(%Block{transactions: txs}, 1)
-
- assert %Block{transactions: [tx_1, tx_2, tx_3]}
- |> Block.inclusion_proof(2)
- |> Base.encode16(case: :lower) ==
- "290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563c62f5e8b8c8303e7845c0e8ea800fc1bae9ccb7782b6f3c870c2d48fdddf5bb4890740a8eb06ce9be422cb8da5cdafc2b58c0a5e24036c578de2a433c828ff7d3b8ec09e026fdc305365dfc94e189a81b38c7597b3d941c279f042e8206e0bd8ecd50eee38e386bd62be9bedb990706951b65fe053bd9d8a521af753d139e2dadefff6d330bb5403f63b14f33b578274160de3a50df4efecf0e0db73bcdd3da5617bdd11f7c0a11f49db22f629387a12da7596f9d1704d7465177c63d88ec7d7292c23a9aa1d8bea7e2435e555a4a60e379a5a35f3f452bae60121073fb6eeade1cea92ed99acdcb045a6726b2f87107e8a61620a232cf4d7d5b5766b3952e107ad66c0a68c72cb89e4fb4303841966e4062a76ab97451e3b9fb526a5ceb7f82e026cc5a4aed3c22a58cbd3d2ac754c9352c5436f638042dca99034e836365163d04cffd8b46a874edf5cfae63077de85f849a660426697b06a829c70dd1409cad676aa337a485e4728a0b240d92b3ef7b3c372d06d189322bfd5f61f1e7203ea2fca4a49658f9fab7aa63289c91b7c7b6c832a6d0e69334ff5b0a3483d09dab4ebfd9cd7bca2505f7bef59cc1c12ecc708fff26ae4af19abe852afe9e20c8622def10d13dd169f550f578bda343d9717a138562e0093b380a1120789d53cf10"
- end
-
@tag fixtures: [:alice]
- test "Block merkle proof smoke test for deposit transactions",
+ test "calculates a block merkle proof for deposit transactions",
%{alice: alice} do
tx = TestHelper.create_encoded([], eth(), [{alice, 7}])
proof = Block.inclusion_proof(%Block{transactions: [tx]}, 0)

diff:
@@ -27,18 +27,11 @@ defmodule OMG.MerkleTest do
# return the same result
values = ["abc", "def", "ghi"]
- proof_1 =
- values
- |> Merkle.create_tx_proof(1)
- |> Base.encode16(case: :lower)
-
- proof_2 =
- values
- |> Merkle.create_tx_proof(2)
- |> Base.encode16(case: :lower)
+ proof_1 = Merkle.create_tx_proof(values, 1)
+ proof_2 = Merkle.create_tx_proof(values, 2)
- assert "6162639621ddbb2d2631190" <> _ = proof_1
assert proof_1 != proof_2
+ assert "6162639621ddbb2d2631190" <> _ = Base.encode16(proof_1, case: :lower)
end
end


message: Get dialyzer to stop complaining about the type specs in Merkle

diff:
@@ -24,7 +24,7 @@ defmodule OMG.Merkle do
# Creates a Merkle proof that transaction under a given transaction index
# is included in block consisting of hashed transactions
- @spec create_tx_proof(list(binary()), non_neg_integer()) :: binary()
+ @spec create_tx_proof(list(String.t()), non_neg_integer()) :: binary()
def create_tx_proof(hashed_txs, txindex) do
build(hashed_txs)
|> prove(txindex)
@@ -33,7 +33,7 @@ defmodule OMG.Merkle do
|> Enum.join()
end
- @spec hash(list(binary())) :: binary()
+ @spec hash(list(String.t())) :: binary()
def hash(hashed_txs) do
MerkleTree.fast_root(hashed_txs,
hash_function: &Crypto.hash/1,


message: Refactor code & tests for OMG.Fees

diff:
defmodule OMG.Fees do
@moduledoc """
- Transaction's fee validation functions
+ Transaction's fee validation functions.
"""
+ alias OMG.FeesValidator
+ alias OMG.MergeTransactionValidator
alias OMG.State.Transaction
alias OMG.Utxo
@@ -25,13 +27,13 @@ defmodule OMG.Fees do
use OMG.Utils.LoggerExt
@type fee_spec_t() :: %{token: Transaction.Payment.currency(), flat_fee: non_neg_integer}
- @type fee_t() :: %{Transaction.Payment.currency() => non_neg_integer} | :ignore
+ @type fee_t() :: %{Transaction.Payment.currency() => non_neg_integer} | :no_fees_transaction
@doc """
- Checks whether transaction's funds cover the fee
+ Checks whether the transaction's inputs cover the fees.
"""
@spec covered?(implicit_paid_fee_by_currency :: map(), fees :: fee_t()) :: boolean()
- def covered?(_, :ignore), do: true
+ def covered?(_, :no_fees_transaction), do: true
def covered?(implicit_paid_fee_by_currency, fees) do
for {input_currency, implicit_paid_fee} <- implicit_paid_fee_by_currency do
@@ -44,54 +46,18 @@ defmodule OMG.Fees do
end
@doc """
- Returns fees to require for a particular transaction, and under particular fee specs listed in `fee_map`
+ Returns the fees to pay for a particular transaction,
+ and under particular fee specs listed in `fee_map`.
"""
- @spec for_tx(Transaction.Recovered.t(), fee_t()) :: fee_t()
- def for_tx(tx, fee_map) do
- if is_merge_transaction?(tx),
- do: :ignore,
- # TODO: reducing fees to output currencies only is incorrect, let's deffer until fees get large
- else: fee_map
- end
-
- defp is_merge_transaction?(recovered_tx) do
- [
- &is_payment?/1,
- &has_less_outputs_than_inputs?/1,
- &has_single_currency?/1,
- &has_same_account?/1
- ]
- |> Enum.all?(fn predicate -> predicate.(recovered_tx) end)
- end
-
- defp is_payment?(%Transaction.Recovered{signed_tx: %{raw_tx: %Transaction.Payment{}}}), do: true
- defp is_payment?(_), do: false
-
- defp has_same_account?(%Transaction.Recovered{witnesses: witnesses} = tx) do
- spenders = Map.values(witnesses)
+ @spec for_transaction(Transaction.Recovered.t(), fee_t()) :: fee_t()
+ def for_transaction(transaction, fee_map) do
+ case MergeTransactionValidator.is_merge_transaction?(transaction) do
+ true ->
+ :no_fees_transaction
- tx
- |> Transaction.get_outputs()
- |> Enum.map(& &1.owner)
- |> Enum.concat(spenders)
- |> single?()
- end
-
- defp has_single_currency?(tx) do
- tx
- |> Transaction.get_outputs()
- |> Enum.map(& &1.currency)
- |> single?()
+ # TODO: reducing fees to output currencies only is incorrect, let's deffer until fees get large
+ false ->
+ fee_map
end
-
- defp has_less_outputs_than_inputs?(tx) do
- has_less_outputs_than_inputs?(
- Transaction.get_inputs(tx),
- Transaction.get_outputs(tx)
- )
end
-
- defp has_less_outputs_than_inputs?(inputs, outputs), do: length(inputs) >= 1 and length(inputs) > length(outputs)
-
- defp single?(list), do: 1 == list |> Enum.dedup() |> length()
end

diff:
@@ -20,6 +20,7 @@ defmodule OMG.State.Transaction.Validator do
@maximum_block_size 65_536
alias OMG.Fees
+ alias OMG.FeesValidator
alias OMG.State.Core
alias OMG.State.Transaction
alias OMG.State.UtxoSet

diff:
+# Copyright 2019 OmiseGO Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule OMG.MergeTransactionValidator do
+ @moduledoc """
+ Checks for transaction attributes. Used by OMG.Fees.
+ """
+
+ alias OMG.State.Transaction
+ alias OMG.Utxo
+
+ require Utxo
+
+ use OMG.Utils.LoggerExt
+
+ def is_merge_transaction?(recovered_transaction) do
+ [
+ &is_payment?/1,
+ &has_less_outputs_than_inputs?/1,
+ &has_single_currency?/1,
+ &has_same_account?/1
+ ]
+ |> Enum.all?(fn predicate -> predicate.(recovered_transaction) end)
+ end
+
+ defp is_payment?(%Transaction.Recovered{signed_tx: %{raw_tx: %Transaction.Payment{}}}), do: true
+ defp is_payment?(_), do: false
+
+ defp has_same_account?(%Transaction.Recovered{witnesses: witnesses} = tx) do
+ spenders = Map.values(witnesses)
+
+ tx
+ |> Transaction.get_outputs()
+ |> Enum.map(& &1.owner)
+ |> Enum.concat(spenders)
+ |> single?()
+ end
+
+ defp has_single_currency?(tx) do
+ tx
+ |> Transaction.get_outputs()
+ |> Enum.map(& &1.currency)
+ |> single?()
+ end
+
+ defp has_less_outputs_than_inputs?(tx) do
+ has_less_outputs_than_inputs?(
+ Transaction.get_inputs(tx),
+ Transaction.get_outputs(tx)
+ )
+ end
+
+ defp has_less_outputs_than_inputs?(inputs, outputs), do: length(inputs) >= 1 and length(inputs) > length(outputs)
+
+ defp single?(list), do: 1 == list |> Enum.dedup() |> length()
+end

diff:
@@ -19,9 +19,10 @@ defmodule OMG.FeesTest do
use OMG.Fixtures
use ExUnit.Case, async: true
- import OMG.Fees
import OMG.TestHelper
+ alias OMG.Fees
+
@eth OMG.Eth.RootChain.eth_pseudo_address()
@not_eth <<1::size(160)>>
@@ -30,87 +31,88 @@ defmodule OMG.FeesTest do
@not_eth => 3
}
- # TODO: brittle test? why not test via public API (State.Core)?
- @tag fixtures: [:alice, :bob]
- test "Transactions covers the fee only in one currency accepted by the operator", %{alice: alice, bob: bob} do
- fees =
- create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}])
- |> for_tx(@fees)
+ describe "covered?/2" do
+ test "does not check the fees when :no_fees_transaction is passed" do
+ assert Fees.covered?(%{@eth => 0}, :no_fees_transaction)
+ end
- assert covered?(%{@eth => 1}, fees)
+ test "returns true when fees are covered by eth" do
+ assert Fees.covered?(%{@eth => 2}, @fees)
+ end
- fees =
- create_recovered([{1, 0, 0, alice}], @not_eth, [{bob, 4}, {alice, 3}])
- |> for_tx(@fees)
+ test "returns true when fees are covered by another currency" do
+ assert Fees.covered?(%{@not_eth => 5}, @fees)
+ end
- assert covered?(%{@not_eth => 3}, fees)
+ test "returns true when multiple implicit fees are given and fee is covered by eth" do
+ assert Fees.covered?(%{@eth => 2, @not_eth => 2}, @fees)
+ end
- fees =
- create_recovered(
- [{1, 0, 0, alice}, {2, 0, 0, alice}],
- [{bob, @eth, 4}, {alice, @eth, 1}, {bob, @not_eth, 5}, {alice, @not_eth, 5}]
- )
- |> for_tx(@fees)
+ test "returns true when multiple implicit fees are given and fee is covered by another currency" do
+ assert Fees.covered?(%{@eth => 0.5, @not_eth => 4}, @fees)
+ end
- assert covered?(%{@eth => 0, @not_eth => 3}, fees)
+ test "returns false when the implicit fees currency does not match any of the supported fee currencies" do
+ other_currency = <<2::160>>
+ refute Fees.covered?(%{other_currency => 100}, @fees)
end
+ # TODO: Fix this test?
@tag fixtures: [:alice, :bob]
- test "Transaction which does not transfer any fee currency is object to fees", %{alice: alice, bob: bob} do
+ test "returns true when one input is dedicated for fee payment", %{alice: alice, bob: bob} do
other_token = <<2::160>>
- fees =
- create_recovered([{1, 0, 0, alice}], other_token, [{bob, 5}, {alice, 3}])
- |> for_tx(@fees)
+ transaction =
+ create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], [{bob, other_token, 5}, {alice, other_token, 5}])
+
+ fees = Fees.for_transaction(transaction, @fees)
- assert false == covered?(%{other_token => 3}, fees)
+ assert Fees.covered?(%{@not_eth => 5, other_token => 0}, fees)
+ end
end
+ describe "for_transaction/2" do
@tag fixtures: [:alice, :bob]
- test "Transaction can dedicate one input for a fee entirely, reducing to tx's outputs currencies is incorrect",
- %{alice: alice, bob: bob} do
- other_token = <<2::160>>
-
- fees =
- create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], [{bob, other_token, 5}, {alice, other_token, 5}])
- |> for_tx(@fees)
-
- assert covered?(%{@not_eth => 5, other_token => 0}, fees)
+ test "returns the fee map when not a merge transaction (different addresses)", %{alice: alice, bob: bob} do
+ transaction = create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}])
+ assert Fees.for_transaction(transaction, @fees) == @fees
end
- describe "Merge transactions are free of cost" do
@tag fixtures: [:alice]
- test "merging utxo erases the fee", %{alice: alice} do
- fees =
- create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @not_eth, [{alice, 10}])
- |> for_tx(@fees)
-
- assert covered?(%{@not_eth => 0}, fees)
+ test "returns :no_fees_transaction for merge transactions with single currency (eth) and same in/out address (free)",
+ %{alice: alice} do
+ transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 10}])
+ assert Fees.for_transaction(transaction, @fees) == :no_fees_transaction
end
@tag fixtures: [:alice]
- test "merge is single currency transaction", %{alice: alice} do
- fees =
+ test "returns :no_fees_transaction for valid merge transactions with multiple inputs/ouputs",
+ %{alice: alice} do
+ transaction =
create_recovered(
[{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}, {2, 1, 0, alice}],
- [{alice, @eth, 10}, {alice, @not_eth, 10}]
+ [{alice, @eth, 10}, {alice, @eth, 10}]
)
- |> for_tx(@fees)
- assert not covered?(%{@eth => 0}, fees)
+ assert Fees.for_transaction(transaction, @fees) == :no_fees_transaction
end
- @tag fixtures: [:alice, :bob]
- test "merge is single same address transaction", %{alice: alice, bob: bob} do
- fees =
+ @tag fixtures: [:alice]
+ test "returns :no_fees_transaction for merge transactions with single currency (not eth) and same in/out address (free)",
+ %{alice: alice} do
+ transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @not_eth, [{alice, 10}])
+ assert Fees.for_transaction(transaction, @fees) == :no_fees_transaction
+ end
+
+ @tag fixtures: [:alice]
+ test "returns fees for merge transactions when not single currency", %{alice: alice} do
+ transaction =
create_recovered(
- [{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}],
- @eth,
- [{alice, 5}, {bob, 5}]
+ [{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}, {2, 1, 0, alice}],
+ [{alice, @eth, 10}, {alice, @not_eth, 10}]
)
- |> for_tx(@fees)
- assert not covered?(%{@eth => 0}, fees)
+ assert Fees.for_transaction(transaction, @fees) == @fees
end
end
end

diff:
@@ -151,7 +151,7 @@ defmodule OMG.State.CoreTest do
test "Merge transaction is fee free", %{alice: alice, state_empty: state} do
fees = %{@eth => 2}
tx = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 15}])
- fee = Fees.for_tx(tx, fees)
+ fee = Fees.for_transaction(tx, fees)
state
|> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})

diff:
+# Copyright 2019 OmiseGO Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+defmodule OMG.MergeTransactionValidatorTest do
+ @moduledoc false
+
+ use ExUnitFixtures
+ use ExUnit.Case, async: true
+
+ import OMG.TestHelper
+
+ alias OMG.MergeTransactionValidator
+ alias OMG.State.Transaction
+
+ @eth OMG.Eth.RootChain.eth_pseudo_address()
+ @not_eth <<1::size(160)>>
+
+ describe "is_merge_transaction?/1" do
+ @tag fixtures: [:alice]
+ test "returns true when the transaction is a payment, has less outputs than inputs, has single currency, and has same account",
+ %{alice: alice} do
+ transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 10}])
+ assert MergeTransactionValidator.is_merge_transaction?(transaction)
+ end
+
+ test "returns false when transaction is not of payment type" do
+ refute MergeTransactionValidator.is_merge_transaction?(%Transaction.Recovered{signed_tx: %{raw_tx: "fake"}})
+ end
+
+ @tag fixtures: [:alice]
+ test "returns false when transaction has as many outputs than inputs", %{alice: alice} do
+ transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 5}, {alice, 5}])
+ refute MergeTransactionValidator.is_merge_transaction?(transaction)
+ end
+
+ @tag fixtures: [:alice]
+ test "returns false when transaction has more outputs than inputs", %{alice: alice} do
+ transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 3}, {alice, 3}, {alice, 4}])
+ refute MergeTransactionValidator.is_merge_transaction?(transaction)
+ end
+
+ @tag fixtures: [:alice]
+ test "returns false when not a single currency", %{alice: alice} do
+ transaction =
+ create_recovered(
+ [{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}, {2, 1, 0, alice}],
+ [{alice, @eth, 10}, {alice, @not_eth, 10}]
+ )
+
+ refute MergeTransactionValidator.is_merge_transaction?(transaction)
+ end
+
+ @tag fixtures: [:alice, :bob]
+ test "returns false when two different accounts in outputs", %{alice: alice, bob: bob} do
+ transaction =
+ create_recovered([{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}], @eth, [{bob, 10}, {alice, 10}])
+
+ refute MergeTransactionValidator.is_merge_transaction?(transaction)
+ end
+ end
+end

diff:
@@ -37,7 +37,7 @@ defmodule OMG.ChildChain do
def submit(transaction) do
with {:ok, recovered_tx} <- Transaction.Recovered.recover_from(transaction),
{:ok, fees} <- FeeServer.transaction_fees(),
- fees = Fees.for_tx(recovered_tx, fees),
+ fees = Fees.for_transaction(recovered_tx, fees),
{:ok, {tx_hash, blknum, tx_index}} <- State.exec(recovered_tx, fees) do
{:ok, %{txhash: tx_hash, blknum: blknum, txindex: tx_index}}
end

original_message: Refactor code & tests for OMG.Fees


message: Rename :ignore to :no_fees_transaction everywhere

diff:
@@ -17,7 +17,6 @@ defmodule OMG.Fees do
Transaction's fee validation functions.
"""
- alias OMG.FeesValidator
alias OMG.MergeTransactionValidator
alias OMG.State.Transaction
alias OMG.Utxo

diff:
@@ -20,7 +20,6 @@ defmodule OMG.State.Transaction.Validator do
@maximum_block_size 65_536
alias OMG.Fees
- alias OMG.FeesValidator
alias OMG.State.Core
alias OMG.State.Transaction
alias OMG.State.UtxoSet

diff:
@@ -43,9 +43,9 @@ defmodule OMG.State.CoreTest do
test "can spend deposits", %{alice: alice, bob: bob, state_empty: state} do
state
|> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{bob, 3}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{bob, 3}]), :no_fees_transaction)
|> success?
end
@@ -55,19 +55,19 @@ defmodule OMG.State.CoreTest do
state1 =
state
|> do_deposit(alice, %{amount: 10, currency: @not_eth, blknum: 1})
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @not_eth, [{alice, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @not_eth, [{alice, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
state1
- |> Core.exec(create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 9}]), :ignore)
+ |> Core.exec(create_recovered([{1000, 0, 0, alice}], @eth, [{alice, 9}]), :no_fees_transaction)
|> fail?(:amounts_do_not_add_up)
state1
- |> Core.exec(create_recovered([{1000, 0, 0, alice}], [{alice, @eth, 9}, {alice, @not_eth, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1000, 0, 0, alice}], [{alice, @eth, 9}, {alice, @not_eth, 3}]), :no_fees_transaction)
|> fail?(:amounts_do_not_add_up)
state1
- |> Core.exec(create_recovered([{1000, 0, 0, alice}], [{alice, @not_eth, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1000, 0, 0, alice}], [{alice, @not_eth, 3}]), :no_fees_transaction)
|> success?
end
@@ -78,10 +78,10 @@ defmodule OMG.State.CoreTest do
# outputs exceed inputs, no fee
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 8}, {bob, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 8}, {bob, 3}]), :no_fees_transaction)
|> fail?(:amounts_do_not_add_up)
|> same?(state)
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 2}, {alice, 8}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 2}, {alice, 8}]), :no_fees_transaction)
|> success?
# outputs exceed inputs, with fee
@@ -93,13 +93,13 @@ defmodule OMG.State.CoreTest do
|> same?(state)
|> Core.exec(
create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, [{alice, 9}, {bob, 2}]),
- :ignore
+ :no_fees_transaction
)
|> fail?(:amounts_do_not_add_up)
|> same?(state)
|> Core.exec(
create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, [{alice, 7}, {bob, 2}]),
- :ignore
+ :no_fees_transaction
)
|> success?()
end
@@ -218,7 +218,7 @@ defmodule OMG.State.CoreTest do
|> do_deposit(alice, %{amount: 2, currency: @not_eth, blknum: 2})
|> Core.exec(
create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], [{bob, @eth, 1}, {bob, @not_eth, 2}]),
- :ignore
+ :no_fees_transaction
)
|> success?
end
@@ -229,9 +229,9 @@ defmodule OMG.State.CoreTest do
state
|> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})
|> do_deposit(bob, %{amount: 20, currency: @eth, blknum: 2})
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 10}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 10}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{2, 0, 0, bob}], @eth, [{alice, 20}]), :ignore)
+ |> Core.exec(create_recovered([{2, 0, 0, bob}], @eth, [{alice, 20}]), :no_fees_transaction)
|> success?
end
@@ -241,7 +241,7 @@ defmodule OMG.State.CoreTest do
state
|> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})
|> do_deposit(bob, %{amount: 20, currency: @eth, blknum: 2})
- |> Core.exec(create_recovered([{1, 0, 0, bob}, {2, 0, 0, alice}], @eth, [{bob, 10}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, bob}, {2, 0, 0, alice}], @eth, [{bob, 10}]), :no_fees_transaction)
|> fail?(:unauthorized_spent)
end
@@ -268,7 +268,7 @@ defmodule OMG.State.CoreTest do
state_deposit = state |> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})
state_deposit
- |> Core.exec(create_recovered([{1, 1, 0, alice}, {1, 0, 0, alice}], @eth, [{bob, 7}]), :ignore)
+ |> Core.exec(create_recovered([{1, 1, 0, alice}, {1, 0, 0, alice}], @eth, [{bob, 7}]), :no_fees_transaction)
|> fail?(:utxo_not_found)
|> same?(state_deposit)
end
@@ -276,10 +276,10 @@ defmodule OMG.State.CoreTest do
@tag fixtures: [:alice, :bob, :state_alice_deposit]
test "can't spend other people's funds", %{alice: alice, bob: bob, state_alice_deposit: state} do
state
- |> Core.exec(create_recovered([{1, 0, 0, bob}], @eth, [{bob, 8}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, bob}], @eth, [{bob, 8}, {alice, 3}]), :no_fees_transaction)
|> fail?(:unauthorized_spent)
|> same?(state)
- |> Core.exec(create_recovered([{1, 0, 0, bob}], @eth, [{alice, 10}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, bob}], @eth, [{alice, 10}]), :no_fees_transaction)
|> fail?(:unauthorized_spent)
|> same?(state)
end
@@ -288,19 +288,19 @@ defmodule OMG.State.CoreTest do
test "all inputs must be authorized to be spent", %{alice: alice, bob: bob, state_alice_deposit: state} do
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> success?()
state
- |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, bob}], @eth, []), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, bob}], @eth, []), :no_fees_transaction)
|> fail?(:unauthorized_spent)
|> same?(state)
- |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}, {@blknum1, 0, 1, alice}], @eth, []), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}, {@blknum1, 0, 1, alice}], @eth, []), :no_fees_transaction)
|> fail?(:unauthorized_spent)
|> same?(state)
state
- |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, []), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}, {@blknum1, 0, 1, alice}], @eth, []), :no_fees_transaction)
|> success?()
end
@@ -313,7 +313,7 @@ defmodule OMG.State.CoreTest do
for first <- transactions,
second <- transactions do
- state |> Core.exec(first, :ignore) |> success? |> Core.exec(second, :ignore) |> fail?(:utxo_not_found)
+ state |> Core.exec(first, :no_fees_transaction) |> success? |> Core.exec(second, :no_fees_transaction) |> fail?(:utxo_not_found)
end
end
@@ -325,13 +325,13 @@ defmodule OMG.State.CoreTest do
state_alice_deposit: state
} do
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}], @eth, [{carol, 7}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}], @eth, [{carol, 7}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{carol, 3}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{carol, 3}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{@blknum1, 1, 0, carol}, {@blknum1, 2, 0, carol}], @eth, [{alice, 10}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 1, 0, carol}, {@blknum1, 2, 0, carol}], @eth, [{alice, 10}]), :no_fees_transaction)
|> success?
end
@@ -341,9 +341,9 @@ defmodule OMG.State.CoreTest do
{:ok, {_, _, _}, state} = form_block_check(state)
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{next_block_height, 0, 0, bob}], @eth, [{bob, 7}]), :ignore)
+ |> Core.exec(create_recovered([{next_block_height, 0, 0, bob}], @eth, [{bob, 7}]), :no_fees_transaction)
|> success?
end
@@ -353,11 +353,11 @@ defmodule OMG.State.CoreTest do
{:ok, {_, _, _}, state} =
state
- |> Core.exec(recovered, :ignore)
+ |> Core.exec(recovered, :no_fees_transaction)
|> success?
|> form_block_check()
- Core.exec(state, recovered, :ignore) |> fail?(:utxo_not_found) |> same?(state)
+ Core.exec(state, recovered, :no_fees_transaction) |> fail?(:utxo_not_found) |> same?(state)
end
@tag fixtures: [:alice, :bob, :state_alice_deposit]
@@ -367,9 +367,9 @@ defmodule OMG.State.CoreTest do
assert {:ok, {%Block{hash: block_hash, number: block_number}, triggers, _}, _} =
state
- |> Core.exec(recover1, :ignore)
+ |> Core.exec(recover1, :no_fees_transaction)
|> success?
- |> Core.exec(recover2, :ignore)
+ |> Core.exec(recover2, :no_fees_transaction)
|> success?
|> form_block_check()
@@ -385,7 +385,7 @@ defmodule OMG.State.CoreTest do
assert state =
state
- |> Core.exec(recover1, :ignore)
+ |> Core.exec(recover1, :no_fees_transaction)
|> success?
assert {_, {_block, [%{submited_at_ethheight: 123}], _db_updates}, _} = Core.form_block(@interval, 123, state)
@@ -399,9 +399,9 @@ defmodule OMG.State.CoreTest do
} do
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}], @eth, [{alice, 7}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 0, bob}], @eth, [{alice, 7}]), :no_fees_transaction)
|> success?
assert {:ok, {_, [_trigger1, _trigger2], _}, _} = form_block_check(state)
@@ -414,7 +414,7 @@ defmodule OMG.State.CoreTest do
state_alice_deposit: state
} do
state
- |> Core.exec(create_recovered([{1, 1, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 1, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> same?(state)
assert {:ok, {_, [], _}, _} = form_block_check(state)
@@ -437,7 +437,7 @@ defmodule OMG.State.CoreTest do
} do
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
assert {:ok, {_, [_trigger], _}, state} = form_block_check(state)
@@ -459,11 +459,11 @@ defmodule OMG.State.CoreTest do
state =
state
- |> Core.exec(recovered_tx_1, :ignore)
+ |> Core.exec(recovered_tx_1, :no_fees_transaction)
|> success?
- |> Core.exec(recovered_tx_2, :ignore)
+ |> Core.exec(recovered_tx_2, :no_fees_transaction)
|> success?
- |> Core.exec(recovered_tx_3, :ignore)
+ |> Core.exec(recovered_tx_3, :no_fees_transaction)
|> success?
assert {:ok,
@@ -491,7 +491,7 @@ defmodule OMG.State.CoreTest do
} do
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
{:ok, {_, _, _}, state} = form_block_check(state)
@@ -517,7 +517,7 @@ defmodule OMG.State.CoreTest do
# persistence tested in-depth elsewhere
{:ok, {_, _, [_ | _]}, state} =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{bob, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
|> form_block_check()
@@ -544,7 +544,7 @@ defmodule OMG.State.CoreTest do
state =
state
- |> Core.exec(tx, :ignore)
+ |> Core.exec(tx, :no_fees_transaction)
|> success?
utxo_pos_exits = [Utxo.position(@blknum1, 0, 0), Utxo.position(@blknum1, 0, 1)]
@@ -578,7 +578,7 @@ defmodule OMG.State.CoreTest do
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, amount_1}, {alice, amount_2}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, amount_1}, {alice, amount_2}]), :no_fees_transaction)
|> success?
utxo_pos_exit_1 = Utxo.position(@blknum1, 0, 0)
@@ -589,10 +589,10 @@ defmodule OMG.State.CoreTest do
Core.exit_utxos(utxo_pos_exits, state)
state_after_exit
- |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 7}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 7}]), :no_fees_transaction)
|> fail?(:utxo_not_found)
|> same?(state_after_exit)
- |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 3}]), :no_fees_transaction)
|> fail?(:utxo_not_found)
end
@@ -601,7 +601,7 @@ defmodule OMG.State.CoreTest do
# persistence tested in-depth elsewhere
tx = create_recovered([{1, 0, 0, alice}], @eth, [{alice, 7}, {alice, 3}])
- state = state |> Core.exec(tx, :ignore) |> success?
+ state = state |> Core.exec(tx, :no_fees_transaction) |> success?
utxo_pos_exits_in_flight = [%{call_data: %{in_flight_tx: Transaction.raw_txbytes(tx)}}]
utxo_pos_exits_piggyback = [%{tx_hash: Transaction.raw_txhash(tx), output_index: 4}]
@@ -613,10 +613,10 @@ defmodule OMG.State.CoreTest do
Core.exit_utxos(utxo_pos_exits_piggyback, state)
state_after_exit
- |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 7}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 7}]), :no_fees_transaction)
|> fail?(:utxo_not_found)
|> same?(state_after_exit)
- |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 3}]), :no_fees_transaction)
|> success?
end
@@ -625,7 +625,7 @@ defmodule OMG.State.CoreTest do
# persistence tested in-depth elsewhere
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 7}, {alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 7}, {alice, 3}]), :no_fees_transaction)
|> success?
tx = create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 3}, {alice, 3}])
@@ -637,10 +637,10 @@ defmodule OMG.State.CoreTest do
Core.exit_utxos(utxo_pos_exits_in_flight, state)
state_after_exit
- |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 7}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 0, alice}], @eth, [{alice, 7}]), :no_fees_transaction)
|> fail?(:utxo_not_found)
|> same?(state_after_exit)
- |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 3}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 0, 1, alice}], @eth, [{alice, 3}]), :no_fees_transaction)
|> success?
end
@@ -667,7 +667,7 @@ defmodule OMG.State.CoreTest do
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]), :no_fees_transaction)
|> success?
assert not Core.utxo_exists?(Utxo.position(1, 0, 0), state)
@@ -694,7 +694,7 @@ defmodule OMG.State.CoreTest do
{:ok, _, state} =
state
|> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 10}]), :no_fees_transaction)
assert {@blknum1, false} = Core.get_status(state)
@@ -708,9 +708,9 @@ defmodule OMG.State.CoreTest do
test "Output can have a zero value; can't be used as input though", %{alice: alice, state_empty: state} do
state
|> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 8}, {alice, 0}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 8}, {alice, 0}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{1000, 0, 1, alice}], @eth, [{alice, 0}]), :ignore)
+ |> Core.exec(create_recovered([{1000, 0, 1, alice}], @eth, [{alice, 0}]), :no_fees_transaction)
|> fail?(:utxo_not_found)
end
@@ -718,9 +718,9 @@ defmodule OMG.State.CoreTest do
test "Output with zero value does not change oindex of other outputs", %{alice: alice, state_empty: state} do
state
|> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 0}, {alice, 8}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 0}, {alice, 8}]), :no_fees_transaction)
|> success?
- |> Core.exec(create_recovered([{1000, 0, 1, alice}], @eth, [{alice, 1}]), :ignore)
+ |> Core.exec(create_recovered([{1000, 0, 1, alice}], @eth, [{alice, 1}]), :no_fees_transaction)
|> success?
end
@@ -728,7 +728,7 @@ defmodule OMG.State.CoreTest do
test "Output with zero value will not be written to DB", %{alice: alice, state_alice_deposit: state} do
state =
state
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 0}]), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, [{alice, 0}]), :no_fees_transaction)
|> success?
{:ok, {_, _, db_updates}, _} = form_block_check(state)
@@ -739,7 +739,7 @@ defmodule OMG.State.CoreTest do
test "Transaction can have no outputs", %{alice: alice, state_empty: state} do
state
|> do_deposit(alice, %{amount: 10, currency: @eth, blknum: 1})
- |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, []), :ignore)
+ |> Core.exec(create_recovered([{1, 0, 0, alice}], @eth, []), :no_fees_transaction)
|> success?
end
@@ -754,19 +754,19 @@ defmodule OMG.State.CoreTest do
# input utxo blknum is greater than state's blknum
state
- |> Core.exec(create_recovered([{future_deposit_blknum, 0, 0, alice}], @eth, [{bob, 6}, {alice, 4}]), :ignore)
+ |> Core.exec(create_recovered([{future_deposit_blknum, 0, 0, alice}], @eth, [{bob, 6}, {alice, 4}]), :no_fees_transaction)
|> fail?(:input_utxo_ahead_of_state)
state
|> Core.exec(
create_recovered([{1, 0, 0, alice}, {future_deposit_blknum, 0, 0, alice}], @eth, [{bob, 6}, {alice, 4}]),
- :ignore
+ :no_fees_transaction
)
|> fail?(:input_utxo_ahead_of_state)
# when non-existent input comes with a blknum of the current block fail with :utxo_not_found
state
- |> Core.exec(create_recovered([{@blknum1, 1, 0, alice}], @eth, [{bob, 6}, {alice, 4}]), :ignore)
+ |> Core.exec(create_recovered([{@blknum1, 1, 0, alice}], @eth, [{bob, 6}, {alice, 4}]), :no_fees_transaction)
|> fail?(:utxo_not_found)
end

diff:
@@ -173,7 +173,7 @@ defmodule OMG.State.PersistenceTest do
end
defp exec(state, tx) do
- assert {:ok, _, state} = Core.exec(state, tx, :ignore)
+ assert {:ok, _, state} = Core.exec(state, tx, :no_fees_transaction)
state
end

diff:
@@ -410,7 +410,7 @@ defmodule OMG.State.TransactionTest do
defp assert_tx_usable(signed, state_core) do
{:ok, transaction} = signed |> Transaction.Signed.encode() |> Transaction.Recovered.recover_from()
- assert {:ok, {_, _, _}, _state} = State.Core.exec(state_core, transaction, :ignore)
+ assert {:ok, {_, _, _}, _state} = State.Core.exec(state_core, transaction, :no_fees_transaction)
end
defp parametrized_tester({inputs, outputs}) do

diff:
@@ -53,7 +53,7 @@ defmodule OMG.StateTest do
# deposits, transactions, utxo existence
assert {:ok, _} = State.deposit([%{owner: alice.addr, currency: @eth, amount: 10, blknum: 1}])
assert true == State.utxo_exists?(Utxo.position(1, 0, 0))
- assert {:ok, _} = State.exec(TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 3}]), :ignore)
+ assert {:ok, _} = State.exec(TestHelper.create_recovered([{1, 0, 0, alice}], @eth, [{alice, 3}]), :no_fees_transaction)
# block forming & status
assert {blknum, _} = State.get_status()
assert :ok = State.form_block()

diff:
@@ -40,7 +40,7 @@ defmodule OMG.ChildChain.FeeServer do
args =
case Application.get_env(:omg_child_chain, :ignore_fees) do
true ->
- :ok = save_fees(:ignore, 0)
+ :ok = save_fees(:no_fees_transaction, 0)
_ = Logger.warn("Fee specs from file are ignored.")
args

diff:
@@ -108,11 +108,11 @@ defmodule OMG.ChildChain.Integration.FeeServerTest do
end
test "fee server ignores file updates" do
- assert {:ok, :ignore} == FeeServer.transaction_fees()
+ assert {:ok, :no_fees_transaction} == FeeServer.transaction_fees()
assert refresh_fees() =~ "Updates takes no effect"
- assert {:ok, :ignore} == FeeServer.transaction_fees()
+ assert {:ok, :no_fees_transaction} == FeeServer.transaction_fees()
assert server_alive?()
end
end

diff:
@@ -108,7 +108,7 @@ defmodule OMG.Watcher.BlockGetter do
end
def handle_continue({:apply_block_step, :execute_transactions, block_application}, state) do
- tx_exec_results = for(tx <- block_application.transactions, do: OMG.State.exec(tx, :ignore))
+ tx_exec_results = for(tx <- block_application.transactions, do: OMG.State.exec(tx, :no_fees_transaction))
case Core.validate_executions(tx_exec_results, block_application, state) do
{:ok, state} ->

diff:
@@ -116,7 +116,7 @@ defmodule OMG.Watcher.BlockGetter.CoreTest do
_} = Core.get_blocks_to_apply(state, [%{blknum: block.number, eth_height: synced_height}], synced_height)
# check feasibility of transactions from block to consume at the OMG.State
- assert {:ok, tx_result, _} = OMG.State.Core.exec(state_alice_deposit, tx, :ignore)
+ assert {:ok, tx_result, _} = OMG.State.Core.exec(state_alice_deposit, tx, :no_fees_transaction)
assert {:ok, ^state} = Core.validate_executions([{:ok, tx_result}], block, state)


message: Remove intermediary validators folder

change_type: RENAME

diff:
defmodule OMG.MergeTransactionValidator do
@moduledoc """
- Checks for transaction attributes. Used by OMG.Fees.
+ Decides whether transactions qualify as "merge" transactions that use a single currency,
+ single recipient address and have fewer outputs than inputs. This decision is necessary
+ to know by the child chain to not require the transaction fees.
"""
alias OMG.State.Transaction
@@ -24,6 +26,7 @@ defmodule OMG.MergeTransactionValidator do
use OMG.Utils.LoggerExt
+ @spec is_merge_transaction?(%Transaction.Recovered{}) :: boolean()
def is_merge_transaction?(recovered_transaction) do
[
&is_payment?/1,
@@ -63,5 +66,5 @@ defmodule OMG.MergeTransactionValidator do
defp has_less_outputs_than_inputs?(inputs, outputs), do: length(inputs) >= 1 and length(inputs) > length(outputs)
- defp single?(list), do: 1 == list |> Enum.dedup() |> length()
+ defp single?(list), do: 1 == list |> Enum.uniq() |> length()
end

original_message: Remove intermediary validators folder


message: Added doctest and removed unnecessary test

diff:
@@ -28,8 +28,14 @@ defmodule OMG.Fees do
@type fee_spec_t() :: %{token: Transaction.Payment.currency(), flat_fee: non_neg_integer}
@type fee_t() :: %{Transaction.Payment.currency() => non_neg_integer} | :no_fees_transaction
- @doc """
+ @doc ~S"""
Checks whether the transaction's inputs cover the fees.
+
+ ## Examples
+
+ iex> Fees.covered?(%{"eth" => 2}, %{"eth" => 1, "omg" => 3})
+ true
+
"""
@spec covered?(implicit_paid_fee_by_currency :: map(), fees :: fee_t()) :: boolean()
def covered?(_, :no_fees_transaction), do: true
@@ -44,9 +50,15 @@ defmodule OMG.Fees do
|> Enum.any?()
end
- @doc """
+ @doc ~S"""
Returns the fees to pay for a particular transaction,
and under particular fee specs listed in `fee_map`.
+
+ ## Examples
+
+ iex> OMG.Fees.for_transaction(%OMG.State.Transaction.Recovered{}, %{"eth" => 1, "omg" => 3})
+ %{"eth" => 1, "omg" => 3}
+
"""
@spec for_transaction(Transaction.Recovered.t(), fee_t()) :: fee_t()
def for_transaction(transaction, fee_map) do

diff:
@@ -23,6 +23,8 @@ defmodule OMG.FeesTest do
alias OMG.Fees
+ doctest OMG.Fees
+
@eth OMG.Eth.RootChain.eth_pseudo_address()
@not_eth <<1::size(160)>>
@@ -36,10 +38,6 @@ defmodule OMG.FeesTest do
assert Fees.covered?(%{@eth => 0}, :no_fees_transaction)
end
- test "returns true when fees are covered by eth" do
- assert Fees.covered?(%{@eth => 2}, @fees)
- end
-
test "returns true when fees are covered by another currency" do
assert Fees.covered?(%{@not_eth => 5}, @fees)
end
@@ -73,13 +71,13 @@ defmodule OMG.FeesTest do
describe "for_transaction/2" do
@tag fixtures: [:alice, :bob]
- test "returns the fee map when not a merge transaction (different addresses)", %{alice: alice, bob: bob} do
+ test "returns the fee map when not a merge transaction", %{alice: alice, bob: bob} do
transaction = create_recovered([{1, 0, 0, alice}], @eth, [{bob, 6}, {alice, 3}])
assert Fees.for_transaction(transaction, @fees) == @fees
end
@tag fixtures: [:alice]
- test "returns :no_fees_transaction for merge transactions with single currency (eth) and same in/out address (free)",
+ test "returns :no_fees_transaction for merge transactions",
%{alice: alice} do
transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @eth, [{alice, 10}])
assert Fees.for_transaction(transaction, @fees) == :no_fees_transaction
@@ -96,23 +94,5 @@ defmodule OMG.FeesTest do
assert Fees.for_transaction(transaction, @fees) == :no_fees_transaction
end
-
- @tag fixtures: [:alice]
- test "returns :no_fees_transaction for merge transactions with single currency (not eth) and same in/out address (free)",
- %{alice: alice} do
- transaction = create_recovered([{1, 0, 0, alice}, {2, 0, 0, alice}], @not_eth, [{alice, 10}])
- assert Fees.for_transaction(transaction, @fees) == :no_fees_transaction
- end
-
- @tag fixtures: [:alice]
- test "returns fees for merge transactions when not single currency", %{alice: alice} do
- transaction =
- create_recovered(
- [{1, 0, 0, alice}, {1, 0, 1, alice}, {2, 0, 0, alice}, {2, 1, 0, alice}],
- [{alice, @eth, 10}, {alice, @not_eth, 10}]
- )
-
- assert Fees.for_transaction(transaction, @fees) == @fees
- end
end
end