هذا هو الإدخال الأول في سلسلة من يوميات المطورين العادية التي سينشرها أعضاء فريق تطوير AdChain Registry خلال الأشهر المقبلة. يتعلق هذا الإدخال الأول بنظام التصويت على الالتزام/الكشف عن القفل الجزئي لسجل AdChain، وقد تمت كتابته بواسطة مايك غولدين من كونسنسيس.
يعد التصويت على قبول النطاقات في سجل AdChain هو قلب لعبة AdChain التحفيزية. ولدعم ذلك، قمنا بتطوير منتج عام مرخص من Apache-2 وفتحنا مصادره تنفيذ التصويت بالقفل الجزئي (PLCR)، مكتوبة في سوليديتي. يُعد تصويت PLCR نظامًا فعالًا للتصويت المرجح بالرمز والذي يمكّن المستخدم من المشاركة في استطلاعات متعددة في وقت واحد مع الرموز المميزة الخاصة به مع منع التصويت المزدوج للرموز داخل استطلاعات الرأي. والأهم من ذلك أنه يسمح للمستخدمين بسحب الحد الأقصى لعدد الرموز التي لا يتم استخدامها بنشاط للتصويت في أي وقت.
تم وصف تصويت PLCR في الأصل في منشور مدونة بقلم آرون فيشر الكتابة لـ مشروع كولوني. إيلينا ديميتروفاتمت الإشارة إلى مشاركات المدونة حول هذا الموضوع بشكل كبير في بناء هذا التنفيذ. نحن ممتنون لهم على عملهم الأصلي!
لماذا التصويت PLCR؟
إن تنفيذ تصويت PLCR في Solidity ليس بالأمر السهل، فلماذا تهتم؟ بالإضافة إلى حجب فرز الأصوات قبل الانتهاء من الاقتراع باستخدام الالتزام/الكشف (وهو أمر مرغوب فيه لمنع عملية التصويت نفسها من التأثير على نتائج التصويت) يتيح تصويت PLCR شيئين:
على سبيل التوضيح: يقوم المستخدم بتحميل 10 رموز في عقد تصويت PLCR. يقوم المستخدم بعد ذلك بتخصيص 10 رموز في الاستطلاع A وستة رموز في الاستطلاع B. بعد الكشف في الاستطلاع A، تظل ستة رموز مقفلة في الاستطلاع B ولكن يمكن للمستخدم سحب أربعة رموز.
في نظام استطلاع ساذج لا يعتمد على PLCR، يمكن للمستخدم قفل الرموز في عقد ذكي يصف استطلاعًا واحدًا. هذا ليس حلاً مثاليًا لأنه يمنع المستخدم من المشاركة في استطلاعات متعددة في وقت واحد باستخدام نفس الرموز. إذا كانت الرموز المميزة للمستخدم مقفلة في بعض العقود الذكية، فلن يتمكن عقد ذكي آخر من «النقل من» المستخدم لقفلها بنفسه.
لا يلزم أن يكون إصلاح هذا واستخدام عقد ذكي واحد لإدارة استطلاعات الرأي المتعددة أمرًا معقدًا للغاية إذا كان العقد ممكنًا. جميع رموز المستخدم بينما يكون لدى المستخدم رموز مميزة ملتزمة بها أي استطلاعات الرأي. ومع ذلك، إذا وافق المستخدم على مثل هذا العقد لقفل 10 رموز وكان أقل من 10 منها ملتزمًا فعليًا باستطلاعات الرأي في أي وقت، فيجب على المستخدم الانتظار جميع سيتم الانتهاء من استطلاعات الرأي قبل الانسحاب أي من الرموز الخاصة بهم. قد يؤدي هذا في الواقع إلى تثبيط المستخدمين عن المشاركة في التصويت، لأنه يقيد أيديهم إذا كان لديهم رموز ملتزمة باستطلاعات الرأي أثناء حدوث أحداث السوق التي قد يرغبون في الرد عليها. يجب أن تزيد أنظمة الاقتراع من السيولة الرمزية إلى أقصى حد ممكن.
يتطلب تعظيم سيولة الرمز على هذا النحو أن يتحمل المطور قدرًا لا بأس به من التعقيد، على الأقل في Solidity. استخدام رموز مينيمي هو نهج واحد. يدعم تصويت PLCR الرموز التي ليست رموز MiniMe.
تصويت لجنة الانتخابات العامة. sol
مثيل تم نشره من PLCRvoting.sol يحدد الرمز المميز التي يمكن توزيع حقوق التصويت عليها. يمكن إجراء أي تصويت مرجح بالرمز يلزم إجراؤه باستخدام هذا الرمز المميز باستخدام نفس عقد تصويت PLCR المنشور، ولن تتداخل هذه الاستطلاعات مع بعضها البعض. يتم تحديد الرمز المميز الذي سيتم استخدامه باعتباره الوسيطة الوحيدة للمُنشئ.
إنشاء استطلاع
وظيفة بدء الاستطلاع يستخدم لإنشاء استطلاعات جديدة. يستغرق الأمر ثلاث حجج ويعيد وحدة. الحجج هي:
نصاب التصويت: النسبة المئوية اللازمة من الأصوات «من أجل» اللازمة لاعتبار الاقتراع ناجحًا. قد تتطلب بعض موضوعات الاستطلاع أغلبية ساحقة لاجتيازها، على سبيل المثال.
مدة الالتزام: مدة فترة الالتزام بالثواني.
كشف المدة: مدة فترة الكشف بالثواني.
في الجسم الوظيفي، أول شيء نقوم به هو زيادة استطلاع العقد مرة واحدة، متغير تخزين. من خلال زيادتها في كل مرة يتم فيها بدء الاستطلاع، نقوم بإنشاء معرف فريد لكل استطلاع. نظرًا لأننا نقوم دائمًا بزيادة الاستطلاع مرة واحدة أولاً، لاحظ ذلك لن يكون هناك أبدًا استطلاع بمعرف صفر.
function startPoll(uint _voteQuorum, uint _commitDuration, uint _revealDuration) public returns (uint pollID) {
pollNonce = pollNonce + 1;
pollMap[pollNonce] = Poll({
voteQuorum: _voteQuorum,
commitEndDate: block.timestamp + _commitDuration,
revealEndDate: block.timestamp + _commitDuration + _revealDuration,
votesFor: 0,
votesAgainst: 0
});
PollCreated(pollNonce);
return pollNonce;
}
التالي نقوم بإنشاء مثيل لبنية الاستطلاع وأضفه إلى خريطة الاستطلاع باستخدام استطلاع الرأي مرة واحدة كمفتاح. تقوم بنية الاستطلاع بتخزين معاملات الاستطلاع التي تم تمريرها كوسيطات وتهيئة عمليات فرز الأصوات المؤيدة والمعارضة إلى الصفر.
أخيرًا، نطلق حدثًا تم إنشاؤه باستخدام PollnOnce، ونعيد PollnOnce لاستخدامه لاحقًا كعنوان استطلاع لهذا الاستطلاع. بسيط جدًا!
الشيء المنطقي التالي الذي قد يحدث بعد إنشاء الاستطلاع، هو أن شخصًا ما قد يرغب في التصويت في هذا الاستطلاع. هناك بضع خطوات لهذه العملية، والتي تبدأ بـ RequestVotingRights.
طلب حقوق التصويت
لمنع التصويت المزدوج على التوكنات داخل الاستطلاع، يحتاج عقد PLCR إلى إدارة رموز المستخدم من وقت الالتزام بها عند الكشف عنها. يمكن استخدام الرموز المُدارة للتصويت في استطلاعات متعددة بشكل متزامن، ولكن ليس عدة مرات في نفس الاستطلاع. طلب وظيفة حقوق التصويت يمنح حقوق التصويت للمستخدم مساوية لوزن الرموز الموضوعة تحت الإدارة.
function requestVotingRights(uint numTokens) external {
require(token.balanceOf(msg.sender) >= numTokens);
require(token.transferFrom(msg.sender, this, numTokens));
voteTokenBalance[msg.sender] += numTokens;
}
في السطر الأول من جسم الوظيفة نتحقق من أن رصيد الرمز الفعلي لمرسل الرسالة كافٍ بالنسبة إلى وسيطة NumTokens المقدمة. ال السطر التالي يستدعي TransferFrom، ينقل NumTokens من رصيد مرسل الرسالة إلى رصيد عقد PLCR. السطر الأول عبارة عن فحص متكرر: ستؤدي عبارة الطلب الثانية إلى حدوث خطأ في أي حالة حدث فيها الأول. هذه مجرد برمجة دفاعية، حيث قد يستخدم الأشخاص هذا العقد مع تطبيقات ERC-20 التي تجرها الدواب (على الرغم من عدم وجود عذر للقيام بذلك عندما تطبيقات جيدة موجود).
لاحظ أيضًا أن TransferFrom سيفشل إذا لم يوافق المستخدم على عقد PLCR لنقل NumTokens قبل الاتصال بـ RequestVotingRights. هذا أمر محزن و هناك مقترحات لتحسين نمط «الموافقة والاتصال»، لكنها لم تنفذ بعد على نطاق واسع.
أخيرًا، إذا نجح السطران الأولان، فإننا زيادة رصيد رمز التصويت/مرسل الرسالة بواسطة NumTokens. يا للعجب. يمكننا التصويت الآن. يمكننا أيضًا سحب كل شيء الآن، حيث لم يتم حتى الآن حجز أي من الرموز التي تمت إدارتها في الاستطلاع.
الالتزام بالتصويت
Commit-reveal هو نمط يستخدم في ENS لإخفاء العطاءات، ويمكن استخدامه للاقتراع السري أيضًا. التصويت الملتزم هو هاش مملح من تصويت المستخدم، مما يعني أن تفضيل المستخدم (نعم أو لا) مرتبط ببعض العشوائية (الملح) قبل أن يتم تجزئته والالتزام به. التصويت على الالتزام يأخذ الحجج التالية:
هوية الاستطلاع: معرف الاستطلاع الذي تم التصويت عليه (تم إرجاعه في الأصل في بعض عمليات استدعاء StartPoll)
سيكريت هاش: قطعة keccak256 التي يختارها الناخب والملح (معبأة بإحكام ومرتبة)
رموز الأرقام: عدد الرموز التي يجب الالتزام بها في الاستطلاع لهذا التصويت
استطلاع الرأي السابق: معرف الاستطلاع الذي يمتلك المستخدم فيه حاليًا أكبر عدد من الرموز المميزة التي تقل عن أو تساوي NumTokens الملتزم بها (سنتحدث عن هذا لاحقًا).
حسنا، دعونا ننظر إلى الجسم الوظيفي. أول شيء سنفعله هو بعض الفحوصات. سنقوم باستدعاء وظيفة مساعدة للتأكد من أن فترة الالتزام الخاصة بـ polLid المقدم نشطة، وأن VoteTokenBalance لمرسل الرسالة هي على الأقل قيمة NumTokens التي تم تمريرها، وأن معرف الاستطلاع المقدم ليس صفراً.
الاستطراد: لماذا نتعامل مع الاستطلاع الخاص بـ PollID zero بشكل خاص؟ لاحظنا سابقًا أنه في StartPoll لن يكون هناك أبدًا معرف استطلاع لـ PollnOnce صفر، وهنا نتحقق على وجه التحديد من عدم التزام الأصوات بالاستطلاع في PolliD zero. في EVM، تتم تهيئة جميع البيانات إلى الصفر. إذا قمت بتعريف uint x ولم تقم بتهيئته، فسيكون (x == 0 && x == false) صحيحًا. بالنسبة للعقود الذكية التي تتفاعل مع عقد تصويت PLCR، قد يكون من المفيد الرجوع إلى الاستطلاع عند المعرف صفر كنوع من القيمة الخالية. في سجل AdChain، على سبيل المثال، يتم تخزين PolLIDs مع القوائم التي تم الطعن فيها. افتراضيًا، ستتم تهيئة هذه العناصر إلى الصفر. من الفعال أن نعرف أن الإدراج باستخدام polliD zero لا يمثل تحديًا نشطًا بدلاً من الاضطرار إلى تخزين قيمة منطقية منفصلة. لهذا السبب نريد إبقاء الاستطلاع عند الفهرس 0 غير مستخدم.
حسنا، لذلك انتهينا من الشيكات. سنقوم الآن بعمل شيء غير تقليدي ونقدم قائمتنا المرتبطة بشكل مزدوج.
قائمة الروابط المزدوجة
يستخدم عقد التصويت PLC قوائم مرتبطة بشكل مزدوج لتتبع استطلاعات الرأي التي يمتلك المستخدمون رموزًا فيها. يعد تنفيذ قائمة مرتبطة بشكل مزدوج مهمة منزلية في السنة الأولى لتخصصات علوم الكمبيوتر، ولكن تنفيذ واحدة في Solidity يعد أكثر صعوبة من القيام بذلك، على سبيل المثال، في Python، لأن Solidity هي لغة منخفضة المستوى. القائمة ذات الارتباط المزدوج هي التي تتيح لنا إصدار أكبر عدد ممكن من الرموز المميزة بكفاءة للمستخدمين الذين يرغبون في سحب حقوق التصويت الخاصة بهم ولديهم رموز تم الالتزام بها في استطلاعات متعددة أقل من إجمالي الرموز التي لديهم حقوق التصويت لها.
library DLL {
struct Node {
uint next;
uint prev;
}
struct Data {
mapping(uint => Node) dll;
}
function getNext(Data storage self, uint curr) returns (uint) {
return self.dll[curr].next;
}
function getPrev(Data storage self, uint curr) returns (uint) {
return self.dll[curr].prev;
}
function insert(Data storage self, uint prev, uint curr, uint next) {
self.dll[curr].prev = prev;
self.dll[curr].next = next;
self.dll[prev].next = curr;
self.dll[next].prev = curr;
}
function remove(Data storage self, uint curr) {
uint next = getNext(self, curr);
uint prev = getPrev(self, curr);
self.dll[next].prev = prev;
self.dll[prev].next = next;
self.dll[curr].next = curr;
self.dll[curr].prev = curr;
}
}
أولاً، التأمل في مفهوم أساسي: يمكن استخدام التعيينات لمعالجة الذاكرة مباشرة في Solidity، مثل المؤشرات في C. هذا هو المفتاح لبناء هياكل بيانات معقدة في Solidity.
في تصويت PLCR، توجد قائمة واحدة مرتبطة بشكل مزدوج لكل مستخدم، تمت معالجتها باستخدام عنوان msg.sender الخاص بالمستخدم. تتوافق العقدة في ملف DLL الخاص بالمستخدم مع PollID. يتم دائمًا فرز ملفات DLL حسب عدد الرموز التي ارتكبها المستخدم للاستطلاعات المقابلة للعقد. يتم تخزين البيانات بشكل منفصل عن العقد نفسها وتتم معالجتها باستخدام التسلسل المجزأ لعنوان المستخدم ومعرف العقدة للفهرسة في رسم خرائط ذات مفاتيح سلسلة للأعداد الصحيحة يسمى متجر السمات.
library AttributeStore {
struct Data {
mapping(bytes32 => uint) store;
}
function getAttribute(Data storage self, bytes32 UUID, string attrName) returns (uint) {
bytes32 key = sha3(UUID, attrName);
return self.store[key];
}
function attachAttribute(Data storage self, bytes32 UUID, string attrName, uint attrVal) {
bytes32 key = sha3(UUID, attrName);
self.store[key] = attrVal;
}
}
للتوضيح: عنوان المستخدم يعالج ملف DLL معين. يعالج NodeId عقدة معينة في DLL، ولكن نظرًا لأن NodeIDs تتوافق مع PolLIDs، يمكن أن تحتوي ملفات DLL المتعددة على عقد ذات NodeIDs متطابقة. من خلال ربط عنوان مستخدم بـ nodeID والتجزئة، نحصل على موقع فريد في الذاكرة حيث يمكننا البحث عن البيانات. السبب وراء قيامنا بتخزين البيانات بشكل منفصل عن العقد هو أننا نحتاج فقط إلى تخزين تعيين واحد لجميع العقد، بدلاً من تعيين واحد لكل عقدة. يستخدم إعلان التعيين، حتى إذا كان التعيين فارغًا، التخزين.
هذه هي النظرة العامة الأساسية لكيفية عمل DLL. دعنا نعود إلى كيفية عمل CommitVote ونرى كيف نستخدم DLL عمليًا.
إجراء التصويت، تابع
على الخط 102 قمنا بتعيين وحدة تسمى NextPollID إلى نتيجة طريقة getNext، وهي طريقة لـ DLL الخاص بـ msg.sender، والتي تأخذ حجة prevPollID. prevPolliD هو معرف العقدة التي ستكون العقدة السابقة للعقدة الجديدة التي نقوم بإدراجها. nextPollID بعد ذلك ستكون العقدة التالية للعقدة الجديدة التي نقوم بإدراجها، لأنها ستتبع prevPollID وبالتالي قبل NextPolliD.
function commitVote(uint pollID, bytes32 secretHash, uint numTokens, uint prevPollID) external {
require(commitPeriodActive(pollID));
require(voteTokenBalance[msg.sender] >= numTokens); // prevent user from overspending
require(pollID != 0); // prevent user from committing to zero node placerholder
uint nextPollID = dllMap[msg.sender].getNext(prevPollID);
require(validPosition(prevPollID, nextPollID, msg.sender, numTokens));
dllMap[msg.sender].insert(prevPollID, pollID, nextPollID);
bytes32 UUID = attrUUID(msg.sender, pollID);
store.attachAttribute(UUID, "numTokens", numTokens);
store.attachAttribute(UUID, "commitHash", uint(secretHash));
}
لماذا نجعل المستخدم يقدم قيمة PrevPollID؟ نحن استطاع ابحث في القائمة للعثور على prevPollID الصحيح، ولكن في القوائم الطويلة جدًا، قد نكسر حد الغاز عند القيام بذلك. من الأفضل السماح للمستخدم بالقيام بذلك خارج السلسلة في مكالمة ثم توفيرها حتى يمكن تشغيل المعاملة في وقت ثابت.
السطر 104 يتحقق، في وقت ثابت، مما إذا كان prevPollID المقدم صالحًا بالنظر إلى عدد الرموز التي يتم الالتزام بها في الاستطلاع الجديد. يجب ألا يكون من الممكن أن تصبح القائمة غير مصنفة. في حالة اجتياز الشيك، قم بتشغيل السطر 105 نقوم بإدخال العقدة الجديدة!
لقد أدخلنا عقدة، ولكن لاحظ أننا لم نقم بإضافة البيانات فعليًا حتى الآن. على السطر 107 سنستخدم المساعد وظيفة في TruID لإنشاء معرف فريد عالميًا جديد وهو تجزئة sha3 لعنوان المستخدم و NodeID. أخيرا نقوم بتخزين عدد الرموز المخصصة للاستطلاع وSecretHash لتصويتنا.
واو، كان ذلك صعبًا!
الكشف عن التصويت
الآن بعد أن عرفنا كيف يعمل الالتزام، سيكون الكشف في الواقع سهلًا نسبيًا. لقد تعلمنا كل الأشياء الصعبة في هذه المرحلة، لذلك دعونا نلقي نظرة على كشف التصويت! يأخذ RevealVote ثلاث حجج:
هوية الاستطلاع: تم الكشف عن رقم الاستطلاع الخاص بالاستطلاع.
خيار التصويت: اختيار المستخدم في الاستطلاع. 1 هو التصويت لصالح، 0 هو التصويت ضد.
ملح: الرقم العشوائي المتسلسل إلى VoteOption لإنتاج SecretHash من CommitVote.
لقد كنت في الجوار في هذه المرحلة، حتى تعرف ما سنفعله أولاً: فحوصات. سوف نتحقق من أن فترة الكشف عن الاستطلاع المقدمة نشطة، وأن المستخدم لم يكشف عنها بالفعل لهذا الاستطلاع، وسنتأكد من تطابق خيار التصويت المقدم والملح فعليًا مع SecretHash الذي تم تنفيذه عن طريق حساب تجزئة sha3 للعنصرين ومقارنة النتيجة بـ SecretHash المخزن في ملف DLL الخاص بالمستخدم.
function revealVote(uint pollID, uint voteOption, uint salt) external {
// Make sure the reveal period is active
require(revealPeriodActive(pollID));
require(!hasBeenRevealed(msg.sender, pollID)); // prevent user from revealing multiple times
require(sha3(voteOption, salt) == getCommitHash(msg.sender, pollID)); // compare resultant hash from inputs to original commitHash
uint numTokens = getNumTokens(msg.sender, pollID);
if (voteOption == 1) // apply numTokens to appropriate poll choice
pollMap[pollID].votesFor += numTokens;
else
pollMap[pollID].votesAgainst += numTokens;
dllMap[msg.sender].remove(pollID); // remove the node referring to this vote upon reveal
}
على السطر 140 سنحصل على عدد الرموز التي التزم بها المستخدم لهذا الاستطلاع. ثم بناءً على اختيار المستخدم في الاستطلاع، سنقوم تحديث الأصوات العالمية للاستطلاع لصالح أو التصويت ضده حصيلة.
أخيرًا السطر 147 سنقوم قم بإزالة العقدة لهذا الاستطلاع في ملف DLL الخاص بالمستخدم.
واو، كان ذلك سهلاً!
من فاز؟
حسنًا، لقد انتهت فترة الكشف عن الاستطلاع ونريد أن نعرف كيف سارت الأمور. هذا بسيط جدًا. ال وظيفة iSpased يأخذ استطلاع الرأي كحجة و يعود صحيحًا إذا عدد الأصوات التي استوفت شرط النصاب القانوني بالنسبة إلى الأصوات المعارضة. في وقت واحد، لا يتم اجتياز الاستطلاع. لاحظ أن النصاب القانوني المطلوب ليس نصاب إجمالي الرموز التي ضرورة التصويت، إنه فقط النصاب القانوني للرموز التي لم تصويت.
سحب الرموز الخاصة بنا
هذا هو الجزء الذي عملنا بجد من أجله. لنفترض أن لدينا حقوق التصويت لـ 10 رموز، ولكن سبعة منها فقط ملتزمة حاليًا في استطلاعات الرأي. استخدام سحب حقوق التصويت يجب أن نكون قادرين على الحصول على ثلاثة. rawrfVotingRights يأخذ كحجة عدد الرموز التي نرغب في سحبها.
function withdrawVotingRights(uint numTokens) external {
uint availableTokens = voteTokenBalance[msg.sender] - getLockedTokens(msg.sender);
require(availableTokens >= numTokens);
require(token.transfer(msg.sender, numTokens));
voteTokenBalance[msg.sender] -= numTokens;
}
يحدث السحر على السطر 69. نقوم بحساب الرموز المتاحة للسحب عن طريق طرح نتيجة voteTokenBalance للمستخدم وظيفة المساعد getLockedTokens أخذ عنوان المستخدم كحجة. هذا يلف آخر وظيفة المساعد getNumTokens والذي يأخذ كحجج عنوان المستخدم الخاص بنا ونتيجة آخر وظيفة المساعد getLastNode.
الآن فكر في هذا: يتم دائمًا فرز ملف DLL الخاص بنا حسب عدد الرموز المميزة الملتزم بها في الاستطلاع. يقوم getLastNode بالفهرسة إلى العقدة صفر (العقدة الجذرية) لملف DLL الخاص بالمستخدم ويحصل على العقدة السابقة، والتي يجب أن تكون الاستطلاع الذي يمتلك المستخدم فيه أكبر عدد من الرموز المميزة المقفلة. بمجرد طرح هذا الرقم من إجمالي عدد الرموز التي قام المستخدم بتحميلها في عقد PLCR، فإننا نعرف عدد الرموز التي يمكنه سحبها.
كل العمل الشاق الذي قمنا به مع DLL، قمنا به من أجل ذلك.
مرة أخرى في سحب حقوق التصويت، كل شيء هو مسك الدفاتر: نرسل الرموز مرة أخرى إلى المستخدم ونخفض رصيد VoteTokenBalance الخاص به.
وهذا كل شيء!
نأمل أن تكون هذه الإرشادات مفيدة لفهمك لكيفية عمل عقد التصويت PLCR الخاص بنا! لا تتردد في استخدامه بنفسك لجميع احتياجات التصويت على الرمز المميز!
الدعائم الضخمة للمتدربين في ConsenSys 2017 يورك رودس، جيم أوزر و أسبين بالاتنييك للعمل الجاد هذا الصيف لجعل حلم التصويت في PLCR حقيقة.