Web3dAppDevCamp

Posted on Apr 13, 2022Read on Mirror.xyz

Build Simple POW Blockchain with Elixir | Elixir Branch 0x03

中文用户请加微信:197626581

Author: Fenix

Translator: Snowyaya, msfew

Livebook Version: https://github.com/zhenfeng-zhu/ex_chain

Tutorial Video:

https://ke.qq.com/course/3616174?tuin=d34bd514&taid=13066085086997934

Histories of this Branch:

https://github.com/WeLightProject/Web3-dApp-Camp/discussions/31

https://github.com/WeLightProject/Web3-dApp-Camp/discussions/32

0x01 Why Elixir

Elixir is a dynamic, functional language for building scalable and maintainable applications.

https://elixir-lang.org/

1.1 Amazing Syntax

Elixir's syntax is a tribute to Ruby with a touch of Erlang. The design of any language syntax is inseparable from the preferences and goals of its authors, and Elixir's authors were core developers in the Rails community, so the syntax is naturally very Ruby. Of course, Elixir, which is rooted in Erlang, has many of its own features.

What's most endearing is the pipe |>, which turns layers of backwards-thinking function calls into a more intuitive representation, for example, we often write code like this:

IO.puts(format(to_map(Store.get_host(host))))

or

list_data = Store.get_host(host)
map = to_map(list)
formatted_output = format(map)
IO.puts(formatted_output)

Such code could be written in Elixir as:

host
|> Store.get_host
|> to_map
|> foramt
|> IO.puts

Very clear! Most importantly, it fits more into your mindset and makes it easier to get the code flowing at your fingertips. When we write code, it is basically a process of constant "partitioning": breaking down big problems into small ones, and small problems into even smaller ones, and finally solving them. And Elixir gives you a high degree of consistency in your code and your thinking.

pipe is very flexible. You can organize your ideas and combine functions at the same time. This is a bit of like building blocks.

1.2 Pattern Matching

Anyone who has learned a little bit of programming should know that = is the assignment operator in most programming languages. Even if this is not the case, there is a similar symbol with a variable on its left and a constant assigned to it on its right. Other variables or some operations such as: a = 1 or a = max(number_list).

However, in Elixir, = is the match operator.

iex>a = 1
1
iex>1 = a
1
iex>2 = a
** (MatchError) no match of right hand side value: 1

Elixir's pattern matching and recursion work really well together. There is no need to use the if else flow to determine the boundary values, and the code is expressed quite elegantly.

I think that recursion can be said to be a very good representation of the First Principle principle. Only by looking at the nature of data processing can we understand recursion. Let's take a look at one of the best quotes from the book.

L. Peter Deutsch once penned, “To iterate is human, to recurse divine.”

As an example to get a feel for it:

defmodule Recursion do
  def sum(0), do: 0
  def sum(n), do: n + sum(n - 1)
end

IO.puts Recursion.sum(5)  # => 15

This function calculates the sum of all positive integers under a given number. The above example is calculating 5+4+3+2+1+0.

Replacing most conditional branches with pattern matching is a great thing: the simplicity of the code goes without saying, and its efficiency can be further optimized. If/else is a sequential logic, and because of the flexibility of its syntax (everyone does it to have the if condition as a function), it can be optimized for special cases using jump tables, most of which are O(N) and difficult to handle in parallel. While pattern matching due to its syntactic limitations, many cases can be optimized into a decision tree, the time complexity is O(logN), and there is room for parallel processing optimization in the future.

1.3 Macro

Many people see a macro and think it represents some kind of mysterious power.

In fact, we can think of a macro as a special function that accepts a syntax tree (one or more) as an argument and then returns a syntax tree.

Is the defmodule block a syntax? No, it is a macro. is a def block a syntax? No, it is still a macro.

x in [1, 2, 3] # => true

This should be a syntax now, right? No, even in is a macro.

1.4 Born with Concurrent Support

Elixir is built on top of Erlang and eventually compiles into fully equivalent bytecode modules that can call each other.

Erlang's actor-based concurrency model, let it crash idea, supervisory tree, error handling, are the best practices that have been summed up during the struggle with concurrency for more than 20 years, both ideologically and practically. Elixir stands on the shoulders of giants and enjoys its success.

1.5 Full Toolchain

Since the 21st century, emerging languages have also gone all out on the toolchain, which has (almost) become part of the language rather than an appendage.

Elixir carries its own mix -- from project creation and scaffolding (mix new), to compilation (mix compile), to testing (mix test), to documentation (mix doc), to dependency management (mix deps), all wrapped up.

Reference: elixir: panacea? or in vain?

1.6 Livebook

Livebook is an interactive and collaborative code notebook for writing for Elixir. It can be compared to Python's jupyter notebook.

https://livebook.dev/

Mix.install/2

We can install the dependencies via Mix.install/2.

:force - if true, removes install cache. This is useful when you want to update your dependencies or your install got into an inconsistent state (Default: false)

:verbose - if true, prints additional debugging information (Default: false)

Mix.install(
  [:poison, :req]
  # force: true,
  # verbose: true
)
:ok
%{hello: "world", arr: [1, 2, 3]}
|> Poison.encode!()
|> IO.inspect()

"{\"hello\": \"world\",\"arr\": [1, 2, 3]}"
|> Poison.decode!()
|> IO.inspect()
"{\"hello\":\"world\",\"arr\":[1,2,3]}"
%{"arr" => [1, 2, 3], "hello" => "world"}
%{"arr" => [1, 2, 3], "hello" => "world"}
Req.get!("https://api.github.com/repos/elixir-lang/elixir").body["description"]
"Elixir is a dynamic, functional language designed for building scalable and maintainable applications"

More fun examples can be found at: https://github.com/wojtekmach/mix_install_examples

0x02 Blockchain Introduction

2.1 Block

The easiest and quickest way to understand what a block really is is to analyze its data structure, using the example of block #514095 in Bitcoin:

{
  "hash": "00000000000000000018b0a6ae560fa33c469b6528bc9e0fb0c669319a186c33",
  "confirmations": 1009,
  "strippedsize": 956228,
  "size": 1112639,
  "weight": 3981323,
  "height": 514095,
  "version": 536870912,
  "versionHex": "20000000",
  "merkleroot": "5f8f8e053fd4c0c3175c10ac5189c15e6ba218909319850936fe54934dcbfeac",
  "tx": [
    // ...
  ],
  "time": 1521380124,
  "mediantime": 1521377506,
  "nonce": 3001236454,
  "bits": "17514a49",
  "difficulty": 3462542391191.563,
  "chainwork": "0000000000000000000000000000000000000000014d2b41a340e60b72292430",
  "previousblockhash": "000000000000000000481ab128418847dc25db4dafec464baa5a33e66490990b",
  "nextblockhash": "0000000000000000000c74966205813839ad1c6d55d75f95c9c5f821db9c3510"
}

In the structure of this Block, previousblockhash and merkleroot are the two most important fields. The former is a hash pointer, which is actually a hash of the previous Block, and through the previousblockhash we can recursively find all the Blocks, i.e., the entire chain; The latter is the root of a Merkle tree, which contains all the transactions in the entire Block. By saving the merkleroot, we can ensure that no transaction in the current Block will be modified.

Ethereum's blockchain model is very different from Bitcoin's, but it has similar information in its Block structure.

{
   "jsonrpc":"2.0",
   "result":{
      "author":"0x00d8ae40d9a06d0e7a2877b62e32eb959afbe16d",
      "difficulty":"0x785042b0",
      "extraData":"0x414952412f7630",
      "gasLimit":"0x47b784",
      "gasUsed":"0x44218a",
      "hash":"0x4de91e4af8d135e061d50ddd6d0d6f4119cd0f7062ebe8ff2d79c5af0e8344b9",
      "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
      "miner":"0x00d8ae40d9a06d0e7a2877b62e32eb959afbe16d",
      "mixHash":"0xb8155224974967443d8b83e484402fb6e1e18ff69a8fc5acdda32f2bcc6dd443",
      "nonce":"0xad14fb6803147c7c",
      "number":"0x2000f1",
      "parentHash":"0x31919e2bf29306778f50bbc376bd490a7d056ddfd5b1f615752e79f32c7f1a38",
      "receiptsRoot":"0xa2a7af5e3b9e1bbb6252ba82a09302321b8f0eea7ec8e3bb977401e4f473e672",
      "sealFields":[
         "0xa0b8155224974967443d8b83e484402fb6e1e18ff69a8fc5acdda32f2bcc6dd443",
         "0x88ad14fb6803147c7c"
      ],
      "sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
      "size":"0x276",
      "stateRoot":"0x87e7e54cf229003014f453d64f0344e2ba4fc7ee3b95c7dd2642cca389fa1efe",
      "timestamp":"0x5a10968a",
      "totalDifficulty":"0x1804de0c47ffe1",
      "transactions":[...],
      "transactionsRoot":"0xc2091b032961ca23cf8323ea827e8956fe6dda9e68d75bcfaa8b910035397e35",
      "uncles":[]
   },
   "id":1
}

parentHash and transactionsRoot correspond to the previousblockhash and merkleroot in Bitcoin, respectively. These two are very important in the whole blockchain network.

2.2 Hash Pointer

The hash pointer in the Block structure has two roles in the blockchain; it not only connects different blocks, but also validates the Block to ensure that the data in the Block is not tampered with by other malicious nodes.

Except for the first Block, the prev_hash in each Block is the hash of the previous Block. If a node wants to modify the transaction of a Block on the main chain, it will change the hash of the current Block. If a node wants to modify the transaction of the Block on the main chain, it will change the hash of the current Block, and the later Block will have no way to find the previous chain through prev_hash. So the current node will be discovered by other nodes if it tampers with the transaction.

2.3 Merkle Tree

Another field merkleroot is actually the root node of a Merkle tree, which is actually a data structure connected using a hash pointer; although Merkle trees have leaf and non-leaf nodes, it only stores data in the leaf nodes. However, only the leaf nodes store data, and all non-leaf nodes are hashes used to verify data integrity.

All transactions in each Block are stored in this Merkle tree and the merkleroot is stored in the Block's structure. This ensures that any transaction tampering in the current Block can be detected immediately.

2.4 Summary

prev_hash and merkleroot ensure that all blocks and transactions are connected by means of "pointers" respectively, which ultimately ensures that blocks and transactions are not tampered with by malicious nodes or attackers, and almost all blockchain projects use similar methods to connect different blocks and transactions. This can be said to be the infrastructure and standard for blockchain projects.

Reference: Draveness's blog

0x03 Create a block

3.1 Block

A basic block structure is as follows:

%{
  :index => 0,
  :timestamp => "",
  :name => "",
  :previous_hash => "",
  :current_transactions => []
}
  • index: index of the current block
  • timestamp: the time when the block was generated
  • name: the name of the block
  • previous_hash: hash of the previous block
  • current_transactions: the transactions of the current block

At this point, the concept of a blockchain should be obvious: each new block contains a hash of the previous block within it.

This is critical because it is the reason why the blockchain is immutable. If an attacker corrupts an earlier block in the blockchain, all subsequent blocks will contain incorrect hashes. Does this make sense? If you haven't figured it out yet, take some time to think about it carefully.

This is the core idea behind the blockchain.

The basic structure of the program is as follows:

defmodule Blockchain do
  defstruct chain: [], current_transactions: []

  def new_block(c, name) do
  end

  def zero() do
  end

  def last_block(c) do
  end

  def hash(block) do
  end
end
warning: variable "c" is unused (if the variable is not meant to be used, prefix it with an underscore)
  share.livemd#cell:4: Blockchain.new_block/2

warning: variable "name" is unused (if the variable is not meant to be used, prefix it with an underscore)
  share.livemd#cell:4: Blockchain.new_block/2

warning: variable "c" is unused (if the variable is not meant to be used, prefix it with an underscore)
  share.livemd#cell:10: Blockchain.last_block/1

warning: variable "block" is unused (if the variable is not meant to be used, prefix it with an underscore)
  share.livemd#cell:13: Blockchain.hash/1
{:module, Blockchain, <<70, 79, 82, 49, 0, 0, 9, ...>>, {:hash, 1}}

3.2 Complete the Program

Completes the new_block function:

def new_block(c, name) do
    b = %{
      :index => length(c.chain),
      :timestamp => NaiveDateTime.utc_now(),
      :name => name,
      :previous_hash => c |> last_block() |> hash(),
      :current_transactions => c.current_transactions
    }

    %{c | chain: c.chain ++ [b], current_transactions: []}
end
  • Compute the hash of the previous block and write it to the previous_hash
  • %{c | chain: c.chain ++ [b], current_transactions: []} is a syntax sugar that appends the current block to the chain.

Elixir hash to be done with the :crypto.hash function like:

value = "fenix"

:crypto.hash(:sha256, value)
|> Base.encode16()
|> String.downcase()
"b170f30d201da73b7637e94391f22a4642f8c396f9c1c9f8b2799919ced46d78"

The complete program is as follows:

defmodule Blockchain do
  defstruct chain: [], current_transactions: []

  def new_block(c, name) do
    b = %{
      :index => length(c.chain),
      :timestamp => NaiveDateTime.utc_now(),
      :name => name,
      :previous_hash => c |> last_block() |> hash(),
      :current_transactions => c.current_transactions
    }

    %{c | chain: c.chain ++ [b], current_transactions: []}
  end

  def zero() do
    b = %{
      :index => 0,
      :timestamp => NaiveDateTime.utc_now(),
      :name => "ZERO",
      :previous_hash => "",
      :current_transactions => []
    }

    %Blockchain{
      chain: [b],
      current_transactions: []
    }
  end

  def last_block(c) do
    c.chain |> Enum.reverse() |> hd()
  end

  def hash(block) do
    value = block |> Poison.encode!()

    :crypto.hash(:sha256, value)
    |> Base.encode16()
    |> String.downcase()
  end
end
{:module, Blockchain, <<70, 79, 82, 49, 0, 0, 14, ...>>, {:hash, 1}}

3.3 Try and Generate Genesis Block

Genesis Block

zero = Blockchain.zero()
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:17:21.126190]
    }
  ],
  current_transactions: []
}

Create more blocks

zero
|> Blockchain.new_block("first")
|> Blockchain.new_block("second")
|> Blockchain.new_block("third")
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:17:21.126190]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "dd9a0b7a81ba060c2d1cd7e581ad0bda35c7019831049b39dbcc1c35a8f3f131",
      timestamp: ~N[2022-04-01 12:17:35.067392]
    },
    %{
      current_transactions: [],
      index: 2,
      name: "second",
      previous_hash: "f93e075bef13d86514a77d407e36ac9d3b2636769bfe483a3ecc4a2afd5b88ac",
      timestamp: ~N[2022-04-01 12:17:35.067467]
    },
    %{
      current_transactions: [],
      index: 3,
      name: "third",
      previous_hash: "0ac2d3ec623b955ef6781ff130f6aa8f65b1dd1022f9c7f6a60d40ee03ed3c8a",
      timestamp: ~N[2022-04-01 12:17:35.067551]
    }
  ],
  current_transactions: []
}

0x04 Realization of Transactions

The word 'transaction' in blockchain is confusing and seems to represent a conversion that necessarily involves money.

In reality, we should abstract transactions to the following abstract model:

{
    from: from_addr,
    to: to_addr,
    amount: amount, # Only for public blockchains
    gas: gas, # transaction service fee
    op: operation # Operations done with the transaction
}

In a public blockchain, each transaction will contain an amount (the amount may be 0) and gas. Some transactions are simply user-to-user transfers, and some transactions are keyed to operation. In this case, to is usually a contract address, and operation tells the "blockchain computer" what to do - for example, to store a value in the blockchain database, or to perform some computation on the blockchain - and we think of the blockchain as a "distributed computer".

In federated chains, the concept of amount is deprecated, and there are no more transactions for native token transfers (note that smart contract-based tokens like ERC20 are supported, though they are often referred to as credits). However, gas still exists and is used to measure the consumption of computational resources.

The format of a transaction in Elixir can be abstracted as follows:

%{
  :sender => sender,
  :recipient => recipient,
  :amount => amount
}

Completes the new_transaction function:

def new_transaction(c, sender, recipient, amount) do
    tx = %{
      :sender => sender,
      :recipient => recipient,
      :amount => amount
    }

    %Blockchain{c | current_transactions: c.current_transactions ++ [tx]}
end

The complete code for our transactions is as follows:

defmodule Blockchain do
  defstruct chain: [], current_transactions: []

  def new_block(c, name) do
    b = %{
      :index => length(c.chain),
      :timestamp => NaiveDateTime.utc_now(),
      :name => name,
      :previous_hash => c |> last_block() |> hash(),
      :current_transactions => c.current_transactions
    }

    %{c | chain: c.chain ++ [b], current_transactions: []}
  end

  def zero() do
    b = %{
      :index => 0,
      :timestamp => NaiveDateTime.utc_now(),
      :name => "ZERO",
      :previous_hash => "",
      :current_transactions => []
    }

    %Blockchain{
      chain: [b],
      current_transactions: []
    }
  end

  def new_transaction(c, sender, recipient, amount) do
    tx = %{
      :sender => sender,
      :recipient => recipient,
      :amount => amount
    }

    %Blockchain{c | current_transactions: c.current_transactions ++ [tx]}
  end

  def last_block(c) do
    c.chain |> Enum.reverse() |> hd()
  end

  def hash(block) do
    value = block |> Poison.encode!()

    :crypto.hash(:sha256, value)
    |> Base.encode16()
    |> String.downcase()
  end
end
{:module, Blockchain, <<70, 79, 82, 49, 0, 0, 16, ...>>, {:hash, 1}}

4.1 Try and create transaction

Genesis block

zero = Blockchain.zero()
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:20:26.788775]
    }
  ],
  current_transactions: []
}

Execute a few transactions and pack into a new block

zero
|> Blockchain.new_transaction("alice", "bob", 100)
|> Blockchain.new_transaction("alice", "bob", 20)
|> Blockchain.new_block("first")
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:20:26.788775]
    },
    %{
      current_transactions: [
        %{amount: 100, recipient: "bob", sender: "alice"},
        %{amount: 20, recipient: "bob", sender: "alice"}
      ],
      index: 1,
      name: "first",
      previous_hash: "56881bced4dd4b52f76cc75e0b7f217fe4696e3e15974d4c174d84fb4205b92a",
      timestamp: ~N[2022-04-01 12:20:39.174474]
    }
  ],
  current_transactions: []
}

The transaction now is in the first block.

0x05 Proof of Work (PoW)

The concept was widely spread because Bitcoin adopted the PoW consensus mechanism. The full name of PoW is Proof of Work. The PoW consensus mechanism is actually a design idea rather than a specific implementation.

The PoW mechanism was first proposed as early as 1997, and it was mostly used in the scenario of resisting abuse of software services in the early days such asanti-spam (that's why PoW is involved in the mail service system).

We'll use a Wikipedia diagram to explain how the PoW mechanism is used in this scenario.

In order to prevent the spread of spam messages, the receiver does not directly accept the message from any sender. Therefore, in a valid conversation, the sender needs to calculate an answer to the problem according to the rules, and send it to the receiver at the same time. The answer needs to be verified with it. If the answer is verified as valid, then the recipient will accept the message.

可以看出 PoW 的核心设计思路是提出一个计算难题,但是这个难题答案的验证过程是非常容易的,这种特性我们称之为计算不对称特性

5.1 How to understand PoW in blockchain

As an example, let's say we are given a string "Fenix" and the problem we pose is, to compute a number, concatenated with the given string, so that the first 4 bits of the SHA256 result of this string are 0. We call this number nonce, such as the string "Fenix1234", nonce is 1234 and we want to find the nonce that meets the criteria.

An Example in Python

#!/usr/bin/env python
import hashlib

def main():
    base_string = "Fenix"
    nonce = 10000
    count = 0
    while True:
        target_string = base_string + str(nonce)
        pow_hash = hashlib.sha256(target_string).hexdigest()
        count = count + 1
        if pow_hash.startswith("0000"):
            print pow_hash
            print "nonce: %s  scan times: %s" % (nonce, count)
            break
        nonce = nonce + 1

if __name__ == '__main__':
    main()

The base string is specified as "Fenix", and the nonce starts to search up from 10000, until a matching nonce value is found.

# The first 4 digits are 0000
0000250248f805c558bc28864a6bb6bf0c244d836a6b1a0c5078987aa219a404
nonce: 68828  scan times: 58829
# The first 5 digits are 00000
0000067fc247325064f685c32f8a079584b19106c5228b533f10c775638d454c
nonce: 1241205  scan times: 1231206
The first 7 digits are 0000000
00000003f41b126ec689b1a2da9e7d46d13d0fd1bece47983d53c5d32eb4ac90
nonce: 165744821  scan times: 165734822

Each time the first N bits of the hash result are required to have one more 0, the number of calculations is many times more. When the first 7 bits are required to be 0, the number of computations reaches 160 million.

Reference: 《深入浅出区块链》

5.2 Other languages

5.2.1 Go

package main

import (
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "strconv"
    "strings"
)

func main() {
    pow("0000")
}

func pow(diff string) {
    baseStr := "fenix"
    nonce := 0
    count := 0
    for {
        targetStr := baseStr + strconv.Itoa(nonce)
        h := sha256.New()
        h.Write([]byte(targetStr))
        powHash := hex.EncodeToString(h.Sum(nil))
        fmt.Printf("\r%s", powHash)
        count += 1
        if strings.HasPrefix(string(powHash), diff) {
            fmt.Println()
            fmt.Println("nonce: ", nonce, "scan times: ", count)
            break
        }
        nonce += 1
    }
}

5.2.2 Rust

use sha256::digest;

fn main() {
    pow(String::from("0000"));
}

fn pow(diff: String) {
    println!("{}", diff);
    let mut count = 0;
    let mut nonce = 0;
    let base_str = String::from("fenix");
    loop {
        count += 1;
        let target_str = base_str.clone() + &nonce.to_string();
        let pow_hash = digest(target_str);
        print!("\r{}", pow_hash);
        if pow_hash.starts_with(&diff) {
            println!("");
            println!("nonce: {}, scan times: {}", nonce, count);
            break;
        }
        nonce += 1;
    }
}

When using mainstream programming languages ​​implement it, then tend to choose to use loop.

Elixir's variables are immutable, so conventional looping methods are not possible. Therefore we may use recursion to implement as follows:

defmodule Pow do
  def work(value, nonce, count, difficulty) do
    if String.starts_with?(value, difficulty) do
      IO.puts(value)
      IO.puts("nonce: #{nonce}, scan times: #{count}")
    else
      digest("#{value}#{nonce}") |> work(nonce + 1, count + 1, difficulty)
    end
  end

  def digest(value) do
    IO.write("\rhash: #{value}")

    :crypto.hash(:sha256, value)
    |> Base.encode16()
    |> String.downcase()
  end
end
{:module, Pow, <<70, 79, 82, 49, 0, 0, 10, ...>>, {:digest, 1}}
Pow.work("Fenix", 0, 0, "0000")
Pow.work("Fenix", 0, 0, "00000")
hash: a9d1ee2ad4586af4acc3b3dbd97d85355fbd4f8634a2062b38f4f878d350eb7e1770240000303ab87561a134d5ba70ff0dc96f59df2cb2c76a12418363a7065d7021e5
nonce: 177025, scan times: 177025
hash: 78ba29b80a1a9a0dbd363f00119a1822aad18cf999b93b732ff6fe7c42d4ee6b1468816000002fc5a2268fc4e118f4a221e1e76991c3f9a20d683f489e8466e26fcde54
nonce: 1468817, scan times: 1468817
:ok

The higher the difficulty, the more calculations are required.

0x06 Final State

Now, we'll add proof-of-work to the blockchain. The complete program is as follows:

defmodule Blockchain do
  defstruct chain: [], current_transactions: [], last_nonce: 0

  def new_block(c, name, nonce) do
    if valid_proof?(c.last_nonce, nonce) do
      b = %{
        :index => length(c.chain),
        :timestamp => NaiveDateTime.utc_now(),
        :name => name,
        :previous_hash => c |> last_block() |> hash(),
        :current_transactions => c.current_transactions
      }

      %{c | chain: c.chain ++ [b], current_transactions: [], last_nonce: nonce}
    else
      IO.puts("\nnonce: #{nonce} is invalid")
    end
  end

  def zero() do
    b = %{
      :index => 0,
      :timestamp => NaiveDateTime.utc_now(),
      :name => "ZERO",
      :previous_hash => "",
      :current_transactions => []
    }

    %Blockchain{
      chain: [b],
      current_transactions: [],
      last_nonce: 0
    }
  end

  def new_transaction(c, sender, recipient, amount) do
    tx = %{
      :sender => sender,
      :recipient => recipient,
      :amount => amount
    }

    %Blockchain{c | current_transactions: c.current_transactions ++ [tx]}
  end

  def last_block(c) do
    c.chain |> Enum.reverse() |> hd()
  end

  def hash(block) do
    value = block |> Poison.encode!()

    :crypto.hash(:sha256, value)
    |> Base.encode16()
    |> String.downcase()
  end

  def proof_of_work(last_nonce, nonce \\ 0) do
    case valid_proof?(last_nonce, nonce) do
      true ->
        nonce

      _ ->
        proof_of_work(last_nonce, nonce + 1)
    end
  end

  def valid_proof?(last_nonce, nonce, difficulty \\ "0000") do
    guess = "#{last_nonce}#{nonce}"

    guess_hash =
      :crypto.hash(:sha256, guess)
      |> Base.encode16()
      |> String.downcase()

    IO.write("\rdifficulty: #{difficulty}, attempt: #{nonce}, hash: #{guess_hash}")
    guess_hash |> String.starts_with?(difficulty)
  end
end
{:module, Blockchain, <<70, 79, 82, 49, 0, 0, 24, ...>>, {:valid_proof?, 3}}

Genesis Block

zero = Blockchain.zero()
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:27:28.717693]
    }
  ],
  current_transactions: [],
  last_nonce: 0
}

If you want to generate a new block, you must calculate the nonce and use the calculated nonce to create the block. If the nonce is not calculated, an error will be reported. For example, I randomly gave a nonce of 333 here and let's try to execute it:

first = zero |> Blockchain.new_block("first", 333)
difficulty: 0000, attempt: 333, hash: e73a36f8264731f64049b86564604e671d4bb1cfc0f6ab26803a5ad18040fee2
nonce: 333 is invalid
:ok

Call proof_of_work to compute the nonce:

nonce = Blockchain.proof_of_work(zero.last_nonce)
difficulty: 0000, attempt: 69732, hash: 0000e326186933fa83f0efd581d09409022ec07b73a10f549bbaa6472e8a1175
69732

Use the calculated nonce to generate blocks:

first = zero |> Blockchain.new_block("first", nonce)
difficulty: 0000, attempt: 69732, hash: 0000e326186933fa83f0efd581d09409022ec07b73a10f549bbaa6472e8a1175
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:27:28.717693]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "d530c9090258905de8009fe933f8e4686916adc3e803b89600615a2fb817392d",
      timestamp: ~N[2022-04-01 12:28:33.450247]
    }
  ],
  current_transactions: [],
  last_nonce: 69732
}

Done!

Complete the following steps:

  • Build the genesis block
  • Calculate the nonce, create the first block
  • Calculate new nonce
  • Execute a few transactions to generate a new block
zero = Blockchain.zero()

nonce = Blockchain.proof_of_work(zero.last_nonce)
IO.inspect(nonce)
first = zero |> Blockchain.new_block("first", nonce)
IO.inspect(first)

nonce = Blockchain.proof_of_work(first.last_nonce)

second =
  first
  |> Blockchain.new_transaction("alice", "bob", 100)
  |> Blockchain.new_block("second", nonce)

IO.inspect(second)
difficulty: 0000, attempt: 69732, hash: 0000e326186933fa83f0efd581d09409022ec07b73a10f549bbaa6472e8a117569732
difficulty: 0000, attempt: 69732, hash: 0000e326186933fa83f0efd581d09409022ec07b73a10f549bbaa6472e8a1175%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:29:06.201828]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "b8099550bfe6931ad0eccb7a18d1d638d33177fe6ecc7621dcb593bdc406cf00",
      timestamp: ~N[2022-04-01 12:29:06.916490]
    }
  ],
  current_transactions: [],
  last_nonce: 69732
}
difficulty: 0000, attempt: 23263, hash: 0000ffc4b3bdbd6d46a4649d48944700b204fe59883f915fe1030f05c16a5492%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:29:06.201828]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "b8099550bfe6931ad0eccb7a18d1d638d33177fe6ecc7621dcb593bdc406cf00",
      timestamp: ~N[2022-04-01 12:29:06.916490]
    },
    %{
      current_transactions: [%{amount: 100, recipient: "bob", sender: "alice"}],
      index: 2,
      name: "second",
      previous_hash: "5e1e75f4822ac949e20944175a39d9301ad278611eb74043cb761ba83742df8c",
      timestamp: ~N[2022-04-01 12:29:07.222425]
    }
  ],
  current_transactions: [],
  last_nonce: 23263
}
%Blockchain{
  chain: [
    %{
      current_transactions: [],
      index: 0,
      name: "ZERO",
      previous_hash: "",
      timestamp: ~N[2022-04-01 12:29:06.201828]
    },
    %{
      current_transactions: [],
      index: 1,
      name: "first",
      previous_hash: "b8099550bfe6931ad0eccb7a18d1d638d33177fe6ecc7621dcb593bdc406cf00",
      timestamp: ~N[2022-04-01 12:29:06.916490]
    },
    %{
      current_transactions: [%{amount: 100, recipient: "bob", sender: "alice"}],
      index: 2,
      name: "second",
      previous_hash: "5e1e75f4822ac949e20944175a39d9301ad278611eb74043cb761ba83742df8c",
      timestamp: ~N[2022-04-01 12:29:07.222425]
    }
  ],
  current_transactions: [],
  last_nonce: 23263
}

0x07 The sage's time

Programming is about Data Transformation

Programming Should Be About Transforming Data.

If you deeply understand this quote, the ETL (Extract - Transform - Load) of data analysis is so natural, and Map/Reduce the dividing & conquering is also well understood.

There is no single correct way to write code, so we may change the way of thinking:

  • Object-orientated programming is not the only way to design code
  • Functional programming does not have to be complex and purely mathematical
  • Programming is not based on assignments, if statements and loops
  • Concurrency doesn't necessarily require locks, semaphores, monitors, etc.
  • Processes don't have to consume a lot of resources
  • Metaprogramming is not just an add-on to languages
  • Even if programming is your job, it should be fun

Mastering Elixir may not yet add luster to your resume, allowing you to get a higher premium in the workplace. But learning Elixir can give you a different perspective on functions, mutability, concurrency, and high availability.

Elixir is very different from mainstream programming languages, and it will enrich your perspective and open your eyes to new programming thinking.

Finally, Why Elixir & Blockchain? Make sure Web3 happens in Elixir!

Reference


snowyaya