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
MODIFY
apps/omg/lib/omg/state/core.ex
apps/omg/lib/omg/state/core.ex
@@ -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(),

MODIFY
apps/omg_eth/lib/eth.ex
apps/omg_eth/lib/eth.ex
@@ -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

MODIFY
apps/omg_eth/lib/omg_eth/root_chain.ex
apps/omg_eth/lib/omg_eth/root_chain.ex
@@ -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}

MODIFY
apps/omg_watcher/lib/omg_watcher/db/eth_event.ex
apps/omg_watcher/lib/omg_watcher/db/eth_event.ex
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

MODIFY
apps/omg_watcher/lib/omg_watcher/db/txoutput.ex
apps/omg_watcher/lib/omg_watcher/db/txoutput.ex
@@ -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)

ADD
apps/omg_watcher/priv/repo/migrations/20190806111817_alter_txoutputs_ethevents_make_many_to_many_relation.exs
+
+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

MODIFY
docs/watcher_db_design.md
docs/watcher_db_design.md
@@ -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;
+```

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