إنشاء عملة مشفرة في لغة البرمجة الكريستالية

نشرت: 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 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"}]}% 

نص بديل للصورة

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.