Packages
elixium_core
0.4.11
0.6.3
0.6.2
0.6.1
0.6.0
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.1
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.4
0.1.3
The core package for the Elixium blockchain, containing all the modules needed to run the chain
Current section
Files
Jump to
Current section
Files
lib/p2p/node/ledger_manager.ex
defmodule Elixium.Node.LedgerManager do
alias Elixium.Store.Ledger
alias Elixium.Validator
alias Elixium.Block
alias Elixium.Pool.Orphan
alias Elixium.Store.Oracle
require Logger
@moduledoc """
Handles high level decision logic for forking, saving, and relaying blocks.
"""
@doc """
Decide what to do when we've received a new block. All block persistence
logic is handled internally; this returns an atom describing what the peer
handler should do with this block.
"""
@spec handle_new_block(Block) :: :ok | :gossip | :ignore | :invalid | {:missing_blocks, list}
def handle_new_block(block) do
# Check if we've already received a block at this index. If we have,
# diff it against the one we've stored. If we haven't, check to see
# if this index is the next index in the chain. In the case that its
# not, we've likely found a new longest chain, so we need to evaluate
# whether or not we want to switch to that chain
case Ledger.block_at_height(block.index) do
:none ->
last_block = Ledger.last_block()
block_index = :binary.decode_unsigned(block.index)
# Will only match if the block we received is building directly
# on the block that we have as the last block in our chain
if block_index == 0 || (last_block != :err && block_index == :binary.decode_unsigned(last_block.index) + 1 && block.previous_hash == last_block.hash) do
# If this block is positioned as the next block in the chain,
# validate it as such
validate_new_block(block)
else
# Otherwise, check if it's a fork and whether we need to swap to
# a fork chain
evaluate_chain_swap(block)
end
stored_block -> handle_possible_fork(block, stored_block)
end
end
# Checks whether a block is valid as the next block in the chain. If it is,
# adds the block to the chain.
@spec validate_new_block(Block) :: :ok | :invalid
defp validate_new_block(block) do
# Recalculate target difficulty if necessary
difficulty = Block.calculate_difficulty(block)
case Validator.is_block_valid?(block, difficulty) do
:ok ->
# Save the block to our chain since its valid
Ledger.append_block(block)
Oracle.inquire(:"Elixir.Elixium.Store.UtxoOracle", {:update_with_transactions, [block.transactions]})
:ok
_err -> :invalid
end
end
# Checks whether a given block is a valid fork of an existing block. Doesn't
# modify the chain, only updates the orphan block pool and decides whether
# the peer should gossip about this block.
@spec handle_possible_fork(Block, Block) :: :gossip | :ignore
defp handle_possible_fork(block, existing_block) do
case Block.diff_header(existing_block, block) do
[] ->
# There is no difference between these blocks. We'll ignore this newly
# recieved block.
:ignore
_diff ->
block_index = :binary.decode_unsigned(block.index)
have_orphan? =
block_index
|> Orphan.blocks_at_height()
|> Enum.any?(& &1 == block)
if have_orphan? do
:ignore
else
# TODO: Should this look at previous_hash as well?
if Ledger.last_block().index == block.index do
# This block is a fork of the current latest block in the pool. Add it
# to our orphan pool and tell the peer to gossip the block.
Logger.warn("Received fork of current block.")
Orphan.add(block)
:gossip
else
if block_index == 0 do
Orphan.add(block)
:gossip
else
# Check the orphan pool for blocks at the previous height whose hash this
# orphan block references as a previous_hash
check_orphan_pool_for_ancestors(block)
end
end
end
end
end
# Checks the orphan pool for blocks with a common previous index or previous_hash
@spec check_orphan_pool_for_ancestors(Block) :: :gossip | :ignore
defp check_orphan_pool_for_ancestors(block) do
case Orphan.blocks_at_height(:binary.decode_unsigned(block.index) - 1) do
[] ->
# We don't know of any ORPHAN blocks that this block might be referencing.
# Perhaps this is a fork of a block that we've accepted as canonical
# into our chain?
case Ledger.retrieve_block(block.previous_hash) do
:not_found ->
# If this block doesn't reference and blocks that we know of, we can not
# build a chain using this block -- we can't validate this block at all.
# Our only option is to drop the block. Realistically we shouldn't ever
# get into this situation unless a malicious actor has sent us a fake block.
Logger.warn("Received orphan block with no reference to a known block. Dropping orphan")
:ignore
_canonical_block ->
# This block is a fork of a canonical block.
Logger.warn("Fork of canonical block received")
Orphan.add(block)
:gossip
end
_orphan_blocks ->
# This block might be a fork of a block that we have stored in our
# orphan pool.
# TODO: Expand this logic. Right now we're adding this block to the
# orphan pool irrespective of whether or not it has an ancestor in the
# pool. We should check before we add.
Logger.warn("Possibly extension of existing fork")
Orphan.add(block)
:gossip
end
end
# Try to rebuild a fork chain based on this block and it's ancestors in the
# orphan pool. If we're successful, validate and try to swap to the new chain.
# Otherwise, just ignore this block.
@spec evaluate_chain_swap(Block) :: :ok | :ignore | {:missing_blocks, list}
defp evaluate_chain_swap(block) do
# Rebuild the chain backwards until reaching a point where we agree on the
# same blocks as the fork does.
case rebuild_fork_chain(block) do
{:missing_blocks, fork_chain} ->
# We don't have anything that this block can reference as a previous
# block, let's save the block as an orphan and see if we can request
# some more blocks.
Orphan.add(block)
{:missing_blocks, fork_chain}
{fork_chain, fork_source} ->
current_utxos_in_pool = Oracle.inquire(:"Elixir.Elixium.Store.UtxoOracle", {:retrieve_all_utxos, []})
# Blocks which need to be reversed. (Everything from the block after
# the fork source to the current block)
blocks_to_reverse =
fork_source.index
|> :binary.decode_unsigned()
|> Kernel.+(1)
|> Range.new(:binary.decode_unsigned(Ledger.last_block().index))
|> Enum.map(&Ledger.block_at_height/1)
|> Enum.filter(& &1 != :none)
# Find transaction inputs that need to be reversed
# TODO: We're looping over blocks_to_reverse twice here (once to parse
# inputs and once for outputs). We can likely do this in the same loop.
all_canonical_transaction_inputs_since_fork =
Enum.flat_map(blocks_to_reverse, &parse_transaction_inputs/1)
canon_output_txoids =
blocks_to_reverse
|> Enum.flat_map(&parse_transaction_outputs/1)
|> Enum.map(& &1.txoid)
# Pool at the time of fork is basically just current pool plus all inputs
# used in canon chain since fork, minus all outputs created in after fork
# (this will also remove inputs that were created as outputs and used in
# the fork)
pool =
current_utxos_in_pool ++ all_canonical_transaction_inputs_since_fork
|> Enum.filter(&(!Enum.member?(canon_output_txoids, &1.txoid)))
# Traverse the fork chain, making sure each block is valid within its own
# context.
{_, final_contextual_pool, _fork_chain, validation_results} =
fork_chain
|> Enum.scan({fork_source, pool, fork_chain, []}, &validate_in_context/2)
|> List.last()
# Ensure that every block passed validation
if Enum.all?(validation_results, & &1) do
Logger.info("Candidate fork chain valid. Switching.")
# Add everything in final_contextual_pool that is not also in current_utxos_in_pool
Enum.each(final_contextual_pool -- current_utxos_in_pool, & Oracle.inquire(:"Elixir.Elixium.Store.UtxoOracle", {:add_utxo, [&1]}))
# Remove everything in current_utxos_in_pool that is not also in final_contextual_pool
current_utxos_in_pool -- final_contextual_pool
|> Enum.map(& &1.txoid)
|> Enum.each(& Oracle.inquire(:"Elixir.Elixium.Store.UtxoOracle", {:remove_utxo, [&1]}))
# Drop canon chain blocks from the ledger, add them to the orphan pool
# in case the chain gets revived by another miner
Enum.each(blocks_to_reverse, fn blk ->
Orphan.add(blk)
Ledger.drop_block(blk)
end)
# Remove fork chain from orphan pool; now it becomes the canon chain,
# so we add its blocks to the ledger
Enum.each(fork_chain, fn blk ->
Ledger.append_block(blk)
Orphan.remove(blk)
end)
:ok
else
:ignore
end
_ -> :ignore
end
end
# Recursively loops through the orphan pool to build a fork chain as long as
# we can, based on a given block.
@spec rebuild_fork_chain(list) :: {list, Block} | {:missing_blocks, list}
defp rebuild_fork_chain(chain) when is_list(chain) do
case Orphan.blocks_at_height(:binary.decode_unsigned(hd(chain).index) - 1) do
[] ->
# If index is 0, we've forked back to the genesis block. Let's start
# validating
if :binary.decode_unsigned(hd(chain).index) == 0 do
{chain, hd(chain)}
else
Logger.warn("Tried rebuilding fork chain, but was unable to find an ancestor.")
{:missing_blocks, chain}
end
orphan_blocks ->
orphan_blocks
|> Enum.filter(& &1.hash == hd(chain).previous_hash)
|> Enum.find_value(fn candidate_orphan ->
# Check if we agree on a previous_hash
case Ledger.retrieve_block(candidate_orphan.previous_hash) do
# We need to dig deeper...
:not_found -> rebuild_fork_chain([candidate_orphan | chain])
# We found the source of this fork. Return the chain we've accumulated
fork_source -> {[candidate_orphan | chain], fork_source}
end
end)
end
end
defp rebuild_fork_chain(block), do: rebuild_fork_chain([block])
# Return a list of all transaction inputs for every transaction in this block
@spec parse_transaction_inputs(Block) :: list
defp parse_transaction_inputs(block), do: Enum.flat_map(block.transactions, &(&1.inputs))
@spec parse_transaction_outputs(Block) :: list
defp parse_transaction_outputs(block), do: Enum.flat_map(block.transactions, &(&1.outputs))
# Validates a given block in the context of the values passed in. This function
# is primarily meant to be used as an accumulator for Enum.scan. The provided
# pool will be used as the utxo pool, the provided chain will be used as a
# faux canonical chain. Results is an array of blocks that have been previously
# validated using this function.
@spec validate_in_context(Block, {Block, list, list, list}) :: {Block, list, list, list}
defp validate_in_context(block, {last, pool, chain, results}) do
retargeting_window = Application.get_env(:elixium_core, :retargeting_window)
curr_index_in_fork = Enum.find_index(chain, &(&1 == block))
blocks_from_canon =
if curr_index_in_fork < retargeting_window do
to_get = retargeting_window - curr_index_in_fork
Ledger.last_n_blocks(to_get, :binary.decode_unsigned(hd(chain).index) - 1)
else
[]
end
get_from_fork = retargeting_window - length(blocks_from_canon)
chain_without_extra_blocks =
if curr_index_in_fork > retargeting_window do
chain -- Enum.take(chain, curr_index_in_fork - retargeting_window)
else
chain
end
blocks_from_fork = Enum.take(chain_without_extra_blocks, get_from_fork)
difficulty = Block.calculate_difficulty(block, blocks_from_canon ++ blocks_from_fork)
valid =
if :binary.decode_unsigned(block.index) == 0 do
:ok == Validator.is_block_valid?(block, difficulty)
else
:ok == Validator.is_block_valid?(block, difficulty, last, &(pool_check(pool, &1)))
end
# Update the contextual utxo pool by removing spent inputs and adding
# unspent outputs from this block. The following block will use the updated
# contextual pool for utxo validation
updated_pool =
if valid do
# Get a list of this blocks inputs (now that we've deemed it valid)
block_input_txoids =
block
|> parse_transaction_inputs()
|> Enum.map(& &1.txoid)
# Get a list of the outputs this block produced
block_outputs = parse_transaction_outputs(block)
# Remove all the outputs that were both created and used within this same
# block
Enum.filter(pool ++ block_outputs, &(!Enum.member?(block_input_txoids, &1.txoid)))
else
pool
end
{block, updated_pool, chain, [valid | results]}
end
# Function that gets passed to Validator.is_block_valid?/3, telling it how to
# evaluate the pool. We're doing this because by default, the validator uses
# the canonical UTXO pool for validation, but when we're processing a potential
# fork, we won't have the same exact UTXO pool, so we reconstruct one based on
# the fork chain. We then use this pool to verify the existence of a particular
# UTXO in the fork chain.
@spec pool_check(list, map) :: true | false
defp pool_check(pool, utxo) do
case Enum.find(pool, false, & &1.txoid == utxo.txoid) do
false -> false
txo_in_pool -> utxo.amount == txo_in_pool.amount && utxo.addr == txo_in_pool.addr
end
end
end