Xây dựng một Blockchain đơn giản nhất với Ruby & Rails
Bài đăng này đã không được cập nhật trong 6 năm
Hello ! Xin chào ! Ok ! Bạn đã click vào bài viết này , vậy thì có lẽ bạn - cũng giống như tôi - cũng đã ít nhất nghe về, cũng như có hứng thú với sự phát triển khủng khiếp trong thời gian gần đây của tiền ảo. Và có thể bạn cũng sẽ muốn tìm hiểu về Blockchains - một trong những công nghệ nền tảng đứng sau nó.
Nhưng để hiểu về Blockchains thì quả thực là không dễ - hay ít nhất là đối với tôi. Tôi đã tốn khá nhiều công sức để theo dõi hàng loạt các video, các loại tutorial , và thực sự bực mình với việc chúng chứa quá ít ví dụ để hiểu.
Tôi thích việc học thông qua thực hành. Nó bắt buộc ta phải đối diện với vấn đề ở tầng coding - việc này giúp cho ta hiểu rõ vấn đề hơn. Và nếu bạn theo tôi cho đến cuối bài viết này, bạn sẽ có (ít nhất) 1 Blockchains có-thể-hoạt-động-được và hiểu 1 cách cơ bản làm thế nào mà nó hoạt động được.
Một chút khái niệm trước khi bắt đầu
Đơn giản nhất, blockchain là một chuỗi (chain) tuần tự các bản ghi (được gọi là Blocks). Và các block này là immutable (tức là một khi được tạo ra thì chúng không thể bị thay đổi nữa). Trong thực tế, các block này được sử dụng để lưu trữ các giao dịch (transaction), file hoặc bất cứ dữ liệu (data) nào bạn muốn. ĐIỀU QUAN TRỌNG là các block được nối (chain) với nhau sử dụng hash
Nếu bạn chưa biết hash là gì, có thể đọc thêm ở đây
Step 1 : Build 1 blockchain
Xài 1 cái texteditor nào đó mà bạn thích (Vim ❤️), đặt 1 cái tên bất kì cho model (blockchain.rb )
Prototype 1 blockchain
Đầu tiên , dựng 1 class Blockchain
với constructor sẽ tạo một mảng rỗng ( để lưu trữ blockchain của ta), mà một cái mảng rỗng nữa để chứa các transactions. Đây có thể coi như là blueprint của class chúng ta sẽ xây dựng
class BlockChain
def initialize
@chain = Array.new
@current_transactions = Array.new
end
def new_block
# Thêm một block vào chain
end
def new_transaction
# Thêm một transaction vào list transaction
end
class << self
def hash(block)
# Trả về hash của một block
end
end
def last_block
# Trả về block cuối trong chain
end
end
class BlockChain
sẽ đảm nhiệm việc quản lý chain. Nó sẽ chứa các transaction và có thêm một vài helper method để thêm block vào chain. Hãy bắt đầu với từng method một.
Một Block sẽ trông như thế nào ?
Mỗi một Block
sẽ có index, một timestamp (ở dạng Unix time), một list các transation, và proof (sẽ giải thích sau), và cuối cùng là 1 hash của Block trước nó.
Đây là ví dụ về định dạng của 1 Block:
block = {
'index': 1,
'timestamp': 1506057125.900785,
'transactions': [
{
'sender': "8527147fe1f5426f9dd545de4b27ee00",
'recipient': "a77f5cdfa2934df3954a5c7c7da5df1f",
'amount': 5,
}
],
'proof': 324984774000,
'previous_hash': "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824"
}
Lúc này, ý tưởng về một chuỗi
là rất rõ ràng - mỗi một block mới - ngoài việc chứa thông tin của chính nó - sẽ chứa thêm cả hash của Block trước đó. Đây là điều thiết yếu nhất vì nó là thứ tạo nên tính immutability của blockchain : Nếu một kẻ tấn công vào một Block trong chuỗi, thì khi đó tất cả các Block phía sau sẽ chứa thông tin (hash) sai.
Thêm một transaction vào Block
Ta cần một cách nào đó để thêm các transaction vào Block. Hàm new_transaction
sẽ chịu trách nhiệm cho việc này:
def new_transaction(sender, recipient, amount)
@current_transactions << {
sender: sender,
recipient: recipient,
amount: amount
}
return last_block[:index] + 1
end
Sau khi new_transaction
thêm một transaction vào list, nó sẽ trả về index
của block mà transaction sẽ được thêm vào - chú ý đây là index kế tiếp
. Giá trị này sẽ có ích về sau này.
Tạo một block mới
Khi BlockChain
được khởi tạo, ta cần tạo cho nó một block ban đầu. Ta cũng cần thêm vào một proof
cho block nguyên thủy này - là kết quả của quá trình mining (có thể coi như là kết quả của công việc). Ta sẽ quay lại với mining sau.
Cùng với việc khởi tạo một block ban đầu trong constructor , ta sẽ xem xét chi tiết implementation cho các method new_block
, new_transaction
, hash
:
class BlockChain
require 'digest'
def initialize
@chain = Array.new
@current_transactions = Array.new
new_block(previous_hash: 1, proof: 100)
end
def new_block(proof:, previous_hash: nil)
block = {
index: @chain.length + 1,
timestamp: Time.current,
transactions: @current_transactions,
proof: proof,
previous_hash: previous_hash || self.class.hash(@chain[-1])
}
@current_transactions = Array.new
@chain << block
return block
end
def new_transaction(sender, recipient, amount)
@current_transactions << {
sender: sender,
recipient: recipient,
amount: amount
}
return last_block[:index] + 1
end
class << self
def hash(block)
Digest::SHA256.hexdigest(block.to_json)
end
end
def last_block
@chain[-1]
end
end
Đoạn code trên có thể coi là gần như đầy đủ - ta đã gần xong trong việc mô tả 1 blockchain ở mức sơ khai nhất. Nhưng đến bây giờ, có lẽ bạn vẫn thắc mắc là bằng cách nào các block mới được khởi tạo và ta sẽ làm gì với chúng.
Hiểu về khái niệm "Proof of work"
Thuật toán proof of work
(PoW) là cách mà một Block mới được tạo hoặc được mined (đào). Mục đích của PoW là tìm ra một con số để giải quyết một vấn đề cụ thể nào đó. Con số đó phải khó để tìm được, nhưng cũng phải dễ để xác minh - đối với tất cả mọi người trong mạng network.
Nghe rất khó hiểu phải không ??? Hãy cùng xem qua ví dụ nhỏ dưới đây.
Đặt ra bài toán rằng 1 hash của một số integer x
nhân với một số y
phải kết thúc với 0
.
Nói cách khác - cặp số của ta phải thỏa mãn: hash(x*y)=aho4d0sl...0
.
Với ví dụ này, coi x=5
, khi đó công việc của ta ở đây là phải tìm ra số y
nhỏ nhất thỏa mãn bài toán trên :
require 'digest'
x = 5 # ví dụ ^^
y = 0 # bắt đầu tìm kiếm y
while Digest::SHA256.hexdigest((x*y).to_json)[-1] != "0"
y += 1
end
puts "Kết quả là: #{y}"
Kết quả được in ra sẽ là y = 21
, hay nói cách khác hash(5*21) = 1253e9373e781b7500266caa55150e08e210bc8cd8cc70d89985e3600155e860
Trong Bitcoin, thuật toán PoW được gọi là Hashcash. Và nó cũng không quá khác biệt với ví dụ ở trên của chúng ta - dĩ nhiên là ở mức độ khó hơn Nó là 1 thuật toán
để các thợ đào (miner) cùng đua tranh để giải, từ đó có quyền tạo một block mới. Độ khó của thuật toán được xác định bởi số lượng character phải tìm trong chuỗi string đó. Cuối cùng, các miner sẽ nhận được phần thưởng cho đáp án của mình bằng coin - trong một transaction.
Nhớ - các máy khác trong network cần phải có khả năng kiểm chứng (verify) giải pháp của miner một cách dễ dàng.
Implement PoW
Hãy cùng implement một thuật toán tương tự trong Blockchain của chúng ta. Luật của ta bây giờ sẽ khác một chút:
Tìm một số
p
mà khihash
nó cùng với kết quả của block trước đó, sẽ trả ra 1 hash mới với 4 số0
đứng đầu
class BlockChain
...
def proof_of_work(last_proof)
proof = 0
while !self.class.valid_proof(last_proof, proof)
proof += 1
end
return proof
end
...
class << self
def valid_proof(last_proof, proof)
guess = "#{last_proof}#{proof}"
guess_hash = Digest::SHA256.hexdigest(guess.to_json)
return guess_hash[0..3] == "0000"
end
end
...
end
Nếu muốn điều chỉnh độ khó của thuật toán, ta có thể thay đổi số lượng số 0
cần validate. Với ví dụ này thì 4 là đủ (bạn có thể thử và thấy rằng chỉ cần thêm 1 số thôi cũng sẽ làm việc tính toán tăng thời gian lên khá nhiều @@).
Class của ta đã gần hoàn thiện và giờ thì ta đã sẵn sàng để đưa nó vào sử dụng.
Step 2: Xây dựng API cho blockchain
Với 1 ứng dụng demo như thế này, ta chỉ cần 3 method để thể hiện các phương thức cần thiết nhất:
/transaction/new
để tạo một transaction mới cho block./mine
để bảo server đào 1 block mới./chain
để trả về Blockchain đầy đủ.
Xây dựng 1 controller block_chain_controller
:
class BlockChainController < ApplicationController
def mine
render json: {message: 'We will mine a new Block'}
end
def new_transaction
render json: {message: 'We will add a new transaction'}
end
def full_chain # Trả về chuỗi block_chain
response = SampleApp::Application.block_chain.to_json
render json: response, status: 200
end
end
... với routing
...
namespace :block_chain do
get :mine
post :new_transaction
get :full_chain
end
Transaction Endpoint
Đây là format của 1 request cho 1 transaction mà user gửi lên server:
{
"sender": "user address",
"recipient": "another address",
"amount": 5
}
Vì chúng ta đã có sẵn method để thêm transaction vào block, phần còn lại khá là đơn giản. Quay trở lại với new_transaction
cho controller:
def new_transaction
transaction_params = params.permit(:sender, :recipient, :amount)
index = SampleApp::Application.block_chain.new_transaction(
transaction_params[:sender],
transaction_params[:recipient],
transaction_params[:amount]
)
response = {message: "Transaction will be added to Block #{index}"}
render json: response, status: 201
end
Mining Endpoint
Phần này còn dễ hơn nữa Và nó có nhiệm vụ là làm 3 việc:
- Tính toán PoW
- Trả công cho miner bằng cách thêm 1 transaction đánh dấu việc trả cho chúng ta 1 coin
- Gắn thêm 1 Block vào chain
def mine
# Ta chạy thuật toán để lấy proof tiếp theo ...
last_block = SampleApp::Application.block_chain.last_block
last_proof = last_block[:proof]
proof = SampleApp::Application.block_chain.proof_of_work(last_proof)
# Nhận lấy phần thưởng cho việc tìm ra proof.
# sender bằng "0" để kí hiệu rằng node này là dành cho việc đào được coin mới.
SampleApp::Application.block_chain.new_transaction(
0,
"node_identifier", # => Trên thực tế, đây là địa chỉ node của ta
1
)
# Đóng dấu Block mới bằng việc thêm nó vào chain
previous_hash = BlockChain.hash(last_block)
block = SampleApp::Application.block_chain.new_block(proof: proof, previous_hash: previous_hash)
response = {
message: 'New Block Forged',
index: block[:index],
transactions: block[:transactions],
proof: block[:proof],
previous_hash: block[:previous_hash]
}
render json: response, status: 200
end
Chú ý rằng recipient
của mined block chính là địa chỉ của node của ta.
Đến bước này, code đã gần hoàn chỉnh và ta có thể test thử - ta có thể thử với 1 client kiểu như cURL trên linux hoặc Postman.
Step 3: Chạy thử blockchain
Chạy server Rails
$ rails server
=> Booting Puma
=> Rails 5.0.7 application starting in development on http://localhost:3000
=> Run `rails server -h` for more startup options
Puma starting in single mode...
* Version 3.11.4 (ruby 2.4.1-p111), codename: Love Song
* Min threads: 5, max threads: 5
* Environment: development
* Listening on tcp://0.0.0.0:3000
Use Ctrl-C to stop
Bắt đầu thử với API đầu tiên - đào một block bằng cách tạo GET
request tới localhost:3000/block_chain/mine
:
Tiếp đó, tạo một transaction mới bằng cách tạo một POST
request tới localhost:3000/block_chain/new_transaction
với body chứa thông tin của transaction:
Tiếp tục đào thêm nhiều block và tạo nhiều transaction mới , sau đó xem lại toàn bộ kết quả dựa theo API thứ 3: localhost:3000/block_chain/full_chain
.
Ta có thể thấy 1 đoạn chain tương tự như thế này:
{
"chain": [
{
"index": 1,
"timestamp": "2018-05-03T07:08:24.332Z",
"transactions": [],
"proof": 100,
"previous_hash": 1
},
{
"index": 2,
"timestamp": "2018-05-03T07:08:28.268Z",
"transactions": [
{
"sender": 0,
"recipient": "node_identifier",
"amount": 1
}
],
"proof": 35493,
"previous_hash": "e8f9a4bf0f97d03a2c275760744c63ca7d6175ee090659bc95f1ef799d59b870"
},
{
"index": 3,
"timestamp": "2018-05-03T07:16:20.573Z",
"transactions": [
{
"sender": "do4d0dk0gk3nvobm50fmv04mfglb0",
"recipient": "\"someone else address\"",
"amount": "5"
},
{
"sender": "do4d0dk0gk3nvobm50fmv04mfglb0",
"recipient": "\"someone else address\"",
"amount": "5"
},
{
"sender": 0,
"recipient": "node_identifier",
"amount": 1
}
],
"proof": 35694,
"previous_hash": "ee507263b637113d74fad784cc830d2ac3f7843f1f1d4e206fd446df4ba01ea6"
},
{
"index": 4,
"timestamp": "2018-05-03T07:16:25.130Z",
"transactions": [
{
"sender": "do4d0dk0gk3nvobm50fmv04mfglb0",
"recipient": "\"someone else address\"",
"amount": "5"
},
{
"sender": 0,
"recipient": "node_identifier",
"amount": 1
}
],
"proof": 34596,
"previous_hash": "876a2d07da34722771d6157f49d8a423fa5c222824df21f7734b3ec37842479c"
},
{
"index": 5,
"timestamp": "2018-05-03T07:16:25.970Z",
"transactions": [
{
"sender": 0,
"recipient": "node_identifier",
"amount": 1
}
],
"proof": 35284,
"previous_hash": "88326603fe0b5e8d797ad917afb58e272c6ad4c80f6252493af70a208f037c60"
}
],
"current_transactions": []
}
Step 4: Đồng thuận (consensus)
Đây là một tính năng rất hay. Ta đã có một blockchain cơ bản - cho phép nhận các transaction và cho phép đào Block mới. Nhưng toàn bộ ý nghĩa của Blockchains đó là chúng nên được tổ chức theo dạng phân tán (decentralized). Và một khi ở dạng phân tán, vậy thì làm cách nào ta đảm bảo tất cả các chain đều thể hiện cho một chain duy nhất ? Đây là vấn đề về tạo sự đồng thuận (Consensus), và ta sẽ phải implement thuật toán Consensus nếu ta muốn có nhiều hơn 1 node tham gia vào network.
Đăng kí thêm 1 node mới
Trước khi ta có thể implement thuật toán Consensus, ta cần có một cách để cho phép một node biết về các node xung quanh nó. Mỗi node trong mạng cần giữ một bản đăng kí danh tính (registry) của các node khác trong mạng. Do đó, ta cần có thêm 2 endpoint:
/nodes/register
- chấp nhận 1 list các node./nodes/resolve
- cho phép ta implement thuật toán Consensus - thuật toán này sẽ giúp giải quyết mọi conflict, giữ cho các node có được chain đúng và đồng bộ.
Ta cần chỉnh sửa lại constructor của BlockChain
và cung cấp thêm method để đăng kí các node:
class BlockChain
attr_accessor :nodes, :chain
def initialize
...
@nodes = Set[]
end
...
def register_node(address)
@nodes << address
end
end
Chú ý lại là ta sử dụng Set
để lưu trữ các nodes, điều này sẽ giúp danh sách các node là uniq - nếu ta có thêm cùng 1 địa chỉ vào node nhiều lần, nó cũng sẽ chỉ tính là 1 địa chỉ duy nhất.
Implement thuật toán Consensus
Như đã đề cập, một conflict xảy ra khi 1 node có chain khác với các node khác. Để giải quyết, ta cần tạo ra một luật : nếu có conflict xảy ra, lấy chain nào valid và dài nhất. Sử dụng thuật toán này, ta sẽ giữ được sự đồng thuận giữa các node trong network, nhất là khi có tranh chấp xảy ra:
class BlockChain
.......
class << self
# Hàm này sẽ kiểm tra xem 1 blockchain có valid hay không.
def valid_chain(chain)
last_block = chain[0]
current_index = 1
while current_index < chain.length
block = chain[current_index]
# Kiểm tra xem hash của block có chính xác không ?
return false if block[:previous_hash] != self.class.hash(last_block)
# Kiểm tra xem Proof of Work có chính xác
return false unless self.class.valid_proof(last_block[:proof], block[:proof])
last_block = block
current_index += 1
end
return true
end
end
...
# Đây là thuật toán Consensus của ta: nó sẽ thực hiện bằng cách thay thế chain của ta bằng chain dài nhất trong network
# Trả về true nếu chain hiện tại bị thay thế
def resolve_conflicts
new_chain = nil
max_length = @chain.length # tính toán độ dài chain dài nhất, khởi đầu là chain hiện tại
for node in @nodes
response = Net::HTTP.new(node).request # => For DEMO purporse: implement request để lấy chain hàng xóm được rút ngắn :)
length = response[:length]
chain = response[:chain]
if (length > max_length && self.class.valid_chain(chain)) # Nếu chain hàng xóm dài hơn của ta và valid => thay thế
max_length = length
new_chain = chain
end
end
# Cuối cùng, trả về true nếu chain hiện tại bị thay thế, trả về false nếu ngược lại
if new_chain
@chain = new_chain
return true
end
return false
end
end
Hàm đầu tiên - valid_chain
- có trách nhiệm kiểm tra xem 1 chain có valid hay không bằng cách duyệt qua từng block và verify cả hash và proof.
Hàm resolve_conflicts
sẽ duyệt qua tất cả các node hàng xóm, download về chain hiện tại của họ, và kiểm tra xác thực. Nếu có một chain nào valid và dài hơn chain hiện tại , thì ta sẽ thay thế bằng chain đó.
Đó là implement cho 2 hàm trong class BlockChain
, giờ ta sẽ implement thêm 2 API endpoint trong controller
class BlockChainController < ApplicationController
...
def register_nodes
nodes = params[:nodes]
for node in nodes
SampleApp::Application.block_chain.register_node(node)
end
response = {
message: 'New nodes have been added',
total_nodes: SampleApp::Application.block_chain.nodes
}
return json: response, status: 200
end
def resolve
replaced = SampleApp::Application.block_chain.resolve_conflicts
message = replaced ? 'Our chain was replaced' : 'Our chain is authoritative'
response = {
message: message,
chain: SampleApp::Application.block_chain.chain
}
return json: response, 200
end
end
Bước này, để test, bạn có thể chạy server trên mạng LAN, kiếm thêm 1, 2 cái máy để chạy thử. Hoặc đơn giản hơn là chạy server test trên các cổng khác nhau
Sau đó, tạo một vài Block mới trong node 2 (cổng 3001), rồi gọi /nodes/resolve
từ node 1 (cổng hiện tại - 3000). Khi đó, chain trên cổng hiện tại sẽ bị thay thế :
Vậy là kết thúc - bạn có thể bày trò nghịch bằng cách rủ một vài bạn bè cùng nghịch thử Blockchain của chính mình
Tôi hi vọng rằng bài viết trên sẽ có thể tạo chút hứng thú cho các bạn. Tôi đã cực kì phấn khích với Cryptocurrencies vì tôi tin rằng Blockchains sẽ nhanh chóng thay đổi cách chúng ta nghĩ về mọi mặt của đời sống từ kinh tế, chính trị ... cho tới những vấn đề nhỏ hơn và gần gũi hơn.
Tài liệu
Source dịch (+ mã nguồn in Python): https://hackernoon.com/learn-blockchains-by-building-one-117428612f46
All rights reserved