Skip to content

Commit 362f4b3

Browse files
committed
feat(core): multisig Signers
1 parent d7345ed commit 362f4b3

19 files changed

Lines changed: 1174 additions & 88 deletions

.changeset/little-zebras-pump.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@ckb-ccc/core": minor
3+
---
4+
5+
feat(core): multisig Signers
6+

packages/core/src/client/clientPublicMainnet.advanced.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ export const MAINNET_SCRIPTS: Record<KnownScript, ScriptInfoLike | undefined> =
5454
},
5555
],
5656
},
57+
[KnownScript.Secp256k1MultisigV2Beta]: {
58+
codeHash:
59+
"0xd1a9f877aed3f5e07cb9c52b61ab96d06f250ae6883cc7f0a2423db0976fc821",
60+
hashType: "type",
61+
cellDeps: [
62+
{
63+
cellDep: {
64+
outPoint: {
65+
txHash:
66+
"0x44be4f4feda80c0e41783ab10e191df3b2bb5c3731b0970c916dbec385dcdc60",
67+
index: 0,
68+
},
69+
depType: "depGroup",
70+
},
71+
},
72+
],
73+
},
5774
[KnownScript.Secp256k1MultisigV2]: {
5875
codeHash:
5976
"0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29",

packages/core/src/client/clientPublicTestnet.advanced.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,23 @@ export const TESTNET_SCRIPTS: Record<KnownScript, ScriptInfoLike> =
5454
},
5555
],
5656
},
57+
[KnownScript.Secp256k1MultisigV2Beta]: {
58+
codeHash:
59+
"0x765b3ed6ae264b335d07e73ac332bf2c0f38f8d3340ed521cb447b4c42dd5f09",
60+
hashType: "type",
61+
cellDeps: [
62+
{
63+
cellDep: {
64+
outPoint: {
65+
txHash:
66+
"0xf2013f123b2cb745e3fdf5c935a3925647496f88090503eef58332a9245b4172",
67+
index: 0,
68+
},
69+
depType: "depGroup",
70+
},
71+
},
72+
],
73+
},
5774
[KnownScript.Secp256k1MultisigV2]: {
5875
codeHash:
5976
"0x36c971b8d41fbd94aabca77dc75e826729ac98447b46f91e00796155dddb0d29",

packages/core/src/client/knownScript.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export enum KnownScript {
55
NervosDao = "NervosDao",
66
Secp256k1Blake160 = "Secp256k1Blake160",
77
Secp256k1Multisig = "Secp256k1Multisig",
8+
Secp256k1MultisigV2Beta = "Secp256k1MultisigV2Beta", // Fix rare failing case (https://github.com/nervosnetwork/ckb-system-scripts/pull/98)
89
Secp256k1MultisigV2 = "Secp256k1MultisigV2", // Enhanced since handling (https://github.com/nervosnetwork/ckb-system-scripts/pull/99)
910
AnyoneCanPay = "AnyoneCanPay",
1011
TypeId = "TypeId",

packages/core/src/hasher/hasherCkb.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { Hex, hexFrom } from "../hex/index.js";
44
import { CKB_BLAKE2B_PERSONAL } from "./advanced.js";
55
import { Hasher } from "./hasher.js";
66

7+
export const HASH_CKB_LENGTH = 32;
8+
export const HASH_CKB_SHORT_LENGTH = 20;
9+
710
/**
811
* @public
912
*/
@@ -73,7 +76,6 @@ export class HasherCkb implements Hasher {
7376
* const hash = hashCkb("some data"); // Outputs something like "0x..."
7477
* ```
7578
*/
76-
7779
export function hashCkb(...data: BytesLike[]): Hex {
7880
const hasher = new HasherCkb();
7981
data.forEach((d) => hasher.update(d));
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
export * from "./secp256k1Signing.js";
12
export * from "./signerCkbPrivateKey.js";
23
export * from "./signerCkbPublicKey.js";
34
export * from "./signerCkbScriptReadonly.js";
4-
export * from "./verifyCkbSecp256k1.js";
5+
export * from "./signerMultisigCkbPrivateKey.js";
6+
export * from "./signerMultisigCkbReadonly.js";
57
export * from "./verifyJoyId.js";
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { secp256k1 } from "@noble/curves/secp256k1.js";
2+
import { describe, expect, it } from "vitest";
3+
import { ccc } from "../../index";
4+
import {
5+
recoverMessageSecp256k1,
6+
signMessageSecp256k1,
7+
verifyMessageSecp256k1,
8+
} from "./secp256k1Signing";
9+
10+
const client = new ccc.ClientPublicTestnet();
11+
const signer = new ccc.SignerCkbPrivateKey(
12+
client,
13+
"0x0123456789012345678901234567890123456789012345678901234567890123",
14+
);
15+
16+
describe("verifyMessageCkbSecp256k1", () => {
17+
it("should verify a message signed by SignerCkbPrivateKey", async () => {
18+
const message = "Hello CKB!";
19+
const { signature, identity } = await signer.signMessage(message);
20+
21+
const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity);
22+
expect(isValid).toBe(true);
23+
});
24+
25+
it("should fail to verify a message with a wrong signature", async () => {
26+
const message = "Hello CKB!";
27+
const { identity } = await signer.signMessage(message);
28+
29+
const signature =
30+
"0x0010000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000";
31+
32+
const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity);
33+
expect(isValid).toBe(false);
34+
});
35+
36+
it("should fail to verify a message with a wrong public key", async () => {
37+
const message = "Hello CKB!";
38+
const { signature } = await signer.signMessage(message);
39+
40+
const identity =
41+
"0x000000000000000000000000000000000000000000000000000000000000000000";
42+
43+
const isValid = ccc.verifyMessageCkbSecp256k1(message, signature, identity);
44+
expect(isValid).toBe(false);
45+
});
46+
});
47+
48+
describe("Secp256k1 Helpers", () => {
49+
const privateKey =
50+
"0x0123456789012345678901234567890123456789012345678901234567890123";
51+
const publicKey = ccc.hexFrom(
52+
secp256k1.getPublicKey(ccc.bytesFrom(privateKey), true),
53+
);
54+
const messageHash =
55+
"0x1234567890123456789012345678901234567890123456789012345678901234";
56+
57+
it("should verifies a message", () => {
58+
const isValid = verifyMessageSecp256k1(
59+
messageHash,
60+
"0xf71fd3e5b90289fa939bd3f3c0e263e8ea8e37550417344e58c9b1675084be456c506a30789a6ec98919e5458b3898199b560a41d5262cb18db37058cff339a300",
61+
publicKey,
62+
);
63+
expect(isValid).toBe(true);
64+
});
65+
66+
it("should sign and verify a message hash", () => {
67+
const signature = signMessageSecp256k1(messageHash, privateKey);
68+
const isValid = verifyMessageSecp256k1(messageHash, signature, publicKey);
69+
expect(isValid).toBe(true);
70+
});
71+
72+
it("should recover the public key from the signature", () => {
73+
const signature = signMessageSecp256k1(messageHash, privateKey);
74+
const recovered = recoverMessageSecp256k1(messageHash, signature);
75+
expect(recovered).toBe(publicKey);
76+
});
77+
78+
it("should fail verification with wrong message", () => {
79+
const signature = signMessageSecp256k1(messageHash, privateKey);
80+
const wrongMessage =
81+
"0x0000000000000000000000000000000000000000000000000000000000000000";
82+
const isValid = verifyMessageSecp256k1(wrongMessage, signature, publicKey);
83+
expect(isValid).toBe(false);
84+
});
85+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { secp256k1 } from "@noble/curves/secp256k1.js";
2+
import { bytesConcat, bytesFrom, BytesLike } from "../../bytes/index.js";
3+
import { hashCkb } from "../../hasher/index.js";
4+
import { Hex, hexFrom } from "../../hex/index.js";
5+
6+
export const SECP256K1_SIGNATURE_LENGTH = 65;
7+
8+
/**
9+
* Sign a message using Secp256k1.
10+
*
11+
* @param message - The message to sign.
12+
* @param privateKey - The private key.
13+
* @returns The signature.
14+
* @public
15+
*/
16+
export function signMessageSecp256k1(
17+
message: BytesLike,
18+
privateKey: BytesLike,
19+
): Hex {
20+
const signature = secp256k1.sign(bytesFrom(message), bytesFrom(privateKey), {
21+
format: "recovered",
22+
prehash: false,
23+
});
24+
return hexFrom(bytesConcat(signature.slice(1), signature.slice(0, 1)));
25+
}
26+
27+
/**
28+
* Verify a message using Secp256k1.
29+
*
30+
* @param message - The message to verify.
31+
* @param signature - The signature.
32+
* @param publicKey - The public key.
33+
* @returns True if the signature is valid, false otherwise.
34+
* @public
35+
*/
36+
export function verifyMessageSecp256k1(
37+
message: BytesLike,
38+
signature: BytesLike,
39+
publicKey: BytesLike,
40+
): boolean {
41+
const signatureBytes = bytesFrom(signature);
42+
return secp256k1.verify(
43+
bytesConcat(signatureBytes.slice(64), signatureBytes.slice(0, 64)),
44+
bytesFrom(message),
45+
bytesFrom(publicKey),
46+
{ format: "recovered", prehash: false },
47+
);
48+
}
49+
50+
/**
51+
* Recover the public key from a Secp256k1 signature.
52+
*
53+
* @param message - The message.
54+
* @param signature - The signature.
55+
* @returns The recovered public key.
56+
* @public
57+
*/
58+
export function recoverMessageSecp256k1(
59+
message: BytesLike,
60+
signature: BytesLike,
61+
): Hex {
62+
const signatureBytes = bytesFrom(signature);
63+
return hexFrom(
64+
secp256k1.recoverPublicKey(
65+
bytesConcat(signatureBytes.slice(64), signatureBytes.slice(0, 64)),
66+
bytesFrom(message),
67+
{ prehash: false },
68+
),
69+
);
70+
}
71+
72+
/**
73+
* @public
74+
*/
75+
export function messageHashCkbSecp256k1(message: string | BytesLike): Hex {
76+
const msg = typeof message === "string" ? message : hexFrom(message);
77+
const buffer = bytesFrom(`Nervos Message:${msg}`, "utf8");
78+
return hashCkb(buffer);
79+
}
80+
81+
/**
82+
* @public
83+
*/
84+
export function verifyMessageCkbSecp256k1(
85+
message: string | BytesLike,
86+
signature: string,
87+
publicKey: string,
88+
): boolean {
89+
return verifyMessageSecp256k1(
90+
messageHashCkbSecp256k1(message),
91+
signature,
92+
publicKey,
93+
);
94+
}

packages/core/src/signer/ckb/signerCkbPrivateKey.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { secp256k1 } from "@noble/curves/secp256k1.js";
2-
import { bytesConcat, bytesFrom, BytesLike } from "../../bytes/index.js";
2+
import { bytesFrom, BytesLike } from "../../bytes/index.js";
33
import { Transaction, TransactionLike, WitnessArgs } from "../../ckb/index.js";
44
import { Client } from "../../client/index.js";
55
import { Hex, hexFrom, HexLike } from "../../hex/index.js";
6+
import {
7+
messageHashCkbSecp256k1,
8+
signMessageSecp256k1,
9+
} from "./secp256k1Signing.js";
610
import { SignerCkbPublicKey } from "./signerCkbPublicKey.js";
7-
import { messageHashCkbSecp256k1 } from "./verifyCkbSecp256k1.js";
811

912
/**
1013
* @public
@@ -23,15 +26,7 @@ export class SignerCkbPrivateKey extends SignerCkbPublicKey {
2326
}
2427

2528
async _signMessage(message: HexLike): Promise<Hex> {
26-
const signature = secp256k1.sign(
27-
bytesFrom(message),
28-
bytesFrom(this.privateKey),
29-
{
30-
format: "recovered",
31-
prehash: false,
32-
},
33-
);
34-
return hexFrom(bytesConcat(signature.slice(1), signature.slice(0, 1)));
29+
return signMessageSecp256k1(message, this.privateKey);
3530
}
3631

3732
async signMessageRaw(message: string | BytesLike): Promise<Hex> {

packages/core/src/signer/ckb/signerCkbPublicKey.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ import { Address } from "../../address/index.js";
22
import { bytesFrom } from "../../bytes/index.js";
33
import { Script, Transaction, TransactionLike } from "../../ckb/index.js";
44
import { CellDepInfo, Client, KnownScript } from "../../client/index.js";
5-
import { hashCkb } from "../../hasher/index.js";
5+
import { HASH_CKB_SHORT_LENGTH, hashCkb } from "../../hasher/index.js";
66
import { Hex, HexLike, hexFrom } from "../../hex/index.js";
77
import { Signer, SignerSignType, SignerType } from "../signer/index.js";
8+
import { SECP256K1_SIGNATURE_LENGTH } from "./secp256k1Signing.js";
89

910
/**
1011
* @public
@@ -47,7 +48,7 @@ export class SignerCkbPublicKey extends Signer {
4748
return Address.fromKnownScript(
4849
this.client,
4950
KnownScript.Secp256k1Blake160,
50-
bytesFrom(hashCkb(this.publicKey)).slice(0, 20),
51+
bytesFrom(hashCkb(this.publicKey)).slice(0, HASH_CKB_SHORT_LENGTH),
5152
);
5253
}
5354

@@ -140,7 +141,11 @@ export class SignerCkbPublicKey extends Signer {
140141

141142
await Promise.all(
142143
(await this.getRelatedScripts(tx)).map(async ({ script, cellDeps }) => {
143-
await tx.prepareSighashAllWitness(script, 65, this.client);
144+
await tx.prepareSighashAllWitness(
145+
script,
146+
SECP256K1_SIGNATURE_LENGTH,
147+
this.client,
148+
);
144149
await tx.addCellDepInfos(this.client, cellDeps);
145150
}),
146151
);

0 commit comments

Comments
 (0)