Transaction.Fork

A Transaction.Fork is a Transaction recorded in a non-consensus block,
such an uncle.
pull/802/head
Luke Imhoff 6 years ago
parent 4b140c87df
commit 818f84a424
  1. 89
      apps/explorer/lib/explorer/chain/import.ex
  2. 27
      apps/explorer/lib/explorer/chain/transaction.ex
  3. 63
      apps/explorer/lib/explorer/chain/transaction/fork.ex
  4. 16
      apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs
  5. 65
      apps/explorer/test/explorer/chain/import_test.exs
  6. 23
      apps/explorer/test/explorer/chain/transaction/fork_test.exs
  7. 8
      apps/explorer/test/support/factory.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)

@ -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,

@ -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

@ -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

@ -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

@ -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

@ -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,

Loading…
Cancel
Save