Membuat Cryptocurrency dalam Bahasa Pemrograman Crystal
Diterbitkan: 2022-03-11Posting ini adalah upaya saya untuk memahami aspek-aspek kunci dari blockchain dengan menjelajahi internal. Saya mulai dengan membaca whitepaper bitcoin asli, tetapi saya merasa satu-satunya cara untuk benar-benar memahami blockchain adalah dengan membangun cryptocurrency baru dari awal.
Itu sebabnya saya memutuskan untuk membuat cryptocurrency menggunakan bahasa pemrograman Crystal baru, dan saya menjulukinya CrystalCoin . Artikel ini tidak akan membahas pilihan algoritme, kesulitan hash, atau topik serupa. Alih-alih, fokusnya adalah pada merinci contoh konkret, yang seharusnya memberikan pemahaman langsung yang lebih dalam tentang kekuatan dan keterbatasan blockchain.
Jika Anda belum membacanya, untuk latar belakang lebih lanjut tentang algos dan hashing, saya sarankan Anda melihat artikel Demir Selmanovic Cryptocurrency for Dummies: Bitcoin and Beyond.
Mengapa Saya Memilih Bahasa Pemrograman Crystal
Untuk demonstrasi yang lebih baik, saya ingin menggunakan bahasa yang produktif seperti Ruby tanpa mengurangi kinerjanya. Cryptocurrency memiliki banyak perhitungan yang memakan waktu (yaitu menambang dan hashing ), dan itulah mengapa bahasa yang dikompilasi seperti C++ dan Java adalah bahasa pilihan untuk membangun cryptocurrency "nyata". Karena itu, saya ingin menggunakan bahasa dengan sintaks yang lebih bersih sehingga saya dapat membuat pengembangan tetap menyenangkan dan memungkinkan keterbacaan yang lebih baik. Kinerja kristal cenderung baik pula.
Jadi, mengapa saya memutuskan untuk menggunakan bahasa pemrograman Crystal? Sintaks Crystal sangat terinspirasi oleh Ruby, jadi bagi saya, terasa alami untuk dibaca dan mudah ditulis. Ini memiliki manfaat tambahan dari kurva belajar yang lebih rendah, terutama untuk pengembang Ruby yang berpengalaman.
Ini adalah bagaimana tim Crystal lang menempatkannya di situs resmi mereka:
Secepat C, licin seperti Ruby.
Namun, tidak seperti Ruby atau JavaScript, yang merupakan bahasa yang ditafsirkan, Crystal adalah bahasa yang dikompilasi, membuatnya lebih cepat dan menawarkan jejak memori yang lebih rendah. Di bawah tenda, ia menggunakan LLVM untuk mengkompilasi ke kode asli.
Crystal juga diketik secara statis, yang berarti kompiler akan membantu Anda menangkap kesalahan ketik pada waktu kompilasi.
Saya tidak akan menjelaskan mengapa saya menganggap bahasa Crystal mengagumkan karena itu di luar cakupan artikel ini, tetapi jika Anda tidak menganggap optimisme saya meyakinkan, silakan lihat artikel ini untuk gambaran yang lebih baik tentang potensi Crystal.
Catatan: Artikel ini mengasumsikan Anda sudah memiliki pemahaman dasar tentang Pemrograman Berorientasi Objek (OOP).
Blockchain
Jadi, apa itu blockchain? Ini adalah daftar (rantai) blok yang terhubung dan diamankan oleh sidik jari digital (juga dikenal sebagai hash kripto).
Cara termudah untuk memikirkannya adalah sebagai struktur data daftar tertaut. Karena itu, daftar tertaut hanya perlu memiliki referensi ke elemen sebelumnya; sebuah blok harus memiliki pengidentifikasi tergantung pada pengidentifikasi blok sebelumnya, artinya Anda tidak dapat mengganti blok tanpa menghitung ulang setiap blok yang muncul setelahnya.
Untuk saat ini, pikirkan blockchain sebagai serangkaian blok dengan beberapa data yang terhubung dengan rantai, rantai menjadi hash dari blok sebelumnya.
Seluruh blockchain akan ada di setiap node yang ingin berinteraksi dengannya, artinya disalin pada setiap node dalam jaringan. Tidak ada satu server pun yang menghostingnya, namun semua perusahaan pengembangan blockchain menggunakannya, yang membuatnya terdesentralisasi .
Ya, ini aneh dibandingkan dengan sistem terpusat konvensional. Setiap node akan memiliki salinan seluruh blockchain (> 149 Gb dalam blockchain Bitcoin pada Desember 2017).
Hashing dan Tanda Tangan Digital
Jadi, apa fungsi hash ini? Pikirkan hash sebagai fungsi, yang mengembalikan sidik jari unik saat kita memberinya teks/objek. Bahkan perubahan terkecil pada objek input akan mengubah sidik jari secara dramatis.
Ada algoritma hashing yang berbeda, dan dalam artikel ini, kita akan menggunakan algoritma hash SHA256
, yang digunakan di Bitcoin
.
Menggunakan SHA256
kami akan selalu menghasilkan 64 karakter heksadesimal (256 bit) panjangnya bahkan jika inputnya kurang dari 256 bit atau jauh lebih besar dari 256 bit:
Memasukkan | Hasil Hash |
---|---|
TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEX SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG TEKS SANGAT PANJANG | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
Toptal | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
paling atas. | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
Perhatikan dengan contoh terakhir, bahwa hanya menambahkan file .
(dot) menghasilkan perubahan dramatis pada hash.
Oleh karena itu, dalam blockchain, rantai dibangun dengan melewatkan data blok ke dalam algoritma hashing yang akan menghasilkan hash, yang terhubung ke blok berikutnya, untuk selanjutnya, membentuk serangkaian blok yang terkait dengan hash dari blok sebelumnya.
Membangun Cryptocurreny di Crystal
Sekarang mari kita mulai membuat proyek Crystal dan membangun enkripsi SHA256
kita.
Dengan asumsi Anda telah menginstal bahasa pemrograman Crystal, mari buat kerangka basis kode CrystalCoin
dengan menggunakan crystal init app [name]
:
% crystal init app crystal_coin create crystal_coin/.gitignore create crystal_coin/.editorconfig create crystal_coin/LICENSE create crystal_coin/README.md create crystal_coin/.travis.yml create crystal_coin/shard.yml create crystal_coin/src/crystal_coin.cr create crystal_coin/src/crystal_coin/version.cr create crystal_coin/spec/spec_helper.cr create crystal_coin/spec/crystal_coin_spec.cr Initialized empty Git repository in /Users/eki/code/crystal_coin/.git/
Perintah ini akan membuat struktur dasar untuk proyek, dengan repositori git, lisensi, dan file readme yang sudah diinisialisasi. Itu juga dilengkapi dengan rintisan untuk pengujian, dan file shard.yml
untuk menjelaskan proyek dan mengelola dependensi, juga dikenal sebagai pecahan.
Mari tambahkan pecahan openssl
, yang diperlukan untuk membangun algoritma SHA256
:
# shard.yml dependencies: openssl: github: datanoise/openssl.cr
Setelah masuk, kembali ke terminal Anda dan jalankan crystal deps
. Melakukan ini akan menurunkan openssl
dan dependensinya untuk kita gunakan.
Sekarang kita telah menginstal library yang diperlukan dalam kode kita, mari kita mulai dengan mendefinisikan kelas Block
dan kemudian membangun fungsi hash.
# src/crystal_coin/block.cr require "openssl" module CrystalCoin class Block def initialize(data : String) @data = data end def hash hash = OpenSSL::Digest.new("SHA256") hash.update(@data) hash.hexdigest end end end puts CrystalCoin::Block.new("Hello, Cryptos!").hash
Anda sekarang dapat menguji aplikasi Anda dengan menjalankan crystal run crystal src/crystal_coin/block.cr
dari terminal Anda.
crystal_coin [master●] % crystal src/crystal_coin/block.cr 33eedea60b0662c66c289ceba71863a864cf84b00e10002ca1069bf58f9362d5
Merancang Blockchain kami
Setiap blok disimpan dengan timestamp
dan, secara opsional, index
. Di CrystalCoin
, kita akan menyimpan keduanya. Untuk membantu memastikan integritas di seluruh blockchain, setiap blok akan memiliki hash pengenal diri . Seperti Bitcoin, hash setiap blok akan menjadi hash kriptografis dari blok tersebut ( index
, timestamp
, data
, dan hash dari hash sebelumnya_hash blok previous_hash
). Data bisa apa saja yang Anda inginkan untuk saat ini.
module CrystalCoin class Block property current_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @current_hash = hash_block end private def hash_block hash = OpenSSL::Digest.new("SHA256") hash.update("#{@index}#{@timestamp}#{@data}#{@previous_hash}") hash.hexdigest end end end puts CrystalCoin::Block.new(data: "Same Data").current_hash
Di Crystal lang, kami mengganti metode attr_accessor
, attr_getter
dan attr_setter
dengan kata kunci baru:
Kata Kunci Ruby | Kata Kunci Kristal |
---|---|
attr_accessor | Properti |
attr_reader | pengambil |
attr_penulis | penyetel |
Hal lain yang mungkin Anda perhatikan di Crystal adalah bahwa kami ingin memberi petunjuk kepada kompiler tentang tipe tertentu melalui kode kami. Crystal menyimpulkan tipenya, tetapi setiap kali Anda memiliki ambiguitas, Anda juga dapat mendeklarasikan tipe secara eksplisit. Itu sebabnya kami menambahkan tipe String
untuk current_hash
.
Sekarang mari kita jalankan block.cr
dua kali dan perhatikan bahwa data yang sama akan menghasilkan hash yang berbeda karena timestamp
yang berbeda :
crystal_coin [master●] % crystal src/crystal_coin/block.cr 361d0df74e28d37b71f6c5f579ee182dd3d41f73f174dc88c9f2536172d3bb66 crystal_coin [master●] % crystal src/crystal_coin/block.cr b1fafd81ba13fc21598fb083d9429d1b8a7e9a7120dbdacc7e461791b96b9bf3
Sekarang kami memiliki struktur blok kami, tetapi kami sedang membuat blockchain. Kita perlu mulai menambahkan blok untuk membentuk rantai yang sebenarnya. Seperti yang saya sebutkan sebelumnya, setiap blok membutuhkan informasi dari blok sebelumnya. Tapi bagaimana blok pertama di blockchain sampai di sana? Nah, blok pertama, atau blok genesis
, adalah blok khusus (blok tanpa pendahulu). Dalam banyak kasus, ini ditambahkan secara manual atau memiliki logika unik yang memungkinkannya untuk ditambahkan.
Kami akan membuat fungsi baru yang mengembalikan blok genesis. Blok ini index=0
, dan memiliki nilai data arbitrer dan nilai arbitrer dalam parameter previous_hash
.
Mari kita buat atau kelas metode Block.first
yang menghasilkan blok genesis:
module CrystalCoin class Block ... def self.first(data="Genesis Block") Block.new(data: data, previous_hash: "0") end ... end end
Dan mari kita uji menggunakan p CrystalCoin::Block.first
:
#<CrystalCoin::Block:0x10b33ac80 @current_hash="acb701a9b70cff5a0617d654e6b8a7155a8c712910d34df692db92455964d54e", @data="Genesis Block", @index=0, @timestamp=2018-05-13 17:54:02 +03:00, @previous_hash="0">
Sekarang kita dapat membuat blok genesis , kita membutuhkan fungsi yang akan menghasilkan blok berikutnya di blockchain.
Fungsi ini akan mengambil blok sebelumnya dalam rantai sebagai parameter, membuat data untuk blok yang akan dihasilkan, dan mengembalikan blok baru dengan data yang sesuai. Ketika blok baru meng-hash informasi dari blok sebelumnya, integritas blockchain meningkat dengan setiap blok baru.
Konsekuensi penting adalah bahwa sebuah blok tidak dapat dimodifikasi tanpa mengubah hash dari setiap blok yang berurutan. Ini ditunjukkan dalam contoh di bawah ini. Jika data di blok 44 diubah dari LOOP
ke EAST
, semua hash dari blok yang berurutan harus diubah. Ini karena hash blok bergantung pada nilai previous_hash
(antara lain).
Jika kami tidak melakukan ini, akan lebih mudah bagi pihak luar untuk mengubah data dan mengganti rantai kami dengan yang sama sekali baru milik mereka. Rantai hash ini bertindak sebagai bukti kriptografi dan membantu memastikan bahwa begitu sebuah blok ditambahkan ke blockchain, blok itu tidak dapat diganti atau dihapus. Mari kita buat metode kelas Block.next
:
module CrystalCoin class Block ... def self.next(previous_node, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_node.index + 1})", index: previous_node.index + 1, previous_hash: previous_hash.hash ) end ... end end
Untuk mencobanya bersama-sama, kami akan membuat blockchain sederhana. Elemen pertama dari daftar adalah blok genesis. Dan tentu saja, kita perlu menambahkan blok berikutnya. Kami akan membuat lima blok baru untuk mendemonstrasikan CrystalCoin
:
blockchain = [ CrystalCoin::Block.first ] previous_block = blockchain[0] 5.times do new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block end p blockchain
[#<CrystalCoin::Block:0x108c57c80 @current_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @index=0, @previous_hash="0", @timestamp=2018-06-04 12:13:21 +03:00, @data="Genesis Block>, #<CrystalCoin::Block:0x109c89740 @current_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @index=1, @previous_hash= "df7f9d47bee95c9158e3043ddd17204e97ccd6e8958e4e41dacc7f0c6c0df610", @timestamp=2018-06-04 12:13:21 +03:00, @data="Transaction data number (1)">, #<CrystalCoin::Block:0x109cc8500 @current_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @index=2, @previous_hash= "d188fcddd056c044c783d558fd6904ceeb9b2366339af491a293d369de4a81f6", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (2)">, #<CrystalCoin::Block:0x109ccbe40 @current_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @index=3, @previous_hash= "0b71b61118b9300b4fe8cdf4a7cbcc1dac4da7a8a3150aa97630994805354107", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (3)">, #<CrystalCoin::Block:0x109cd0980 @current_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @index=4, @previous_hash= "9111406deea4f07f807891405078a3f8537416b31ab03d78bda3f86d9ae4c584", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (4)">, #<CrystalCoin::Block:0x109cd0100 @current_hash= "00a0c70e5412edd7389a0360b48c407ce4ddc8f14a0bcf16df277daf3c1a00c7", @index=5, @previous_hash= "0063bfc5695c0d49b291a8813c566b047323f1808a428e0eb1fca5c399547875", @timestamp=2018-06-04 12:13:21 +03:00, @transactions="Transaction data number (5)">
Bukti Kerja
Algoritma Proof of Work (PoW) adalah bagaimana blok baru dibuat atau ditambang di blockchain. Tujuan dari PoW adalah untuk menemukan nomor yang memecahkan masalah. Nomor tersebut pasti sulit ditemukan tetapi mudah diverifikasi secara komputasi oleh siapa pun di jaringan. Ini adalah ide inti di balik Proof of Work.
Mari kita tunjukkan dengan contoh untuk memastikan semuanya jelas. Kami akan berasumsi bahwa hash dari beberapa bilangan bulat x dikalikan dengan y lain harus dimulai dengan 00
. Jadi:
hash(x * y) = 00ac23dc...
Dan untuk contoh sederhana ini, mari perbaiki x=5
dan terapkan ini di Crystal:
x = 5 y = 0 while hash((x*y).to_s)[0..1] != "00" y += 1 end puts "The solution is y = #{y}" puts "Hash(#{x}*#{y}) = #{hash((x*y).to_s)}"
Mari kita jalankan kodenya:
crystal_coin [master●●] % time crystal src/crystal_coin/pow.cr The solution is y = 530 Hash(5*530) = 00150bc11aeeaa3cdbdc1e27085b0f6c584c27e05f255e303898dcd12426f110 crystal src/crystal_coin/pow.cr 1.53s user 0.23s system 160% cpu 1.092 total
Seperti yang Anda lihat, nomor ini y=530
sulit ditemukan (brute-force) tetapi mudah diverifikasi menggunakan fungsi hash.
Mengapa repot-repot dengan algoritma PoW ini? Mengapa kita tidak membuat satu hash per blok dan hanya itu? Sebuah hash harus valid . Dalam kasus kami, hash akan valid jika dua karakter pertama dari hash kami adalah 00
. Jika hash kami dimulai dengan 00......
, itu dianggap valid. Ini disebut kesulitan . Semakin tinggi tingkat kesulitannya, semakin lama waktu yang dibutuhkan untuk mendapatkan hash yang valid.
Tapi, jika hash pertama kali tidak valid, ada sesuatu yang harus diubah pada data yang kita gunakan. Jika kita menggunakan data yang sama berulang-ulang, kita akan mendapatkan hash yang sama berulang-ulang dan hash kita tidak akan pernah valid. Kami menggunakan sesuatu yang disebut nonce
di hash kami (dalam contoh kami sebelumnya adalah y
). Ini hanyalah angka yang kami tambahkan setiap kali hash tidak valid. Kami mendapatkan data kami (tanggal, pesan, hash sebelumnya, indeks) dan nonce 1. Jika hash yang kami dapatkan dengan ini tidak valid, kami mencoba dengan nonce 2. Dan kami menambahkan nonce sampai kami mendapatkan hash yang valid .
Di Bitcoin, algoritma Proof of Work disebut Hashcash. Mari tambahkan proof-of-work ke kelas Block kita dan mulai menambang untuk menemukan nonce. Kami akan menggunakan kesulitan hard-code dari dua angka nol di depan '00':
Sekarang mari kita rancang ulang kelas Blok kita untuk mendukungnya. Blok CrystalCoin
kami akan berisi atribut berikut:
1) index: indicates the index of the block ex: 0,1 2) timestamp: timestamp in epoch, number of seconds since 1 Jan 1970 3) data: the actual data that needs to be stored on the blockchain. 4) previous_hash: the hash of the previous block, this is the chain/link between the blocks 5) nonce: this is the number that is to be mined/found. 6) current_hash: The hash value of the current block, this is generated by combining all the above attributes and passing it to a hashing algorithm
Saya akan membuat modul terpisah untuk melakukan hashing dan menemukan nonce
sehingga kami menjaga kode kami tetap bersih dan modular. Saya akan menyebutnya proof_of_work.cr
:
require "openssl" module CrystalCoin module ProofOfWork private def proof_of_work(difficulty = "00") nonce = 0 loop do hash = calc_hash_with_nonce(nonce) if hash[0..1] == difficulty return nonce else nonce += 1 end end end private def calc_hash_with_nonce(nonce = 0) sha = OpenSSL::Digest.new("SHA256") sha.update("#{nonce}#{@index}#{@timestamp}#{@data}#{@previous_hash}") sha.hexdigest end end end
Kelas Block
kami akan terlihat seperti:
require "./proof_of_work" module CrystalCoin class Block include ProofOfWork property current_hash : String property index : Int32 property nonce : Int32 property previous_hash : String def initialize(index = 0, data = "data", previous_hash = "hash") @data = data @index = index @timestamp = Time.now @previous_hash = previous_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end def self.first(data = "Genesis Block") Block.new(data: data, previous_hash: "0") end def self.next(previous_block, data = "Transaction Data") Block.new( data: "Transaction data number (#{previous_block.index + 1})", index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end
Beberapa hal yang perlu diperhatikan tentang kode Crystal dan contoh bahasa Crystal secara umum. Di Crystal, metode bersifat publik secara default. Crystal mengharuskan setiap metode pribadi untuk diawali dengan kata kunci pribadi yang dapat membingungkan berasal dari Ruby.
Anda mungkin telah memperhatikan bahwa tipe Integer Crystal ada Int8
, Int16
, Int32
, Int64
, UInt8
, UInt16
, UInt32
, atau UInt64
dibandingkan dengan Ruby's Fixnum
. true
dan false
adalah nilai di kelas Bool
daripada nilai di kelas TrueClass
atau FalseClass
di Ruby.
Crystal memiliki argumen metode opsional dan bernama sebagai fitur bahasa inti, dan tidak memerlukan penulisan kode khusus untuk menangani argumen yang cukup keren. Periksa Block#initialize(index = 0, data = "data", previous_hash = "hash")
dan kemudian panggil dengan sesuatu seperti Block.new(data: data, previous_hash: "0")
.
Untuk daftar perbedaan yang lebih rinci antara bahasa pemrograman Crystal dan Ruby, lihat Crystal for Rubyists.
Sekarang, mari kita coba membuat lima transaksi menggunakan:
blockchain = [ CrystalCoin::Block.first ] puts blockchain.inspect previous_block = blockchain[0] 5.times do |i| new_block = CrystalCoin::Block.next(previous_block: previous_block) blockchain << new_block previous_block = new_block puts new_block.inspect end
[#<CrystalCoin::Block:0x108f8fea0 @current_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759", @index=0, @nonce=17, @timestamp=2018-05-14 17:20:46 +03:00, @data="Genesis Block", @previous_hash="0">] #<CrystalCoin::Block:0x108f8f660 @current_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0", @index=1, @nonce=24, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (1)", @previous_hash="0088ca080a49334e1cb037ed4c42795d635515ef1742e6bcf439bf0f95711759"> #<CrystalCoin::Block:0x109fc5ba0 @current_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5", @index=2, @nonce=61, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (2)", @previous_hash="001bc2b04d7ad8ef25ada30e2bde19d7bbbbb3ad42348017036b0d4974d0ccb0"> #<CrystalCoin::Block:0x109fdc300 @current_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97", @index=3, @nonce=149, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (3)", @previous_hash="0019256c998028111838b872a437cd8adced53f5e0f8f43388a1dc4654844fe5"> #<CrystalCoin::Block:0x109ff58a0 @current_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484", @index=4, @nonce=570, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (4)", @previous_hash="0080a30d0da33426a1d4f36d870d9eb709eaefb0fca62cc68e497169c5368b97"> #<CrystalCoin::Block:0x109fde120 @current_hash="00720bb6e562a25c19ecd2b277925057626edab8981ff08eb13773f9bb1cb842", @index=5, @nonce=475, @timestamp=2018-05-14 17:20:46 +03:00, @data="Transaction data number (5)", @previous_hash="00074399d51c700940e556673580a366a37dec16671430141f6013f04283a484">
Lihat perbedaannya? Sekarang semua hash dimulai dengan 00
. Itulah keajaiban proof-of-work. Dengan menggunakan ProofOfWork
kami menemukan nonce
dan proof adalah hash dengan kesulitan pencocokan, yaitu, dua angka nol di depan 00
.
Perhatikan dengan blok pertama yang kami buat, kami mencoba 17 nonce hingga menemukan angka keberuntungan yang cocok:
Memblokir | Loop / Jumlah Perhitungan Hash |
---|---|
#0 | 17 |
#1 | 24 |
#2 | 61 |
#3 | 149 |
#4 | 570 |
#5 | 475 |
Sekarang mari kita coba tingkat kesulitan dari empat angka nol di depan ( difficulty="0000"
):

Memblokir | Loop / Jumlah Perhitungan Hash |
---|---|
#1 | 26 762 |
#2 | 68 419 |
#3 | 23 416 |
#4 | 15 353 |
Di blok pertama mencoba 26762 nonce (bandingkan 17 nonces dengan kesulitan '00') sampai menemukan nomor keberuntungan yang cocok.
Blockchain kami sebagai API
Sejauh ini bagus. Kami membuat blockchain sederhana kami dan itu relatif mudah dilakukan. Tapi masalahnya di sini adalah CrystalCoin
hanya bisa berjalan di satu mesin (tidak terdistribusi/terdesentralisasi).
Mulai sekarang, kita akan mulai menggunakan data JSON untuk CrystalCoin
. Data tersebut akan berupa transaksi, sehingga setiap field data blok akan menjadi daftar transaksi.
Setiap transaksi akan menjadi objek JSON yang merinci sender
koin, receiver
koin, dan amount
CrystalCoin yang ditransfer:
{ "from": "71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij", "to": "93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo", "amount": 3 }
Beberapa modifikasi pada kelas Block
kami untuk mendukung format transaction
baru (sebelumnya disebut data
). Jadi, untuk menghindari kebingungan dan menjaga konsistensi, mulai sekarang kita akan menggunakan istilah transaction
untuk merujuk pada data
yang diposting di aplikasi contoh kita.
Kami akan memperkenalkan Transaction
kelas sederhana baru :
module CrystalCoin class Block class Transaction property from : String property to : String property amount : Int32 def initialize(@from, @to, @amount) end end end end
Transaksi dikemas ke dalam blok, sehingga satu blok dapat berisi hanya satu atau banyak transaksi. Blok yang berisi transaksi sering dibuat dan ditambahkan ke blockchain.
Blockchain seharusnya menjadi kumpulan blok. Kami dapat menyimpan semua blok dalam daftar Crystal, dan itulah mengapa kami memperkenalkan Blockchain
kelas baru :
Blockchain
akan memiliki array chain
dan uncommitted_transactions
. chain
akan mencakup semua blok yang ditambang di blockchain, dan uncommitted_transactions
akan memiliki semua transaksi yang belum ditambahkan ke blockchain (masih belum ditambang). Setelah kami menginisialisasi Blockchain
, kami membuat blok genesis menggunakan Block.first
dan menambahkannya ke array chain
, dan kami menambahkan array uncommitted_transactions
kosong.
Kami akan membuat metode Blockchain#add_transaction
untuk menambahkan transaksi ke array uncommitted_transactions
.
Mari kita bangun kelas Blockchain
baru kita:
require "./block" require "./transaction" module CrystalCoin class Blockchain getter chain getter uncommitted_transactions def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction end def add_transaction(transaction) @uncommitted_transactions << transaction end end end
Di kelas Block
kami akan mulai menggunakan transactions
alih-alih data
:
module CrystalCoin class Block include ProofOfWork def initialize(index = 0, transactions = [] of Transaction, previous_hash = "hash") @transactions = transactions ... end .... def self.next(previous_block, transactions = [] of Transaction) Block.new( transactions: transactions, index: previous_block.index + 1, previous_hash: previous_block.current_hash ) end end end
Sekarang kita tahu seperti apa transaksi kita, kita memerlukan cara untuk menambahkannya ke salah satu komputer di jaringan blockchain kita, yang disebut node
. Untuk melakukannya, kita akan membuat server HTTP sederhana.
Kami akan membuat empat titik akhir:
- [POST]
/transactions/new
: untuk membuat transaksi baru ke blok - [GET]
/mine
: untuk memberi tahu server kami untuk menambang blok baru. - [GET]
/chain
: untuk mengembalikan blockchain lengkap dalam formatJSON
. - [GET]
/pending
: untuk mengembalikan transaksi yang tertunda (uncommitted_transactions
).
Kami akan menggunakan kerangka web Kemal. Ini adalah kerangka kerja mikro yang memudahkan pemetaan titik akhir ke fungsi Crystal. Kemal sangat dipengaruhi oleh Sinatra untuk Rubyist dan bekerja dengan cara yang sangat mirip. Jika Anda mencari Ruby on Rails yang setara, lihat Amber.
Server kami akan membentuk satu node di jaringan blockchain kami. Pertama-tama, tambahkan Kemal
ke file shard.yml
sebagai a dan instal ketergantungannya:
dependencies: kemal: github: kemalcr/kemal
Sekarang mari kita buat kerangka server HTTP kita:
# src/server.cr require "kemal" require "./crystal_coin" # Generate a globally unique address for this node node_identifier = UUID.random.to_s # Create our Blockchain blockchain = Blockchain.new get "/chain" do "Send the blockchain as json objects" end get "/mine" do "We'll mine a new Block" end get "/pending" do "Send pending transactions as json objects" end post "/transactions/new" do "We'll add a new transaction" end Kemal.run
Dan jalankan server:
crystal_coin [master●●] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Mari kita pastikan server berfungsi dengan baik:
% curl http://0.0.0.0:3000/chain Send the blockchain as json objects%
Sejauh ini bagus. Sekarang, kita dapat melanjutkan dengan mengimplementasikan setiap titik akhir. Mari kita mulai dengan menerapkan /transactions/new
dan titik akhir yang pending
:
get "/pending" do { transactions: blockchain.uncommitted_transactions }.to_json end post "/transactions/new" do |env| transaction = CrystalCoin::Block::Transaction.new( from: env.params.json["from"].as(String), to: env.params.json["to"].as(String), amount: env.params.json["amount"].as(Int64) ) blockchain.add_transaction(transaction) "Transaction #{transaction} has been added to the node" end
Implementasi langsung. Kami baru saja membuat objek CrystalCoin::Block::Transaction
dan menambahkan transaksi ke array uncommitted_transactions
menggunakan Blockchain#add_transaction
.
Saat ini, transaksi awalnya disimpan dalam kumpulan uncommitted_transactions
. Proses memasukkan transaksi yang belum dikonfirmasi ke dalam blok dan menghitung Proof of Work (PoW) dikenal sebagai penambangan blok. Setelah nonce
yang memenuhi batasan kami diketahui, kami dapat mengatakan bahwa sebuah blok telah ditambang, dan blok baru dimasukkan ke dalam blockchain.
Di CrystalCoin
, kita akan menggunakan algoritma Proof-of-Work sederhana yang kita buat sebelumnya. Untuk membuat blok baru, komputer penambang harus:
- Temukan blok terakhir dalam
chain
. - Temukan transaksi yang tertunda (
uncommitted_transactions
). - Buat blok baru menggunakan
Block.next
. - Tambahkan blok yang ditambang ke array
chain
. - Bersihkan array
uncommitted_transactions
.
Jadi untuk mengimplementasikan /mine
end-point, pertama-tama mari kita implementasikan langkah-langkah di atas di Blockchain#mine
:
module CrystalCoin class Blockchain include Consensus BLOCK_SIZE = 25 ... def mine raise "No transactions to be mined" if @uncommitted_transactions.empty? new_block = Block.next( previous_block: @chain.last, transactions: @uncommitted_transactions.shift(BLOCK_SIZE) ) @chain << new_block end end end
Kami memastikan terlebih dahulu bahwa kami memiliki beberapa transaksi yang tertunda untuk ditambang. Kemudian kami mendapatkan blok terakhir menggunakan @chain.last
, dan 25
transaksi pertama yang akan ditambang (kami menggunakan Array#shift(BLOCK_SIZE)
untuk mengembalikan array dari 25 uncommitted_transactions
pertama, dan kemudian menghapus elemen mulai dari indeks 0) .
Sekarang mari kita implementasikan /mine
end-point:
get "/mine" do blockchain.mine "Block with index=#{blockchain.chain.last.index} is mined." end
Dan untuk titik akhir /chain
:
get "/chain" do { chain: blockchain.chain }.to_json end
Berinteraksi Dengan Blockchain kami
Kami akan menggunakan cURL
untuk berinteraksi dengan API kami melalui jaringan.
Pertama, mari kita jalankan servernya:
crystal_coin [master] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Kemudian mari kita buat dua transaksi baru dengan membuat permintaan POST
ke http://localhost:3000/transactions/new
dengan isi yang berisi struktur transaksi kita:
crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"iron_man", "amount": 1000}' Transaction #<CrystalCoin::Block::Transaction:0x10c4159f0 @from="eki", @to="iron_man", @amount=1000_i64> has been added to the node% crystal_coin [master●] % curl -X POST http://0.0.0.0:3000/transactions/new -H "Content-Type: application/json" -d '{"from": "eki", "to":"hulk", "amount": 700}' Transaction #<CrystalCoin::Block::Transaction:0x10c415810 @from="eki", @to="hulk", @amount=700_i64> has been added to the node%
Sekarang mari kita daftar transaksi yang tertunda (yaitu transaksi yang belum ditambahkan ke blok):
crystal_coin [master●] % curl http://0.0.0.0:3000/pendings { "transactions":[ { "from":"ekis", "to":"huslks", "amount":7090 }, { "from":"ekis", "to":"huslks", "amount":70900 } ] }
Seperti yang bisa kita lihat, dua transaksi yang kita buat sebelumnya telah ditambahkan ke uncommitted_transactions
.
Sekarang mari menambang dua transaksi dengan membuat permintaan GET
ke http://0.0.0.0:3000/mine
:
crystal_coin [master●] % curl http://0.0.0.0:3000/mine Block with index=1 is mined.
Sepertinya kami berhasil menambang blok pertama dan menambahkannya ke chain
kami. Mari kita periksa kembali chain
kita dan apakah itu termasuk blok yang ditambang:
crystal_coin [master●] % curl http://0.0.0.0:3000/chain { "chain": [ { "index": 0, "current_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "nonce": 363, "previous_hash": "0", "transactions": [ ], "timestamp": "2018-05-23T01:59:52+0300" }, { "index": 1, "current_hash": "003c05da32d3672670ba1e25ecb067b5bc407e1d5f8733b5e33d1039de1a9bf1", "nonce": 320, "previous_hash": "00d469d383005b4303cfa7321c02478ce76182564af5d16e1a10d87e31e2cb30", "transactions": [ { "from": "ekis", "to": "huslks", "amount": 7090 }, { "from": "ekis", "to": "huslks", "amount": 70900 } ], "timestamp": "2018-05-23T02:02:38+0300" } ] }
Konsensus dan Desentralisasi
Ini keren. Kami mendapatkan blockchain dasar yang menerima transaksi dan memungkinkan kami untuk menambang blok baru. Tetapi kode yang telah kami terapkan sampai sekarang dimaksudkan untuk berjalan di satu komputer, sedangkan inti dari blockchain adalah bahwa mereka harus didesentralisasi. Tetapi jika mereka terdesentralisasi, bagaimana kami memastikan bahwa mereka semua mencerminkan rantai yang sama?
Ini adalah masalah Consensus
.
Kita harus menerapkan algoritma konsensus jika kita menginginkan lebih dari satu node di jaringan kita.
Mendaftarkan Node Baru
Untuk mengimplementasikan algoritma konsensus, kita membutuhkan cara untuk memberi tahu sebuah node tentang node tetangga di jaringan. Setiap node di jaringan kami harus menyimpan registri dari node lain di jaringan. Oleh karena itu, kita akan membutuhkan lebih banyak titik akhir:
- [POST]
/nodes/register
: untuk menerima daftar node baru dalam bentuk URL. - [GET]
/nodes/resolve
: untuk mengimplementasikan Algoritma Konsensus kami, yang menyelesaikan konflik apa pun—untuk memastikan sebuah node memiliki rantai yang benar.
Kami perlu memodifikasi konstruktor blockchain kami dan menyediakan metode untuk mendaftarkan node:
--- a/src/crystal_coin/blockchain.cr +++ b/src/crystal_coin/blockchain.cr @@ -7,10 +7,12 @@ module CrystalCoin getter chain getter uncommitted_transactions + getter nodes def initialize @chain = [ Block.first ] @uncommitted_transactions = [] of Block::Transaction + @nodes = Set(String).new [] of String end def add_transaction(transaction)
Perhatikan bahwa kami telah menggunakan struktur data Set
dengan tipe String
untuk menampung daftar node. Ini adalah cara murah untuk memastikan bahwa penambahan node baru bersifat idempoten dan tidak peduli berapa kali kita menambahkan node tertentu, node tersebut muncul tepat satu kali.
Sekarang mari tambahkan modul baru ke Consensus
dan implementasikan metode pertama register_node(address)
:
require "uri" module CrystalCoin module Consensus def register_node(address : String) uri = URI.parse(address) node_address = "#{uri.scheme}:://#{uri.host}" node_address = "#{node_address}:#{uri.port}" unless uri.port.nil? @nodes.add(node_address) rescue raise "Invalid URL" end end end
Fungsi register_node
, akan mengurai URL node dan memformatnya.
Dan di sini mari kita buat /nodes/register
titik akhir:
post "/nodes/register" do |env| nodes = env.params.json["nodes"].as(Array) raise "Empty array" if nodes.empty? nodes.each do |node| blockchain.register_node(node.to_s) end "New nodes have been added: #{blockchain.nodes}" end
Sekarang dengan implementasi ini, kita mungkin menghadapi masalah dengan banyak node. Salinan rantai dari beberapa node dapat berbeda. Dalam hal ini, kita perlu menyepakati beberapa versi rantai untuk menjaga integritas seluruh sistem. Kita perlu mencapai konsensus.
Untuk mengatasi ini, kami akan membuat aturan bahwa rantai valid terpanjang adalah yang akan digunakan. Dengan menggunakan algoritme ini, kami mencapai konsensus di antara node di jaringan kami. Alasan di balik pendekatan ini adalah bahwa rantai terpanjang adalah perkiraan yang baik dari jumlah pekerjaan yang paling banyak dilakukan.
module CrystalCoin module Consensus ... def resolve updated = false @nodes.each do |node| node_chain = parse_chain(node) return unless node_chain.size > @chain.size return unless valid_chain?(node_chain) @chain = node_chain updated = true rescue IO::Timeout puts "Timeout!" end updated end ... end end
Ingatlah bahwa resolve
adalah metode yang mengulang semua node tetangga kita, mengunduh rantainya, dan memverifikasinya menggunakan valid_chain?
metode. If a valid chain is found, whose length is greater than ours, we replace ours.
Now let's implement parse_chain()
and valid_chain?()
private methods:
module CrystalCoin module Consensus ... private def parse_chain(node : String) node_url = URI.parse("#{node}/chain") node_chain = HTTP::Client.get(node_url) node_chain = JSON.parse(node_chain.body)["chain"].to_json Array(CrystalCoin::Block).from_json(node_chain) end private def valid_chain?(node_chain) previous_hash = "0" node_chain.each do |block| current_block_hash = block.current_hash block.recalculate_hash return false if current_block_hash != block.current_hash return false if previous_hash != block.previous_hash return false if current_block_hash[0..1] != "00" previous_hash = block.current_hash end return true end end end
For parse_chain()
we:
- Issue a
GET
HTTP request usingHTTP::Client.get
to/chain
end-point. - Parse the
/chain
JSON response usingJSON.parse
. - Extract an array of
CrystalCoin::Block
objects from the JSON blob that was returned usingArray(CrystalCoin::Block).from_json(node_chain)
.
There is more than one way of parsing JSON in Crystal. The preferred method is to use Crystal's super-handy JSON.mapping(key_name: Type)
functionality that gives us the following:
- A way to create an instance of that class from a JSON string by running
Class.from_json
. - A way to serialize an instance of that class into a JSON string by running
instance.to_json
. - Getters and setters for keys defined in that class.
In our case, we had to define JSON.mapping
in CrystalCoin::Block
object, and we removed property
usage in the class, like so:
module CrystalCoin class Block JSON.mapping( index: Int32, current_hash: String, nonce: Int32, previous_hash: String, transactions: Array(Transaction), timestamp: Time ) ... end end
Now for Blockchain#valid_chain?
, we iterate through all of the blocks, and for each we:
- Recalculate the hash for the block using
Block#recalculate_hash
and check that the hash of the block is correct:
module CrystalCoin class Block ... def recalculate_hash @nonce = proof_of_work @current_hash = calc_hash_with_nonce(@nonce) end end end
- Check each of the blocks linked correctly with their previous hashes.
- Check the block's hash is valid for the number of zeros (
difficulty
in our case00
).
And finally we implement /nodes/resolve
end-point:
get "/nodes/resolve" do if blockchain.resolve "Successfully updated the chain" else "Current chain is up-to-date" end end
It's done! You can find the final code on GitHub.
The structure of our project should look like this:
crystal_coin [master●] % tree src/ src/ ├── crystal_coin │ ├── block.cr │ ├── blockchain.cr │ ├── consensus.cr │ ├── proof_of_work.cr │ ├── transaction.cr │ └── version.cr ├── crystal_coin.cr └── server.cr
Let's Try it Out
- Grab a different machine, and run different nodes on your network. Or spin up processes using different ports on the same machine. In my case, I created two nodes on my machine, on a different port to have two nodes:
http://localhost:3000
andhttp://localhost:3001
. - Register the second node address to the first node using:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3000/nodes/register -H "Content-Type: application/json" -d '{"nodes": ["http://0.0.0.0:3001"]}' New nodes have been added: Set{"http://0.0.0.0:3001"}%
- Let's add a transaction to the second node:
crystal_coin [master●●] % curl -X POST http://0.0.0.0:3001/transactions/new -H "Content-Type: application/json" -d '{"from": "eqbal", "to":"spiderman", "amount": 100}' Transaction #<CrystalCoin::Block::Transaction:0x1039c29c0> has been added to the node%
- Let's mine transactions into a block at the second node:
crystal_coin [master●●] % curl http://0.0.0.0:3001/mine Block with index=1 is mined.%
- At this point, the first node has only one block (genesis block), and the second node has two nodes (genesis and the mined block):
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"00fe9b1014901e3a00f6d8adc6e9d9c1df03344dda84adaeddc8a1c2287fb062","nonce":157,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:45+0300"}]}%
crystal_coin [master●●] % curl http://0.0.0.0:3001/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
- Our goal is to update the chain in the first node to include the newly generated block at the second one. So let's resolve the first node:
crystal_coin [master●●] % curl http://0.0.0.0:3000/nodes/resolve Successfully updated the chain%
Let's check if the chain in the first node has updated:
crystal_coin [master●●] % curl http://0.0.0.0:3000/chain {"chain":[{"index":0,"current_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","nonce":147,"previous_hash":"0","transactions":[],"timestamp":"2018-05-24T00:21:38+0300"},{"index":1,"current_hash":"00441a4d9a4dfbab0b07acd4c7639e53686944953fa3a6c64d2333a008627f7d","nonce":92,"previous_hash":"007635d82950bc4b994a91f8b0b20afb73a3939e660097c9ea8416ad614faf8e","transactions":[{"from":"eqbal","to":"spiderman","amount":100}],"timestamp":"2018-05-24T00:23:57+0300"}]}%
Hore! Our Crystal language example works like a charm, and I hope you found this lengthy tutorial crystal clear, pardon the pun.
Membungkus
This Crystal language tutorial covered the fundamentals of a public blockchain. If you followed along, you implemented a blockchain from scratch and built a simple application allowing users to share information on the blockchain.
We've made a fairly sized blockchain at this point. Now, CrystalCoin
can be launched on multiple machines to create a network, and real CrystalCoins
can be mined.
I hope that this has inspired you to create something new, or at the very least take a closer look at Crystal programming.
Note: The code in this tutorial is not ready for real-world use. Please refer to this as a general guide only.