Crystal Programlama Dilinde Kripto Para Birimi Oluşturma

Yayınlanan: 2022-03-11

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

Kristal Programlama Dili illüstrasyonu

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.

Kristal kripto para birimi karma diyagramı

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 

resim alt metni

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 zincirini JSON 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.

resim alt metni

 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 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"}]}% 

resim alt metni

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.