Featured image of post البرمجة الموجهة للعقود - مقدمة صلابة للغة

البرمجة الموجهة للعقود - مقدمة صلابة للغة

عينة مقالة تعرض بناء جملة ومفاهيم الصلابة الأساسية - البرمجة الموجهة للعقد Ethereum.

البرمجة الموجهة للعقد

تركز هذه السلسلة على [البرمجة الموجه نحو التعاقد] (https://en.wikipedia.org/wiki/Design_by_contract) والصلابة. “العقد” في هذا السياق ليس حقًا عقدًا إيثريومًا أو عقدًا ذكيًا ؛ إنه الاسم المستخدم (غير رسمي حاليًا) يحدد وظيفة أو وحدة وظيفية أخرى. الفكرة الرئيسية هي أنه يجب تقسيم الوظيفة إلى شروط مسبقة ، والجسم والشرطات البريدية في الوصف (العقد) وفي الكود.

من الواضح أن هناك ما هو أكثر من ذلك ، وسنعود إلى مزيد من التفاصيل لاحقًا ، ولكن في الوقت الحالي ، سنبدأ من خلال النظر في بعض الميزات الرئيسية لميزات الصلابة وواجهاتها ، وكيف يمكن تطبيق الأساليب الموجهة للعقود .

على الرغم من أن الصلابة بسيطة للغاية ، إلا أنها ليست لغة سهلة لكتابة التعليمات البرمجية. هناك عقود ومكتبات ووظائف ومعدلات مخصصة وأحداث. تحتوي الوظائف على الكثير من المعدلات القياسية التي تحتاج إلى اختيار بشكل صحيح ، وليس من الواضح دائمًا ما يفعله بعضها فعليًا (مثل “الخارج” و “داخلي”). لا يتم تطبيق بعض المعدلات حتى الآن (مثل “دائم”) أيضًا - حتى لو قمت بتعيين جميع المعدلات بشكل صحيح ، فلا تزال هناك مشكلات أخرى ، مثل بعض أنواع الصلابة التي لا يمكن استخدامها كإدخال أو إخراج في أنواع معينة. المهام.

في الوقت نفسه ، في الواقع ، ليس سيئًا للغاية عندما تعتاد على ذلك ، وفي كل مرة تتحسن. المشكلات التي أذكرها هنا لا تسببها تصميم اللغة السيئة ، ولكن من خلال حقيقة أن بعض الميزات (بغض النظر عما إذا كانت لغة خاصة أو محددة EVM) لم يتم إضافتها بعد. اللغة لا تزال شابة. ومع ذلك ، أشعر أنه ينبغي تطهير بعضهم قبل الانتقال إلى جزء العقد.

(يرجى ملاحظة أن هذه المنشورات يتم إجراؤها في وقت تكون فيه البرمجة الرسمية الموجهة نحو العقد ليست معيارًا ؛ فهي لا تحتوي على دعم اللغة الكاملة وأمثلة قليلة جدًا من المعلومات وأمثلة رمز. لذلك هناك درجة من البحث والتجريب ، لذا كن احرص على عدم تطبيق هذه الأساليب مباشرة (بشكل صحيح أم لا) ، وافترض أن القيام بذلك سيجعل العقود آمنة بنسبة 100 ٪.)

وظائف ومعدلات قياسية

تتم إضافة المعدلات إلى المتغيرات والوظائف للتأثير على سلوكها. يمكن العثور على جميع المعدلات الأساسية [هنا] (https://en.wikipedia.org/wiki/design_by_contract). تتوفر معظم المعدلات الشائعة (مثل العامة والخاصة) ، لكن البعض يتصرف بشكل مختلف قليلاً عما قد تتوقعه.

قبل وصف جميع المعدلات بالكامل ، يجب أن أفكر في ميكانيكا “Call” و “المعاملة”:

“المعاملة” هي معاملة موقعة أرسلها المستخدم إلى حساب عقد أو حساب خارجي يتغير (أو على الأقل يحاول تغيير) حالة العالم. يتم وضع المعاملات في قائمة انتظار المعاملة ولا تعتبر صالحة حتى يتم تحويلها في النهاية إلى كتلة. يجب دائمًا استخدام المعاملات عند إرسال الأثير أو تنفيذ أي شكل من أشكال ** كتابة **.

يتم استخدام “التحدي” لقراءة ** بيانات من السلسلة أو إجراء حسابات لا تغير حالة العالم ، لذلك لا يتطلب توقيعًا صحيحًا أو إجماعًا من مستخدمين آخرين على الشبكة. مثال على استخدام “Call” هو التحقق من قيمة حقل العقد باستخدام وظيفة الإكسسوارات العامة.

`ثابت

لا يتم تطبيق المعدل “الثابت” بواسطة برنامج التحويل البرمجي للصلابة حتى الآن ، ولكن الغرض من ذلك هو الإشارة إلى المترجم والمتصلين بأن الوظيفة لا تحفر الحالة العالمية. يظهر “ثابت” في وصف ملفات JSON ABI للوظيفة ، ويستخدمه “Web3.js` - API الرسمي JavaScript - لتحديد ما إذا كان ينبغي أن تستدعي الوظيفة من خلال” معاملة “أو” Call “.

`خارجيا” و “عام”

من منظور الرؤية ، فإن “الخارجي” هو في الأساس نفس “الجمهور”. عند نشر عقد يحتوي على وظيفة “عامة” أو “خارجية” ، يمكن استدعاء الوظيفة من العقود والمكالمات والمعاملات الأخرى.

يجب عدم الخلط بين المعدل “الخارجي” وبين “خارجي” كما هو مستخدم في الورقة البيضاء ، أي “الحسابات الخارجية” ، مما يعني حسابات ليست حسابات العقود. يمكن استدعاء وظائف “الخارجية” من عقود أخرى وكذلك من خلال المعاملات أو المكالمات.

الفرق الرئيسي بين وظائف “الخارجية” و “العامة” هو الطريقة التي يطلق عليها من العقد الذي يحتوي عليها ، وكيف يتم التعامل مع معلمات الإدخال. إذا استدعت وظيفة “عامة” من وظيفة أخرى في نفس العقد ، فسيتم تنفيذ الكود باستخدام “القفز” ، مثل الوظائف الخاصة والداخلية ، في حين يجب استدعاء وظائف “خارجية” باستخدام تعليمات “الاتصال”. بالإضافة إلى ذلك ، لا تقوم وظائف “الخارجية” بنسخ بيانات الإدخال من صفيف CallData للقراءة فقط إلى متغيرات الذاكرة و/أو المكدس ، والتي يمكن استخدامها للتحسين.

أخيرًا ، “الجمهور” هو الرؤية الافتراضية ، فستكون وظائف المعنى أن تكون عامة إذا لم يتم تحديد شيء آخر. هناك نظام مماثل للمتغيرات أيضًا ، ولكن يمكن العثور على المزيد حول ذلك في الوثائق.

Internal

“داخلي” هو في الأساس نفس “محمي”.لا يمكن استدعاء الوظيفة من العقود الأخرى ، أو عن طريق التعامل مع العقد أو الاتصال بها ، ولكن يمكن استدعاؤها من وظائف أخرى في نفس العقد وأي عقود تمدده.

private

لا يمكن استدعاء وظائف “الخاصة إلا من وظائف في نفس العقد.

أمثلة

هذا هو عقد بسيط مع الوظائف العامة والخاصة.يمكن نسخه ولصقه في المترجم عبر الإنترنت.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
contract HelloVisibility {
    function hello() constant returns (string) {
        return "hello";
    }

    function helloLazy() constant returns (string) {
        return hello();
    }

    function helloAgain() constant returns (string) {
        return helloQuiet();
    }

    function helloQuiet() constant private returns (string) {
        return "hello";
    }

}

يمكن استدعاء وظيفة “Hello” من عقود أخرى ، ويمكن أيضًا استدعاؤها من العقد نفسه ، كما يتضح من وظيفة “Hellolazy” ، والتي تدعو ببساطة إلى وظيفة “Hello”. يمكن استدعاء وظيفة “HelloQuiet” من وظائف أخرى ، كما يتضح من وظيفة “Hellaagain” ، ولكن لا يمكن استدعاؤها من عقود أخرى أو من خلال المعاملات/المكالمات الخارجية.

لاحظ أن جميع الوظائف يتم تمييزها على أنها “ثابتة” ، لأن أيا منها لن يغير الدولة العالمية.

حاول إضافة “خارجي” إلى وظيفة “Hello” ، وسترى أن العقد فشل الآن في التجميع. إذا كان يجب أن تكون “مرحبًا” خارجًا لسبب ما ، فيمكننا محاولة إصلاح الكود عن طريق تغيير المكالمة داخل “Hellolazy” إلى “This.hello ()” ، على الرغم من أن القيام بذلك لن يعمل أيضًا! هذا بسبب مشكلة أخرى ذكرتها ، وهي أنه لا يمكن استخدام أنواع معينة (المصفوفات بحجم ديناميكي على وجه الخصوص) كمدخلات أو إخراج في وظائف معينة.

توضح العقدين التاليين الفرق بين القطاعين الخاص والداخلي.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
contract HelloGenerator {

    function helloQuiet() constant internal returns (string) {
        return "hello";
    }

}


contract Hello is HelloGenerator {
    function hello() constant external returns (string) {
        return helloQuiet();
    }
}

“مرحبًا” يمتد “Hellogenerator” ويستخدم وظيفته الداخلية لإنشاء السلسلة.“Hellogenerator” لديه JSON ABI فارغ ، لأنه ليس له وظائف عامة.حاول تغيير “داخلي” إلى “خاص” في “HelloQuiet” وستحصل على خطأ في برنامج التحويل البرمجي.

المعدلات المخصصة

يمكن العثور على وثائق المعدلات المخصصة [هنا] (http://solidity.readthedocs.io/en/latest/contracts.html#function-modifiers).كمثال على كيفية استخدام المعدلات ، فيما يلي ثلاث طرق مختلفة لإنشاء نفس الوظيفة:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
contract GuardedFunctionExample1 {

    uint public data = 1;

    function guardedFunction(uint _data) {
        if(_data == 0)
            throw;
        data = _data;
    }

}


contract GuardedFunctionExample2 {

    uint public data = 1;

    function guardedFunction(uint _data) {
        check(_data);
        data = _data;
    }

    function check(uint _data) private {
        if (_data == 0)
            throw;
    }

}


contract GuardedFunctionExample3 {

    uint public data = 1;

    modifier checked(uint _data) {
        if (_data == 0)
            throw;
        _
    }

    function guardedFunction(uint _data) checked(_data) {
        data = _data;
    }

}

ملاحظة: لا يظهر المعدل في أي مكان في JSON ABI.

البرمجة الموجهة نحو الحالة

بالنظر إلى القسم السابق ، قد يسأل المرء “لماذا تعقيد الأشياء باستخدام المعدلات المخصصة”؟ إذا كنت أرغب في إعادة استخدام الحارس في وظائف مختلفة ، فإن التحلل الطبيعي قد يكفي (مثال 2). إذا كنت أرغب في ضمها ، لمزيد من الكفاءة ، يمكنني فقط القيام بذلك أيضًا (مثال 1). هذا صحيح ، لكن دلالات المعدل أكثر ملاءمة لشيء يسمى البرمجة الموجهة للشرط. تم تحديد الفكرة الأساسية وراء COP في [منشور المدونة] (https://medium.com/@gavofyork/condition-oriented-programming-969f6ba0161a#.8dw7jp1gq) للدكتور غافن وود. يسلط منشور المدونة الضوء على عدد من الآثار الجانبية القبيحة التي تم خلط الظروف (قبل) في وظيفة مع “منطق العمل” نفسه ، ويوضح كيف يمكن استخدام COP لتجنب ذلك.

الأخطاء المحتملة تختبئ عندما يعتقد المبرمج أن الشرطية (وبالتالي فإن الحالة التي تعرض فيها) تعني شيئًا واحدًا عندما يعني ذلك في الواقع شيئًا مختلفًا بمهارة.

مثال جيد على ذلك هو الكود الذي تسبب في فشل “DAO”. لقد تم استنتاج أن بعض الوظائف في عقد DAO كانت عرضة لما يسمى بهجمات إعادة الانحناء ، ولكن هذا لم يكن بسبب الأخطاء في EVM ، أو لغة الصلابة نفسها ؛ كان بسبب خطأ في البرمجة يسهل القيام به. يمكن العثور على مزيد من المعلومات حول هذا الضعف في [منشور المدونة هذا] (https://eng.erisindustries.com/programming/2016/06/18/lessons-learned-dao/) بواسطة مطور الصلابة الأساسي RJ.

يستمر Gavin في توضيح أنه * بشكل أساسي ، يستخدم COP الشروط المسبقة كمواطن من الدرجة الأولى في البرمجة * - وهو ما هي في الأساس المعدلات المخصصة في الصلابة - ثم تتابع بعض الأمثلة البسيطة على كيفية استخدامها اكتب رمز صلابة الشرطي.

COP Applied

يحتوي هذا القسم على تطبيق بسيط للتقنيات الموضحة في منشور المدونة. العقد التالي هو تباين في العقد الرمزي المثال ، سنبدأ بدون تعديل.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
contract Token
{
    address public owner;

    // The balance of everyone
    mapping (address => uint) public balances;

    mapping (address => bool) public blacklisted;


    // Constructor - we're a millionaire!
    function Token() {
        owner = msg.sender;
        balances[msg.sender] = 1000000;
    }

    function blacklist(address _addr) {
        if(msg.sender != owner)
            return;
        blacklisted[_addr] = true;
    }

    function transfer(uint _amount, address _dest) {
        if(blacklisted[msg.sender])
            return;
        if(balances[msg.sender] >= _amount) {
            balances[msg.sender] -= _amount;
            balances[_dest] += _amount;
        }
    }

}

أول شيء سنفعله هو تحريك الحارس من وظيفة “القائمة السوداء” إلى تعديل وإضافة هذا التعديل إلى الوظيفة ، مما يمنحنا هذا:

1
2
3
4
5
6
7
8
9
modifier is_owner {
    if (msg.sender != owner)
        return;
    _
}

function blacklist(address _addr) is_owner {
    blacklisted[_addr] = true;
}

لاحظ أن المعدل لا يحتاج إلى “()” إذا لم يتطلب الأمر أي وسيطات.

إصلاح وظيفة النقل ليس بالأمر الصعب للغاية.يمكن استخدام نفس الإجراء الذي تم استخدامه لـ `isohener” لكلا الحراس.الفرق هو أننا سنضيف اثنين من المعدلات إلى الوظيفة بدلاً من واحدة.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
modifier not_blacklisted {
    if (blacklisted[msg.sender])
        return;
    _
}

modifier at_least(uint x) {
    if (balances[msg.sender] < x)
        return;
    _
}

function transfer(uint _amount, address _dest) not_blacklisted at_least(_amount) {
    balances[msg.sender] -= _amount;
    balances[_dest] += _amount;
}

لاحظ أن ترتيب المعدلات من اليسار إلى اليمين ، لذلك من أجل الحصول على نفس الترتيب بالضبط ، يجب أن يأتي فحص القائمة السوداء أولاً.أيضا ، تم تغيير فحص التوازن قليلاً ، على الرغم من أننا يمكن أن نجعل الأمر كذلك:

1
2
3
4
modifier at_least(uint x) {
    if (balances[msg.sender] >= x)
      _
}

هذا هو العقد الناتج.أنيق جدا.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

contract COPToken {

    address public owner;

    // The balance of everyone
    mapping (address => uint) public balances;

    mapping (address => bool) public blacklisted;


    // Constructor - we're a millionaire!
    function COPToken() {
        owner = msg.sender;
        balances[msg.sender] = 1000000;
    }

    modifier is_owner {
        if (msg.sender != owner)
            return;
        _
    }

    modifier not_blacklisted {
        if (blacklisted[msg.sender])
            return;
        _
    }

    modifier at_least(uint x) {
        if (balances[msg.sender] < x)
            return;
        _
    }

    function blacklist(address _addr) is_owner {
        blacklisted[_addr] = true;
    }

    function transfer(uint _amount, address _dest) not_blacklisted at_least(_amount) {
        balances[msg.sender] -= _amount;
        balances[_dest] += _amount;
    }

}

وحدة التجارب

الآن يأتي الجزء المثير للاهتمام: كيف نتأكد من أن هذه المعدلات تعمل بالفعل؟في هذه المرحلة ، سيتعين على بعض اختبارات الوحدة الأساسية القيام به.يمكننا فقط استدعاء الوظيفة مع معاملات مختلفة والتحقق من النتائج ، لكن هذا ليس نظيفًا بشكل خاص.بدلاً من ذلك ، سنستخدم الميراث لإنشاء عقد مع الوظائف المستخدمة لاختبار المعدل ، ووظائف منفصلة لاختبار الوظيفة المعدلة نفسها.هذا العقد الذي تم اختباره هنا هو أبسط ثم العقد السابق.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
contract Token
{

    // The balance of everyone
    mapping (address => uint) public balances;


    // Constructor - we're a millionaire!
    function Token() {
        balances[msg.sender] = 1000000;
    }

    modifier at_least(uint x) {
        if (balances[msg.sender] < x)
            return;
        _
    }

    function transfer(uint _amount, address _dest) at_least(_amount) {
        balances[msg.sender] -= _amount;
        balances[_dest] += _amount;
    }

}


contract TokenTest is Token {

    address constant EMPTY_ACCOUNT = 0xDEADBEA7;

    function atLeastTester(uint _amount) at_least(_amount) constant private returns (bool) {
        return true;
    }

    function testAtLeastSuccess() returns (bool) {
        balances[msg.sender] = 1000;
        return atLeastTester(1000);
    }

    function testAtLeastFailBalanceTooLow() returns (bool) {
        balances[msg.sender] = 999;
        return !atLeastTester(1000);
    }

    // Test transferring to account with no money, then check their balance.
    function testTransfer() returns (bool) {
        balances[msg.sender] = 500;
        balances[EMPTY_ACCOUNT] = 0;
        transfer(500, EMPTY_ACCOUNT);
        return balances[msg.sender] == 0 && balances[EMPTY_ACCOUNT] == 500;
    }

    // Test transferring to account with no money, then check their balance.
    function testTransferFailBalanceTooLow() returns (bool) {
        balances[msg.sender] = 500;
        balances[EMPTY_ACCOUNT] = 0;
        transfer(600, EMPTY_ACCOUNT);
        return balances[msg.sender] == 500 && balances[EMPTY_ACCOUNT] == 0;
    }

}

الوظيفة الأولى في عقد الاختبار تدير ببساطة المعدل وتُرجع صحيحًا في حالة مرورها. هذا يعني أن الوظيفة ستعود “صحيحًا” إذا كان توازن المتصل مساوياً أو أعلى ثم القيمة المقدمة ، وإلا فإنها تعود خاطئة. بعد ذلك ، هناك وظيفتان بسيطتان للتحقق مما إذا كان المعدل يعمل على النحو المقصود. أخيرًا ، هناك وظيفتان للتحقق من أن جسم وظيفة النقل يعمل على النحو المقصود.

هذا يجعل اختبار وظيفة النقل أسهل. إذا تم تمرير اختبارات المعدل ولكن وظيفة النقل لا ، فمن الواضح أن هناك شيئًا خاطئًا في وظيفة النقل. إذا فشل اختبارات المعدل ، فلن نتوقع أن تعمل وظيفة النقل بشكل صحيح حتى يتم إصلاح المعدل.

المضاعفات

نهج تعديل الشرطي لا يأتي بدون مشاكل.

عند استخدام الوظائف العادية ، بدون المعدلات ، من الممكن إرجاع رمز الخطأ عندما يحدث خطأ ما. في العقد المثال ، ربما كنا نريد إعادة رمز خطأ مختلف اعتمادًا على ما إذا كان النقل ناجحًا ، أو إذا فشلت لأن المتصل كان مدرجًا في القائمة السوداء ، أو إذا فشلت لأن توازن المتصلين كان منخفضًا جدًا. يمكن أن يكون هذا مفيدًا جدًا عندما يتم استدعاء الوظيفة من قبل العقود الأخرى. المعدلات ثقيلة بعض الشيء في هذا الصدد لأن الشيء الوحيد الذي يمكنهم فعله حقًا (في هذه المرحلة) هو رمي ، وهو ما ينهي VM ويعود إلى جميع التغييرات ، أو العائد ، مما يعيد القيمة الفارغة لجميع معاملات الإرجاع.

القضية الأخرى هي بالطبع المشكلة الكلاسيكية التي تنشأ عندما “الأمور رسمية” - يصبح الكود أكثر صعوبة في الكتابة. مثال على ذلك هو وظيفة تتكون من عدة شرطية متداخلة ، وكل كتلة تحتوي على الكثير من المنطق. يستخدم مثال Gavin وظيفة رفض التصويت تسمى بعد تشغيل منطق النقل (وحراسها) ، ولكن يمكن أن تكون الأمور أكثر تعقيدًا ؛ يتم استدعاء وظيفة التصويت افتراضيًا ، ولا يأخذ المعدل المخصص في هذه الوظيفة أي وسيطة ، ولكن ماذا لو كان ينبغي استدعاء وظائف مختلفة اعتمادًا على ما إذا كانت المعاملة ناجحة أم لا ، وتلك بدورها لها شروط مماثلة فيها. قد يصبح من الصعب كتابة ذلك باستخدام شرطي مناسب.

استنتاج

هذا نهج مثير للاهتمام للغاية لكتابة العقود الذكية في صلابة. جنبا إلى جنب مع التحقق الرسمي ، واختبار الوحدة الجديدة وأدوات التحليل الثابت ، والميزات المخططة الجديدة مثل وظائف Lambda ، ومن خلال التمسك بميزات اللغة الآمنة الموجودة بالفعل ، نأمل أن نتجنب مشاكل “DAO” من الحدوث مرة أخرى.

تجدر الإشارة أيضًا إلى أن التصميم من خلال العقد ليس بعض البناء النظري الغامض الذي يتم تطبيقه فقط بلغات متخصصة خاصة ؛ إنه نموذج برمجة راسخ ، وهناك العديد من المكتبات التي تجعلها ممكنة في اللغات السائدة مثل C ++ و Java (إلى حد ما). بعض اللغات (مثل C ++) لديها حتى دعم اللغة الكاملة.

أخيرًا ، لا أريد حقًا أن أضع الرأي الشخصي هنا ، لكن بما أنني أمسك بشكل أساسي بالركوب. المبرمجون للمشاكل. لا أحد يستطيع أن يكتب 100 ٪ خالية من الأخطاء ، رمز مثالي ، وهذا في الأساس ما كان من المفترض أن يفعلوه. على الأقل كان لديهم البصيرة لوضع حماية في مكانها تمنع الأثير من مجرد امتصاص من العقد.

سيحتوي الجزء التالي على عقود ووظائف أكثر تقدمًا. من المحتمل أن يأتي بعد أن نشر Gavin الجزء 2 من البرنامج التعليمي.

سعيد (وآمن) كتابة العقود الذكية!

comments powered by Disqus
مبني بستخدام Hugo
قالب Stack مصمم من Jimmy