From 0c0825de17a3d8dde567e62e4ef7a46692ed95ce Mon Sep 17 00:00:00 2001 From: homura Date: Tue, 5 Mar 2024 19:03:28 +0800 Subject: [PATCH 1/2] feat!: replace `Buffer` with `Uint8Array` --- packages/base/src/utils.ts | 37 +- packages/base/src/values.ts | 2 +- .../examples/pw_lock/config.json | 50 --- .../common-scripts/examples/pw_lock/lock.ts | 389 ------------------ packages/common-scripts/src/common.ts | 2 +- packages/hd/package.json | 2 + packages/hd/src/extended_key.ts | 33 +- packages/hd/src/helper.ts | 9 + packages/hd/src/key.ts | 19 +- packages/hd/src/keychain.ts | 68 +-- packages/hd/src/keystore.ts | 95 +++-- packages/hd/src/mnemonic/index.ts | 38 +- packages/hd/tests/key.test.ts | 3 +- packages/hd/tests/keychain.test.ts | 14 + packages/hd/tests/keystore.test.ts | 14 + pnpm-lock.yaml | 10 + 16 files changed, 191 insertions(+), 594 deletions(-) delete mode 100644 packages/common-scripts/examples/pw_lock/config.json delete mode 100644 packages/common-scripts/examples/pw_lock/lock.ts diff --git a/packages/base/src/utils.ts b/packages/base/src/utils.ts index 8f7a60260..14d158c00 100644 --- a/packages/base/src/utils.ts +++ b/packages/base/src/utils.ts @@ -6,6 +6,7 @@ import { BI, BIish } from "@ckb-lumos/bi"; import * as blockchain from "./blockchain"; import { Script, Input } from "./api"; import { Hash, HexNumber, HexString } from "./primitive"; +import { Uint128LE, Uint64LE } from "@ckb-lumos/codec/lib/number"; type CKBHasherOptions = { outLength?: number; @@ -54,7 +55,7 @@ function computeScriptHash(script: Script): string { return ckbHash(blockchain.Script.pack(script)); } -function hashCode(buffer: Buffer): number { +function hashCode(buffer: Uint8Array): number { return xxHash32(buffer, 0); } @@ -69,11 +70,7 @@ function toBigUInt64LE(num: BIish): HexString { function toBigUInt64LECompatible(num: BIish): HexString { num = BI.from(num); - const buf = Buffer.alloc(8); - buf.writeUInt32LE(num.and("0xffffffff").toNumber(), 0); - num = num.shr(32); - buf.writeUInt32LE(num.and("0xffffffff").toNumber(), 4); - return `0x${buf.toString("hex")}`; + return bytes.hexify(Uint64LE.pack(num)); } /** @@ -90,8 +87,7 @@ function readBigUInt64LE(hex: HexString): bigint { * @deprecated please follow the {@link https://lumos-website.vercel.app/migrations/migrate-to-v0.19 migration-guide} */ function readBigUInt64LECompatible(hex: HexString): BI { - const buf = Buffer.from(hex.slice(2), "hex"); - return BI.from(buf.readUInt32LE()).add(BI.from(buf.readUInt32LE(4)).shl(32)); + return Uint64LE.unpack(hex); } // const U128_MIN = BigInt(0); @@ -117,18 +113,7 @@ function toBigUInt128LECompatible(num: BIish): HexNumber { if (num.gt(U128_MAX_COMPATIBLE)) { throw new Error(`u128 ${num} too large`); } - - const buf = Buffer.alloc(16); - buf.writeUInt32LE(num.and(0xffffffff).toNumber(), 0); - num = num.shr(32); - buf.writeUInt32LE(num.and(0xffffffff).toNumber(), 4); - - num = num.shr(32); - buf.writeUInt32LE(num.and(0xffffffff).toNumber(), 8); - - num = num.shr(32); - buf.writeUInt32LE(num.and(0xffffffff).toNumber(), 12); - return `0x${buf.toString("hex")}`; + return bytes.hexify(Uint128LE.pack(num)); } /** @@ -145,17 +130,7 @@ function readBigUInt128LE(leHex: HexString): bigint { * @deprecated please follow the {@link https://lumos-website.vercel.app/migrations/migrate-to-v0.19 migration-guide} */ function readBigUInt128LECompatible(leHex: HexString): BI { - if (leHex.length < 34 || !leHex.startsWith("0x")) { - throw new Error(`leHex format error`); - } - - const buf = Buffer.from(leHex.slice(2, 34), "hex"); - - return BI.from(buf.readUInt32LE(0)) - .shl(0) - .add(BI.from(buf.readUInt32LE(4)).shl(32)) - .add(BI.from(buf.readUInt32LE(8)).shl(64)) - .add(BI.from(buf.readUInt32LE(12)).shl(96)); + return Uint128LE.unpack(bytes.bytify(leHex).slice(0, 16)); } function assertHexString(debugPath: string, str: string): void { diff --git a/packages/base/src/values.ts b/packages/base/src/values.ts index 480a6622c..e85154016 100644 --- a/packages/base/src/values.ts +++ b/packages/base/src/values.ts @@ -16,7 +16,7 @@ class Value { } hashCode(): number { - return xxHash32(Buffer.from(this.buffer), 0); + return xxHash32(this.buffer, 0); } hash(): Hash { diff --git a/packages/common-scripts/examples/pw_lock/config.json b/packages/common-scripts/examples/pw_lock/config.json deleted file mode 100644 index e5a344822..000000000 --- a/packages/common-scripts/examples/pw_lock/config.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "__COMMENT__": "It's a devnet config, different devnet has different config. Set env.LUMOS_CONFIG_FILE to this config file (or your own config file) and call `initializeConfig()`, see ./lock.ts#L349-L350 as an example.", - "PREFIX": "ckt", - "SCRIPTS": { - "SECP256K1_BLAKE160": { - "CODE_HASH": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8", - "HASH_TYPE": "type", - "TX_HASH": "0x785aa819c8f9f8565a62f744685f8637c1b34886e57154e4e5a2ac7f225c7bf5", - "INDEX": "0x0", - "DEP_TYPE": "depGroup", - "SHORT_ID": 0 - }, - "SECP256K1_BLAKE160_MULTISIG": { - "CODE_HASH": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", - "HASH_TYPE": "type", - "TX_HASH": "0x785aa819c8f9f8565a62f744685f8637c1b34886e57154e4e5a2ac7f225c7bf5", - "INDEX": "0x1", - "DEP_TYPE": "depGroup", - "SHORT_ID": 1 - }, - "DAO": { - "CODE_HASH": "0x82d76d1b75fe2fd9a27dfbaa65a039221a380d76c926f378d3f81cf3e7e13f2e", - "HASH_TYPE": "type", - "TX_HASH": "0x13c137fdf071c0ab3e6a4c8aaefc16c9bb7b9593b77822b151b18412ecd2ee41", - "INDEX": "0x2", - "DEP_TYPE": "code" - }, - "SUDT": { - "CODE_HASH": "0x48dbf59b4c7ee1547238021b4869bceedf4eea6b43772e5d66ef8865b6ae7212", - "HASH_TYPE": "data", - "TX_HASH": "0x473a05da909427e2b77d3c4226d29a15539b12d9ddf7d4fda4de8a76df18555c", - "INDEX": "0x0", - "DEP_TYPE": "code" - }, - "ANYONE_CAN_PAY": { - "CODE_HASH": "0x636be2afb13c1d0f3478c22c6572b12dd13bd492f648302d5682fffe7e7efdaa", - "HASH_TYPE": "type", - "TX_HASH": "0xc520c895e20ade5b6b0bd90598f8b205afefdc316e38e87fac239ca3f0829378", - "INDEX": "0x0", - "DEP_TYPE": "depGroup" - }, - "PW_LOCK": { - "CODE_HASH": "0xabd63bf842d098c568f32b53191106649a84817288fd0116d4f3ed1b88f0f7e2", - "HASH_TYPE": "type", - "TX_HASH": "0x7441b129b99b4c6ced2016d9b44fcb88544510b426fcdcbf11e9f3c9d0a338dc", - "INDEX": "0x0", - "DEP_TYPE": "depGroup" - } - } -} diff --git a/packages/common-scripts/examples/pw_lock/lock.ts b/packages/common-scripts/examples/pw_lock/lock.ts deleted file mode 100644 index b6822aff2..000000000 --- a/packages/common-scripts/examples/pw_lock/lock.ts +++ /dev/null @@ -1,389 +0,0 @@ -import { LockScriptInfo, FromInfo, parseFromInfo, common } from "../../src"; -import { - Script, - CellProvider, - QueryOptions, - CellCollector as CellCollectorInterface, - Cell, - HexString, - PackedSince, - OutPoint, - values, - WitnessArgs, - utils, - CellDep, -} from "@ckb-lumos/base"; -import { - Options, - TransactionSkeletonType, - createTransactionFromSkeleton, -} from "@ckb-lumos/helpers"; -import { getConfig, Config, initializeConfig } from "@ckb-lumos/config-manager"; -import { Set } from "immutable"; -import keccak, { Keccak } from "keccak"; - -const { ScriptValue } = values; - -// https://github.com/lay2dev/pw-lock/commit/b447c2bb3f855e933e36212b45af4dec92adf705 pw-lock is a lock script which uses secp256k1_keccak256 algorithm. - -/* 65-byte zeros in hex */ -export const SIGNATURE_PLACEHOLDER = - "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"; - -function isPwLock(script: Script, config: Config) { - const template = config.SCRIPTS.PW_LOCK!; - return ( - script.codeHash === template.CODE_HASH && - script.hashType === template.HASH_TYPE - ); -} - -// Help to deal with cell deps, add cell dep to txSkeleton.get("cellDeps") if not exists. -function addCellDep( - txSkeleton: TransactionSkeletonType, - newCellDep: CellDep -): TransactionSkeletonType { - const cellDep = txSkeleton.get("cellDeps").find((cellDep) => { - return ( - cellDep.depType === newCellDep.depType && - new values.OutPointValue(cellDep.outPoint, { validate: false }).equals( - new values.OutPointValue(newCellDep.outPoint, { validate: false }) - ) - ); - }); - - if (!cellDep) { - txSkeleton = txSkeleton.update("cellDeps", (cellDeps) => { - return cellDeps.push({ - outPoint: newCellDep.outPoint, - depType: newCellDep.depType, - }); - }); - } - - return txSkeleton; -} - -// Defined a `CellCollector` class that implements `CellCollectorInterface`. -// `collect` method will collect pw-lock cells. -class CellCollector implements CellCollectorInterface { - private cellCollector: CellCollectorInterface; - private config: Config; - private fromScript: Script; - - constructor( - fromInfo: FromInfo, - cellProvider: CellProvider, - { - config = undefined, - queryOptions = {}, - }: Options & { - queryOptions?: QueryOptions; - } - ) { - if (!cellProvider) { - throw new Error(`Cell provider is missing!`); - } - config = config || getConfig(); - this.fromScript = parseFromInfo(fromInfo, { config }).fromScript; - this.config = config; - - queryOptions = { - ...queryOptions, - lock: this.fromScript, - type: queryOptions.type || "empty", - }; - - this.cellCollector = cellProvider.collector(queryOptions); - } - - async *collect(): AsyncGenerator { - if (!isPwLock(this.fromScript, this.config)) { - return; - } - - for await (const inputCell of this.cellCollector.collect()) { - yield inputCell; - } - } -} - -// `setupInputCell` accpet a input and transfer this input to an output. -// Then add the input and output to txSkeleton, it should be noted that the output must be added to the end of txSkeleton.get("outputs"). -// And this function should also add required cell deps and witnesses. -async function setupInputCell( - txSkeleton: TransactionSkeletonType, - inputCell: Cell, - _fromInfo?: FromInfo, - { - config = undefined, - defaultWitness = "0x", - since = undefined, - }: Options & { - defaultWitness?: HexString; - since?: PackedSince; - } = {} -): Promise { - config = config || getConfig(); - - const fromScript = inputCell.cellOutput.lock; - if (!isPwLock(fromScript, config)) { - throw new Error(`Not PW_LOCK input!`); - } - - // add inputCell to txSkeleton - txSkeleton = txSkeleton.update("inputs", (inputs) => { - return inputs.push(inputCell); - }); - - const output: Cell = { - cellOutput: { - capacity: inputCell.cellOutput.capacity, - lock: inputCell.cellOutput.lock, - type: inputCell.cellOutput.type, - }, - data: inputCell.data, - }; - - txSkeleton = txSkeleton.update("outputs", (outputs) => { - return outputs.push(output); - }); - - if (since) { - txSkeleton = txSkeleton.update("inputSinces", (inputSinces) => { - return inputSinces.set(txSkeleton.get("inputs").size - 1, since); - }); - } - - txSkeleton = txSkeleton.update("witnesses", (witnesses) => { - return witnesses.push(defaultWitness); - }); - - const template = config.SCRIPTS.PW_LOCK; - if (!template) { - throw new Error(`PW_LOCK script not defined in config!`); - } - - const scriptOutPoint: OutPoint = { - txHash: template.TX_HASH, - index: template.INDEX, - }; - - // add cell dep - txSkeleton = addCellDep(txSkeleton, { - outPoint: scriptOutPoint, - depType: template.DEP_TYPE, - }); - - // add witness - /* - * Modify the skeleton, so the first witness of the fromAddress script group - * has a WitnessArgs construct with 65-byte zero filled values. While this - * is not required, it helps in transaction fee estimation. - */ - const firstIndex = txSkeleton - .get("inputs") - .findIndex((input) => - new ScriptValue(input.cellOutput.lock, { validate: false }).equals( - new ScriptValue(fromScript, { validate: false }) - ) - ); - if (firstIndex !== -1) { - while (firstIndex >= txSkeleton.get("witnesses").size) { - txSkeleton = txSkeleton.update("witnesses", (witnesses) => - witnesses.push("0x") - ); - } - let witness: string = txSkeleton.get("witnesses").get(firstIndex)!; - const newWitnessArgs: WitnessArgs = { - /* 65-byte zeros in hex */ - lock: SIGNATURE_PLACEHOLDER, - }; - if (witness !== "0x") { - const witnessArgs = new core.WitnessArgs(new Reader(witness)); - const lock = witnessArgs.getLock(); - if ( - lock.hasValue() && - new Reader(lock.value().raw()).serializeJson() !== newWitnessArgs.lock - ) { - throw new Error( - "Lock field in first witness is set aside for signature!" - ); - } - const inputType = witnessArgs.getInputType(); - if (inputType.hasValue()) { - newWitnessArgs.inputType = new Reader( - inputType.value().raw() - ).serializeJson(); - } - const outputType = witnessArgs.getOutputType(); - if (outputType.hasValue()) { - newWitnessArgs.outputType = new Reader( - outputType.value().raw() - ).serializeJson(); - } - } - witness = new Reader( - core.SerializeWitnessArgs( - normalizers.NormalizeWitnessArgs(newWitnessArgs) - ) - ).serializeJson(); - txSkeleton = txSkeleton.update("witnesses", (witnesses) => - witnesses.set(firstIndex, witness) - ); - } - - return txSkeleton; -} - -// It's a secp256k1_keccak256 sighash all lock script, so we need a keccak256 hash method. -class Keccak256Hasher { - private hasher: Keccak; - - constructor() { - this.hasher = keccak("keccak256"); - } - - update(data: string | ArrayBuffer | Reader): this { - const reader = new Reader(data); - const array: Buffer = Buffer.from(reader.serializeJson().slice(2), "hex"); - this.hasher.update(array); - return this; - } - - digestReader(): Reader { - const hex = "0x" + this.hasher.digest("hex").toString(); - return new Reader(hex); - } - - digestHex() { - return this.digestReader().serializeJson(); - } -} - -function hashWitness(hasher: any, witness: HexString): void { - const lengthBuffer = new ArrayBuffer(8); - const view = new DataView(lengthBuffer); - const witnessReader = new Reader(witness); - view.setBigUint64(0, BigInt(witnessReader.length()), true); - hasher.update(view.buffer); - hasher.update(witnessReader); -} - -// This function help to generate signing messages from pw-lock inputs. -function prepareSigningEntries( - txSkeleton: TransactionSkeletonType, - { config = undefined }: Options = {} -): TransactionSkeletonType { - config = config || getConfig(); - - const template = config.SCRIPTS.PW_LOCK; - if (!template) { - throw new Error(`Provided config does not have PW_LOCK script setup!`); - } - let processedArgs = Set(); - const tx = createTransactionFromSkeleton(txSkeleton); - const txHash = utils - .ckbHash( - core.SerializeRawTransaction(normalizers.NormalizeRawTransaction(tx)) - ) - .serializeJson(); - const inputs = txSkeleton.get("inputs"); - const witnesses = txSkeleton.get("witnesses"); - let signingEntries = txSkeleton.get("signingEntries"); - for (let i = 0; i < inputs.size; i++) { - const input = inputs.get(i)!; - if ( - template.CODE_HASH === input.cellOutput.lock.codeHash && - template.HASH_TYPE === input.cellOutput.lock.hashType && - !processedArgs.has(input.cellOutput.lock.args) - ) { - processedArgs = processedArgs.add(input.cellOutput.lock.args); - const lockValue = new values.ScriptValue(input.cellOutput.lock, { - validate: false, - }); - const hasher = new Keccak256Hasher(); - hasher.update(txHash); - if (i >= witnesses.size) { - throw new Error( - `The first witness in the script group starting at input index ${i} does not exist, maybe some other part has invalidly tampered the transaction?` - ); - } - hashWitness(hasher, witnesses.get(i)!); - for (let j = i + 1; j < inputs.size && j < witnesses.size; j++) { - const otherInput = inputs.get(j)!; - if ( - lockValue.equals( - new values.ScriptValue(otherInput.cellOutput.lock, { - validate: false, - }) - ) - ) { - hashWitness(hasher, witnesses.get(j)!); - } - } - for (let j = inputs.size; j < witnesses.size; j++) { - hashWitness(hasher, witnesses.get(j)!); - } - const hh = new Keccak256Hasher(); - // This magic number is from https://github.com/lay2dev/pw-lock/blob/b447c2bb3f855e933e36212b45af4dec92adf705/c/secp256k1_keccak256_lock.h#L523 - hh.update("0x19457468657265756d205369676e6564204d6573736167653a0a3332"); - hh.update(hasher.digestHex()); - const signingEntry = { - type: "witness_args_lock", - index: i, - message: hh.digestHex(), - }; - signingEntries = signingEntries.push(signingEntry); - } - } - txSkeleton = txSkeleton.set("signingEntries", signingEntries); - return txSkeleton; -} - -export async function main() { - // set config - // deploy your own pw-lock and update config.json - process.env.LUMOS_CONFIG_FILE = __dirname + "/config.json"; - initializeConfig(); - - const config = getConfig(); - const template = config.SCRIPTS.PW_LOCK!; - // Get a lockScriptInfo and register to common - // `setupOutputCell` is an optional method, if you only want to add a to output, you can ignore this. - // `anyone_can_pay` script shows how to use `setupOutputCell`. - const lockScriptInfo: LockScriptInfo = { - codeHash: template.CODE_HASH, - hashType: template.HASH_TYPE, - lockScriptInfo: { - CellCollector, - setupInputCell, - prepareSigningEntries, - }, - }; - common.registerCustomLockScriptInfos([lockScriptInfo]); - - // Then you can use functions like `common.setupInputCell` and `common.transfer` as other lock scripts. - // Flowing is a example to show how to do. - - // let txSkeleton = TransactionSkeleton({ cellProvider: indexer }) - // const fromScript: Script = { - // codeHash: template.CODE_HASH, - // hashType: template.HASH_TYPE, - // args: pwLockArgs, - // } - // const fromAddress = generateAddress(fromScript) - - // const toAddress = "ckt1qyqrdsefa43s6m882pcj53m4gdnj4k440axqswmu83" - - // txSkeleton = await common.transfer( - // txSkeleton, - // [fromAddress], - // toAddress, - // BigInt(200*10**8), - // ) - - // txSkeleton = common.prepareSigningEntries(txSkeleton) - - // Then sign messages by key pair. -} diff --git a/packages/common-scripts/src/common.ts b/packages/common-scripts/src/common.ts index 3ec93aff9..52039e8df 100644 --- a/packages/common-scripts/src/common.ts +++ b/packages/common-scripts/src/common.ts @@ -207,7 +207,7 @@ function generateLockScriptInfos({ config = undefined }: Options = {}): void { }; const configHashCode: number = utils.hashCode( - Buffer.from(JSON.stringify(config)) + new TextEncoder().encode(JSON.stringify(config)) ); if (lockScriptInfos.infos.length === 0) { diff --git a/packages/hd/package.json b/packages/hd/package.json index 4b6aa3cc0..9ac0e447a 100644 --- a/packages/hd/package.json +++ b/packages/hd/package.json @@ -21,8 +21,10 @@ "dependencies": { "@ckb-lumos/base": "0.22.0-next.4", "@ckb-lumos/bi": "0.22.0-next.4", + "@ckb-lumos/codec": "0.22.0-next.4", "bn.js": "^5.1.3", "elliptic": "^6.5.4", + "js-sha3": "^0.9.3", "scrypt-js": "^3.0.1", "sha3": "^2.1.3", "uuid": "^8.3.0" diff --git a/packages/hd/src/extended_key.ts b/packages/hd/src/extended_key.ts index 1ca2d6908..5ef16f54e 100644 --- a/packages/hd/src/extended_key.ts +++ b/packages/hd/src/extended_key.ts @@ -2,6 +2,7 @@ import Keychain from "./keychain"; import key, { privateToPublic } from "./key"; import { utils, HexString } from "@ckb-lumos/base"; import { assertPublicKey, assertChainCode, assertPrivateKey } from "./helper"; +import { bytes } from "@ckb-lumos/codec"; export enum AddressType { Receiving = 0, @@ -63,11 +64,11 @@ export class AccountExtendedPublicKey extends ExtendedPublicKey { }; } - public static pathForReceiving(index: number) { + public static pathForReceiving(index: number): string { return AccountExtendedPublicKey.pathFor(AddressType.Receiving, index); } - public static pathForChange(index: number) { + public static pathForChange(index: number): string { return AccountExtendedPublicKey.pathFor(AddressType.Change, index); } @@ -77,14 +78,14 @@ export class AccountExtendedPublicKey extends ExtendedPublicKey { private getPublicKey(type = AddressType.Receiving, index: number): HexString { const keychain = Keychain.fromPublicKey( - Buffer.from(this.publicKey.slice(2), "hex"), - Buffer.from(this.chainCode.slice(2), "hex"), + bytes.bytify(this.publicKey), + bytes.bytify(this.chainCode), AccountExtendedPublicKey.ckbAccountPath ) .deriveChild(type, false) .deriveChild(index, false); - return "0x" + keychain.publicKey.toString("hex"); + return bytes.hexify(keychain.publicKey); } } @@ -117,24 +118,24 @@ export class ExtendedPrivateKey { toAccountExtendedPublicKey(): AccountExtendedPublicKey { const masterKeychain = new Keychain( - Buffer.from(this.privateKey.slice(2), "hex"), - Buffer.from(this.chainCode.slice(2), "hex") + bytes.bytify(this.privateKey), + bytes.bytify(this.chainCode) ); const accountKeychain = masterKeychain.derivePath( AccountExtendedPublicKey.ckbAccountPath ); return new AccountExtendedPublicKey( - "0x" + accountKeychain.publicKey.toString("hex"), - "0x" + accountKeychain.chainCode.toString("hex") + bytes.hexify(accountKeychain.publicKey), + bytes.hexify(accountKeychain.chainCode) ); } - static fromSeed(seed: Buffer): ExtendedPrivateKey { + static fromSeed(seed: Uint8Array): ExtendedPrivateKey { const keychain = Keychain.fromSeed(seed); return new ExtendedPrivateKey( - "0x" + keychain.privateKey.toString("hex"), - "0x" + keychain.chainCode.toString("hex") + bytes.hexify(keychain.privateKey), + bytes.hexify(keychain.chainCode) ); } @@ -145,8 +146,8 @@ export class ExtendedPrivateKey { privateKeyInfoByPath(path: string): PrivateKeyInfo { const keychain = new Keychain( - Buffer.from(this.privateKey.slice(2), "hex"), - Buffer.from(this.chainCode.slice(2), "hex") + bytes.bytify(this.privateKey), + bytes.bytify(this.chainCode) ).derivePath(path); return this.privateKeyInfoFromKeychain(keychain, path); @@ -157,8 +158,8 @@ export class ExtendedPrivateKey { path: string ): PrivateKeyInfo { return { - privateKey: "0x" + keychain.privateKey.toString("hex"), - publicKey: "0x" + keychain.publicKey.toString("hex"), + privateKey: bytes.hexify(keychain.privateKey), + publicKey: bytes.hexify(keychain.publicKey), path: path, }; } diff --git a/packages/hd/src/helper.ts b/packages/hd/src/helper.ts index e10439acb..18cc575fe 100644 --- a/packages/hd/src/helper.ts +++ b/packages/hd/src/helper.ts @@ -1,4 +1,5 @@ import { utils, HexString } from "@ckb-lumos/base"; +import { BytesLike, bytes } from "@ckb-lumos/codec"; const { assertHexString } = utils; export function assertPublicKey( @@ -25,3 +26,11 @@ export function assertChainCode(chainCode: HexString): void { throw new Error(`chainCode must be length of 32 bytes!`); } } + +export function hexifyWithout0x(value: BytesLike): string { + return bytes.hexify(value).slice(2); +} + +export function bytifyWithout0x(value: string): Uint8Array { + return bytes.bytify("0x" + value); +} diff --git a/packages/hd/src/key.ts b/packages/hd/src/key.ts index 2aa62b654..633fbda72 100644 --- a/packages/hd/src/key.ts +++ b/packages/hd/src/key.ts @@ -1,6 +1,7 @@ import { HexString, utils } from "@ckb-lumos/base"; import { ec as EC, SignatureInput } from "elliptic"; import { assertPrivateKey, assertPublicKey } from "./helper"; +import { bytes } from "@ckb-lumos/codec"; const ec = new EC("secp256k1"); @@ -31,8 +32,8 @@ export function recoverFromSignature( utils.assertHexString("message", message); utils.assertHexString("signature", signature); - const msgBuffer = Buffer.from(message.slice(2), "hex"); - const sigBuffer = Buffer.from(signature.slice(2), "hex"); + const msgBuffer = bytes.bytify(message); + const sigBuffer = bytes.bytify(signature); const sign: SignatureInput = { r: sigBuffer.slice(0, 32), @@ -45,26 +46,26 @@ export function recoverFromSignature( return publicKey; } -export function privateToPublic(privateKey: Buffer): Buffer; +export function privateToPublic(privateKey: Uint8Array): Uint8Array; export function privateToPublic(privateKey: HexString): HexString; export function privateToPublic( - privateKey: Buffer | HexString -): Buffer | HexString { + privateKey: Uint8Array | HexString +): Uint8Array | HexString { let pkBuffer = privateKey; if (typeof privateKey === "string") { assertPrivateKey(privateKey); - pkBuffer = Buffer.from(privateKey.slice(2), "hex"); + pkBuffer = bytes.bytify(privateKey); } if (pkBuffer.length !== 32) { throw new Error("Private key must be 32 bytes!"); } - const publickey = ec.keyFromPrivate(pkBuffer).getPublic(true, "hex"); + const publickey = ec.keyFromPrivate(pkBuffer).getPublic(true, "array"); if (typeof privateKey === "string") { - return "0x" + publickey; + return bytes.hexify(publickey); } - return Buffer.from(publickey, "hex"); + return bytes.bytify(publickey); } export function publicKeyToBlake160(publicKey: HexString): HexString { diff --git a/packages/hd/src/keychain.ts b/packages/hd/src/keychain.ts index 9e65f88b2..65f376b5f 100644 --- a/packages/hd/src/keychain.ts +++ b/packages/hd/src/keychain.ts @@ -2,23 +2,25 @@ import crypto from "crypto"; import { ec as EC } from "elliptic"; import BN from "bn.js"; import { privateToPublic } from "./key"; +import { Uint32BE } from "@ckb-lumos/codec/lib/number"; +import { bytes } from "@ckb-lumos/codec"; const ec = new EC("secp256k1"); -const EMPTY_BUFFER = Buffer.from(""); +const EMPTY_BUFFER = Uint8Array.from([]); // BIP32 Keychain. Not a full implementation. export default class Keychain { - privateKey: Buffer = EMPTY_BUFFER; - publicKey: Buffer = EMPTY_BUFFER; - chainCode: Buffer = EMPTY_BUFFER; - index: number = 0; - depth: number = 0; - identifier: Buffer = EMPTY_BUFFER; - fingerprint: number = 0; - parentFingerprint: number = 0; - - constructor(privateKey: Buffer, chainCode: Buffer) { + privateKey: Uint8Array = EMPTY_BUFFER; + publicKey: Uint8Array = EMPTY_BUFFER; + chainCode: Uint8Array = EMPTY_BUFFER; + index = 0; + depth = 0; + identifier: Uint8Array = EMPTY_BUFFER; + fingerprint = 0; + parentFingerprint = 0; + + constructor(privateKey: Uint8Array, chainCode: Uint8Array) { this.privateKey = privateKey; this.chainCode = chainCode; @@ -29,12 +31,12 @@ export default class Keychain { calculateFingerprint(): void { this.identifier = this.hash160(this.publicKey); - this.fingerprint = this.identifier.slice(0, 4).readUInt32BE(0); + this.fingerprint = Uint32BE.unpack(this.identifier.slice(0, 4)); } - public static fromSeed(seed: Buffer): Keychain { + public static fromSeed(seed: Uint8Array): Keychain { const i = crypto - .createHmac("sha512", Buffer.from("Bitcoin seed", "utf8")) + .createHmac("sha512", new TextEncoder().encode("Bitcoin seed")) .update(seed) .digest(); const keychain = new Keychain(i.slice(0, 32), i.slice(32)); @@ -45,9 +47,9 @@ export default class Keychain { // Create a child keychain with extended public key and path. // Children of this keychain should not have any hardened paths. public static fromPublicKey( - publicKey: Buffer, - chainCode: Buffer, - path: String + publicKey: Uint8Array, + chainCode: Uint8Array, + path: string ): Keychain { const keychain = new Keychain(EMPTY_BUFFER, chainCode); keychain.publicKey = publicKey; @@ -61,17 +63,18 @@ export default class Keychain { } public deriveChild(index: number, hardened: boolean): Keychain { - let data: Buffer; + let data: Uint8Array; - const indexBuffer: Buffer = Buffer.allocUnsafe(4); + const indexBuffer: Uint8Array = new Uint8Array(4); + const view = new DataView(indexBuffer.buffer); if (hardened) { - const pk = Buffer.concat([Buffer.alloc(1, 0), this.privateKey]); - indexBuffer.writeUInt32BE(index + 0x80000000, 0); - data = Buffer.concat([pk, indexBuffer]); + const pk = bytes.concat([0], this.privateKey); + view.setUint32(0, index + 0x80000000); + data = bytes.concat(pk, indexBuffer); } else { - indexBuffer.writeUInt32BE(index, 0); - data = Buffer.concat([this.publicKey, indexBuffer]); + view.setUint32(0, index); + data = bytes.concat(this.publicKey, indexBuffer); } const i = crypto.createHmac("sha512", this.chainCode).update(data).digest(); @@ -102,6 +105,7 @@ export default class Keychain { return this; } + // eslint-disable-next-line @typescript-eslint/no-this-alias let bip32: Keychain = this; let entries = path.split("/"); @@ -117,16 +121,19 @@ export default class Keychain { return bip32; } - isNeutered(): Boolean { + isNeutered(): boolean { return this.privateKey === EMPTY_BUFFER; } - hash160(data: Buffer): Buffer { + hash160(data: Uint8Array): Uint8Array { const sha256 = crypto.createHash("sha256").update(data).digest(); return crypto.createHash("ripemd160").update(sha256).digest(); } - private static privateKeyAdd(privateKey: Buffer, factor: Buffer): Buffer { + private static privateKeyAdd( + privateKey: Uint8Array, + factor: Uint8Array + ): Uint8Array { const result = new BN(factor); result.iadd(new BN(privateKey)); if (result.cmp(ec.curve.n) >= 0) { @@ -136,13 +143,16 @@ export default class Keychain { return result.toArrayLike(Buffer, "be", 32); } - private static publicKeyAdd(publicKey: Buffer, factor: Buffer): Buffer { + private static publicKeyAdd( + publicKey: Uint8Array, + factor: Uint8Array + ): Uint8Array { const x = new BN(publicKey.slice(1)).toRed(ec.curve.red); let y = x.redSqr().redIMul(x).redIAdd(ec.curve.b).redSqrt(); if ((publicKey[0] === 0x03) !== y.isOdd()) { y = y.redNeg(); } const point = ec.curve.g.mul(new BN(factor)).add({ x, y }); - return Buffer.from(point.encode(true, true)); + return bytes.bytify(point.encode(true, true)); } } diff --git a/packages/hd/src/keystore.ts b/packages/hd/src/keystore.ts index f3c2f87c4..10707db3f 100644 --- a/packages/hd/src/keystore.ts +++ b/packages/hd/src/keystore.ts @@ -1,9 +1,11 @@ import crypto from "crypto"; -import { Keccak } from "sha3"; import { v4 as uuid } from "uuid"; import { ExtendedPrivateKey } from "./extended_key"; import { HexString } from "@ckb-lumos/base"; import { syncScrypt } from "scrypt-js"; +import { bytifyWithout0x, hexifyWithout0x } from "./helper"; +import { bytes } from "@ckb-lumos/codec"; +import { keccak_256 } from "js-sha3"; export type HexStringWithoutPrefix = string; @@ -123,11 +125,11 @@ export default class Keystore { // Create an empty keystore object that contains empty private key static createEmpty(): Keystore { - const salt: Buffer = crypto.randomBytes(32); - const iv: Buffer = crypto.randomBytes(16); + const salt = crypto.randomBytes(32); + const iv = crypto.randomBytes(16); const kdfparams: KdfParams = { dklen: 32, - salt: salt.toString("hex"), + salt: hexifyWithout0x(salt), n: 2 ** 18, r: 8, p: 1, @@ -136,7 +138,7 @@ export default class Keystore { { ciphertext: "", cipherparams: { - iv: iv.toString("hex"), + iv: hexifyWithout0x(iv), }, cipher: CIPHER, kdf: "scrypt", @@ -150,26 +152,24 @@ export default class Keystore { static create( extendedPrivateKey: ExtendedPrivateKey, password: string, - options: { salt?: Buffer; iv?: Buffer } = {} + options: { salt?: Uint8Array; iv?: Uint8Array } = {} ): Keystore { - const salt: Buffer = options.salt || crypto.randomBytes(32); - const iv: Buffer = options.iv || crypto.randomBytes(16); + const salt: Uint8Array = options.salt || crypto.randomBytes(32); + const iv: Uint8Array = options.iv || crypto.randomBytes(16); const kdfparams: KdfParams = { dklen: 32, - salt: salt.toString("hex"), + salt: hexifyWithout0x(salt), n: 2 ** 18, r: 8, p: 1, }; - const derivedKey: Buffer = Buffer.from( - syncScrypt( - Buffer.from(password), - salt, - kdfparams.n, - kdfparams.r, - kdfparams.p, - kdfparams.dklen - ) + const derivedKey: Uint8Array = syncScrypt( + new TextEncoder().encode(password), + salt, + kdfparams.n, + kdfparams.r, + kdfparams.p, + kdfparams.dklen ); const cipher: crypto.Cipher = crypto.createCipheriv( @@ -180,18 +180,16 @@ export default class Keystore { if (!cipher) { throw new UnsupportedCipher(); } - const ciphertext: Buffer = Buffer.concat([ - cipher.update( - Buffer.from(extendedPrivateKey.serialize().slice(2), "hex") - ), - cipher.final(), - ]); + const ciphertext: Uint8Array = bytes.concat( + cipher.update(bytes.bytify(extendedPrivateKey.serialize())), + cipher.final() + ); return new Keystore( { - ciphertext: ciphertext.toString("hex"), + ciphertext: hexifyWithout0x(ciphertext), cipherparams: { - iv: iv.toString("hex"), + iv: hexifyWithout0x(iv), }, cipher: CIPHER, kdf: "scrypt", @@ -210,20 +208,17 @@ export default class Keystore { // Decrypt and return serialized extended private key. decrypt(password: string): HexString { const derivedKey = this.derivedKey(password); - const ciphertext = Buffer.from(this.crypto.ciphertext, "hex"); + const ciphertext = bytifyWithout0x(this.crypto.ciphertext); if (Keystore.mac(derivedKey, ciphertext) !== this.crypto.mac) { throw new IncorrectPassword(); } const decipher = crypto.createDecipheriv( this.crypto.cipher, derivedKey.slice(0, 16), - Buffer.from(this.crypto.cipherparams.iv, "hex") + bytifyWithout0x(this.crypto.cipherparams.iv) ); - return ( - "0x" + - Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString( - "hex" - ) + return bytes.hexify( + bytes.concat(decipher.update(ciphertext), decipher.final()) ); } @@ -233,28 +228,32 @@ export default class Keystore { checkPassword(password: string): boolean { const derivedKey = this.derivedKey(password); - const ciphertext = Buffer.from(this.crypto.ciphertext, "hex"); + const ciphertext = bytifyWithout0x(this.crypto.ciphertext); return Keystore.mac(derivedKey, ciphertext) === this.crypto.mac; } - derivedKey(password: string): Buffer { + derivedKey(password: string): Uint8Array { const { kdfparams } = this.crypto; - return Buffer.from( - syncScrypt( - Buffer.from(password), - Buffer.from(kdfparams.salt, "hex"), - kdfparams.n, - kdfparams.r, - kdfparams.p, - kdfparams.dklen - ) + return syncScrypt( + Buffer.from(password), + Buffer.from(kdfparams.salt, "hex"), + kdfparams.n, + kdfparams.r, + kdfparams.p, + kdfparams.dklen ); } - static mac(derivedKey: Buffer, ciphertext: Buffer): HexStringWithoutPrefix { - return new Keccak(256) - .update(Buffer.concat([derivedKey.slice(16, 32), ciphertext])) - .digest("hex"); + static mac( + derivedKey: Uint8Array, + ciphertext: Uint8Array + ): HexStringWithoutPrefix { + const digest = keccak_256 + .create() + .update(bytes.concat(derivedKey.slice(16, 32), ciphertext)) + .digest(); + + return hexifyWithout0x(digest); } static scryptOptions(kdfparams: KdfParams): crypto.ScryptOptions { diff --git a/packages/hd/src/mnemonic/index.ts b/packages/hd/src/mnemonic/index.ts index 0b3f956a4..48191b2e0 100644 --- a/packages/hd/src/mnemonic/index.ts +++ b/packages/hd/src/mnemonic/index.ts @@ -1,6 +1,8 @@ import crypto from "crypto"; import wordList from "./word_list"; import { HexString } from "@ckb-lumos/base"; +import { bytes } from "@ckb-lumos/codec"; +import { bytify } from "@ckb-lumos/codec/lib/bytes"; const RADIX = 2048; const PBKDF2_ROUNDS = 2048; @@ -28,29 +30,28 @@ if (wordList.length !== RADIX) { ); } -function bytesToBinary(bytes: Buffer): string { +function bytesToBinary(bytes: Uint8Array): string { return bytes.reduce((binary, byte) => { return binary + byte.toString(2).padStart(8, "0"); }, ""); } -function deriveChecksumBits(entropyBuffer: Buffer): string { +function deriveChecksumBits(entropyBuffer: Uint8Array): string { const ENT = entropyBuffer.length * 8; const CS = ENT / 32; const hash = crypto.createHash("sha256").update(entropyBuffer).digest(); return bytesToBinary(hash).slice(0, CS); } -function salt(password: string = ""): string { +function salt(password = ""): string { return `mnemonic${password}`; } -export function mnemonicToSeedSync( - mnemonic: string = "", - password: string = "" -): Buffer { - const mnemonicBuffer = Buffer.from(mnemonic.normalize("NFKD"), "utf8"); - const saltBuffer = Buffer.from(salt(password.normalize("NFKD")), "utf8"); +export function mnemonicToSeedSync(mnemonic = "", password = ""): Uint8Array { + const textEncoder = new TextEncoder(); + + const mnemonicBuffer = textEncoder.encode(mnemonic.normalize("NFKD")); + const saltBuffer = textEncoder.encode(salt(password.normalize("NFKD"))); return crypto.pbkdf2Sync( mnemonicBuffer, saltBuffer, @@ -60,14 +61,13 @@ export function mnemonicToSeedSync( ); } -export function mnemonicToSeed( - mnemonic: string = "", - password: string = "" -): Promise { +export function mnemonicToSeed(mnemonic = "", password = ""): Promise { return new Promise((resolve, reject) => { try { - const mnemonicBuffer = Buffer.from(mnemonic.normalize("NFKD"), "utf8"); - const saltBuffer = Buffer.from(salt(password.normalize("NFKD")), "utf8"); + const textEncoder = new TextEncoder(); + + const mnemonicBuffer = textEncoder.encode(mnemonic.normalize("NFKD")); + const saltBuffer = textEncoder.encode(salt(password.normalize("NFKD"))); crypto.pbkdf2( mnemonicBuffer, saltBuffer, @@ -87,7 +87,7 @@ export function mnemonicToSeed( }); } -export function mnemonicToEntropy(mnemonic: string = ""): HexString { +export function mnemonicToEntropy(mnemonic = ""): HexString { const words = mnemonic.normalize("NFKD").split(" "); if (words.length < MIN_WORDS_SIZE) { throw new Error(WORDS_TOO_SHORT); @@ -125,17 +125,17 @@ export function mnemonicToEntropy(mnemonic: string = ""): HexString { throw new Error(ENTROPY_NOT_DIVISIBLE); } - const entropy = Buffer.from(entropyBytes); + const entropy = bytes.bytify(entropyBytes); const newChecksum = deriveChecksumBits(entropy); if (newChecksum !== checksumBits) { throw new Error(INVALID_CHECKSUM); } - return "0x" + entropy.toString("hex"); + return bytes.hexify(entropy); } export function entropyToMnemonic(entropyStr: HexString): string { - const entropy = Buffer.from(entropyStr.slice(2), "hex"); + const entropy = bytify(entropyStr); if (entropy.length < MIN_ENTROPY_SIZE) { throw new TypeError(ENTROPY_TOO_SHORT); diff --git a/packages/hd/tests/key.test.ts b/packages/hd/tests/key.test.ts index 5df4115d6..d7d2549be 100644 --- a/packages/hd/tests/key.test.ts +++ b/packages/hd/tests/key.test.ts @@ -1,5 +1,6 @@ import test from "ava"; import { key } from "../src"; +import { bytes } from "@ckb-lumos/codec"; const { signRecoverable, recoverFromSignature, @@ -38,7 +39,7 @@ test("privateToPublic, derive public key from private key, Buffer", (t) => { "03e5b310636a0f6e7dcdfffa98f28d7ed70df858bb47acf13db830bfde3510b3f3", "hex" ); - t.deepEqual(privateToPublic(privateKey), publicKey); + t.deepEqual(privateToPublic(privateKey), bytes.bytify(publicKey)); }); test("privateToPublic, derive public key from private key, HexString", (t) => { diff --git a/packages/hd/tests/keychain.test.ts b/packages/hd/tests/keychain.test.ts index 30d043470..714877519 100644 --- a/packages/hd/tests/keychain.test.ts +++ b/packages/hd/tests/keychain.test.ts @@ -1,6 +1,20 @@ import test from "ava"; import { Keychain } from "../src"; +declare global { + interface Uint8Array { + toString(encoding?: string): string; + } +} + +const originalToString = Uint8Array.prototype.toString; +Uint8Array.prototype.toString = function (this: Uint8Array, encoding?: string) { + if (encoding === "hex") { + return Buffer.from(this).toString("hex"); + } + return originalToString.call(this); +}; + // https://en.bitcoin.it/wiki/BIP_0032_TestVectors const shortSeed = Buffer.from("000102030405060708090a0b0c0d0e0f", "hex"); const longSeed = Buffer.from( diff --git a/packages/hd/tests/keystore.test.ts b/packages/hd/tests/keystore.test.ts index a3382e9e7..b5efaa82a 100644 --- a/packages/hd/tests/keystore.test.ts +++ b/packages/hd/tests/keystore.test.ts @@ -1,6 +1,20 @@ import test from "ava"; import { ExtendedPrivateKey, Keystore, IncorrectPassword } from "../src"; +declare global { + interface Uint8Array { + toString(encoding?: string): string; + } +} + +const originalToString = Uint8Array.prototype.toString; +Uint8Array.prototype.toString = function (this: Uint8Array, encoding?: string) { + if (encoding === "hex") { + return Buffer.from(this).toString("hex"); + } + return originalToString.call(this); +}; + const fixture = { privateKey: "0xe8f32e723decf4051aefac8e2c93c9c5b214313817cdb01a1494b917c8436b35", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6efe5741..08581a022 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -431,12 +431,18 @@ importers: '@ckb-lumos/bi': specifier: 0.22.0-next.4 version: link:../bi + '@ckb-lumos/codec': + specifier: 0.22.0-next.4 + version: link:../codec bn.js: specifier: ^5.1.3 version: 5.1.3 elliptic: specifier: ^6.5.4 version: 6.5.4 + js-sha3: + specifier: ^0.9.3 + version: 0.9.3 scrypt-js: specifier: ^3.0.1 version: 3.0.1 @@ -12212,6 +12218,10 @@ packages: resolution: {integrity: sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==} dev: true + /js-sha3@0.9.3: + resolution: {integrity: sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg==} + dev: false + /js-string-escape@1.0.1: resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} engines: {node: '>= 0.8'} From f8fcd995b61a4c5308eaf9b32a65973ba68957f6 Mon Sep 17 00:00:00 2001 From: homura Date: Tue, 5 Mar 2024 19:18:24 +0800 Subject: [PATCH 2/2] chore: changeset for buffer to u8a --- .changeset/shiny-falcons-mix.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/shiny-falcons-mix.md diff --git a/.changeset/shiny-falcons-mix.md b/.changeset/shiny-falcons-mix.md new file mode 100644 index 000000000..9c6b3729c --- /dev/null +++ b/.changeset/shiny-falcons-mix.md @@ -0,0 +1,7 @@ +--- +"@ckb-lumos/common-scripts": minor +"@ckb-lumos/base": minor +"@ckb-lumos/hd": minor +--- + +feat!: replace all `Buffer` with `Uint8Array` for the browser compatibility