إنشاء عملة مشفرة في لغة البرمجة الكريستالية
نشرت: 2022-03-11هذا المنشور هو محاولتي لفهم الجوانب الرئيسية لـ blockchain من خلال استكشاف العناصر الداخلية. لقد بدأت بقراءة الورقة البيضاء الأصلية لعملة البيتكوين ، لكنني شعرت أن الطريقة الوحيدة لفهم blockchain حقًا هي بناء عملة رقمية جديدة من الصفر.
لهذا السبب قررت إنشاء عملة مشفرة باستخدام لغة البرمجة الجديدة Crystal ، وأطلق عليها اسم CrystalCoin . لن تناقش هذه المقالة اختيارات الخوارزمية أو صعوبة التجزئة أو الموضوعات المماثلة. بدلاً من ذلك ، سيكون التركيز على تفصيل مثال ملموس ، والذي يجب أن يوفر فهمًا أعمق وعمليًا لنقاط القوة والقيود في blockchain.
إذا لم تكن قد قرأتها بعد ، لمزيد من المعلومات الأساسية حول الطحالب والتجزئة ، أقترح عليك إلقاء نظرة على مقال ديمير سلمانوفيتش Cryptocurrency for Dummies: Bitcoin and Beyond.
لماذا اخترت لغة البرمجة الكريستالية
للحصول على عرض أفضل ، أردت استخدام لغة مثمرة مثل روبي دون المساس بالأداء. تحتوي العملة المشفرة على العديد من العمليات الحسابية التي تستغرق وقتًا طويلاً (مثل التعدين والتجزئة ) ، ولهذا السبب فإن اللغات المجمعة مثل C ++ و Java هي اللغات المفضلة لبناء عملات رقمية "حقيقية". ومع ذلك ، كنت أرغب في استخدام لغة ذات بناء جملة أنظف حتى أتمكن من الحفاظ على متعة التطوير وإتاحة إمكانية قراءة أفضل. يميل الأداء الكريستالي إلى أن يكون جيدًا على أي حال.
فلماذا قررت استخدام لغة البرمجة Crystal؟ إن تركيب Crystal مستوحى بشكل كبير من Ruby ، لذلك بالنسبة لي ، من الطبيعي أن أقرأ ويسهل الكتابة. له فائدة إضافية تتمثل في منحنى التعلم المنخفض ، خاصة لمطوري Ruby ذوي الخبرة.
هذه هي الطريقة التي يضعها فريق Crystal lang على موقعه الرسمي على الإنترنت:
سريع مثل C ، أملس مثل روبي.
ومع ذلك ، على عكس Ruby أو JavaScript ، وهما لغات مفسرة ، فإن Crystal هي لغة مجمعة ، مما يجعلها أسرع بكثير وتوفر بصمة ذاكرة أقل. تحت الغطاء ، يستخدم LLVM للترجمة إلى الكود الأصلي.
يتم أيضًا كتابة Crystal بشكل ثابت ، مما يعني أن المترجم سيساعدك على اكتشاف أخطاء الكتابة في وقت الترجمة.
لن أشرح لماذا أعتبر لغة Crystal رائعة لأنها خارج نطاق هذه المقالة ، ولكن إذا لم تجد تفاؤلي مقنعًا ، فلا تتردد في مراجعة هذه المقالة للحصول على نظرة عامة أفضل على إمكانات Crystal.
ملاحظة: تفترض هذه المقالة أن لديك بالفعل فهمًا أساسيًا للبرمجة الموجهة للكائنات (OOP).
بلوكشين
إذن ، ما هو blockchain؟ إنها قائمة (سلسلة) من الكتل المرتبطة والمؤمنة ببصمات الأصابع الرقمية (المعروفة أيضًا باسم تجزئات التشفير).
أسهل طريقة للتفكير في الأمر هي أن تكون بنية بيانات قائمة مرتبطة. ومع ذلك ، فإن القائمة المرتبطة تتطلب فقط إشارة إلى العنصر السابق ؛ يجب أن يكون للكتلة معرّف بناءً على معرف الكتلة السابق ، مما يعني أنه لا يمكنك استبدال كتلة دون إعادة حساب كل كتلة تأتي بعد ذلك.
في الوقت الحالي ، فكر في blockchain كسلسلة من الكتل مع بعض البيانات المرتبطة بسلسلة ، السلسلة هي تجزئة الكتلة السابقة.
سيكون blockchain بأكمله موجودًا على كل عقدة تريد التفاعل معها ، مما يعني أنه يتم نسخها على كل عقد في الشبكة. لا يوجد خادم واحد يستضيفه ، ومع ذلك تستخدمه جميع شركات تطوير blockchain ، مما يجعله لامركزيًا .
نعم ، هذا غريب مقارنة بالأنظمة المركزية التقليدية. سيكون لكل عقد نسخة من blockchain بالكامل (> 149 جيجا بايت في Bitcoin blockchain بحلول ديسمبر 2017).
التجزئة والتوقيع الرقمي
إذن ، ما هي وظيفة التجزئة هذه؟ فكر في التجزئة كدالة ، تُرجع بصمة فريدة عندما نعطيها نصًا / كائنًا. حتى أصغر تغيير في كائن الإدخال سيغير بصمة الإصبع بشكل كبير.
توجد خوارزميات تجزئة مختلفة ، وفي هذه المقالة ، سنستخدم خوارزمية التجزئة SHA256
، وهي الخوارزمية المستخدمة في Bitcoin
.
باستخدام SHA256
، سننتج دائمًا 64 حرفًا سداسيًا عشريًا (256 بت) بطول حتى لو كان الإدخال أقل من 256 بت أو أكبر بكثير من 256 بت:
إدخال | نتائج مجزأة |
---|---|
نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا نص طويل جدًا | cf49bbb21c8b7c078165919d7e57c145ccb7f398e7b58d9a3729de368d86294a |
توبتال | 2e4e500e20f1358224c08c7fb7d3e0e9a5e4ab7a013bfd6774dfa54d7684dd21 |
توبتال. | 12075307ce09a6859601ce9d451d385053be80238ea127c5df6e6611eed7c6f0 |
لاحظ مع المثال الأخير ، أن مجرد إضافة ملف .
(نقطة) أدى إلى تغيير جذري في التجزئة.
لذلك ، في blockchain ، يتم إنشاء السلسلة عن طريق تمرير بيانات الكتلة إلى خوارزمية تجزئة من شأنها إنشاء تجزئة ، والتي ترتبط بالكتلة التالية ، من الآن فصاعدًا ، لتشكيل سلسلة من الكتل مرتبطة بتجزئة الكتل السابقة.
بناء Cryptocurreny في الكريستال
لنبدأ الآن في إنشاء مشروع Crystal الخاص بنا وبناء تشفير SHA256
الخاص بنا.
بافتراض أن لديك لغة برمجة Crystal مثبتة ، فلنقم بإنشاء الهيكل العظمي لقاعدة بيانات CrystalCoin
باستخدام 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/
سينشئ هذا الأمر الهيكل الأساسي للمشروع ، مع مستودع git مُهيأ بالفعل ، وترخيص وملفات تمهيدية. يأتي أيضًا مع أجزاء روتينية للاختبارات ، وملف shard.yml
لوصف المشروع وإدارة التبعيات ، المعروفة أيضًا باسم الأجزاء.
دعنا نضيف الجزء openssl
، وهو مطلوب لبناء خوارزمية SHA256
:
# shard.yml dependencies: openssl: github: datanoise/openssl.cr
بمجرد دخولك ، عد مرة أخرى إلى المحطة الطرفية وقم بتشغيل crystal deps
. سيؤدي القيام بذلك إلى هدم openssl
وتبعياته لنا لاستخدامها.
الآن لدينا المكتبة المطلوبة مثبتة في الكود الخاص بنا ، فلنبدأ بتحديد فئة Block
ثم بناء وظيفة التجزئة.
# 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
يمكنك الآن اختبار التطبيق الخاص بك عن طريق تشغيل crystal src/crystal_coin/block.cr
تشغيل الكريستال من جهازك.
crystal_coin [master●] % crystal src/crystal_coin/block.cr 33eedea60b0662c66c289ceba71863a864cf84b00e10002ca1069bf58f9362d5
تصميم Blockchain لدينا
يتم تخزين كل كتلة مع timestamp
index
اختياريًا. في CrystalCoin
، سنقوم بتخزين كليهما. للمساعدة في ضمان النزاهة في جميع أنحاء blockchain ، سيكون لكل كتلة تجزئة ذاتية التعريف. مثل Bitcoin ، ستكون تجزئة كل كتلة عبارة عن تجزئة تشفير للكتلة ( index
، timestamp
، data
، وتجزئة التجزئة السابقة للكتلة previous_hash
). يمكن أن تكون البيانات أي شيء تريده الآن.
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 ، نستبدل أساليب attr_getter
attr_accessor
attr_setter
من Ruby بكلمات رئيسية جديدة:
كلمة روبي | كريستال الكلمة |
---|---|
attr_accessor | خاصية |
Attr_reader | جامع |
الكاتب | واضع |
شيء آخر ربما تكون قد لاحظته في Crystal هو أننا نريد تلميح المترجم حول أنواع معينة من خلال الكود الخاص بنا. تستنتج Crystal الأنواع ، ولكن كلما كان لديك غموض يمكنك التصريح عن الأنواع صراحةً أيضًا. لهذا السبب أضفنا أنواع String
لـ current_hash
.
لنقم الآن بتشغيل block.cr
مرتين ونلاحظ أن نفس البيانات ستولد تجزئات مختلفة بسبب timestamp
المختلف:
crystal_coin [master●] % crystal src/crystal_coin/block.cr 361d0df74e28d37b71f6c5f579ee182dd3d41f73f174dc88c9f2536172d3bb66 crystal_coin [master●] % crystal src/crystal_coin/block.cr b1fafd81ba13fc21598fb083d9429d1b8a7e9a7120dbdacc7e461791b96b9bf3
الآن لدينا هيكل الكتلة الخاص بنا ، لكننا نقوم بإنشاء blockchain. نحتاج إلى البدء في إضافة الكتل لتشكيل سلسلة فعلية. كما ذكرت سابقًا ، تتطلب كل كتلة معلومات من الكتلة السابقة. ولكن كيف تصل الكتلة الأولى في blockchain إلى هناك؟ حسنًا ، الكتلة الأولى ، أو كتلة genesis
، هي كتلة خاصة (كتلة ليس لها سابقات). في كثير من الحالات ، تتم إضافته يدويًا أو يكون له منطق فريد يسمح بإضافته.
سننشئ وظيفة جديدة ترجع كتلة التكوين. هذه الكتلة من index=0
، ولها قيمة بيانات عشوائية وقيمة عشوائية في معلمة تجزئة previous_hash
.
دعونا نبني أو نصنف طريقة Block.first
التي تولد كتلة التكوين:
module CrystalCoin class Block ... def self.first(data="Genesis Block") Block.new(data: data, previous_hash: "0") end ... end end
ودعنا نختبرها باستخدام 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">
الآن بعد أن أصبحنا قادرين على إنشاء كتلة التكوين ، نحتاج إلى وظيفة من شأنها إنشاء الكتل الناجحة في blockchain.
ستأخذ هذه الوظيفة الكتلة السابقة في السلسلة كمعامل ، وإنشاء البيانات للكتلة المراد إنشاؤها ، وإرجاع الكتلة الجديدة بالبيانات المناسبة. عندما تقوم الكتل الجديدة بتجزئة المعلومات من الكتل السابقة ، تزداد سلامة blockchain مع كل كتلة جديدة.
إحدى النتائج المهمة هي أنه لا يمكن تعديل الكتلة دون تغيير تجزئة كل كتلة متتالية. هذا موضح في المثال أدناه. إذا تم تغيير البيانات الموجودة في الكتلة 44 من LOOP
إلى EAST
، فيجب تغيير جميع تجزئات الكتل المتتالية. هذا لأن تجزئة الكتلة تعتمد على قيمة التجزئة previous_hash
(من بين أشياء أخرى).
إذا لم نفعل ذلك ، فسيكون من الأسهل على طرف خارجي تغيير البيانات واستبدال سلسلتنا بسلسلة جديدة تمامًا خاصة به. تعمل سلسلة التجزئة هذه كدليل تشفير وتساعد على ضمان أنه بمجرد إضافة كتلة إلى blockchain ، لا يمكن استبدالها أو إزالتها. لنقم بإنشاء طريقة الفصل 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
لتجربتها معًا ، سننشئ blockchain بسيطًا. العنصر الأول في القائمة هو كتلة التكوين. وبالطبع ، نحتاج إلى إضافة الكتل التالية. سننشئ خمس كتل جديدة لإظهار 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)">
إثبات العمل
خوارزمية إثبات العمل (PoW) هي كيفية إنشاء كتل جديدة أو تعدينها على blockchain. الهدف من إثبات العمل هو اكتشاف رقم يحل مشكلة. يجب أن يكون من الصعب العثور على الرقم ولكن من السهل التحقق من حسابه بواسطة أي شخص على الشبكة. هذه هي الفكرة الأساسية وراء إثبات العمل.
دعنا نوضح بمثال للتأكد من أن كل شيء واضح. سنفترض أن تجزئة عدد صحيح x مضروبًا في y أخرى يجب أن تبدأ بـ 00
. وبالتالي:
hash(x * y) = 00ac23dc...
وفي هذا المثال المبسط ، دعنا نصلح x=5
وننفذ هذا في 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)}"
لنقم بتشغيل الكود:
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
كما ترى ، كان من الصعب العثور على هذا الرقم y=530
(القوة الغاشمة) ولكن من السهل التحقق منه باستخدام وظيفة التجزئة.
لماذا تهتم بخوارزمية إثبات العمل هذه؟ لماذا لا نقوم فقط بإنشاء تجزئة واحدة لكل كتلة وهذا كل شيء؟ يجب أن تكون التجزئة صالحة . في حالتنا ، ستكون التجزئة صالحة إذا كان أول حرفين من التجزئة لدينا 00
. إذا كانت التجزئة لدينا تبدأ بـ 00......
، فإنها تعتبر صالحة. هذا يسمى الصعوبة . كلما زادت الصعوبة ، كلما طالت مدة الحصول على تجزئة صالحة.
ولكن ، إذا لم تكن التجزئة صالحة في المرة الأولى ، فيجب تغيير شيء ما في البيانات التي نستخدمها. إذا استخدمنا نفس البيانات مرارًا وتكرارًا ، فسنحصل على نفس التجزئة مرارًا وتكرارًا ولن تكون التجزئة الخاصة بنا صالحة أبدًا. نستخدم شيئًا يسمى nonce
في تجزئة لدينا (في المثال السابق لدينا هو y
). إنه ببساطة رقم نزيده في كل مرة تكون فيها التجزئة غير صالحة. نحصل على بياناتنا (التاريخ ، والرسالة ، والتجزئة السابقة ، والفهرس) وعدد غير محدود من 1. إذا كانت التجزئة التي نحصل عليها مع هذه غير صالحة ، فإننا نحاول باستخدام nonce 2. ونزيد قيمة nonce حتى نحصل على تجزئة صالحة .
في Bitcoin ، تسمى خوارزمية إثبات العمل Hashcash. دعنا نضيف إثبات عمل إلى فئة Block الخاصة بنا ونبدأ في التعدين للعثور على nonce. سنستخدم صعوبة مشفرة من اثنين من الأصفار البادئة "00":
الآن دعنا نعيد تصميم فئة Block لدعم ذلك. سوف يحتوي CrystalCoin
Block الخاص بنا على السمات التالية:
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
سأقوم بإنشاء وحدة منفصلة لإجراء التجزئة والعثور على nonce
حتى نحافظ على الكود الخاص بنا نظيفًا ونمطيًا. 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
بنا على النحو التالي:
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
القليل من الأشياء التي يجب ملاحظتها حول كود Crystal وأمثلة لغة Crystal بشكل عام. في Crystal ، تكون الطرق عامة بشكل افتراضي. تتطلب Crystal أن تكون كل طريقة خاصة مسبوقة بالكلمة الأساسية الخاصة والتي قد تكون مربكة من Ruby.
ربما لاحظت أن أنواع عدد صحيح من Crystal هي Int8
أو Int16
أو Int32
أو Int64
أو UInt8
أو UInt16
أو UInt32
أو UInt64
مقارنةً بـ Ruby's Fixnum
. true
false
هما قيمتان في فئة Bool
بدلاً من القيم في الفئات TrueClass
أو FalseClass
في Ruby.
يحتوي Crystal على وسيطات طريقة اختيارية ومسمية كميزات أساسية للغة ، ولا يتطلب كتابة رمز خاص للتعامل مع الحجج وهو أمر رائع جدًا. تحقق من Block#initialize(index = 0, data = "data", previous_hash = "hash")
ثم استدعائها بشيء مثل Block.new(data: data, previous_hash: "0")
.
للحصول على قائمة أكثر تفصيلاً بالاختلافات بين لغة البرمجة Crystal و Ruby ، تحقق من Crystal for Rubyists.
الآن ، دعنا نحاول إنشاء خمس معاملات باستخدام:
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">
ترى الفرق؟ الآن تبدأ كل التجزئة بـ 00
. هذا هو سحر إثبات العمل. باستخدام ProofOfWork
وجدنا أن nonce
والإثبات هما التجزئة مع صعوبة المطابقة ، أي الصفرين البادئين 00
.
لاحظ مع الكتلة الأولى التي أنشأناها ، جربنا 17 مرة حتى تم العثور على رقم الحظ المطابق:
حاجز | الحلقات / عدد حسابات التجزئة |
---|---|
# 0 | 17 |
# 1 | 24 |
# 2 | 61 |
# 3 | 149 |
# 4 | 570 |
# 5 | 475 |
لنجرب الآن صعوبة أربعة أصفار بادئة ( difficulty="0000"
):
حاجز | الحلقات / عدد حسابات التجزئة |
---|---|
# 1 | 26762 |
# 2 | 68419 |
# 3 | 23416 |
# 4 | 15353 |
في الكتلة الأولى ، جربت 26762 nonces (قارن 17 nonces بصعوبة '00') حتى العثور على رقم الحظ المطابق.
Blockchain لدينا باعتباره API
حتى الان جيدة جدا. لقد أنشأنا blockchain البسيط الخاص بنا وكان من السهل نسبيًا القيام به. لكن المشكلة هنا هي أن CrystalCoin
لا يمكن تشغيله إلا على جهاز واحد (إنه غير موزع / لامركزي).
من الآن فصاعدًا ، سنبدأ في استخدام بيانات JSON لـ CrystalCoin
. ستكون البيانات عبارة عن معاملات ، لذلك سيكون حقل بيانات كل كتلة قائمة بالمعاملات.
ستكون كل معاملة عبارة عن كائن JSON يوضح بالتفصيل sender
العملة receiver
العملة amount
CrystalCoin الذي يتم تحويله:
{ "from": "71238uqirbfh894-random-public-key-a-alkjdflakjfewn204ij", "to": "93j4ivnqiopvh43-random-public-key-b-qjrgvnoeirbnferinfo", "amount": 3 }
بعض التعديلات على فئة Block
الخاصة بنا لدعم تنسيق transaction
الجديد (المعروف سابقًا data
). لذلك ، فقط لتجنب الالتباس والحفاظ على الاتساق ، سنستخدم مصطلح transaction
للإشارة إلى data
المنشورة في نموذج التطبيق الخاص بنا من الآن فصاعدًا.

سنقدم Transaction
فئة بسيطة جديدة:
module CrystalCoin class Block class Transaction property from : String property to : String property amount : Int32 def initialize(@from, @to, @amount) end end end end
يتم تجميع المعاملات في كتل ، لذلك يمكن أن تحتوي الكتلة على معاملة واحدة أو عدة معاملات. يتم إنشاء الكتل التي تحتوي على المعاملات بشكل متكرر وإضافتها إلى blockchain.
من المفترض أن تكون blockchain عبارة عن مجموعة من الكتل. يمكننا تخزين جميع الكتل في قائمة Crystal ، ولهذا السبب قدمنا فئة Blockchain
الجديدة:
سيكون لدى Blockchain
مصفوفات chain
ومعاملات uncommitted_transactions
ملتزمة. ستشمل chain
جميع الكتل الملغومة في uncommitted_transactions
، وستتضمن المعاملات غير الملتزمة جميع المعاملات التي لم تتم إضافتها إلى blockchain (لم يتم تعدينها بعد). بمجرد أن نقوم بتهيئة Blockchain
، نقوم بإنشاء كتلة التكوين باستخدام Block.first
وإضافتها إلى chain
مصفوفة ، ونضيف مصفوفة uncommitted_transactions
فارغة.
سننشئ طريقة Blockchain#add_transaction
لإضافة معاملات إلى صفيف uncommitted_transactions
.
دعونا نبني فئة Blockchain
الجديدة لدينا:
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
، سنبدأ في استخدام transactions
بدلاً من 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
الآن بعد أن عرفنا كيف ستبدو معاملاتنا ، نحتاج إلى طريقة لإضافتها إلى أحد أجهزة الكمبيوتر في شبكة blockchain الخاصة بنا ، تسمى node
. للقيام بذلك ، سننشئ خادم HTTP بسيطًا.
سننشئ أربع نقاط نهاية:
- [POST]
/transactions/new
: لإنشاء معاملة جديدة للكتلة - [GET]
/mine
: لإخبار خادمنا بتعدين كتلة جديدة. - [GET]
/chain
: لإرجاع blockchain الكامل بتنسيقJSON
. - [GET]
/pending
: لإعادة المعاملات المعلقة (uncommitted_transactions
).
سنستخدم إطار عمل ويب كمال. إنه إطار عمل صغير يجعل من السهل تعيين نقاط النهاية لوظائف Crystal. يتأثر كمال بشدة بسيناترا بالنسبة إلى روبيين ويعمل بطريقة مشابهة جدًا. إذا كنت تبحث عن مكافئ لـ Ruby on Rails ، فقم بإلقاء نظرة على Amber.
سيشكل خادمنا عقدة واحدة في شبكة blockchain الخاصة بنا. دعنا نضيف Kemal
أولاً إلى ملف shard.yml
التبعية:
dependencies: kemal: github: kemalcr/kemal
لنقم الآن ببناء الهيكل العظمي لخادم HTTP الخاص بنا:
# 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
وقم بتشغيل الخادم:
crystal_coin [master●●] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
لنتأكد من أن الخادم يعمل بشكل جيد:
% curl http://0.0.0.0:3000/chain Send the blockchain as json objects%
حتى الان جيدة جدا. الآن ، يمكننا المضي قدمًا في تنفيذ كل نقطة من نقاط النهاية. لنبدأ بتنفيذ /transactions/new
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
تنفيذ مباشر. نقوم فقط بإنشاء كائن CrystalCoin::Block::Transaction
وأضفنا المعاملة إلى صفيف uncommitted_transactions
باستخدام Blockchain#add_transaction
.
في الوقت الحالي ، يتم تخزين المعاملات مبدئيًا في مجموعة من المعاملات uncommitted_transactions
. تُعرف عملية وضع المعاملات غير المؤكدة في كتلة وحساب إثبات العمل (PoW) باسم تعدين الكتل. بمجرد اكتشاف عدم تلبية nonce
، يمكننا القول أنه تم تعدين كتلة ، وتم وضع الكتلة الجديدة في blockchain.
في CrystalCoin
، سنستخدم خوارزمية إثبات العمل البسيطة التي أنشأناها مسبقًا. لإنشاء كتلة جديدة ، يجب على كمبيوتر المُعدِّن أن:
- ابحث عن آخر كتلة في
chain
. - البحث عن المعاملات المعلقة (
uncommitted_transactions
). - قم بإنشاء كتلة جديدة باستخدام
Block.next
. - أضف الكتلة الملغومة إلى مجموعة
chain
. - تنظيف صفيف
uncommitted_transactions
.
لذلك لتنفيذ /mine
نقطة النهاية ، دعنا أولاً ننفذ الخطوات المذكورة أعلاه في 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
نتأكد أولاً من أن لدينا بعض المعاملات المعلقة الخاصة بي. ثم نحصل على الكتلة الأخيرة باستخدام @chain.last
، وأول 25
معاملة يتم تعدينها (نحن نستخدم Array#shift(BLOCK_SIZE)
لإرجاع مصفوفة من أول 25 معاملة uncommitted_transactions
ملتزمة ، ثم إزالة العناصر بدءًا من الفهرس 0) .
الآن دعنا ننفذ /mine
ننفذ نقطة النهاية:
get "/mine" do blockchain.mine "Block with index=#{blockchain.chain.last.index} is mined." end
و /chain
:
get "/chain" do { chain: blockchain.chain }.to_json end
التفاعل مع Blockchain لدينا
cURL
للتفاعل مع API الخاص بنا عبر الشبكة.
أولاً ، دعنا نشغل الخادم:
crystal_coin [master] % crystal run src/server.cr [development] Kemal is ready to lead at http://0.0.0.0:3000
بعد ذلك ، لنقم بإنشاء معاملتين جديدتين عن طريق تقديم طلب POST
إلى http://localhost:3000/transactions/new
مع هيئة تحتوي على هيكل معاملاتنا:
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%
الآن دعنا ندرج المعاملات المعلقة (أي المعاملات التي لم تتم إضافتها إلى الكتلة حتى الآن):
crystal_coin [master●] % curl http://0.0.0.0:3000/pendings { "transactions":[ { "from":"ekis", "to":"huslks", "amount":7090 }, { "from":"ekis", "to":"huslks", "amount":70900 } ] }
كما نرى ، تمت إضافة المعاملات التي أنشأناها سابقًا إلى المعاملات uncommitted_transactions
.
الآن دعنا نتخلص من المعاملتين عن طريق تقديم طلب GET
إلى http://0.0.0.0:3000/mine
:
crystal_coin [master●] % curl http://0.0.0.0:3000/mine Block with index=1 is mined.
يبدو أننا نجحنا في تعدين الكتلة الأولى وإضافتها إلى chain
. دعنا نتحقق مرتين من chain
وإذا كانت تتضمن الكتلة الملغومة:
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" } ] }
التوافق واللامركزية
ان هذا رائع. لقد حصلنا لأنفسنا على blockchain أساسي يقبل المعاملات ويسمح لنا بتعدين كتل جديدة. لكن الكود الذي طبقناه حتى الآن يُقصد به أن يعمل على جهاز كمبيوتر واحد ، في حين أن بيت القصيد من blockchain هو أنه يجب أن تكون لامركزية. ولكن إذا كانت لا مركزية ، فكيف نضمن أنها تعكس جميعها نفس السلسلة؟
هذه هي مشكلة Consensus
.
سيتعين علينا تنفيذ خوارزمية إجماع إذا أردنا أكثر من عقدة في شبكتنا.
تسجيل عقد جديدة
لتنفيذ خوارزمية الإجماع ، نحتاج إلى طريقة لإعلام العقدة بالعقد المجاورة على الشبكة. يجب أن تحتفظ كل عقدة على شبكتنا بسجل للعقد الأخرى على الشبكة. لذلك ، سنحتاج إلى المزيد من نقاط النهاية:
- [POST]
/nodes/register
: لقبول قائمة بالعقد الجديدة في شكل عناوين URL. - [GET]
/nodes/resolve
: لتنفيذ خوارزمية الإجماع لدينا ، والتي تحل أي تعارضات - لضمان أن العقدة لديها السلسلة الصحيحة.
نحتاج إلى تعديل مُنشئ blockchain الخاص بنا وتوفير طريقة لتسجيل العقد:
--- 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)
لاحظ أننا استخدمنا بنية بيانات Set
مع نوع String
للاحتفاظ بقائمة العقد. هذه طريقة رخيصة للتأكد من أن إضافة العقد الجديدة غير فعالة وأنه بغض النظر عن عدد المرات التي نضيف فيها عقدة معينة ، فإنها تظهر مرة واحدة بالضبط.
الآن دعنا نضيف وحدة نمطية جديدة إلى Consensus
الطريقة الأولى 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
ستقوم وظيفة register_node
بتحليل عنوان URL الخاص بالعقدة وتنسيقها.
وهنا لنقم بإنشاء /nodes/register
نقطة نهاية:
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
الآن مع هذا التنفيذ ، قد نواجه مشكلة في العقد المتعددة. يمكن أن تختلف نسخة سلاسل بضع عقد. في هذه الحالة ، نحتاج إلى الاتفاق على نسخة معينة من السلسلة للحفاظ على تكامل النظام بأكمله. نحن بحاجة إلى تحقيق توافق في الآراء.
لحل هذه المشكلة ، سنضع القاعدة التي تنص على أن أطول سلسلة صالحة هي التي يجب استخدامها. باستخدام هذه الخوارزمية ، توصلنا إلى إجماع بين العقد في شبكتنا. السبب وراء هذا النهج هو أن أطول سلسلة هي تقدير جيد لأكبر قدر من العمل المنجز.
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
ضع في اعتبارك أن resolve
عبارة عن طريقة تمر عبر جميع العقد المجاورة ، وتنزيل سلاسلها وتتحقق منها باستخدام valid_chain?
طريقة. 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.
تغليف
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.