From 3fcb21d0a5ed02a58318b966a0c8b4c878eb77ce Mon Sep 17 00:00:00 2001 From: Pawel Gebal Date: Mon, 21 Sep 2020 17:57:11 +0200 Subject: [PATCH] get txindex on inserting tx draft --- .../callbacks/in_flight_exit_started.ex | 1 + apps/engine/lib/engine/callbacks/piggyback.ex | 3 + apps/engine/lib/engine/db/block.ex | 70 ++++++++++++++++++- apps/engine/lib/engine/db/output.ex | 5 +- apps/engine/lib/engine/db/transaction.ex | 17 ++++- .../20200326103841_create_blocks_table.exs | 13 ++++ .../20200422044422_create_transactions.exs | 1 + .../test/engine/db/transaction_test.exs | 13 ++++ apps/engine/test/support/db/factory.ex | 2 + .../support/{entity.ex => test_entity.ex} | 0 10 files changed, 120 insertions(+), 5 deletions(-) rename apps/engine/test/support/{entity.ex => test_entity.ex} (100%) 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 4a60d127..1c5e9054 100644 --- a/apps/engine/lib/engine/callbacks/in_flight_exit_started.ex +++ b/apps/engine/lib/engine/callbacks/in_flight_exit_started.ex @@ -34,6 +34,7 @@ defmodule Engine.Callbacks.InFlightExitStarted do defp do_callback(multi, positions, []) do query = where(Output.usable(), [output], output.position in ^positions) + # how precise do we want to be in tracking output state? Should we have statuses exited, challenged? Multi.update_all(multi, :exiting_outputs, query, set: [state: "exiting", updated_at: NaiveDateTime.utc_now()]) end end diff --git a/apps/engine/lib/engine/callbacks/piggyback.ex b/apps/engine/lib/engine/callbacks/piggyback.ex index ecedace3..51ec8d1f 100644 --- a/apps/engine/lib/engine/callbacks/piggyback.ex +++ b/apps/engine/lib/engine/callbacks/piggyback.ex @@ -58,6 +58,9 @@ defmodule Engine.Callbacks.Piggyback do transaction -> case transaction |> Map.get(type) |> Enum.at(index) do + # transaction inputs status is set to exiting when ife starts + # which means only transaction outputs can have piggybacked status + # is that intentional? %Output{state: "confirmed"} = output -> changeset = change(output, state: "piggybacked") position = get_field(changeset, :position) diff --git a/apps/engine/lib/engine/db/block.ex b/apps/engine/lib/engine/db/block.ex index cadda43c..82e32af4 100644 --- a/apps/engine/lib/engine/db/block.ex +++ b/apps/engine/lib/engine/db/block.ex @@ -17,13 +17,17 @@ defmodule Engine.DB.Block do require Logger + @max_transaction_in_block 65_000 + @optional_fields [:hash, :tx_hash, :formed_at_ethereum_height, :submitted_at_ethereum_height, :gas, :attempts_counter] - @required_fields [:nonce, :blknum] + @required_fields [:nonce, :blknum, :txcount] @type t() :: %{ hash: binary(), + state: binary(), nonce: pos_integer(), blknum: pos_integer() | nil, + txcount: non_neg_integer(), tx_hash: binary() | nil, formed_at_ethereum_height: pos_integer() | nil, id: pos_integer(), @@ -42,12 +46,13 @@ defmodule Engine.DB.Block do @timestamps_opts [inserted_at: :node_inserted_at, updated_at: :node_updated_at] schema "blocks" do - # Extracted from `output_id` field(:hash, :binary) + field(:state, :binary) # nonce = max(nonce) + 1 field(:nonce, :integer) # blknum = nonce * 1000 field(:blknum, :integer) + field(:txcount, :integer) field(:tx_hash, :binary) field(:formed_at_ethereum_height, :integer) field(:submitted_at_ethereum_height, :integer) @@ -91,6 +96,7 @@ defmodule Engine.DB.Block do @decorate trace(service: :ecto, type: :backend) def form() do Multi.new() + # We are missing a constraint for a maximum number of transactions in block |> Multi.run("new-block", &insert_block/2) |> Multi.run("form-block", &attach_transactions_to_block/2) |> Multi.run("hash-block", &generate_block_hash/2) @@ -111,6 +117,31 @@ defmodule Engine.DB.Block do end end + @doc """ + Get a forming block or inserts a new one if current forming block hit it's transaction limit + """ + def get_forming_or_insert_block(repo, _params) do + forming_block = IO.inspect(Repo.one(from(block in __MODULE__, where: block.state == "forming", lock: "FOR UPDATE"))) + + case forming_block do + nil -> insert_block(repo) + block -> insert_block_if_transaction_limit_exceeded(repo, block) + end + end + + @doc """ + Increases and returns number of transaction in block + """ + def increase_txcount(repo, %{"block" => block}) do + {1, [{txcount}]} = + repo.update_all( + from(b in __MODULE__, where: b.id == ^block.id, select: {b.txcount}), + inc: [txcount: 1] + ) + + {:ok, txcount} + end + defp get_all(repo, _changeset, new_height, mined_child_block) do query = from(p in __MODULE__, @@ -156,6 +187,39 @@ defmodule Engine.DB.Block do end end + defp insert_block_if_transaction_limit_exceeded(_repo, %{txcount: txcount} = block) + when txcount < @max_transaction_in_block do + {:ok, block} + end + + defp insert_block_if_transaction_limit_exceeded(repo, block) do + {:ok, _} = finalize_block(repo, block) + insert_block(repo) + end + + defp finalize_block(repo, block) do + changeset = change(block, state: "finalizing") + repo.update(changeset) + end + + defp insert_block(repo) do + nonce = + query_max_nonce() + |> Repo.one() + |> case do + nil -> 1 + found_nonce -> found_nonce + 1 + end + + blknum = nonce * Configuration.child_block_interval() + + params = %{state: "forming", nonce: nonce, blknum: blknum, txcount: 0} + + %__MODULE__{} + |> changeset(params) + |> repo.insert() + end + defp insert_block(repo, _params) do nonce = query_max_nonce() @@ -178,7 +242,7 @@ defmodule Engine.DB.Block do defp attach_transactions_to_block(repo, %{"new-block" => block}) do updates = [block_id: block.id, updated_at: NaiveDateTime.utc_now()] - {total, _} = repo.update_all(Transaction.pending(), set: updates) + {total, _} = repo.update_all(Transaction.query_pending(), set: updates) {:ok, total} end diff --git a/apps/engine/lib/engine/db/output.ex b/apps/engine/lib/engine/db/output.ex index 5de267b4..0be8a213 100644 --- a/apps/engine/lib/engine/db/output.ex +++ b/apps/engine/lib/engine/db/output.ex @@ -27,7 +27,7 @@ defmodule Engine.DB.Output do position: pos_integer() | nil, spending_transaction: Transaction.t() | nil, spending_transaction_id: pos_integer() | nil, - state: String.t(), + state: :forming | :pending_submission | :submitted | :confirmed, updated_at: DateTime.t() } @@ -35,6 +35,7 @@ defmodule Engine.DB.Output do schema "outputs" do # Extracted from `output_id` + # it depends on type if output_id stores a position field(:position, :integer) field(:output_type, :integer) @@ -69,6 +70,8 @@ defmodule Engine.DB.Output do """ def usable() do from(o in __MODULE__, + # what does "confirmed" status means? + # Block in which this output was created is published on root chain? where: is_nil(o.spending_transaction_id) and o.state == "confirmed" ) end diff --git a/apps/engine/lib/engine/db/transaction.ex b/apps/engine/lib/engine/db/transaction.ex index c80e8389..3d53c8a6 100644 --- a/apps/engine/lib/engine/db/transaction.ex +++ b/apps/engine/lib/engine/db/transaction.ex @@ -15,6 +15,7 @@ defmodule Engine.DB.Transaction do import Ecto.Changeset, only: [cast: 3, cast_assoc: 2, validate_required: 2] import Ecto.Query, only: [from: 2] + alias Ecto.Multi alias Engine.DB.Block alias Engine.DB.Output alias Engine.DB.Transaction.Validator @@ -26,6 +27,7 @@ defmodule Engine.DB.Transaction do @type t() :: %{ block: Block.t(), block_id: pos_integer(), + tx_index: non_neg_integer(), id: pos_integer(), inputs: list(Output.t()), inserted_at: DateTime.t(), @@ -53,6 +55,7 @@ defmodule Engine.DB.Transaction do field(:tx_bytes, :binary) field(:tx_hash, :binary) field(:tx_type, :integer) + field(:tx_index, :integer) field(:kind, Ecto.Atom) # Virtual fields used for convenience and validation @@ -122,7 +125,19 @@ defmodule Engine.DB.Transaction do |> Validator.validate_statefully(params) end - def insert(changeset), do: Repo.insert(changeset) + def insert(changeset) do + Multi.new() + |> Multi.run("block", &Block.get_forming_or_insert_block/2) + |> Multi.run("block-txcount", &Block.increase_txcount/2) + |> Multi.run("new-transaction", fn repo, params -> insert_transaction(repo, params, changeset) end) + |> Repo.transaction() + end + + defp insert_transaction(repo, %{"block-txcount" => block_txcount, "block" => block}, changeset) do + changeset + |> Ecto.Changeset.change(%{tx_index: block_txcount - 1, block: block}) + |> repo.insert() + end defp recovered_to_map(transaction) do inputs = Enum.map(transaction.inputs, &Map.from_struct/1) diff --git a/apps/engine/priv/repo/migrations/20200326103841_create_blocks_table.exs b/apps/engine/priv/repo/migrations/20200326103841_create_blocks_table.exs index 100258f8..ae672c69 100644 --- a/apps/engine/priv/repo/migrations/20200326103841_create_blocks_table.exs +++ b/apps/engine/priv/repo/migrations/20200326103841_create_blocks_table.exs @@ -5,10 +5,14 @@ defmodule Engine.Repo.Migrations.CreateBlocksTable do create table(:blocks) do # keccak hash of transactions add(:hash, :binary) + # blocks state: forming, pending_submission, submitted, confirmed + add(:state, :string, null: false) # transaction order! add(:nonce, :integer, null: false) # plasma block number add(:blknum, :integer, null: false) + # number of transactions in block + add(:txcount, :integer, null: false) # submitted transaction hash (gets updated with submitted_at_ethereum_height) add(:tx_hash, :binary) # at which height did we form the block @@ -26,6 +30,7 @@ defmodule Engine.Repo.Migrations.CreateBlocksTable do create(unique_index(:blocks, :blknum)) create(unique_index(:blocks, :hash)) + create(unique_index(:blocks, [:state], where: "state = 'forming'", name: :single_forming_block_index)) create( constraint( @@ -35,6 +40,14 @@ defmodule Engine.Repo.Migrations.CreateBlocksTable do ) ) + create( + constraint( + :blocks, + :txcount, + check: "txcount <= 65000" + ) + ) + execute("SELECT ecto_manage_updated_at('blocks');") end end diff --git a/apps/engine/priv/repo/migrations/20200422044422_create_transactions.exs b/apps/engine/priv/repo/migrations/20200422044422_create_transactions.exs index 6217d23f..ca5b3a51 100644 --- a/apps/engine/priv/repo/migrations/20200422044422_create_transactions.exs +++ b/apps/engine/priv/repo/migrations/20200422044422_create_transactions.exs @@ -6,6 +6,7 @@ defmodule Engine.Repo.Migrations.CreateTransactions do add(:tx_bytes, :binary) add(:tx_hash, :binary) add(:tx_type, :integer) + add(:tx_index, :integer) add(:kind, :string) add(:block_id, references(:blocks)) diff --git a/apps/engine/test/engine/db/transaction_test.exs b/apps/engine/test/engine/db/transaction_test.exs index 8958c78c..e028cfec 100644 --- a/apps/engine/test/engine/db/transaction_test.exs +++ b/apps/engine/test/engine/db/transaction_test.exs @@ -2,6 +2,7 @@ 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.Support.TestEntity @@ -9,16 +10,28 @@ defmodule Engine.DB.TransactionTest do setup do _ = insert(:fee, hash: "22", type: :merged_fees) + _ = insert(:block, %{}) :ok end + describe "insert/1" do + test "attaches transaction to a forming block" do + changeset = build(:payment_v1_transaction, %{blknum: 1}) + IO.inspect(Transaction.insert(changeset)) + end + + test "inserts new block if transaction limit is reached" do + end + end + describe "decode/2" do test "decodes tx_bytes and validates for a deposit" do %{tx_bytes: tx_bytes} = build(:deposit_transaction, amount: 0) {:ok, changeset} = Transaction.decode(tx_bytes, Transaction.kind_deposit()) refute changeset.valid? + # assert assert? assert assert "Cannot be zero" in errors_on(changeset).amount end diff --git a/apps/engine/test/support/db/factory.ex b/apps/engine/test/support/db/factory.ex index 73d987fb..2ad6a25c 100644 --- a/apps/engine/test/support/db/factory.ex +++ b/apps/engine/test/support/db/factory.ex @@ -202,6 +202,8 @@ defmodule Engine.DB.Factory do hash: Map.get(attr, :hash) || :crypto.strong_rand_bytes(32), nonce: nonce, blknum: blknum, + state: Map.get(attr, :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), diff --git a/apps/engine/test/support/entity.ex b/apps/engine/test/support/test_entity.ex similarity index 100% rename from apps/engine/test/support/entity.ex rename to apps/engine/test/support/test_entity.ex