diff --git a/.circleci/config.yml b/.circleci/config.yml index 36a905fc..6dd352cc 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -304,7 +304,7 @@ jobs: _counter=$(grep -ri "child-chain" . | grep -c "child-chain") echo "Current occurrences of child-chain:" echo $_counter - if [ $_counter -gt 3 ]; then + if [ $_counter -gt 4 ]; then echo "Have you been naughty or nice? Find out if Santa knows." exit 1 fi diff --git a/apps/api/test/api/v1/controllers/block_controller_test.exs b/apps/api/test/api/v1/controllers/block_controller_test.exs index b74ea003..30ac866a 100644 --- a/apps/api/test/api/v1/controllers/block_controller_test.exs +++ b/apps/api/test/api/v1/controllers/block_controller_test.exs @@ -2,27 +2,17 @@ defmodule API.V1.Controller.BlockControllerTest do use Engine.DB.DataCase, async: true alias API.V1.Controller.BlockController - alias Engine.DB.Block - alias Engine.DB.Transaction alias ExPlasma.Encoding describe "get_by_hash/1" do test "it returns a matching block" do - _ = insert(:fee, hash: "22", type: :merged_fees) + %{block: block} = transaction = insert(:payment_v1_transaction, block: insert(:block)) - {:ok, %{:new_transaction => %{id: id}}} = - :payment_v1_transaction - |> build() - |> Transaction.insert() - - _ = Block.form() - transaction = Transaction |> Repo.get(id) |> Repo.preload(:block) - - hash = Encoding.to_hex(transaction.block.hash) + hash = Encoding.to_hex(block.hash) hex_tx_bytes = [Encoding.to_hex(transaction.tx_bytes)] assert BlockController.get_by_hash(hash) == - {:ok, %{blknum: transaction.block.blknum, hash: hash, transactions: hex_tx_bytes}} + {:ok, %{blknum: block.blknum, hash: hash, transactions: hex_tx_bytes}} end test "it returns `no_block_matching_hash` for missing blocks" do diff --git a/apps/api/test/api/v1/controllers/fee_controller_test.exs b/apps/api/test/api/v1/controllers/fee_controller_test.exs index dfb2e6d1..7226bc04 100644 --- a/apps/api/test/api/v1/controllers/fee_controller_test.exs +++ b/apps/api/test/api/v1/controllers/fee_controller_test.exs @@ -3,47 +3,10 @@ defmodule API.V1.Controller.FeeControllerTest do alias API.V1.Controller.FeeController - setup_all do - fee_specs = %{ - 1 => %{ - Base.decode16!("0000000000000000000000000000000000000000") => %{ - amount: 1, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 100, - updated_at: DateTime.from_unix!(1_546_336_800) - }, - Base.decode16!("0000000000000000000000000000000000000001") => %{ - amount: 2, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 100, - updated_at: DateTime.from_unix!(1_546_336_800) - } - }, - 2 => %{ - Base.decode16!("0000000000000000000000000000000000000000") => %{ - amount: 2, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 100, - updated_at: DateTime.from_unix!(1_546_336_800) - } - } - } - - params = [term: fee_specs, type: :current_fees] - - _ = insert(:fee, params) - - :ok - end - describe "all/1" do test "returns fees" do + insert(:current_fee) + assert FeeController.all(%{}) == {:ok, %{ @@ -81,7 +44,9 @@ defmodule API.V1.Controller.FeeControllerTest do }} end - test "filters fees" do + test "filters fees by tx_types" do + insert(:current_fee) + assert FeeController.all(%{"tx_types" => [1]}) == { :ok, %{ @@ -108,5 +73,27 @@ defmodule API.V1.Controller.FeeControllerTest do } } end + + test "filters fees by currency" do + insert(:current_fee) + + assert FeeController.all(%{"currencies" => ["0x0000000000000000000000000000000000000001"]}) == { + :ok, + %{ + "1" => [ + %{ + amount: 2, + currency: "0x0000000000000000000000000000000000000001", + pegged_amount: 1, + pegged_currency: "USD", + pegged_subunit_to_unit: 100, + subunit_to_unit: 1_000_000_000_000_000_000, + updated_at: ~U[2019-01-01 10:00:00Z] + } + ], + "2" => [] + } + } + end end end diff --git a/apps/api/test/api/v1/controllers/transaction_controller_test.exs b/apps/api/test/api/v1/controllers/transaction_controller_test.exs index 2779b3f4..4d8f52e7 100644 --- a/apps/api/test/api/v1/controllers/transaction_controller_test.exs +++ b/apps/api/test/api/v1/controllers/transaction_controller_test.exs @@ -2,25 +2,42 @@ defmodule API.V1.Controllere.TransactionControllerTest do use Engine.DB.DataCase, async: true alias API.V1.Controller.TransactionController + alias Engine.Support.TestEntity alias ExPlasma.Builder alias ExPlasma.Encoding setup do - _ = insert(:fee, hash: "55", term: :no_fees_required, type: :merged_fees) + insert(:merged_fee) :ok end describe "submit/1" do test "decodes and inserts a tx_bytes into the DB" do - txn = build(:payment_v1_transaction) - tx_hash = Encoding.to_hex(txn.tx_hash) - tx_bytes = Encoding.to_hex(txn.tx_bytes) + entity = TestEntity.alice() - assert TransactionController.submit(tx_bytes) == {:ok, %{tx_hash: tx_hash, blknum: 1_000, tx_index: 0}} + %{output_id: output_id} = insert(:deposit_output, amount: 10, output_guard: entity.addr) + %{output_data: output_data} = build(:output, output_guard: entity.addr, amount: 9) + + transaction = + Builder.new(ExPlasma.payment_v1(), %{ + inputs: [ExPlasma.Output.decode_id!(output_id)], + outputs: [ExPlasma.Output.decode!(output_data)] + }) + + tx_bytes = + transaction + |> Builder.sign!([entity.priv_encoded]) + |> ExPlasma.encode!() + |> Encoding.to_hex() + + {:ok, tx_hash} = ExPlasma.Transaction.hash(transaction) + + assert TransactionController.submit(tx_bytes) == + {:ok, %{tx_hash: Encoding.to_hex(tx_hash), blknum: 1_000, tx_index: 0}} end - test "it raises an error if the tranasaction is invalid" do + test "it raises an error if the transaction is invalid" do invalid_hex_tx_bytes = ExPlasma.payment_v1() |> Builder.new() diff --git a/apps/api/test/api/v1/router_test.exs b/apps/api/test/api/v1/router_test.exs index bc4d893e..d95fb4b3 100644 --- a/apps/api/test/api/v1/router_test.exs +++ b/apps/api/test/api/v1/router_test.exs @@ -4,56 +4,14 @@ defmodule API.V1.RouterTest do alias API.V1.Router alias Engine.DB.Block - alias Engine.DB.Fee alias Engine.DB.Transaction alias Engine.Repo + alias Engine.Support.TestEntity + alias ExPlasma.Builder alias ExPlasma.Encoding setup do - fee_specs = %{ - 1 => %{ - Base.decode16!("0000000000000000000000000000000000000000") => %{ - amount: 1, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 10, - updated_at: DateTime.from_unix!(1_546_336_800) - }, - Base.decode16!("0000000000000000000000000000000000000001") => %{ - amount: 2, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 10, - updated_at: DateTime.from_unix!(1_546_336_800) - } - }, - 2 => %{ - Base.decode16!("0000000000000000000000000000000000000000") => %{ - amount: 2, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 10, - updated_at: DateTime.from_unix!(1_546_336_800) - } - } - } - - params = [ - term: fee_specs, - type: :current_fees, - hash: - :sha256 - |> :crypto.hash(inspect(fee_specs)) - |> Base.encode16(case: :lower), - inserted_at: DateTime.add(DateTime.utc_now(), 10_000_000, :second) - ] - - _ = insert(:fee, params) - - _ = insert(:fee, hash: "11", type: :merged_fees) + _ = insert(:current_fee) %{ expected_result: %{ @@ -63,7 +21,7 @@ defmodule API.V1.RouterTest do "currency" => "0x0000000000000000000000000000000000000000", "pegged_amount" => 1, "pegged_currency" => "USD", - "pegged_subunit_to_unit" => 10, + "pegged_subunit_to_unit" => 100, "subunit_to_unit" => 1_000_000_000_000_000_000, "updated_at" => "2019-01-01T10:00:00Z" }, @@ -72,7 +30,7 @@ defmodule API.V1.RouterTest do "currency" => "0x0000000000000000000000000000000000000001", "pegged_amount" => 1, "pegged_currency" => "USD", - "pegged_subunit_to_unit" => 10, + "pegged_subunit_to_unit" => 100, "subunit_to_unit" => 1_000_000_000_000_000_000, "updated_at" => "2019-01-01T10:00:00Z" } @@ -83,7 +41,7 @@ defmodule API.V1.RouterTest do "currency" => "0x0000000000000000000000000000000000000000", "pegged_amount" => 1, "pegged_currency" => "USD", - "pegged_subunit_to_unit" => 10, + "pegged_subunit_to_unit" => 100, "subunit_to_unit" => 1_000_000_000_000_000_000, "updated_at" => "2019-01-01T10:00:00Z" } @@ -208,15 +166,28 @@ defmodule API.V1.RouterTest do describe "transaction.submit" do test "decodes a transaction and inserts it" do - Repo.delete_all(Fee) - _ = insert(:fee, hash: "77", term: :no_fees_required, type: :merged_fees) + insert(:merged_fee) + + entity = TestEntity.alice() + %{output_id: output_id} = insert(:deposit_output, amount: 10, output_guard: entity.addr) + %{output_data: output_data} = build(:output, output_guard: entity.addr, amount: 9) + + transaction = + Builder.new(ExPlasma.payment_v1(), %{ + inputs: [ExPlasma.Output.decode_id!(output_id)], + outputs: [ExPlasma.Output.decode!(output_data)] + }) + + tx_bytes = + transaction + |> Builder.sign!([entity.priv_encoded]) + |> ExPlasma.encode!() + + {:ok, tx_hash} = ExPlasma.Transaction.hash(transaction) - txn = build(:payment_v1_transaction) - tx_bytes = Encoding.to_hex(txn.tx_bytes) - tx_hash = Encoding.to_hex(txn.tx_hash) - {:ok, payload} = post("transaction.submit", %{transaction: tx_bytes}) + {:ok, payload} = post("transaction.submit", %{transaction: Encoding.to_hex(tx_bytes)}) - assert_payload_data(payload, %{"tx_hash" => tx_hash, "blknum" => 1_000, "tx_index" => 0}) + assert_payload_data(payload, %{"tx_hash" => Encoding.to_hex(tx_hash), "blknum" => 1_000, "tx_index" => 0}) end test "that it returns an error if missing transaction params" do diff --git a/apps/api/test/api/v1/views/transaction_view_test.exs b/apps/api/test/api/v1/views/transaction_view_test.exs index c795d5a3..3a4c1c3f 100644 --- a/apps/api/test/api/v1/views/transaction_view_test.exs +++ b/apps/api/test/api/v1/views/transaction_view_test.exs @@ -2,20 +2,17 @@ defmodule API.V1.View.TransactionViewTest do use Engine.DB.DataCase, async: true alias API.V1.View.TransactionView - alias Engine.DB.Transaction alias ExPlasma.Encoding describe "serialize/1" do - test "serializes a transaction" do - _ = insert(:fee, hash: "55", term: :no_fees_required, type: :merged_fees) - - transaction_changeset = build(:payment_v1_transaction) - {:ok, %{:new_transaction => transaction}} = Transaction.insert(transaction_changeset) + test "serialize a transaction" do + block = insert(:block) + transaction = insert(:payment_v1_transaction, %{block: block}) assert TransactionView.serialize(transaction) == %{ tx_hash: Encoding.to_hex(transaction.tx_hash), - blknum: 1_000, - tx_index: 0 + blknum: block.blknum, + tx_index: transaction.tx_index } end end diff --git a/apps/engine/lib/engine/callbacks/deposit.ex b/apps/engine/lib/engine/callbacks/deposit.ex index ad92bc85..f4a950fe 100644 --- a/apps/engine/lib/engine/callbacks/deposit.ex +++ b/apps/engine/lib/engine/callbacks/deposit.ex @@ -21,7 +21,6 @@ defmodule Engine.Callbacks.Deposit do alias Engine.Callback alias Engine.DB.Output alias Engine.Repo - alias ExPlasma.Output.Position @doc """ Inserts deposit events, forming the associated UTXOs. @@ -42,22 +41,10 @@ defmodule Engine.Callbacks.Deposit do defp do_callback(multi, []), do: multi defp build_deposit(multi, event) do - output_id = Position.new(event.data["blknum"], 0, 0) + deposit_blknum = event.data["blknum"] + changeset = Output.deposit(deposit_blknum, event.data["depositor"], event.data["token"], event.data["amount"]) - output_params = %{ - state: "confirmed", - output_type: ExPlasma.payment_v1(), - output_data: %{ - output_guard: event.data["depositor"], - token: event.data["token"], - amount: event.data["amount"] - }, - output_id: output_id - } - - output = Output.changeset(%Output{}, output_params) - - Ecto.Multi.insert(multi, "deposit-output-#{output_id.position}", output, + Multi.insert(multi, "deposit-#{deposit_blknum}", changeset, on_conflict: :nothing, conflict_target: :position ) diff --git a/apps/engine/lib/engine/callbacks/exit_started.ex b/apps/engine/lib/engine/callbacks/exit_started.ex index 0f38b74d..86d51592 100644 --- a/apps/engine/lib/engine/callbacks/exit_started.ex +++ b/apps/engine/lib/engine/callbacks/exit_started.ex @@ -9,8 +9,6 @@ defmodule Engine.Callbacks.ExitStarted do use Spandex.Decorators - import Ecto.Query - alias Ecto.Multi alias Engine.Callback alias Engine.DB.Output @@ -34,8 +32,5 @@ defmodule Engine.Callbacks.ExitStarted do do_callback(multi, positions ++ [position], tail) end - defp do_callback(multi, positions, []) do - query = where(Output.usable(), [output], output.position in ^positions) - Multi.update_all(multi, :exiting_outputs, query, set: [state: "exiting", updated_at: NaiveDateTime.utc_now()]) - end + defp do_callback(multi, positions, []), do: Output.exit(multi, positions) end diff --git a/apps/engine/lib/engine/callbacks/in_flight_exit_started.ex b/apps/engine/lib/engine/callbacks/in_flight_exit_started.ex index 3bcb8739..8d7bb4d3 100644 --- a/apps/engine/lib/engine/callbacks/in_flight_exit_started.ex +++ b/apps/engine/lib/engine/callbacks/in_flight_exit_started.ex @@ -9,8 +9,6 @@ defmodule Engine.Callbacks.InFlightExitStarted do use Spandex.Decorators - import Ecto.Query - alias Ecto.Multi alias Engine.Callback alias Engine.DB.Output @@ -34,8 +32,5 @@ defmodule Engine.Callbacks.InFlightExitStarted do do_callback(multi, positions ++ inputs, tail) end - defp do_callback(multi, positions, []) do - query = where(Output.usable(), [output], output.position in ^positions) - Multi.update_all(multi, :exiting_outputs, query, set: [state: "exiting", updated_at: NaiveDateTime.utc_now()]) - end + defp do_callback(multi, positions, []), do: Output.exit(multi, positions) end diff --git a/apps/engine/lib/engine/callbacks/piggyback.ex b/apps/engine/lib/engine/callbacks/piggyback.ex index e5cc3a4a..2e24d6ea 100644 --- a/apps/engine/lib/engine/callbacks/piggyback.ex +++ b/apps/engine/lib/engine/callbacks/piggyback.ex @@ -9,8 +9,6 @@ defmodule Engine.Callbacks.Piggyback do use Spandex.Decorators - import Ecto.Changeset, only: [change: 2] - alias Ecto.Multi alias Engine.Callback alias Engine.DB.Output @@ -37,30 +35,25 @@ defmodule Engine.Callbacks.Piggyback do # For us, we keep track of the history to some degree(e.g state change). # # See: https://github.com/omisego/elixir-omg/blob/8189b812b4b3cf9256111bd812235fb342a6fd50/apps/omg/lib/omg/state/utxo_set.ex#L81 - defp piggyback(multi, %{data: %{"input_index" => index, "tx_hash" => tx_hash}}) do - do_piggyback(multi, :inputs, index, tx_hash) - end + # + # We shouldn't have to do anything with input being piggybacked + # See: https://github.com/omgnetwork/elixir-omg/blob/652023025f0cc53370e77802af5659c72eab0592/docs/exit_validation.md#notes-on-the-child-chain-server + defp piggyback(multi, %{data: %{"input_index" => _index}}), do: multi defp piggyback(multi, %{data: %{"output_index" => index, "tx_hash" => tx_hash}}) do - do_piggyback(multi, :outputs, index, tx_hash) - end - - defp do_piggyback(multi, type, index, tx_hash) do - tx_hash - |> Transaction.query_by_tx_hash() - |> Engine.Repo.one() - |> Engine.Repo.preload(type) - |> get_output(type, index) - |> set_as_piggybacked(multi, tx_hash, type) + [tx_hash: tx_hash] + |> Transaction.get_by(:outputs) + |> get_output(index) + |> set_as_piggybacked(multi, tx_hash) end - defp get_output(nil, _type, _index), do: nil - defp get_output(transaction, type, index), do: transaction |> Map.get(type) |> Enum.at(index) + defp get_output(nil, _index), do: nil + defp get_output(transaction, index), do: Enum.at(transaction.outputs, index) - defp set_as_piggybacked(%Output{state: "confirmed"} = output, multi, tx_hash, type) do - changeset = change(output, state: "piggybacked") - Multi.update(multi, "piggyback-#{tx_hash}-#{type}-#{output.position}", changeset) + defp set_as_piggybacked(%Output{state: :confirmed} = output, multi, tx_hash) do + changeset = Output.piggyback(output) + Multi.update(multi, "piggyback-#{tx_hash}-#{output.position}", changeset) end - defp set_as_piggybacked(_, multi, _tx_hash, _type), do: multi + defp set_as_piggybacked(_, multi, _tx_hash), do: multi end diff --git a/apps/engine/lib/engine/db/output.ex b/apps/engine/lib/engine/db/output.ex index 57c96f70..3eb14f48 100644 --- a/apps/engine/lib/engine/db/output.ex +++ b/apps/engine/lib/engine/db/output.ex @@ -16,18 +16,21 @@ defmodule Engine.DB.Output do - output_data: The binary encoded output data, for payment v1 and fees, this is the RLP encoded binary of the output type, owner, token and amount. - output_id: The binary encoded output id, this is the result of the encoding of the position - state: The current output state: - - "pending": the default state when creating an output - - "confirmed": the output is confirmed on the rootchain - - "exiting": the output is beeing exited - - "piggybacked": the output is a part of an IFE and has been piggybacked + - :pending - the default state when creating an output + - :confirmed - the output is confirmed on the rootchain + - :spent - the output is spent by a transaction + - :exiting - the output is being exited + - :piggybacked - the output is a part of an IFE and has been piggybacked """ use Ecto.Schema - import Ecto.Changeset - import Ecto.Query, only: [from: 2] - + alias __MODULE__.OutputChangeset + alias __MODULE__.OutputQuery + alias Ecto.Atom + alias Ecto.Multi alias Engine.DB.Transaction + alias ExPlasma.Output.Position @type t() :: %{ creating_transaction: Transaction.t(), @@ -46,15 +49,18 @@ defmodule Engine.DB.Output do @timestamps_opts [inserted_at: :node_inserted_at, updated_at: :node_updated_at] + @states [:pending, :confirmed, :spent, :exiting, :piggybacked] + + def states(), do: @states + schema "outputs" do - # Extracted from `output_id` + field(:output_id, :binary) field(:position, :integer) field(:output_type, :integer) field(:output_data, :binary) - field(:output_id, :binary) - field(:state, :string, default: "pending") + field(:state, Atom) belongs_to(:spending_transaction, Engine.DB.Transaction) belongs_to(:creating_transaction, Engine.DB.Transaction) @@ -66,65 +72,58 @@ defmodule Engine.DB.Output do end @doc """ - Default changset. Generates the Output and ensures - that it meets the state-less validations. + Generates an output changeset corresponding to a deposit output being inserted. + The output state is `:confirmed`. """ - def changeset(struct, params) do - struct - |> cast(params, [:state, :output_type]) - |> extract_position(params) - |> encode_output_data(params) - |> encode_output_id(params) + @spec deposit(pos_integer(), <<_::160>>, <<_::160>>, pos_integer()) :: Ecto.Changeset.t() + def deposit(blknum, depositor, token, amount) do + params = %{ + state: :confirmed, + output_type: ExPlasma.payment_v1(), + output_data: %{ + output_guard: depositor, + token: token, + amount: amount + }, + output_id: Position.new(blknum, 0, 0) + } + + OutputChangeset.deposit(%__MODULE__{}, params) end @doc """ - Query to return all usable outputs. + Generates an output changeset corresponding to a new output being inserted. + The output state is `:pending`. """ - def usable() do - from(o in __MODULE__, - where: is_nil(o.spending_transaction_id) and o.state == "confirmed" - ) + @spec new(%__MODULE__{}, map()) :: Ecto.Changeset.t() + def new(struct, params) do + OutputChangeset.new(struct, Map.put(params, :state, :pending)) end - # Extract the position from the output id and store it on the table. - # Used by the Transaction to find outputs quickly. - defp extract_position(changeset, %{output_id: %{position: position}}) do - put_change(changeset, :position, position) + @doc """ + Generates an output changeset corresponding to an output being spent. + The output state is `:spent`. + """ + @spec spend(%__MODULE__{}, map()) :: Ecto.Changeset.t() + def spend(struct, _params) do + OutputChangeset.state(struct, %{state: :spent}) end - defp extract_position(changeset, _), do: put_change(changeset, :position, nil) - - # Doing this hacky work around so we don't need to convert to/from binary to hex string for the json column. - # Instead, we re-encoded as rlp encoded items per specification. This helps us future proof it a bit because - # we don't necessarily know what the future output data looks like yet. If there's data we need, we can - # at a later time pull them out and turn them into columns. - defp encode_output_data(changeset, params) do - case Map.get(params, :output_data) do - nil -> - changeset - - _output_data -> - {:ok, output_data} = - %ExPlasma.Output{} - |> struct(params) - |> ExPlasma.Output.encode() - - put_change(changeset, :output_data, output_data) - end + @doc """ + Generates an output changeset corresponding to an output being piggybacked. + The output state is `:piggybacked`. + """ + @spec piggyback(%__MODULE__{}) :: Ecto.Changeset.t() + def piggyback(output) do + OutputChangeset.state(output, %{state: :piggybacked}) end - defp encode_output_id(changeset, params) do - case Map.get(params, :output_id) do - nil -> - changeset - - _output_id -> - {:ok, output_id} = - %ExPlasma.Output{} - |> struct(params) - |> ExPlasma.Output.encode(as: :input) - - put_change(changeset, :output_id, output_id) - end + @doc """ + Updates the given multi by setting all outputs found at the given `positions` to an `:exiting` state. + """ + @spec exit(Multi.t(), list(pos_integer())) :: Multi.t() + def exit(multi, positions) do + query = OutputQuery.usable_for_positions(positions) + Multi.update_all(multi, :exiting_outputs, query, set: [state: :exiting, updated_at: NaiveDateTime.utc_now()]) end end diff --git a/apps/engine/lib/engine/db/output/output_changeset.ex b/apps/engine/lib/engine/db/output/output_changeset.ex new file mode 100644 index 00000000..065bf55a --- /dev/null +++ b/apps/engine/lib/engine/db/output/output_changeset.ex @@ -0,0 +1,98 @@ +defmodule Engine.DB.Output.OutputChangeset do + @moduledoc """ + Contains changesets related to outputs + """ + + import Ecto.Changeset + + alias Engine.DB.Output + + @doc """ + Changeset for deposits. + This updates: + - position + - output_id + - output_data + - output_type + - state (should be :confirmed) + """ + def deposit(output, params) do + output + |> state(params) + |> output_position(params) + |> output_data(params) + end + + @doc """ + Changeset for new outputs (created by transactions). + This updates: + - output_data + - output_type + - state (should be :pending) + """ + def new(output, params) do + output + |> state(params) + |> output_data(params) + end + + @doc """ + Changeset for output state change. + This updates: + - state (should be :pending) + """ + def state(output, params) do + output + |> cast(params, [:state]) + |> validate_required([:state]) + |> validate_inclusion(:state, Output.states()) + end + + defp output_data(output, params) do + output + |> cast(params, [:output_type]) + |> put_output_data(params) + |> validate_required([:output_type, :output_data]) + end + + defp output_position(output, params) do + output + |> put_position(params) + |> put_output_id(params) + |> validate_required([:output_id, :position]) + end + + # Extract the position from the output id and store it on the table. + # Used by the Transaction to find outputs quickly. + defp put_position(changeset, %{output_id: %{position: position}}) do + put_change(changeset, :position, position) + end + + defp put_position(changeset, _), do: changeset + + # Doing this hacky work around so we don't need to convert to/from binary to hex string for the json column. + # Instead, we re-encoded as rlp encoded items per specification. This helps us future proof it a bit because + # we don't necessarily know what the future output data looks like yet. If there's data we need, we can + # at a later time pull them out and turn them into columns. + defp put_output_data(changeset, %{output_data: nil}), do: changeset + + defp put_output_data(changeset, params) do + {:ok, output_data} = + %ExPlasma.Output{} + |> struct(params) + |> ExPlasma.Output.encode() + + put_change(changeset, :output_data, output_data) + end + + defp put_output_id(changeset, %{output_id: nil}), do: changeset + + defp put_output_id(changeset, params) do + {:ok, output_id} = + %ExPlasma.Output{} + |> struct(params) + |> ExPlasma.Output.encode(as: :input) + + put_change(changeset, :output_id, output_id) + end +end diff --git a/apps/engine/lib/engine/db/output/output_query.ex b/apps/engine/lib/engine/db/output/output_query.ex new file mode 100644 index 00000000..0ff256d2 --- /dev/null +++ b/apps/engine/lib/engine/db/output/output_query.ex @@ -0,0 +1,28 @@ +defmodule Engine.DB.Output.OutputQuery do + @moduledoc """ + Contains queries related to outputs + """ + + import Ecto.Query, only: [from: 2] + + alias Engine.DB.Output + + @doc """ + Return all `:confirmed` outputs without a spending transaction that have the given positions. + """ + def usable_for_positions(positions) do + Output |> usable() |> by_position(positions) + end + + defp usable(query) do + from(o in query, + where: is_nil(o.spending_transaction_id) and o.state == ^:confirmed + ) + end + + defp by_position(query, positions) do + from(o in query, + where: o.position in ^positions + ) + end +end diff --git a/apps/engine/lib/engine/db/transaction.ex b/apps/engine/lib/engine/db/transaction.ex index 6bbdd575..fac8d527 100644 --- a/apps/engine/lib/engine/db/transaction.ex +++ b/apps/engine/lib/engine/db/transaction.ex @@ -26,7 +26,7 @@ defmodule Engine.DB.Transaction do """ use Ecto.Schema - import Ecto.Changeset, only: [cast: 3, cast_assoc: 2, validate_required: 2] + import Ecto.Changeset, only: [cast: 3, cast_assoc: 3, validate_required: 2] import Ecto.Query, only: [from: 2] alias Ecto.Multi @@ -91,6 +91,16 @@ defmodule Engine.DB.Transaction do """ def query_by_tx_hash(tx_hash), do: from(t in __MODULE__, where: t.tx_hash == ^tx_hash) + @doc """ + Query a transaction by the given `field`. + Also preload given `preloads` + """ + def get_by(field, preloads) do + __MODULE__ + |> Repo.get_by(field) + |> Repo.preload(preloads) + end + @doc """ The main action of the system. Takes tx_bytes and forms the appropriate associations for the transaction and outputs and runs the changeset. @@ -98,9 +108,8 @@ defmodule Engine.DB.Transaction do @spec decode(tx_bytes) :: {:ok, Ecto.Changeset.t()} | {:error, atom()} def decode(tx_bytes) do with {:ok, decoded} <- ExPlasma.decode(tx_bytes), - {:ok, recovered} <- ExPlasma.Transaction.with_witnesses(decoded) do - {:ok, fees} = Fee.accepted_fees() - + {:ok, recovered} <- ExPlasma.Transaction.with_witnesses(decoded), + {:ok, fees} <- load_fees(recovered.tx_type) do params = recovered |> recovered_to_map() @@ -120,14 +129,11 @@ defmodule Engine.DB.Transaction do def changeset(struct, params) do struct - |> Repo.preload(:inputs) - |> Repo.preload(:outputs) |> cast(params, @optional_fields ++ @required_fields) |> validate_required(@required_fields) - |> cast_assoc(:inputs) - |> cast_assoc(:outputs) |> Validator.validate_protocol() - |> Validator.validate_inputs() + |> Validator.associate_inputs(params) + |> cast_assoc(:outputs, with: &Output.new/2) |> Validator.validate_statefully(params) end @@ -147,6 +153,13 @@ defmodule Engine.DB.Transaction do |> Repo.transaction() end + defp load_fees(type) do + with {:ok, all_fees} when is_map(all_fees) <- Fee.accepted_fees(), + fees_for_type <- Map.get(all_fees, type, {:error, :invalid_transaction_type}) do + {:ok, fees_for_type} + end + end + defp recovered_to_map(transaction) do inputs = Enum.map(transaction.inputs, &Map.from_struct/1) outputs = Enum.map(transaction.outputs, &Map.from_struct/1) diff --git a/apps/engine/lib/engine/db/transaction/validator.ex b/apps/engine/lib/engine/db/transaction/validator.ex index a82e5572..13979c67 100644 --- a/apps/engine/lib/engine/db/transaction/validator.ex +++ b/apps/engine/lib/engine/db/transaction/validator.ex @@ -5,8 +5,7 @@ defmodule Engine.DB.Transaction.Validator do their insertion in the DB. """ - import Ecto.Changeset, only: [get_field: 2, add_error: 3, put_change: 3] - import Ecto.Query, only: [where: 3] + import Ecto.Changeset, only: [get_field: 2, add_error: 3, put_assoc: 3] alias Engine.DB.Output alias Engine.DB.Transaction.PaymentV1 @@ -35,28 +34,30 @@ defmodule Engine.DB.Transaction.Validator do end @doc """ - Validates that the given changesets inputs are correct. To create a transaction with inputs: + Validates that the given input positions are correct. To create a transaction with inputs: * The position for the input must exist. * The position for the input must not have been spent. - If so, associate the records to this transaction. + If so, loads and associates the records to this transaction keeping the order and setting their state to :spent. Returns the changeset with associated input or an error. """ - @spec validate_inputs(Ecto.Changeset.t()) :: Ecto.Changeset.t() - def validate_inputs(changeset) do - given_input_positions = get_input_positions(changeset) - usable_inputs = given_input_positions |> usable_outputs_for() |> Repo.all() + @spec associate_inputs(Ecto.Changeset.t(), map()) :: Ecto.Changeset.t() + def associate_inputs(changeset, params) do + given_input_positions = Enum.map(params.inputs, & &1.output_id.position) + usable_inputs = given_input_positions |> Output.OutputQuery.usable_for_positions() |> Repo.all() usable_input_positions = Enum.map(usable_inputs, & &1.position) case given_input_positions -- usable_input_positions do [] -> - sorted_usable_inputs = + ordered_spent_inputs = Enum.map(given_input_positions, fn given_input_position -> - Enum.find(usable_inputs, &(&1.position == given_input_position)) + usable_inputs + |> Enum.find(&(&1.position == given_input_position)) + |> Output.spend(%{}) end) - put_change(changeset, :inputs, sorted_usable_inputs) + put_assoc(changeset, :inputs, ordered_spent_inputs) missing_inputs -> add_error(changeset, :inputs, "inputs #{inspect(missing_inputs)} are missing, spent, or not yet available") @@ -82,15 +83,6 @@ defmodule Engine.DB.Transaction.Validator do end # Private - defp get_input_positions(changeset) do - changeset |> get_field(:inputs) |> Enum.map(&Map.get(&1, :position)) - end - - # Return all confirmed outputs that have the given positions. - defp usable_outputs_for(positions) do - where(Output.usable(), [output], output.position in ^positions) - end - defp process_protocol_validation_results(:ok, changeset), do: changeset defp process_protocol_validation_results({:error, {field, message}}, changeset) do diff --git a/apps/engine/lib/engine/fee/server.ex b/apps/engine/lib/engine/fee/server.ex index c6009993..5ff39a0d 100644 --- a/apps/engine/lib/engine/fee/server.ex +++ b/apps/engine/lib/engine/fee/server.ex @@ -173,7 +173,7 @@ defmodule Engine.Fee.Server do end defp update_merged_fees(new_fee_specs) do - # we will update merged fees only if previouse merged fees are expired, i.e. + # we will update merged fees only if previous merged fees are expired, i.e. # previous_fees are deleted _ = if is_nil(load_previous_fees()) do diff --git a/apps/engine/test/engine/callbacks/deposit_test.exs b/apps/engine/test/engine/callbacks/deposit_test.exs index c0849b9a..db5e91d1 100644 --- a/apps/engine/test/engine/callbacks/deposit_test.exs +++ b/apps/engine/test/engine/callbacks/deposit_test.exs @@ -7,12 +7,6 @@ defmodule Engine.Callbacks.DepositTest do alias Engine.DB.ListenerState alias Engine.DB.Output - setup do - _ = insert(:fee, type: :merged_fees) - - :ok - end - describe "callback/2" do test "generates a confirmed output for the deposit" do token = <<0::160>> @@ -22,7 +16,7 @@ defmodule Engine.Callbacks.DepositTest do deposit_event = build(:deposit_event, token: token, amount: amount, blknum: blknum, depositor: depositor) - assert {:ok, %{"deposit-output-3000000000" => %Output{} = output}} = Deposit.callback([deposit_event], :depositor) + assert {:ok, %{"deposit-3" => %Output{} = output}} = Deposit.callback([deposit_event], :depositor) assert ExPlasma.Output.decode!(output.output_data) == %ExPlasma.Output{ output_data: %{amount: amount, token: token, output_guard: depositor}, @@ -42,8 +36,8 @@ defmodule Engine.Callbacks.DepositTest do assert {:ok, %{ - "deposit-output-6000000000" => %Output{}, - "deposit-output-5000000000" => %Output{} + "deposit-6" => %Output{}, + "deposit-5" => %Output{} }} = Deposit.callback(events, :depositor) end @@ -51,10 +45,9 @@ defmodule Engine.Callbacks.DepositTest do event = build(:deposit_event, blknum: 1) events = [event, build(:deposit_event, blknum: 2)] - assert {:ok, %{"deposit-output-1000000000" => _}} = Deposit.callback([event], :depositor) + assert {:ok, %{"deposit-1" => _}} = Deposit.callback([event], :depositor) - assert {:ok, %{"deposit-output-1000000000" => _, "deposit-output-2000000000" => _}} = - Deposit.callback(events, :depositor) + assert {:ok, %{"deposit-1" => _, "deposit-2" => _}} = Deposit.callback(events, :depositor) deposit_outputs = all_sorted_outputs() @@ -80,8 +73,7 @@ defmodule Engine.Callbacks.DepositTest do build(:deposit_event, blknum: 5, height: 406) ] - assert {:ok, %{"deposit-output-3000000000" => _, "deposit-output-4000000000" => _}} = - Deposit.callback(deposit_events_listener1, :depositor) + assert {:ok, %{"deposit-3" => _, "deposit-4" => _}} = Deposit.callback(deposit_events_listener1, :depositor) assert listener_for(:depositor, height: 405) @@ -91,7 +83,7 @@ defmodule Engine.Callbacks.DepositTest do assert Enum.at(deposit_outputs_1, 0).position == 3_000_000_000 assert Enum.at(deposit_outputs_1, 1).position == 4_000_000_000 - assert {:ok, %{"deposit-output-5000000000" => _}} = Deposit.callback(deposit_events_listener2, :depositor) + assert {:ok, %{"deposit-5" => _}} = Deposit.callback(deposit_events_listener2, :depositor) assert listener_for(:depositor, height: 406) diff --git a/apps/engine/test/engine/callbacks/exit_started_test.exs b/apps/engine/test/engine/callbacks/exit_started_test.exs index b05ace26..fa072d58 100644 --- a/apps/engine/test/engine/callbacks/exit_started_test.exs +++ b/apps/engine/test/engine/callbacks/exit_started_test.exs @@ -17,7 +17,7 @@ defmodule Engine.Callbacks.ExitStartedTest do assert listener_for(:exit_started, height: 100) query = from(o in Output, where: o.position == ^position, select: o.state) - assert Repo.one(query) == "exiting" + assert Repo.one(query) == :exiting end test "marks multiple utxos as exiting" do @@ -34,7 +34,7 @@ defmodule Engine.Callbacks.ExitStartedTest do assert listener_for(:exit_started, height: 102) query = from(o in Output, where: o.position in [^pos1, ^pos2], select: o.state) - assert Repo.all(query) == ["exiting", "exiting"] + assert Repo.all(query) == [:exiting, :exiting] end test "returns {:ok, :noop} when no event given" do diff --git a/apps/engine/test/engine/callbacks/in_flight_exit_started_test.exs b/apps/engine/test/engine/callbacks/in_flight_exit_started_test.exs index 7c1ef11c..e508d634 100644 --- a/apps/engine/test/engine/callbacks/in_flight_exit_started_test.exs +++ b/apps/engine/test/engine/callbacks/in_flight_exit_started_test.exs @@ -16,7 +16,7 @@ defmodule Engine.Callbacks.InFlightExitStartedTest do assert listener_for(:in_flight_exit_started, height: 100) query = from(o in Output, where: o.position == ^position, select: o.state) - assert Repo.one(query) == "exiting" + assert Repo.one(query) == :exiting end test "marks multiple inputs in a single IFE that are exiting" do @@ -30,7 +30,7 @@ defmodule Engine.Callbacks.InFlightExitStartedTest do assert listener_for(:in_flight_exit_started, height: 101) query = from(o in Output, where: o.position in [^pos1, ^pos2], select: o.state) - assert Repo.all(query) == ["exiting", "exiting"] + assert Repo.all(query) == [:exiting, :exiting] end test "marks multiple IFEs as exiting" do @@ -49,7 +49,7 @@ defmodule Engine.Callbacks.InFlightExitStartedTest do assert listener_for(:in_flight_exit_started, height: 102) query = from(o in Output, where: o.position in [^pos1, ^pos2, ^pos3, ^pos4], select: o.state) - assert Repo.all(query) == ["exiting", "exiting", "exiting", "exiting"] + assert Repo.all(query) == [:exiting, :exiting, :exiting, :exiting] end test "returns {:ok, :noop} when no event given" do diff --git a/apps/engine/test/engine/callbacks/piggyback_test.exs b/apps/engine/test/engine/callbacks/piggyback_test.exs index 7a6b9293..c40338c6 100644 --- a/apps/engine/test/engine/callbacks/piggyback_test.exs +++ b/apps/engine/test/engine/callbacks/piggyback_test.exs @@ -6,58 +6,28 @@ defmodule Engine.Callbacks.PiggybackTest do alias Engine.DB.Output alias Engine.Repo - setup do - _ = insert(:fee, hash: "10", term: :no_fees_required, type: :merged_fees) - - :ok - end - describe "callback/2" do - test "marks an input as piggybacked" do - %{inputs: [input]} = transaction = insert(:payment_v1_transaction) - assert input.state == "confirmed" - - events = [build(:input_piggyback_event, tx_hash: transaction.tx_hash, input_index: 0, height: 405)] - key = "piggyback-#{transaction.tx_hash}-inputs-#{input.position}" - - assert {:ok, %{^key => input}} = Piggyback.callback(events, :piggybacker) - assert input.state == "piggybacked" - assert listener_for(:piggybacker, height: 405) - end - test "marks an output as piggybacked" do %{outputs: [output]} = transaction = insert(:payment_v1_transaction) - output |> change(state: "confirmed") |> Repo.update() + output |> change(state: :confirmed) |> Repo.update() events = [build(:output_piggyback_event, tx_hash: transaction.tx_hash, output_index: 0, height: 404)] - key = "piggyback-#{transaction.tx_hash}-outputs-#{output.position}" + key = "piggyback-#{transaction.tx_hash}-#{output.position}" assert {:ok, %{^key => output}} = Piggyback.callback(events, :piggybacker) - assert output.state == "piggybacked" - assert listener_for(:piggybacker, height: 404) - end - - test "doesn't mark input as piggyback if its unusable" do - %{inputs: [input]} = transaction = insert(:payment_v1_transaction) - input |> change(state: "spent") |> Repo.update() - - events = [build(:input_piggyback_event, tx_hash: transaction.tx_hash, input_index: 0, height: 404)] - - assert {:ok, multi} = Piggyback.callback(events, :piggybacker) - refute is_map_key(multi, "piggyback-#{transaction.tx_hash}-inputs-#{input.position}") - assert Repo.get(Output, input.id).state == "spent" + assert output.state == :piggybacked assert listener_for(:piggybacker, height: 404) end test "doesn't mark output as piggyback if its unusable" do %{outputs: [output]} = transaction = insert(:payment_v1_transaction) - output |> change(state: "exited") |> Repo.update() + output |> change(state: :exited) |> Repo.update() - events = [build(:input_piggyback_event, tx_hash: transaction.tx_hash, output_index: 0, height: 404)] + events = [build(:output_piggyback_event, tx_hash: transaction.tx_hash, output_index: 0, height: 404)] assert {:ok, multi} = Piggyback.callback(events, :piggybacker) - refute is_map_key(multi, "piggyback-#{transaction.tx_hash}-outputs-#{output.position}") - assert Repo.get(Output, output.id).state == "exited" + refute is_map_key(multi, "piggyback-#{transaction.tx_hash}-#{output.position}") + assert Repo.get(Output, output.id).state == :exited assert listener_for(:piggybacker, height: 404) end diff --git a/apps/engine/test/engine/db/block_test.exs b/apps/engine/test/engine/db/block_test.exs index 928e47e3..0375a6f4 100644 --- a/apps/engine/test/engine/db/block_test.exs +++ b/apps/engine/test/engine/db/block_test.exs @@ -8,12 +8,6 @@ defmodule Engine.DB.BlockTest do alias Engine.Repo alias ExPlasma.Merkle - setup do - _ = insert(:fee, type: :merged_fees) - - :ok - end - describe "form/0" do test "forms a block with all transaction associated with it" do _ = insert_transaction(:payment_v1_transaction) @@ -166,8 +160,8 @@ defmodule Engine.DB.BlockTest do test "fails to insert two block with the same hash" do assert_raise Ecto.ConstraintError, ~r/blocks_hash_index/, fn -> - _ = insert(:block, hash: "1", blknum: 2000) - _ = insert(:block, hash: "1", blknum: 5000) + _ = insert(:block, hash: "1", blknum: 2000, nonce: 2) + _ = insert(:block, hash: "1", blknum: 5000, nonce: 5) end end end diff --git a/apps/engine/test/engine/db/output/output_changeset_test.exs b/apps/engine/test/engine/db/output/output_changeset_test.exs new file mode 100644 index 00000000..a06ef2dd --- /dev/null +++ b/apps/engine/test/engine/db/output/output_changeset_test.exs @@ -0,0 +1,102 @@ +defmodule Engine.DB.Output.OutputChangesetTest do + use Engine.DB.DataCase, async: true + + alias Ecto.Changeset + alias Engine.DB.Output + alias Engine.DB.Output.OutputChangeset + alias ExPlasma.Output.Position + + describe "deposit/2" do + test "generates a deposit changeset with input data, posision, output data and state" do + params = %{ + state: :confirmed, + output_type: ExPlasma.payment_v1(), + output_data: %{ + output_guard: <<1::160>>, + token: <<0::160>>, + amount: 10 + }, + output_id: Position.new(1, 0, 0) + } + + changeset = OutputChangeset.deposit(%Output{}, params) + encoded_output_data = encoded_output_data(params) + encoded_output_id = encoded_output_id(params) + position = position(params) + + assert changeset.valid? + + assert %Output{ + output_data: ^encoded_output_data, + output_id: ^encoded_output_id, + output_type: 1, + position: ^position, + state: :confirmed + } = Changeset.apply_changes(changeset) + end + end + + describe "new/2" do + test "generates a changeset for a new ouutput with output data and state" do + params = %{ + state: :pending, + output_type: ExPlasma.payment_v1(), + output_data: %{ + output_guard: <<1::160>>, + token: <<0::160>>, + amount: 10 + } + } + + changeset = OutputChangeset.new(%Output{}, params) + encoded_output_data = encoded_output_data(params) + + assert changeset.valid? + + assert %Output{ + output_data: ^encoded_output_data, + output_id: nil, + output_type: 1, + position: nil, + state: :pending + } = Changeset.apply_changes(changeset) + end + end + + describe "state/2" do + test "generates a state change changeset" do + assert %{state: :pending} = insert(:output) + + params = %{ + state: :confirmed + } + + changeset = OutputChangeset.state(%Output{}, params) + assert changeset.valid? + + assert %Output{ + state: :confirmed + } = Changeset.apply_changes(changeset) + end + end + + defp encoded_output_data(params) do + {:ok, encoded_output_data} = + %ExPlasma.Output{} + |> struct(params) + |> ExPlasma.Output.encode() + + encoded_output_data + end + + defp encoded_output_id(params) do + {:ok, encoded_output_id} = + %ExPlasma.Output{} + |> struct(params) + |> ExPlasma.Output.encode(as: :input) + + encoded_output_id + end + + defp position(params), do: params.output_id.position +end diff --git a/apps/engine/test/engine/db/output/output_query_test.exs b/apps/engine/test/engine/db/output/output_query_test.exs new file mode 100644 index 00000000..4f28f062 --- /dev/null +++ b/apps/engine/test/engine/db/output/output_query_test.exs @@ -0,0 +1,18 @@ +defmodule Engine.DB.Output.OutputQueryTest do + use Engine.DB.DataCase, async: true + + alias Engine.DB.Output + alias Engine.DB.Output.OutputQuery + alias Engine.Repo + + describe "usable_for_positions/1" do + test "returns :confirmed output without a spending_transaction and with matching position" do + %{position: p_1} = insert(:deposit_output) + %{position: p_2} = :deposit_output |> insert() |> Output.spend(%{}) |> Engine.Repo.update!() + :output |> insert() |> Output.piggyback() |> Repo.update!() + insert(:deposit_output) + + assert [%{position: ^p_1}] = [p_1, p_2] |> OutputQuery.usable_for_positions() |> Repo.all() + end + end +end diff --git a/apps/engine/test/engine/db/output_test.exs b/apps/engine/test/engine/db/output_test.exs index 0c5815bd..cf773069 100644 --- a/apps/engine/test/engine/db/output_test.exs +++ b/apps/engine/test/engine/db/output_test.exs @@ -2,32 +2,99 @@ defmodule Engine.DB.OutputTest do use Engine.DB.DataCase, async: true doctest Engine.DB.Output, import: true - alias ExPlasma.Output.Position + alias Ecto.Changeset + alias Engine.DB.Output - describe "changeset/2" do - test "populates the position column" do - {:ok, output_id} = %{blknum: 1, txindex: 0, oindex: 0} |> Position.pos() |> Position.to_map() - output = build(:output, output_id: output_id) + describe "deposit/4" do + test "returns a deposit changeset" do + blknum = 1 + token = <<0::160>> + depositor = <<1::160>> + amount = 10 - assert output_id.position == output.position + changeset = Output.deposit(blknum, depositor, token, amount) + + assert changeset.valid? + + assert %Output{ + output_data: encoded_output_data, + output_id: encoded_output_id, + output_type: 1, + position: position, + state: :confirmed + } = Changeset.apply_changes(changeset) + + assert %{output_data: %{amount: ^amount, output_guard: ^depositor, token: ^token}} = + ExPlasma.Output.decode!(encoded_output_data) + + assert %{output_id: %{blknum: ^blknum, oindex: 0, position: 1_000_000_000, txindex: 0}} = + ExPlasma.Output.decode_id!(encoded_output_id) end + end - test "encodes the output_data" do - data = %{output_guard: <<1::160>>, token: <<0::160>>, amount: 1} - params = %ExPlasma.Output{output_id: nil, output_data: data, output_type: 1} - {:ok, encoded} = ExPlasma.Output.encode(params) + describe "new/2" do + test "returns a changeset for a new :pending output" do + token = <<0::160>> + output_guard = <<1::160>> + amount = 10 - output = build(:output, output_data: data) + params = %{ + output_type: ExPlasma.payment_v1(), + output_data: %{ + output_guard: output_guard, + token: token, + amount: amount + } + } - assert encoded == output.output_data + changeset = Output.new(%Output{}, params) + + assert changeset.valid? + + assert %Output{ + output_data: encoded_output_data, + output_id: nil, + output_type: 1, + position: nil, + state: :pending + } = Changeset.apply_changes(changeset) + + assert %{output_data: %{amount: ^amount, output_guard: ^output_guard, token: ^token}} = + ExPlasma.Output.decode!(encoded_output_data) end + end + + describe "spend/2" do + test "returns a changeset with a state updated to :spent" do + assert %{state: :confirmed} = output = insert(:deposit_output) + + changeset = Output.spend(output, %{}) + + assert changeset.valid? + assert %Output{state: :spent} = Changeset.apply_changes(changeset) + end + end + + describe "piggyback/2" do + test "returns a changeset with a state updated to :piggybacked" do + assert %{state: :confirmed} = output = insert(:deposit_output) + + changeset = Output.piggyback(output) + + assert changeset.valid? + assert %Output{state: :piggybacked} = Changeset.apply_changes(changeset) + end + end - test "encodes the output_id" do - {:ok, output_id} = %{blknum: 1, txindex: 0, oindex: 0} |> Position.pos() |> Position.to_map() - {:ok, encoded} = ExPlasma.Output.encode(%ExPlasma.Output{output_id: output_id}, as: :input) - output = build(:output, output_id: output_id) + describe "exit/2" do + test "returns an updated multi with state of outputs for positions updated to :exiting" do + %{position: p_1} = insert(:deposit_output) + %{position: p_2} = :deposit_output |> insert() |> Output.spend(%{}) |> Engine.Repo.update!() + :output |> insert() |> Output.piggyback() |> Repo.update!() + insert(:deposit_output) - assert encoded == output.output_id + multi = Output.exit(Ecto.Multi.new(), [p_1, p_2]) + assert Engine.Repo.transaction(multi) == {:ok, %{exiting_outputs: {1, nil}}} end end end diff --git a/apps/engine/test/engine/db/transaction/validator_test.exs b/apps/engine/test/engine/db/transaction/validator_test.exs index 7889898f..be87ef6b 100644 --- a/apps/engine/test/engine/db/transaction/validator_test.exs +++ b/apps/engine/test/engine/db/transaction/validator_test.exs @@ -8,77 +8,66 @@ defmodule Engine.DB.Transaction.ValidatorTest do alias ExPlasma.Builder alias ExPlasma.Output - describe "validate_inputs/1" do + describe "associate_inputs/2" do test "associate inputs if all inputs are usable in the correct order" do - i_1 = build_input(3000, 0, 0) - i_1_in_db = Repo.get(DbOutput, insert(:output, Map.put(i_1, :state, "confirmed")).id) + %{output_id: output_id_1} = insert(:deposit_output) + %{output_id: %{position: i_1_position}} = i_1 = build_input(output_id_1) - i_2 = build_input(4000, 0, 0) - i_2_in_db = Repo.get(DbOutput, insert(:output, Map.put(i_2, :state, "confirmed")).id) + %{output_id: output_id_2} = insert(:deposit_output) + %{output_id: %{position: i_2_position}} = i_2 = build_input(output_id_2) - i_3 = build_input(2000, 0, 0) - i_3_in_db = Repo.get(DbOutput, insert(:output, Map.put(i_3, :state, "confirmed")).id) + %{output_id: output_id_3} = insert(:deposit_output) + %{output_id: %{position: i_3_position}} = i_3 = build_input(output_id_3) - i_4 = build_input(1, 0, 0) - i_4_in_db = Repo.get(DbOutput, insert(:output, Map.put(i_4, :state, "confirmed")).id) + %{output_id: output_id_4} = insert(:deposit_output) + %{output_id: %{position: i_4_position}} = i_4 = build_input(output_id_4) changeset = - [i_3, i_2, i_4, i_1] - |> build_changeset_with_inputs() - |> Validator.validate_inputs() + %Transaction{} + |> change(%{}) + |> Validator.associate_inputs(%{inputs: [i_3, i_2, i_4, i_1]}) assert changeset.valid? - assert get_field(changeset, :inputs) == - [i_3_in_db, i_2_in_db, i_4_in_db, i_1_in_db] + assert [ + %{position: ^i_3_position}, + %{position: ^i_2_position}, + %{position: ^i_4_position}, + %{position: ^i_1_position} + ] = get_field(changeset, :inputs) end test "returns an error if inputs don't exist" do - i_1 = build_input(1, 0, 0) - i_2 = build_input(2, 0, 0) - i_3 = build_input(3, 0, 0) - - insert(:output, Map.put(i_1, :state, "confirmed")) + %{output_id: output_id_1} = insert(:deposit_output) + i_1 = build_input(output_id_1) + %{output_id: %{position: i_2_position}} = i_2 = build_input(2, 0, 0) + %{output_id: %{position: i_3_position}} = i_3 = build_input(3, 0, 0) changeset = - [i_1, i_2, i_3] - |> build_changeset_with_inputs() - |> Validator.validate_inputs() + %Transaction{} + |> change(%{}) + |> Validator.associate_inputs(%{inputs: [i_1, i_2, i_3]}) refute changeset.valid? - assert {"inputs [2000000000, 3000000000] are missing, spent, or not yet available", _} = changeset.errors[:inputs] - end - - test "returns an error if inputs are spent" do - i_1 = build_input(1, 0, 0) - i_2 = build_input(2, 0, 0) - - insert(:output, Map.put(i_1, :state, "confirmed")) - insert(:output, Map.put(i_2, :state, "spent")) - changeset = - [i_1, i_2] - |> build_changeset_with_inputs() - |> Validator.validate_inputs() - - refute changeset.valid? - assert {"inputs [2000000000] are missing, spent, or not yet available", _} = changeset.errors[:inputs] + assert changeset.errors[:inputs] == + {"inputs [#{i_2_position}, #{i_3_position}] are missing, spent, or not yet available", []} end - test "returns an error if inputs are pending" do - i_1 = build_input(1, 0, 0) - i_2 = build_input(2, 0, 0) + test "returns an error if inputs are spent" do + %{output_id: output_id_1} = insert(:deposit_output) + %{output_id: output_id_2, state: :spent} = :deposit_output |> insert() |> DbOutput.spend(%{}) |> Repo.update!() - insert(:output, Map.put(i_1, :state, "confirmed")) - insert(:output, Map.put(i_2, :state, "pending")) + i_1 = build_input(output_id_1) + %{output_id: %{position: i_2_position}} = i_2 = build_input(output_id_2) changeset = - [i_1, i_2] - |> build_changeset_with_inputs() - |> Validator.validate_inputs() + %Transaction{} + |> change(%{}) + |> Validator.associate_inputs(%{inputs: [i_1, i_2]}) refute changeset.valid? - assert {"inputs [2000000000] are missing, spent, or not yet available", _} = changeset.errors[:inputs] + assert changeset.errors[:inputs] == {"inputs [#{i_2_position}] are missing, spent, or not yet available", []} end end @@ -196,17 +185,11 @@ defmodule Engine.DB.Transaction.ValidatorTest do end end - defp build_input(blknum, oindex, txindex) do - map = %{blknum: blknum, oindex: oindex, txindex: txindex} - output_id = Map.put(map, :position, Output.Position.pos(map)) + defp build_input(blknum, txindex, oindex) do + output_id = Output.Position.new(blknum, txindex, oindex) Map.from_struct(%Output{output_id: output_id}) end - defp build_changeset_with_inputs(inputs) do - %Transaction{} - |> Repo.preload(:inputs) - |> cast(%{inputs: inputs}, []) - |> cast_assoc(:inputs) - end + defp build_input(output_id), do: output_id |> Output.decode_id!() |> Map.from_struct() end diff --git a/apps/engine/test/engine/db/transaction_test.exs b/apps/engine/test/engine/db/transaction_test.exs index 08807baf..c819465a 100644 --- a/apps/engine/test/engine/db/transaction_test.exs +++ b/apps/engine/test/engine/db/transaction_test.exs @@ -2,26 +2,24 @@ defmodule Engine.DB.TransactionTest do use Engine.DB.DataCase, async: true doctest Engine.DB.Transaction, import: true - alias Engine.DB.Block alias Engine.DB.Output alias Engine.DB.Transaction - alias Engine.Repo alias Engine.Support.TestEntity alias ExPlasma.Builder @max_txcount 65_000 setup do - _ = insert(:fee, hash: "22", type: :merged_fees) + _ = insert(:merged_fee) :ok end describe "insert/1" do test "attaches transaction to a forming block" do - block = insert(:block, %{}) + block = insert(:block) - changeset = build(:payment_v1_transaction, %{blknum: 1}) + changeset = build(:payment_v1_transaction) {:ok, %{:new_transaction => tx}} = Transaction.insert(changeset) assert tx.block.id == block.id @@ -29,10 +27,10 @@ defmodule Engine.DB.TransactionTest do end test "assigns consecutive transaction indicies" do - changeset1 = build(:payment_v1_transaction, %{blknum: 1}) + changeset1 = build(:payment_v1_transaction) {:ok, %{:new_transaction => tx1}} = Transaction.insert(changeset1) - changeset2 = build(:payment_v1_transaction, %{blknum: 2}) + changeset2 = build(:payment_v1_transaction) {:ok, %{:new_transaction => tx2}} = Transaction.insert(changeset2) assert tx1.tx_index + 1 == tx2.tx_index @@ -78,7 +76,7 @@ defmodule Engine.DB.TransactionTest do test "builds the outputs" do input_blknum = 1 - insert(:output, %{blknum: input_blknum, state: "confirmed"}) + insert(:deposit_output, %{blknum: input_blknum}) o_1_data = [token: <<0::160>>, amount: 10, output_guard: <<1::160>>] o_2_data = [token: <<0::160>>, amount: 10, output_guard: <<1::160>>] @@ -101,7 +99,7 @@ defmodule Engine.DB.TransactionTest do test "builds the inputs" do input_blknum = 1 - input = Repo.get(Output, insert(:output, %{blknum: input_blknum, state: "confirmed"}).id) + assert %{id: id, state: :confirmed} = insert(:deposit_output, %{blknum: input_blknum}) tx_bytes = ExPlasma.payment_v1() @@ -113,40 +111,44 @@ defmodule Engine.DB.TransactionTest do assert {:ok, changeset} = Transaction.decode(tx_bytes) - assert get_field(changeset, :inputs) == [input] + assert [spent_input] = get_field(changeset, :inputs) + assert spent_input.id == id + assert spent_input.state == :spent end test "is valid when inputs are signed correctly" do - _ = - insert(:fee, - type: :merged_fees, - term: :no_fees_required, - inserted_at: DateTime.add(DateTime.utc_now(), 10_000_000, :second) - ) - %{priv_encoded: priv_encoded_1, addr: addr_1} = TestEntity.alice() %{priv_encoded: priv_encoded_2, addr: addr_2} = TestEntity.bob() - data_1 = %{output_guard: addr_1, token: <<0::160>>, amount: 10} - data_2 = %{output_guard: addr_2, token: <<0::160>>, amount: 10} - insert(:output, %{output_data: data_1, blknum: 1, state: "confirmed"}) - insert(:output, %{output_data: data_2, blknum: 2, state: "confirmed"}) + insert(:deposit_output, %{output_guard: addr_1, token: <<0::160>>, amount: 10, blknum: 1}) + insert(:deposit_output, %{output_guard: addr_2, token: <<0::160>>, amount: 10, blknum: 2}) tx_bytes = ExPlasma.payment_v1() |> Builder.new() |> Builder.add_input(blknum: 1, txindex: 0, oindex: 0) |> Builder.add_input(blknum: 2, txindex: 0, oindex: 0) - |> Builder.add_output(output_guard: <<1::160>>, token: <<0::160>>, amount: 20) + |> Builder.add_output(output_guard: <<1::160>>, token: <<0::160>>, amount: 19) |> Builder.sign!([priv_encoded_1, priv_encoded_2]) |> ExPlasma.encode!() assert {:ok, changeset} = Transaction.decode(tx_bytes) - assert changeset.valid? end end + describe "get_by/2" do + test "returns the transaction given a query and preloads" do + %{id: id_1, inputs: [%{id: input_id}]} = insert(:payment_v1_transaction) + %{tx_hash: tx_hash_2} = insert(:payment_v1_transaction) + + assert %{id: ^id_1, inputs: [%{id: ^input_id}]} = Transaction.get_by([id: id_1], :inputs) + + assert %{tx_hash: ^tx_hash_2, inputs: %Ecto.Association.NotLoaded{}} = + Transaction.get_by([tx_hash: tx_hash_2], []) + end + end + describe "query_pending/0" do test "get all pending transactions" do block = insert(:block) diff --git a/apps/engine/test/support/db/factory.ex b/apps/engine/test/support/db/factory.ex index fd2b5975..16d816e8 100644 --- a/apps/engine/test/support/db/factory.ex +++ b/apps/engine/test/support/db/factory.ex @@ -5,7 +5,6 @@ defmodule Engine.DB.Factory do use ExMachina.Ecto, repo: Engine.Repo - alias Ecto.Changeset alias Engine.DB.Block alias Engine.DB.Fee alias Engine.DB.Output @@ -15,21 +14,6 @@ defmodule Engine.DB.Factory do alias ExPlasma.Builder alias ExPlasma.Output.Position - def input_piggyback_event_factory(attr \\ %{}) do - tx_hash = Map.get(attr, :tx_hash, <<1::256>>) - index = Map.get(attr, :input_index, 0) - - params = - attr - |> Map.put(:signature, "InFlightExitInputPiggybacked(address,bytes32,uint16)") - |> Map.put(:data, %{ - "tx_hash" => tx_hash, - "input_index" => index - }) - - build(:event, params) - end - def output_piggyback_event_factory(attr \\ %{}) do tx_hash = Map.get(attr, :tx_hash, <<1::256>>) index = Map.get(attr, :output_index, 0) @@ -106,140 +90,178 @@ defmodule Engine.DB.Factory do end def deposit_output_factory(attr \\ %{}) do - blknum = Map.get(attr, :blknum, 1) - output_guard = Map.get(attr, :output_guard) || <<1::160>> + entity = TestEntity.alice() + + default_blknum = sequence(:blknum, fn seq -> seq + 1 end) + + blknum = Map.get(attr, :blknum, default_blknum) + output_guard = Map.get(attr, :output_guard, entity.addr) amount = Map.get(attr, :amount, 1) token = Map.get(attr, :token, <<0::160>>) - output_id = Position.new(blknum, 0, 0) - - output_params = %{ - state: "confirmed", - output_type: ExPlasma.payment_v1(), - output_data: %{ - output_guard: output_guard, - token: token, - amount: amount - }, - output_id: output_id - } + {:ok, encoded_output_data} = + %ExPlasma.Output{} + |> struct(%{ + output_type: ExPlasma.payment_v1(), + output_data: %{ + output_guard: output_guard, + token: token, + amount: amount + } + }) + |> ExPlasma.Output.encode() - %Output{} - |> Output.changeset(output_params) - |> Changeset.apply_changes() - end + output_id = Position.new(blknum, 0, 0) - def transaction_factory(params) do - tx_bytes = params[:tx_bytes] - {:ok, hash} = ExPlasma.hash(tx_bytes) + {:ok, encoded_output_id} = + %ExPlasma.Output{} + |> struct(%{output_id: output_id}) + |> ExPlasma.Output.encode(as: :input) - %Transaction{ - tx_bytes: tx_bytes, - tx_type: 1, - tx_hash: hash + %Output{ + state: :confirmed, + output_type: ExPlasma.payment_v1(), + output_data: encoded_output_data, + output_id: encoded_output_id, + position: output_id.position } end - def payment_v1_transaction_factory(attr) do + def payment_v1_transaction_factory() do entity = TestEntity.alice() - priv_encoded = Map.get(attr, :priv_encoded, entity.priv_encoded) - addr = Map.get(attr, :addr, entity.addr) - - data = %{output_guard: addr, token: <<0::160>>, amount: 1} - default_blknum = sequence(:blknum, fn seq -> (seq + 1) * 1000 end) - insert(:output, %{output_data: data, blknum: Map.get(attr, :blknum, default_blknum), state: "confirmed"}) + %{output_id: output_id} = input = :deposit_output |> build() |> set_state(:spent) + %{output_data: output_data} = output = build(:output) tx_bytes = - attr[:tx_bytes] || - ExPlasma.payment_v1() - |> Builder.new() - |> Builder.add_input(blknum: Map.get(attr, :blknum, default_blknum), txindex: 0, oindex: 0) - |> Builder.add_output(output_guard: <<1::160>>, token: <<0::160>>, amount: 1) - |> Builder.sign!([priv_encoded]) - |> ExPlasma.encode!() - - {:ok, changeset} = Transaction.decode(tx_bytes) - Changeset.apply_changes(changeset) + ExPlasma.payment_v1() + |> Builder.new(%{inputs: [ExPlasma.Output.decode_id!(output_id)], outputs: [ExPlasma.Output.decode!(output_data)]}) + |> Builder.sign!([entity.priv_encoded]) + |> ExPlasma.encode!() + + {:ok, tx_hash} = ExPlasma.Transaction.hash(tx_bytes) + + %Transaction{ + inputs: [input], + outputs: [output], + tx_bytes: tx_bytes, + tx_hash: tx_hash, + tx_index: 0, + tx_type: ExPlasma.payment_v1(), + inserted_at: DateTime.truncate(DateTime.utc_now(), :second), + updated_at: DateTime.truncate(DateTime.utc_now(), :second) + } end # The "lowest" unit in the hierarchy. This is made to form into transactions def output_factory(attr \\ %{}) do - default_data = %{output_guard: <<1::160>>, token: <<0::160>>, amount: 10} - default_blknum = sequence(:blknum, fn seq -> (seq + 1) * 1000 end) - - {:ok, default_id} = - %{blknum: Map.get(attr, :blknum, default_blknum), txindex: 0, oindex: 0} - |> Position.pos() - |> Position.to_map() - - %Output{} - |> Output.changeset(%{ - output_type: Map.get(attr, :output_type, 1), - output_id: Map.get(attr, :output_id, default_id), - output_data: Map.get(attr, :output_data, default_data), - state: Map.get(attr, :state, "pending") - }) - |> Changeset.apply_changes() - end + default_data = %{ + output_guard: Map.get(attr, :output_guard, <<1::160>>), + token: Map.get(attr, :token, <<0::160>>), + amount: Map.get(attr, :amount, 10) + } - def spent(%Transaction{outputs: [output]} = txn), do: %{txn | outputs: [%{output | state: "spent"}]} + default_blknum = sequence(:blknum, fn seq -> (seq + 1) * 1000 end) + default_txindex = sequence(:txindex, fn seq -> seq + 1 end) + default_oindex = sequence(:oindex, fn seq -> seq + 1 end) + + default_output_id = + Position.new( + Map.get(attr, :blknum, default_blknum), + Map.get(attr, :txindex, default_txindex), + Map.get(attr, :oindex, default_oindex) + ) + + {:ok, encoded_output_data} = + %ExPlasma.Output{} + |> struct(%{ + output_type: Map.get(attr, :output_type, 1), + output_data: Map.get(attr, :output_data, default_data) + }) + |> ExPlasma.Output.encode() - def set_state(%Transaction{outputs: [output]}, state), do: %{output | state: state} - def set_state(%Output{} = output, state), do: %{output | state: state} + {:ok, encoded_output_id} = + %ExPlasma.Output{} + |> struct(%{output_id: Map.get(attr, :output_id, default_output_id)}) + |> ExPlasma.Output.encode(as: :input) - def block_factory(attr \\ %{}) do - blknum = Map.get(attr, :blknum, 1000) - _child_block_interval = 1000 - nonce = round(blknum / 1000) + %Output{ + state: :pending, + output_type: ExPlasma.payment_v1(), + output_data: encoded_output_data, + output_id: encoded_output_id, + position: default_output_id.position + } + end + def block_factory() do %Block{ - hash: Map.get(attr, :hash) || :crypto.strong_rand_bytes(32), - nonce: nonce, - blknum: blknum, - state: Map.get(attr, :state, :forming), - txcount: Map.get(attr, :txcount, 0), + hash: :crypto.strong_rand_bytes(32), + nonce: 1, + blknum: 1000, + state: :forming, + txcount: 0, tx_hash: :crypto.strong_rand_bytes(64), formed_at_ethereum_height: 1, - submitted_at_ethereum_height: Map.get(attr, :submitted_at_ethereum_height, 1), - attempts_counter: Map.get(attr, :attempts_counter), + submitted_at_ethereum_height: 1, + attempts_counter: 0, gas: 827 } end - def fee_factory(params) do - fees = - params[:term] || - %{ - 1 => %{ - Base.decode16!("0000000000000000000000000000000000000000") => %{ - amount: 1, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 100, - updated_at: DateTime.from_unix!(1_546_336_800) - }, - Base.decode16!("0000000000000000000000000000000000000001") => %{ - amount: 2, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 100, - updated_at: DateTime.from_unix!(1_546_336_800) - } - }, - 2 => %{ - Base.decode16!("0000000000000000000000000000000000000000") => %{ - amount: 2, - subunit_to_unit: 1_000_000_000_000_000_000, - pegged_amount: 1, - pegged_currency: "USD", - pegged_subunit_to_unit: 100, - updated_at: DateTime.from_unix!(1_546_336_800) - } - } + def merged_fee_factory() do + fees = %{ + 1 => %{ + Base.decode16!("0000000000000000000000000000000000000000") => [1, 2], + Base.decode16!("0000000000000000000000000000000000000001") => [1] + }, + 2 => %{Base.decode16!("0000000000000000000000000000000000000000") => [1]} + } + + hash = + :sha256 + |> :crypto.hash(inspect(fees)) + |> Base.encode16(case: :lower) + + %Fee{ + type: :merged_fees, + term: fees, + hash: hash, + inserted_at: DateTime.utc_now() + } + end + + def current_fee_factory() do + fees = %{ + 1 => %{ + Base.decode16!("0000000000000000000000000000000000000000") => %{ + amount: 1, + subunit_to_unit: 1_000_000_000_000_000_000, + pegged_amount: 1, + pegged_currency: "USD", + pegged_subunit_to_unit: 100, + updated_at: DateTime.from_unix!(1_546_336_800) + }, + Base.decode16!("0000000000000000000000000000000000000001") => %{ + amount: 2, + subunit_to_unit: 1_000_000_000_000_000_000, + pegged_amount: 1, + pegged_currency: "USD", + pegged_subunit_to_unit: 100, + updated_at: DateTime.from_unix!(1_546_336_800) } + }, + 2 => %{ + Base.decode16!("0000000000000000000000000000000000000000") => %{ + amount: 2, + subunit_to_unit: 1_000_000_000_000_000_000, + pegged_amount: 1, + pegged_currency: "USD", + pegged_subunit_to_unit: 100, + updated_at: DateTime.from_unix!(1_546_336_800) + } + } + } hash = :sha256 @@ -247,10 +269,12 @@ defmodule Engine.DB.Factory do |> Base.encode16(case: :lower) %Fee{ - type: params[:type] || :current_fees, + type: :current_fees, term: fees, - hash: params[:hash] || hash, - inserted_at: params[:inserted_at] + hash: hash, + inserted_at: DateTime.utc_now() } end + + defp set_state(%Output{} = output, state), do: %{output | state: state} end