Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

# **Elixir blockchain research**

This is an elixir implementation of a basic blockchain. We aim to keep the blockchain as simple as possible and to research and experiment with different technologies
This is an elixir implementation of a basic blockchain. We aim to keep the blockchain as simple as possible and to research and experiment with different technologies

## Getting Started

These instructions will get you a copy of the project up and running on your local machine for development and testing purposes.

## Prerequisites

To install and use the Elixir Blockchain you will need [Elixir](https://elixir-lang.org/install.html) and the source code by cloning or downloading the repository.
To install and use the Elixir Blockchain you will need [Elixir](https://elixir-lang.org/install.html) and the source code by cloning or downloading the repository.

## Usage
#### **Starting the application**
Expand All @@ -19,41 +19,41 @@ Start the application in interactive Elixir mode
`iex -S mix phx.server`

#### **Starting the miner**
To start the miner use the following command in the command prompt:
To start the miner use the following command in the command prompt:

`Aecore.Miner.Worker.resume()`

This will continuously mine new blocks until terminated or suspended.
To suspend/stop the miner from mining:
To suspend/stop the miner from mining:

`Aecore.Miner.Worker.suspend() `

#### **API calls**
To add a block to the blockchain:
To add a block to the blockchain:

`Aecore.Chain.Worker.add_block(%Block{}) :: :ok`

To get all blocks in the current chain:
To get all blocks in the current chain:

`Aecore.Chain.Worker.all_blocks() :: list()`

To get the latest block added to the chain:
To get the latest block added to the chain:

`Aecore.Chain.Worker.latest_block() :: %Block{}`

To get the latest chainstate:
To get the latest chainstate:

`Aecore.Chain.Worker.chain_state() :: map()`

To add a transaction to the Transaction Pool:

`Aecore.Txs.Pool.Worker.add_transaction(%SignedTx{}) :: :ok | :error`

To remove a transaction from the Transaction Pool:
To remove a transaction from the Transaction Pool:

`Aecore.Txs.Pool.Worker.remove_transaction(%SignedTx{}) :: :ok | :error`

To inspect all transactions in the Transaction Pool:
To inspect all transactions in the Transaction Pool:

`Aecore.Txs.Pool.Worker.get_pool() :: map() `

Expand All @@ -63,9 +63,9 @@ To run the automatic tests:

`mix test`

## Logging
## Logging

To debug, see errors, warnings and info about the blockchain,
To debug, see errors, warnings and info about the blockchain,
the log can be found in the source folder under:`apps/aecore/logs`

`09:59:16.298 [info] Added block #1 with hash 6C449AC3B5E38857DC85310873979F45992270BF54304B3F60BE4F64373991B5, total tokens: 100 `
Expand Down
25 changes: 19 additions & 6 deletions apps/aecore/lib/aecore/chain/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Aecore.Chain.Worker do
alias Aecore.Utils.Blockchain.BlockValidation
alias Aecore.Peers.Worker, as: Peers
alias Aecore.Utils.Persistence
alias Aecore.Utils.Blockchain.Difficulty

use GenServer

Expand Down Expand Up @@ -69,7 +70,22 @@ defmodule Aecore.Chain.Worker do

@spec add_block(%Block{}) :: :ok
def add_block(%Block{} = block) do
GenServer.call(__MODULE__, {:add_block, block})
latest_block = latest_block()

prev_block_chain_state = chain_state()
new_block_state = ChainState.calculate_block_state(block.txs)
new_chain_state = ChainState.calculate_chain_state(new_block_state, prev_block_chain_state)

latest_header_hash = BlockValidation.block_header_hash(latest_block.header)

blocks_for_difficulty_calculation = get_blocks(latest_header_hash, Difficulty.get_number_of_blocks())
BlockValidation.validate_block!(block, latest_block, new_chain_state, blocks_for_difficulty_calculation)
add_validated_block(block)
end

@spec add_validated_block(%Block{}) :: :ok
defp add_validated_block(%Block{} = block) do
GenServer.call(__MODULE__, {:add_validated_block, block})
end

@spec chain_state(binary()) :: map()
Expand Down Expand Up @@ -127,18 +143,15 @@ defmodule Aecore.Chain.Worker do
end
end

def handle_call({:add_block, %Block{} = block}, _from, state) do
def handle_call({:add_validated_block, %Block{} = block}, _from, state) do
{block_map, latest_block_chain_state, txs_index} = state
prev_block_chain_state = latest_block_chain_state[block.header.prev_hash]
new_block_state = ChainState.calculate_block_state(block.txs)
new_chain_state =
ChainState.calculate_chain_state(new_block_state, prev_block_chain_state)
new_chain_state = ChainState.calculate_chain_state(new_block_state, prev_block_chain_state)

new_block_txs_index = calculate_block_acc_txs_info(block)
new_txs_index = update_txs_index(txs_index, new_block_txs_index)
try do
BlockValidation.validate_block!(block, block_map[block.header.prev_hash], new_chain_state)

Enum.each(block.txs, fn(tx) -> Pool.remove_transaction(tx) end)

block_hash = BlockValidation.block_header_hash(block.header)
Expand Down
74 changes: 52 additions & 22 deletions apps/aecore/lib/aecore/miner/worker.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,15 @@ defmodule Aecore.Miner.Worker do

## Running ##
def running(:cast, :mine, start_nonce) do
{_, next_nonce} = mine_next_block(start_nonce)
GenStateMachine.cast(__MODULE__, :mine)
{:next_state, :running, next_nonce}
{status, next_nonce} = mine_next_block(start_nonce)
case status do
:error ->
Logger.info("Mining stopped by error")
{:next_state, :idle, 0}
_ ->
GenStateMachine.cast(__MODULE__, :mine)
{:next_state, :running, next_nonce}
end
end

def running({:call, from}, :get_state, _data) do
Expand All @@ -88,7 +94,7 @@ defmodule Aecore.Miner.Worker do
end

def running({:call, from}, :suspend, data) do
Logger.info("Mined stopped by user")
Logger.info("Mining stopped by user")
{:next_state, :idle, data, [{:reply, from, :ok}]}
end

Expand All @@ -115,9 +121,13 @@ defmodule Aecore.Miner.Worker do
def coinbase_transaction_value, do: @coinbase_transaction_value

def calculate_total_fees(txs) do
List.foldl(txs, 0, fn(tx, acc) ->
List.foldl(
txs,
0,
fn (tx, acc) ->
acc + tx.data.fee
end)
end
)
end

## Internal
Expand All @@ -127,21 +137,37 @@ defmodule Aecore.Miner.Worker do
latest_block_hash = BlockValidation.block_header_hash(latest_block.header)
chain_state = Chain.chain_state(latest_block_hash)

txs_list = Map.values(Pool.get_pool())
ordered_txs_list = Enum.sort(txs_list, fn(tx1, tx2) -> tx1.data.nonce < tx2.data.nonce end)
blocks_for_difficulty_validation = if(latest_block.header.height == 0) do
Chain.get_blocks(latest_block_hash, Difficulty.get_number_of_blocks())
else
[_ | something] = Chain.get_blocks(latest_block_hash, Difficulty.get_number_of_blocks() + 1)
something
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I trust this logic is correct. it looks weird

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nedelcho-delchev-tues maybe we can optimize this somehow to be better readable and add some comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I pushed a change directly to master, you can see if it is better and more understandable.
@cytadela8


blocks_for_difficulty_calculation = Chain.get_blocks(latest_block_hash, Difficulty.get_number_of_blocks())
previous_block = cond do
latest_block == Block.genesis_block() -> nil
true ->
blocks = Chain.get_blocks(latest_block_hash, 2)
Enum.at(blocks, 1)
end

try do
BlockValidation.validate_block!(latest_block, previous_block, chain_state)
BlockValidation.validate_block!(
latest_block,
previous_block,
chain_state,
blocks_for_difficulty_validation
)

blocks_for_difficulty_calculation = Chain.get_blocks(latest_block_hash, Difficulty.get_number_of_blocks())
difficulty = Difficulty.calculate_next_difficulty(blocks_for_difficulty_calculation)

txs_list = Map.values(Pool.get_pool())
ordered_txs_list = Enum.sort(txs_list, fn (tx1, tx2) -> tx1.data.nonce < tx2.data.nonce end)
valid_txs = BlockValidation.filter_invalid_transactions_chainstate(ordered_txs_list, chain_state)

{_, pubkey} = Keys.pubkey()

total_fees = calculate_total_fees(valid_txs)
valid_txs = [get_coinbase_transaction(pubkey, total_fees) | valid_txs]
root_hash = BlockValidation.calculate_root_hash(valid_txs)
Expand All @@ -152,37 +178,41 @@ defmodule Aecore.Miner.Worker do

latest_block_hash = BlockValidation.block_header_hash(latest_block.header)

difficulty = Difficulty.calculate_next_difficulty(blocks_for_difficulty_calculation)

unmined_header =
Header.create(
latest_block.header.height + 1,
latest_block_hash,
root_hash,
chain_state_hash,
difficulty,
0, #start from nonce 0, will be incremented in mining
0,
#start from nonce 0, will be incremented in mining
Block.current_block_version()
)

Logger.debug("start nonce #{start_nonce}. Final nonce = #{start_nonce + @nonce_per_cycle}")
case Cuckoo.generate(%{unmined_header
| nonce: start_nonce + @nonce_per_cycle}) do

case Cuckoo.generate(%{unmined_header | nonce: start_nonce + @nonce_per_cycle}) do
{:ok, mined_header} ->
block = %Block{header: mined_header, txs: valid_txs}
Logger.info(fn ->
"Mined block ##{block.header.height}, difficulty target #{block.header.difficulty_target}, nonce #{block.header.nonce}"
end)
Logger.info(
fn ->
"Mined block ##{block.header.height}, difficulty target #{block.header.difficulty_target}, nonce #{
block.header.nonce
}" end
)
Chain.add_block(block)
{:block_found, 0}

{:error, _message} ->
{:no_block_found, start_nonce + @nonce_per_cycle}
end

catch
{:error, _message} ->
Logger.error(fn ->
"Failed to mine block"
end)
message ->
Logger.error(fn -> "Failed to mine block: #{Kernel.inspect(message)}" end)
{:error, message}
end

end
end
1 change: 0 additions & 1 deletion apps/aecore/lib/aecore/peers/sync.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ defmodule Aecore.Peers.Sync do
peers_count = map_size(Peers.all_peers())
cond do
peers_count == 0 ->
Logger.error(fn -> "No peers" end)
{:error, "No peers"}
peers_count < @peers_target_count ->
all_peers = Map.keys(Peers.all_peers())
Expand Down
9 changes: 7 additions & 2 deletions apps/aecore/lib/aecore/utils/blockchain/block_validation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ defmodule Aecore.Utils.Blockchain.BlockValidation do
alias Aecore.Structures.Header
alias Aecore.Structures.SignedTx
alias Aecore.Chain.ChainState
alias Aecore.Utils.Blockchain.Difficulty

@spec validate_block!(Block.block(), Block.block(), map()) :: {:error, term()} | :ok
def validate_block!(new_block, previous_block, chain_state) do
@spec validate_block!(Block.block(), Block.block(), map(), list()) :: {:error, term()} | :ok
def validate_block!(new_block, previous_block, chain_state, blocks_for_difficulty_calculation) do

is_genesis = new_block == Block.genesis_block() && previous_block == nil
chain_state_hash = ChainState.calculate_chain_state_hash(chain_state)
Expand All @@ -18,6 +19,7 @@ defmodule Aecore.Utils.Blockchain.BlockValidation do
is_difficulty_target_met = Cuckoo.verify(new_block.header)
coinbase_transactions_sum = sum_coinbase_transactions(new_block)
total_fees = Miner.calculate_total_fees(new_block.txs)
difficulty = Difficulty.calculate_next_difficulty(blocks_for_difficulty_calculation)

cond do
# do not check previous block hash for genesis block, there is none
Expand Down Expand Up @@ -49,6 +51,9 @@ defmodule Aecore.Utils.Blockchain.BlockValidation do
new_block.header.version != Block.current_block_version() ->
throw({:error, "Invalid block version"})

difficulty != new_block.header.difficulty_target ->
throw({:error, "Invalid block difficulty"})

true ->
:ok
end
Expand Down
9 changes: 1 addition & 8 deletions apps/aecore/lib/aecore/utils/blockchain/difficulty.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
defmodule Aecore.Utils.Blockchain.Difficulty do
alias Aecore.Chain.Worker, as: Chain

@number_of_blocks 100
@max_difficulty_change 2
Expand All @@ -9,13 +8,7 @@ defmodule Aecore.Utils.Blockchain.Difficulty do
@number_of_blocks
end

@spec calculate_next_difficulty :: integer()
def calculate_next_difficulty() do
Chain.get_blocks_for_difficulty_calculation()
|> calculate_next_difficulty()
end

@spec calculate_next_difficulty :: integer()
@spec calculate_next_difficulty(list()) :: integer()
def calculate_next_difficulty(list) do
[latest_block | _] = list

Expand Down
13 changes: 10 additions & 3 deletions apps/aecore/test/aecore_chain_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule AecoreChainTest do
alias Aecore.Utils.Blockchain.BlockValidation
alias Aecore.Chain.Worker, as: Chain
alias Aecore.Miner.Worker, as: Miner
alias Aecore.Utils.Blockchain.Difficulty

setup do
Chain.start_link([])
Expand All @@ -32,20 +33,26 @@ defmodule AecoreChainTest do

block = %Block{header: %Header{height: latest_block.header.height + 1,
prev_hash: latest_block_hash,
txs_hash: <<0::256>>,chain_state_hash: new_chain_state_hash,
txs_hash: <<0::256>>,
chain_state_hash: new_chain_state_hash,
difficulty_target: 1, nonce: 0,
timestamp: System.system_time(:milliseconds), version: 1}, txs: []}
timestamp: System.system_time(:milliseconds),
version: 1}, txs: []}
{:ok, nbh} = Aecore.Pow.Cuckoo.generate(block.header)
block = %{block | header: nbh}

latest_block = Chain.latest_block()
latest_block_hash = BlockValidation.block_header_hash(latest_block.header)
blocks_for_difficulty_calculation = Chain.get_blocks(latest_block_hash,
Difficulty.get_number_of_blocks)
latest_block_hash_hex = latest_block_hash |> Base.encode16()
[latest_block | [previous_block | []]] = Chain.get_blocks(latest_block_hash, 2)

assert latest_block == Chain.get_block_by_hex_hash(latest_block_hash_hex)
assert previous_block.header.height + 1 == latest_block.header.height
assert BlockValidation.validate_block!(latest_block, previous_block, Chain.chain_state())
assert BlockValidation.validate_block!(latest_block, previous_block,
Chain.chain_state(),
blocks_for_difficulty_calculation)
assert :ok = Chain.add_block(block)
assert latest_block = Chain.latest_block()
assert latest_block.header.height == block.header.height
Expand Down
6 changes: 3 additions & 3 deletions apps/aecore/test/aecore_hashcash_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ defmodule HashcashTest do
use ExUnit.Case
doctest Aecore.Pow.Hashcash

alias Aecore.Pow.Hashcash
alias Aecore.Structures.Header
alias Aecore.Structures.Block
# alias Aecore.Pow.Hashcash
# alias Aecore.Structures.Header
# alias Aecore.Structures.Block

#@tag timeout: 10000000
#test "successfull test" do
Expand Down
4 changes: 3 additions & 1 deletion apps/aecore/test/aecore_validation_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ defmodule AecoreValidationTest do
txs_hash: <<0::256>>,
version: 1},
txs: []}
assert BlockValidation.validate_block!(new_block,prev_block, %{}) == :ok
blocks_for_difficulty_calculation = [new_block, prev_block]
assert BlockValidation.validate_block!(new_block, prev_block, %{},
blocks_for_difficulty_calculation) == :ok
end

test "validate transactions in a block" do
Expand Down