Skip to content

Commit

Permalink
refactor(helpers): stricter encode/decode address (#347)
Browse files Browse the repository at this point in the history
* refactor: stricter encode/decode address

* test(helpers): cases for mixed bech32(m)
  • Loading branch information
homura authored May 20, 2022
1 parent fb6ab6a commit 91a1d29
Show file tree
Hide file tree
Showing 7 changed files with 335 additions and 115 deletions.
2 changes: 1 addition & 1 deletion commitlint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const scopeEnumValues = [
"config-manager",
"hd",
"hd-cache",
"helper",
"helpers",
"indexer",
"lumos",
"rpc",
Expand Down
142 changes: 142 additions & 0 deletions packages/helpers/src/address-to-script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// https://github.com/nervosnetwork/rfcs/blob/master/rfcs/0021-ckb-address-format/0021-ckb-address-format.md
// | format type | description |
// |:-----------:|--------------------------------------------------------------|
// | 0x00 | full version identifies the hash_type |
// | 0x01 | short version for locks with popular code_hash, deprecated |
// | 0x02 | full version with hash_type = "Data", deprecated |
// | 0x04 | full version with hash_type = "Type", deprecated |

import { Address, Script } from "@ckb-lumos/base";
import { getConfig } from "@ckb-lumos/config-manager";
import { bech32, bech32m } from "bech32";
import { Options } from "./";
import { byteArrayToHex } from "./utils";

const BECH32_LIMIT = 1023;

/**
* full version identifies the hash_type
*/
export const ADDRESS_FORMAT_FULL = 0x00;
/**
* @deprecated
* short version for locks with popular code_hash, deprecated
*/
export const ADDRESS_FORMAT_SHORT = 0x01;

/**
* @deprecated
* full version with hash_type = "Data", deprecated
*/
export const ADDRESS_FORMAT_FULLDATA = 0x02;

/**
* @deprecated
* full version with hash_type = "Type", deprecated
*/
export const ADDRESS_FORMAT_FULLTYPE = 0x04;

export function parseFullFormatAddress(
address: Address,
{ config }: Options
): Script {
config = config || getConfig();

// throw error here if polymod not 0x2bc830a3(BECH32M_CONST)
// https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#bech32m
const { words, prefix } = bech32m.decode(address, BECH32_LIMIT);

if (prefix !== config.PREFIX) {
throw Error(
`Invalid prefix! Expected: ${config.PREFIX}, actual: ${prefix}`
);
}

const [formatType, ...body] = bech32m.fromWords(words);

if (formatType !== ADDRESS_FORMAT_FULL) {
throw new Error("Invalid address format type");
}

if (body.length < 32 + 1) {
throw new Error("Invalid payload length, too short!");
}

const code_hash = byteArrayToHex(body.slice(0, 32));
const hash_type = (() => {
const serializedHashType = body[32];

if (serializedHashType === 0) return "data";
if (serializedHashType === 1) return "type";
if (serializedHashType === 2) return "data1";

throw new Error(`Invalid hash_type ${serializedHashType}`);
})();
const args = byteArrayToHex(body.slice(33));

return { code_hash, hash_type, args };
}

export function parseDeprecatedCkb2019Address(
address: string,
{ config }: Options
): Script {
config = config || getConfig();

// throw error here if polymod not 1
// https://github.com/bitcoin/bips/blob/master/bip-0350.mediawiki#bech32m
const { prefix, words } = bech32.decode(address, BECH32_LIMIT);

if (prefix !== config.PREFIX) {
throw Error(
`Invalid prefix! Expected: ${config.PREFIX}, actual: ${prefix}`
);
}
const [formatType, ...body] = bech32.fromWords(words);

switch (formatType) {
// payload = 0x01 | code_hash_index | args
case ADDRESS_FORMAT_SHORT: {
const [shortId, ...argsBytes] = body;

/* secp256k1 / multisig / ACP */
if (argsBytes.length !== 20) {
throw Error(`Invalid payload length!`);
}
const scriptTemplate = Object.values(config.SCRIPTS).find(
(s) => s && s.SHORT_ID === shortId
);
if (!scriptTemplate) {
throw Error(`Invalid code hash index: ${shortId}!`);
}
return {
code_hash: scriptTemplate.CODE_HASH,
hash_type: scriptTemplate.HASH_TYPE,
args: byteArrayToHex(argsBytes),
};
}
// payload = 0x02 | code_hash | args
case ADDRESS_FORMAT_FULLDATA: {
if (body.length < 32) {
throw Error(`Invalid payload length!`);
}
return {
code_hash: byteArrayToHex(body.slice(0, 32)),
hash_type: "data",
args: byteArrayToHex(body.slice(32)),
};
}
// payload = 0x04 | code_hash | args
case ADDRESS_FORMAT_FULLTYPE: {
if (body.length < 32) {
throw Error(`Invalid payload length!`);
}
return {
code_hash: byteArrayToHex(body.slice(0, 32)),
hash_type: "type",
args: byteArrayToHex(body.slice(32)),
};
}
}
throw Error(`Invalid payload format type: ${formatType}`);
}
145 changes: 32 additions & 113 deletions packages/helpers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,18 @@ import { normalizers, Reader, validators } from "@ckb-lumos/toolkit";
import { List, Map as ImmutableMap, Record } from "immutable";
import { Config, getConfig } from "@ckb-lumos/config-manager";
import { BI } from "@ckb-lumos/bi";
import {
parseDeprecatedCkb2019Address,
parseFullFormatAddress,
} from "./address-to-script";
import { hexToByteArray } from "./utils";

export interface Options {
config?: Config;
}

const BECH32_LIMIT = 1023;

function byteArrayToHex(a: number[]): HexString {
return "0x" + a.map((i) => ("00" + i.toString(16)).slice(-2)).join("");
}

function hexToByteArray(h: HexString): number[] {
if (!/^(0x)?([0-9a-fA-F][0-9a-fA-F])*$/.test(h)) {
throw new Error("Invalid hex string!");
}
if (h.startsWith("0x")) {
h = h.slice(2);
}
const array = [];
while (h.length >= 2) {
array.push(parseInt(h.slice(0, 2), 16));
h = h.slice(2);
}
return array;
}

export function minimalCellCapacity(
fullCell: Cell,
{ validate = true }: { validate?: boolean } = {}
Expand Down Expand Up @@ -96,11 +82,26 @@ export function locateCellDep(
return null;
}

let HAS_WARNED_FOR_DEPRECATED_ADDRESS = false;

/**
* @deprecated please migrate to {@link encodeToAddress}, the short format address will be removed in the future
* @param script
* @param param1
* @returns
*/
export function generateAddress(
script: Script,
{ config = undefined }: Options = {}
): Address {
config = config || getConfig();
if (!HAS_WARNED_FOR_DEPRECATED_ADDRESS) {
console.warn(
"The address format generated by generateAddress or scriptToAddress will be deprecated, please migrate to encodeToAddress to generate the new ckb2021 full format address as soon as possible"
);
HAS_WARNED_FOR_DEPRECATED_ADDRESS = true;
}

const scriptTemplate = Object.values(config.SCRIPTS).find(
(s) =>
s && s.CODE_HASH === script.code_hash && s.HASH_TYPE === script.hash_type
Expand All @@ -110,14 +111,19 @@ export function generateAddress(
data.push(1, scriptTemplate.SHORT_ID);
data.push(...hexToByteArray(script.args));
} else {
data.push(script.hash_type === "type" ? 4 : 2);
if (script.hash_type === "type") data.push(0x04);
else if (script.hash_type === "data") data.push(0x02);
else throw new Error(`Invalid hash_type ${script.hash_type}`);

data.push(...hexToByteArray(script.code_hash));
data.push(...hexToByteArray(script.args));
}
const words = bech32.toWords(data);
return bech32.encode(config.PREFIX, words, BECH32_LIMIT);
}

/**
* @deprecated please migrate to {@link encodeToAddress}, the short format address will be removed in the future */
export const scriptToAddress = generateAddress;

function generatePredefinedAddress(
Expand Down Expand Up @@ -158,104 +164,17 @@ export function generateSecp256k1Blake160MultisigAddress(
});
}

function trySeries<T extends (...args: unknown[]) => unknown>(
...fns: T[]
): ReturnType<T> {
let latestCatch: unknown;
for (const fn of fns) {
try {
return fn() as ReturnType<T>;
} catch (e) {
latestCatch = e;
}
}
/* c8 ignore next */
throw latestCatch;
}

export function parseAddress(
address: Address,
{ config = undefined }: Options = {}
): Script {
config = config || getConfig();
const { prefix, words } = trySeries(
() => bech32m.decode(address, BECH32_LIMIT),
() => bech32.decode(address, BECH32_LIMIT)
);
if (prefix !== config.PREFIX) {
throw Error(
`Invalid prefix! Expected: ${config.PREFIX}, actual: ${prefix}`
);
}
const data = trySeries(
() => bech32m.fromWords(words),
() => bech32.fromWords(words)
);
switch (data[0]) {
case 0: {
// 1 + 32 + 1
// 00 code_hash hash_type
/* c8 ignore next 3 */
if (data.length < 34) {
throw new Error(`Invalid payload length!`);
}
const serializedHashType = data.slice(33, 34)[0];
return {
code_hash: byteArrayToHex(data.slice(1, 33)),
hash_type: (() => {
if (serializedHashType === 0) return "data" as const;
if (serializedHashType === 1) return "type" as const;
if (serializedHashType === 2) return "data1" as const;

/* c8 ignore next */
throw new Error(`unknown hash_type ${serializedHashType}`);
})(),
args: byteArrayToHex(data.slice(34)),
};
}
case 1: {
/* c8 ignore next 3 */
if (data.length < 2) {
throw Error(`Invalid payload length!`);
}
const scriptTemplate = Object.values(config.SCRIPTS).find(
(s) => s && s.SHORT_ID === data[1]
);
/* c8 ignore next 3 */
if (!scriptTemplate) {
throw Error(`Invalid code hash index: ${data[1]}!`);
}
return {
code_hash: scriptTemplate.CODE_HASH,
hash_type: scriptTemplate.HASH_TYPE,
args: byteArrayToHex(data.slice(2)),
};
}
case 2: {
/* c8 ignore next 3 */
if (data.length < 33) {
throw Error(`Invalid payload length!`);
}
return {
code_hash: byteArrayToHex(data.slice(1, 33)),
hash_type: "data",
args: byteArrayToHex(data.slice(33)),
};
}
case 4: {
/* c8 ignore next 3 */
if (data.length < 33) {
throw Error(`Invalid payload length!`);
}
return {
code_hash: byteArrayToHex(data.slice(1, 33)),
hash_type: "type",
args: byteArrayToHex(data.slice(33)),
};
}

try {
return parseFullFormatAddress(address, { config });
} catch {
return parseDeprecatedCkb2019Address(address, { config });
}
/* c8 ignore next */
throw Error(`Invalid payload format type: ${data[0]}`);
}

export const addressToScript = parseAddress;
Expand All @@ -274,7 +193,7 @@ export function encodeToAddress(
if (script.hash_type === "data1") return 2;

/* c8 ignore next */
throw new Error(`unknown hash_type ${script.hash_type}`);
throw new Error(`Invalid hash_type ${script.hash_type}`);
})();

data.push(0x00);
Expand Down
20 changes: 20 additions & 0 deletions packages/helpers/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { HexString } from "@ckb-lumos/base";

export function hexToByteArray(h: HexString): number[] {
if (!/^(0x)?([0-9a-fA-F][0-9a-fA-F])*$/.test(h)) {
throw new Error("Invalid hex string!");
}
if (h.startsWith("0x")) {
h = h.slice(2);
}
const array = [];
while (h.length >= 2) {
array.push(parseInt(h.slice(0, 2), 16));
h = h.slice(2);
}
return array;
}

export function byteArrayToHex(a: number[]): HexString {
return "0x" + a.map((i) => ("00" + i.toString(16)).slice(-2)).join("");
}
13 changes: 13 additions & 0 deletions packages/helpers/tests/generate_address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,16 @@ test("generateSecp256k1Blake160Address, empty config", (t) => {
"Invalid script type: SECP256K1_BLAKE160, only support: "
);
});

test("invalid deprecated address with ckb2021 data1 hash_type", (t) => {
t.throws(
() =>
generateAddress({
code_hash:
"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
hash_type: "data1",
args: "0x36c329ed630d6ce750712a477543672adab57f4c",
}),
{ message: /Invalid hash_type/ }
);
});
Loading

1 comment on commit 91a1d29

@vercel
Copy link

@vercel vercel bot commented on 91a1d29 May 20, 2022

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

lumos-website – ./

lumos-website-git-develop-cryptape.vercel.app
lumos-website.vercel.app
lumos-website-cryptape.vercel.app

Please sign in to comment.