diff --git a/apps/omg_watcher/lib/omg_watcher/event.ex b/apps/omg_watcher/lib/omg_watcher/event.ex index 740c9a1acd..aeeea54205 100644 --- a/apps/omg_watcher/lib/omg_watcher/event.ex +++ b/apps/omg_watcher/lib/omg_watcher/event.ex @@ -143,6 +143,7 @@ defmodule OMG.Watcher.Event do :owner, :utxo_pos, :root_chain_txhash, + :spending_txhash, :eth_height, name: :invalid_exit ] @@ -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 @@ -171,6 +173,7 @@ defmodule OMG.Watcher.Event do :owner, :utxo_pos, :root_chain_txhash, + :spending_txhash, :eth_height, name: :unchallenged_exit ] @@ -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 diff --git a/apps/omg_watcher/lib/omg_watcher/exit_processor/exit_info.ex b/apps/omg_watcher/lib/omg_watcher/exit_processor/exit_info.ex index 615b077a58..4438718e41 100644 --- a/apps/omg_watcher/lib/omg_watcher/exit_processor/exit_info.ex +++ b/apps/omg_watcher/lib/omg_watcher/exit_processor/exit_info.ex @@ -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 @@ -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() @@ -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 @@ -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) } diff --git a/apps/omg_watcher/lib/omg_watcher/exit_processor/standard_exit.ex b/apps/omg_watcher/lib/omg_watcher/exit_processor/standard_exit.ex index 6ebe2ef7ad..a31176c388 100644 --- a/apps/omg_watcher/lib/omg_watcher/exit_processor/standard_exit.ex +++ b/apps/omg_watcher/lib/omg_watcher/exit_processor/standard_exit.ex @@ -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 @@ -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()) diff --git a/apps/omg_watcher/test/omg_watcher/exit_processor/core/state_interaction_test.exs b/apps/omg_watcher/test/omg_watcher/exit_processor/core/state_interaction_test.exs index 79ec434a8c..5fadd5b9fe 100644 --- a/apps/omg_watcher/test/omg_watcher/exit_processor/core/state_interaction_test.exs +++ b/apps/omg_watcher/test/omg_watcher/exit_processor/core/state_interaction_test.exs @@ -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 @@ -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 diff --git a/apps/omg_watcher/test/omg_watcher/exit_processor/standard_exit_test.exs b/apps/omg_watcher/test/omg_watcher/exit_processor/standard_exit_test.exs index 4d563f10fe..53b592523e 100644 --- a/apps/omg_watcher/test/omg_watcher/exit_processor/standard_exit_test.exs +++ b/apps/omg_watcher/test/omg_watcher/exit_processor/standard_exit_test.exs @@ -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 alias OMG.Block alias OMG.State.Transaction @@ -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 @@ -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, @@ -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, @@ -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", @@ -412,19 +441,22 @@ 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]} = @@ -432,9 +464,13 @@ defmodule OMG.Watcher.ExitProcessor.StandardExitTest do %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]) diff --git a/priv/cabbage/apps/itest/test/features/in_flight_exits.feature b/priv/cabbage/apps/itest/test/features/in_flight_exits.feature index 593b5ebb24..96e092629d 100644 --- a/priv/cabbage/apps/itest/test/features/in_flight_exits.feature +++ b/priv/cabbage/apps/itest/test/features/in_flight_exits.feature @@ -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 \ No newline at end of file diff --git a/priv/cabbage/apps/itest/test/itest/in_flight_exits_test.exs b/priv/cabbage/apps/itest/test/itest/in_flight_exits_test.exs index c2a7f1793b..b7ac599779 100644 --- a/priv/cabbage/apps/itest/test/itest/in_flight_exits_test.exs +++ b/priv/cabbage/apps/itest/test/itest/in_flight_exits_test.exs @@ -25,9 +25,12 @@ defmodule InFlightExitsTests do alias Itest.ApiModel.IfeInputChallenge alias Itest.ApiModel.IfeOutputChallenge alias Itest.ApiModel.SubmitTransactionResponse + alias Itest.ApiModel.Utxo alias Itest.ApiModel.WatcherSecurityCriticalConfiguration alias Itest.Client alias Itest.Fee + alias Itest.StandardExitChallengeClient + alias Itest.StandardExitClient alias Itest.Transactions.Currency alias Itest.Transactions.Encoding alias WatcherSecurityCriticalAPI.Api.InFlightExit @@ -44,6 +47,7 @@ defmodule InFlightExitsTests do import Itest.Poller, only: [ pull_for_utxo_until_recognized_deposit: 4, + pull_balance_until_amount: 2, pull_api_until_successful: 4, wait_on_receipt_confirmed: 1, all_events_in_status?: 1 @@ -415,21 +419,22 @@ defmodule InFlightExitsTests do {:ok, Map.put(state, entity, alice_state)} end - defgiven ~r/^Alice verifies its in flight exit from the most recently created transaction$/, _, state do + defgiven ~r/^"(?[^"]+)" verifies its in flight exit from the most recently created transaction$/, + %{entity: entity}, + state do exit_game_contract_address = state["exit_game_contract_address"] - %{exit_data: exit_data} = alice_state = state["Alice"] + %{exit_data: exit_data} = entity_state = state[entity] in_flight_exit_id = get_in_flight_exit_id(exit_game_contract_address, exit_data) [in_flight_exit_ids] = get_in_flight_exits(exit_game_contract_address, in_flight_exit_id) assert in_flight_exit_ids.exit_map == 0 - alice_state = - alice_state + entity_state = + entity_state |> Map.put(:in_flight_exit_id, in_flight_exit_id) |> Map.put(:in_flight_exit_ids, in_flight_exit_ids) - entity = "Alice" - {:ok, Map.put(state, entity, alice_state)} + {:ok, Map.put(state, entity, entity_state)} end defgiven ~r/^Bob piggybacks inputs and outputs from Alices most recent in flight exit$/, _, state do @@ -600,16 +605,235 @@ defmodule InFlightExitsTests do {:ok, Map.put(state, entity, alice_state)} end - defthen ~r/^Alice can processes her own most recent in flight exit$/, _, state do - %{address: address} = alice_state = state["Alice"] + defthen ~r/^"(?[^"]+)" can processes its own most recent in flight exit$/, %{entity: entity}, state do + %{address: address} = entity_state = state[entity] _ = wait_for_min_exit_period() receipt_hash = process_exits(address) assert get_next_exit_from_queue() == 0 - alice_state = Map.put(alice_state, :receipt_hashes, [receipt_hash | alice_state.receipt_hashes]) + entity_state = Map.put(entity_state, :receipt_hashes, [receipt_hash | entity_state.receipt_hashes]) + {:ok, Map.put(state, entity, entity_state)} + end + + defwhen ~r/^Bob sends Alice "(?[^"]+)" ETH on the child chain$/, + %{amount: amount}, + state do + amount = Currency.to_wei(amount) + + %{address: alice_address} = state["Alice"] + + %{address: bob_address, utxos: bob_utxos, pkey: bob_pkey, child_chain_balance: bob_child_chain_balance} = + state["Bob"] + + # inputs + bob_deposit_utxo = hd(bob_utxos) + + bob_input = %ExPlasma.Utxo{ + blknum: bob_deposit_utxo["blknum"], + currency: Currency.ether(), + oindex: 0, + txindex: 0, + output_type: 1, + owner: bob_address + } + + alice_output = %ExPlasma.Utxo{ + currency: Currency.ether(), + owner: alice_address, + amount: amount + } + + bob_output = %ExPlasma.Utxo{ + currency: Currency.ether(), + owner: bob_address, + amount: bob_child_chain_balance - amount - state["fee"] + } + + transaction = %Payment{inputs: [bob_input], outputs: [alice_output, bob_output]} + + submitted_tx = + ExPlasma.Transaction.sign(transaction, + keys: [bob_pkey] + ) + + txbytes = ExPlasma.Transaction.encode(submitted_tx) + + _submit_transaction_response = send_transaction(txbytes) + + {:ok, state} + end + + defthen ~r/^"(?[^"]+)" should have "(?[^"]+)" ETH on the child chain after a successful transaction$/, + %{entity: entity, amount: amount}, + state do + %{address: address} = entity_state = state[entity] + _ = Logger.info("#{entity} should have #{amount} ETH on the child chain after a successful transaction") + + amount = Currency.to_wei(amount) + %{"amount" => child_chain_balance} = pull_balance_until_amount(address, amount) + + {:ok, %{"data" => all_utxos}} = Client.get_utxos(%{address: address}) + + entity_state = + entity_state + |> Map.put(:utxos, all_utxos) + |> Map.put(:child_chain_balance, child_chain_balance) + + {:ok, Map.put(state, entity, entity_state)} + end + + # Alice creates a transaction sending 5 eth to bob (creates! not sends!) + defgiven ~r/^Alice creates a transaction spending her recently received input to Bob$/, + _, + state do + %{utxos: alice_utxos, pkey: alice_pkey} = alice_state = state["Alice"] + + amount = Currency.to_wei(5) + + %{address: bob_address} = state["Bob"] + + double_spent_utxo = alice_utxos |> Enum.reverse() |> Enum.at(0) + + assert double_spent_utxo["amount"] == amount + + alice_deposit_input = %ExPlasma.Utxo{ + blknum: double_spent_utxo["blknum"], + currency: double_spent_utxo["currency"], + oindex: double_spent_utxo["oindex"], + txindex: double_spent_utxo["txindex"], + output_type: double_spent_utxo["otype"], + owner: double_spent_utxo["owner"] + } + + bob_output = %ExPlasma.Utxo{ + currency: Currency.ether(), + owner: bob_address, + amount: amount - state["fee"] + } + + transaction = %Payment{inputs: [alice_deposit_input], outputs: [bob_output]} + + submitted_tx = + ExPlasma.Transaction.sign(transaction, + keys: [alice_pkey] + ) + + txbytes = ExPlasma.Transaction.encode(submitted_tx) + + ## we need to duplicate the transaction because we need an unsigned one later! + unsigned_submitted_tx = + ExPlasma.Transaction.sign(transaction, + keys: [] + ) + + unsigned_txbytes = ExPlasma.Transaction.encode(unsigned_submitted_tx) + + alice_state = + alice_state + |> Map.put(:submitted_tx, submitted_tx) + |> Map.put(:txbytes, txbytes) + |> Map.put(:unsigned_submitted_tx, unsigned_submitted_tx) + |> Map.put(:unsigned_txbytes, unsigned_txbytes) + + entity = "Alice" + {:ok, Map.put(state, entity, alice_state)} + end + + defwhen ~r/^Alice starts a standard exit on the child chain from her recently received input from Bob$/, + _, + state do + %{utxos: alice_utxos, address: alice_address} = state["Alice"] + utxo = alice_utxos |> Enum.reverse() |> Enum.at(0) + + assert utxo["amount"] == Currency.to_wei(5) + + utxo = %{blknum: utxo["blknum"], oindex: utxo["oindex"], txindex: utxo["txindex"]} + + standard_exit_client = %StandardExitClient{address: alice_address, utxo: %Utxo{utxo_pos: ExPlasma.Utxo.pos(utxo)}} + StandardExitClient.start_standard_exit(standard_exit_client) + + {:ok, state} + end + + defand ~r/^Bob starts an in flight exit from the most recently created transaction$/, _, state do + exit_game_contract_address = state["exit_game_contract_address"] + in_flight_exit_bond_size = state["in_flight_exit_bond_size"] + %{txbytes: txbytes} = state["Alice"] + %{address: bob_address} = bob_state = state["Bob"] + payload = %InFlightExitTxBytesBodySchema{txbytes: Encoding.to_hex(txbytes)} + response = pull_api_until_successful(InFlightExit, :in_flight_exit_get_data, Watcher.new(), payload) + exit_data = IfeExitData.to_struct(response) + receipt_hash = do_in_flight_exit(exit_game_contract_address, in_flight_exit_bond_size, bob_address, exit_data) + + bob_state = + bob_state + |> Map.put(:exit_data, exit_data) + |> Map.put(:receipt_hashes, [receipt_hash | bob_state.receipt_hashes]) + + entity = "Bob" + {:ok, Map.put(state, entity, bob_state)} + end + + defgiven ~r/^Bob piggybacks outputs from his most recent in flight exit$/, _, state do + exit_game_contract_address = state["exit_game_contract_address"] + + %{exit_data: exit_data, in_flight_exit_id: in_flight_exit_id, address: address} = bob_state = state["Bob"] + + output_index = 0 + + receipt_hash_1 = piggyback_output(exit_game_contract_address, address, output_index, exit_data) + + bob_state = + Map.put( + bob_state, + :receipt_hashes, + Enum.concat([receipt_hash_1], bob_state.receipt_hashes) + ) + + [in_flight_exit_ids] = get_in_flight_exits(exit_game_contract_address, in_flight_exit_id) + # bits is flagged when output is piggybacked + assert in_flight_exit_ids.exit_map != 0 + entity = "Bob" + + {:ok, Map.put(state, entity, bob_state)} + end + + defwhen ~r/^Bob fully challenges Alices most recent invalid exit$/, + _, + state do + assert all_events_in_status?(["invalid_exit"]) + + %{exit_data: %{input_utxos_pos: [utxo_pos | _]}, address: address} = state["Bob"] + + StandardExitChallengeClient.challenge_standard_exit(utxo_pos, address) + + {:ok, state} + end + + defwhen ~r/^Alice piggybacks inputs from Bobs most recent in flight exit$/, _, state do + exit_game_contract_address = state["exit_game_contract_address"] + + %{exit_data: exit_data, in_flight_exit_id: in_flight_exit_id} = state["Bob"] + %{address: address} = alice_state = state["Alice"] + + input_index = 0 + + receipt_hash_1 = piggyback_input(exit_game_contract_address, address, input_index, exit_data) + + alice_state = + Map.put( + alice_state, + :receipt_hashes, + Enum.concat([receipt_hash_1], alice_state.receipt_hashes) + ) + + [in_flight_exit_ids] = get_in_flight_exits(exit_game_contract_address, in_flight_exit_id) + # bits is flagged when input is piggybacked + assert in_flight_exit_ids.exit_map != 0 entity = "Alice" + {:ok, Map.put(state, entity, alice_state)} end