Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Introduce spending_txhash in invalid exit events #1492

Merged
merged 5 commits into from
May 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions apps/omg_watcher/lib/omg_watcher/event.ex
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ defmodule OMG.Watcher.Event do
:owner,
:utxo_pos,
:root_chain_txhash,
:spending_txhash,
:eth_height,
name: :invalid_exit
]
Expand All @@ -153,8 +154,9 @@ defmodule OMG.Watcher.Event do
owner: binary(),
utxo_pos: pos_integer(),
eth_height: pos_integer(),
name: atom(),
root_chain_txhash: Transaction.tx_hash() | nil
spending_txhash: Transaction.tx_hash() | nil,
root_chain_txhash: Transaction.tx_hash() | nil,
name: atom()
}
end

Expand All @@ -171,6 +173,7 @@ defmodule OMG.Watcher.Event do
:owner,
:utxo_pos,
:root_chain_txhash,
:spending_txhash,
:eth_height,
name: :unchallenged_exit
]
Expand All @@ -181,8 +184,9 @@ defmodule OMG.Watcher.Event do
owner: binary(),
utxo_pos: pos_integer(),
eth_height: pos_integer(),
name: atom(),
root_chain_txhash: Transaction.tx_hash() | nil
root_chain_txhash: Transaction.tx_hash() | nil,
spending_txhash: Transaction.tx_hash() | nil,
name: atom()
}
end

Expand Down
10 changes: 7 additions & 3 deletions apps/omg_watcher/lib/omg_watcher/exit_processor/exit_info.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ defmodule OMG.Watcher.ExitProcessor.ExitInfo do
:exiting_txbytes,
:is_active,
:eth_height,
:root_chain_txhash
:root_chain_txhash,
:spending_txhash
]
defstruct @enforce_keys

Expand All @@ -48,7 +49,8 @@ defmodule OMG.Watcher.ExitProcessor.ExitInfo do
# this means the exit has been first seen active. If false, it won't be considered harmful
is_active: boolean(),
eth_height: pos_integer(),
root_chain_txhash: Transaction.tx_hash() | nil
root_chain_txhash: Transaction.tx_hash() | nil,
spending_txhash: Transaction.tx_hash() | nil
}

@spec new(map(), map()) :: t()
Expand All @@ -73,7 +75,8 @@ defmodule OMG.Watcher.ExitProcessor.ExitInfo do
exit_id: exit_id,
exiting_txbytes: txbytes,
eth_height: eth_height,
root_chain_txhash: root_chain_txhash
root_chain_txhash: root_chain_txhash,
spending_txhash: nil
)
end

Expand Down Expand Up @@ -125,6 +128,7 @@ defmodule OMG.Watcher.ExitProcessor.ExitInfo do
exiting_txbytes: exit_info.exiting_txbytes,
is_active: exit_info.is_active,
eth_height: exit_info.eth_height,
spending_txhash: nil,
# defaults value to nil if non-existent in the DB.
root_chain_txhash: Map.get(exit_info, :root_chain_txhash)
}
Expand Down
69 changes: 59 additions & 10 deletions apps/omg_watcher/lib/omg_watcher/exit_processor/standard_exit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,30 +63,67 @@ defmodule OMG.Watcher.ExitProcessor.StandardExit do
end

@doc """
Gets all standard exits that are invalid, all and late ones separately
Gets all standard exits that are invalid, all and late ones separately, also adds their :spending_txhash
"""
@spec get_invalid(Core.t(), %{Utxo.Position.t() => boolean}, pos_integer()) ::
{%{Utxo.Position.t() => ExitInfo.t()}, %{Utxo.Position.t() => ExitInfo.t()}}
def get_invalid(%Core{sla_margin: sla_margin} = state, utxo_exists?, eth_height_now) do
active_exits = active_exits(state)

exits_invalid_by_ife =
state
|> TxAppendix.get_all()
|> get_invalid_exits_based_on_ifes(active_exits)

invalid_exit_positions =
active_exits
|> Enum.map(fn {utxo_pos, _value} -> utxo_pos end)
|> only_utxos_checked_and_missing(utxo_exists?)

tx_appendix = TxAppendix.get_all(state)
exits_invalid_by_ife = get_invalid_exits_based_on_ifes(active_exits, tx_appendix)
invalid_exits = active_exits |> Map.take(invalid_exit_positions) |> Enum.concat(exits_invalid_by_ife) |> Enum.uniq()
standard_invalid_exits =
active_exits
|> Map.take(invalid_exit_positions)
|> Enum.map(fn {utxo_pos, invalid_exit} ->
spending_txhash = spending_txhash_for_exit_at(utxo_pos)

{utxo_pos, %{invalid_exit | spending_txhash: spending_txhash}}
end)

invalid_exits = standard_invalid_exits |> Enum.concat(exits_invalid_by_ife) |> Enum.uniq()

# get exits which are still invalid and after the SLA margin
late_invalid_exits =
invalid_exits
|> Enum.filter(fn {_, %ExitInfo{eth_height: eth_height}} -> eth_height + sla_margin <= eth_height_now end)
Enum.filter(invalid_exits, fn {_, %ExitInfo{eth_height: eth_height}} ->
eth_height + sla_margin <= eth_height_now
end)

{Map.new(invalid_exits), Map.new(late_invalid_exits)}
end

defp spending_txhash_for_exit_at(utxo_pos) do
utxo_pos
|> Utxo.Position.to_input_db_key()
|> OMG.DB.spent_blknum()
|> List.wrap()
|> Core.handle_spent_blknum_result([utxo_pos])
|> do_get_blocks()
|> case do
[block] ->
%DoubleSpend{known_tx: %KnownTx{signed_tx: spending_tx}} = get_double_spend_for_standard_exit(block, utxo_pos)
Transaction.raw_txhash(spending_tx)

_ ->
nil
end
end

defp do_get_blocks(blknums) do
{:ok, hashes} = OMG.DB.block_hashes(blknums)
{:ok, blocks} = OMG.DB.blocks(hashes)

Enum.map(blocks, &Block.from_db_value/1)
end

@doc """
Determines the utxo-creating and utxo-spending blocks to get from `OMG.DB`
`se_spending_blocks_to_get` are requested by the UTXO position they spend
Expand Down Expand Up @@ -169,12 +206,24 @@ defmodule OMG.Watcher.ExitProcessor.StandardExit do
Enum.at(get_double_spends_by_utxo_pos(utxo_pos, known_tx), 0)
end

# Gets all standard exits invalidated by IFEs exiting their utxo positions
@spec get_invalid_exits_based_on_ifes(%{Utxo.Position.t() => ExitInfo.t()}, TxAppendix.t()) ::
# Gets all standard exits invalidated by IFEs exiting their utxo positions and append the spending_txhash
@spec get_invalid_exits_based_on_ifes(TxAppendix.t(), %{Utxo.Position.t() => ExitInfo.t()}) ::
list({Utxo.Position.t(), ExitInfo.t()})
defp get_invalid_exits_based_on_ifes(active_exits, tx_appendix) do
defp get_invalid_exits_based_on_ifes(tx_appendix, active_exits) do
known_txs_by_input = get_ife_txs_by_spent_input(tx_appendix)
Enum.filter(active_exits, fn {utxo_pos, _exit_info} -> Map.has_key?(known_txs_by_input, utxo_pos) end)

active_exits
|> Enum.filter(fn {utxo_pos, _exit_info} -> Map.has_key?(known_txs_by_input, utxo_pos) end)
|> Enum.map(fn {utxo_pos, exit_info} ->
spending_txhash =
known_txs_by_input
|> Map.get(utxo_pos)
|> Enum.at(0)
|> Map.get(:signed_tx)
|> Transaction.raw_txhash()

{utxo_pos, %{exit_info | spending_txhash: spending_txhash}}
end)
end

@spec get_double_spends_by_utxo_pos(Utxo.Position.t(), KnownTx.t()) :: list(DoubleSpend.t())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ defmodule OMG.Watcher.ExitProcessor.Core.StateInteractionTest do
@moduledoc """
Test talking to OMG.State.Core
"""
use ExUnit.Case, async: true
use ExUnit.Case, async: false

alias OMG.Eth.Configuration
alias OMG.State
Expand Down Expand Up @@ -44,10 +44,21 @@ defmodule OMG.Watcher.ExitProcessor.Core.StateInteractionTest do
@exit_id 9876

setup do
db_path = Briefly.create!(directory: true)
Application.put_env(:omg_db, :path, db_path, persistent: true)
:ok = OMG.DB.init()
{:ok, started_apps} = Application.ensure_all_started(:omg_db)

{:ok, processor_empty} = Core.init([], [], [])
child_block_interval = Configuration.child_block_interval()
{:ok, state_empty} = State.Core.extract_initial_state(0, child_block_interval, @fee_claimer_address)

on_exit(fn ->
Application.put_env(:omg_db, :path, nil)

Enum.map(started_apps, fn app -> :ok = Application.stop(app) end)
end)

{:ok, %{alice: TestHelper.generate_entity(), processor_empty: processor_empty, state_empty: state_empty}}
end

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ defmodule OMG.Watcher.ExitProcessor.StandardExitTest do
Test of the logic of exit processor, in the area of standard exits
"""

use OMG.Watcher.ExitProcessor.Case, async: true
use OMG.Watcher.ExitProcessor.Case, async: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an unfortunate change.


alias OMG.Block
alias OMG.State.Transaction
Expand Down Expand Up @@ -53,6 +53,17 @@ defmodule OMG.Watcher.ExitProcessor.StandardExitTest do

setup do
{:ok, empty} = Core.init([], [], [])
db_path = Briefly.create!(directory: true)
Application.put_env(:omg_db, :path, db_path, persistent: true)
:ok = OMG.DB.init()
{:ok, started_apps} = Application.ensure_all_started(:omg_db)

on_exit(fn ->
Application.put_env(:omg_db, :path, nil)

Enum.map(started_apps, fn app -> :ok = Application.stop(app) end)
end)

%{processor_empty: empty, alice: TestHelper.generate_entity(), bob: TestHelper.generate_entity()}
end

Expand Down Expand Up @@ -348,7 +359,9 @@ defmodule OMG.Watcher.ExitProcessor.StandardExitTest do
%{processor_empty: processor, alice: alice} do
exiting_pos = @utxo_pos_tx
exiting_pos_enc = Utxo.Position.encode(exiting_pos)
standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}])
# standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}])
%{signed_tx_bytes: signed_tx_bytes, tx_hash: tx_hash} =
standard_exit_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}])

request = %ExitProcessor.Request{
eth_height_now: 5,
Expand All @@ -358,15 +371,24 @@ defmodule OMG.Watcher.ExitProcessor.StandardExitTest do
}

# before the exit starts
assert {:ok, []} = request |> Core.check_validity(processor)
assert {:ok, []} = Core.check_validity(request, processor)
# after
processor = processor |> start_se_from(standard_exit_tx, exiting_pos)
assert {:ok, [%Event.InvalidExit{utxo_pos: ^exiting_pos_enc}]} = request |> Core.check_validity(processor)
processor = start_se_from(processor, standard_exit_tx, exiting_pos)

block_updates = [{:put, :block, %{number: @blknum, hash: <<0::160>>, transactions: [signed_tx_bytes]}}]
spent_blknum_updates = [{:put, :spend, {Utxo.Position.to_input_db_key(@utxo_pos_tx), @blknum}}]
:ok = OMG.DB.multi_update(block_updates ++ spent_blknum_updates)

assert {:ok, [%Event.InvalidExit{utxo_pos: ^exiting_pos_enc, spending_txhash: ^tx_hash}]} =
Core.check_validity(request, processor)
end

test "detect old invalid standard exit", %{processor_empty: processor, alice: alice} do
exiting_pos = @utxo_pos_tx
standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}])
exiting_pos_enc = Utxo.Position.encode(exiting_pos)

%{signed_tx_bytes: signed_tx_bytes, tx_hash: tx_hash} =
standard_exit_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}])

request = %ExitProcessor.Request{
eth_height_now: 50,
Expand All @@ -375,10 +397,17 @@ defmodule OMG.Watcher.ExitProcessor.StandardExitTest do
utxo_exists_result: [false]
}

processor = processor |> start_se_from(standard_exit_tx, exiting_pos)
processor = start_se_from(processor, standard_exit_tx, exiting_pos)

block_updates = [{:put, :block, %{number: @blknum, hash: <<0::160>>, transactions: [signed_tx_bytes]}}]
spent_blknum_updates = [{:put, :spend, {Utxo.Position.to_input_db_key(@utxo_pos_tx), @blknum}}]
:ok = OMG.DB.multi_update(block_updates ++ spent_blknum_updates)

assert {{:error, :unchallenged_exit}, [%Event.UnchallengedExit{}, %Event.InvalidExit{}]} =
request |> Core.check_validity(processor)
assert {{:error, :unchallenged_exit},
[
%Event.UnchallengedExit{utxo_pos: ^exiting_pos_enc, spending_txhash: ^tx_hash},
%Event.InvalidExit{utxo_pos: ^exiting_pos_enc, spending_txhash: ^tx_hash}
]} = Core.check_validity(request, processor)
end

test "invalid exits that have been witnessed already inactive don't excite events",
Expand Down Expand Up @@ -412,29 +441,36 @@ defmodule OMG.Watcher.ExitProcessor.StandardExitTest do
test "detect invalid standard exit based on ife tx which spends same input",
%{processor_empty: processor, alice: alice} do
standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}])
tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], [{alice, @eth, 1}])
%{tx_hash: tx_hash} = tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], [{alice, @eth, 1}])
exiting_pos = @utxo_pos_tx
exiting_pos_enc = Utxo.Position.encode(exiting_pos)
processor = processor |> start_se_from(standard_exit_tx, exiting_pos) |> start_ife_from(tx)

assert {:ok, [%Event.InvalidExit{utxo_pos: ^exiting_pos_enc}]} =
%ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum}
|> check_validity_filtered(processor, only: [Event.InvalidExit])
assert {:ok, [%Event.InvalidExit{utxo_pos: ^exiting_pos_enc, spending_txhash: ^tx_hash}]} =
check_validity_filtered(%ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum}, processor,
only: [Event.InvalidExit]
)
end

test "ifes and standard exits don't interfere",
%{alice: alice, processor_empty: processor, transactions: [tx | _]} do
standard_exit_tx = TestHelper.create_recovered([{@deposit_blknum, 0, 0, alice}], @eth, [{alice, 10}])
%{signed_tx_bytes: signed_tx_bytes, tx_hash: tx_hash} =
standard_exit_tx = TestHelper.create_recovered([{@blknum, 0, 0, alice}], @eth, [{alice, 10}])

processor = processor |> start_se_from(standard_exit_tx, @utxo_pos_tx) |> start_ife_from(tx)

assert %{utxos_to_check: [_, Utxo.position(1, 2, 1), @utxo_pos_tx]} =
exit_processor_request =
%ExitProcessor.Request{eth_height_now: 5, blknum_now: @late_blknum}
|> Core.determine_utxo_existence_to_get(processor)

block_updates = [{:put, :block, %{number: @blknum, hash: <<0::160>>, transactions: [signed_tx_bytes]}}]
spent_blknum_updates = [{:put, :spend, {Utxo.Position.to_input_db_key(@utxo_pos_tx), @blknum}}]
:ok = OMG.DB.multi_update(block_updates ++ spent_blknum_updates)

# here it's crucial that the missing utxo related to the ife isn't interpeted as a standard invalid exit
# that missing utxo isn't enough for any IFE-related event too
assert {:ok, [%Event.InvalidExit{}]} =
assert {:ok, [%Event.InvalidExit{spending_txhash: ^tx_hash}]} =
exit_processor_request
|> struct!(utxo_exists_result: [false, false, false])
|> check_validity_filtered(processor, exclude: [Event.PiggybackAvailable])
Expand Down
20 changes: 18 additions & 2 deletions priv/cabbage/apps/itest/test/features/in_flight_exits.feature
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,24 @@ Feature: In Flight Exits
And Alice sends the most recently created transaction
And Bob spends an output from the most recently sent transaction
And Alice starts an in flight exit from the most recently created transaction
Then Alice verifies its in flight exit from the most recently created transaction
Then "Alice" verifies its in flight exit from the most recently created transaction
Given Bob piggybacks inputs and outputs from Alices most recent in flight exit
And Bob starts a piggybacked in flight exit using his most recently prepared in flight exit data
And Alice fully challenges Bobs most recent invalid in flight exit
Then Alice can processes her own most recent in flight exit
Then "Alice" can processes its own most recent in flight exit

Scenario: Standard exit invalidated with an In Flight Exit
Given "Alice" deposits "10" ETH to the root chain
Then "Alice" should have "10" ETH on the child chain after finality margin
Given "Bob" deposits "10" ETH to the root chain
Then "Bob" should have "10" ETH on the child chain after finality margin
Given Bob sends Alice "5" ETH on the child chain
Then "Alice" should have "15" ETH on the child chain after a successful transaction
Given Alice creates a transaction spending her recently received input to Bob
And Bob starts an in flight exit from the most recently created transaction
Then "Bob" verifies its in flight exit from the most recently created transaction
Given Bob piggybacks outputs from his most recent in flight exit
And Alice starts a standard exit on the child chain from her recently received input from Bob
And Alice piggybacks inputs from Bobs most recent in flight exit
And Bob fully challenges Alices most recent invalid exit
Then "Bob" can processes its own most recent in flight exit
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

Loading