From b37050219988debffa7888b9b739edd485109d5f Mon Sep 17 00:00:00 2001 From: homura Date: Wed, 12 Jun 2024 12:05:12 +0900 Subject: [PATCH 1/4] BREAKING CHANGE: remove isDeepEqual function (#711) --- .changeset/angry-rabbits-refuse.md | 5 +++++ packages/base/package.json | 6 +++--- packages/base/src/utils.ts | 6 ------ packages/base/tests/since.test.js | 5 +++-- pnpm-lock.yaml | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) create mode 100644 .changeset/angry-rabbits-refuse.md diff --git a/.changeset/angry-rabbits-refuse.md b/.changeset/angry-rabbits-refuse.md new file mode 100644 index 000000000..58c94eae8 --- /dev/null +++ b/.changeset/angry-rabbits-refuse.md @@ -0,0 +1,5 @@ +--- +"@ckb-lumos/base": minor +--- + +**BREAKING CHANGE**: remove unused `isDeepEqual` function to reduce the package size diff --git a/packages/base/package.json b/packages/base/package.json index 154755da1..6d2aff77f 100644 --- a/packages/base/package.json +++ b/packages/base/package.json @@ -50,11 +50,11 @@ "@types/blake2b": "^2.1.0", "@types/lodash.isequal": "^4.5.5", "blake2b": "^2.1.3", - "js-xxhash": "^1.0.4", - "lodash.isequal": "^4.5.0" + "js-xxhash": "^1.0.4" }, "devDependencies": { - "jsbi": "^4.1.0" + "jsbi": "^4.1.0", + "lodash.isequal": "^4.5.0" }, "publishConfig": { "access": "public" diff --git a/packages/base/src/utils.ts b/packages/base/src/utils.ts index 7c269e4a2..b952803b0 100644 --- a/packages/base/src/utils.ts +++ b/packages/base/src/utils.ts @@ -1,5 +1,4 @@ import blake2b, { Blake2b } from "blake2b"; -import isEqual from "lodash.isequal"; import { xxHash32 } from "js-xxhash"; import { bytes, number, BytesLike } from "@ckb-lumos/codec"; import { BI, BIish } from "@ckb-lumos/bi"; @@ -149,10 +148,6 @@ function assertHexadecimal(debugPath: string, str: string): void { } } -// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types -function isDeepEqual(a: any, b: any): boolean { - return isEqual(a, b); -} // Buffer.from('TYPE_ID') const TYPE_ID_CODE_HASH = "0x00000000000000000000000000000000000000000000000000545950455f4944"; @@ -272,6 +267,5 @@ export { hashCode, assertHexString, assertHexadecimal, - isDeepEqual, generateTypeIdScript, }; diff --git a/packages/base/tests/since.test.js b/packages/base/tests/since.test.js index bfff204a9..33a6e6e25 100644 --- a/packages/base/tests/since.test.js +++ b/packages/base/tests/since.test.js @@ -1,7 +1,8 @@ const test = require("ava"); +const isEqual = require("lodash.isequal"); const { BI } = require("@ckb-lumos/bi"); -const { since, utils } = require("../src"); +const { since } = require("../src"); const { parseSinceCompatible, @@ -91,7 +92,7 @@ test.before(() => { test("parsedSince", (t) => { fixtrues.forEach((v) => { const parsed = parseSinceCompatible(v.since); - t.true(utils.isDeepEqual(parsed, v.parsed)); + t.true(isEqual(parsed, v.parsed)); }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e989cd0d..098b65758 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,13 +143,13 @@ importers: js-xxhash: specifier: ^1.0.4 version: 1.0.4 - lodash.isequal: - specifier: ^4.5.0 - version: 4.5.0 devDependencies: jsbi: specifier: ^4.1.0 version: 4.3.0 + lodash.isequal: + specifier: ^4.5.0 + version: 4.5.0 packages/bi: dependencies: @@ -11493,7 +11493,7 @@ packages: /lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} - dev: false + dev: true /lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} From 43a7bc7ea6d44df3fd758ded467b0c2aeef78d12 Mon Sep 17 00:00:00 2001 From: homura Date: Thu, 13 Jun 2024 19:54:08 +0900 Subject: [PATCH 2/4] BREAKING CHANGE: deploy defaults to data1 (#710) --- .changeset/calm-crews-doubt.md | 5 ++ packages/common-scripts/src/deploy.ts | 68 ++++++++++----------------- 2 files changed, 31 insertions(+), 42 deletions(-) create mode 100644 .changeset/calm-crews-doubt.md diff --git a/.changeset/calm-crews-doubt.md b/.changeset/calm-crews-doubt.md new file mode 100644 index 000000000..3bae85f93 --- /dev/null +++ b/.changeset/calm-crews-doubt.md @@ -0,0 +1,5 @@ +--- +"@ckb-lumos/common-scripts": minor +--- + +**BREAKING CHANGE**: default `generateDeployWithDataTx` to `data1` since `data2` is not active diff --git a/packages/common-scripts/src/deploy.ts b/packages/common-scripts/src/deploy.ts index 8133113f9..3afccdd64 100644 --- a/packages/common-scripts/src/deploy.ts +++ b/packages/common-scripts/src/deploy.ts @@ -10,7 +10,12 @@ import { blockchain, } from "@ckb-lumos/base"; import { bytes } from "@ckb-lumos/codec"; -import { getConfig, Config, helpers } from "@ckb-lumos/config-manager"; +import { + getConfig, + Config, + helpers, + ScriptConfig, +} from "@ckb-lumos/config-manager"; import { TransactionSkeletonType, TransactionSkeleton, @@ -21,18 +26,10 @@ import { } from "@ckb-lumos/helpers"; import { Set } from "immutable"; import { FromInfo, parseFromInfo, MultisigScript } from "./from_info"; -import { BI, BIish } from "@ckb-lumos/bi"; +import { BI, BIish, parseUnit } from "@ckb-lumos/bi"; import { RPC } from "@ckb-lumos/rpc"; const { ScriptValue } = values; -function bytesToHex(bytes: Uint8Array): string { - let res = "0x"; - for (let i = 0; i < bytes.length; i++) { - res += bytes[i].toString(16).padStart(2, "0"); - } - return res; -} - async function findCellsByLock( lockScript: Script, cellProvider: CellProvider @@ -127,6 +124,8 @@ async function completeTx( return txSkeleton; } +const ONE_CKB = parseUnit("1", "ckb"); + async function injectCapacity( txSkeleton: TransactionSkeletonType, fromInfo: FromInfo, @@ -140,8 +139,8 @@ async function injectCapacity( const _feeRate = feeRate || 1000; let _amount = BI.from(amount); const { fromScript, multisigScript } = parseFromInfo(fromInfo, { config }); - _amount = _amount.add(BI.from(10).pow(8)); - let changeCapacity = BI.from(10).pow(8); + _amount = _amount.add(ONE_CKB); + let changeCapacity = ONE_CKB; const changeCell: Cell = { cellOutput: { capacity: "0x0", @@ -152,7 +151,7 @@ async function injectCapacity( }; const minimalChangeCapacity: BI = BI.from( minimalCellCapacityCompatible(changeCell) - ).add(BI.from(10).pow(8)); + ).add(ONE_CKB); if (_amount.lt(0)) { changeCapacity = changeCapacity.sub(_amount); @@ -236,12 +235,14 @@ async function injectCapacity( if (typeof fromInfo !== "string") { newWitnessArgs = { - lock: - "0x" + - multisigScript!.slice(2) + - SECP_SIGNATURE_PLACEHOLDER.slice(2).repeat( - (fromInfo as MultisigScript).M - ), + lock: bytes.hexify( + bytes.concat( + multisigScript!, + ...new Array((fromInfo as MultisigScript).M).fill( + SECP_SIGNATURE_PLACEHOLDER + ) + ) + ), }; } else { newWitnessArgs = { lock: SECP_SIGNATURE_PLACEHOLDER }; @@ -298,7 +299,8 @@ function getTransactionSize(txSkeleton: TransactionSkeletonType): number { function getTransactionSizeByTx(tx: Transaction): number { const serializedTx = blockchain.Transaction.pack(tx); // 4 is serialized offset bytesize - const size = serializedTx.byteLength + 4; + const MOLECULE_TABLE_OFFSET = 4; + const size = serializedTx.byteLength + MOLECULE_TABLE_OFFSET; return size; } @@ -341,24 +343,6 @@ async function getDataHash(outPoint: OutPoint, rpc: RPC): Promise { return new utils.CKBHasher().update(bytes.bytify(outputData)).digestHex(); } -interface ScriptConfig { - // if hashType is type, codeHash is ckbHash(type_script) - // if hashType is data, codeHash is ckbHash(data) - CODE_HASH: string; - - HASH_TYPE: "type" | "data2"; - - TX_HASH: string; - // the deploy cell can be found at index of tx's outputs - INDEX: string; - - // now deployWithX only supportted `code ` - DEP_TYPE: "depGroup" | "code"; - - // empty - SHORT_ID?: number; -} - function calculateTxHash(txSkeleton: TransactionSkeletonType): string { const tx = createTransactionFromSkeleton(txSkeleton); const txHash = utils.ckbHash(blockchain.RawTransaction.pack(tx)); @@ -374,7 +358,7 @@ function getScriptConfigByDataHash( const txHash = calculateTxHash(txSkeleton); const scriptConfig: ScriptConfig = { CODE_HASH: codeHash, - HASH_TYPE: "data2", + HASH_TYPE: "data1", TX_HASH: txHash, INDEX: "0x0", DEP_TYPE: "code", @@ -485,7 +469,7 @@ export async function generateDeployWithDataTx( capacity: "0x0", lock: fromScript, }, - data: bytesToHex(options.scriptBinary), + data: bytes.hexify(options.scriptBinary), }; txSkeleton = updateOutputs(txSkeleton, output); @@ -535,7 +519,7 @@ export async function generateDeployWithTypeIdTx( lock: fromScript, type: typeId, }, - data: bytesToHex(options.scriptBinary), + data: bytes.hexify(options.scriptBinary), }; txSkeleton = updateOutputs(txSkeleton, output); @@ -584,7 +568,7 @@ export async function generateUpgradeTypeIdDataTx( lock: fromScript, type: options.typeId, }, - data: bytesToHex(options.scriptBinary), + data: bytes.hexify(options.scriptBinary), }; txSkeleton = updateOutputs(txSkeleton, output); From f2f5666f0f8e9e0bf5aec4023abecd0a6c0ddc8b Mon Sep 17 00:00:00 2001 From: homura Date: Fri, 14 Jun 2024 12:50:35 +0900 Subject: [PATCH 3/4] BREAKING CHANGE: disallow p2sh converting by default since risky (#699) --- .changeset/green-pumpkins-build.md | 5 + .eslintrc.next.js | 1 + .../common-scripts/src/omnilock-bitcoin.ts | 107 +++++++++++++----- packages/common-scripts/src/omnilock.ts | 18 ++- .../tests/omnilock-bitcoin.test.ts | 63 ++++++++++- website/docs/migrations/migrate-to-v0.24.md | 19 ++++ 6 files changed, 175 insertions(+), 38 deletions(-) create mode 100644 .changeset/green-pumpkins-build.md create mode 100644 website/docs/migrations/migrate-to-v0.24.md diff --git a/.changeset/green-pumpkins-build.md b/.changeset/green-pumpkins-build.md new file mode 100644 index 000000000..82f4b422c --- /dev/null +++ b/.changeset/green-pumpkins-build.md @@ -0,0 +1,5 @@ +--- +"@ckb-lumos/common-scripts": minor +--- + +**BREAKING CHANGE**: `createOmnilockScript` uses the `allows` option to restrict allowed btc addresses diff --git a/.eslintrc.next.js b/.eslintrc.next.js index 240fa6c24..7905fb589 100644 --- a/.eslintrc.next.js +++ b/.eslintrc.next.js @@ -23,6 +23,7 @@ module.exports = { -1, // index -1 is not found 0, // first element of an array 1, // common for i + 1 in a loop + 2, // slice(2) for string that starts with "0x" is common to work with 3rd-party libs 16, // toString(16) 1000, // second to millisecond ], diff --git a/packages/common-scripts/src/omnilock-bitcoin.ts b/packages/common-scripts/src/omnilock-bitcoin.ts index b6b537afb..d576dcee9 100644 --- a/packages/common-scripts/src/omnilock-bitcoin.ts +++ b/packages/common-scripts/src/omnilock-bitcoin.ts @@ -1,10 +1,9 @@ -// TODO the magic number eslint will be resolved in 0.24 by recovering https://github.com/ckb-js/lumos/pull/682 -/*eslint-disable @typescript-eslint/no-magic-numbers*/ - import { bytes, BytesLike } from "@ckb-lumos/codec"; import { bech32 } from "bech32"; import bs58 from "bs58"; +export type SupportedBtcAddressType = "P2SH-P2WPKH" | "P2WPKH" | "P2PKH"; + // https://github.com/cryptape/omnilock/blob/9419b7795641da0ade25a04127e25d8a0b709077/c/ckb_identity.h#L28 const BTC_PREFIX = "CKB (Bitcoin Layer) transaction: 0x"; @@ -12,35 +11,57 @@ const BTC_PREFIX = "CKB (Bitcoin Layer) transaction: 0x"; * Decode bitcoin address to public key hash in bytes * @see https://en.bitcoin.it/wiki/List_of_address_prefixes * @param address + * @param allows */ -export function decodeAddress(address: string): ArrayLike { - try { - // Bech32 - if (address.startsWith("bc1q")) { - return bech32.fromWords(bech32.decode(address).words.slice(1)); - } +export function decodeAddress( + address: string, + allows: SupportedBtcAddressType[] = ["P2WPKH", "P2PKH"] +): ArrayLike { + const btcAddressFlagSize = 1; + const hashSize = 20; - // P2PKH - if (address.startsWith("1")) { - return bs58.decode(address).slice(1, 21); - } + if (isP2wpkhAddress(address)) { + assertAddressType(allows, "P2WPKH"); + return bech32.fromWords(bech32.decode(address).words.slice(1)); + } - // P2SH - if (address.startsWith("3")) { - return bs58.decode(address).slice(1, 21); - } - } catch { - // https://bitcoin.design/guide/glossary/address/#taproot-address---p2tr - if (address.startsWith("bc1p")) { - throw new Error("Taproot address is not supported yet."); - } + if (isP2pkhAddress(address)) { + assertAddressType(allows, "P2PKH"); + return bs58 + .decode(address) + .slice(btcAddressFlagSize, btcAddressFlagSize + hashSize); + } + + if (isP2shAddress(address)) { + assertAddressType(allows, "P2SH-P2WPKH"); + return bs58 + .decode(address) + .slice(btcAddressFlagSize, btcAddressFlagSize + hashSize); + } + + // https://bitcoin.design/guide/glossary/address/#taproot-address---p2tr + if (address.startsWith("bc1p")) { + throw new Error("Taproot address is not supported yet."); } throw new Error( - `Unsupported bitcoin address ${address}, only 1...(P2PKH) 3...(P2SH), and bc1...(Bech32) are supported.` + "Unsupported address: " + + address + + "Only Native SegWit(P2WPKH) and Legacy(P2PKH) addresses are supported" ); } +function assertAddressType( + allows: SupportedBtcAddressType[], + usingAddressType: SupportedBtcAddressType +): void { + if (!allows.includes(usingAddressType)) { + throw new Error( + `'${usingAddressType}' must be included in the 'allows' for the address` + ); + } +} + export interface Provider { requestAccounts(): Promise; signMessage(message: string, type?: "ecdsa"): Promise; @@ -82,21 +103,25 @@ export async function signMessage( const signature = bytes.bytify(base64ToHex(signatureBase64)); const address = accounts[0]; + /* eslint-disable @typescript-eslint/no-magic-numbers */ + // a secp256k1 private key can be used to sign various types of messages // the first byte of signature used as a recovery id to identify the type of message // https://github.com/XuJiandong/omnilock/blob/4e9fdb6ca78637651c8145bb7c5b82b4591332fb/c/ckb_identity.h#L249-L266 - if (address.startsWith("bc1q")) { + if (isP2wpkhAddress(address)) { signature[0] = 39 + ((signature[0] - 27) % 4); - } else if (address.startsWith("3")) { + } else if (isP2shAddress(address)) { signature[0] = 35 + ((signature[0] - 27) % 4); - } else if (address.startsWith("1")) { + } else if (isP2pkhAddress(address)) { signature[0] = 31 + ((signature[0] - 27) % 4); } else { throw new Error( - `Unsupported bitcoin address ${address}, only 1...(P2PKH) 3...(P2SH), and bc1...(Bech32) are supported.` + `Unsupported bitcoin address ${address}. Only supports SegWit, P2SH-P2WPKH, P2PKH` ); } + /* eslint-enable @typescript-eslint/no-magic-numbers */ + return bytes.hexify(signature); } @@ -104,8 +129,32 @@ function base64ToHex(str: string) { const raw = atob(str); let result = ""; for (let i = 0; i < raw.length; i++) { - const hex = raw.charCodeAt(i).toString(16); - result += hex.length === 2 ? hex : "0" + hex; + // eslint-disable-next-line @typescript-eslint/no-magic-numbers + result += raw.charCodeAt(i).toString(16).padStart(2, "0"); } return "0x" + result; } + +/* https://en.bitcoin.it/wiki/List_of_address_prefixes */ + +function isP2wpkhAddress(address: string): boolean { + return ( + address.startsWith("bc1") || // mainnet + address.startsWith("tb1") // testnet + ); +} + +function isP2shAddress(address: string): boolean { + return ( + address.startsWith("3") || // mainnet + address.startsWith("2") // testnet + ); +} + +function isP2pkhAddress(address: string): boolean { + return ( + address.startsWith("1") || // mainnet + address.startsWith("m") || // testnet + address.startsWith("n") // testnet + ); +} diff --git a/packages/common-scripts/src/omnilock.ts b/packages/common-scripts/src/omnilock.ts index 756629f8e..c1d6dc5d8 100644 --- a/packages/common-scripts/src/omnilock.ts +++ b/packages/common-scripts/src/omnilock.ts @@ -38,6 +38,7 @@ import * as bitcoin from "./omnilock-bitcoin"; import * as solana from "./omnilock-solana"; import { decode as bs58Decode } from "bs58"; import { ckbHash } from "@ckb-lumos/base/lib/utils"; +import { SupportedBtcAddressType } from "./omnilock-bitcoin"; const { ScriptValue } = values; @@ -66,6 +67,7 @@ export type IdentityEthereum = { */ content: BytesLike; }; + export type IdentityBitcoin = { flag: "BITCOIN"; /** @@ -75,6 +77,13 @@ export type IdentityBitcoin = { * `Bech32(bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4)` */ content: string; + + /** + * Allows the P2PKH and P2WPKH by default. + * To allow the P2SH-P2WPKH address, + * change this option to `["P2PKH", "P2WPKH", "P2SH-P2WPKH"]` + */ + allows?: SupportedBtcAddressType[]; }; export type IdentitySolana = { @@ -110,8 +119,8 @@ const SECP256K1_SIGNATURE_PLACEHOLDER_LENGTH = 65; const ED25519_SIGNATURE_PLACEHOLDER_LENGTH = 96; /** - * only support ETHEREUM and SECP256K1_BLAKE160 mode currently - * refer to: @link https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0042-omnilock/0042-omnilock.md omnilock + * Create an Omnilock script based on other networks' wallet + * @see https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0042-omnilock/0042-omnilock.md * @param omnilockInfo * @param options * @returns @@ -176,7 +185,10 @@ export function createOmnilockScript( return bytes.hexify( bytes.concat( [IdentityFlagsType.IdentityFlagsBitcoin], - bitcoin.decodeAddress(omnilockInfo.auth.content), + bitcoin.decodeAddress( + omnilockInfo.auth.content, + omnilockInfo.auth.allows + ), omnilockArgs ) ); diff --git a/packages/common-scripts/tests/omnilock-bitcoin.test.ts b/packages/common-scripts/tests/omnilock-bitcoin.test.ts index c6772357e..415255b63 100644 --- a/packages/common-scripts/tests/omnilock-bitcoin.test.ts +++ b/packages/common-scripts/tests/omnilock-bitcoin.test.ts @@ -12,7 +12,11 @@ import { mockOutPoint } from "@ckb-lumos/debugger/lib/context"; import { createOmnilockScript, OmnilockWitnessLock } from "../src/omnilock"; import { address, AddressType, core, keyring } from "@unisat/wallet-sdk"; import { NetworkType } from "@unisat/wallet-sdk/lib/network"; -import { Provider, signMessage } from "../src/omnilock-bitcoin"; +import { + Provider, + signMessage, + SupportedBtcAddressType, +} from "../src/omnilock-bitcoin"; import { SimpleKeyring } from "@unisat/wallet-sdk/lib/keyring"; test.before(async () => { @@ -41,15 +45,15 @@ test.serial("Omnilock#Bitcoin P2WPKH", async (t) => { test.serial("Omnilock#Bitcoin P2SH_P2WPKH", async (t) => { const { provider } = makeProvider(AddressType.P2SH_P2WPKH); - const result = await execute(provider); + const result = await execute(provider, ["P2SH-P2WPKH"]); t.is(result.code, 0, result.message); }); -async function execute(provider: Provider) { +async function execute(provider: Provider, allows?: SupportedBtcAddressType[]) { const addr = (await provider.requestAccounts())[0]; - const { txSkeleton, lock } = await setupTxSkeleton(addr); + const { txSkeleton, lock } = await setupTxSkeleton(addr, allows); const message = txSkeleton.get("signingEntries").get(0)!.message; const signature = await signMessage(message, "ecdsa", provider); @@ -95,11 +99,14 @@ function makeProvider(addressType: AddressType): { }; } -async function setupTxSkeleton(addr: string) { +async function setupTxSkeleton( + addr: string, + allows?: SupportedBtcAddressType[] +) { const txSkeleton = TransactionSkeleton().asMutable(); const lock = createOmnilockScript( - { auth: { flag: "BITCOIN", content: addr } }, + { auth: { flag: "BITCOIN", content: addr, allows } }, { config: managerConfig } ); @@ -117,3 +124,47 @@ async function setupTxSkeleton(addr: string) { common.prepareSigningEntries(txSkeleton, { config: managerConfig }); return { txSkeleton: txSkeleton, lock }; } + +// 02 indicates that the pubkey is compressed +const pubkey = + "02b602ad190efb7b4f520068e3f8ecf573823d9e2557c5229231b4e14b79bbc0d8"; + +test("Omnilock#Bitcoin P2SH", (t) => { + const p2shAddr = address.publicKeyToAddress( + pubkey, + AddressType.P2SH_P2WPKH, + NetworkType.MAINNET + ); + + t.throws(() => + createOmnilockScript({ auth: { flag: "BITCOIN", content: p2shAddr } }) + ); + + t.notThrows(() => + createOmnilockScript({ + auth: { flag: "BITCOIN", content: p2shAddr, allows: ["P2SH-P2WPKH"] }, + }) + ); +}); + +test("Unsupported BTC address", (t) => { + const p2trAddr = address.publicKeyToAddress( + pubkey, + AddressType.P2TR, + NetworkType.MAINNET + ); + + t.throws(() => + createOmnilockScript({ auth: { flag: "BITCOIN", content: p2trAddr } }) + ); + + const unknownAddr = address.publicKeyToAddress( + pubkey, + AddressType.UNKNOWN, + NetworkType.MAINNET + ); + + t.throws(() => + createOmnilockScript({ auth: { flag: "BITCOIN", content: unknownAddr } }) + ); +}); diff --git a/website/docs/migrations/migrate-to-v0.24.md b/website/docs/migrations/migrate-to-v0.24.md new file mode 100644 index 000000000..faf4b1df6 --- /dev/null +++ b/website/docs/migrations/migrate-to-v0.24.md @@ -0,0 +1,19 @@ +# Migrate to Lumos v0.24 + +## Disallow the Omnilock P2SH Address by Default + +The default options of `createOmnilockScript` disallows the use of P2SH addresses for security reasons. +Not all P2SH addresses are P2SH-P2WPKH addresses. +This means that developers may unintentionally use a non-P2SH-P2WPKH address to convert to an Omnilock script, +which can lead to the script not being lockable. +If you still need to use a P2SH address, use the following code + +```diff +createOmnilockScript({ + auth: { + flag: "BITCOIN", + content: addr, ++ allows: ["P2WPKH", "P2PKH", "P2SH-P2WPKH"] + } +}) +``` From d279b87c5f843f07f77b11923e6659025f1f8bdd Mon Sep 17 00:00:00 2001 From: homura Date: Mon, 24 Jun 2024 19:11:48 +0900 Subject: [PATCH 4/4] feat(common-scripts): support eth displaying for omnilock (#718) --- .changeset/lemon-tomatoes-pay.md | 6 + packages/common-scripts/package.json | 3 +- .../src/omnilock-ethereum-displaying.ts | 44 +++++++ packages/common-scripts/src/omnilock.ts | 16 +++ .../omnilock-ethereum-displaying.test.ts | 108 ++++++++++++++++++ packages/lumos/src/common-scripts/omnilock.ts | 1 + pnpm-lock.yaml | 51 +++++++++ 7 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 .changeset/lemon-tomatoes-pay.md create mode 100644 packages/common-scripts/src/omnilock-ethereum-displaying.ts create mode 100644 packages/common-scripts/tests/omnilock-ethereum-displaying.test.ts diff --git a/.changeset/lemon-tomatoes-pay.md b/.changeset/lemon-tomatoes-pay.md new file mode 100644 index 000000000..bbb9c8982 --- /dev/null +++ b/.changeset/lemon-tomatoes-pay.md @@ -0,0 +1,6 @@ +--- +"@ckb-lumos/common-scripts": minor +"@ckb-lumos/lumos": minor +--- + +feat: support eth displaying auth mode for omnilock diff --git a/packages/common-scripts/package.json b/packages/common-scripts/package.json index 7fc2d2cdc..23ae0701a 100644 --- a/packages/common-scripts/package.json +++ b/packages/common-scripts/package.json @@ -26,8 +26,8 @@ "@ckb-lumos/helpers": "0.23.0", "@ckb-lumos/rpc": "0.23.0", "@ckb-lumos/toolkit": "0.23.0", - "bs58": "^5.0.0", "bech32": "^2.0.0", + "bs58": "^5.0.0", "immutable": "^4.3.0" }, "repository": { @@ -57,6 +57,7 @@ "devDependencies": { "@ckb-lumos/debugger": "0.23.0", "@ckb-lumos/hd": "0.23.0", + "@ethereumjs/util": "^9.0.3", "@types/keccak": "^3.0.1", "@unisat/wallet-sdk": "^1.1.2", "keccak": "^3.0.1", diff --git a/packages/common-scripts/src/omnilock-ethereum-displaying.ts b/packages/common-scripts/src/omnilock-ethereum-displaying.ts new file mode 100644 index 000000000..3cce8e2da --- /dev/null +++ b/packages/common-scripts/src/omnilock-ethereum-displaying.ts @@ -0,0 +1,44 @@ +import { BytesLike, bytes } from "@ckb-lumos/codec"; +import { hexify } from "@ckb-lumos/codec/lib/bytes"; + +const COMMON_PREFIX = "CKB transaction: 0x"; + +export interface Provider { + request: { + (payload: { + method: "personal_sign"; + params: [from: string, message: string]; + }): Promise; + }; +} + +export async function signMessage( + address: string, + digest: BytesLike, + provider?: Provider +): Promise { + /* c8 ignore start */ + const internal: Provider | undefined = + provider ?? (globalThis as { ethereum?: Provider }).ethereum; + + if (!internal) { + throw new Error( + "No provider found, make sure you have installed MetaMask or the other EIP1193 compatible wallet" + ); + } + /* c8 ignore end */ + + const sig = await internal.request({ + method: "personal_sign", + params: [address, `${COMMON_PREFIX}${hexify(digest).slice(2)}`], + }); + + const signature = bytes.bytify(sig); + + const [tweakedV] = signature.slice(-1); + // https://eips.ethereum.org/EIPS/eip-155 + const PARITY_FLAG = 27; + const v = tweakedV >= PARITY_FLAG ? tweakedV - PARITY_FLAG : tweakedV; + signature.set([v], signature.length - 1); + return bytes.hexify(signature); +} diff --git a/packages/common-scripts/src/omnilock.ts b/packages/common-scripts/src/omnilock.ts index c1d6dc5d8..4c53562cb 100644 --- a/packages/common-scripts/src/omnilock.ts +++ b/packages/common-scripts/src/omnilock.ts @@ -49,6 +49,7 @@ export type OmnilockInfo = { export type OmnilockAuth = | IdentityCkb | IdentityEthereum + | IdentityEthereumDisplaying | IdentityBitcoin | IdentitySolana; @@ -68,6 +69,11 @@ export type IdentityEthereum = { content: BytesLike; }; +export type IdentityEthereumDisplaying = { + flag: "ETHEREUM-DISPLAYING"; + content: BytesLike; +}; + export type IdentityBitcoin = { flag: "BITCOIN"; /** @@ -173,6 +179,14 @@ export function createOmnilockScript( omnilockArgs ) ); + case "ETHEREUM-DISPLAYING": + return bytes.hexify( + bytes.concat( + [IdentityFlagsType.IdentityFlagsEthereumDisplaying], + omnilockInfo.auth.content, + omnilockArgs + ) + ); case "SECP256K1_BLAKE160": return bytes.hexify( bytes.concat( @@ -408,6 +422,7 @@ export async function setupInputCell( case IdentityFlagsType.IdentityFlagsCkb: case IdentityFlagsType.IdentityFlagsEthereum: + case IdentityFlagsType.IdentityFlagsEthereumDisplaying: case IdentityFlagsType.IdentityFlagsBitcoin: { return SECP256K1_SIGNATURE_PLACEHOLDER_LENGTH; } @@ -456,6 +471,7 @@ export function prepareSigningEntries( return _prepareSigningEntries(txSkeleton, config, "OMNILOCK"); } +export * as ethereumDisplaying from "./omnilock-ethereum-displaying"; export { bitcoin }; export { solana }; diff --git a/packages/common-scripts/tests/omnilock-ethereum-displaying.test.ts b/packages/common-scripts/tests/omnilock-ethereum-displaying.test.ts new file mode 100644 index 000000000..5bd244df3 --- /dev/null +++ b/packages/common-scripts/tests/omnilock-ethereum-displaying.test.ts @@ -0,0 +1,108 @@ +import test from "ava"; +import { createOmnilockScript, OmnilockWitnessLock } from "../src/omnilock"; +import { Provider, signMessage } from "../src/omnilock-ethereum-displaying"; +import { BytesLike, bytes } from "@ckb-lumos/codec"; +import { TransactionSkeleton } from "@ckb-lumos/helpers"; +import { + CKBDebuggerDownloader, + createTestContext, + getDefaultConfig, +} from "@ckb-lumos/debugger"; +import common from "../src/common"; +import { mockOutPoint } from "@ckb-lumos/debugger/lib/context"; +import { blockchain, utils } from "@ckb-lumos/base"; +import { + ecsign, + fromSigned, + hashPersonalMessage, + privateToAddress, + toUnsigned, +} from "@ethereumjs/util"; +import { Uint8 } from "@ckb-lumos/codec/lib/number"; +import { randomBytes } from "node:crypto"; + +const context = createTestContext(getDefaultConfig()); +const managerConfig = { PREFIX: "ckt", SCRIPTS: context.scriptConfigs }; + +test.before(async () => { + await new CKBDebuggerDownloader().downloadIfNotExists(); +}); + +test("Omnilock with IdentityEthereumDisplayingFlag", async (t) => { + const privateKey = randomBytes(32); + const provider = makeProvider(privateKey); + + const lock = createOmnilockScript( + { + auth: { flag: "ETHEREUM-DISPLAYING", content: provider.selectedAddress }, + }, + { config: managerConfig } + ); + + const txSkeleton = TransactionSkeleton().asMutable(); + + await common.setupInputCell( + txSkeleton, + { + cellOutput: { lock, capacity: "0x1" }, + data: "0x", + outPoint: mockOutPoint(), + }, + undefined, + { config: managerConfig } + ); + + common.prepareSigningEntries(txSkeleton, { config: managerConfig }); + + const signedMessage = await signMessage( + provider.selectedAddress, + txSkeleton.get("signingEntries").get(0)!.message, + provider + ); + + const signedWitness = bytes.hexify( + blockchain.WitnessArgs.pack({ + lock: OmnilockWitnessLock.pack({ signature: signedMessage }), + }) + ); + + txSkeleton.update("witnesses", (witnesses) => + witnesses.set(0, signedWitness) + ); + + const result = await context.executor.execute(txSkeleton, { + scriptHash: utils.computeScriptHash(lock), + scriptGroupType: "lock", + }); + + t.is(result.code, 0); +}); + +function makeProvider( + privateKey: BytesLike +): Provider & { selectedAddress: string } { + const privKey = bytes.bytify(privateKey); + const selectedAddress = bytes.hexify(privateToAddress(privKey)); + + return { + selectedAddress, + request: async ({ params }) => { + const message = new TextEncoder().encode(params[1]); + const msgHash = hashPersonalMessage(message); + const sig = ecsign(msgHash, privKey); + + const serialized = concatSig(Uint8.pack(sig.v), sig.r, sig.s); + return serialized; + }, + }; +} + +function concatSig(v: Uint8Array, r: Uint8Array, s: Uint8Array): string { + const rSig = fromSigned(r); + const sSig = fromSigned(s); + + const rStr = toUnsigned(rSig); + const sStr = toUnsigned(sSig); + + return bytes.hexify(bytes.concat(rStr, sStr, v)); +} diff --git a/packages/lumos/src/common-scripts/omnilock.ts b/packages/lumos/src/common-scripts/omnilock.ts index c759931b2..a758b44dc 100644 --- a/packages/lumos/src/common-scripts/omnilock.ts +++ b/packages/lumos/src/common-scripts/omnilock.ts @@ -1,6 +1,7 @@ export { bitcoin, solana, + ethereumDisplaying, OmnilockWitnessLock, createOmnilockScript, setupInputCell, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 098b65758..ac7f6ede2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -258,6 +258,9 @@ importers: '@ckb-lumos/hd': specifier: 0.23.0 version: link:../hd + '@ethereumjs/util': + specifier: ^9.0.3 + version: 9.0.3 '@types/keccak': specifier: ^3.0.1 version: 3.0.4 @@ -3699,6 +3702,20 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + /@ethereumjs/rlp@5.0.2: + resolution: {integrity: sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA==} + engines: {node: '>=18'} + hasBin: true + dev: true + + /@ethereumjs/util@9.0.3: + resolution: {integrity: sha512-PmwzWDflky+7jlZIFqiGsBPap12tk9zK5SVH9YW2OEnDN7OEhCjUOMzbOqwuClrbkSIkM2ERivd7sXZ48Rh/vg==} + engines: {node: '>=18'} + dependencies: + '@ethereumjs/rlp': 5.0.2 + ethereum-cryptography: 2.2.0 + dev: true + /@floating-ui/core@1.6.1: resolution: {integrity: sha512-42UH54oPZHPdRHdw6BgoBD6cg/eVTmVrFcgeRDM3jbO7uxSoipVcmcIGFcA5jmOHO5apcyvBhkSKES3fQJnu7A==} dependencies: @@ -4311,6 +4328,12 @@ packages: dev: true optional: true + /@noble/curves@1.4.0: + resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} + dependencies: + '@noble/hashes': 1.4.0 + dev: true + /@noble/hashes@1.4.0: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} @@ -4370,6 +4393,25 @@ packages: resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} dev: false + /@scure/base@1.1.7: + resolution: {integrity: sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==} + dev: true + + /@scure/bip32@1.4.0: + resolution: {integrity: sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==} + dependencies: + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + dev: true + + /@scure/bip39@1.3.0: + resolution: {integrity: sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==} + dependencies: + '@noble/hashes': 1.4.0 + '@scure/base': 1.1.7 + dev: true + /@sideway/address@4.1.5: resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==} dependencies: @@ -8724,6 +8766,15 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + /ethereum-cryptography@2.2.0: + resolution: {integrity: sha512-hsm9JhfytIf8QME/3B7j4bc8V+VdTU+Vas1aJlvIS96ffoNAosudXvGoEvWmc7QZYdkC8mrMJz9r0fcbw7GyCA==} + dependencies: + '@noble/curves': 1.4.0 + '@noble/hashes': 1.4.0 + '@scure/bip32': 1.4.0 + '@scure/bip39': 1.3.0 + dev: true + /eval@0.1.8: resolution: {integrity: sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==} engines: {node: '>= 0.8'}