From ed7c2fadf8483c1d3739ee403da254da46444b98 Mon Sep 17 00:00:00 2001 From: homura Date: Sat, 11 May 2024 11:20:22 +0900 Subject: [PATCH] feat(common-scripts): deprecate bitcoin p2sh in omnilock (#682) --- .changeset/brave-dragons-retire.md | 5 ++ .changeset/cyan-geese-relate.md | 5 ++ .eslintrc.next.js | 1 + .../common-scripts/src/omnilock-bitcoin.ts | 78 +++++++++++++++++-- packages/common-scripts/src/omnilock.ts | 60 +++++++++++++- .../tests/omnilock-bitcoin.test.ts | 28 ++++--- 6 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 .changeset/brave-dragons-retire.md create mode 100644 .changeset/cyan-geese-relate.md diff --git a/.changeset/brave-dragons-retire.md b/.changeset/brave-dragons-retire.md new file mode 100644 index 000000000..79315fe54 --- /dev/null +++ b/.changeset/brave-dragons-retire.md @@ -0,0 +1,5 @@ +--- +"@ckb-lumos/common-scripts": minor +--- + +feat: support create omnilock address from btc testnet address diff --git a/.changeset/cyan-geese-relate.md b/.changeset/cyan-geese-relate.md new file mode 100644 index 000000000..e86d32e1d --- /dev/null +++ b/.changeset/cyan-geese-relate.md @@ -0,0 +1,5 @@ +--- +"@ckb-lumos/common-scripts": minor +--- + +feat: deprecated omnilock btc auth with p2sh diff --git a/.eslintrc.next.js b/.eslintrc.next.js index 0b3f580c2..dfbd60488 100644 --- a/.eslintrc.next.js +++ b/.eslintrc.next.js @@ -21,6 +21,7 @@ module.exports = { -1, // index -1 is not found 0, // first element of an array 1, // common for i + 1 in a loop + 2, // many .slice(2) since the '0x' prefix should be removed while calling 3rd-party library 16, // toString(16) ], }, diff --git a/packages/common-scripts/src/omnilock-bitcoin.ts b/packages/common-scripts/src/omnilock-bitcoin.ts index 262adb276..8097079e7 100644 --- a/packages/common-scripts/src/omnilock-bitcoin.ts +++ b/packages/common-scripts/src/omnilock-bitcoin.ts @@ -7,10 +7,14 @@ const BTC_PREFIX = "CKB (Bitcoin Layer) transaction: 0x"; /** * Decode bitcoin address to public key hash in bytes + * @deprecated please migrate to {@link parseAddressToPublicKeyHash} * @see https://en.bitcoin.it/wiki/List_of_address_prefixes * @param address */ export function decodeAddress(address: string): ArrayLike { + const btcAddressFlagSize = 1; + const hashSize = 20; + try { // Bech32 if (address.startsWith("bc1q")) { @@ -19,12 +23,16 @@ export function decodeAddress(address: string): ArrayLike { // P2PKH if (address.startsWith("1")) { - return bs58.decode(address).slice(1, 21); + return bs58 + .decode(address) + .slice(btcAddressFlagSize, btcAddressFlagSize + hashSize); } // P2SH if (address.startsWith("3")) { - return bs58.decode(address).slice(1, 21); + return bs58 + .decode(address) + .slice(btcAddressFlagSize, btcAddressFlagSize + hashSize); } } catch { // https://bitcoin.design/guide/glossary/address/#taproot-address---p2tr @@ -38,6 +46,32 @@ export function decodeAddress(address: string): ArrayLike { ); } +export function parseAddressToPublicKeyHash( + address: string +): ArrayLike { + try { + // Bech32 + if (isP2wpkhAddress(address)) { + return bech32.fromWords(bech32.decode(address).words.slice(1)); + } + + // P2PKH + if (isP2pkhAddress(address)) { + const networkSize = 1; + const pubkeyHashSize = 20; + // public key hash + // a P2PKH address is composed of network(1 byte) + pubkey hash(20 bytes) + return bs58 + .decode(address) + .slice(networkSize, networkSize + pubkeyHashSize); + } + } catch { + // do nothing here, throw an error below + } + + throw new Error("Only supports Native Segwit(P2WPKH) and Legacy(P2PKH)"); +} + export interface Provider { requestAccounts(): Promise; signMessage(message: string, type?: "ecdsa"): Promise; @@ -79,21 +113,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); } @@ -101,8 +139,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..a88ff4330 100644 --- a/packages/common-scripts/src/omnilock.ts +++ b/packages/common-scripts/src/omnilock.ts @@ -110,8 +110,9 @@ 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 + * @deprecated please migrate to {@link createSimplePublicKeyBasedOmnilockScript} + * @see https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0042-omnilock/0042-omnilock.md * @param omnilockInfo * @param options * @returns @@ -136,6 +137,58 @@ export function createOmnilockScript( ): Script { const config = options?.config || getConfig(); const omnilockConfig = config.SCRIPTS.OMNILOCK; + + if (!omnilockConfig) { + throw new Error("OMNILOCK script config not found."); + } + + const defaultOmnilockArgs = 0b00000000; + const omnilockArgs = [defaultOmnilockArgs]; + + if (omnilockInfo.auth.flag === "BITCOIN") { + return { + codeHash: omnilockConfig.CODE_HASH, + hashType: omnilockConfig.HASH_TYPE, + args: bytes.hexify( + bytes.concat( + [IdentityFlagsType.IdentityFlagsBitcoin], + bitcoin.decodeAddress(omnilockInfo.auth.content), + omnilockArgs + ) + ), + }; + } + + return createSimplePublicKeyBasedOmnilockScript(omnilockInfo, options); +} + +/** + * 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 + * @example + * // create an omnilock to work with MetaMask wallet + * createOmnilockScript({ + * auth: { + * flag: "ETHEREUM", + * content: "an ethereum address here", + * }, { config }) + * // or we can create an omnilock to work with UniSat wallet + * createOmnilockScript({ + * auth: { + * flag: "BITCOIN", + * content: "a bitcoin address here", + * } + * }, {config}) + */ +export function createSimplePublicKeyBasedOmnilockScript( + omnilockInfo: OmnilockInfo, + options?: Options +): Script { + const config = options?.config || getConfig(); + const omnilockConfig = config.SCRIPTS.OMNILOCK; if (!omnilockConfig) { throw new Error("OMNILOCK script config not found."); } @@ -176,7 +229,7 @@ export function createOmnilockScript( return bytes.hexify( bytes.concat( [IdentityFlagsType.IdentityFlagsBitcoin], - bitcoin.decodeAddress(omnilockInfo.auth.content), + bitcoin.parseAddressToPublicKeyHash(omnilockInfo.auth.content), omnilockArgs ) ); @@ -453,4 +506,5 @@ export default { CellCollector, OmnilockWitnessLock, createOmnilockScript, + createSimplePublicKeyBasedOmnilockScript, }; diff --git a/packages/common-scripts/tests/omnilock-bitcoin.test.ts b/packages/common-scripts/tests/omnilock-bitcoin.test.ts index c6772357e..1c9e3c4eb 100644 --- a/packages/common-scripts/tests/omnilock-bitcoin.test.ts +++ b/packages/common-scripts/tests/omnilock-bitcoin.test.ts @@ -9,7 +9,10 @@ import { blockchain, utils } from "@ckb-lumos/base"; import { bytes } from "@ckb-lumos/codec"; import { common } from "../src"; import { mockOutPoint } from "@ckb-lumos/debugger/lib/context"; -import { createOmnilockScript, OmnilockWitnessLock } from "../src/omnilock"; +import { + createSimplePublicKeyBasedOmnilockScript, + 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"; @@ -32,6 +35,12 @@ test.serial("Omnilock#Bitcoin P2PKH", async (t) => { t.is(result.code, 0, result.message); }); +test.serial("Omnilock#Bitcoin P2PKH Testnet", async (t) => { + const { provider } = makeProvider(AddressType.P2PKH, NetworkType.TESTNET); + const result = await execute(provider); + t.is(result.code, 0, result.message); +}); + test.serial("Omnilock#Bitcoin P2WPKH", async (t) => { const { provider } = makeProvider(AddressType.P2WPKH); const result = await execute(provider); @@ -39,8 +48,8 @@ test.serial("Omnilock#Bitcoin P2WPKH", async (t) => { t.is(result.code, 0, result.message); }); -test.serial("Omnilock#Bitcoin P2SH_P2WPKH", async (t) => { - const { provider } = makeProvider(AddressType.P2SH_P2WPKH); +test.serial("Omnilock#Bitcoin P2WPKH Testnet", async (t) => { + const { provider } = makeProvider(AddressType.P2WPKH, NetworkType.TESTNET); const result = await execute(provider); t.is(result.code, 0, result.message); @@ -70,7 +79,10 @@ async function execute(provider: Provider) { }); } -function makeProvider(addressType: AddressType): { +function makeProvider( + addressType: AddressType, + network: NetworkType = NetworkType.MAINNET +): { provider: Provider; pair: core.ECPairInterface; keyring: SimpleKeyring; @@ -78,11 +90,7 @@ function makeProvider(addressType: AddressType): { const pair = core.ECPair.makeRandom(); const ring = new keyring.SimpleKeyring([pair.privateKey!.toString("hex")]); const publicKey = pair.publicKey.toString("hex"); - const addr = address.publicKeyToAddress( - publicKey, - addressType, - NetworkType.MAINNET - ); + const addr = address.publicKeyToAddress(publicKey, addressType, network); return { pair, @@ -98,7 +106,7 @@ function makeProvider(addressType: AddressType): { async function setupTxSkeleton(addr: string) { const txSkeleton = TransactionSkeleton().asMutable(); - const lock = createOmnilockScript( + const lock = createSimplePublicKeyBasedOmnilockScript( { auth: { flag: "BITCOIN", content: addr } }, { config: managerConfig } );