Crystal Programlama Dilinde Kripto Para Birimi Oluşturma
Yayınlanan: 2022-03-11Bu gönderi, içselleri keşfederek blok zincirinin temel yönlerini anlama girişimimdir. Orijinal bitcoin teknik incelemesini okuyarak başladım, ancak blok zincirini gerçekten anlamanın tek yolunun sıfırdan yeni bir kripto para birimi oluşturmak olduğunu hissettim.
Bu yüzden yeni Crystal programlama dilini kullanarak bir kripto para birimi oluşturmaya karar verdim ve buna CrystalCoin adını verdim. Bu makale algoritma seçimlerini, karma zorluğu veya benzer konuları tartışmayacaktır. Bunun yerine odak, blok zincirlerin güçlü yönleri ve sınırlamaları hakkında daha derin, uygulamalı bir anlayış sağlaması gereken somut bir örneği detaylandırmak olacaktır.
Henüz okumadıysanız, algos ve hashing hakkında daha fazla bilgi için Demir Selmanovic'in Cryptocurrency for Dummies: Bitcoin and Beyond başlıklı makalesine bir göz atmanızı öneririm.
Neden Crystal Programlama Dilini Seçtim?
Daha iyi bir gösterim için performanstan ödün vermeden Ruby gibi üretken bir dil kullanmak istedim. Kripto para biriminde çok zaman alan hesaplamalar (yani madencilik ve karma ) vardır ve bu nedenle C++ ve Java gibi derlenmiş diller “gerçek” kripto para birimleri oluşturmak için tercih edilen dillerdir. Bununla birlikte, geliştirmeyi eğlenceli tutabilmek ve daha iyi okunabilirlik sağlamak için daha temiz bir sözdizimine sahip bir dil kullanmak istedim. Kristal performansı zaten iyi olma eğilimindedir.
Peki neden Crystal programlama dilini kullanmaya karar verdim? Crystal'ın sözdizimi büyük ölçüde Ruby'ninkinden esinlenmiştir, bu yüzden bana okuması doğal ve yazması kolay geliyor. Özellikle deneyimli Ruby geliştiricileri için daha düşük bir öğrenme eğrisinin ek avantajına sahiptir.
Crystal lang ekibi bunu resmi web sitesinde şöyle açıklıyor:
C kadar hızlı, Ruby kadar kaygan.
Ancak, yorumlanan diller olan Ruby veya JavaScript'ten farklı olarak Crystal, derlenmiş bir dildir, bu da onu çok daha hızlı hale getirir ve daha düşük bellek ayak izi sunar. Kaputun altında, yerel kodu derlemek için LLVM kullanır.
Crystal ayrıca statik olarak yazılmıştır; bu, derleyicinin derleme zamanında tür hatalarını yakalamanıza yardımcı olacağı anlamına gelir.
Crystal dilini neden harika bulduğumu açıklamayacağım çünkü bu makalenin kapsamı dışındadır, ancak iyimserliğimi inandırıcı bulmazsanız, Crystal'ın potansiyeline daha iyi bir genel bakış için bu makaleye göz atmaktan çekinmeyin.
Not: Bu makale, Nesne Yönelimli Programlama (OOP) hakkında temel bilgilere sahip olduğunuzu varsaymaktadır.
blok zinciri
Peki, blok zinciri nedir? Bu, dijital parmak izleriyle (kripto karmaları olarak da bilinir) birbirine bağlanmış ve güvence altına alınmış blokların bir listesidir (zinciri).
Bunu düşünmenin en kolay yolu bağlantılı liste veri yapısıdır. Bununla birlikte, bağlantılı bir listenin yalnızca bir önceki öğeye referans olması gerekir; bir bloğun, önceki bloğun tanımlayıcısına bağlı olarak bir tanımlayıcısı olmalıdır, bu, sonraki her bloğu yeniden hesaplamadan bir bloğu değiştiremeyeceğiniz anlamına gelir.
Şimdilik, blok zincirini, bir zincirle bağlantılı bazı veriler içeren bir dizi blok olarak düşünün; zincir, önceki bloğun karmasıdır.
Blok zincirinin tamamı, onunla etkileşim kurmak isteyen her düğümde bulunur, yani ağdaki düğümlerin her birine kopyalanır. Tek bir sunucu onu barındırmaz, ancak tüm blockchain geliştirme şirketleri onu kullanır, bu da onu merkezi olmayan hale getirir.
Evet, geleneksel merkezi sistemlere kıyasla bu garip. Düğümlerin her biri, tüm blok zincirinin bir kopyasına sahip olacaktır (Aralık 2017'ye kadar Bitcoin blok zincirinde > 149 Gb).
Hashing ve Dijital İmza
Peki nedir bu hash fonksiyonu? Karmayı, ona bir metin/nesne verdiğimizde benzersiz bir parmak izi döndüren bir işlev olarak düşünün. Giriş nesnesindeki en küçük değişiklik bile parmak izini önemli ölçüde değiştirir.
Farklı hash algoritmaları vardır ve bu yazımızda Bitcoin
kullanılan SHA256
hash algoritmasını kullanacağız.
SHA256
kullanarak, giriş 256 bitten küçük veya 256 bitten çok daha büyük olsa bile, her zaman 64 onaltılık karakter (256 bit) uzunluğunda sonuçlanır:
Giriş | Karma Sonuçlar |
---|---|
ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN ÇOK UZUN METİN | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
Toptal | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
Toptal. | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
Son örnekte, yalnızca bir .
(nokta) hash değerinde dramatik bir değişikliğe neden oldu.
Bu nedenle, bir blok zincirinde, zincir, blok verilerinin bir sonraki bloğa bağlı olan bir karma oluşturacak bir karma algoritmaya geçirilmesiyle oluşturulur ve bundan sonra önceki blokların karmalarıyla bağlantılı bir dizi blok oluşturur.
Crystal'de Kripto Para Birimi Oluşturmak
Şimdi Crystal projemizi oluşturmaya ve SHA256
şifrelememizi oluşturmaya başlayalım.
Crystal programlama dilinin kurulu olduğunu varsayarsak, Crystal'in yerleşik proje tooling crystal init app [name]
kullanarak CrystalCoin
kod tabanının iskeletini oluşturalım:
% 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/
Bu komut, önceden başlatılmış bir git deposu, lisans ve benioku dosyaları ile proje için temel yapıyı yaratacaktır. Ayrıca, testler için taslaklar ve projeyi tanımlamak ve bağımlılıkları yönetmek için shard.yml
dosyasıyla birlikte gelir; bunlar aynı zamanda shard olarak da bilinir.
SHA256
algoritmasını oluşturmak için gerekli olan openssl
parçasını ekleyelim:
# shard.yml dependencies: openssl: github: datanoise/openssl.cr
Bu girdikten sonra, terminalinize geri dönün ve crystal deps
çalıştırın. Bunu yapmak, kullanmamız için openssl
ve bağımlılıklarını aşağı çekecektir.
Şimdi kodumuzda gerekli kütüphaneyi kurduk, Block
sınıfını tanımlayarak ve ardından hash fonksiyonunu oluşturarak başlayalım.
# 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
Artık terminalinizden kristal çalıştırma crystal src/crystal_coin/block.cr
çalıştırarak uygulamanızı test edebilirsiniz.
crystal_coin [master●] % crystal src/crystal_coin/block.cr 33eedea60b0662c66c289ceba71863a864cf84b00e10002ca1069bf58f9362d5
Blockchain'imizi Tasarlamak
Her blok bir timestamp
ve isteğe bağlı olarak bir index
ile saklanır. CrystalCoin
her ikisini de saklayacağız. Blok zinciri boyunca bütünlüğün sağlanmasına yardımcı olmak için her bloğun kendi kendini tanımlayan bir hash'i olacaktır. Bitcoin gibi, her bloğun hash'i, bloğun kriptografik hash'i olacaktır ( index
, timestamp
, data
ve previous_hash
bloğun hash'inin önceki_hash'inin hash'i). Veriler şimdilik istediğiniz herhangi bir şey olabilir.
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
Crystal lang'da Ruby'nin attr_accessor
, attr_getter
ve attr_setter
yöntemlerini yeni anahtar kelimelerle değiştiriyoruz:
Yakut Anahtar Kelime | Kristal Anahtar Kelime |
---|---|
attr_accessor | Emlak |
attr_reader | alıcı |
attr_writer | pasör |
Crystal'de fark etmiş olabileceğiniz başka bir şey de, kodumuz aracılığıyla derleyiciye belirli türler hakkında ipucu vermek istememizdir. Crystal türleri çıkarır, ancak belirsizliğe sahip olduğunuzda türleri de açıkça bildirebilirsiniz. Bu nedenle current_hash
için String
türleri ekledik.
Şimdi block.cr
iki kez çalıştıralım ve farklı timestamp
nedeniyle aynı verinin farklı hash'ler üreteceğini not edelim:
crystal_coin [master●] % crystal src/crystal_coin/block.cr 361d0df74e28d37b71f6c5f579ee182dd3d41f73f174dc88c9f2536172d3bb66 crystal_coin [master●] % crystal src/crystal_coin/block.cr b1fafd81ba13fc21598fb083d9429d1b8a7e9a7120dbdacc7e461791b96b9bf3
Şimdi blok yapımız var, ancak bir blok zinciri oluşturuyoruz. Gerçek bir zincir oluşturmak için blok eklemeye başlamamız gerekiyor. Daha önce de belirttiğim gibi, her blok bir önceki bloktan bilgi gerektirir. Fakat blok zincirindeki ilk blok oraya nasıl ulaşır? Eh, ilk blok veya genesis
bloğu, özel bir bloktur (öncülleri olmayan bir blok). Çoğu durumda, manuel olarak eklenir veya eklenmesine izin veren benzersiz bir mantığı vardır.
Genesis bloğu döndüren yeni bir fonksiyon yaratacağız. Bu blok index=0
ve bir previous_hash
parametresinde isteğe bağlı bir veri değerine ve isteğe bağlı bir değere sahiptir.
Genesis bloğunu oluşturan Block.first
yöntemini oluşturalım veya sınıflandıralım:
module CrystalCoin class Block ... def self.first(data="Genesis Block") Block.new(data: data, previous_hash: "0") end ... end end
Ve p CrystalCoin::Block.first
kullanarak test edelim:
#<CrystalCoin::Block:0x10b33ac80 @current_hash="acb701a9b70cff5a0617d654e6b8a7155a8c712910d34df692db92455964d54e", @data="Genesis Block", @index=0, @timestamp=2018-05-13 17:54:02 +03:00, @previous_hash="0">
Artık bir genesis bloğu oluşturabildiğimize göre, blok zincirinde birbirini takip eden bloklar oluşturacak bir fonksiyona ihtiyacımız var.
Bu fonksiyon, zincirdeki önceki bloğu parametre olarak alacak, oluşturulacak blok için veri oluşturacak ve yeni bloğu uygun verilerle döndürecektir. Yeni bloklar önceki bloklardan gelen bilgileri hash ettiğinde, blok zincirinin bütünlüğü her yeni blokta artar.
Önemli bir sonuç, her ardışık bloğun karmasını değiştirmeden bir bloğun değiştirilememesidir. Bu, aşağıdaki örnekte gösterilmiştir. 44 bloğundaki veriler LOOP
EAST
, ardışık blokların tüm karmaları değiştirilmelidir. Bunun nedeni, bloğun karma değerinin (diğer şeylerin yanı sıra) previous_hash
değerine bağlı olmasıdır.
Bunu yapmasaydık, dışarıdaki bir tarafın verileri değiştirmesi ve zincirimizi tamamen yenisiyle değiştirmesi daha kolay olurdu. Bu karma zinciri, kriptografik kanıt görevi görür ve blok zincirine bir blok eklendikten sonra değiştirilemeyeceğini veya kaldırılamayacağını garanti etmeye yardımcı olur. Block.next
sınıf yöntemini oluşturalım:
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
Hepsini birlikte denemek için basit bir blok zinciri oluşturacağız. Listenin ilk öğesi genesis bloğudur. Ve tabii ki, sonraki blokları da eklememiz gerekiyor. CrystalCoin
göstermek için beş yeni blok oluşturacağız:
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)">
İşin kanıtı
İş Kanıtı algoritması (PoW), blok zincirinde yeni blokların nasıl oluşturulduğu veya çıkarıldığıdır . PoW'un amacı, bir sorunu çözen bir sayı bulmaktır. Numarayı bulmak zor olmalı, ancak ağdaki herhangi biri tarafından sayısal olarak doğrulanması kolay olmalıdır. Proof of Work'ün arkasındaki ana fikir budur.
Her şeyin net olduğundan emin olmak için bir örnekle gösterelim. Bir x tamsayısının başka bir y ile çarpımının hash değerinin 00
ile başlaması gerektiğini varsayacağız. Böyle:
hash(x * y) = 00ac23dc...
Bu basitleştirilmiş örnek için x=5
düzeltelim ve bunu Crystal'de uygulayalım:
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)}"
Kodu çalıştıralım:
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
Gördüğünüz gibi, bu y=530
sayısını bulmak zor (kaba kuvvet), ancak hash işlevini kullanarak doğrulamak kolaydı.
Bu PoW algoritmasıyla neden uğraşasınız ki? Neden blok başına bir hash oluşturmuyoruz, o kadar? Bir karma geçerli olmalıdır. Bizim durumumuzda, hashimizin ilk iki karakteri 00
ise bir hash geçerli olacaktır. Hashimiz 00......
ile başlıyorsa geçerli kabul edilir. Buna zorluk denir. Zorluk ne kadar yüksekse, geçerli bir hash elde etmek o kadar uzun sürer.
Ancak, eğer hash ilk seferde geçerli değilse, kullandığımız verilerde bir şeylerin değişmesi gerekir. Aynı verileri tekrar tekrar kullanırsak, aynı hash'i tekrar tekrar alırız ve hash'imiz asla geçerli olmaz. nonce
adında bir şey kullanıyoruz (önceki örneğimizde bu y
). Karma geçerli olmadığında her seferinde artırdığımız bir sayıdır. Verilerimizi (tarih, mesaj, önceki hash, indeks) ve 1 nonce alıyoruz. Bunlarla elde ettiğimiz hash geçerli değilse, 2 nonce ile deniyoruz ve geçerli bir hash elde edene kadar nonce'yi artırıyoruz. .
Bitcoin'de İş Kanıtı algoritmasına Hashcash denir. Block sınıfımıza bir çalışma kanıtı ekleyelim ve nonce'yi bulmak için madenciliğe başlayalım. İki baştaki sıfır '00' olarak sabit kodlanmış bir zorluk kullanacağız:
Şimdi bunu desteklemek için Block sınıfımızı yeniden tasarlayalım. CrystalCoin
aşağıdaki özellikleri içerecektir:
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
Kodumuzu temiz ve modüler tutmak için hashing yapmak ve nonce
bulmak için ayrı bir modül oluşturacağım. Ben buna 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
Block
sınıfımız şuna benzer:
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
Genel olarak Kristal kodu ve Kristal dil örnekleri hakkında dikkat edilmesi gereken birkaç şey. Crystal'de yöntemler varsayılan olarak herkese açıktır. Crystal, her bir özel yöntemin, Ruby'den gelen kafa karıştırıcı olabilecek özel anahtar kelime ile öneklenmesini gerektirir.
Ruby'nin Fixnum
ile karşılaştırıldığında Crystal'in Tamsayı türlerinin Int8
, Int16
, Int32
, Int64
, UInt8
, UInt16
, UInt32
veya UInt64
olduğunu fark etmiş olabilirsiniz. true
ve false
, Ruby'deki TrueClass
veya FalseClass
sınıflarındaki değerler yerine Bool
sınıfındaki değerlerdir.
Crystal, temel dil özellikleri olarak isteğe bağlı ve adlandırılmış yöntem argümanlarına sahiptir ve oldukça havalı olan argümanları işlemek için özel kod yazmayı gerektirmez. Block#initialize(index = 0, data = "data", previous_hash = "hash")
öğesini kontrol edin ve ardından Block.new(data: data, previous_hash: "0")
gibi bir şeyle çağırın.
Crystal ve Ruby programlama dili arasındaki farkların daha ayrıntılı bir listesi için Crystal for Rubyists'e bakın.
Şimdi, aşağıdakileri kullanarak beş işlem oluşturmaya çalışalım:
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">
Farkı gör? Artık tüm karmalar 00
ile başlar. İş kanıtının büyüsü budur. nonce
kullanarak ProofOfWork
bulduk ve ispat, eşleşen zorluğa sahip hash, yani baştaki iki sıfır 00
.
Yarattığımız ilk blokta, eşleşen şanslı sayıyı bulana kadar 17 nonce denedik:
Engellemek | Döngüler / Hash Hesaplama Sayısı |
---|---|
#0 | 17 |
#1 | 24 |
#2 | 61 |
#3 | 149 |
#4 | 570 |
#5 | 475 |
Şimdi önde gelen dört sıfırdan oluşan bir zorluk deneyelim ( difficulty="0000"
):

Engellemek | Döngüler / Hash Hesaplama Sayısı |
---|---|
#1 | 26 762 |
#2 | 68 419 |
#3 | 23 416 |
#4 | 15 353 |
İlk blokta eşleşen şanslı sayıyı bulana kadar 26762 nonce denendi ('00' zorluğuyla 17 nonce'yi karşılaştırın).
API olarak Blockchain'imiz
Çok uzak çok iyi. Basit blok zincirimizi yarattık ve yapması nispeten kolaydı. Ancak buradaki sorun, CrystalCoin
yalnızca tek bir makinede çalışabilmesidir (dağıtılmış/merkezi olmayan).
Şu andan itibaren CrystalCoin
için JSON verilerini kullanmaya başlayacağız. Veriler işlemler olacak, bu nedenle her bloğun veri alanı bir işlem listesi olacaktır.
Her işlem, receiver
paranın sender
, parayı alan kişiyi ve aktarılmakta olan CrystalCoin amount
ayrıntılandıran bir JSON nesnesi olacaktır:
{ "from": "71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij", "to": "93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo", "amount": 3 }
Yeni transaction
biçimini (önceden data
olarak adlandırılır) desteklemek için Block
sınıfımızda birkaç değişiklik. Bu nedenle, sadece karışıklığı önlemek ve tutarlılığı korumak için, bundan sonra örnek uygulamamızda yayınlanan data
atıfta bulunmak için transaction
terimini kullanacağız.
Yeni bir basit sınıf Transaction
tanıtacağız:
module CrystalCoin class Block class Transaction property from : String property to : String property amount : Int32 def initialize(@from, @to, @amount) end end end end
İşlemler bloklar halinde paketlenir, böylece bir blok sadece bir veya çok sayıda işlem içerebilir. İşlemleri içeren bloklar sıklıkla oluşturulur ve blok zincirine eklenir.
Blok zincirinin bir blok koleksiyonu olması gerekiyordu. Tüm blokları Crystal listesinde saklayabiliriz ve bu yüzden yeni Blockchain
sınıfını tanıtıyoruz:
Blockchain
, chain
ve uncommitted_transactions
dizilerine sahip olacaktır. chain
, blok zincirindeki tüm mayınlı blokları içerecek ve uncommitted_transactions
, blok zincirine eklenmemiş (hala mayınlı olmayan) tüm işlemlere sahip olacaktır. Blockchain
, Block.first
kullanarak genesis bloğunu oluşturuyoruz ve onu chain
dizisine ekliyoruz ve boş bir uncommitted_transactions
dizisi ekliyoruz.
uncommitted_transactions
dizisine işlemleri eklemek için Blockchain#add_transaction
yöntemini oluşturacağız.
Yeni Blockchain
sınıfımızı oluşturalım:
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
Block
sınıfında data
yerine transactions
kullanmaya başlayacağız:
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
Artık işlemlerimizin nasıl görüneceğini bildiğimize göre, bunları blok zinciri ağımızdaki node
adı verilen bilgisayarlardan birine eklemenin bir yoluna ihtiyacımız var. Bunu yapmak için basit bir HTTP sunucusu oluşturacağız.
Dört uç nokta oluşturacağız:
- [POST]
/transactions/new
: bir bloğa yeni bir işlem oluşturmak için - [GET]
/mine
: sunucumuza yeni bir blok kazmasını söylemek için. - [GET]
/chain
: tam blok zinciriniJSON
formatında döndürmek için. - [GET]
/pending
: bekleyen işlemleri döndürmek için (uncommitted_transactions
).
Kemal web çerçevesini kullanacağız. Uç noktaları Crystal işlevleriyle eşleştirmeyi kolaylaştıran bir mikro çerçevedir. Kemal, Rubyistler için Sinatra'dan büyük ölçüde etkilenir ve çok benzer bir şekilde çalışır. Ruby on Rails eşdeğerini arıyorsanız Amber'e bakın.
Sunucumuz blok zinciri ağımızda tek bir düğüm oluşturacaktır. Önce Kemal
shard.yml
dosyasına a olarak ekleyelim ve bağımlılığı yükleyelim:
dependencies: kemal: github: kemalcr/kemal
Şimdi HTTP sunucumuzun iskeletini oluşturalım:
# 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
Ve sunucuyu çalıştırın:
crystal_coin [master●●] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Sunucunun iyi çalıştığından emin olalım:
% curl http://0.0.0.0:3000/chain Send the blockchain as json objects%
Çok uzak çok iyi. Şimdi, uç noktaların her birini uygulamaya devam edebiliriz. /transactions/new
ve pending
bitiş noktalarını uygulayarak başlayalım:
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
Kolay uygulama. Sadece bir CrystalCoin::Block::Transaction
nesnesi oluşturuyoruz ve Blockchain#add_transaction
kullanarak işlemi uncommitted_transactions
dizisine ekliyoruz.
Şu anda, işlemler başlangıçta uncommitted_transactions
havuzunda depolanır. Onaylanmamış işlemleri bir bloğa koyma ve İş Kanıtı'nı (PoW) hesaplama süreci, blok madenciliği olarak bilinir. Kısıtlarımızı karşılayan nonce
bulunduktan sonra, bir bloğun çıkarıldığını ve yeni bloğun blok zincirine yerleştirildiğini söyleyebiliriz.
CrystalCoin
, daha önce oluşturduğumuz basit Proof-of-Work algoritmasını kullanacağız. Yeni bir blok oluşturmak için bir madencinin bilgisayarının şunları yapması gerekir:
-
chain
son bloğu bulun. - Bekleyen işlemleri bulun (
uncommitted_transactions
). -
Block.next
kullanarak yeni bir blok oluşturun. - Mayınlı bloğu
chain
dizisine ekleyin. -
uncommitted_transactions
dizisini temizleyin.
/mine
bitiş noktasını uygulamak için, önce yukarıdaki adımları Blockchain#mine
içinde uygulayalım:
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
Önce benim için bekleyen bazı işlemler olduğundan emin oluruz. Ardından @chain.last
kullanarak son bloğu ve çıkarılacak ilk 25
işlemi alırız (ilk 25 uncommitted_transactions
dizisini döndürmek için Array#shift(BLOCK_SIZE)
kullanıyoruz ve ardından 0 dizininden başlayan öğeleri kaldırıyoruz) .
Şimdi /mine
bitiş noktasını uygulayalım:
get "/mine" do blockchain.mine "Block with index=#{blockchain.chain.last.index} is mined." end
Ve /chain
bitiş noktası için:
get "/chain" do { chain: blockchain.chain }.to_json end
Blockchain ile Etkileşim
API'miz ile bir ağ üzerinden etkileşim kurmak için cURL
kullanacağız.
İlk önce sunucuyu çalıştıralım:
crystal_coin [master] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
Ardından işlem yapımızı içeren bir body ile http://localhost:3000/transactions/new
POST
isteği yaparak iki yeni işlem oluşturalım:
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%
Şimdi bekleyen işlemleri (yani henüz bloğa eklenmemiş işlemleri) listeleyelim:
crystal_coin [master●] % curl http://0.0.0.0:3000/pendings { "transactions":[ { "from":"ekis", "to":"huslks", "amount":7090 }, { "from":"ekis", "to":"huslks", "amount":70900 } ] }
Gördüğümüz gibi, daha önce oluşturduğumuz iki işlem, uncommitted_transactions
eklendi.
Şimdi http://0.0.0.0:3000/mine
için bir GET
isteğinde bulunarak iki işlemin madenciliğini yapalım:
crystal_coin [master●] % curl http://0.0.0.0:3000/mine Block with index=1 is mined.
Görünüşe göre ilk bloğu başarıyla kazdık ve chain
ekledik. chain
bir kez daha kontrol edelim ve mayınlı bloğu içeriyorsa:
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" } ] }
Konsensüs ve Yerinden Yönetim
Bu havalı. Kendimize işlemleri kabul eden ve yeni bloklar çıkarmamıza izin veren temel bir blok zinciri aldık. Ancak şimdiye kadar uyguladığımız kodun tek bir bilgisayarda çalışması amaçlanırken, blok zincirlerin tüm amacı, merkezi olmayan olmaları gerektiğidir. Ama eğer merkezden dağıtılmışlarsa, hepsinin aynı zinciri yansıtmasını nasıl sağlarız?
Bu Consensus
sorunudur.
Ağımızda birden fazla düğüm istiyorsak, bir fikir birliği algoritması uygulamamız gerekecek.
Yeni Düğümlerin Kaydedilmesi
Bir konsensüs algoritması uygulamak için, bir düğüme ağdaki komşu düğümler hakkında bilgi vermenin bir yoluna ihtiyacımız var. Ağımızdaki her düğüm, ağdaki diğer düğümlerin kaydını tutmalıdır. Bu nedenle, daha fazla uç noktaya ihtiyacımız olacak:
- [POST]
/nodes/register
: URL biçimindeki yeni düğümlerin listesini kabul etmek için. - [GET]
/nodes/resolve
: herhangi bir çakışmayı çözen Konsensüs Algoritmamızı uygulamak—bir düğümün doğru zincire sahip olduğundan emin olmak için.
Blok zincirimizin yapıcısını değiştirmemiz ve düğümleri kaydetmek için bir yöntem sağlamamız gerekiyor:
--- 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)
Düğümlerin listesini tutmak için String
tipinde bir Set
veri yapısı kullandığımızı unutmayın. Bu, yeni düğümlerin eklenmesinin önemsiz olmasını ve belirli bir düğümü kaç kez eklersek ekleyelim, tam olarak bir kez görünmesini sağlamanın ucuz bir yoludur.
Şimdi Consensus
yeni bir modül ekleyelim ve ilk yöntemi register_node(address)
uygulayalım:
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
register_node
işlevi, düğümün URL'sini ayrıştırır ve biçimlendirir.
Ve burada /nodes/register
bitiş noktası oluşturalım:
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
Şimdi bu uygulama ile birden çok düğümle ilgili bir sorunla karşılaşabiliriz. Birkaç düğümün zincirlerinin kopyası farklı olabilir. Bu durumda, tüm sistemin bütünlüğünü korumak için zincirin bir versiyonu üzerinde anlaşmamız gerekir. Konsensüs sağlamamız gerekiyor.
Bunu çözmek için, en uzun geçerli zincirin kullanılacak zincir olduğu kuralını koyacağız. Bu algoritmayı kullanarak ağımızdaki düğümler arasında fikir birliğine varıyoruz. Bu yaklaşımın arkasındaki neden, en uzun zincirin, yapılan en fazla iş miktarının iyi bir tahmini olmasıdır.
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
valid_chain?
resolve
doğrulayan bir yöntem olduğunu unutmayın. yöntem. 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"}]}%
Hooray! Our Crystal language example works like a charm, and I hope you found this lengthy tutorial crystal clear, pardon the pun.
Toplama
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.