From 818f84a42489f79ba06e8c594a0f184255c407b6 Mon Sep 17 00:00:00 2001 From: Luke Imhoff Date: Tue, 25 Sep 2018 10:34:16 -0500 Subject: [PATCH] Transaction.Fork A Transaction.Fork is a Transaction recorded in a non-consensus block, such an uncle. --- apps/explorer/lib/explorer/chain/import.ex | 89 ++++++++++++++++--- .../lib/explorer/chain/transaction.ex | 27 ++++-- .../lib/explorer/chain/transaction/fork.ex | 63 +++++++++++++ ...20180918200001_create_transaction_fork.exs | 16 ++++ .../test/explorer/chain/import_test.exs | 65 ++++++++++++++ .../explorer/chain/transaction/fork_test.exs | 23 +++++ apps/explorer/test/support/factory.ex | 8 ++ 7 files changed, 270 insertions(+), 21 deletions(-) create mode 100644 apps/explorer/lib/explorer/chain/transaction/fork.ex create mode 100644 apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs create mode 100644 apps/explorer/test/explorer/chain/transaction/fork_test.exs diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index 7cffb5396f..22987fb5c1 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -70,6 +70,10 @@ defmodule Explorer.Chain.Import do optional(:on_conflict) => :nothing | :replace_all, optional(:timeout) => timeout } + @type transaction_forks_options :: %{ + required(:params) => params, + optional(:timeout) => timeout + } @type token_balances_options :: %{ required(:params) => params, optional(:timeout) => timeout @@ -87,7 +91,8 @@ defmodule Explorer.Chain.Import do optional(:token_transfers) => token_transfers_options, optional(:tokens) => tokens_options, optional(:token_balances) => token_balances_options, - optional(:transactions) => transactions_options + optional(:transactions) => transactions_options, + optional(:transaction_forks) => transaction_forks_options } @type all_result :: {:ok, @@ -108,7 +113,10 @@ defmodule Explorer.Chain.Import do optional(:token_transfers) => [TokenTransfer.t()], optional(:tokens) => [Token.t()], optional(:token_balances) => [TokenBalance.t()], - optional(:transactions) => [Hash.Full.t()] + optional(:transactions) => [Hash.Full.t()], + optional(:transaction_forks) => [ + %{required(:uncle_hash) => Hash.Full.t(), required(:hash) => Hash.Full.t()} + ] }} | {:error, [Changeset.t()]} | {:error, step :: Ecto.Multi.name(), failed_value :: any(), @@ -130,23 +138,25 @@ defmodule Explorer.Chain.Import do @insert_token_balances_timeout 60_000 @insert_tokens_timeout 60_000 @insert_transactions_timeout 60_000 + @insert_transaction_forks_timeout 60_000 @doc """ Bulk insert all data stored in the `Explorer`. The import returns the unique key(s) for each type of record inserted. - | Key | Value Type | Value Description | - |----------------------------------|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------| - | `:addresses` | `[Explorer.Chain.Address.t()]` | List of `t:Explorer.Chain.Address.t/0`s | - | `:balances` | `[%{address_hash: Explorer.Chain.Hash.t(), block_number: Explorer.Chain.Block.block_number()}]` | List of `t:Explorer.Chain.Address.t/0`s | - | `:blocks` | `[Explorer.Chain.Block.t()]` | List of `t:Explorer.Chain.Block.t/0`s | - | `:internal_transactions` | `[%{index: non_neg_integer(), transaction_hash: Explorer.Chain.Hash.t()}]` | List of maps of the `t:Explorer.Chain.InternalTransaction.t/0` `index` and `transaction_hash` | - | `:logs` | `[Explorer.Chain.Log.t()]` | List of `t:Explorer.Chain.Log.t/0`s | - | `:token_transfers` | `[Explorer.Chain.TokenTransfer.t()]` | List of `t:Explor.Chain.TokenTransfer.t/0`s | - | `:tokens` | `[Explorer.Chain.Token.t()]` | List of `t:Explorer.Chain.token.t/0`s | - | `:transactions` | `[Explorer.Chain.Hash.t()]` | List of `t:Explorer.Chain.Transaction.t/0` `hash` | - | `:block_second_degree_relations` | `[%{uncle_hash: Explorer.Chain.Hash.t(), nephew_hash: Explorer.Chain.Hash.t()]` | List of maps `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` `uncle_hash` and `nephew_hash` | + | Key | Value Type | Value Description | + |----------------------------------|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------| + | `:addresses` | `[Explorer.Chain.Address.t()]` | List of `t:Explorer.Chain.Address.t/0`s | + | `:balances` | `[%{address_hash: Explorer.Chain.Hash.t(), block_number: Explorer.Chain.Block.block_number()}]` | List of `t:Explorer.Chain.Address.t/0`s | + | `:blocks` | `[Explorer.Chain.Block.t()]` | List of `t:Explorer.Chain.Block.t/0`s | + | `:internal_transactions` | `[%{index: non_neg_integer(), transaction_hash: Explorer.Chain.Hash.t()}]` | List of maps of the `t:Explorer.Chain.InternalTransaction.t/0` `index` and `transaction_hash` | + | `:logs` | `[Explorer.Chain.Log.t()]` | List of `t:Explorer.Chain.Log.t/0`s | + | `:token_transfers` | `[Explorer.Chain.TokenTransfer.t()]` | List of `t:Explor.Chain.TokenTransfer.t/0`s | + | `:tokens` | `[Explorer.Chain.Token.t()]` | List of `t:Explorer.Chain.token.t/0`s | + | `:transactions` | `[Explorer.Chain.Hash.t()]` | List of `t:Explorer.Chain.Transaction.t/0` `hash` | + | `:transaction_forks` | `[%{uncle_hash: Explorer.Chain.Hash.t(), hash: Explorer.Chain.Hash.t()}]` | List of maps of the `t:Explorer.Chain.Transaction.Fork.t/0` `uncle_hash` and `hash` | + | `:block_second_degree_relations` | `[%{uncle_hash: Explorer.Chain.Hash.t(), nephew_hash: Explorer.Chain.Hash.t()]` | List of maps of the `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` `uncle_hash` and `nephew_hash` | The params for each key are validated using the corresponding `Ecto.Schema` module's `changeset/2` function. If there are errors, they are returned in `Ecto.Changeset.t`s, so that the original, invalid value can be reconstructed for any @@ -297,7 +307,8 @@ defmodule Explorer.Chain.Import do token_transfers: TokenTransfer, token_balances: TokenBalance, tokens: Token, - transactions: Transaction + transactions: Transaction, + transaction_forks: Transaction.Fork } defp ecto_schema_module_to_changes_list_map_to_multi(ecto_schema_module_to_changes_list_map, options) @@ -311,6 +322,7 @@ defmodule Explorer.Chain.Import do |> run_blocks(ecto_schema_module_to_changes_list_map, full_options) |> run_block_second_degree_relations(ecto_schema_module_to_changes_list_map, full_options) |> run_transactions(ecto_schema_module_to_changes_list_map, full_options) + |> run_transaction_forks(ecto_schema_module_to_changes_list_map, full_options) |> run_internal_transactions(ecto_schema_module_to_changes_list_map, full_options) |> run_logs(ecto_schema_module_to_changes_list_map, full_options) |> run_tokens(ecto_schema_module_to_changes_list_map, full_options) @@ -404,6 +416,27 @@ defmodule Explorer.Chain.Import do end end + defp run_transaction_forks(multi, ecto_schema_module_to_changes_list_map, options) + when is_map(ecto_schema_module_to_changes_list_map) and is_map(options) do + case ecto_schema_module_to_changes_list_map do + %{Transaction.Fork => transaction_fork_changes} -> + %{timestamps: timestamps} = options + + Multi.run(multi, :transaction_forks, fn _ -> + insert_transaction_forks( + transaction_fork_changes, + %{ + timeout: options[:transaction_forks][:timeout] || @insert_transaction_forks_timeout, + timestamps: timestamps + } + ) + end) + + _ -> + multi + end + end + defp run_internal_transactions(multi, ecto_schema_module_to_changes_list_map, options) when is_map(ecto_schema_module_to_changes_list_map) and is_map(options) do case ecto_schema_module_to_changes_list_map do @@ -889,6 +922,34 @@ defmodule Explorer.Chain.Import do {:ok, for(transaction <- transactions, do: transaction.hash)} end + @spec insert_transaction_forks([map()], %{ + required(:timeout) => timeout, + required(:timestamps) => timestamps + }) :: {:ok, [%{uncle_hash: Hash.t(), hash: Hash.t()}]} + defp insert_transaction_forks(changes_list, %{timeout: timeout, timestamps: timestamps}) + when is_list(changes_list) do + # order so that row ShareLocks are grabbed in a consistent order + ordered_changes_list = Enum.sort_by(changes_list, &{&1.uncle_hash, &1.hash}) + + insert_changes_list( + ordered_changes_list, + conflict_target: [:uncle_hash, :index], + on_conflict: + from( + transaction_fork in Transaction.Fork, + update: [ + set: [ + hash: fragment("EXCLUDED.hash") + ] + ] + ), + for: Transaction.Fork, + returning: [:uncle_hash, :hash], + timeout: timeout, + timestamps: timestamps + ) + end + defp insert_changes_list(changes_list, options) when is_list(changes_list) do ecto_schema_module = Keyword.fetch!(options, :for) diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 39881284af..16873920a9 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -20,7 +20,7 @@ defmodule Explorer.Chain.Transaction do Wei } - alias Explorer.Chain.Transaction.Status + alias Explorer.Chain.Transaction.{Fork, Status} @optional_attrs ~w(block_hash block_number created_contract_address_hash cumulative_gas_used error gas_used index internal_transactions_indexed_at status to_address_hash)a @@ -66,9 +66,12 @@ defmodule Explorer.Chain.Transaction do @type wei_per_gas :: Wei.t() @typedoc """ - * `block` - the block in which this transaction was mined/validated. `nil` when transaction is pending. - * `block_hash` - `block` foreign key. `nil` when transaction is pending. - * `block_number` - Denormalized `block` `number`. `nil` when transaction is pending. + * `block` - the block in which this transaction was mined/validated. `nil` when transaction is pending or has only + been collated into one of the `uncles` in one of the `forks`. + * `block_hash` - `block` foreign key. `nil` when transaction is pending or has only been collated into one of the + `uncles` in one of the `forks`. + * `block_number` - Denormalized `block` `number`. `nil` when transaction is pending or has only been collated into + one of the `uncles` in one of the `forks`. * `created_contract_address` - belongs_to association to `address` corresponding to `created_contract_address_hash`. * `created_contract_address_hash` - Denormalized `internal_transaction` `created_contract_address_hash` populated only when `to_address_hash` is nil. @@ -76,13 +79,16 @@ defmodule Explorer.Chain.Transaction do `transaction`'s `index`. `nil` when transaction is pending. * `error` - the `error` from the last `t:Explorer.Chain.InternalTransaction.t/0` in `internal_transactions` that caused `status` to be `:error`. Only set after `internal_transactions_index_at` is set AND if there was an error. + * `forks` - copies of this transactions that were collated into `uncles` not on the primary consensus of the chain. * `from_address` - the source of `value` * `from_address_hash` - foreign key of `from_address` * `gas` - Gas provided by the sender * `gas_price` - How much the sender is willing to pay for `gas` - * `gas_used` - the gas used for just `transaction`. `nil` when transaction is pending. + * `gas_used` - the gas used for just `transaction`. `nil` when transaction is pending or has only been collated into + one of the `uncles` in one of the `forks`. * `hash` - hash of contents of this transaction - * `index` - index of this transaction in `block`. `nil` when transaction is pending. + * `index` - index of this transaction in `block`. `nil` when transaction is pending or has only been collated into + one of the `uncles` in one of the `forks`. * `input`- data sent along with the transaction * `internal_transactions` - transactions (value transfers) created while executing contract used for this transaction @@ -93,9 +99,11 @@ defmodule Explorer.Chain.Transaction do the X coordinate of a point R, modulo the curve order n. * `s` - The S field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as the X coordinate of a point R, modulo the curve order n. - * `status` - whether the transaction was successfully mined or failed. `nil` when transaction is pending. + * `status` - whether the transaction was successfully mined or failed. `nil` when transaction is pending or has only + been collated into one of the `uncles` in one of the `forks. * `to_address` - sink of `value` * `to_address_hash` - `to_address` foreign key + * `uncles` - uncle blocks where `forks` were collated * `v` - The V field of the signature. * `value` - wei transferred from `from_address` to `to_address` """ @@ -107,6 +115,7 @@ defmodule Explorer.Chain.Transaction do created_contract_address_hash: Hash.Address.t() | nil, cumulative_gas_used: Gas.t() | nil, error: String.t() | nil, + forks: %Ecto.Association.NotLoaded{} | [Fork.t()], from_address: %Ecto.Association.NotLoaded{} | Address.t(), from_address_hash: Hash.Address.t(), gas: Gas.t(), @@ -124,6 +133,7 @@ defmodule Explorer.Chain.Transaction do status: Status.t() | nil, to_address: %Ecto.Association.NotLoaded{} | Address.t() | nil, to_address_hash: Hash.Address.t() | nil, + uncles: %Ecto.Association.NotLoaded{} | [Block.t()], v: v(), value: Wei.t() } @@ -149,6 +159,7 @@ defmodule Explorer.Chain.Transaction do timestamps() belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full) + has_many(:forks, Fork, foreign_key: :hash) belongs_to( :from_address, @@ -170,6 +181,8 @@ defmodule Explorer.Chain.Transaction do type: Hash.Address ) + has_many(:uncles, through: [:forks, :uncle]) + belongs_to( :created_contract_address, Address, diff --git a/apps/explorer/lib/explorer/chain/transaction/fork.ex b/apps/explorer/lib/explorer/chain/transaction/fork.ex new file mode 100644 index 0000000000..74e2fc921d --- /dev/null +++ b/apps/explorer/lib/explorer/chain/transaction/fork.ex @@ -0,0 +1,63 @@ +defmodule Explorer.Chain.Transaction.Fork do + @moduledoc """ + A transaction fork has the same `hash` as a `t:Explorer.Chain.Transaction.t/0`, but associates that `hash` with a + non-consensus uncle `t:Explorer.Chain.Block.t/0` instead of the consensus block linked in the + `t:Explorer.Chain.Transaction.t/0` `block_hash`. + """ + + use Explorer.Schema + + alias Explorer.Chain.{Block, Hash, Transaction} + + @optional_attrs ~w()a + @required_attrs ~w(hash index uncle_hash)a + @allowed_attrs @optional_attrs ++ @required_attrs + + @typedoc """ + * `hash` - hash of contents of this transaction + * `index` - index of this transaction in `uncle`. + * `transaction` - the data shared between all forks and the consensus transaction. + * `uncle` - the block in which this transaction was mined/validated. + * `uncle_hash` - `uncle` foreign key. + """ + @type t :: %__MODULE__{ + hash: Hash.t(), + index: Transaction.transaction_index(), + transaction: %Ecto.Association.NotLoaded{} | Transaction.t(), + uncle: %Ecto.Association.NotLoaded{} | Block.t(), + uncle_hash: Hash.t() + } + + @primary_key false + schema "transaction_forks" do + field(:index, :integer) + + timestamps() + + belongs_to(:transaction, Transaction, foreign_key: :hash, references: :hash, type: Hash.Full) + belongs_to(:uncle, Block, foreign_key: :uncle_hash, references: :hash, type: Hash.Full) + end + + @doc """ + All fields are required for transaction fork + + iex> changeset = Fork.changeset( + ...> %Fork{}, + ...> %{ + ...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6", + ...> index: 1, + ...> uncle_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b48" + ...> } + ...> ) + iex> changeset.valid? + true + + """ + def changeset(%__MODULE__{} = fork, attrs \\ %{}) do + fork + |> cast(attrs, @allowed_attrs) + |> validate_required(@required_attrs) + |> assoc_constraint(:transaction) + |> assoc_constraint(:uncle) + end +end diff --git a/apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs b/apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs new file mode 100644 index 0000000000..06e77dcd41 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs @@ -0,0 +1,16 @@ +defmodule Explorer.Repo.Migrations.CreateTransactionBlockUncles do + use Ecto.Migration + + def change do + create table(:transaction_forks, primary_key: false) do + add(:hash, references(:transactions, column: :hash, on_delete: :delete_all, type: :bytea), null: false) + add(:index, :integer, null: false) + add(:uncle_hash, references(:blocks, column: :hash, on_delete: :delete_all, type: :bytea), null: false) + + timestamps() + end + + create(index(:transaction_forks, :uncle_hash)) + create(unique_index(:transaction_forks, [:uncle_hash, :index])) + end +end diff --git a/apps/explorer/test/explorer/chain/import_test.exs b/apps/explorer/test/explorer/chain/import_test.exs index 440cea88e4..1e39d49983 100644 --- a/apps/explorer/test/explorer/chain/import_test.exs +++ b/apps/explorer/test/explorer/chain/import_test.exs @@ -1054,5 +1054,70 @@ defmodule Explorer.Chain.ImportTest do assert %Transaction{status: :error, error: "Out of gas"} = Repo.get(Transaction, "0xab349efbe1ddc6d85d84a993aa52bdaadce66e8ee166dd10013ce3f2a94ca724") end + + test "uncles record their transaction indexes in transactions_forks" do + miner_hash = address_hash() + from_address_hash = address_hash() + transaction_hash = transaction_hash() + uncle_hash = block_hash() + + assert {:ok, _} = + Import.all(%{ + addresses: %{ + params: [ + %{hash: miner_hash}, + %{hash: from_address_hash} + ] + }, + blocks: %{ + params: [ + %{ + consensus: false, + difficulty: 0, + gas_limit: 21_000, + gas_used: 21_000, + hash: uncle_hash, + miner_hash: miner_hash, + nonce: 0, + number: 0, + parent_hash: block_hash(), + size: 0, + timestamp: DateTime.utc_now(), + total_difficulty: 0 + } + ] + }, + transactions: %{ + params: [ + %{ + block_hash: nil, + block_number: nil, + from_address_hash: from_address_hash, + gas: 21_000, + gas_price: 1, + hash: transaction_hash, + input: "0x", + nonce: 0, + r: 0, + s: 0, + v: 0, + value: 0 + } + ], + on_conflict: :replace_all + }, + transaction_forks: %{ + params: [ + %{ + uncle_hash: uncle_hash, + index: 0, + hash: transaction_hash + } + ] + } + }) + + assert Repo.aggregate(Transaction.Fork, :count, :hash) == 1 + end end end diff --git a/apps/explorer/test/explorer/chain/transaction/fork_test.exs b/apps/explorer/test/explorer/chain/transaction/fork_test.exs new file mode 100644 index 0000000000..d5028af6ab --- /dev/null +++ b/apps/explorer/test/explorer/chain/transaction/fork_test.exs @@ -0,0 +1,23 @@ +defmodule Explorer.Chain.Transaction.ForkTest do + use Explorer.DataCase + + alias Ecto.Changeset + alias Explorer.Chain.Transaction.Fork + + doctest Fork + + test "a transaction fork cannot be inserted if the corresponding transaction does not exist" do + assert %Changeset{valid?: true} = changeset = Fork.changeset(%Fork{}, params_for(:transaction_fork)) + + assert {:error, %Changeset{errors: [transaction: {"does not exist", []}]}} = Repo.insert(changeset) + end + + test "a transaction fork cannot be inserted if the corresponding uncle does not exist" do + transaction = insert(:transaction) + + assert %Changeset{valid?: true} = + changeset = Fork.changeset(%Fork{}, %{hash: transaction.hash, index: 0, uncle_hash: block_hash()}) + + assert {:error, %Changeset{errors: [uncle: {"does not exist", []}]}} = Repo.insert(changeset) + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 3f1af70945..e4c05a4abd 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -428,6 +428,14 @@ defmodule Explorer.Factory do data(:transaction_input) end + def transaction_fork_factory do + %Transaction.Fork{ + hash: transaction_hash(), + index: 0, + uncle_hash: block_hash() + } + end + def smart_contract_factory() do %SmartContract{ address_hash: insert(:address).hash,