Skip to content

Commit

Permalink
feat(common-scripts): deprecate bitcoin p2sh in omnilock (#682)
Browse files Browse the repository at this point in the history
  • Loading branch information
homura authored May 11, 2024
1 parent aef7250 commit ed7c2fa
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/brave-dragons-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-lumos/common-scripts": minor
---

feat: support create omnilock address from btc testnet address
5 changes: 5 additions & 0 deletions .changeset/cyan-geese-relate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ckb-lumos/common-scripts": minor
---

feat: deprecated omnilock btc auth with p2sh
1 change: 1 addition & 0 deletions .eslintrc.next.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
],
},
Expand Down
78 changes: 70 additions & 8 deletions packages/common-scripts/src/omnilock-bitcoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<number> {
const btcAddressFlagSize = 1;
const hashSize = 20;

try {
// Bech32
if (address.startsWith("bc1q")) {
Expand All @@ -19,12 +23,16 @@ export function decodeAddress(address: string): ArrayLike<number> {

// 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
Expand All @@ -38,6 +46,32 @@ export function decodeAddress(address: string): ArrayLike<number> {
);
}

export function parseAddressToPublicKeyHash(
address: string
): ArrayLike<number> {
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<string[]>;
signMessage(message: string, type?: "ecdsa"): Promise<string>;
Expand Down Expand Up @@ -79,30 +113,58 @@ 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);
}

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
);
}
60 changes: 57 additions & 3 deletions packages/common-scripts/src/omnilock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.");
}
Expand Down Expand Up @@ -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
)
);
Expand Down Expand Up @@ -453,4 +506,5 @@ export default {
CellCollector,
OmnilockWitnessLock,
createOmnilockScript,
createSimplePublicKeyBasedOmnilockScript,
};
28 changes: 18 additions & 10 deletions packages/common-scripts/tests/omnilock-bitcoin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -32,15 +35,21 @@ 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);

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);
Expand Down Expand Up @@ -70,19 +79,18 @@ async function execute(provider: Provider) {
});
}

function makeProvider(addressType: AddressType): {
function makeProvider(
addressType: AddressType,
network: NetworkType = NetworkType.MAINNET
): {
provider: Provider;
pair: core.ECPairInterface;
keyring: SimpleKeyring;
} {
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,
Expand All @@ -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 }
);
Expand Down

1 comment on commit ed7c2fa

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 New canary release: 0.0.0-canary-ed7c2fa-20240511022143

npm install @ckb-lumos/[email protected]

Please sign in to comment.