From d279b87c5f843f07f77b11923e6659025f1f8bdd Mon Sep 17 00:00:00 2001 From: homura Date: Mon, 24 Jun 2024 19:11:48 +0900 Subject: [PATCH] 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'}