Ethereum HD-Wallet in JavaScript: Vom öffentlichen Schlüssel zur Ethereum-Adresse [Teil 6]

In diesem Artikel werden wir uns mit den unterschiedlichen Darstellungsformen eines öffentlichen Schlüssels beschäftigen (komprimierter vs. unkomprimierter öffentlicher Schlüssel) und aus einem unkomprimierten öffentlichen Schlüssel eine Ethereum-Adresse ableiten.

In dem letzten Artikel haben wir einen privaten Schlüssel generiert und mit Hilfe der Bibliothek hdkey einen öffentlichen Schlüssel in komprimierter Form generiert. Wir haben uns jedoch noch nicht damit beschäftigt, was überhaupt eine komprimierte Form ist und vor allem wie eine unkomprimierte Form eines öffentlichen Schlüssels aussieht.

Die verwendeten BIPs und Bibliotheken zur Abbildung findet ihr am Ende des Artikels.

Obgleich wir im letzten Artikel bereits einen öffentlichen Schlüssel generiert haben, habe ich diesen in unserer Übersichtsfolie noch orange belassen, da wir diesen in einer anderen Darstellungsform benötigen. Hierzu werden wir die JavaScript Bibliothek Etherumjs-util verwenden und uns diese auch genauer anschauen.

Implementierung

Um einen unkomprimierten öffentlichen Schlüssel und auch eine Ethereum-Adresse mit Prüfsumme zu erstellen, installieren wir uns zunächst die ethereumjs-util-Bibliothek mittels npm:

npm install ethereumjs-util

Wie üblich erscheint diese wieder in den Abhängigkeiten in unserer package.json und speichert die entsprechenden Datei im nodes_modules Ordner. Danach importieren wir uns die Funktion privateToPublic mittels der schon öfter in dieser Artikelreihe verwendeten Import-Anweisung aus der ethereumjs-util Bibliothek. Sodann legen wir eine neue Konstante an, in der wir das Ergebnis dieser Funktion speichern werden. Jetzt müssen wir die privateToPublic-Funktion nur noch aufrufen und dieser Funktion den privaten Schlüssel unseres zuvor abgeleiteten Kind-Schlüssels übergeben. Abschließend können wir den Wert der zuvor angelegten Konstante wieder mittels Console.log ausgeben lassen. Zu beachten ist natürlich, dass wir den Ausgabewert wieder mittels der toString-Funktion in die hexadezimale Darstellung umwandeln. Zusätzlich verdeutlichen wir noch einmal, dass es sich bei der Ausgabe um einen öffentlichen Schlüssel, der mittels der ethereumjs-util Bibliothek erzeugt wurde, handelt.

import {generateMnemonic, mnemonicToSeed} from 'bip39';
import HDKey from 'hdkey';
import {privateToPublic} 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");
    // console.log('0x' + childKey.privateKey.toString('hex'));
    console.log('Öffentlicher Schlüssel mittels hdkey: ' + '0x' + childKey.publicKey.toString('hex'));

    const toPublicFromEthJS = privateToPublic(childKey.privateKey);
    console.log('Öffentlicher Schlüssel mittels ethereumjs-util: ' + '0x' + toPublicFromEthJS.toString('hex'));

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

Wir stellen schnell fest, dass dies zumindest augenscheinlich ein anderes Ergebnis ist als der öffentliche Schlüssel, welchen wir mit unserer hdkey Bibliothek erstellt haben:

slow extra bid describe refuse lunch lizard used nominee usual license toward
Öffentlicher Schlüssel mittels hdkey: 0x02de53287cb7a79a255d5adbd2e7184462e8e166d28314a45e041c95b12bf67a5b
Öffentlicher Schlüssel mittels ethereumjs-util: 0xde53287cb7a79a255d5adbd2e7184462e8e166d28314a45e041c95b12bf67a5bad92e15f0c792dc6416da5a0e36ffa523b85e6eae70da74bf407461ee0819c5e

Bevor wir uns näher damit beschäftigen, was genau ein komprimierter und ein unkomprimierter Schlüssel ist, möchte ich euch mittels einer dritten Bibliothek kurz beweisen, dass es sich bei diesem Schlüssel um den gleichen Schlüssel in einer anderen Darstellungsform handelt – auf die Hintergründe kommen wir dann später zu sprechen. Wir installieren uns noch kurz die Bibliothek eth-crypto, welche sehr einfach komprimierte Schlüssel in unkomprimierte Schlüssel umwandeln kann.

 npm i eth-crypto

Nach der Installation sehen wir auch wieder einen entsprechenden Eintrag in der package.json-Datei. Die zu dieser Bibliothek gehörigen Dateien werden wie üblich im Ordner node_modules hinterlegt. Anschließend importieren wir uns die eth-crypto Bibliothek wieder mittels der Import-Anweisung in unser Projekt (Zeile 4).

Nun steht uns eine Funktion namens decompress zur Verfügung, welche aus einem komprimierten öffentlichen Schlüssel einen unkomprimierten öffentlichen Schlüssel erstellen kann. Hierzu legen wir uns eine neue Konstante namens uncompressed an (Zeile 20) in der wir das Ergebnis dieser Funktion speichern werden. Die Funktion decompress steht uns aufgrund des Imports unter EthCrypto.publicKey.decompress zur Verfügungund erwartet als Parameter einen komprimierten öffentlichen Schlüssel. Wir übergeben hier den komprimierten öffentlichen Schlüssel unseres zuvor abgeleiteten Kind-Schlüssels. Da die decompress-Funktion einen String erwartet, wandeln wir unseren öffentlichen komprimierten Schlüssel wieder mittels der toString-Funktion in die hexadezimale Darstellung um.

Der Rückgabewert der decompress-Funktion steht uns anschließend in der Konstante uncompressed zur Verfügung und wir können diesen über Console.log (Zeile 21) ausgeben.

import {generateMnemonic, mnemonicToSeed} from 'bip39';
import HDKey from 'hdkey';
import {privateToPublic} from 'ethereumjs-util';
import EthCrypto from 'eth-crypto';

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");
    // console.log('0x' + childKey.privateKey.toString('hex'));
    console.log('Öffentlicher Schlüssel mittels hdkey: ' + '0x' + childKey.publicKey.toString('hex'));

    const toPublicFromEthJS = privateToPublic(childKey.privateKey);
    console.log('Öffentlicher Schlüssel mittels ethereumjs-util: ' + '0x' + toPublicFromEthJS.toString('hex'));

    const uncompressed = EthCrypto.publicKey.decompress(childKey.publicKey);
    console.log('Öffentlicher Schlüssel (unkomprimiert) mittels eth-crypto: ' + '0x' + uncompressed);

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

So erhalten wir den gleichen öffentlichen Schlüssel wie uns unsere ethereumjs-util Bibliothek liefert:

aerobic dose annual token foster auto ball summer can stone few swarm
Öffentlicher Schlüssel mittels hdkey: 0x02a82d0afe70f4f53f73767de22ba3cba9386f639f1d8e40b425455da169a2d576
Öffentlicher Schlüssel mittels ethereumjs-util: 0xa82d0afe70f4f53f73767de22ba3cba9386f639f1d8e40b425455da169a2d5769c7767f614257dac474fb88314f9fc585ed8d786bba1935741f77705951e7470
Öffentlicher Schlüssel (unkomprimiert) mittels eth-crypto: 0xa82d0afe70f4f53f73767de22ba3cba9386f639f1d8e40b425455da169a2d5769c7767f614257dac474fb88314f9fc585ed8d786bba1935741f77705951e7470

Da die eth-crypto-Bibliothek uns hier lediglich demonstrieren sollte, dass man aus einem komprimierten öffentlichen Schlüssel problemlos einen unkomprimierten öffentlichen Schlüssel erstellen kann und nun für das Gesamtprojekt keinerlei Bedeutung mehr hat, entfernen wir diese wieder mittels

npm remove eth-crypto

Auch den dazugehörigen Import sowie die entsprechenden Ausgaben benötigen wir im weiteren Verlauf nicht mehr.

Schauen wir uns jetzt einmal kurz an, was genau der Unterschied zwischen diesen beiden Schlüsseln ist und wieso sich ein öffentlicher Schlüssel überhaupt komprimieren lässt.

Da der öffentliche Schlüssel lediglich einen Punkt auf der von Ethereum verwendeten elliptischen Kurve darstellt, ist dieser auch nichts anderes als eine X- und eine Y-Koordinate. In der unkomprimierten Darstellung eines öffentlichen Schlüssels werden die X- und Y-Koordinate einfach aneinandergereiht und üblicherweise das Präfix 04 vorangestellt. Die von uns verwendete Bibliothek Ethereumjs-util schneidet dieses Präfix jedoch raus, da wir zur Ableitung einer Ethereum-Adresse lediglich die X- und Y-Koordinate benötigen. Dies können wir auch gut in dem Code dieser Bibliothek sehen (Zeile 4):

export const privateToPublic = function (privateKey: Buffer): Buffer {
  assertIsBuffer(privateKey)
  // skip the type flag and use the X, Y points
  return Buffer.from(publicKeyCreate(privateKey, false)).slice(1)
}

Wie wir aus einen unkomprimierten Schlüssel bzw. aus der X- und der Y-Koordinate eines öffentlichen Schlüssels eine Ethereum-Adresse ableiten, schauen wir uns später genauer an.

Aufgrund der Symmetrie bei elliptischen Kurven kommen für einen X-Wert immer zwei mögliche Y-Werte infrage, weswegen die unkomprimierte Darstellung auch zusätzlich zur X-Koordinate die Y-Koordinate enthält.

Da es jedoch für jeden X-Wert nur einen geraden und einen ungeraden Y-Wert gibt, kann man die gleiche Information mit einer entsprechenden Ergänzung auch komprimierter darstellen, indem man den X-Wert durch ein Präfix ergänzt, welches darüber Aufschluss gibt, ob es sich bei dem Y-Wert eben um den geraden oder den ungeraden Wert handelt.

Dies geschieht durch das Präfix 02 bei geraden Y-Werten und das Präfix 03 bei ungeraden Y-Werten. Somit kann man die Y-Werte entsprechend berechnen und anhand des Präfix dann den richtigen Y-Wert bestimmen.

Praktisches Beispiel

Bevor uns damit beschäftigen, wie wir aus einem öffentlichen Schlüssel eine Ethereum-Adresse erhalten, habe ich hier noch einmal zwei öffentliche Schlüssel in jeweils komprimierter und unkomprimierter Form dargestellt. Bei dem ersten handelt es sich um einen geraden Y-Wert (hier grün markiert), sodass wir das Präfix 02 (hier ebenfalls grün markiert), für einen komprimierten öffentlichen Schlüssel benötigen. Möchten wir diesen Schlüssel hingegen unkomprimiert darstellen, so wird das Präfix 04 (hier blau markiert) verwendet und wir schreiben sowohl den X-, als auch den Y-Wert hintereinander.

Analog hierzu verhält sich das zweite Beispiel, welches jedoch einen ungeraden Y-Wert hat (hier rot markiert), sodass wir das Präfix 03 (hier ebenfalls rot markiert) für den komprimierten Schlüssel verwenden müssen. Der unkomprimierte Schlüssel wird wie im vorherigen Beispiel ebenfalls durch die Aneinanderreihung des X- und Y-Wertes mit vorangestellten 04-Präfix erreicht.

Vom öffentlichen Schlüssel zur Ethereum-Adresse

Doch wie bekommt man nun aus einem unkomprimierten öffentlichen Schlüssel eine gültige Ethereum-Adresse?

Zunächst gehen wir davon aus, dass ein öffentlicher Schlüssel in unkomprimierter Form ohne das Präfix 04 bereits vorliegt. Diesen packen wir anschließend in die Keccak256-Hashfunktion und erhalten dann hieraus einen 256 Bit Wert. Da 8 Bit ein Byte sind, entspricht dieser Wert 32 Byte. Würde man diese 32 Byte hexadezimal darstellen, so bräuchten wir hierfür 64 Zeichen.

Von dieser Ausgabe nehmen wir einfach die letzten 20 Byte, was 160 Bit entspricht, oder auch 40 Zeichen im hexadezimalen System.

Schauen wir uns diese Vorgehensweise einmal genauer in der Praxis an. Wie bereits zuvor erwähnt, benötigen wir zur Erstellung einer Ethereum-Adresse den unkomprimierten öffentlichen Schlüssel ohne das Präfix 04. Genau das liefert uns die bereits ausgeführte Funktion privateToPublic.

Um nun eine Ethereum-Adresse zu erhalten, müssen wir lediglich die publicToAddress-Funktion importieren (Zeile 3), den Wert der zuvor ausgeführten privateToPublic-Funktion der publicToAddress-Funktion übergeben und erhalten so eine gültige Ethereum-Adresse (Zeile 19 f.).

import {generateMnemonic, mnemonicToSeed} from 'bip39';
import HDKey from 'hdkey';
import { privateToPublic, publicToAddress } 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");
    // console.log('0x' + childKey.privateKey.toString('hex'));
    console.log('Öffentlicher Schlüssel mittels hdkey: ' + '0x' + childKey.publicKey.toString('hex'));

    const toPublicFromEthJS = privateToPublic(childKey.privateKey);
    console.log('Öffentlicher Schlüssel mittels ethereumjs-util: ' + '0x' + toPublicFromEthJS.toString('hex'));

    const toAddress = publicToAddress(toPublicFromEthJS);
    console.log('Ethereum-Adresse: ' + '0x' + toAddress.toString('hex'));

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

Es wird folgende Ausgabe erzeugt:

rule top minimum interest maple visual cabbage witness craft game wood metal
Öffentlicher Schlüssel mittels hdkey: 0x030e921ed561111ced25958318055eded73c5dd26b798959641ae7a29dab18230d
Öffentlicher Schlüssel mittels ethereumjs-util: 0x0e921ed561111ced25958318055eded73c5dd26b798959641ae7a29dab18230d11d4a42a2ec45f5c2ca01b585a2a160f9201d3d1059f0fa435bb420cf0031171
Ethereum-Adresse: 0x46231cf66ad561bdc56f2ae598e74dbf2b271c5a

Gucken wir uns abschließend noch einmal kurz den Quellcode der hier verwendeten Bibliothek an:

export const pubToAddress = function (pubKey: Buffer, sanitize: boolean = false): Buffer {
  assertIsBuffer(pubKey)
  if (sanitize && pubKey.length !== 64) {
    pubKey = Buffer.from(publicKeyConvert(pubKey, false).slice(1))
  }
  assert(pubKey.length === 64)
  // Only take the lower 160bits of the hash
  return keccak(pubKey).slice(-20)
}
export const publicToAddress = pubToAddress

Wir sehen auch noch einmal im Quellcode der Bibliothek, dass hier im Prinzip lediglich die entsprechende keccak-Hashfunktion aufgerufen wird und die letzten 20 Elemente des Puffer-Objektes, welche den letzten 20 Byte der Ausgabe entsprechen und im hexadezimalen System 40 Zeichen darstellen, zurückgegeben werden (Zeile 8).

Im nächsten Artikel werden wir uns angucken, wie man einer Ethereum-Adresse eine Prüfsumme hinzufügen kann und auch wie man überprüfen kann, ob eine gegebene Ethereum-Adresse eine richtige Prüfsumme hat.

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