Membuat Cryptocurrency dalam Bahasa Pemrograman Crystal

Diterbitkan: 2022-03-11

Posting 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.

Ilustrasi Bahasa Pemrograman Kristal

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).

Diagram hashing mata uang kripto kristal

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 

teks alternatif gambar

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 format JSON .
  • [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.

teks alternatif gambar

 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 using HTTP::Client.get to /chain end-point.
  • Parse the /chain JSON response using JSON.parse .
  • Extract an array of CrystalCoin::Block objects from the JSON blob that was returned using Array(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 case 00 ).

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 and http://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"}]}% 

teks alternatif gambar

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.