From 1428b4165af9333f37a4424f0ca56be9670adbb4 Mon Sep 17 00:00:00 2001 From: Lyndon Date: Tue, 6 May 2025 13:49:41 +0800 Subject: [PATCH 1/5] feat(core): add getTxMessageAll method for signing hash computation --- packages/core/package.json | 2 +- packages/core/src/ckb/transaction.ts | 85 ++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 60488f51e..b080f4902 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -78,4 +78,4 @@ "ws": "^8.18.0" }, "packageManager": "pnpm@10.8.1" -} \ No newline at end of file +} diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 295f44491..782dc2998 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -1193,6 +1193,91 @@ export class Transaction extends mol.Entity.Base< position, }; } + /** + * Computes the signing hash information for a given script, specified in the spec: + * https://github.com/nervosnetwork/rfcs/pull/446 + * + * @param scriptLike - The script associated with the transaction, represented as a ScriptLike object. + * @param client - The client for complete extra infos in the transaction. + * @returns A promise that resolves to an object containing the signing message and the witness position, + * or undefined if no matching input is found. + * + * @example + * ```typescript + * const signHashInfo = await tx.getTxMessageAll(scriptLike, client); + * if (signHashInfo) { + * console.log(signHashInfo.message); // Outputs the signing message + * console.log(signHashInfo.position); // Outputs the witness position + * } + * ``` + */ + async getTxMessageAll( + scriptLike: ScriptLike, + client: Client, + hasher: Hasher = new HasherCkb(), + ): Promise<{ message: Hex; position: number } | undefined> { + const script = Script.from(scriptLike); + let position = -1; + hasher.update(this.hash()); + + for (let i = 0; i < this.inputs.length; i += 1) { + const { cellOutput, outputData } = await this.inputs[i].getCell(client); + hasher.update(cellOutput.toBytes()); + hasher.update(numToBytes(outputData.length, 4)); + hasher.update(outputData); + } + + for (let i = 0; i < this.witnesses.length; i += 1) { + const input = this.inputs[i]; + if (input) { + const { cellOutput } = await input.getCell(client); + + if (!script.eq(cellOutput.lock)) { + continue; + } + + if (position === -1) { + position = i; + } + } + + if (position === -1) { + return undefined; + } + if (i == position) { + // the first witness field in current script group + + // The spec requires that: + // The first witness field of the current running script group, must be a valid WitnessArgs structure serialized + // in the molecule serialization format, with compatible mode turned off. + // The molecule in ccc is by default in compatible mode off. + const witnessArgs = this.getWitnessArgsAt(i); + + const inputType: BytesLike = witnessArgs?.inputType ?? "0x"; + hasher.update(numToBytes(inputType.length, 4)); + hasher.update(inputType); + + const outputType: BytesLike = witnessArgs?.outputType ?? "0x"; + hasher.update(numToBytes(outputType.length, 4)); + hasher.update(outputType); + } else { + // 1. Starting from the second witness field in current script group + // 2. Starting from the first witness that do not have an input cell of the same index + const raw = bytesFrom(hexFrom(this.witnesses[i])); + hasher.update(numToBytes(raw.length, 4)); + hasher.update(raw); + } + } + + if (position === -1) { + return undefined; + } + + return { + message: hasher.digest(), + position, + }; + } /** * Find the first occurrence of a input with the specified lock id From 6d1e2f7910fae3b699596d3ae68987d430630c30 Mon Sep 17 00:00:00 2001 From: Lyndon Date: Wed, 7 May 2025 16:05:46 +0800 Subject: [PATCH 2/5] feat(core): refactor transaction hashing methods --- packages/core/src/ckb/transaction.ts | 31 +++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 782dc2998..8823b15ef 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -1137,6 +1137,21 @@ export class Transaction extends mol.Entity.Base< hasher.update(raw); } + static hashBytesToHasher(bytes: BytesLike, hasher: Hasher) { + const raw = bytesFrom(hexFrom(bytes)); + hasher.update(numToBytes(raw.length, 4)); + hasher.update(raw); + } + static hashBytesOptToHasher(bytes: BytesLike | undefined, hasher: Hasher) { + if (bytes) { + const raw = bytesFrom(hexFrom(bytes)); + hasher.update(numToBytes(raw.length + 4, 4)); + hasher.update(numToBytes(raw.length, 4)); + hasher.update(raw); + } else { + hasher.update(numToBytes(0, 4)); + } + } /** * Computes the signing hash information for a given script. * @@ -1223,8 +1238,7 @@ export class Transaction extends mol.Entity.Base< for (let i = 0; i < this.inputs.length; i += 1) { const { cellOutput, outputData } = await this.inputs[i].getCell(client); hasher.update(cellOutput.toBytes()); - hasher.update(numToBytes(outputData.length, 4)); - hasher.update(outputData); + Transaction.hashBytesToHasher(outputData, hasher); } for (let i = 0; i < this.witnesses.length; i += 1) { @@ -1253,19 +1267,12 @@ export class Transaction extends mol.Entity.Base< // The molecule in ccc is by default in compatible mode off. const witnessArgs = this.getWitnessArgsAt(i); - const inputType: BytesLike = witnessArgs?.inputType ?? "0x"; - hasher.update(numToBytes(inputType.length, 4)); - hasher.update(inputType); - - const outputType: BytesLike = witnessArgs?.outputType ?? "0x"; - hasher.update(numToBytes(outputType.length, 4)); - hasher.update(outputType); + Transaction.hashBytesOptToHasher(witnessArgs?.inputType, hasher); + Transaction.hashBytesOptToHasher(witnessArgs?.outputType, hasher); } else { // 1. Starting from the second witness field in current script group // 2. Starting from the first witness that do not have an input cell of the same index - const raw = bytesFrom(hexFrom(this.witnesses[i])); - hasher.update(numToBytes(raw.length, 4)); - hasher.update(raw); + Transaction.hashBytesToHasher(this.witnesses[i], hasher); } } From 133fbb0ff0fca753531b62533282a953b2a8b48d Mon Sep 17 00:00:00 2001 From: Lyndon Date: Mon, 12 May 2025 16:58:54 +0800 Subject: [PATCH 3/5] refactor(core): make transaction hashing methods private and improve null handling --- packages/core/src/ckb/transaction.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 8823b15ef..0affc8bbe 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -1137,14 +1137,17 @@ export class Transaction extends mol.Entity.Base< hasher.update(raw); } - static hashBytesToHasher(bytes: BytesLike, hasher: Hasher) { + private static hashBytesToHasher(bytes: BytesLike, hasher: Hasher) { const raw = bytesFrom(hexFrom(bytes)); hasher.update(numToBytes(raw.length, 4)); hasher.update(raw); } - static hashBytesOptToHasher(bytes: BytesLike | undefined, hasher: Hasher) { + private static hashBytesOptToHasher( + bytes: BytesLike | undefined | null, + hasher: Hasher, + ) { if (bytes) { - const raw = bytesFrom(hexFrom(bytes)); + const raw = bytesFrom(bytes); hasher.update(numToBytes(raw.length + 4, 4)); hasher.update(numToBytes(raw.length, 4)); hasher.update(raw); @@ -1258,7 +1261,7 @@ export class Transaction extends mol.Entity.Base< if (position === -1) { return undefined; } - if (i == position) { + if (i === position) { // the first witness field in current script group // The spec requires that: From d557e298191aa907a3c8f5aee8bb2b44181f1126 Mon Sep 17 00:00:00 2001 From: Lyndon Date: Tue, 13 May 2025 09:49:10 +0800 Subject: [PATCH 4/5] fix(core): update bytes processing --- .changeset/polite-bats-shop.md | 5 +++++ packages/core/src/ckb/transaction.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/polite-bats-shop.md diff --git a/.changeset/polite-bats-shop.md b/.changeset/polite-bats-shop.md new file mode 100644 index 000000000..b3e0836ae --- /dev/null +++ b/.changeset/polite-bats-shop.md @@ -0,0 +1,5 @@ +--- +"@ckb-ccc/core": patch +--- + +Add CKB_TX_MESSAGE_ALL signing encoding diff --git a/packages/core/src/ckb/transaction.ts b/packages/core/src/ckb/transaction.ts index 0affc8bbe..cc1361210 100644 --- a/packages/core/src/ckb/transaction.ts +++ b/packages/core/src/ckb/transaction.ts @@ -1137,20 +1137,20 @@ export class Transaction extends mol.Entity.Base< hasher.update(raw); } - private static hashBytesToHasher(bytes: BytesLike, hasher: Hasher) { + private static hashBytesToHasher(bytes: HexLike, hasher: Hasher) { const raw = bytesFrom(hexFrom(bytes)); hasher.update(numToBytes(raw.length, 4)); hasher.update(raw); } private static hashBytesOptToHasher( - bytes: BytesLike | undefined | null, + bytes: HexLike | undefined | null, hasher: Hasher, ) { if (bytes) { - const raw = bytesFrom(bytes); - hasher.update(numToBytes(raw.length + 4, 4)); - hasher.update(numToBytes(raw.length, 4)); - hasher.update(raw); + const raw = bytesFrom(hexFrom(bytes)); + const moleculeBytes = mol.Bytes.encode(raw); + hasher.update(numToBytes(moleculeBytes.length, 4)); + hasher.update(moleculeBytes); } else { hasher.update(numToBytes(0, 4)); } From 396dbb442891c0e62857c38265096837f9183995 Mon Sep 17 00:00:00 2001 From: Lyndon Date: Tue, 13 May 2025 09:52:19 +0800 Subject: [PATCH 5/5] docs: fix changeset --- .changeset/polite-bats-shop.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.changeset/polite-bats-shop.md b/.changeset/polite-bats-shop.md index b3e0836ae..0ba17513e 100644 --- a/.changeset/polite-bats-shop.md +++ b/.changeset/polite-bats-shop.md @@ -2,4 +2,5 @@ "@ckb-ccc/core": patch --- -Add CKB_TX_MESSAGE_ALL signing encoding +Add CKB_TX_MESSAGE_ALL: https://github.com/nervosnetwork/rfcs/pull/446 +