تخطٍ إلى المحتوى الرئيسي
Change page

خنجر هاشيموتو

آخر تحديث للصفحة: 11 أبريل 2024

كان Dagger-Hashimoto هو التنفيذ البحثي الأصلي والمواصفات الخاصة بخوارزمية تعدين إيثريوم. تم استبدال Dagger-Hashimoto بـ إيثاش. تم إيقاف التنقيب بالكامل عند الدمج في 15 سبتمبر 2022. منذ ذلك الحين، تم تأمين إيثريوم باستخدام آلية إثبات الحصة بدلًا من ذلك. هذه الصفحة مخصصة للاهتمام التاريخي - المعلومات الواردة هنا لم تعد ذات صلة بـ إيثريوم بعد الدمج.

المتطلبات الأساسية

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

Dagger-Hashimoto

يهدف Dagger-Hashimoto إلى تحقيق هدفين:

  1. مقاومة ASIC: يجب أن تكون الفائدة من إنشاء أجهزة متخصصة للخوارزمية صغيرة قدر الإمكان
  2. إمكانية التحقق لدى العميل الخفيف: يجب أن تكون الكتلة قابلة للتحقق بكفاءة من قبل عميل خفيف.

من خلال تعديل إضافي، نحدد أيضًا كيفية تحقيق الهدف الثالث إذا رغبنا في ذلك، ولكن على حساب التعقيد الإضافي:

تخزين السلسلة الكاملة: يجب أن يتطلب التنقيب تخزين حالة البلوكتشين الكاملة (نظرًا للهيكل غير المنتظم لـ إيثريوم state trie، نتوقع أن يكون بعض التقليم ممكنًا، خاصة لبعض العقود المستخدمة بشكل متكرر، لكننا نريد تقليل ذلك).

إنشاء DAG

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

1NUM_BITS = 512
2
3def encode_int(x):
4 "ترميز عدد صحيح x كسلسلة من 64 حرفًا باستخدام مخطط big-endian"
5 o = ''
6 for _ in range(NUM_BITS / 8):
7 o = chr(x % 256) + o
8 x //= 256
9 return o
10
11def decode_int(s):
12 "فك ترميز عدد صحيح x من سلسلة باستخدام مخطط big-endian"
13 x = 0
14 for c in s:
15 x *= 256
16 x += ord(c)
17 return x
إظهار الكل

نفترض بعد ذلك أن sha3 هي دالة تأخذ عددًا صحيحًا وتُخرج عددًا صحيحًا، وdbl_sha3 هي دالة sha3-مزدوجة؛ إذا كنت تريد تحويل هذا الكود المرجعي إلى تنفيذ، استخدم:

1from pyethereum import utils
2def sha3(x):
3 if isinstance(x, (int, long)):
4 x = encode_int(x)
5 return decode_int(utils.sha3(x))
6
7def dbl_sha3(x):
8 if isinstance(x, (int, long)):
9 x = encode_int(x)
10 return decode_int(utils.sha3(utils.sha3(x)))
إظهار الكل

المعلمات

المعلمات المستخدمة في الخوارزمية هي:

1SAFE_PRIME_512 = 2**512 - 38117 # أكبر عدد أولي آمن أقل من 2**512
2
3params = {
4 "n": 4000055296 * 8 // NUM_BITS, # حجم مجموعة البيانات (4 جيجابايت)؛ يجب أن يكون من مضاعفات 65536
5 "n_inc": 65536, # الزيادة في قيمة n لكل فترة؛ يجب أن يكون من مضاعفات 65536
6 # مع epochtime=20000 يعطي نموًا قدره 882 ميجابايت سنويًا
7 "cache_size": 2500, # حجم ذاكرة التخزين المؤقت للعميل الخفيف (يمكن اختياره من قبل العميل الخفيف؛ ليس جزءًا من مواصفات الخوارزمية)
8 "diff": 2**14, # الصعوبة (يتم تعديلها أثناء تقييم الكتلة)
9 "epochtime": 100000, # طول الحقبة بالكتل (كم مرة يتم تحديث مجموعة البيانات)
10 "k": 1, # عدد آباء العقدة
11 "w": w, # يُستخدم لتجزئة الأُسِّي المعياري
12 "accesses": 200, # عدد مرات الوصول إلى مجموعة البيانات أثناء hashimoto
13 "P": SAFE_PRIME_512 # عدد أولي آمن للتجزئة وإنشاء الأرقام العشوائية
14}
إظهار الكل

P في هذه الحالة هو عدد أولي تم اختياره بحيث يكون log₂(P) أقل بقليل من 512، وهو ما يتوافق مع 512 بت التي نستخدمها لتمثيل أرقامنا. لاحظ أن النصف الأخير فقط من DAG هو الذي يحتاج فعليًا إلى التخزين، وبالتالي فإن متطلبات ذاكرة الوصول العشوائي الفعلية تبدأ من 1 جيجابايت وتنمو بمقدار 441 ميجابايت سنويًا.

بناء مخطط Dagger

يتم تعريف بدائية بناء الرسم البياني للخنجر على النحو التالي:

1def produce_dag(params, seed, length):
2 P = params["P"]
3 picker = init = pow(sha3(seed), params["w"], P)
4 o = [init]
5 for i in range(1, length):
6 x = picker = (picker * init) % P
7 for _ in range(params["k"]):
8 x ^= o[x % i]
9 o.append(pow(x, params["w"], P))
10 return o
إظهار الكل

بشكل أساسي، يبدأ المخطط بعقدة واحدة، sha3(seed)، ومن هناك يبدأ في إضافة عُقد أخرى بالتتابع استنادًا إلى عُقد سابقة عشوائية. عند إنشاء عقدة جديدة، يتم حساب قوة معيارية للبذرة لاختيار بعض الفهارس الأقل من i بشكل عشوائي (باستخدام x % i أعلاه)، وتُستخدم قيم العُقد في تلك الفهارس في عملية حسابية لتوليد قيمة جديدة لـ x، والتي يتم بعد ذلك إدخالها في دالة إثبات عمل صغيرة (تعتمد على XOR) لتوليد قيمة المخطط في النهاية عند الفهرس i. إن الأساس المنطقي وراء هذا التصميم المحدد هو فرض الوصول المتسلسل إلى DAG؛ ولا يمكن تحديد القيمة التالية لـ DAG التي سيتم الوصول إليها حتى يتم معرفة القيمة الحالية. أخيرًا، تقوم الأسس المعيارية بتجزئه النتيجة بشكل أكبر.

تعتمد هذه الخوارزمية على العديد من النتائج المستمدة من نظرية الأعداد. انظر الملحق أدناه للمناقشة.

تقييم العميل الخفيف

يهدف إنشاء الرسم البياني أعلاه إلى السماح بإعادة بناء كل عقدة في الرسم البياني عن طريق حساب شجرة فرعية من عدد صغير فقط من العقد وتتطلب قدرًا صغيرًا فقط من الذاكرة المساعدة. لاحظ أنه مع k=1، تكون الشجرة الفرعية عبارة عن سلسلة من القيم تصل إلى العنصر الأول في DAG فقط.

تعمل وظيفة الحوسبة الخفيفة للعميل لـ DAG على النحو التالي:

1def quick_calc(params, seed, p):
2 w, P = params["w"], params["P"]
3 cache = {}
4
5 def quick_calc_cached(p):
6 if p in cache:
7 pass
8 elif p == 0:
9 cache[p] = pow(sha3(seed), w, P)
10 else:
11 x = pow(sha3(seed), (p + 1) * w, P)
12 for _ in range(params["k"]):
13 x ^= quick_calc_cached(x % p)
14 cache[p] = pow(x, w, P)
15 return cache[p]
16
17 return quick_calc_cached(p)
إظهار الكل

في الأساس، إنه ببساطة إعادة كتابة للخوارزمية المذكورة أعلاه والتي تزيل حلقة حساب القيم لـ DAG بالكامل وتستبدل البحث السابق عن العقدة بمكالمة متكررة أو بحث في ذاكرة التخزين المؤقت. لاحظ أنه بالنسبة لـ k=1، فإن ذاكرة التخزين المؤقت غير ضرورية، على الرغم من أن التحسين الإضافي يقوم في الواقع بحساب أول بضعة آلاف من قيم DAG مسبقًا ويحتفظ بها كذاكرة تخزين مؤقت ثابتة للعمليات الحسابية؛ راجع الملحق للاطلاع على تطبيق الكود لهذا.

المخزن المؤقت المزدوج لـ DAGs

في العميل الكامل، يتم استخدام مخزن مؤقت مزدوج (opens in a new tab) لـ 2 DAGs تم إنتاجهما بواسطة الصيغة أعلاه. الفكرة هي أن DAGs يتم إنتاجها كل epochtime عدد من الكتل وفقًا للمعلمات المذكورة أعلاه. بدلاً من استخدام العميل لأحدث DAG تم إنتاجه، فإنه يستخدم DAG السابق. تكمن فائدة هذا في أنه يسمح باستبدال DAGs بمرور الوقت دون الحاجة إلى دمج خطوة حيث يتعين على عمال المناجم إعادة حساب جميع البيانات فجأة. وإلا، فإن هناك احتمالية لحدوث تباطؤ مؤقت مفاجئ في معالجة السلسلة على فترات منتظمة وزيادة المركزية بشكل كبير. وبالتالي، هناك 51% من مخاطر الهجوم خلال تلك الدقائق القليلة قبل إعادة حساب كافة البيانات.

الخوارزمية المستخدمة لتوليد مجموعة DAGs المستخدمة لحساب العمل للكتلة هي كما يلي:

1def get_prevhash(n):
2 from pyethereum.blocks import GENESIS_PREVHASH
3 from pyethereum import chain_manager
4 if n <= 0:
5 return hash_to_int(GENESIS_PREVHASH)
6 else:
7 prevhash = chain_manager.index.get_block_by_number(n - 1)
8 return decode_int(prevhash)
9
10def get_seedset(params, block):
11 seedset = {}
12 seedset["back_number"] = block.number - (block.number % params["epochtime"])
13 seedset["back_hash"] = get_prevhash(seedset["back_number"])
14 seedset["front_number"] = max(seedset["back_number"] - params["epochtime"], 0)
15 seedset["front_hash"] = get_prevhash(seedset["front_number"])
16 return seedset
17
18def get_dagsize(params, block):
19 return params["n"] + (block.number // params["epochtime"]) * params["n_inc"]
20
21def get_daggerset(params, block):
22 dagsz = get_dagsize(params, block)
23 seedset = get_seedset(params, block)
24 if seedset["front_hash"] <= 0:
25 # لا يمكن وجود مخزن مؤقت خلفي، فقط أنشئ مخزنًا مؤقتًا أماميًا
26 return {"front": {"dag": produce_dag(params, seedset["front_hash"], dagsz),
27 "block_number": 0}}
28 else:
29 return {"front": {"dag": produce_dag(params, seedset["front_hash"], dagsz),
30 "block_number": seedset["front_number"]},
31 "back": {"dag": produce_dag(params, seedset["back_hash"], dagsz),
32 "block_number": seedset["back_number"]}}
إظهار الكل

Hashimoto

الفكرة وراء Hashimoto الأصلي هي استخدام blockchain كمجموعة بيانات، وإجراء عملية حسابية تختار N من المؤشرات من blockchain، وجمع المعاملات في تلك المؤشرات، وتنفيذ XOR لهذه البيانات، وإرجاع تجزئة النتيجة. الخوارزمية الأصلية لثاديوس دريجا، والتي تمت ترجمتها إلى بايثون لتحقيق الاتساق، هي كما يلي:

1def orig_hashimoto(prev_hash, merkle_root, list_of_transactions, nonce):
2 hash_output_A = sha256(prev_hash + merkle_root + nonce)
3 txid_mix = 0
4 for i in range(64):
5 shifted_A = hash_output_A >> i
6 transaction = shifted_A % len(list_of_transactions)
7 txid_mix ^= list_of_transactions[transaction] << i
8 return txid_mix ^ (nonce << 192)

لسوء الحظ، على الرغم من أن Hashimoto يعتبر من الصعب التعامل مع ذاكرة الوصول العشوائي (RAM)، إلا أنه يعتمد على العمليات الحسابية التي تبلغ 256 بت، وهو ما ينطوي على تكلفة حسابية كبيرة. ومع ذلك، يستخدم Dagger-Hashimoto فقط أقل 64 بت أهمية عند فهرسة مجموعة البيانات الخاصة به لمعالجة هذه المشكلة.

1def hashimoto(dag, dagsize, params, header, nonce):
2 m = dagsize / 2
3 mix = sha3(encode_int(nonce) + header)
4 for _ in range(params["accesses"]):
5 mix ^= dag[m + (mix % 2**64) % m]
6 return dbl_sha3(mix)

يسمح استخدام SHA3 المزدوج بنوع من التحقق المسبق شبه الفوري بدون بيانات، والتحقق فقط من توفير قيمة وسيطة صحيحة. تعتبر هذه الطبقة الخارجية من إثبات العمل صديقة للغاية لـ ASIC وهي ضعيفة إلى حد ما، ولكنها موجودة لجعل هجمات DDoS أكثر صعوبة حيث يجب القيام بكمية صغيرة من العمل لإنتاج كتلة لن يتم رفضها على الفور. وهنا إصدار العميل الخفيف:

1def quick_hashimoto(seed, dagsize, params, header, nonce):
2 m = dagsize // 2
3 mix = sha3(nonce + header)
4 for _ in range(params["accesses"]):
5 mix ^= quick_calc(params, seed, m + (mix % 2**64) % m)
6 return dbl_sha3(mix)

التنقيب والتحقق

الآن، دعونا نجمع كل ذلك معًا في خوارزمية التعدين:

1def mine(daggerset, params, block):
2 from random import randint
3 nonce = randint(0, 2**64)
4 while 1:
5 result = hashimoto(daggerset, get_dagsize(params, block),
6 params, decode_int(block.prevhash), nonce)
7 if result * params["diff"] < 2**256:
8 break
9 nonce += 1
10 if nonce >= 2**64:
11 nonce = 0
12 return nonce
إظهار الكل

وهنا خوارزمية التحقق:

1def verify(daggerset, params, block, nonce):
2 result = hashimoto(daggerset, get_dagsize(params, block),
3 params, decode_int(block.prevhash), nonce)
4 return result * params["diff"] < 2**256

التحقق السهل من العميل:

1def light_verify(params, header, nonce):
2 seedset = get_seedset(params, block)
3 result = quick_hashimoto(seedset["front_hash"], get_dagsize(params, block),
4 params, decode_int(block.prevhash), nonce)
5 return result * params["diff"] < 2**256

لاحظ أيضًا أن Dagger-Hashimoto يفرض متطلبات إضافية على رأس الكتلة:

  • لكي يعمل التحقق من طبقتين، يجب أن يحتوي رأس الكتلة على كل من القيمة العشوائية والقيمة الوسطى قبل sha3
  • في مكان ما، يجب أن يخزن رأس الكتلة sha3 لمجموعة البذور الحالية

قراءة إضافية

هل تعرف أحد الموارد المجتمعية التي ساعدتك؟ عدّل هذه الصفحة وأضفه!

الملحق

كما هو مذكور أعلاه، يعتمد RNG المستخدم لتوليد DAG على بعض النتائج من نظرية الأعداد. أولًا، نقدم تأكيدًا بأن Lehmer RNG الذي هو أساس متغير picker له فترة واسعة. ثانيًا، نوضح أن pow(x,3,P) لن يربط x بـ 1 أو P-1 بشرط أن يكون x ∈ [2,P-2] في البداية. أخيرًا، نوضح أن pow(x,3,P) له معدل تصادم منخفض عند التعامل معه كدالة تجزئة.

مولد أرقام Lehmer العشوائي

في حين أن الدالة produce_dag لا تحتاج إلى إنتاج أرقام عشوائية غير متحيزة، إلا أن التهديد المحتمل هو أن seed**i % P لا يأخذ سوى عدد قليل من القيم. قد يوفر هذا ميزة للمنقبين الذين يتعرفون على النمط مقارنة بأولئك الذين لا يتعرفون عليه.

لتجنب ذلك، تم الاستعانة بنتيجة من نظرية الأعداد. يُعرَّف العدد الأولي الآمن (opens in a new tab) بأنه عدد أولي P بحيث يكون (P-1)/2 عددًا أوليًا أيضًا. تُعرَّف رتبة العضو x في المجموعة الضربية (opens in a new tab) ℤ/nℤ بأنها الحد الأدنى m بحيث

xᵐ mod P ≡ 1
بالنظر إلى هذه التعريفات، لدينا:

Observation 1. ليكن x عضوًا في المجموعة الضربية ℤ/Pℤ لعدد أولي آمن P. إذا كان x mod P ≠ 1 mod P وx mod P ≠ P-1 mod P، فإن رتبة x تكون إما P-1 أو (P-1)/2.

إثبات. بما أن P عدد أولي آمن، فإنه بموجب [نظرية لاغرانج][lagrange]، فإن رتبة x هي إما 1 أو 2 أو (P-1)/2 أو P-1.

لا يمكن أن تكون رتبة x هي 1، لأنه بموجب نظرية فيرما الصغرى لدينا:

xP-1 mod P ≡ 1

وبالتالي يجب أن يكون x هوية ضربية لـ ℤ/nℤ، وهي فريدة من نوعها. بما أننا افترضنا أن x ≠ 1، فإن هذا غير ممكن.

لا يمكن أن تكون رتبة x هي 2 إلا إذا كان x = P-1، لأن هذا من شأنه أن ينتهك كون P عددًا أوليًا.

من الاقتراح أعلاه، يمكننا أن ندرك أن تكرار (picker * init) % P سيكون له طول دورة لا يقل عن (P-1)/2. هذا لأننا اخترنا P ليكون عددًا أوليًا آمنًا يساوي تقريبًا قوة أعلى لاثنين، وinit في الفترة [2,2**256+1]. نظرًا لحجم P، لا ينبغي أن نتوقع أبدًا دورة من الأُسِّي المعياري.

عندما نقوم بتعيين الخلية الأولى في DAG (المتغير المسمى init)، نحسب pow(sha3(seed) + 2, 3, P). للوهلة الأولى، هذا لا يضمن أن النتيجة ليست 1 ولا P-1. ومع ذلك، بما أن P-1 هو عدد أولي آمن، فلدينا الضمان الإضافي التالي، وهو نتيجة طبيعية للملاحظة 1:

Observation 2. ليكن x عضوًا في المجموعة الضربية ℤ/Pℤ لعدد أولي آمن P، وليكن w عددًا طبيعيًا. إذا كان x mod P ≠ 1 mod P و x mod P ≠ P-1 mod P، وكذلك w mod P ≠ P-1 mod P و w mod P ≠ 0 mod P، فإن xʷ mod P ≠ 1 mod P و xʷ mod P ≠ P-1 mod P

الأُسِّي المعياري كدالة تجزئة

بالنسبة لقيم معينة من P و w، قد تحتوي الدالة pow(x, w, P) على العديد من التصادمات. على سبيل المثال، pow(x,9,19) يأخذ فقط القيم {1,18}.

بافتراض أن P عدد أولي، يمكن اختيار w مناسب لدالة تجزئة الأُسِّي المعياري باستخدام النتيجة التالية:

Observation 3. ليكن P عددًا أوليًا؛ يكون w و P-1 أوليين نسبيًا إذا وفقط إذا كان لكل a و b في ℤ/Pℤ:

aʷ mod P ≡ bʷ mod P إذا وفقط إذا كان a mod P ≡ b mod P

وبالتالي، بما أن P عدد أولي وw أولي نسبيًا مع P-1، فلدينا |{pow(x, w, P) : x ∈ ℤ}| = P، مما يعني أن دالة التجزئة لديها الحد الأدنى الممكن من معدل التصادم.

في الحالة الخاصة التي يكون فيها P عددًا أوليًا آمنًا كما اخترنا، فإن P-1 له فقط العوامل 1، 2، (P-1)/2 و P-1. بما أن P > 7، فإننا نعلم أن 3 أولي نسبيًا مع P-1، وبالتالي فإن w=3 يفي بالاقتراح أعلاه.

خوارزمية تقييم أكثر كفاءة تعتمد على ذاكرة التخزين المؤقت

1def quick_calc(params, seed, p):
2 cache = produce_dag(params, seed, params["cache_size"])
3 return quick_calc_cached(cache, params, p)
4
5def quick_calc_cached(cache, params, p):
6 P = params["P"]
7 if p < len(cache):
8 return cache[p]
9 else:
10 x = pow(cache[0], p + 1, P)
11 for _ in range(params["k"]):
12 x ^= quick_calc_cached(cache, params, x % p)
13 return pow(x, params["w"], P)
14
15def quick_hashimoto(seed, dagsize, params, header, nonce):
16 cache = produce_dag(params, seed, params["cache_size"])
17 return quick_hashimoto_cached(cache, dagsize, params, header, nonce)
18
19def quick_hashimoto_cached(cache, dagsize, params, header, nonce):
20 m = dagsize // 2
21 mask = 2**64 - 1
22 mix = sha3(encode_int(nonce) + header)
23 for _ in range(params["accesses"]):
24 mix ^= quick_calc_cached(cache, params, m + (mix & mask) % m)
25 return dbl_sha3(mix)
إظهار الكل

هل كانت هذه المقالة مفيدة؟