الحسابات والمعاملات والرمز التنفيذي
يتم تخزين عقود Ethereum في حسابات عقود خاصة ، ويتم تنفيذ رمز العقد عندما يكون الحساب هو هدف معاملة (ناجحة). إذا كنت ، كمستخدم ، أرغب في تنفيذ عقد ، فأنا بحاجة أولاً إلى معرفة العنوان إلى الحساب الذي يتم فيه تخزين العقد ، ثم التعامل معه. المعلمات للمعاملة هي:
سعر الغاز الذي أريد أن أدفعه (“Gasprice”).
الحد الأقصى لكمية الغاز التي يمكن إنفاقها (“الغاز”).
مقدار الأثير الذي أريد نقله (“القيمة”).
عنوان حسابي (“من”).
عنوان الحساب المستهدف (“إلى”).
nonce (
nonce
).بيانات المعاملة (“البيانات”).
بدلاً من ذلك ، من الممكن إنشاء كائن المعاملة نفسه على جانب المتصل ، وتوقيعه محليًا ، والتمرير في البايتات الموقعة. يتم استخدام المعلمات المستخدمة أعلاه عند إجراء مكالمات RPC ، وسيقوم العميل بعد ذلك بإنشاء (وتوقيع) كائن المعاملة لك.
على أي حال ، إذا كانت معاملتي صالحة ، فإن عميل Ethereum سيضع بيانات المعاملة هذه في [سياق] (http://solidity.readthedocs.org/en/latest/units-and-global-variables.html#special-variables- والوظائف) ، جنبا إلى جنب مع بيانات الكتلة الحالية وبعض الأشياء الأخرى. ثم يمرر رمز حساب العقد والسياق في مثيل جديد من EVM ، وتنفيذه. Example
انتقل إلى [مترجم الصلابة عبر الإنترنت] (https://chriseth.github.io/browser-solidity/) ، وقم بتنظيف أي شيء موجود (أو فتح علامة تبويب جديدة) ، ولصق هذا الرمز:
|
|
تلميح: إذا حصلت على نتائج مختلفة ، فإن تلك المنشورة هنا ، قم بتغيير إصدار SOLC إلى 0.2.2-2016-03-02-32F3A653
بعد اللصق في الكود ، يجب أن ترى العقد المدرج على اليمين:
سيعرض النقر على “تفاصيل تبديل” بيانات العقد الأكثر تقدماً ، مثل رمز وقت التشغيل وتوقيعات الوظائف وتجميع EVM.قبل النظر إلى جميع الحقول المختلفة بالتفصيل ، سنذهب أولاً إلى تشريح عقد Ethereum.
عقد Ethereum
تتكون عقود Ethereum من جزأين رئيسيين - الجزء الرئيسي والجسم.
فيه
الأول هو جزء التهيئة. أنه يحتوي على مُنشئ ومنطق التهيئة الآخر (مثل تهيئة الحقل). يتم تنفيذ الجزء الرئيسي مرة واحدة فقط خلال دورة حياة العقود ، وذلك عندما يتم تحميل العقد على السلسلة ، أي عندما تؤدي تعليمات “إنشاء” إلى إنشاء مثيل جديد من العقد.
الجسم
الجزء الثاني هو الجزء “وقت التشغيل” ، أو “الجسم”. هذا هو الجزء من الكود المخزّن فعليًا في حساب العقد.
إنشاء عقود
يمكن إنشاء عقود جديدة بطريقتين ، إما عن طريق إرسال معاملة عقد جديدة إلى السلسلة من حساب خارجي ، أو عن طريق إنشائها ضمن عقد موجود بالفعل باستخدام “جديد”.
يتم إنشاء العقد معاملات عن طريق تمرير رمز bytecode المترجم (كله) في حقل البيانات للمعاملة ، وترك عنوان المستلم فارغًا. ربما تكون قد استخدمت Web3 لإنشاء عقود ، ورأيت أنه من أجل إنشاء عقد جديد ، ستتصل بـ “جديد” على كائن مصنع العقد ، وتمريره في كائن المعاملة. هذا هو السبب.
الميكانيكا وراء نظام init/الجسم هذا مستقيم للأمام. init هي مجرد سلسلة من تعليمات VM التي تنتهي بالعودة. القيمة التي تم إرجاعها هي في الواقع رمز Bytecode لجزء وقت التشغيل. يمكن توضيح ذلك باستخدام لغة LLL القديمة. هنا عقد أساسي للغاية مكتوب في LLL:
|
|
العقد هو في الأساس قسم “init” في LLL. يستخدم “[[a]] b (وهو اختصار لـ
(sstore a b) ) لكتابة عنوان المتصل إلى عنوان التخزين
0x0`. ثم ينتهي عن طريق إعادة جزء وقت التشغيل.
تبدو العودة غريبة ، ولكن من خلال تقسيمها ، يمكننا أن نرى أنها في النموذج (إرجاع 0x0 x)
، مما يعني “إرجاع x
بايت من الذاكرة ، بدءا من الموضع 0x0
. التعبيرx = (lll (lll y 0x0)
يعني فقط” تفسير التعبير “y`` as lll ، قم بتجميعه واكتب البايتات إلى الذاكرة ، بدءًا من العنوان z
. يقيم إلى إجمالي المبلغ البايت المكتوب. جزء وقت التشغيل (y = {(عندما (= ((المتصل) 0) (الانتحار (المتصل)))}
) مجرد رمز لـ “إذا كان المتصل هو الخالق ، انتحر العقد”.
هذا هو الهيكل العظمي لكل عقد LLL مكتوبة على الإطلاق ، أو على الأقل تم إضافة العقد المصنوع بعد أمر “lll(يومًا رائعًا). يعمل Serpent أيضًا بهذه الطريقة ، وحتى لديه وظيفة
init` الخاصة. تعمل الصلابة بهذه الطريقة أيضًا ، بالطبع ، ولكنها تخفيها تمامًا.
مترجم المتصفح
مع وضع ذلك في الاعتبار ، من الأسهل شرح ما تعنيه جميع البيانات في المتصفح.
bytecode
رمز Bytecode الكامل للعقد. أنه يحتوي على أجزاء init و body.
وقت التشغيل bytecode
يحتوي فقط على جزء وقت التشغيل. هذا هو ما يضاف إلى حساب العقد الفعلي.
Opcodes
هذا هو رمز bytecode إذا تم استبدال بايت الرمز OPCODE مع ذاكريها. على سبيل المثال ، فإن الدفع هو ذاكري للرقم 0x60
.
إذا تم تثبيت Go-ethereum (GETH) ، فيمكنك بالفعل تفكيك رمز Bytecode باستخدام أمر “Disasm”. حاول تشغيله باستخدام رمز Bytecode لعقد C ويمكنك التحقق من أن النتيجة تتطابق مع البيانات في قسم OpCodes.
|
|
حَشد
التجميع هو نسخة مختلفة قليلاً ومشروحة من البيانات في قسم Opcodes.
واجهة / واجهة صلابة / وظائف
هذه مجرد تمثيلات مختلفة لواجهة العقد.وظائف وسيتم معالجتها لاحقًا.
Web3 نشر / udapp
تستخدم لنشر العقود مع Web3 و Universal DAPP.
تقديرات الغاز
تستخدم لتقدير تكلفة الغاز للوظائف المختلفة.
تجميع العقد البسيط
في هذا القسم ، سنبحث عن كثب في رمز التجميع ، ولماذا يبدو كما هو الحال.لنبدأ من خلال اللصق في عقد أبسط في مترجم المتصفح:
|
|
هذا هو إخراج التجميع:
|
|
هذه هي الرموز المطلقة:
push1 0x60 push1 0x40 mstore push1 0xa dup1 push1 0x10 push1 0x0 push0 push1 0x0 return push1 0x60 push1 0x40 mstore push1 0x8 Jumpdest stop
سنبدأ بالنظر إلى الجزء الأول بين code
و data
.
|
|
هذا ينتهي عند العودة ، مما يعني أنه يمكننا قطع الرموز المفروضة في أول “عودة” أيضًا.
push1 0x60 push1 0x40 mstore push1 0xa dup1 push1 0x10 push1 0x0 push1 0x0 return
هذا هو قسم “init” من العقد. ما يفعله أولاً هو “دفع” الأرقام “0x60” و “0x40” على المكدس ، ثم تشغيل “Mstore”. وفقًا للورقة الصفراء] (http://gavwood.com/paper.pdf) (صفحة 27) ، لا يوجد تعليمات “push ؛ بدلاً من ذلك ، هناك 32 تعليمات مختلفة "pushn
، لـ n = 1 إلى 32
. بالنظر إلى الرموز البصرية ، يمكننا أن نرى أن “Push” في هذه الحالة هو في الواقع “Push1” ، مما يدفع البايت التالي إلى المكدس (يمكن رؤيته أيضًا من خلال النظر إلى حجم البايت للقيمة التي يتم دفعها) .
عند الوصول إلى تعليمات “MSTORE” ، فهذا يعني أن لدينا عنصرين على المكدس: 0x60
، و (في الأعلى) 0x40
. في الصفحة 26 من الورقة الصفراء ، تقول “Mstore” تستخدم لتخزين كلمة في الذاكرة. يستغرق معلمتين: الأول هو الإزاحة ، والثاني هو الكلمة. في هذه الحالة ، سيتم تخزين البايت 0x60
على عنوان الذاكرة 0x40
.
ستكون الحالة في هذه المرحلة عبارة عن مكدس فارغ ، وتخزين القيمة 0x60على عنوان الذاكرة
0x40`. سيكون عداد البرنامج في 5:
0: push1
1: 0x60
2: push1
3: 0x40
4: Mstore
التالي يبدأ في الحصول على غريب. التعليمات التالية هي دفع 0xa
إلى المكدس ثم قم بتدوينه. ثم يدفع 0x10
و 0x00
و codecopy
. بعد ذلك يدفع “0x0” ثم يعمل “العودة”.
هذا يحدث بسبب قاعدة “init init the Body” المذكورة أعلاه. سيقوم برنامج الترميز بنسخ الكود الذي يتم تنفيذه حاليًا في الذاكرة. إذا بحثنا عن “الترميز” في الورقة الصفراء ، فإننا نرى أنه يحتوي على ثلاثة معاملات. اكتشاف ما هم في الواقع أصعب قليلاً (…) ، ولكن هذا:
CODECOPY memoryOffset codeOffset codeLength
المعلمة الأولى هي عنوان الذاكرة حيث نريد بدء صفيف الرمز. والثاني هو المكان الذي نريد أن نبدأ في نسخه ، والثالث هو عدد البايتات التي نريد نسخها.
العنصر الأخير الذي وضع على المكدس هو 0x0
، مما يعني أن عنوان الذاكرة حيث ستبدأ بايت الكود.
الهدف هنا هو نسخ جميع التعليمات البرمجية التي تأتي بعد بيان “العودة” ، لأننا نريد فقط الجسم ، لذلك كل شيء من البايت 16 وما بعده. هذا بالطبع 0x10
في Hex ، وهذا هو السبب في أن هذا هو عنصر المكدس الثاني.
الطول بسيط جدا. عندما تم تجميع العقد ، قام تلقائيًا بحساب طول الجسم (طول الكود ناقص 16) ، تمامًا كما تم حساب حيث يبدأ الجسم. طول الجسم هنا هو “10” بايت ، أو “0xa، وهذا هو السبب في أن هذا هو العنصر الثالث على المكدس. يتم عرض كل هذه عندما يتم تنفيذ تعليمات "الترميز" ، تاركًا
0xa` باعتباره العنصر المتبقي الوحيد (تذكر أننا خداعنا). ستضاف الذاكرة 10 بايت إليها ، بدءًا من العنوان 0.
أخيرًا ، يدفع “0x0” ثم يعود. لقد شرحت بالفعل لماذا في مثال LLL. في الصفحة 29 من الورقة الصفراء ، يمكننا رؤية تعليمات “العودة”. يحدد جزءًا من الذاكرة التي سيتم إرجاعها كإخراج تنفيذ. يستغرق معلمتين ، الأول هو إزاحة الذاكرة ، والثاني هو عدد البايتات. تم دفع العنصر الأول على المكدس للتو ، وهو 0x0
، والعنصر الثاني (والأخير) هو 0xa
.
في هذه المرحلة ، انتهينا أخيرًا ، وسيتم إنشاء العقد. ولكن يبقى سؤال واحد: لماذا بدأت بهذا “mstore”؟ تم التنفيذ الآن ، ولم يتم الإشارة إلى العنوان أبدًا؟
في الواقع يتم استخدامه كمؤشر لعنوان الذاكرة التالي الذي يمكن استخدامه للكتابة. إذا بدأنا في قراءة جزء وقت التشغيل من الكود ، فإننا نرى أن نفس الشيء بالضبط يحدث مرة أخرى. العنوان 0x40
محجوز ، ويشير إلى فتحة الذاكرة المتاحة التالية والتي هي في البداية 0x60
(0x40
+ 0x20
، أو 32 ، وهي حجم الكلمات EVM). لاحظ أنني لست متأكدًا بنسبة 100 ٪ من كيفية عمل هذه العملية بأكملها الآن ، لذلك إذا تغير شيء ما ، فسوف أقوم بتصحيح هذا عندما أحصل على المزيد في الذاكرة في منشور قادم.
الجسم
جزء الجسم أسهل في الفهم. هذا هو التجميع:
|
|
هذه هي الرموز المطلقة:
PUSH1 0x60 PUSH1 0x40 MSTORE PUSH1 0x8 JUMP JUMPDEST STOP
الجزء الأول هو مجرد شيء الذاكرة (إعداد المؤشر).بعد ذلك ، نرى القيمة “0x8” التي يتم دفعها (“العلامة 1) في التجميع.ثم نرى قفزة.يأخذ
Jump (YP ، صفحة 26) القيمة الأولى على المكدس (وهو
0x8`) واستخدم ذلك لتعيين عداد البرنامج.بالنظر إلى الرموز البصرية ، نرى أن الرمز البصري مع الفهرس 8 هو “Jumpdest” ، وهو في الأساس وسيلة للقول إنه موقع قفزة مخصص ، ويتم تخطيه ، لذلك فإن التعليمات التالية التي سيتم تشغيلها بعد القفزة هي “توقف”.توقف عن إيقاف التنفيذ ببساطة ولا يعيد شيئًا.
هذا ما يبدو عليه الجسم الفارغ.لا يفعل شيئًا ، باستثناء تهيئة مؤشر الذاكرة.لاحظ أن القفزة الغريبة ليست مطلوبة بالفعل هنا.يمكن أن يكون مجرد توقف.في الواقع ، إذا قمت بتمكين المُحسِّن (مربع الاختيار في الأعلى ، بالقرب من إصدار المترجم) ، فستكتشف ذلك ويصبح مجموعة الجسم:
|
|
المهام
أوضح القسم السابق كيف يبدو رمز البداية ، وما هي الإرشادات التي يتم إضافتها إلى الرمز بغض النظر عن نوع العقد.بعد ذلك سننظر في عقد مع وظيفة بسيطة واحدة فيه.يشرح كيف تسمى الوظائف ، وهي الخطوة الثانية في فهم كيفية عمل الصلابة.
العقد الذي سنستخدمه الآن هو:
|
|
الصق في مترجم المتصفح ، وتأكد من فحص “تمكين التحسين”.يجب أن يكون لها التجميع التالي:
|
|
هذه هي الرموز المطلقة:
`push1 0x60 push1 0x40 mstore push1 0x1e dup1 push1 0x10 push1 0x0 push0 codecopy push1 0x0 return push1 0x60 push1 0x40 mstore push1 0xe0 push1 0x2 exp push1 0x0 calldataload div siv 4 0x26121ff0 dup dup eq eq ex1
يمكننا أن نرى على الفور أن قسم init له نفس التنسيق كما هو الحال في العقد الفارغ.يستخدم قيمة مختلفة لطول رمز الجسم (0x1e
) ، ولكن ذلك هو نفسه.
الجسم مختلف رغم ذلك ، وهناك بعض التعليمات الجديدة هناك.ها هي مجموعة الجسم:
|
|
فيما يلي الرموز المطلقة:
push1 0x60 push1 0x40 mstore push1 0xe0 push1 0x2 exp push1 0x0 calldataload div push4 0x26121ff0 dup2 eq push1 0x1a jumpdest stop stopest push1 0x18 Jump
`
الشيء حتى الأول “mstore” واضح. إنه مجرد شيء مؤشر الذاكرة. ثم يدفع 0xe0
و0x2
إلى المكدس ويدير exp
، مما يؤدي إلى 2^225
، والتي سيتم شرحها لاحقًا.
بعد ذلك ، يدفع 0x0
إلى المكدس (الفارغ) ويدير calldataload
. في الصفحة 24 من الورق الأصفر ، نرى أن الأمر يتطلب معلمة واحدة ، وهو إزاحة. “CallDataload هو مجرد إشارة إلى بيانات المعاملة ، أي البيانات التي تم تمريرها مع المعاملة. في هذه الحالة ، يريد البيانات المخزنة على عنوان CallData 0. يعيد CallDataload
دائمًا كلمة كاملة ، لذلك سيتم مسح 0x0
من المكدس واستبدالها بأول 32 بايت من CallData. إذا كانت Calldata فارغة ، أو أقل ثم 32 بايت ، فستظل قيمة الإرجاع مبطنة بالأصفار لتصبح 32 بايت.
بعد ذلك ، يقسم القيمة من calldataload 0
مع الرقم الهائل ، ويدفع الرقم 0x256121ff0
على المكدس ، ويخفف نتيجة القسم ويقوم بفحص متساوٍ ضد الرقم الجديد. إن الانقسام بين Calldata والرقم الهائل هو مجرد حلق كل شيء ما عدا البايت الأربعة الأولى ، بحيث يمكن مقارنته بـ 0x256121ff0. السبب في أن هذا يصبح أكثر وضوحًا عند النظر إلى قسم “وظائف” من مترجم المتصفح ، و [Ethereum Contract ABI] ( المحدد).
تحدد البايتات الأربعة الأولى لبيانات الاتصال لمكالمة دالة الوظيفة المراد استدعاؤها. هذا هو الأول (اليسار ، عالي الترتيب باللغة الكبرى) أربعة بايت من تجزئة Keccak (SHA-3) لتوقيع الوظيفة.
في قسم “الوظائف” من مترجم المتصفح ، نرى هذا: 26121ff0 f ()
، وهذا يعني أن كل هذا مجرد شيك ما إذا كان المتصل يريد استدعاء f
. إذا تم استخدام المزيد من الوظائف ، فستعمل بنفس الطريقة:
احصل على أول 32 بايت من Calldata.
حلق كل شيء ما عدا البايت الأربعة الأولى.
قارن مع معرف الوظيفة 1. إذا لم يكن هناك تطابق ، تابع إلى الوظيفة التالية. كرر لجميع الوظائف.
إذا كان التوقيع لا يتطابق مع وظيفة معروفة ، إما “توقف” أو تشغيل وظيفة الاحتياط إذا كان هناك وظيفة.
إذن ما الذي يحدث بالفعل إذا كانت مباراة؟ في هذه الحالة ، ستدفع 0x1a
(علامة 2) على المكدس ، وقم بقفزة مشروطة ، Jumpi
. في الصفحة 26 من الورق الأصفر ، نرى أن القفزة الشرطية لها params:
`jumpi newpcval cond '
سيؤدي ذلك إلى تغيير عداد البرنامج إلى “newpcval” إذا كان المعلمة الثانية ليست 0. في هذه الحالة ، إذا كان معرف الوظيفة في CallData يساوي معرف “F ، فسيتم تعيين عداد البرنامج على" 0x1a
. هذا هو “القفز” ، ونقطة الدخول إلى “F”. إذا لم يتطابق مع ذلك ، فسيتم تشغيله فقط ، مما يعني تمرير “القفز” التالي (العلامة 1) ثم “توقف”.
أخيرًا ، لاحظ أن تنفيذ “F` لا ينطوي فقط على دفع” 0x18 “(العلامة 1) إلى المكدس ثم” القفز “، مما يعني أنه مجرد تحريف سريع لنفس” التوقف “.
المشاركات القادمة
في المنشورات القادمة ، سأذهب إلى مزيد من التفاصيل حول Ethereum ABI ، والأنواع ، و CallData ، والذاكرة والتخزين. هذا ضروري لمعرفة من أجل كتابة رمز التجميع.
متعاقد ذكي سعيد!