From 855646f48dcf4f7b4ab9795c3b74acf85e361133 Mon Sep 17 00:00:00 2001 From: Alejo Acosta Date: Fri, 13 Sep 2024 09:43:06 -0300 Subject: [PATCH 1/4] implement address grinding --- src/_tests/unit/bip47.unit.test.ts | 10 ++-- src/wallet/hdwallet.ts | 25 ++++----- src/wallet/qi-hdwallet.ts | 81 ++++++++++++++++++++++++++++-- 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/src/_tests/unit/bip47.unit.test.ts b/src/_tests/unit/bip47.unit.test.ts index dd6fa7d7..480b6bb4 100644 --- a/src/_tests/unit/bip47.unit.test.ts +++ b/src/_tests/unit/bip47.unit.test.ts @@ -1,4 +1,4 @@ -import { Mnemonic, QiHDWallet } from '../../index.js'; +import { Mnemonic, QiHDWallet, Zone } from '../../index.js'; import assert from 'assert'; describe('Test generation of payment codes and payment addresses', function () { @@ -26,11 +26,11 @@ describe('Test generation of payment codes and payment addresses', function () { ); // Alice generates a payment address for sending funds to Bob - const bobAddress = await aliceQiWallet.generateSendAddress(bobPaymentCode); - assert.equal(bobAddress, '0x798e976dfAffe2174c3b21ac7116A35D09DB837d'); + const bobAddress = await aliceQiWallet.generateSendAddress(bobPaymentCode, Zone.Cyprus1); + assert.equal(bobAddress, '0x0083d552Fc0A3f9269089cbb9Ca11eaba93802e3'); // Bob generates a payment address for receiving funds from Alice - const receiveAddress = await bobQiWallet.generateReceiveAddress(alicePaymentCode); - assert.equal(receiveAddress, '0x798e976dfAffe2174c3b21ac7116A35D09DB837d'); + const receiveAddress = await bobQiWallet.generateReceiveAddress(alicePaymentCode, Zone.Cyprus1); + assert.equal(receiveAddress, '0x0083d552Fc0A3f9269089cbb9Ca11eaba93802e3'); }); }); diff --git a/src/wallet/hdwallet.ts b/src/wallet/hdwallet.ts index a0976f1e..e127238f 100644 --- a/src/wallet/hdwallet.ts +++ b/src/wallet/hdwallet.ts @@ -34,7 +34,7 @@ export interface SerializedHDWallet { /** * Constant to represent the maximum attempt to derive an address. */ -const MAX_ADDRESS_DERIVATION_ATTEMPTS = 10000000; +export const MAX_ADDRESS_DERIVATION_ATTEMPTS = 10000000; export const _guard = {}; @@ -94,6 +94,17 @@ export abstract class AbstractHDWallet { return this._root.extendedKey; } + // helper method to check if an address is valid for a given zone + protected isValidAddressForZone(address: string, zone: Zone): boolean { + const addressZone = getZoneForAddress(address); + if (!addressZone) { + return false; + } + const isCorrectShard = addressZone === zone; + const isCorrectLedger = this.coinType() === 969 ? isQiAddress(address) : !isQiAddress(address); + return isCorrectShard && isCorrectLedger; + } + /** * Derives the next valid address node for a specified account, starting index, and zone. The method ensures the * derived address belongs to the correct shard and ledger, as defined by the Quai blockchain specifications. @@ -117,19 +128,9 @@ export abstract class AbstractHDWallet { let addrIndex = startingIndex; let addressNode: HDNodeWallet; - const isValidAddressForZone = (address: string): boolean => { - const addressZone = getZoneForAddress(address); - if (!addressZone) { - return false; - } - const isCorrectShard = addressZone === zone; - const isCorrectLedger = this.coinType() === 969 ? isQiAddress(address) : !isQiAddress(address); - return isCorrectShard && isCorrectLedger; - }; - for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) { addressNode = changeNode.deriveChild(addrIndex++); - if (isValidAddressForZone(addressNode.address)) { + if (this.isValidAddressForZone(addressNode.address, zone)) { return addressNode; } } diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index b9d85f25..9f415538 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -1,4 +1,10 @@ -import { AbstractHDWallet, NeuteredAddressInfo, SerializedHDWallet, _guard } from './hdwallet.js'; +import { + AbstractHDWallet, + NeuteredAddressInfo, + SerializedHDWallet, + _guard, + MAX_ADDRESS_DERIVATION_ATTEMPTS, +} from './hdwallet.js'; import { HDNodeWallet } from './hdnodewallet.js'; import { QiTransactionRequest, Provider, TransactionResponse } from '../providers/index.js'; import { computeAddress } from '../address/index.js'; @@ -30,6 +36,13 @@ interface OutpointInfo { account?: number; } +interface paymentCodeInfo { + address: string; + index: number; + isUsed: boolean; + zone: Zone; +} + /** * @extends SerializedHDWallet * @property {OutpointInfo[]} outpoints - Array of outpoint information. @@ -126,6 +139,16 @@ export class QiHDWallet extends AbstractHDWallet { //! Review this private _ecc!: TinySecp256k1Interface; + /** + * Map of paymentcodes to paymentCodeInfo for the receiver + */ + private _receiverPaymentCodeInfo: Map = new Map(); + + /** + * Map of paymentcodes to paymentCodeInfo for the sender + */ + private _senderPaymentCodeInfo: Map = new Map(); + /** * @ignore * @param {HDNodeWallet} root - The root HDNodeWallet. @@ -672,7 +695,7 @@ export class QiHDWallet extends AbstractHDWallet { * @returns {Promise} A promise that resolves to the payment address for sending funds. * @throws {Error} Throws an error if the payment code version is invalid. */ - public async generateSendAddress(receiverPaymentCode: string): Promise { + public async generateSendAddress(receiverPaymentCode: string, zone: Zone): Promise { const bip32 = await this._getBIP32API(); const buf = await this._decodeBase58(receiverPaymentCode); const version = buf[0]; @@ -680,7 +703,31 @@ export class QiHDWallet extends AbstractHDWallet { const receiverPCodePrivate = await this._getPaymentCodePrivate(0); const senderPCodePublic = new PaymentCodePublic(this._ecc, bip32, buf.slice(1)); - return senderPCodePublic.getPaymentAddress(receiverPCodePrivate, 0); + + const paymentCodeInfoArray = this._receiverPaymentCodeInfo.get(receiverPaymentCode); + const lastIndex = + paymentCodeInfoArray && paymentCodeInfoArray.length > 0 + ? paymentCodeInfoArray[paymentCodeInfoArray.length - 1].index + : 0; + + let addrIndex = lastIndex; + for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) { + const address = senderPCodePublic.getPaymentAddress(receiverPCodePrivate, addrIndex++); + if (this.isValidAddressForZone(address, zone)) { + if (paymentCodeInfoArray) { + paymentCodeInfoArray.push({ address, index: addrIndex, isUsed: false, zone }); + } else { + this._receiverPaymentCodeInfo.set(receiverPaymentCode, [ + { address, index: addrIndex, isUsed: false, zone }, + ]); + } + return address; + } + } + + throw new Error( + `Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`, + ); } /** @@ -691,7 +738,7 @@ export class QiHDWallet extends AbstractHDWallet { * @returns {Promise} A promise that resolves to the payment address for receiving funds. * @throws {Error} Throws an error if the payment code version is invalid. */ - public async generateReceiveAddress(senderPaymentCode: string): Promise { + public async generateReceiveAddress(senderPaymentCode: string, zone: Zone): Promise { const bip32 = await this._getBIP32API(); const buf = await this._decodeBase58(senderPaymentCode); const version = buf[0]; @@ -699,6 +746,30 @@ export class QiHDWallet extends AbstractHDWallet { const senderPCodePublic = new PaymentCodePublic(this._ecc, bip32, buf.slice(1)); const receiverPCodePrivate = await this._getPaymentCodePrivate(0); - return receiverPCodePrivate.getPaymentAddress(senderPCodePublic, 0); + + const paymentCodeInfoArray = this._senderPaymentCodeInfo.get(senderPaymentCode); + const lastIndex = + paymentCodeInfoArray && paymentCodeInfoArray.length > 0 + ? paymentCodeInfoArray[paymentCodeInfoArray.length - 1].index + : 0; + + let addrIndex = lastIndex; + for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) { + const address = receiverPCodePrivate.getPaymentAddress(senderPCodePublic, addrIndex++); + if (this.isValidAddressForZone(address, zone)) { + if (paymentCodeInfoArray) { + paymentCodeInfoArray.push({ address, index: addrIndex, isUsed: false, zone }); + } else { + this._senderPaymentCodeInfo.set(senderPaymentCode, [ + { address, index: addrIndex, isUsed: false, zone }, + ]); + } + return address; + } + } + + throw new Error( + `Failed to derive a valid address for the zone ${zone} after ${MAX_ADDRESS_DERIVATION_ATTEMPTS} attempts.`, + ); } } From 6aa1453f3d077ba2ed156bddcb3a09f70c8a2042 Mon Sep 17 00:00:00 2001 From: Alejo Acosta Date: Mon, 16 Sep 2024 17:06:16 -0300 Subject: [PATCH 2/4] allow setting account for paymentcodes addr gen --- src/wallet/qi-hdwallet.ts | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index 9f415538..66c0bc96 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -41,6 +41,7 @@ interface paymentCodeInfo { index: number; isUsed: boolean; zone: Zone; + account: number; } /** @@ -637,7 +638,7 @@ export class QiHDWallet extends AbstractHDWallet { * @param {number} account - The account index to derive the payment code from. * @returns {Promise} A promise that resolves to the Base58-encoded BIP47 payment code. */ - public async getPaymentCode(account: number): Promise { + public async getPaymentCode(account: number = 0): Promise { const privatePcode = await this._getPaymentCodePrivate(account); return privatePcode.toBase58(); } @@ -695,13 +696,13 @@ export class QiHDWallet extends AbstractHDWallet { * @returns {Promise} A promise that resolves to the payment address for sending funds. * @throws {Error} Throws an error if the payment code version is invalid. */ - public async generateSendAddress(receiverPaymentCode: string, zone: Zone): Promise { + public async generateSendAddress(receiverPaymentCode: string, zone: Zone, account: number = 0): Promise { const bip32 = await this._getBIP32API(); const buf = await this._decodeBase58(receiverPaymentCode); const version = buf[0]; if (version !== PC_VERSION) throw new Error('Invalid payment code version'); - const receiverPCodePrivate = await this._getPaymentCodePrivate(0); + const receiverPCodePrivate = await this._getPaymentCodePrivate(account); const senderPCodePublic = new PaymentCodePublic(this._ecc, bip32, buf.slice(1)); const paymentCodeInfoArray = this._receiverPaymentCodeInfo.get(receiverPaymentCode); @@ -714,12 +715,17 @@ export class QiHDWallet extends AbstractHDWallet { for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) { const address = senderPCodePublic.getPaymentAddress(receiverPCodePrivate, addrIndex++); if (this.isValidAddressForZone(address, zone)) { + const pcInfo: paymentCodeInfo = { + address, + index: addrIndex, + account, + zone, + isUsed: false, + }; if (paymentCodeInfoArray) { - paymentCodeInfoArray.push({ address, index: addrIndex, isUsed: false, zone }); + paymentCodeInfoArray.push(pcInfo); } else { - this._receiverPaymentCodeInfo.set(receiverPaymentCode, [ - { address, index: addrIndex, isUsed: false, zone }, - ]); + this._receiverPaymentCodeInfo.set(receiverPaymentCode, [pcInfo]); } return address; } @@ -738,14 +744,14 @@ export class QiHDWallet extends AbstractHDWallet { * @returns {Promise} A promise that resolves to the payment address for receiving funds. * @throws {Error} Throws an error if the payment code version is invalid. */ - public async generateReceiveAddress(senderPaymentCode: string, zone: Zone): Promise { + public async generateReceiveAddress(senderPaymentCode: string, zone: Zone, account: number = 0): Promise { const bip32 = await this._getBIP32API(); const buf = await this._decodeBase58(senderPaymentCode); const version = buf[0]; if (version !== PC_VERSION) throw new Error('Invalid payment code version'); const senderPCodePublic = new PaymentCodePublic(this._ecc, bip32, buf.slice(1)); - const receiverPCodePrivate = await this._getPaymentCodePrivate(0); + const receiverPCodePrivate = await this._getPaymentCodePrivate(account); const paymentCodeInfoArray = this._senderPaymentCodeInfo.get(senderPaymentCode); const lastIndex = @@ -757,12 +763,17 @@ export class QiHDWallet extends AbstractHDWallet { for (let attempts = 0; attempts < MAX_ADDRESS_DERIVATION_ATTEMPTS; attempts++) { const address = receiverPCodePrivate.getPaymentAddress(senderPCodePublic, addrIndex++); if (this.isValidAddressForZone(address, zone)) { + const pcInfo: paymentCodeInfo = { + address, + index: addrIndex, + account, + zone, + isUsed: false, + }; if (paymentCodeInfoArray) { - paymentCodeInfoArray.push({ address, index: addrIndex, isUsed: false, zone }); + paymentCodeInfoArray.push(pcInfo); } else { - this._senderPaymentCodeInfo.set(senderPaymentCode, [ - { address, index: addrIndex, isUsed: false, zone }, - ]); + this._senderPaymentCodeInfo.set(senderPaymentCode, [pcInfo]); } return address; } From 0894612e36e4f4053dd8e8a5bf682220bf1e4b93 Mon Sep 17 00:00:00 2001 From: Alejo Acosta Date: Tue, 17 Sep 2024 16:22:09 -0300 Subject: [PATCH 3/4] replace tiny-secp256k1 with bitcoinerlab --- package-lock.json | 41 +++++++++++++++--------------- package.json | 2 +- src/_tests/unit/bip47.unit.test.ts | 1 + src/wallet/qi-hdwallet.ts | 18 +++++-------- 4 files changed, 29 insertions(+), 33 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5473649b..54bf2a7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0-alpha.10", "license": "MIT", "dependencies": { + "@bitcoinerlab/secp256k1": "^1.1.1", "@brandonblack/musig": "^0.0.1-alpha.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", @@ -16,7 +17,6 @@ "aes-js": "4.0.0-beta.5", "dotenv": "^16.4.1", "google-protobuf": "^3.21.4", - "tiny-secp256k1": "^2.2.3", "tslib": "^2.6.2", "ws": "^8.17.1" }, @@ -176,6 +176,15 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@bitcoinerlab/secp256k1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@bitcoinerlab/secp256k1/-/secp256k1-1.1.1.tgz", + "integrity": "sha512-uhjW51WfVLpnHN7+G0saDcM/k9IqcyTbZ+bDgLF3AX8V/a3KXSE9vn7UPBrcdU72tp0J4YPR7BHp2m7MLAZ/1Q==", + "dependencies": { + "@noble/hashes": "^1.1.5", + "@noble/secp256k1": "^1.7.1" + } + }, "node_modules/@brandonblack/musig": { "version": "0.0.1-alpha.1", "resolved": "https://registry.npmjs.org/@brandonblack/musig/-/musig-0.0.1-alpha.1.tgz", @@ -411,6 +420,17 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@noble/secp256k1": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@noble/secp256k1/-/secp256k1-1.7.1.tgz", + "integrity": "sha512-hOUk6AyBFmqVrv7k5WAw/LpszxVbj9gGN4JRkIX52fdFAj1UA61KXmZDvqVEm+pOyec3+fIeZB02LYa/pWOArw==", + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ] + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5975,17 +5995,6 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, - "node_modules/tiny-secp256k1": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.3.tgz", - "integrity": "sha512-SGcL07SxcPN2nGKHTCvRMkQLYPSoeFcvArUSCYtjVARiFAWU44cCIqYS0mYAU6nY7XfvwURuTIGo2Omt3ZQr0Q==", - "dependencies": { - "uint8array-tools": "0.0.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6268,14 +6277,6 @@ "node": ">=0.8.0" } }, - "node_modules/uint8array-tools": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", - "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index cd32e83c..4bd160d5 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "./lib/esm/wordlists/wordlists.js": "./lib/esm/wordlists/wordlists-browser.js" }, "dependencies": { + "@bitcoinerlab/secp256k1": "^1.1.1", "@brandonblack/musig": "^0.0.1-alpha.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", @@ -15,7 +16,6 @@ "aes-js": "4.0.0-beta.5", "dotenv": "^16.4.1", "google-protobuf": "^3.21.4", - "tiny-secp256k1": "^2.2.3", "tslib": "^2.6.2", "ws": "^8.17.1" }, diff --git a/src/_tests/unit/bip47.unit.test.ts b/src/_tests/unit/bip47.unit.test.ts index 480b6bb4..b229283c 100644 --- a/src/_tests/unit/bip47.unit.test.ts +++ b/src/_tests/unit/bip47.unit.test.ts @@ -2,6 +2,7 @@ import { Mnemonic, QiHDWallet, Zone } from '../../index.js'; import assert from 'assert'; describe('Test generation of payment codes and payment addresses', function () { + this.timeout(10000); const ALICE_MNEMONIC = 'empower cook violin million wool twelve involve nice donate author mammal salt royal shiver birth olympic embody hello beef suit isolate mixed text spot'; const aliceMnemonic = Mnemonic.fromPhrase(ALICE_MNEMONIC); diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index 66c0bc96..3491d06e 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -20,7 +20,8 @@ import { AllowedCoinType, Zone } from '../constants/index.js'; import { Mnemonic } from './mnemonic.js'; import { PaymentCodePrivate, PaymentCodePublic, PC_VERSION } from './payment-codes.js'; import { HDNodeBIP32Adapter } from './bip32-types.js'; -import type { TinySecp256k1Interface, BIP32API } from './bip32-types.js'; +import type { BIP32API } from './bip32-types.js'; +import ecc from '@bitcoinerlab/secp256k1'; /** * @property {Outpoint} outpoint - The outpoint object. @@ -137,9 +138,6 @@ export class QiHDWallet extends AbstractHDWallet { */ protected _outpoints: OutpointInfo[] = []; - //! Review this - private _ecc!: TinySecp256k1Interface; - /** * Map of paymentcodes to paymentCodeInfo for the receiver */ @@ -157,10 +155,6 @@ export class QiHDWallet extends AbstractHDWallet { */ constructor(guard: any, root: HDNodeWallet, provider?: Provider) { super(guard, root, provider); - //! Review this - import('tiny-secp256k1').then((ecc) => { - this._ecc = ecc; - }); } /** @@ -647,7 +641,7 @@ export class QiHDWallet extends AbstractHDWallet { private async _getBIP32API(): Promise { const module = await import('@samouraiwallet/bip32'); const { BIP32Factory } = module; - return BIP32Factory(this._ecc) as unknown as BIP32API; + return BIP32Factory(ecc) as BIP32API; } // helper method to decode a base58 string into a Uint8Array @@ -685,7 +679,7 @@ export class QiHDWallet extends AbstractHDWallet { const adapter = new HDNodeBIP32Adapter(accountNode); - return new PaymentCodePrivate(adapter, this._ecc, bip32, pc); + return new PaymentCodePrivate(adapter, ecc, bip32, pc); } /** @@ -703,7 +697,7 @@ export class QiHDWallet extends AbstractHDWallet { if (version !== PC_VERSION) throw new Error('Invalid payment code version'); const receiverPCodePrivate = await this._getPaymentCodePrivate(account); - const senderPCodePublic = new PaymentCodePublic(this._ecc, bip32, buf.slice(1)); + const senderPCodePublic = new PaymentCodePublic(ecc, bip32, buf.slice(1)); const paymentCodeInfoArray = this._receiverPaymentCodeInfo.get(receiverPaymentCode); const lastIndex = @@ -750,7 +744,7 @@ export class QiHDWallet extends AbstractHDWallet { const version = buf[0]; if (version !== PC_VERSION) throw new Error('Invalid payment code version'); - const senderPCodePublic = new PaymentCodePublic(this._ecc, bip32, buf.slice(1)); + const senderPCodePublic = new PaymentCodePublic(ecc, bip32, buf.slice(1)); const receiverPCodePrivate = await this._getPaymentCodePrivate(account); const paymentCodeInfoArray = this._senderPaymentCodeInfo.get(senderPaymentCode); From af0f3d9d0cea365337d21a00e745c3eed111b279 Mon Sep 17 00:00:00 2001 From: rileystephens28 Date: Wed, 18 Sep 2024 17:07:00 -0500 Subject: [PATCH 4/4] Port over BIP32 logic from samourai wallet --- package-lock.json | 141 +----- package.json | 2 +- src/wallet/bip32/bip32.ts | 423 ++++++++++++++++++ src/wallet/bip32/crypto.ts | 15 + src/wallet/bip32/testecc.ts | 131 ++++++ src/wallet/{bip32-types.ts => bip32/types.ts} | 14 +- src/wallet/bip32/uint8array-utils.ts | 17 + src/wallet/payment-codes.ts | 7 +- src/wallet/qi-hdwallet.ts | 8 +- 9 files changed, 603 insertions(+), 155 deletions(-) create mode 100644 src/wallet/bip32/bip32.ts create mode 100644 src/wallet/bip32/crypto.ts create mode 100644 src/wallet/bip32/testecc.ts rename src/wallet/{bip32-types.ts => bip32/types.ts} (94%) create mode 100644 src/wallet/bip32/uint8array-utils.ts diff --git a/package-lock.json b/package-lock.json index 54bf2a7a..44e6286b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@brandonblack/musig": "^0.0.1-alpha.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", - "@samouraiwallet/bip32": "^5.1.0", + "@scure/base": "^1.1.9", "aes-js": "4.0.0-beta.5", "dotenv": "^16.4.1", "google-protobuf": "^3.21.4", @@ -721,35 +721,10 @@ "win32" ] }, - "node_modules/@samouraiwallet/bip32": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@samouraiwallet/bip32/-/bip32-5.1.0.tgz", - "integrity": "sha512-wyNb1sjLEtVWPcmVP+xNFZPAJ1Pd4oRGjlVFzVQh8S7Eg9OIbrLAzhvkOBcH0/tPrrJWrMB+IOCU9BWlpJBmYA==", - "dependencies": { - "@noble/hashes": "1.4.0", - "@scure/base": "1.1.6", - "ow": "1.1.1", - "wif": "4.0.0" - }, - "engines": { - "node": ">=16.6.0" - } - }, - "node_modules/@samouraiwallet/bip32/node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@scure/base": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.6.tgz", - "integrity": "sha512-ok9AWwhcgYuGG3Zfhyqg+zwl+Wn5uE+dwC0NV/2qQkx4dABbb/bx96vWu8NSj+BNjjSjno+JRYRjle1jV08k3g==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", "funding": { "url": "https://paulmillr.com/funding/" } @@ -760,17 +735,6 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, - "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1461,11 +1425,6 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, - "node_modules/base-x": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.0.tgz", - "integrity": "sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==" - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -1511,23 +1470,6 @@ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", "dev": true }, - "node_modules/bs58": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", - "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", - "dependencies": { - "base-x": "^4.0.0" - } - }, - "node_modules/bs58check": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", - "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", - "dependencies": { - "@noble/hashes": "^1.2.0", - "bs58": "^5.0.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -2082,31 +2024,6 @@ "node": ">=6.0.0" } }, - "node_modules/dot-prop": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-7.2.0.tgz", - "integrity": "sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA==", - "dependencies": { - "type-fest": "^2.11.2" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dot-prop/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/dotenv": { "version": "16.4.5", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", @@ -4034,11 +3951,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5083,35 +4995,6 @@ "node": ">= 0.8.0" } }, - "node_modules/ow": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ow/-/ow-1.1.1.tgz", - "integrity": "sha512-sJBRCbS5vh1Jp9EOgwp1Ws3c16lJrUkJYlvWTYC03oyiYVwS/ns7lKRWow4w4XjDyTrA2pplQv4B2naWSR6yDA==", - "dependencies": { - "@sindresorhus/is": "^5.3.0", - "callsites": "^4.0.0", - "dot-prop": "^7.2.0", - "lodash.isequal": "^4.5.0", - "vali-date": "^1.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ow/node_modules/callsites": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-4.2.0.tgz", - "integrity": "sha512-kfzR4zzQtAE9PC7CzZsjl3aBNbXWuXiSeOCdLcPpBfGW8YuCqQHcRPFDbr/BPVmd3EEPVpuFzLyuT/cUhPr4OQ==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -6329,14 +6212,6 @@ "node": ">=10.12.0" } }, - "node_modules/vali-date": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/vali-date/-/vali-date-1.0.0.tgz", - "integrity": "sha512-sgECfZthyaCKW10N0fm27cg8HYTFK5qMWgypqkXMQ4Wbl/zZKx7xZICgcoxIIE+WFAP/MBL2EFwC/YvLxw3Zeg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -6401,14 +6276,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/wif": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/wif/-/wif-4.0.0.tgz", - "integrity": "sha512-kADznC+4AFJNXpT8rLhbsfI7EmAcorc5nWvAdKUchGmwXEBD3n55q0/GZ3DBmc6auAvuTSsr/utiKizuXdNYOQ==", - "dependencies": { - "bs58check": "^3.0.1" - } - }, "node_modules/workerpool": { "version": "6.2.1", "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", diff --git a/package.json b/package.json index 4bd160d5..1ba3460e 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "@brandonblack/musig": "^0.0.1-alpha.1", "@noble/curves": "1.2.0", "@noble/hashes": "1.3.2", - "@samouraiwallet/bip32": "^5.1.0", + "@scure/base": "^1.1.9", "aes-js": "4.0.0-beta.5", "dotenv": "^16.4.1", "google-protobuf": "^3.21.4", diff --git a/src/wallet/bip32/bip32.ts b/src/wallet/bip32/bip32.ts new file mode 100644 index 00000000..c7e130f4 --- /dev/null +++ b/src/wallet/bip32/bip32.ts @@ -0,0 +1,423 @@ +import * as crypto from './crypto.js'; +import { testEcc } from './testecc.js'; +// import ow from 'ow'; +import { BIP32API, Network } from './types.js'; + +interface Bip32SignerConstructor { + __D?: Uint8Array; + __Q?: Uint8Array; +} + +interface BIP32Constructor extends Bip32SignerConstructor { + chainCode: Uint8Array; + network: Network; + __DEPTH?: number; + __INDEX?: number; + __PARENT_FINGERPRINT?: number; +} + +export function BIP32Factory(ecc: any): BIP32API { + testEcc(ecc); + // const UINT256_TYPE = ow.uint8Array.length(32); + // const NETWORK_TYPE = ow.object.partialShape({ + // wif: ow.number.uint8, + // bip32: ow.object.exactShape({ + // public: ow.number.uint32, + // private: ow.number.uint32, + // }), + // }); + const BITCOIN: Network = { + bip32: { + public: 0x0488b21e, + private: 0x0488ade4, + }, + wif: 0x80, + }; + const HIGHEST_BIT = 0x80000000; + // const UINT31_MAX = Math.pow(2, 31) - 1; + + function toXOnly(pubKey: Uint8Array): Uint8Array { + return pubKey.length === 32 ? pubKey : pubKey.subarray(1, 33); + } + + class Bip32Signer { + protected __D?: Uint8Array; + protected __Q?: Uint8Array; + public lowR: boolean; + + constructor({ __D, __Q }: Bip32SignerConstructor) { + this.__D = __D; + this.__Q = __Q; + this.lowR = false; + } + + get publicKey(): Uint8Array { + if (this.__Q === undefined) this.__Q = ecc.pointFromScalar(this.__D, true); + return this.__Q as Uint8Array; + } + + get privateKey(): Uint8Array | undefined { + return this.__D; + } + + sign(hash: Uint8Array, lowR?: boolean): Uint8Array { + if (!this.privateKey) throw new Error('Missing private key'); + if (lowR === undefined) lowR = this.lowR; + if (lowR === false) { + return ecc.sign(hash, this.privateKey); + } else { + let sig = ecc.sign(hash, this.privateKey); + const extraData = new Uint8Array(32); + const extraDataView = new DataView(extraData.buffer); + let counter = 0; + // if first try is lowR, skip the loop + // for second try and on, add extra entropy counting up + while (sig[0] > 0x7f) { + counter++; + extraDataView.setUint32(0, counter, true); + sig = ecc.sign(hash, this.privateKey, extraData); + } + return sig; + } + } + + signSchnorr(hash: Uint8Array): Uint8Array { + if (!this.privateKey) throw new Error('Missing private key'); + if (!ecc.signSchnorr) throw new Error('signSchnorr not supported by ecc library'); + return ecc.signSchnorr(hash, this.privateKey); + } + + verify(hash: Uint8Array, signature: Uint8Array): boolean { + return ecc.verify(hash, this.publicKey, signature); + } + + verifySchnorr(hash: Uint8Array, signature: Uint8Array): boolean { + if (!ecc.verifySchnorr) throw new Error('verifySchnorr not supported by ecc library'); + return ecc.verifySchnorr(hash, this.publicKey.subarray(1, 33), signature); + } + } + + class BIP32 extends Bip32Signer { + public chainCode: Uint8Array; + public network: Network; + private __DEPTH: number; + private __INDEX: number; + private __PARENT_FINGERPRINT: number; + + constructor({ + __D, + __Q, + chainCode, + network, + __DEPTH = 0, + __INDEX = 0, + __PARENT_FINGERPRINT = 0x00000000, + }: BIP32Constructor) { + super({ __D, __Q }); + this.chainCode = chainCode; + this.network = network; + this.__DEPTH = __DEPTH; + this.__INDEX = __INDEX; + this.__PARENT_FINGERPRINT = __PARENT_FINGERPRINT; + // ow(network, NETWORK_TYPE); + } + + get depth(): number { + return this.__DEPTH; + } + + get index(): number { + return this.__INDEX; + } + + get parentFingerprint(): number { + return this.__PARENT_FINGERPRINT; + } + + get identifier(): Uint8Array { + return crypto.hash160(this.publicKey); + } + + get fingerprint(): Uint8Array { + return this.identifier.subarray(0, 4); + } + + get compressed(): boolean { + return true; + } + + isNeutered(): boolean { + return this.__D === undefined; + } + + neutered(): BIP32 { + return fromPublicKeyLocal( + this.publicKey, + this.chainCode, + this.network, + this.depth, + this.index, + this.parentFingerprint, + ); + } + + toBase58(): string { + const network = this.network; + const version = !this.isNeutered() ? network.bip32.private : network.bip32.public; + const buffer = new Uint8Array(78); + const bufferView = new DataView(buffer.buffer); + // 4 bytes: version bytes + bufferView.setUint32(0, version, false); + // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 descendants, .... + bufferView.setUint8(4, this.depth); + // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) + bufferView.setUint32(5, this.parentFingerprint, false); + // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. + // This is encoded in big endian. (0x00000000 if master key) + bufferView.setUint32(9, this.index, false); + // 32 bytes: the chain code + buffer.set(this.chainCode, 13); + // 33 bytes: the public key or private key data + if (!this.isNeutered()) { + // 0x00 + k for private keys + bufferView.setUint8(45, 0); + buffer.set(this.privateKey as Uint8Array, 46); + // 33 bytes: the public key + } else { + // X9.62 encoding for public keys + buffer.set(this.publicKey, 45); + } + return crypto.bs58check.encode(buffer); + } + + derive(index: number): BIP32 { + // ow(index, ow.number.message('Expected UInt32').uint32.message('Expected UInt32')); + const isHardened = index >= HIGHEST_BIT; + const data = new Uint8Array(37); + const dataView = new DataView(data.buffer); + // Hardened child + if (isHardened) { + if (this.isNeutered()) throw new TypeError('Missing private key for hardened child key'); + // data = 0x00 || ser256(kpar) || ser32(index) + data[0] = 0x00; + data.set(this.privateKey as Uint8Array, 1); + dataView.setUint32(33, index, false); + // Normal child + } else { + // data = serP(point(kpar)) || ser32(index) + // = serP(Kpar) || ser32(index) + data.set(this.publicKey, 0); + dataView.setUint32(33, index, false); + } + const I = crypto.hmacSHA512(this.chainCode, data); + const IL = I.slice(0, 32); + const IR = I.slice(32); + // if parse256(IL) >= n, proceed with the next value for i + if (!ecc.isPrivate(IL)) return this.derive(index + 1); + // Private parent key -> private child key + let hd: BIP32; + if (!this.isNeutered()) { + // ki = parse256(IL) + kpar (mod n) + const ki = ecc.privateAdd(this.privateKey, IL); + // In case ki == 0, proceed with the next value for i + if (ki == null) return this.derive(index + 1); + hd = fromPrivateKeyLocal( + ki, + IR, + this.network, + this.depth + 1, + index, + new DataView(this.fingerprint.buffer).getUint32(0, false), + ); + // Public parent key -> public child key + } else { + // Ki = point(parse256(IL)) + Kpar + // = G*IL + Kpar + const Ki = ecc.pointAddScalar(this.publicKey, IL, true); + // In case Ki is the point at infinity, proceed with the next value for i + if (Ki === null) return this.derive(index + 1); + hd = fromPublicKeyLocal( + Ki, + IR, + this.network, + this.depth + 1, + index, + new DataView(this.fingerprint.buffer).getUint32(0, false), + ); + } + return hd; + } + + deriveHardened(index: number): BIP32 { + // ow(index, ow.number + // .message('Expected UInt31') + // .uint32.message('Expected UInt31') + // .is(value => value <= UINT31_MAX) + // .message('Expected UInt31')); + // Only derives hardened private keys by default + return this.derive(index + HIGHEST_BIT); + } + + derivePath(path: string): BIP32 { + // ow(path, ow.string + // .is(value => value.match(/^(m\/)?(\d+'?\/)*\d+'?$/) !== null) + // .message(value => `Expected BIP32Path, got ${value}`)); + let splitPath = path.split('/'); + if (splitPath[0] === 'm') { + if (this.parentFingerprint) throw new TypeError('Expected master, got child'); + splitPath = splitPath.slice(1); + } + return splitPath.reduce((prevHd: BIP32, indexStr: string) => { + let index; + if (indexStr.slice(-1) === `'`) { + index = parseInt(indexStr.slice(0, -1), 10); + return prevHd.deriveHardened(index); + } else { + index = parseInt(indexStr, 10); + return prevHd.derive(index); + } + }, this); + } + + tweak(t: Uint8Array): Bip32Signer { + if (this.privateKey) return this.tweakFromPrivateKey(t); + return this.tweakFromPublicKey(t); + } + + tweakFromPublicKey(t: Uint8Array): Bip32Signer { + const xOnlyPubKey = toXOnly(this.publicKey); + if (!ecc.xOnlyPointAddTweak) throw new Error('xOnlyPointAddTweak not supported by ecc library'); + const tweakedPublicKey = ecc.xOnlyPointAddTweak(xOnlyPubKey, t); + if (!tweakedPublicKey || tweakedPublicKey.xOnlyPubkey === null) throw new Error('Cannot tweak public key!'); + const parityByte = Uint8Array.from([tweakedPublicKey.parity === 0 ? 0x02 : 0x03]); + const tweakedPublicKeyCompresed = new Uint8Array(tweakedPublicKey.xOnlyPubkey.length + 1); + tweakedPublicKeyCompresed.set(parityByte); + tweakedPublicKeyCompresed.set(tweakedPublicKey.xOnlyPubkey, 1); + return new Bip32Signer({ __Q: tweakedPublicKeyCompresed }); + } + + tweakFromPrivateKey(t: Uint8Array): Bip32Signer { + const hasOddY = this.publicKey[0] === 3 || (this.publicKey[0] === 4 && (this.publicKey[64] & 1) === 1); + const privateKey = (() => { + if (!hasOddY) return this.privateKey; + else if (!ecc.privateNegate) throw new Error('privateNegate not supported by ecc library'); + else return ecc.privateNegate(this.privateKey); + })(); + const tweakedPrivateKey = ecc.privateAdd(privateKey, t); + if (!tweakedPrivateKey) throw new Error('Invalid tweaked private key!'); + return new Bip32Signer({ __D: tweakedPrivateKey }); + } + } + + function fromBase58(inString: string, network?: Network): BIP32 { + const buffer = crypto.bs58check.decode(inString); + const bufferView = new DataView(buffer.buffer); + if (buffer.length !== 78) throw new TypeError('Invalid buffer length'); + network = network || BITCOIN; + // 4 bytes: version bytes + const version = bufferView.getUint32(0, false); + if (version !== network.bip32.private && version !== network.bip32.public) + throw new TypeError('Invalid network version'); + // 1 byte: depth: 0x00 for master nodes, 0x01 for level-1 descendants, ... + const depth = buffer[4]; + // 4 bytes: the fingerprint of the parent's key (0x00000000 if master key) + const parentFingerprint = bufferView.getUint32(5, false); + if (depth === 0) { + if (parentFingerprint !== 0x00000000) throw new TypeError('Invalid parent fingerprint'); + } + // 4 bytes: child number. This is the number i in xi = xpar/i, with xi the key being serialized. + // This is encoded in MSB order. (0x00000000 if master key) + const index = bufferView.getUint32(9, false); + if (depth === 0 && index !== 0) throw new TypeError('Invalid index'); + // 32 bytes: the chain code + const chainCode = buffer.subarray(13, 45); + let hd: BIP32; + // 33 bytes: private key data (0x00 + k) + if (version === network.bip32.private) { + if (bufferView.getUint8(45) !== 0x00) throw new TypeError('Invalid private key'); + const k = buffer.subarray(46, 78); + hd = fromPrivateKeyLocal(k, chainCode, network, depth, index, parentFingerprint); + // 33 bytes: public key data (0x02 + X or 0x03 + X) + } else { + const X = buffer.subarray(45, 78); + hd = fromPublicKeyLocal(X, chainCode, network, depth, index, parentFingerprint); + } + return hd; + } + + function fromPrivateKey(privateKey: Uint8Array, chainCode: Uint8Array, network?: Network): BIP32 { + return fromPrivateKeyLocal(privateKey, chainCode, network || BITCOIN, 0, 0, 0); + } + + function fromPrivateKeyLocal( + privateKey: Uint8Array, + chainCode: Uint8Array, + network: Network, + depth: number, + index: number, + parentFingerprint: number, + ): BIP32 { + // ow({ privateKey, chainCode }, ow.object.exactShape({ + // privateKey: UINT256_TYPE, + // chainCode: UINT256_TYPE, + // })); + network = network || BITCOIN; + if (!ecc.isPrivate(privateKey)) throw new TypeError('Private key not in range [1, n)'); + return new BIP32({ + __D: privateKey, + chainCode, + network, + __DEPTH: depth, + __INDEX: index, + __PARENT_FINGERPRINT: parentFingerprint, + }); + } + + function fromPublicKey(publicKey: Uint8Array, chainCode: Uint8Array, network?: Network): BIP32 { + return fromPublicKeyLocal(publicKey, chainCode, network || BITCOIN, 0, 0, 0); + } + + function fromPublicKeyLocal( + publicKey: Uint8Array, + chainCode: Uint8Array, + network: Network, + depth: number, + index: number, + parentFingerprint: number, + ): BIP32 { + // ow({ publicKey, chainCode }, ow.object.exactShape({ + // publicKey: ow.uint8Array.length(33), + // chainCode: UINT256_TYPE, + // })); + network = network || BITCOIN; + // verify the X coordinate is a point on the curve + if (!ecc.isPoint(publicKey)) throw new TypeError('Point is not on the curve'); + return new BIP32({ + __Q: publicKey, + chainCode, + network, + __DEPTH: depth, + __INDEX: index, + __PARENT_FINGERPRINT: parentFingerprint, + }); + } + + function fromSeed(seed: Uint8Array, network?: Network): BIP32 { + // ow(seed, ow.uint8Array); + if (seed.length < 16) throw new TypeError('Seed should be at least 128 bits'); + if (seed.length > 64) throw new TypeError('Seed should be at most 512 bits'); + network = network || BITCOIN; + const encoder = new TextEncoder(); + const I = crypto.hmacSHA512(encoder.encode('Bitcoin seed'), seed); + const IL = I.slice(0, 32); + const IR = I.slice(32); + return fromPrivateKey(IL, IR, network); + } + + return { + fromSeed, + fromBase58, + fromPublicKey, + fromPrivateKey, + }; +} diff --git a/src/wallet/bip32/crypto.ts b/src/wallet/bip32/crypto.ts new file mode 100644 index 00000000..104d1bba --- /dev/null +++ b/src/wallet/bip32/crypto.ts @@ -0,0 +1,15 @@ +import { hmac } from '@noble/hashes/hmac'; +import { ripemd160 } from '@noble/hashes/ripemd160'; +import { sha256 } from '@noble/hashes/sha256'; +import { sha512 } from '@noble/hashes/sha512'; +import { base58check } from '@scure/base'; + +export const bs58check = base58check(sha256); + +export function hash160(buffer: Uint8Array): Uint8Array { + return ripemd160(sha256(buffer)); +} + +export function hmacSHA512(key: Uint8Array | string, data: Uint8Array | string): Uint8Array { + return hmac(sha512, key, data); +} diff --git a/src/wallet/bip32/testecc.ts b/src/wallet/bip32/testecc.ts new file mode 100644 index 00000000..cbde7466 --- /dev/null +++ b/src/wallet/bip32/testecc.ts @@ -0,0 +1,131 @@ +import { hexToBytes } from '@noble/hashes/utils'; +import { areUint8ArraysEqual } from './uint8array-utils.js'; +const h = (hex: string): Uint8Array => hexToBytes(hex); + +export function testEcc(ecc: any) { + assert(ecc.isPoint(h('0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))); + assert(!ecc.isPoint(h('030000000000000000000000000000000000000000000000000000000000000005'))); + assert(ecc.isPrivate(h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'))); + // order - 1 + assert(ecc.isPrivate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'))); + // 0 + assert(!ecc.isPrivate(h('0000000000000000000000000000000000000000000000000000000000000000'))); + // order + assert(!ecc.isPrivate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'))); + // order + 1 + assert(!ecc.isPrivate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142'))); + assert( + areUint8ArraysEqual( + ecc.pointFromScalar(h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af')), + h('02b07ba9dca9523b7ef4bd97703d43d20399eb698e194704791a25ce77a400df99'), + ), + ); + if (ecc.xOnlyPointAddTweak) { + assert( + ecc.xOnlyPointAddTweak( + h('79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ) === null, + ); + let xOnlyRes = ecc.xOnlyPointAddTweak( + h('1617d38ed8d8657da4d4761e8057bc396ea9e4b9d29776d4be096016dbd2509b'), + h('a8397a935f0dfceba6ba9618f6451ef4d80637abf4e6af2669fbc9de6a8fd2ac'), + ); + assert( + areUint8ArraysEqual( + xOnlyRes.xOnlyPubkey, + h('e478f99dab91052ab39a33ea35fd5e6e4933f4d28023cd597c9a1f6760346adf'), + ) && xOnlyRes.parity === 1, + ); + xOnlyRes = ecc.xOnlyPointAddTweak( + h('2c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'), + h('823c3cd2142744b075a87eade7e1b8678ba308d566226a0056ca2b7a76f86b47'), + ); + } + assert( + areUint8ArraysEqual( + ecc.pointAddScalar( + h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h('0000000000000000000000000000000000000000000000000000000000000003'), + ), + h('02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5'), + ), + ); + assert( + areUint8ArraysEqual( + ecc.privateAdd( + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e'), + h('0000000000000000000000000000000000000000000000000000000000000002'), + ), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ); + if (ecc.privateNegate) { + assert( + areUint8ArraysEqual( + ecc.privateNegate(h('0000000000000000000000000000000000000000000000000000000000000001')), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + ); + assert( + areUint8ArraysEqual( + ecc.privateNegate(h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd036413e')), + h('0000000000000000000000000000000000000000000000000000000000000003'), + ), + ); + assert( + areUint8ArraysEqual( + ecc.privateNegate(h('b1121e4088a66a28f5b6b0f5844943ecd9f610196d7bb83b25214b60452c09af')), + h('4eede1bf775995d70a494f0a7bb6bc11e0b8cccd41cce8009ab1132c8b0a3792'), + ), + ); + } + assert( + areUint8ArraysEqual( + ecc.sign( + h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), + h('fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364140'), + ), + h( + '54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5', + ), + ), + ); + assert( + ecc.verify( + h('5e9f0a0d593efdcf78ac923bc3313e4e7d408d574354ee2b3288c0da9fbba6ed'), + h('0379be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798'), + h( + '54c4a33c6423d689378f160a7ff8b61330444abb58fb470f96ea16d99d4a2fed07082304410efa6b2943111b6a4e0aaa7b7db55a07e9861d1fb3cb1f421044a5', + ), + ), + ); + if (ecc.signSchnorr) { + assert( + areUint8ArraysEqual( + ecc.signSchnorr( + h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), + h('c90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b14e5c9'), + h('c87aa53824b4d7ae2eb035a2b5bbbccc080e76cdc6d1692c4b0b62d798e6d906'), + ), + h( + '5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7', + ), + ), + ); + } + if (ecc.verifySchnorr) { + assert( + ecc.verifySchnorr( + h('7e2d58d8b3bcdf1abadec7829054f90dda9805aab56c77333024b9d0a508b75c'), + h('dd308afec5777e13121fa72b9cc1b7cc0139715309b086c960e18fd969774eb8'), + h( + '5831aaeed7b44bb74e5eab94ba9d4294c49bcf2a60728d8b4c200f50dd313c1bab745879a5ad954a72c45a91c3a51d3c7adea98d82f8481e0e1e03674a6f3fb7', + ), + ), + ); + } +} +function assert(bool: boolean) { + if (!bool) throw new Error('ecc library invalid'); +} diff --git a/src/wallet/bip32-types.ts b/src/wallet/bip32/types.ts similarity index 94% rename from src/wallet/bip32-types.ts rename to src/wallet/bip32/types.ts index a6dc1601..89063e84 100644 --- a/src/wallet/bip32-types.ts +++ b/src/wallet/bip32/types.ts @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { HDNodeWallet } from './hdnodewallet.js'; -import { getBytes } from '../utils/data.js'; +import { HDNodeWallet } from '../hdnodewallet.js'; +import { getBytes } from '../../utils/data.js'; interface XOnlyPointAddTweakResult { parity: 1 | 0; xOnlyPubkey: Uint8Array; } -interface TinySecp256k1InterfaceBIP32 { +export interface TinySecp256k1InterfaceBIP32 { isPoint(p: Uint8Array): boolean; isPrivate(d: Uint8Array): boolean; pointFromScalar(d: Uint8Array, compressed?: boolean): Uint8Array | null; @@ -20,15 +20,11 @@ interface TinySecp256k1InterfaceBIP32 { privateNegate?(d: Uint8Array): Uint8Array; } -interface Network { - messagePrefix: string; - bech32: string; +export interface Network { bip32: { public: number; private: number; }; - pubKeyHash: number; - scriptHash: number; wif: number; } @@ -40,7 +36,7 @@ export interface TinySecp256k1Interface extends TinySecp256k1InterfaceBIP32 { xOnlyPointFromPoint(p: Uint8Array): Uint8Array; } -interface SignerBIP32 { +export interface SignerBIP32 { publicKey: Uint8Array; lowR: boolean; sign(hash: Uint8Array, lowR?: boolean): Uint8Array; diff --git a/src/wallet/bip32/uint8array-utils.ts b/src/wallet/bip32/uint8array-utils.ts new file mode 100644 index 00000000..30de1e12 --- /dev/null +++ b/src/wallet/bip32/uint8array-utils.ts @@ -0,0 +1,17 @@ +/** + * Uint8Array comparison + */ +export function areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a === b) { + return true; + } + if (a.length !== b.length) { + return false; + } + for (let index = 0; index < a.length; index++) { + if (a[index] !== b[index]) { + return false; + } + } + return true; +} diff --git a/src/wallet/payment-codes.ts b/src/wallet/payment-codes.ts index 3e76637d..c5371ea3 100644 --- a/src/wallet/payment-codes.ts +++ b/src/wallet/payment-codes.ts @@ -1,9 +1,10 @@ -import { BIP32API, BIP32Interface, HDNodeBIP32Adapter } from './bip32-types'; import { sha256 } from '@noble/hashes/sha256'; import { keccak256 } from '../crypto/index.js'; import { getBytes, hexlify } from '../utils/data.js'; import { getAddress } from '../address/address.js'; -import type { TinySecp256k1Interface } from './bip32-types.js'; +import { bs58check } from './bip32/crypto.js'; +import { HDNodeBIP32Adapter } from './bip32/types.js'; +import type { TinySecp256k1Interface, BIP32API, BIP32Interface } from './bip32/types.js'; export const PC_VERSION = 0x47; @@ -83,7 +84,7 @@ export class PaymentCodePublic { buf.set(version); buf.set(this.buf, version.length); - const { bs58check } = await import('@samouraiwallet/bip32/crypto'); + // const { bs58check } = await import('@samouraiwallet/bip32/crypto'); return bs58check.encode(buf); } diff --git a/src/wallet/qi-hdwallet.ts b/src/wallet/qi-hdwallet.ts index 3491d06e..f69995ac 100644 --- a/src/wallet/qi-hdwallet.ts +++ b/src/wallet/qi-hdwallet.ts @@ -19,8 +19,9 @@ import { getZoneForAddress } from '../utils/index.js'; import { AllowedCoinType, Zone } from '../constants/index.js'; import { Mnemonic } from './mnemonic.js'; import { PaymentCodePrivate, PaymentCodePublic, PC_VERSION } from './payment-codes.js'; -import { HDNodeBIP32Adapter } from './bip32-types.js'; -import type { BIP32API } from './bip32-types.js'; +import { BIP32Factory } from './bip32/bip32.js'; +import { bs58check } from './bip32/crypto.js'; +import { type BIP32API, HDNodeBIP32Adapter } from './bip32/types.js'; import ecc from '@bitcoinerlab/secp256k1'; /** @@ -639,14 +640,11 @@ export class QiHDWallet extends AbstractHDWallet { // helper method to get a bip32 API instance private async _getBIP32API(): Promise { - const module = await import('@samouraiwallet/bip32'); - const { BIP32Factory } = module; return BIP32Factory(ecc) as BIP32API; } // helper method to decode a base58 string into a Uint8Array private async _decodeBase58(base58: string): Promise { - const { bs58check } = await import('@samouraiwallet/bip32/crypto'); return bs58check.decode(base58); }