Dagger-Hashimoto
Letzte Aktualisierung der Seite: 11. April 2024
Dagger-Hashimoto war die ursprüngliche Forschungsimplementierung und Spezifikation für den Mining-Algorithmus von Ethereum. Dagger-Hashimoto wurde durch Ethash ersetzt. Das Mining wurde beim Merge am 15. September 2022 vollständig abgeschaltet. Seitdem wird Ethereum stattdessen durch einen Proof-of-Stake-Mechanismus gesichert. Diese Seite ist von historischem Interesse – die hierin enthaltenen Informationen sind für das Post-Merge-Ethereum nicht mehr relevant.
Voraussetzungen
Um diese Seite besser zu verstehen, empfehlen wir Ihnen, sich zunächst über den Proof-of-Work-Konsens, Mining und Mining-Algorithmen zu informieren.
Dagger-Hashimoto
Dagger-Hashimoto zielt darauf ab, zwei Ziele zu erfüllen:
- ASIC-Resistenz: Der Nutzen aus der Entwicklung spezialisierter Hardware für den Algorithmus sollte so gering wie möglich sein.
- Verifizierbarkeit durch Light Clients: Ein Block sollte von einem Light Client effizient verifizierbar sein.
Mit einer zusätzlichen Modifikation spezifizieren wir auch, wie ein drittes Ziel auf Wunsch erfüllt werden kann, jedoch auf Kosten zusätzlicher Komplexität:
Speicherung der gesamten Chain: Das Mining sollte die Speicherung des gesamten Blockchain-Zustands erfordern (aufgrund der unregelmäßigen Struktur des Ethereum-Zustands-Tries gehen wir davon aus, dass ein gewisses Pruning möglich sein wird, insbesondere bei einigen häufig verwendeten Verträgen, aber wir möchten dies minimieren).
DAG-Generierung
Der Code für den Algorithmus wird unten in Python definiert. Zuerst geben wir encode_int an, um vorzeichenlose Ganzzahlen (unsigned ints) einer bestimmten Genauigkeit in Strings umzuwandeln. Die Umkehrung ist ebenfalls angegeben:
1NUM_BITS = 51223def encode_int(x):4 "Encode an integer x as a string of 64 characters using a big-endian scheme"5 o = ''6 for _ in range(NUM_BITS / 8):7 o = chr(x % 256) + o8 x //= 2569 return o1011def decode_int(s):12 "Unencode an integer x from a string using a big-endian scheme"13 x = 014 for c in s:15 x *= 25616 x += ord(c)17 return xAlle anzeigenWir nehmen im Folgenden an, dass sha3 eine Funktion ist, die eine Ganzzahl annimmt und eine Ganzzahl ausgibt, und dbl_sha3 eine Double-SHA3-Funktion ist; wenn Sie diesen Referenzcode in eine Implementierung umwandeln, verwenden Sie:
1from pyethereum import utils2def sha3(x):3 if isinstance(x, (int, long)):4 x = encode_int(x)5 return decode_int(utils.sha3(x))67def dbl_sha3(x):8 if isinstance(x, (int, long)):9 x = encode_int(x)10 return decode_int(utils.sha3(utils.sha3(x)))Alle anzeigenParameter
Die für den Algorithmus verwendeten Parameter sind:
1SAFE_PRIME_512 = 2**512 - 38117 # Größte sichere Primzahl kleiner als 2**51223params = {4 "n": 4000055296 * 8 // NUM_BITS, # Größe des Datensatzes (4 Gigabyte); MUSS EIN VIELFACHES VON 65536 SEIN5 "n_inc": 65536, # Erhöhung des Wertes von n pro Periode; MUSS EIN VIELFACHES VON 65536 SEIN6 # mit epochtime=20000 ergibt sich ein Wachstum von 882 MB pro Jahr7 "cache_size": 2500, # Größe des Caches des Light-Clients (kann vom Light-8 # Client gewählt werden; nicht Teil der Algo-Spezifikation)9 "diff": 2**14, # Schwierigkeit (wird während der Blockauswertung angepasst)10 "epochtime": 100000, # Länge einer Epoche in Blöcken (wie oft der Datensatz aktualisiert wird)11 "k": 1, # Anzahl der Elternknoten eines Knotens12 "w": w, # Wird für modulares Exponentiations-Hashing verwendet13 "accesses": 200, # Anzahl der Datensatzzugriffe während Hashimoto14 "P": SAFE_PRIME_512 # Sichere Primzahl für Hashing und Zufallszahlengenerierung15}Alle anzeigenIn diesem Fall ist P eine Primzahl, die so gewählt wurde, dass log₂(P) nur geringfügig kleiner als 512 ist, was den 512 Bits entspricht, die wir zur Darstellung unserer Zahlen verwendet haben. Beachten Sie, dass nur die zweite Hälfte des DAG tatsächlich gespeichert werden muss, sodass der De-facto-RAM-Bedarf bei 1 GB beginnt und um 441 MB pro Jahr wächst.
Dagger-Graphenerstellung
Das Primitiv zur Erstellung des Dagger-Graphen ist wie folgt definiert:
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) % P7 for _ in range(params["k"]):8 x ^= o[x % i]9 o.append(pow(x, params["w"], P))10 return oAlle anzeigenIm Wesentlichen beginnt es einen Graphen als einzelnen Knoten, sha3(seed), und fügt von dort aus sequenziell weitere Knoten basierend auf zufälligen vorherigen Knoten hinzu. Wenn ein neuer Knoten erstellt wird, wird eine modulare Potenz des Seeds berechnet, um zufällig einige Indizes kleiner als i auszuwählen (unter Verwendung von x % i oben), und die Werte der Knoten an diesen Indizes werden in einer Berechnung verwendet, um einen neuen Wert für x zu generieren, der dann in eine kleine Proof-of-Work-Funktion (basierend auf XOR) eingespeist wird, um letztendlich den Wert des Graphen am Index i zu generieren. Der Grund für dieses spezielle Design ist es, einen sequenziellen Zugriff auf den DAG zu erzwingen; der nächste Wert des DAG, auf den zugegriffen wird, kann erst bestimmt werden, wenn der aktuelle Wert bekannt ist. Schließlich hasht die modulare Exponentiation das Ergebnis weiter.
Dieser Algorithmus stützt sich auf mehrere Ergebnisse aus der Zahlentheorie. Siehe den Anhang unten für eine Diskussion.
Light-Client-Auswertung
Die obige Graphenkonstruktion zielt darauf ab, dass jeder Knoten im Graphen rekonstruiert werden kann, indem ein Teilbaum aus nur einer kleinen Anzahl von Knoten berechnet wird und nur eine geringe Menge an Hilfsspeicher benötigt wird. Beachten Sie, dass bei k=1 der Teilbaum nur eine Kette von Werten ist, die bis zum ersten Element im DAG reicht.
Die Light-Client-Berechnungsfunktion für den DAG funktioniert wie folgt:
1def quick_calc(params, seed, p):2 w, P = params["w"], params["P"]3 cache = {}45 def quick_calc_cached(p):6 if p in cache:7 pass8 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]1617 return quick_calc_cached(p)Alle anzeigenIm Wesentlichen handelt es sich einfach um eine Umschreibung des obigen Algorithmus, die die Schleife zur Berechnung der Werte für den gesamten DAG entfernt und die frühere Knotensuche durch einen rekursiven Aufruf oder eine Cache-Suche ersetzt. Beachten Sie, dass für k=1 der Cache unnötig ist, obwohl eine weitere Optimierung tatsächlich die ersten paar tausend Werte des DAG vorberechnet und diese als statischen Cache für Berechnungen bereithält; siehe den Anhang für eine Code-Implementierung davon.
Doppelter Puffer von DAGs
In einem vollständigen Client wird ein doppelter Puffer (Double Buffer) (opens in a new tab) von 2 DAGs verwendet, die durch die obige Formel erzeugt wurden. Die Idee ist, dass DAGs alle epochtime Anzahl von Blöcken gemäß den obigen Parametern erzeugt werden. Anstatt dass der Client den zuletzt erzeugten DAG verwendet, verwendet er den vorherigen. Der Vorteil davon ist, dass die DAGs im Laufe der Zeit ersetzt werden können, ohne dass ein Schritt integriert werden muss, bei dem Miner plötzlich alle Daten neu berechnen müssen. Andernfalls besteht die Gefahr einer abrupten vorübergehenden Verlangsamung der Chain-Verarbeitung in regelmäßigen Abständen und einer dramatisch zunehmenden Zentralisierung. Somit bestehen Risiken für einen 51-%-Angriff innerhalb dieser wenigen Minuten, bevor alle Daten neu berechnet sind.
Der Algorithmus, der verwendet wird, um den Satz von DAGs zu generieren, die zur Berechnung der Arbeit für einen Block verwendet werden, lautet wie folgt:
1def get_prevhash(n):2 from pyethereum.blocks import GENESIS_PREVHASH3 from pyethereum import chain_manager4 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)910def 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 seedset1718def get_dagsize(params, block):19 return params["n"] + (block.number // params["epochtime"]) * params["n_inc"]2021def get_daggerset(params, block):22 dagsz = get_dagsize(params, block)23 seedset = get_seedset(params, block)24 if seedset["front_hash"] <= 0:25 # Kein Back-Buffer möglich, erstelle einfach einen Front-Buffer26 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"]}}Alle anzeigenHashimoto
Die Idee hinter dem ursprünglichen Hashimoto ist es, die Blockchain als Datensatz zu verwenden und eine Berechnung durchzuführen, die N Indizes aus der Blockchain auswählt, die Transaktionen an diesen Indizes sammelt, ein XOR dieser Daten durchführt und den Hash des Ergebnisses zurückgibt. Der ursprüngliche Algorithmus von Thaddeus Dryja, der aus Konsistenzgründen in Python übersetzt wurde, lautet wie folgt:
1def orig_hashimoto(prev_hash, merkle_root, list_of_transactions, nonce):2 hash_output_A = sha256(prev_hash + merkle_root + nonce)3 txid_mix = 04 for i in range(64):5 shifted_A = hash_output_A >> i6 transaction = shifted_A % len(list_of_transactions)7 txid_mix ^= list_of_transactions[transaction] << i8 return txid_mix ^ (nonce << 192)Obwohl Hashimoto als RAM-hart gilt, stützt es sich leider auf 256-Bit-Arithmetik, was einen erheblichen Rechenaufwand bedeutet. Dagger-Hashimoto verwendet jedoch nur die niedrigstwertigen 64 Bits bei der Indizierung seines Datensatzes, um dieses Problem zu beheben.
1def hashimoto(dag, dagsize, params, header, nonce):2 m = dagsize / 23 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)Die Verwendung von Double-SHA3 ermöglicht eine Form der datenlosen, nahezu sofortigen Vorab-Verifizierung, bei der nur überprüft wird, ob ein korrekter Zwischenwert bereitgestellt wurde. Diese äußere Schicht des Proof-of-Work ist sehr ASIC-freundlich und ziemlich schwach, existiert aber, um DDoS noch schwieriger zu machen, da diese geringe Menge an Arbeit geleistet werden muss, um einen Block zu produzieren, der nicht sofort abgelehnt wird. Hier ist die Light-Client-Version:
1def quick_hashimoto(seed, dagsize, params, header, nonce):2 m = dagsize // 23 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)Mining und Verifizierung
Lassen Sie uns nun alles im Mining-Algorithmus zusammenfassen:
1def mine(daggerset, params, block):2 from random import randint3 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 break9 nonce += 110 if nonce >= 2**64:11 nonce = 012 return nonceAlle anzeigenHier ist der Verifizierungsalgorithmus:
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**256Light-Client-freundliche Verifizierung:
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**256Beachten Sie auch, dass Dagger-Hashimoto zusätzliche Anforderungen an den Block-Header stellt:
- Damit die zweischichtige Verifizierung funktioniert, muss ein Block-Header sowohl die Nonce als auch den mittleren Wert vor SHA3 enthalten.
- Irgendwo muss ein Block-Header den SHA3 des aktuellen Seed-Sets speichern.
Weiterführende Literatur
Kennen Sie eine Community-Ressource, die Ihnen geholfen hat? Bearbeiten Sie diese Seite und fügen Sie sie hinzu!
Anhang
Wie oben angemerkt, stützt sich der für die DAG-Generierung verwendete RNG (Zufallszahlengenerator) auf einige Ergebnisse aus der Zahlentheorie. Erstens stellen wir sicher, dass der Lehmer-RNG, der die Basis für die Variable picker bildet, eine große Periode hat. Zweitens zeigen wir, dass pow(x,3,P) x nicht auf 1 oder P-1 abbildet, vorausgesetzt, x ∈ [2,P-2] zu Beginn. Schließlich zeigen wir, dass pow(x,3,P) eine niedrige Kollisionsrate aufweist, wenn es als Hash-Funktion behandelt wird.
Lehmer-Zufallszahlengenerator
Während die Funktion produce_dag keine unverzerrten Zufallszahlen erzeugen muss, besteht eine potenzielle Bedrohung darin, dass seed**i % P nur eine Handvoll Werte annimmt. Dies könnte Minern, die das Muster erkennen, einen Vorteil gegenüber denjenigen verschaffen, die dies nicht tun.
Um dies zu vermeiden, wird auf ein Ergebnis aus der Zahlentheorie zurückgegriffen. Eine sichere Primzahl (Safe Prime) (opens in a new tab) ist definiert als eine Primzahl P, bei der (P-1)/2 ebenfalls eine Primzahl ist. Die Ordnung eines Elements x der multiplikativen Gruppe (opens in a new tab) ℤ/nℤ ist definiert als das minimale m, sodass
xᵐ mod P ≡ 1Unter Berücksichtigung dieser Definitionen haben wir:
Beobachtung 1. Sei
xein Element der multiplikativen Gruppeℤ/Pℤfür eine sichere PrimzahlP. Wennx mod P ≠ 1 mod Pundx mod P ≠ P-1 mod P, dann ist die Ordnung vonxentwederP-1oder(P-1)/2.
Beweis. Da P eine sichere Primzahl ist, gilt nach dem [Satz von Lagrange][lagrange], dass die Ordnung von x entweder 1, 2, (P-1)/2 oder P-1 ist.
Die Ordnung von x kann nicht 1 sein, da nach dem kleinen Satz von Fermat gilt:
xP-1 mod P ≡ 1
Daher muss x eine multiplikative Identität von ℤ/nℤ sein, die eindeutig ist. Da wir angenommen haben, dass x ≠ 1, ist dies nicht möglich.
Die Ordnung von x kann nicht 2 sein, es sei denn, x = P-1, da dies verletzen würde, dass P eine Primzahl ist.
Aus der obigen Aussage können wir erkennen, dass die Iteration von (picker * init) % P eine Zykluslänge von mindestens (P-1)/2 haben wird. Dies liegt daran, dass wir P als eine sichere Primzahl ausgewählt haben, die ungefähr einer höheren Zweierpotenz entspricht, und init im Intervall [2,2**256+1] liegt. Angesichts der Größe von P sollten wir niemals einen Zyklus aus der modularen Exponentiation erwarten.
Wenn wir die erste Zelle im DAG zuweisen (die Variable mit der Bezeichnung init), berechnen wir pow(sha3(seed) + 2, 3, P). Auf den ersten Blick garantiert dies nicht, dass das Ergebnis weder 1 noch P-1 ist. Da P-1 jedoch eine sichere Primzahl ist, haben wir die folgende zusätzliche Sicherheit, die ein Korollar aus Beobachtung 1 ist:
Beobachtung 2. Sei
xein Element der multiplikativen Gruppeℤ/Pℤfür eine sichere PrimzahlP, und seiweine natürliche Zahl. Wennx mod P ≠ 1 mod Pundx mod P ≠ P-1 mod P, sowiew mod P ≠ P-1 mod Pundw mod P ≠ 0 mod P, dann istxʷ mod P ≠ 1 mod Pundxʷ mod P ≠ P-1 mod P
Modulare Exponentiation als Hash-Funktion
Für bestimmte Werte von P und w kann die Funktion pow(x, w, P) viele Kollisionen aufweisen. Zum Beispiel nimmt pow(x,9,19) nur die Werte {1,18} an.
Da P eine Primzahl ist, kann ein geeignetes w für eine modulare Exponentiations-Hash-Funktion unter Verwendung des folgenden Ergebnisses ausgewählt werden:
Beobachtung 3. Sei
Peine Primzahl;wundP-1sind teilerfremd, wenn und nur wenn für alleaundbinℤ/Pℤgilt:aʷ mod P ≡ bʷ mod Pwenn und nur wenna mod P ≡ b mod P
Da P eine Primzahl ist und w teilerfremd zu P-1 ist, gilt somit |{pow(x, w, P) : x ∈ ℤ}| = P, was impliziert, dass die Hash-Funktion die minimal mögliche Kollisionsrate aufweist.
In dem speziellen Fall, dass P eine sichere Primzahl ist, wie wir sie ausgewählt haben, hat P-1 nur die Faktoren 1, 2, (P-1)/2 und P-1. Da P > 7, wissen wir, dass 3 teilerfremd zu P-1 ist, daher erfüllt w=3 die obige Aussage.
Effizienterer Cache-basierter Auswertungsalgorithmus
1def quick_calc(params, seed, p):2 cache = produce_dag(params, seed, params["cache_size"])3 return quick_calc_cached(cache, params, p)45def 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)1415def 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)1819def quick_hashimoto_cached(cache, dagsize, params, header, nonce):20 m = dagsize // 221 mask = 2**64 - 122 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)Alle anzeigen