In diesem Artikel werden wir mit der Implementierung eines konsolenbasierten HD-Wallets in JavaScript beginnen. Da eine Mnemonic (mnemonische Wortfolge) die Grundlage für ein HD-Wallet bildet, befassen wir uns in diesem Artikel intensiv mit der Generierung einer Mnemonic.
Hierzu beschäftigen wir uns zunächst kurz mit den Voraussetzungen für die Implementierung. An dieser Stelle sei vielleicht vorab gesagt, dass sich der Implementierungsteil an Personen richtet, die bereits Erfahrung mit JavaScript und NodeJS haben, sodass nicht jeder einzelne Schritt bis ins letzte Detail erklärt wird. Anschließend beschäftigen wir uns mit der Verwendung der JavaScript-Bibliothek BIP 39 und werden eine Mnemonic hiermit generieren. Im Anschluss werden wir uns tiefer mit der Theorie und der Praxis, welche sich hinter dieser Bibliothek verbirgt, beschäftigen. Hierzu werden wir sowohl die Vorgehensweise des Bitcoin Improvement Proposal 39 theoretisch darstellen als auch in den Code der verwendeten Bibliothek hineinschauen, um zu gucken, wie dieser die zuvor besprochene Theorie implementiert.
Aktueller Stand
Schauen wir uns noch einmal unsere Übersichtsfolie aus dem letzten Artikel an. Wir starten mit einer Mnemonic, weswegen das hier genutzte Bitcoin Improvement Proposal 39 sowie die dazugehörige JavaScript-Bibliothek orange markiert sind. Wir werden im Laufe dieser Artikelreihe immer wieder auf diese Folie zurückkommen, um unseren aktuellen Stand sowie unsere zukünftigen Ziele besser vor Augen zu haben. Sobald wir ein Ziel erreicht haben, werden wir dieses Grün markieren und uns dem nächsten Ziel widmen.
Voraussetzungen für die Implementierung
Zuerst müssen wir überprüfen, ob Node.js installiert ist und der Node.js-Paketmanager npm funktioniert. In Windows öffnet ihr hierzu die PowerShell. Wenn Ihr Linux oder auch das Windows Subsystem für Linux verwendet, könnt ihr diese Anweisungen selbstverständlich auch in der von euch verwendeten Shell eingeben.
Nachfolgend überprüfen wir, welche Node.js-Version installiert ist.
node -v
Ferner überprüfen wir, welche Version des Paketmanagers installiert ist.
npm -v
Sofern ihr hier durch eine entsprechende Fehlermeldung feststellt, dass ihr Node.js nicht installiert habt, holt das einfach schnell nach, indem ihr node.js googlt und den Installationsanweisungen für euer Betriebssystem folgt. Als nächstes legen wir einen neuen Ordner für unser Projekt an und öffnen diesen in unserem Editor Visual Studio Code. Natürlich könnt ihr auch irgendeinen anderen für JavaScript geeigneten Editor verwenden. Sodann öffnen wir uns noch ein Terminal in unserem Ordner und initialisieren unser Node.js-Projekt mit
npm init -y
Hierdurch erstellen wir ein neues Paket, indem wir alle für unser Projekt nötigen Dateien speichern können. Auch sämtliche Bibliotheken, welche wir im Folgenden über NPM installieren, werden hier automatisch abgespeichert.
Den Parameter -y verwenden wir an dieser Stelle, um alle Fragen, die uns NPM bei der Initialisierung eines neuen Projektes stellt mit Default-Werten zu belegen.
So wird bspw. als Name des Projektes automatisch der Name des Ordners übernommen. Das -y kann man aber natürlich auch weglassen und die Fragen einzeln beantworten.
Jetzt enthält unser Projekt eine package.json, in der dann bspw. der Name des Projektes und weitere Informationen zu unserem Projekt aufgelistet sind. Wenn wir gleich den Node.js-Paketmanager dazu nutzen werden, zusätzliche Bibliotheken zu installieren, werden diese hier mit ihrer jeweilig installierten Version unter dem bis jetzt noch nicht vorhandenen Abschnitt “dependencies” erscheinen.
{
"name": "hd-wallet",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
Generierung einer Mnemonic
Nachdem wir erfolgreich die Voraussetzungen für die Implementierung geschaffen haben, werden wir nun auf der Grundlage des BIP 39 unter Verwendung der gleichnamigen JavaScript-Bibliothek eine Mnemonic generieren. Vereinfacht ausgedrückt generieren wir 12 – 24 zufällige Worte. Die eigentliche Generierung mittels der Bibliothek stellt sich relativ einfach dar und ist in wenigen Zeilen Code gelöst. Schwieriger zu verstehen ist jedoch, die Theorie und Praxis, welche uns ermöglicht ein sicheres Mnemonic zu generieren und sich hinter dem BIP 39 verbirgt. Diese werden wir uns später zum besseren Verständnis noch einmal kurz genauer anschauen.
Installation und Verwendung der BIP39-Bibliothek
Wir gucken uns an dieser Stelle einmal kurz die Webseite der NPM-Bibliothek an und bekommen oben rechts auch direkt schon eine Anleitung geliefert, wie wir diese Bibliothek mittels NPM installieren. Wir können einfach oben rechts unter „Installation“ auf „npm i bip39“ klicken, um uns das Kommando zu kopieren oder wir geben es selber in die Konsole / PowerShell im Ordner des jeweiligen Projektes ein:
npm i bip39
Das „i“ ist die Abkürzung für „install“. Es könnte an dieser Stelle jedoch auch ausgeschrieben werden.
Wenn wir nun unsere package.json öffnen, sehen wir direkt, dass die Bibliothek erfolgreich in unserem Projekt eingebunden worden ist und nun verwendet werden kann. Die Bibliothek wurde im Ordner node_modules gespeichert. Um nun das installierte Paket mittels der ES6-Modules Syntax, also der Import-Anweisung, in unserem Projekt importieren zu können, müssen wir noch eine kleine Anpassung in der package.json-Datei vornehmen (Ergänzung in der Zeile 6):
{
"name": "hd-wallet",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"bip39": "^3.1.0"
}
}
Hierbei solltet ihr darauf achten, dass eure Node.js Version größer oder gleich der Version 13 ist. Wir verwenden im Rahmen dieses Artikels die LTS-Version 18.13.
Als nächstes erstellen wir eine neue Datei in unserem Projekt, welche wir ethWallet.js nennen und importieren uns hier aus dem zuvor installierten Paket die Funktion generateMnemonic (Zeile 1):
import {generateMnemonic} from 'bip39';
const mnemonic = generateMnemonic();
console.log(mnemonic);
Zur Verwendung der Funktion können wir diese einfach aufrufen und das Ergebnis in einer Konstante speichern, welche wir uns mittels console.log ausgeben lassen (Zeile 2 und 3).
Führen wir nun das Programm mittels node ethWallet.js in der Konsole aus, haben wir auch schon unsere Mnemonic, welche aus 12 Wörter besteht.
Wie funktioniert das BIP 39 ?
Um die verwendete Funktion besser zu verstehen, gucken wir uns nun einmal an, wie eine Mnemonic gem. BIP 39 generiert wird.
Zunächst einmal werden 128 – 256 Bit, je nach verwendeter Einstellung, zufällig generiert. In der Default-Einstellung werden 128 Bit generiert. Wir nennen diesen Wert die Entropie. Wichtig hierbei ist, dass der Wert durch 32 teilbar ist, was in unserem Fall 4 ergibt. Wir sehen gleich wofür wir dieses Ergebnis benötigen. Sodann wird aus diesen 128 Bit ein Hash mittels der SHA-256-Hashfunktion erzeugt. Nun nehmen wir die ersten vier Bits dieses Hashs und hängen diese vier Bits als Prüfsumme an unsere 128 Bit, sodass wir insgesamt 132 Bit erhalten. Hätten wir eine Entropie von 256 Bit gewählt, würden wir an dieser Stelle entsprechend die ersten 8 Bit dieses Hashs als Prüfsumme verwenden.
Schließlich nehmen wir in diesem Fall 132 Bit und teilen diese in 11 Bit Stücke auf, da man mit 11 Bit alle Dezimalzahlen von 0 – 2047 darstellen kann und das verwendete Wörterbuch 2048 Wörter enthält. Somit können wir über jedes dieser 11 Bit ein Wort aus der Wörterliste heraussuchen und erhalten dann insgesamt 12 verschiedene Wörter, welche unsere Mnemonic darstellen. Das Ganze könnt ihr auch noch einmal online im BIP 39 nachlesen.
Implementierung in der JavaScript-Bibliothek BIP39
Im folgendem werden wir uns anschauen, wie die zuvor beschriebene Theorie in der JavaScript-Bibliothek BIP39 implementiert wurde. Hierzu werden wir vereinzelt Code-Ausschnitte aus der BIP39-Bibliothek der Version 3.1.0 analysieren. Den gesamten Code findet Ihr auch auf der entsprechenden GitHub-Seite.
export declare function generateMnemonic(strength?: number, rng?: (size: number) => Buffer, wordlist?: string[]): string;
function generateMnemonic(strength, rng, wordlist) {
strength = strength || 128;
if (strength % 32 !== 0) {
throw new TypeError(INVALID_ENTROPY);
}
rng = rng || ((size) => Buffer.from(utils_1.randomBytes(size)));
return entropyToMnemonic(rng(strength / 8), wordlist);
}
Zuerst sehen wir die generateMnemonic-Funktion mit den drei optionalen Parametern, von denen wir keinen beim Aufruf verwenden werden. Direkt danach wird die Stärke auf 128 festgelegt, sofern der Parameter, wie in unserem Fall, leer gelassen wurde (Zeile 2). Es wird dann auch geprüft, ob die Zahl durch 32 teilbar ist bzw. ob ein Rest bleibt, wenn man durch 32 teilt (Zeile 3). Nun bekommt die Variable rng, welche vermutlich für Random Number Generator steht, bei Auslassung des Parameters eine Funktion übergeben, die wiederum die randomBytes-Funktion aufruft, welche die im Parameter size übergebene Anzahl an Bytes generiert. Da die Stärke in Bit, in unserem Fall 128 Bit, angegeben wurde, müssen wir diese noch durch 8 teilen, um die entsprechende Anzahl an Bytes zu erhalten (Zeile 6). Der Rückgabewert dieser Funktion sowie eine Wordlist wird dann der Funktion entropyToMneomnic übergeben, welche für uns bei der Verwendung der Bibliothek verborgen bleibt.
function entropyToMnemonic(entropy, wordlist) {
if (!Buffer.isBuffer(entropy)) {
entropy = Buffer.from(entropy, 'hex');
}
wordlist = wordlist || DEFAULT_WORDLIST;
if (!wordlist) {
throw new Error(WORDLIST_REQUIRED);
}
// 128 <= ENT <= 256
if (entropy.length < 16) {
throw new TypeError(INVALID_ENTROPY);
}
if (entropy.length > 32) {
throw new TypeError(INVALID_ENTROPY);
}
if (entropy.length % 4 !== 0) {
throw new TypeError(INVALID_ENTROPY);
}
const entropyBits = bytesToBinary(Array.from(entropy));
const checksumBits = deriveChecksumBits(entropy);
const bits = entropyBits + checksumBits;
console.log(bits);
const chunks = bits.match(/(.{1,11})/g);
console.log(chunks);
const words = chunks.map((binary) => {
const index = binaryToByte(binary);
return wordlist[index];
});
return wordlist[0] === '\u3042\u3044\u3053\u304f\u3057\u3093' // Japanese wordlist
? words.join('\u3000')
: words.join(' ');
}
Sofern hier keine Wordlist, wie in unserem Fall übergeben wurde, wird die Default_Wordlist, welche die Englische ist, übergeben (Zeile 5). Danach wird noch überprüft ob sich die Entropie im gültigen Bereich befindet (Zeile 9 ff.), also zwischen 128 – 256 Bit liegt (hier geprüft in Bytes, also 16 oder 32 Byte). Sodann werden die 16 Bytes in das Binärformat umgewandelt und die entsprechenden Prüfsummenbits generiert. Wir haben hier in Zeile 22 und 24 eine entsprechende Ausgabe in der Konsole hinzugefügt, um die Funktionsweise gleich besser zu veranschaulichen.
function deriveChecksumBits(entropyBuffer) {
const ENT = entropyBuffer.length * 8;
const CS = ENT / 32;
const hash = sha256_1.sha256(Uint8Array.from(entropyBuffer));
return bytesToBinary(Array.from(hash)).slice(0, CS);
}
In Zeile 2 werden nur die Länge der verwendeten Entropie wieder in Bit umgerechnet, also in unserem Fall 16 * 8 = 128. Nachfolgend werden diese 128 / 32 geteilt, sodass man auf insgesamt 4 Prüfsummenbits kommt (Zeile 3). Dann generieren wir den entsprechenden Hash aus der Entropie und geben die ersten 4 Bits des Ergebnisses der Hash-Funktion mittels slice zurück (Zeile 5).
Danach befinden wir uns wieder in der entropyToMnemonic-Funktion und addieren diese 4 Bits auf unsere 128 Entropiebits und erhalten somit 132 Bits (Zeile 21). Die Die generierten Bits lassen wir uns in diesem Beispiel mittels console.log auf der Konsole ausgeben (Zeile 22). Anschließend werden diese 132 Bits mittels eines regulären Ausdrucks in jeweils 11 Bit große Chunks („Stücke“) zerlegtund in einem Array gespeichert, welches wir uns zum besseren Verständnis ebenfalls ausgeben lassen (Zeile 24), sodass wir insgesamt folgende Ausgabe erhalten:
010110101011111101101010111001101111110001100101100101101110001111101001110110111000001100110010010011110001101001000011110001001110
[
'01011010101', '11111011010',
'10111001101', '11111000110',
'01011001011', '01110001111',
'10100111011', '01110000011',
'00110010010', '01111000110',
'10010000111', '10001001110'
]
Danach werden diese im Binärformat vorliegenden Bits in der binaryToByte-Funktion mittels der parseInt-Funktion wieder in Integer-Werte umgewandelt und zurückgegeben. Anhand dieser Integer-Werte wird ein entsprechender Eintrag im Wörterbuch ausgewählt.
function binaryToByte(bin) {
return parseInt(bin, 2);
}
Wir können das auch überprüfen, indem wir den ersten Chunk in eine Dezimalzahl umrechnen, anschließend in das englische Wörterbuch gehen und an der entsprechende Stelle schauen.
Rechenen wir in diesem Fall das erste Chunk (01011010101) in eine Dezimalzahl um, so erhalten wir die Zahl 725.
Nun gehen wir in das entsprechende Wörterbuch der Bibliothek (/bip39/src/wordlists/english.json) und suchen uns hierbei die entsprechende Stelle aus. Das Wörterbuch liegt als Array im JSON-Format vor, sodass wir berücksichtigen müssen, dass die erste Zeile hier nicht mitgezählt wird. Ferner beginnt die Indexierung eines Arrays bekanntlich bei Null, sodass unser erstes Wort also in dieser Datei in der Zeile 727, also zwei Zeile später steht. Das Wort dieses Zeile entspricht dann dem ersten Wort unserer Mnemonic.
Dieser Prozess wird natürlich über die map-Funktion für jedes Chunk einmal durchgeführt, sodass wir insgesamt 12 zufällig gewählte Wörter erhalten, die unsere Mnemonic darstellen (Zeile 25, entropyToMnemonic–Funktion).
Im nächsten Artikel gucken wir uns an, wie man aus einer Mnemonic einen Seed erstellt.
BIPs und verwendete Bibliotheken
BIPs:
[1] https://github.com/bitcoin/bips
[2] https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki
[3] https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
[4] https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
Genutzte Bibliotheken:
[5] https://www.npmjs.com/package/bip39
[6] https://www.npmjs.com/package/hdkey
[7] https://www.npmjs.com/package/ethereumjs-util