From 3ad7abde0b29e65898b4a93976002b1275caac60 Mon Sep 17 00:00:00 2001 From: yanguoyu <841185308@qq.com> Date: Mon, 20 Jun 2022 17:56:20 +0800 Subject: [PATCH] feat: Sign support multisig. (#593) --- .../__tests__/multisig/fixtures.json | 149 +++++++ .../__tests__/multisig/index.test.js | 85 ++++ .../__tests__/signWitnessGroup/fixtures.json | 34 ++ .../__tests__/signWitnessGroup/index.test.js | 90 +++- .../__tests__/signWitnesses/fixtures.json | 401 ++++++++++++++++++ .../__tests__/signWitnesses/index.test.js | 16 +- .../ckb-sdk-core/examples/signMultisig.js | 111 +++++ packages/ckb-sdk-core/src/multisig.ts | 79 ++++ packages/ckb-sdk-core/src/signWitnessGroup.ts | 36 +- packages/ckb-sdk-core/src/signWitnesses.ts | 63 ++- 10 files changed, 1041 insertions(+), 23 deletions(-) create mode 100644 packages/ckb-sdk-core/__tests__/multisig/fixtures.json create mode 100644 packages/ckb-sdk-core/__tests__/multisig/index.test.js create mode 100644 packages/ckb-sdk-core/examples/signMultisig.js create mode 100644 packages/ckb-sdk-core/src/multisig.ts diff --git a/packages/ckb-sdk-core/__tests__/multisig/fixtures.json b/packages/ckb-sdk-core/__tests__/multisig/fixtures.json new file mode 100644 index 00000000..2938d790 --- /dev/null +++ b/packages/ckb-sdk-core/__tests__/multisig/fixtures.json @@ -0,0 +1,149 @@ +{ + "serializeMultisigConfig": { + "exception with config is less than 0": { + "config": { + "r": -1, + "m": 2, + "n": 3, + "blake160s": [] + }, + "exception": "For multisig sign, signer should between 0 and 255" + }, + "exception with config is grater than 255": { + "config": { + "r": 256, + "m": 2, + "n": 3, + "blake160s": [] + }, + "exception": "For multisig sign, signer should between 0 and 255" + }, + "exception with r shouldn't be greater than n": { + "config": { + "r": 3, + "m": 2, + "n": 3, + "blake160s": [] + }, + "exception": "For m of n multisig sign, r shouldn't be greater than m" + }, + "exception with m is greater than n": { + "config": { + "r": 1, + "m": 4, + "n": 3, + "blake160s": [] + }, + "exception": "For m of n multisig sign, m shouldn't be greater than n" + }, + "exception blake160s length not equal with n": { + "config": { + "r": 1, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1"] + }, + "exception": "For m of n multisig sign, signer's length should equal with n" + }, + "test serializeMultisigConfig": { + "config": { + "r": 1, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xb7672fcde903607f6bb150a730085c2a43c422fa", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1"] + }, + "expected": "0x000102037c021957a27000e794f25828270f187c791443e3b7672fcde903607f6bb150a730085c2a43c422fad93b3564ef1b2dcf7bca781f968b3c7d2db85fd1" + } + }, + "hashMultisig": { + "normal": { + "config": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "expected": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + "getMultisigStatus": { + "Unsigned": { + "config": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": [], + "expected": "Unsigned" + }, + "PartiallySigned": { + "config": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": ["0x7c021957a27000e794f25828270f187c791443e3"], + "expected": "PartiallySigned" + }, + "Signed": { + "config": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1"], + "expected": "Signed" + }, + "exception with More signature for multisig, all signatures overflow": { + "config": { + "r": 1, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"], + "exception": "More signature for multisig" + }, + "exception with More signature for multisig, m signatures overflow": { + "config": { + "r": 1, + "m": 1, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": ["0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1"], + "exception": "More signature for multisig" + } + }, + "isMultisigConfig": { + "false with loss field": { + "config": { + "m": "2", + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3"] + }, + "expected": false + }, + "false with unmatch field type": { + "config": { + "r": "a", + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3"] + }, + "expected": false + }, + "success": { + "config": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3"] + }, + "expected": true + } + } +} diff --git a/packages/ckb-sdk-core/__tests__/multisig/index.test.js b/packages/ckb-sdk-core/__tests__/multisig/index.test.js new file mode 100644 index 00000000..cbb5baf2 --- /dev/null +++ b/packages/ckb-sdk-core/__tests__/multisig/index.test.js @@ -0,0 +1,85 @@ +const { serializeMultisigConfig, hashMultisig, getMultisigStatus, isMultisigConfig } = require('../../lib/multisig') +const fixtures = require('./fixtures.json') + +describe('test serializeMultisigConfig', () => { + const serializeMultisigConfigTable = Object.entries(fixtures.serializeMultisigConfig).map( + ([title, { config, expected, exception }]) => [ + title, + config, + exception, + expected + ], + ) + test.each(serializeMultisigConfigTable)( + '%s', + (_title, config, exception, expected) => { + if (exception !== undefined) { + expect(() =>serializeMultisigConfig(config)).toThrowError(exception) + } else { + const result = serializeMultisigConfig(config) + expect(result).toEqual(expected) + } + }, + ) +}) +describe('test hashMultisig', () => { + const hashMultisigTable = Object.entries(fixtures.hashMultisig).map( + ([title, { config, expected, exception }]) => [ + title, + config, + exception, + expected + ], + ) + + test.each(hashMultisigTable)( + '%s', + (_title, config, exception, expected) => { + if (exception !== undefined) { + expect(() => hashMultisig(config)).toThrowError(exception) + } else { + const result = hashMultisig(config) + expect(result).toEqual(expected) + } + }, + ) +}) +describe('test getMultisigStatus', () => { + const table = Object.entries(fixtures.getMultisigStatus).map( + ([title, { config, signatures, expected, exception }]) => [ + title, + config, + signatures, + exception, + expected + ], + ) + + test.each(table)( + '%s', + (_title, config, signatures, exception, expected) => { + if (exception !== undefined) { + expect(() =>getMultisigStatus(config, signatures)).toThrowError(exception) + } else { + const result = getMultisigStatus(config, signatures) + expect(result).toEqual(expected) + } + }, + ) +}) +describe('test isMultisigConfig', () => { + const table = Object.entries(fixtures.isMultisigConfig).map( + ([title, { config, expected }]) => [ + title, + config, + expected + ], + ) + + test.each(table)( + '%s', + (_title, config, expected) => { + expect(isMultisigConfig(config)).toEqual(expected) + }, + ) +}) \ No newline at end of file diff --git a/packages/ckb-sdk-core/__tests__/signWitnessGroup/fixtures.json b/packages/ckb-sdk-core/__tests__/signWitnessGroup/fixtures.json index 65229224..ad7546f7 100644 --- a/packages/ckb-sdk-core/__tests__/signWitnessGroup/fixtures.json +++ b/packages/ckb-sdk-core/__tests__/signWitnessGroup/fixtures.json @@ -41,5 +41,39 @@ "outputType": "" } ] + }, + "sign with multisig config": { + "privateKey": "0xdcec27d0d975b0378471183a03f7071dea8532aaf968be796719ecd20af6988f", + "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", + "witnesses": [ + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ], + "multisigConfig": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "expected": [ + { + "lock": "0x52e62abe0aa34889eef6a27f29e78a500e6534e91c3ddbeaab0ea4f5a4a53a12628773309a9b18d047f318d3647315626bfab7c64ca95a21a1e1fe32a8eec6a201", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ] } } diff --git a/packages/ckb-sdk-core/__tests__/signWitnessGroup/index.test.js b/packages/ckb-sdk-core/__tests__/signWitnessGroup/index.test.js index 7c489b0e..a1ea4e65 100644 --- a/packages/ckb-sdk-core/__tests__/signWitnessGroup/index.test.js +++ b/packages/ckb-sdk-core/__tests__/signWitnessGroup/index.test.js @@ -4,22 +4,104 @@ const fixtures = require('./fixtures.json') describe('test sign witness group', () => { const fixtureTable = Object.entries( fixtures, - ).map(([title, { privateKey, transactionHash, witnesses, expected, exception }]) => [ + ).map(([title, { privateKey, transactionHash, witnesses, multisigConfig, expected, exception }]) => [ title, privateKey, transactionHash, witnesses, + multisigConfig, exception, expected, ]) - test.each(fixtureTable)('%s', (_title, privateKey, transactionHash, witnesses, exception, expected) => { + test.each(fixtureTable)('%s', (_title, privateKey, transactionHash, witnesses, multisigConfig, exception, expected) => { expect.assertions(1) if (exception !== undefined) { - expect(() => signWitnessGroup(privateKey, transactionHash, witnesses)).toThrowError(exception) + expect(() => signWitnessGroup(privateKey, transactionHash, witnesses, multisigConfig)).toThrowError(exception) } else if (privateKey !== undefined) { - const signedWitnessGroup = signWitnessGroup(privateKey, transactionHash, witnesses) + const signedWitnessGroup = signWitnessGroup(privateKey, transactionHash, witnesses, multisigConfig) expect(signedWitnessGroup).toEqual(expected) } }) + + + describe('sk is function', () => { + const transactionHash = '0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a' + const witnesses = [ + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ] + const multisigConfig = { + r: 1, + m: 1, + n: 2, + blake160s: ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1"] + } + it('sk is sync', () => { + const privateKey = v => v + const signedWitnessGroup = signWitnessGroup(privateKey, transactionHash, witnesses) + expect(signedWitnessGroup).toEqual([ + '0x34000000100000003400000034000000200000007739c6307c4e3698a8a8ebfdb3908a29a7cb5a382040c89806cace1ddc538b0e', + { + "lock": "", + "inputType": "", + "outputType": "" + } + ]) + }) + it('sk is sync with multisigConfig', () => { + const privateKey = v => v + const signedWitnessGroup = signWitnessGroup(privateKey, transactionHash, witnesses, multisigConfig) + expect(signedWitnessGroup).toEqual([ + { + "lock": "0x040db42399af0c6e32cb68160079cc40c4e8d207052fec335c52b76e3442a8a3", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ]) + }) + + it('sk result is promise', async () => { + const privateKey = (v) => Promise.resolve(v) + const signedWitnessGroup = await signWitnessGroup(privateKey, transactionHash, witnesses) + expect(signedWitnessGroup).toEqual([ + '0x34000000100000003400000034000000200000007739c6307c4e3698a8a8ebfdb3908a29a7cb5a382040c89806cace1ddc538b0e', + { + "lock": "", + "inputType": "", + "outputType": "" + } + ]) + }) + + it('sk result is promise with multisigConfig', async () => { + const privateKey = (v) => Promise.resolve(v) + const signedWitnessGroup = await signWitnessGroup(privateKey, transactionHash, witnesses, multisigConfig) + expect(signedWitnessGroup).toEqual([ + { + "lock": "0x040db42399af0c6e32cb68160079cc40c4e8d207052fec335c52b76e3442a8a3", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ]) + }) + }) }) diff --git a/packages/ckb-sdk-core/__tests__/signWitnesses/fixtures.json b/packages/ckb-sdk-core/__tests__/signWitnesses/fixtures.json index e8569877..125fc9e3 100644 --- a/packages/ckb-sdk-core/__tests__/signWitnesses/fixtures.json +++ b/packages/ckb-sdk-core/__tests__/signWitnesses/fixtures.json @@ -23,6 +23,24 @@ } ] }, + "multi group no cells": { + "privateKeys": [ + [ + "0xc6a5303d5fb970e2bd4e81e984c0a52b7bc5b42ab2b777583ea2cb74868f5708", + "0xdcec27d0d975b0378471183a03f7071dea8532aaf968be796719ecd20af6988f" + ] + ], + "inputCells": [], + "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", + "witnesses": [ + { + "lock": "", + "inputType": "", + "outputType": "" + } + ], + "exception": "Cell shouldn't be empty when key is Map" + }, "multi groups": { "privateKeys": [ [ @@ -95,6 +113,34 @@ } ] }, + "multi groups with skip": { + "privateKeys": [], + "skipMissingKeys": true, + "inputCells": [ + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xedb5c73f2a4ad8df23467c9f3446f5851b5e33da" + } + } + ], + "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", + "witnesses": [ + { + "lock": "", + "inputType": "", + "outputType": "" + } + ], + "expected": [ + { + "lock": "", + "inputType": "", + "outputType": "" + } + ] + }, "multi groups using signature provider": { "signatureProviders": [ [ @@ -258,5 +304,360 @@ "privateKey": "0xdcec27d0d975b0378471183a03f7071dea8532aaf968be796719ecd20af6988f", "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", "exception": "Witnesses is empty" + }, + "spec256 and multisig sign multi groups": { + "privateKeys": [ + [ + "0xf6e72cadbef39c71be1f128fb335db67e8a95e408814e30e1a6d609aef684c00", + { + "sk": "0xdcec27d0d975b0378471183a03f7071dea8532aaf968be796719ecd20af6988f", + "blake160": "0x7c021957a27000e794f25828270f187c791443e3", + "config": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": [] + } + ], + [ + "0x0fec94c611533c9588c8ddfed557b9024f4431a65ace4b1e7106388ddd5dd87b", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ] + ], + "inputCells": [ + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + }, + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + } + ], + "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", + "witnesses": [ + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ], + "expected": [ + { + "lock": "0x000002037c021957a27000e794f25828270f187c791443e3d93b3564ef1b2dcf7bca781f968b3c7d2db85fd1b7672fcde903607f6bb150a730085c2a43c422fa52e62abe0aa34889eef6a27f29e78a500e6534e91c3ddbeaab0ea4f5a4a53a12628773309a9b18d047f318d3647315626bfab7c64ca95a21a1e1fe32a8eec6a201", + "inputType": "", + "outputType": "" + }, + "0x550000001000000055000000550000004100000091af5eeb1632565dc4a9fb1c6e08d1f1c7da96e10ee00595a2db208f1d15faca03332a1f0f7a0f8522f6e112bb8dde4ed0015d1683b998744a0d8644f0dfd0f800", + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ] + }, + "spec256 and multisig sign multi groups second sign": { + "privateKeys": [ + [ + "0xf6e72cadbef39c71be1f128fb335db67e8a95e408814e30e1a6d609aef684c00", + { + "sk": "0xdcec27d0d975b0378471183a03f7071dea8532aaf968be796719ecd20af6988f", + "blake160": "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", + "config": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": ["0x7c021957a27000e794f25828270f187c791443e3"] + } + ], + [ + "0x0fec94c611533c9588c8ddfed557b9024f4431a65ace4b1e7106388ddd5dd87b", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ] + ], + "inputCells": [ + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + }, + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + } + ], + "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", + "witnesses": [ + { + "lock": "0x000002037c021957a27000e794f25828270f187c791443e3d93b3564ef1b2dcf7bca781f968b3c7d2db85fd1b7672fcde903607f6bb150a730085c2a43c422fa5ce7f8c8a6368e9755cb7cfe5f4e40aa68c9b8fbb91894c9c5b8ac4ec52df67274d00c95e55b1f3911d9b7dc4ba0ca890652d68fe48fa1ed2f98ed8b5abd801700", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ], + "expected": [ + "0xd600000010000000d6000000d6000000c2000000000002037c021957a27000e794f25828270f187c791443e3d93b3564ef1b2dcf7bca781f968b3c7d2db85fd1b7672fcde903607f6bb150a730085c2a43c422fa5ce7f8c8a6368e9755cb7cfe5f4e40aa68c9b8fbb91894c9c5b8ac4ec52df67274d00c95e55b1f3911d9b7dc4ba0ca890652d68fe48fa1ed2f98ed8b5abd80170052e62abe0aa34889eef6a27f29e78a500e6534e91c3ddbeaab0ea4f5a4a53a12628773309a9b18d047f318d3647315626bfab7c64ca95a21a1e1fe32a8eec6a201", + "0x550000001000000055000000550000004100000091af5eeb1632565dc4a9fb1c6e08d1f1c7da96e10ee00595a2db208f1d15faca03332a1f0f7a0f8522f6e112bb8dde4ed0015d1683b998744a0d8644f0dfd0f800", + "0x10000000100000001000000010000000", + { + "lock": "", + "inputType": "", + "outputType": "" + } + ] + }, + "sign with multisig config, but config is incorrect": { + "privateKeys": [ + [ + "0xf6e72cadbef39c71be1f128fb335db67e8a95e408814e30e1a6d609aef684c00", + { + "sk": "0xdcec27d0d975b0378471183a03f7071dea8532aaf968be796719ecd20af6988f", + "blake160": "0x7c021957a27000e794f25828270f187c791443e3", + "config": { + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": [] + } + ], + [ + "0x0fec94c611533c9588c8ddfed557b9024f4431a65ace4b1e7106388ddd5dd87b", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ] + ], + "inputCells": [ + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + }, + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + } + ], + "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", + "witnesses": [ + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ], + "exception": "Multisig options is incorrect" + }, + "sign with multisig config, but config loss some property": { + "privateKeys": [ + [ + "0xf6e72cadbef39c71be1f128fb335db67e8a95e408814e30e1a6d609aef684c00", + { + "sk": "0xdcec27d0d975b0378471183a03f7071dea8532aaf968be796719ecd20af6988f", + "blake160": "0x7c021957a27000e794f25828270f187c791443e3", + "signatures": [] + } + ], + [ + "0x0fec94c611533c9588c8ddfed557b9024f4431a65ace4b1e7106388ddd5dd87b", + "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee" + ] + ], + "inputCells": [ + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + }, + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + }, + { + "lock": { + "codeHash": "0x1892ea40d82b53c678ff88312450bbb17e164d7a3e0a90941aa58839f56f8df2", + "hashType": "type", + "args": "0xe2fa82e70b062c8644b80ad7ecf6e015e5f352f6" + } + } + ], + "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", + "witnesses": [ + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + }, + { + "lock": "", + "inputType": "", + "outputType": "" + } + ], + "exception": "Multisig options miss some property" + }, + "sign with multisig config, but first witness not object": { + "privateKeys": [ + [ + "0xf6e72cadbef39c71be1f128fb335db67e8a95e408814e30e1a6d609aef684c00", + { + "sk": "0xdcec27d0d975b0378471183a03f7071dea8532aaf968be796719ecd20af6988f", + "blake160": "0x7c021957a27000e794f25828270f187c791443e3", + "config": { + "r": 0, + "m": 2, + "n": 3, + "blake160s": ["0x7c021957a27000e794f25828270f187c791443e3", "0xd93b3564ef1b2dcf7bca781f968b3c7d2db85fd1", "0xb7672fcde903607f6bb150a730085c2a43c422fa"] + }, + "signatures": [] + } + ] + ], + "inputCells": [ + { + "lock": { + "codeHash": "0x5c5069eb0857efc65e1bca0c07df34c31663b3622fd3876c876320fc9634e2a8", + "hashType": "type", + "args": "0xe7db180742a9b5c4f2d9319d74982503fbc88a37" + } + } + ], + "transactionHash": "0x4a4bcfef1b7448e27edf533df2f1de9f56be05eba645fb83f42d55816797ad2a", + "witnesses": [ + "0x7c021957a27000e794f25828270f187c791443e3" + ], + "exception": "The first witness in the group should be type of WitnessArgs" } } diff --git a/packages/ckb-sdk-core/__tests__/signWitnesses/index.test.js b/packages/ckb-sdk-core/__tests__/signWitnesses/index.test.js index e045173d..137c807f 100644 --- a/packages/ckb-sdk-core/__tests__/signWitnesses/index.test.js +++ b/packages/ckb-sdk-core/__tests__/signWitnesses/index.test.js @@ -4,7 +4,7 @@ const fixtures = require('./fixtures.json') describe('test sign witnesses', () => { const fixtureTable = Object.entries(fixtures).map( - ([title, { privateKey, privateKeys, signatureProviders, transactionHash, witnesses, inputCells, expected, exception }]) => [ + ([title, { privateKey, privateKeys, signatureProviders, transactionHash, witnesses, inputCells, expected, exception, skipMissingKeys }]) => [ title, privateKey, privateKeys, @@ -12,6 +12,7 @@ describe('test sign witnesses', () => { transactionHash, witnesses, inputCells, + skipMissingKeys, exception, expected, ], @@ -19,20 +20,23 @@ describe('test sign witnesses', () => { test.each(fixtureTable)( '%s', - (_title, privateKey, privateKeys, signatureProviders, transactionHash, witnesses, inputCells, exception, expected) => { + (_title, privateKey, privateKeys, signatureProviders, transactionHash, witnesses, inputCells, skipMissingKeys, exception, expected) => { if (exception !== undefined) { const key = privateKey || (privateKeys && new Map(privateKeys)) - expect(() => - signWitnesses(key)({ + expect( + () => signWitnesses(key)({ transactionHash, witnesses, inputCells, - }), + skipMissingKeys, + }) ).toThrowError(exception) } else if (privateKey !== undefined) { const signedWitnesses = signWitnesses(privateKey)({ transactionHash, witnesses, + inputCells, + skipMissingKeys, }) expect(signedWitnesses).toEqual(expected) } else if (privateKeys !== undefined) { @@ -41,6 +45,7 @@ describe('test sign witnesses', () => { transactionHash, witnesses, inputCells, + skipMissingKeys }) expect(signedWitnesses).toEqual(expected) } else if (signatureProviders !== undefined) { @@ -52,6 +57,7 @@ describe('test sign witnesses', () => { transactionHash, witnesses, inputCells, + skipMissingKeys }) expect(signedWitnesses).toEqual(expected) } diff --git a/packages/ckb-sdk-core/examples/signMultisig.js b/packages/ckb-sdk-core/examples/signMultisig.js new file mode 100644 index 00000000..5772a533 --- /dev/null +++ b/packages/ckb-sdk-core/examples/signMultisig.js @@ -0,0 +1,111 @@ +const CKB = require('../lib').default +const { hashMultisig } = require('../lib/multisig') + +const alice = { + blake160: '0xc9dc0591a8edf3ddbd48e3dd24c85d68706df86f', + privateKey: '0x13f88a4a4f06cdbe693ef77b8fcbda1d44ea28567c47a7284e7542bc3eafe6c7' +} +const bob = { + blake160: '0x8aa16d7f71b352fa8c3bd4ca790ca4b662343381', + privateKey: '0x51594d34890e2c817ac0a58d702a4c19cc314000f308926de739faa460f149c9' +} +const charlie = { + blake160: '0x6f8f1a16bf40f0171bbb6d5abcec5473485f916b', + privateKey: '0xac5f70a5b36e645eebfaad00357e3b1d94b9d50718824a818ec08b90c68a48af' +} + +const multisigConfig = { + r: 0, + m: 2, + n: 3, + blake160s: [alice.blake160, bob.blake160, charlie.blake160] +} +const ckb = new CKB('http://localhost:8114') +const multisigLockScript = { + hashType: ckb.utils.systemScripts.SECP256K1_MULTISIG.hashType, + codeHash: ckb.utils.systemScripts.SECP256K1_MULTISIG.codeHash, + args: hashMultisig(multisigConfig) +} +const multisigLockHash = ckb.utils.scriptToHash(multisigLockScript) + +const inputCells = [ + { + "previousOutput":{ + "txHash":"0x4a978176babec5441a9a15182f3cc35799b60b4fc09e0a478f9fc640c32aa7f0", + "index":"0x0" + }, + "since": "0x0", + "lock": multisigLockScript + } +] +const tx = { + "cellDeps":[ + { + "outPoint": ckb.utils.systemScripts.SECP256K1_MULTISIG.testnetOutPoint, + "depType": ckb.utils.systemScripts.SECP256K1_MULTISIG.depType + } + ], + "headerDeps":[ + + ], + "inputs": inputCells.map(v => ({ + since: v.since, + previousOutput: v.previousOutput + })), + "outputs":[ + { + "capacity": `0x${BigInt('6100000000').toString(16)}`, + "lock": { + "args": "0x62260b4dd406bee8a021185edaa60b7a77f7e99a", + "codeHash": ckb.utils.systemScripts.SECP256K1_BLAKE160.codeHash, + "hashType": ckb.utils.systemScripts.SECP256K1_BLAKE160.hashType, + }, + }, + { + "capacity": `0x${(BigInt('100000000000') - BigInt('6100000000') - BigInt('593')).toString(16)}`, + "lock": multisigLockScript, + } + ], + "version":"0x0", + "outputsData":[ + "0x", + "0x" + ] +} +const transactionHash = ckb.utils.rawTransactionToHash(tx) + +// will be PartiallySigned after alice sign +const aliceSign = ckb.signWitnesses(new Map([[multisigLockHash, { + sk: alice.privateKey, + blake160: alice.blake160, + config: multisigConfig, + signatures: [] +}]]))({ + transactionHash, + witnesses: [ + { + lock: "", + inputType: "", + outputType: "" + } + ], + inputCells +}) + +// deliver alice's blake160 to signatures, and deliver signed witness as witness parameter will be Signed after bob sign +const bobSign = ckb.signWitnesses(new Map([[multisigLockHash, { + sk: bob.privateKey, + blake160: bob.blake160, + config: multisigConfig, + signatures: [alice.blake160] +}]]))({ + transactionHash, + witnesses: aliceSign, + inputCells +}); + +(async function(){ + tx.witnesses = bobSign + const txHash = await ckb.rpc.sendTransaction(tx) + console.log(txHash) +}()) diff --git a/packages/ckb-sdk-core/src/multisig.ts b/packages/ckb-sdk-core/src/multisig.ts new file mode 100644 index 00000000..216e268c --- /dev/null +++ b/packages/ckb-sdk-core/src/multisig.ts @@ -0,0 +1,79 @@ +import { blake2b, PERSONAL, hexToBytes } from '@nervosnetwork/ckb-sdk-utils' + +export type MultisigConfig = { + r: number + m: number + n: number + blake160s: string[] +} + +export function isMultisigConfig(config: any): config is MultisigConfig { + return config + && !Number.isNaN(+config.r) + && !Number.isNaN(+config.m) + && !Number.isNaN(+config.n) + && Array.isArray(config.blake160s) +} + +export type Signatures = Record + +export enum SignStatus { + Signed = 'Signed', + Unsigned = 'Unsigned', + PartiallySigned = 'PartiallySigned' +} + +const validateMultisigCount = (v: number) => { + if (v < 0 || v > 255) { + throw new Error('For multisig sign, signer should between 0 and 255') + } +} + +const toHex = (v: number) => { + return v.toString(16).padStart(2, '0') +} + +const validateMultisigConfig = (config: MultisigConfig) => { + validateMultisigCount(config.r) + validateMultisigCount(config.m) + validateMultisigCount(config.n) + if (config.m > config.n) throw new Error(`For m of n multisig sign, m shouldn't be greater than n`) + if (config.r > config.m) throw new Error(`For m of n multisig sign, r shouldn't be greater than m`) + if (config.n !== config.blake160s.length) throw new Error(`For m of n multisig sign, signer's length should equal with n`) +} + +export const serializeMultisigConfig = (config: MultisigConfig) => { + validateMultisigConfig(config) + // default s is 00 + return `0x00${toHex(config.r)}${toHex(config.m)}${toHex(config.n)}${config.blake160s.reduce((pre, cur) => pre + cur.slice(2), '')}` +} + +export const hashMultisig = (config: MultisigConfig) => { + const blake2bHash = blake2b(32, null, null, PERSONAL) + blake2bHash.update(hexToBytes(serializeMultisigConfig(config))) + return `0x${blake2bHash.digest('hex')}`.slice(0, 42) +} + +export const getMultisigStatus = (config: MultisigConfig, signatures: CKBComponents.Bytes[] = []) => { + let signedForM = 0 + let signedForR = 0 + for (let i = 0; i < config.n; i++) { + if (signatures.includes(config.blake160s[i])) { + if (i < config.r) { + signedForR += 1 + } else { + signedForM += 1 + } + } + } + if (signedForM + signedForR === 0) { + return SignStatus.Unsigned + } + if (signedForM > config.m - config.r) { + throw new Error('More signature for multisig') + } + if (signedForM + signedForR < config.m) { + return SignStatus.PartiallySigned + } + return SignStatus.Signed +} diff --git a/packages/ckb-sdk-core/src/signWitnessGroup.ts b/packages/ckb-sdk-core/src/signWitnessGroup.ts index c8039f03..c4dc880d 100644 --- a/packages/ckb-sdk-core/src/signWitnessGroup.ts +++ b/packages/ckb-sdk-core/src/signWitnessGroup.ts @@ -1,14 +1,29 @@ import { blake2b, hexToBytes, PERSONAL, toUint64Le, serializeWitnessArgs } from '@nervosnetwork/ckb-sdk-utils' import ECPair from '@nervosnetwork/ckb-sdk-utils/lib/ecpair' +import { serializeMultisigConfig, MultisigConfig } from './multisig' -type SignatureProvider = string | ((message: string | Uint8Array) => string) +export type SignatureProvider = string | ((message: string | Uint8Array) => string) type TransactionHash = string -const signWitnessGroup = ( +function signWitnessGroup( sk: SignatureProvider, transactionHash: TransactionHash, witnessGroup: StructuredWitness[], -) => { + multisigConfig?: MultisigConfig +): StructuredWitness[] +function signWitnessGroup( + sk: (message: string | Uint8Array, witness: StructuredWitness[]) => Promise, + transactionHash: TransactionHash, + witnessGroup: StructuredWitness[], + multisigConfig?: MultisigConfig +): Promise + +function signWitnessGroup( + sk: SignatureProvider | ((message: string | Uint8Array, witness: StructuredWitness[]) => Promise), + transactionHash: TransactionHash, + witnessGroup: StructuredWitness[], + multisigConfig?: MultisigConfig +) { if (!witnessGroup.length) { throw new Error('WitnessGroup cannot be empty') } @@ -20,6 +35,9 @@ const signWitnessGroup = ( ...witnessGroup[0], lock: `0x${'0'.repeat(130)}`, } + if (multisigConfig) { + emptyWitness.lock = `${serializeMultisigConfig(multisigConfig)}${'0'.repeat(130 * multisigConfig.m)}` + } const serializedEmptyWitnessBytes = hexToBytes(serializeWitnessArgs(emptyWitness)) const serializedEmptyWitnessSize = serializedEmptyWitnessBytes.length @@ -39,10 +57,18 @@ const signWitnessGroup = ( if (typeof sk === 'string') { const keyPair = new ECPair(sk) emptyWitness.lock = keyPair.signRecoverable(message) + return [multisigConfig ? emptyWitness : serializeWitnessArgs(emptyWitness), ...witnessGroup.slice(1)] } else { - emptyWitness.lock = sk(message) + const skResult = sk(message, [emptyWitness, ...witnessGroup.slice(1)]) + if (typeof skResult === 'string') { + emptyWitness.lock = skResult + return [multisigConfig ? emptyWitness : serializeWitnessArgs(emptyWitness), ...witnessGroup.slice(1)] + } + return skResult.then(res => { + emptyWitness.lock = res + return [multisigConfig ? emptyWitness : serializeWitnessArgs(emptyWitness), ...witnessGroup.slice(1)] + }) } - return [serializeWitnessArgs(emptyWitness), ...witnessGroup.slice(1)] } export default signWitnessGroup diff --git a/packages/ckb-sdk-core/src/signWitnesses.ts b/packages/ckb-sdk-core/src/signWitnesses.ts index 0856870c..d03c26de 100644 --- a/packages/ckb-sdk-core/src/signWitnesses.ts +++ b/packages/ckb-sdk-core/src/signWitnesses.ts @@ -1,24 +1,31 @@ +import { serializeWitnessArgs } from '@nervosnetwork/ckb-sdk-utils' import { ParameterRequiredException } from '@nervosnetwork/ckb-sdk-utils/lib/exceptions' -import signWitnessGroup from './signWitnessGroup' +import signWitnessGroup, { SignatureProvider } from './signWitnessGroup' import groupScripts from './groupScripts' +import { getMultisigStatus, isMultisigConfig, MultisigConfig, serializeMultisigConfig, SignStatus } from './multisig' -type SignatureProvider = string | ((message: string | Uint8Array) => string) type LockHash = string type TransactionHash = string type CachedLock = { lock: CKBComponents.Script } +export type MultisigOption = { + sk: SignatureProvider + blake160: string + config: MultisigConfig + signatures: string[] +} export interface SignWitnesses { (key: SignatureProvider): (params: { transactionHash: TransactionHash witnesses: StructuredWitness[] }) => StructuredWitness[] - (key: Map): (params: { + (key: Map): (params: { transactionHash: TransactionHash witnesses: StructuredWitness[] inputCells: CachedLock[] skipMissingKeys: boolean }) => StructuredWitness[] - (key: SignatureProvider | Map): (params: { + (key: SignatureProvider | Map): (params: { transactionHash: TransactionHash witnesses: StructuredWitness[] inputCells?: CachedLock[] @@ -30,7 +37,21 @@ export const isMap = (val: any): val is Map => { return val.size !== undefined } -const signWitnesses: SignWitnesses = (key: SignatureProvider | Map) => ({ +function isMultisigOption(params: any): params is MultisigOption { + if (params.sk && params.blake160 && params.config && params.signatures) { + if ((typeof params.sk === 'string' || typeof params.sk === 'function') + && typeof params.blake160 === 'string' + && Array.isArray(params.signatures) + && isMultisigConfig(params.config) + ) { + return true + } + throw new Error('Multisig options is incorrect') + } + throw new Error('Multisig options miss some property') +} + +const signWitnesses: SignWitnesses = (key: SignatureProvider | Map) => ({ transactionHash, witnesses = [], inputCells = [], @@ -46,10 +67,12 @@ const signWitnesses: SignWitnesses = (key: SignatureProvider | Map { const sk = key.get(lockhash) if (!sk) { @@ -61,9 +84,31 @@ const signWitnesses: SignWitnesses = (key: SignatureProvider | Map witnesses[idx]), ...restWitnesses] - - const witnessIncludeSignature = signWitnessGroup(sk, transactionHash, ws)[0] - rawWitnesses[indices[0]] = witnessIncludeSignature + if (typeof sk === 'object' && isMultisigOption(sk)) { + const witnessIncludeSignature = signWitnessGroup(sk.sk, transactionHash, ws, sk.config)[0] + // is multisig sign + const firstWitness = rawWitnesses[indices[0]] + if (typeof firstWitness !== 'object') { + throw new Error('The first witness in the group should be type of WitnessArgs') + } + let lockAfterSign = (witnessIncludeSignature as CKBComponents.WitnessArgs).lock + if (firstWitness.lock) { + lockAfterSign = firstWitness.lock + lockAfterSign?.slice(2) + } else { + lockAfterSign = serializeMultisigConfig(sk.config) + lockAfterSign?.slice(2) + } + const firstWitSigned = { ...firstWitness, lock: lockAfterSign } + rawWitnesses[indices[0]] = firstWitSigned + if(getMultisigStatus(sk.config, [...sk.signatures, sk.blake160]) === SignStatus.Signed) { + indices.forEach(idx => { + const wit = rawWitnesses[idx] + rawWitnesses[idx] = typeof wit === 'string' ? wit : serializeWitnessArgs(wit) + }) + } + } else { + const witnessIncludeSignature = signWitnessGroup(sk, transactionHash, ws)[0] + rawWitnesses[indices[0]] = witnessIncludeSignature + } }) return rawWitnesses }