Ethereum HD-Wallet: Generierung einer Vielzahl von Adressen mit einer Mnemonic [Teil 8]

In diesem Artikel werden wir unser konsolenbasiertes HD-Wallet so modifizieren, dass wir eine Vielzahl an privaten Schlüsseln mit entsprechenden Ethereum-Adressen anhand einer Mnemonic erstellen können.

Hierzu werden wir zunächst unser Ziel genauer definieren und anschließend direkt in das Refactoring und in die Implementierung übergehen.

Um diesem Artikel besser folgen zu können, ist es empfehlenswert sich die vorherigen Artikel, welche jeweils auf die in dieser Übersichtsfolie dargestellten einzelnen Schritte eingehen, anzuschauen. Die Links zu den einzelnen Bibliotheken und BIPs findet ihr weiter unten.

Wir beabsichtigen aus einer Mnemonic ein Array mit einer von uns definierten Anzahl an Objekten, welche jeweils einen privaten Schlüssel mit der dazugehörigen Ethereum-Adresse mit Prüfsumme enthalten, zu generieren. Das Endergebnis soll auf der Konsole dann wie hier in der Folie aussehen.

Implementierung

Wir starten mit dem im letzten Artikel angefertigten Code, welcher uns lediglich eine Mnemonic sowie eine Ethereum-Adresse mit Prüfsumme ausgibt.

import {generateMnemonic, mnemonicToSeed} from 'bip39';
import HDKey from 'hdkey';
import { privateToPublic, publicToAddress, toChecksumAddress } from 'ethereumjs-util';

const mnemonic = generateMnemonic();
console.log(mnemonic);

try {
    const seed = await mnemonicToSeed(mnemonic);
    const myHDKey = HDKey.fromMasterSeed(seed);

    let childKey = myHDKey.derive("m/44'/60'/0'/0/0");
    const toPublic = privateToPublic(childKey.privateKey);
    const toAddress = publicToAddress(toPublic);
    const checksumAddress = toChecksumAddress('0x' + toAddress.toString('hex'));
    console.log('Ethereum-Adresse mit Prüfsumme: ' + checksumAddress);

} catch (error) {
    console.log(error);
}

Um eine bessere Übersichtlichkeit zu erreichen, legen wir uns eine neue JavaScript-Datei an, welche wir „ethHelper.js“ nennen.

Wir legen dann eine Funktion an, die eine zuvor definierte Anzahl an Ableitungspfaden generiert, damit wir in der Folge aus diesen Ableitungspfaden verschiedene private Schlüssel generieren können.

Diese Funktion bekommt den Namen “generatePaths” und erhält als Parameter die Anzahl der Ableitungspfade, welche generiert werden sollen. Anschließend legen wir unseren Ableitungspfad bis zur vorletzten Ebene für Ethereum fest. Wem dieser Pfad noch nichts sagt, der sollte sich den Artikel über die Baumstruktur und die Ableitungspfade vorher anschauen.

function generatePaths(numberOfPaths) {
    const ethereumPath = "m/44'/60'/0'/0/";
    const myPaths = [];
    for(let i = 0; i < numberOfPaths; i++) {
        myPaths[i] = ethereumPath + i;
    }
    return myPaths;
}

export { generatePaths };

Wir legen hier den Pfad nur bis zur vorletzten Ebene fest, da wir die letzte Ebene mittels einer Schleife gleich generieren werden (Zeile 2).

Sodann legen wir ein leeres Array an, in welches wir später unsere Ableitungspfade, die zu den verschiedenen privaten Schlüsseln führen werden, speichern können (Zeile 3).

Nun können wir einfach eine Schleife erstellen, welche so lange durchläuft, bis unsere zu inkrementierende Variable i nicht mehr kleiner als der durch unseren Parameter übergebene Wert ist. Packen wir also als Parameter hier eine 10 herein, werden die Pfade mit der Endung 0-9 erstellt (Zeile 4). Danach müssen wir nur noch unser Array mit den Ableitungspfaden befüllen, indem wir unser Array an der Stelle i den jeweiligen Pfad zuweisen und die letzte Ebene dieses Pfades mit der Variable i ergänzen. Hierdurch wird der Anfangspfad jeweils, je nach Schleifendurchlauf entsprechend ergänzt (Zeile 5). Abschließend geben wir unser gefülltes Array noch zurück und sind fertig mit dieser Funktion (Zeile 7). Nun exportieren wir uns diese Funktion und importieren sie wieder in eine neue JavaScript-Datei namens „hdWallet.js“.

Zudem erstellen wir eine neue Konstante, in der wir das Ergebnis unserer zuvor erstellten Funktion „generatePaths“ speichern. Da wir diese Funktion mit dem Parameter 10 aufrufen erhalten wir als Rückgabewert ein Array mit 10 Ableitungspfaden (Zeile 3).

    import { generatePaths } from "./ethHelper.js";

    const generatedPaths = generatePaths(10);
    console.log(generatedPaths);

Jetzt lassen wir uns das Array noch kurz ausgeben, um zu überprüfen, ob auch alles richtig funktioniert.

[
  "m/44'/60'/0'/0/0",
  "m/44'/60'/0'/0/1",
  "m/44'/60'/0'/0/2",
  "m/44'/60'/0'/0/3",
  "m/44'/60'/0'/0/4",
  "m/44'/60'/0'/0/5",
  "m/44'/60'/0'/0/6",
  "m/44'/60'/0'/0/7",
  "m/44'/60'/0'/0/8",
  "m/44'/60'/0'/0/9"
]

In die Datei „hdWallet.js“ können wir schon einen Teil aus unserem zuvor implementierten Code hereinkopieren, da sich bspw. an der Generierung einer Mnemonic, der Erstellung eines Seeds sowie eines darauf basierenden Master-Schlüssels hier nichts ändert.

import { generatePaths } from "./ethHelper.js";
import {generateMnemonic, mnemonicToSeed} from 'bip39';
import HDKey from 'hdkey';

const mnemonic = generateMnemonic();
console.log(mnemonic);

try {
    const seed = await mnemonicToSeed(mnemonic);
    const myHDKey = HDKey.fromMasterSeed(seed);

    const generatedPaths = generatePaths(10);
    console.log(generatedPaths);

} catch (error) {
    console.log(error);
}

Jetzt wo wir bereits alle Ableitungspfade für unsere zukünftigen Schlüssel generieren können, müssen wir auch noch eine Funktion implementieren, die anhand dieser Ableitungspfade private Schlüssel generiert. Hierzu wechseln wir wieder in die Datei „ethHelper.js“ und legen uns eine neue Funktion, welche wir „keysFromHDKey“ nennen, an. Als Parameter erhält diese Funktion einmal den masterHDKey und einmal ein Array mit Ableitungspfaden.

import {privateToPublic, publicToAddress, toChecksumAddress} from 'ethereumjs-util';

function generatePaths(amountOfPaths) {
    const ethereumPathBeginning = "m/44'/60'/0'/0/";
    const myPaths = [];
    for (let i = 0; i < amountOfPaths; i++) {
        myPaths[i] = ethereumPathBeginning + i;
    }
    return myPaths;
}

function keysFromHDKey(masterHDKey, paths) {
    const hexPrefix = '0x';
    let myKeys = new Array();
    paths.forEach(path => {

        const childKey = masterHDKey.derive(path);
        const toPublic = privateToPublic(childKey.privateKey);
        const toAddressHex = hexPrefix + publicToAddress(toPublic).toString('hex');
        const checksumAddress = toChecksumAddress(toAddressHex);

        const key = {
            privateKeyHex: hexPrefix + childKey.privateKey.toString('hex'),
            checksumAddressHex: checksumAddress
        }

        myKeys.push(key);
        
    });
    return myKeys;
}

export {generatePaths, keysFromHDKey};

Zunächst erstellen wir uns eine Konstante, welches das hex-Präfix enthält, da wir dieses später öfter benötigen werden (Zeile 13). Nun legen wir uns ein neues leeres Array in einer Variable an, um unsere Schlüssel, welche wir gleich generieren werden, abspeichern zu können (Zeile 14).

Jetzt durchlaufen wir das in den Parametern übergebene Array, welches unsere Ableitungspfade enthält, mittels forEach und rufen für jedes Element, also jeden Ableitungspfad, eine Pfeilfunktion auf (Zeile 15). Danach können wir auch schon den Rückgabe Wert dieser Funktion festlegen – um den Inhalt des Arrays werden wir uns noch kümmern.

In die Pfeilfunktion kopieren wir uns den Code aus unserem ursprünglichen HD-Wallet herein, da an der Vorgehensweise zur Ableitung eines privaten Kind-Schlüssels natürlich nichts geändert werden muss.

Wir ersetzen die Variable hier kurz durch eine Konstante, da sich diese im Rahmen eines Schleifendurchlaufs nicht mehr ändern muss und leiten den Kind-Schlüssel entsprechend von dem dieser Funktion übergebenen Master-Schlüssel ab. Zusätzlich ersetzen wir noch den konkreten Ableitungspfad durch den jeweils aktuellen Ableitungspfad des Schleifendurchlaufs, sodass für jeden zuvor generierten Ableitungspfad ein Kind-Schlüssel generiert werden kann.

Sodann benennen wir toAdress in toAddressHex um und erzeugen aus den hier vorliegenden Puffer-Objekt direkt einen Adresse, welche in der hexadezimalen Darstellung vorliegt. Hierzu fügen wir zum einen das Hex-Präfix hinzu und zum anderen wandeln wir das vorliegende Puffer-Objekt mittels der toString-Funktion entsprechend in die hexadezimale Darstellung um. Nun können wir auch die Erzeugung der Adresse mit Prüfsumme etwas vereinfachen, indem wir hier einfach die Konstante toAdressHex übergeben, da diese jetzt bereits in der richtigen Darstellungsform vorliegt. Wir entfernen noch kurz die Ausgabe, da wir diese hier nicht mehr benötigen.

Das einzige was wir jetzt noch hinzufügen müssen, ist ein Objekt, welches den privaten Schlüssel sowie die Ethereum-Adresse mit Prüfsumme enthält. Dieses können wir dann dem zuvor angelegten Array hinzufügen.

Also legen wir mittels einer Konstante ein Objekt an und füllen dieses mit einer Eigenschaft namens privateKeyHex, welche unseren privaten Schlüssel in der hexadezimalen Darstellung mit Hex-Präfix enthält (Zeile 22 f.). Den eigentlichen privaten Schlüssel wandeln wir noch in eine hexadezimale Darstellung um. Anschließend fügen wir unserem Objekt noch die Eigenschaft checksumAddressHex hinzu, welche dann unsere Ethereum-Adresse mit Prüfsumme enthält (Zeile 24). Jetzt müssen wir dieses Objekt nur noch unseren zuvor angelegten Array, welches bereits am Ende der Funktion zurückgegeben wird, hinzufügen. (Zeile 27 und 30).

Nun können wir auch diese Funktion exportieren und in der Datei „hdWallet.js“ wieder importieren.

import { generatePaths, keysFromHDKey } from "./ethHelper.js";
import {generateMnemonic, mnemonicToSeed} from 'bip39';
import HDKey from 'hdkey';

const mnemonic = generateMnemonic();
console.log(mnemonic);

try {
    const seed = await mnemonicToSeed(mnemonic);
    const myHDKey = HDKey.fromMasterSeed(seed);

    const generatedPaths = generatePaths(10);
    // console.log(generatedPaths);
    const myKeys = keysFromHDKey(myHDKey, generatedPaths);
    console.log(myKeys);

} catch (error) {
    console.log(error);
}

Zu guter Letzt legen wir noch kurz eine Konstante an, die unsere privaten Schlüssel und Ethereum-Adressen speichert und weisen dieser Konstante das Ergebnis unserer zuvor implementieren Funktion zu (Zeile 14). Als Parameter übergeben wir unseren Master-Schlüssel (myHDKey) sowie unsere zuvor generierten Ableitungspfade.

Nun können wir uns das Ergebnis über die Konsole ausgeben und dann sind wir auch schon fertig mit unserem konsolenbasierten HD-Wallet.

truth rubber lyrics pool grace pitch real split order home usual soup
[
  {
    privateKeyHex: '0xb73fe37e650506574b3fd681aabbbf048468bc32176e6292ff118ace79523ff7',
    checksumAddressHex: '0xE00c75a52Ef65cE0C8eD390922D22C934F69E38e'
  },
  {
    privateKeyHex: '0x57b979d3914ceda2dd96a6fd669b96c3197644686ca2ccdba81c87d476d01dc2',
    checksumAddressHex: '0x4953aF652cb6fa49d2127baC28f7fC4209d9f815'
  },
  {
    privateKeyHex: '0x6849305870851ee758b69d7dd8a134c8a84fd1646d8909a0f2223841c94ab99e',
    checksumAddressHex: '0x935DC702c0155589EAfC5a316CD7BB4cdf7566aD'
  },
  {
    privateKeyHex: '0x95a04e0b85a63ef2fa4e165a4836a8dc65ac2b82fe9aab41fa01b7b8066e3c73',
    checksumAddressHex: '0x1514DeCab05E0bCf310c8a8Ae0bFD935B654e989'
  },
  {
    privateKeyHex: '0xf82ae1d4297d6db640bf6a97da5808abd5369ff3d88da4ebc13be234f8be32d6',
    checksumAddressHex: '0xE4F882dEa756bD31f07789C35A548267E5656D6B'
  },
  {
    privateKeyHex: '0xec2450748697f14fa3d1c5e5c2d8a10043e42345a5a8b76b8b07d4d629fe84db',
    checksumAddressHex: '0xca21A31487999DDF297774db8ea9d15FFd600e87'
  },
  {
    privateKeyHex: '0x37269772fbf8f1d2da3d58e043d79cc48123e3fc12ddd9d8f60f25c3a4f3b929',
    checksumAddressHex: '0x7915eeCfbA06c1DaBe10B9831E536Cf6a861b3C0'
  },
  {
    privateKeyHex: '0x707d807d0f3f5721c83745faca426329a945ee838872d8ac154836a15725bf7e',
    checksumAddressHex: '0x4D7B1Caa968da0dCb079C58360fe6e9f171DBF04'
  },
  {
    privateKeyHex: '0xb532236bfc3706a1d207d177b8646ef47186a99a4fd1eb8cbe721799291302f6',
    checksumAddressHex: '0x65B571fe04fa47683b363944F5921095705521c3'
  },
  {
    privateKeyHex: '0x4cb1ec892763d7813961ac3fe1c81367a7a1b311adbfead85850543c18f08650',
    checksumAddressHex: '0xca365869354db13475456525f6d942EA9785EE6c'
  }
]

Unsere Ausgabe können wir auch noch einmal beispielhaft mit dem Programm Ganache vergleichen. Ganache ist eigentlich ein Tool für Entwickler, welches ein Ethereum-Netzwerk auf dem eigenen Computer simuliert, um dezentrale Anwendungen lokal zu testen. Es hat jedoch auch eine HD-Wallet-Funktion integriert, die wir kurz nutzen werden, um aus unserer Mnemonic eine Vielzahl an privaten Schlüsseln zu generieren. Hierzu könnt ihr euch einfach für euer Betriebssystem das Programm herunterladen und installieren. Für Linux gibt es auch ein AppImage, welches man nach dem Download direkt auszuführen kann.

Nach dem Start des Programmes klicken wir auf das Zahnrad oben rechts, dann auf Accounts & Keys und geben hier unsere Mnemonic, welche wir zuvor generiert haben, ein.

Danach klicken wir auf „SAVE AND RESTART“ und schon erhalten wir 10 Ethereum-Adressen mit privaten Schlüsseln, die mit den von unserem HD-Wallet generierten Adressen übereinstimmen müssten, da wir hier das gleich Mnemonic und denselben Ableitungspfad verwenden. Diesen sehen wir in Ganache noch einmal oben rechts.

Wir sehen, dass die Adressen hier die gleichen sind, wie die von uns generierten. Zudem können wir auf den Schlüssel klicken, um uns den privaten Schlüssel zu der jeweiligen Adresse anzeigen zu lassen. Auch diese sind identisch mit denen von uns generierten Schlüsseln.

Nun haben wir alle Schritte, von der Generierung einer Mnemonic hin zu einer Ethereum-Adresse mit Prüfsumme auf unserer Übersichtsfolie erreicht. Obgleich das Ziel unserer Übersichtsfolie erreicht ist, werde ich ggf. in Zukunft eine Artikelreihe aufbauen, die erklärt, wie man mittels VueJS und Vuetify eine grafische Oberfläche für dieses HD-Wallet 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



Nach oben scrollen