From bd745adf05177797b5f16b0af7a3e00bc566b754 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 30 Mar 2023 13:37:25 +0200 Subject: [PATCH 1/6] Document createMultisigThresholdPubkey --- packages/amino/src/multisig.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/amino/src/multisig.ts b/packages/amino/src/multisig.ts index 7e8ecc5a22..4dea03e8fa 100644 --- a/packages/amino/src/multisig.ts +++ b/packages/amino/src/multisig.ts @@ -17,6 +17,15 @@ export function compareArrays(a: Uint8Array, b: Uint8Array): number { return aHex === bHex ? 0 : aHex < bHex ? -1 : 1; } +/** + * Creates a multisig pubkey of type tendermint/PubKeyMultisigThreshold. + * This is a single pubkey which internally includes the list of potential signers + * as well as the threshold value. + * + * The provided pubkeys are sorted internally. For some rare cases where unsorted + * multisig addresses have been created you can set the `nosort` argument to true + * to deactivate the sorting. + */ export function createMultisigThresholdPubkey( pubkeys: readonly SinglePubkey[], threshold: number, From 1f402c539d863e64db29ea0c21beaab48f9b3103 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 30 Mar 2023 13:38:47 +0200 Subject: [PATCH 2/6] Create signDirectForMultisig and make it work --- packages/stargate/src/multisignature.spec.ts | 124 +++++++++++++++++- packages/stargate/src/multisignature.ts | 61 ++++++--- .../stargate/src/signingstargateclient.ts | 51 ++++++- 3 files changed, 209 insertions(+), 27 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 1609e2ec92..0a022a60c4 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -5,8 +5,8 @@ import { pubkeyToAddress, Secp256k1HdWallet, } from "@cosmjs/amino"; -import { coins } from "@cosmjs/proto-signing"; -import { assert } from "@cosmjs/utils"; +import { coins, DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { assert, sleep } from "@cosmjs/utils"; import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; import { MsgSendEncodeObject } from "./modules"; @@ -169,9 +169,10 @@ describe("multisignature", () => { }); describe("makeMultisignedTxBytes", () => { - it("works", async () => { + const multisigAccountAddress = "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"; + + it("works for Amino JSON sign mode", async () => { pendingWithoutSimapp(); - const multisigAccountAddress = "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"; // On the composer's machine signing instructions are created. // The composer does not need to be one of the signers. @@ -213,7 +214,7 @@ describe("multisignature", () => { [pubkey4, signature4], ] = await Promise.all( [0, 1, 2, 3, 4].map(async (i) => { - // Signing environment + // Signing environment. Secp256k1HdWallet only supports Amino JSON signing. const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, { hdPaths: [makeCosmoshubPath(i)], }); @@ -270,5 +271,118 @@ describe("multisignature", () => { assertIsDeliverTxSuccess(result); } }); + + it("works for Direct sign mode", async () => { + pendingWithoutSimapp(); + + await sleep(500); + + // On the composer's machine signing instructions are created. + // The composer does not need to be one of the signers. + const signingInstruction = await (async () => { + const client = await StargateClient.connect(simapp.tendermintUrl); + const accountOnChain = await client.getAccount(multisigAccountAddress); + assert(accountOnChain, "Account does not exist on chain"); + + // In practice we don't need this wallet. Only the pubkeys are needed. + const allPubkeysWallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { + hdPaths: [ + makeCosmoshubPath(0), + makeCosmoshubPath(1), + makeCosmoshubPath(2), + makeCosmoshubPath(3), + makeCosmoshubPath(4), + ], + }); + const multisigPubkey = createMultisigThresholdPubkey( + (await allPubkeysWallet.getAccounts()).map((a) => encodeSecp256k1Pubkey(a.pubkey)), + 2, + ); + expect(pubkeyToAddress(multisigPubkey, "cosmos")).toEqual(multisigAccountAddress); + + const msgSend: MsgSend = { + fromAddress: multisigAccountAddress, + toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", + amount: coins(1234, "ucosm"), + }; + const msg: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const gasLimit = 200000; + const fee = { + amount: coins(2000, "ucosm"), + gas: gasLimit.toString(), + }; + + return { + msgs: [msg], + chainId: await client.getChainId(), + multisigPubkey, + accountNumber: accountOnChain.accountNumber, + sequence: accountOnChain.sequence, + fee: fee, + memo: "Use your tokens wisely", + }; + })(); + + const [ + [pubkey0, signature0, bodyBytes], + [pubkey1, signature1], + [pubkey2, signature2], + [pubkey3, signature3], + [pubkey4, signature4], + ] = await Promise.all( + [0, 1, 2, 3, 4].map(async (i) => { + // Signing environment. DirectSecp256k1HdWallet only supports Direct signing. + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { + hdPaths: [makeCosmoshubPath(i)], + }); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const { bodyBytes: bb, signatures } = await signingClient.signDirectForMultisig( + address, + signingInstruction.msgs, + signingInstruction.chainId, + signingInstruction.multisigPubkey, + signingInstruction.sequence, + signingInstruction.accountNumber, + signingInstruction.fee, + signingInstruction.memo, + ); + return [pubkey, signatures[0], bb] as const; + }), + ); + + // From here on, no private keys are required anymore. Any anonymous entity + // can collect, assemble and broadcast. + { + const address0 = pubkeyToAddress(pubkey0, "cosmos"); + const address1 = pubkeyToAddress(pubkey1, "cosmos"); + const address2 = pubkeyToAddress(pubkey2, "cosmos"); + const address3 = pubkeyToAddress(pubkey3, "cosmos"); + const address4 = pubkeyToAddress(pubkey4, "cosmos"); + + const broadcaster = await StargateClient.connect(simapp.tendermintUrl); + const signedTx = makeMultisignedTxBytes( + signingInstruction.multisigPubkey, + signingInstruction.sequence, + signingInstruction.fee, + bodyBytes, + new Map([ + [address0, signature0], + [address1, signature1], + [address2, signature2], + [address3, signature3], + [address4, signature4], + ]), + "direct", + ); + // ensure signature is valid + const result = await broadcaster.broadcastTx(signedTx); + assertIsDeliverTxSuccess(result); + } + }); }); }); diff --git a/packages/stargate/src/multisignature.ts b/packages/stargate/src/multisignature.ts index dd5440ad5b..5c8abc2b9f 100644 --- a/packages/stargate/src/multisignature.ts +++ b/packages/stargate/src/multisignature.ts @@ -22,12 +22,48 @@ export function makeCompactBitArray(bits: readonly boolean[]): CompactBitArray { return CompactBitArray.fromPartial({ elems: bytes, extraBitsStored: extraBits }); } +export function makeAuthInfoBytesForMultisig( + multisigPubkey: MultisigThresholdPubkey, + sequence: number, + fee: StdFee, + signers: boolean[], + signMode: "amino_json" | "direct", +): Uint8Array { + const mode: SignMode = + signMode == "direct" ? SignMode.SIGN_MODE_DIRECT : SignMode.SIGN_MODE_LEGACY_AMINO_JSON; + const signerInfo: SignerInfo = { + publicKey: encodePubkey(multisigPubkey), + modeInfo: { + multi: { + bitarray: makeCompactBitArray(signers), + modeInfos: signers.map((_) => ({ + // here we assume the signers themselves are no multisigs + single: { mode }, + })), + }, + }, + sequence: Long.fromNumber(sequence), + }; + + const authInfo = AuthInfo.fromPartial({ + signerInfos: [signerInfo], + fee: { + amount: [...fee.amount], + gasLimit: Long.fromString(fee.gas), + }, + }); + + return AuthInfo.encode(authInfo).finish(); +} + /** * Creates a signed transaction from signer info, transaction body and signatures. * The result can be broadcasted after serialization. * * Consider using `makeMultisignedTxBytes` instead if you want to broadcast the * transaction immediately. + * + * @param signMode is the sign mode used by all signers. Mixed signing mode is not yet supported. */ export function makeMultisignedTx( multisigPubkey: MultisigThresholdPubkey, @@ -35,6 +71,7 @@ export function makeMultisignedTx( fee: StdFee, bodyBytes: Uint8Array, signatures: Map, + signMode: "amino_json" | "direct" = "amino_json", ): TxRaw { const addresses = Array.from(signatures.keys()); const prefix = fromBech32(addresses[0]).prefix; @@ -50,26 +87,7 @@ export function makeMultisignedTx( } } - const signerInfo: SignerInfo = { - publicKey: encodePubkey(multisigPubkey), - modeInfo: { - multi: { - bitarray: makeCompactBitArray(signers), - modeInfos: signaturesList.map((_) => ({ single: { mode: SignMode.SIGN_MODE_LEGACY_AMINO_JSON } })), - }, - }, - sequence: Long.fromNumber(sequence), - }; - - const authInfo = AuthInfo.fromPartial({ - signerInfos: [signerInfo], - fee: { - amount: [...fee.amount], - gasLimit: Long.fromString(fee.gas), - }, - }); - - const authInfoBytes = AuthInfo.encode(authInfo).finish(); + const authInfoBytes = makeAuthInfoBytesForMultisig(multisigPubkey, sequence, fee, signers, signMode); const signedTx = TxRaw.fromPartial({ bodyBytes: bodyBytes, authInfoBytes: authInfoBytes, @@ -90,7 +108,8 @@ export function makeMultisignedTxBytes( fee: StdFee, bodyBytes: Uint8Array, signatures: Map, + signMode: "amino_json" | "direct" = "amino_json", ): Uint8Array { - const signedTx = makeMultisignedTx(multisigPubkey, sequence, fee, bodyBytes, signatures); + const signedTx = makeMultisignedTx(multisigPubkey, sequence, fee, bodyBytes, signatures, signMode); return Uint8Array.from(TxRaw.encode(signedTx).finish()); } diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index d782e08c72..3042273f4f 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -1,4 +1,9 @@ -import { encodeSecp256k1Pubkey, makeSignDoc as makeSignDocAmino, StdFee } from "@cosmjs/amino"; +import { + encodeSecp256k1Pubkey, + makeSignDoc as makeSignDocAmino, + MultisigThresholdPubkey, + StdFee, +} from "@cosmjs/amino"; import { fromBase64 } from "@cosmjs/encoding"; import { Int53, Uint53 } from "@cosmjs/math"; import { @@ -50,6 +55,7 @@ import { createStakingAminoConverters, createVestingAminoConverters, } from "./modules"; +import { makeAuthInfoBytesForMultisig } from "./multisignature"; import { DeliverTxResponse, StargateClient, StargateClientOptions } from "./stargateclient"; export const defaultRegistryTypes: ReadonlyArray<[string, GeneratedType]> = [ @@ -345,6 +351,49 @@ export class SigningStargateClient extends StargateClient { : this.signAmino(signerAddress, messages, fee, memo, signerData); } + public async signDirectForMultisig( + signerAddress: string, + messages: readonly EncodeObject[], + chainId: string, + multisigPubkey: MultisigThresholdPubkey, + multisigSequence: number, + multisigAccountNumber: number, + fee: StdFee, + memo: string, + ): Promise { + assert(isOfflineDirectSigner(this.signer)); + const accountFromSigner = (await this.signer.getAccounts()).find( + (account) => account.address === signerAddress, + ); + if (!accountFromSigner) { + throw new Error("Failed to retrieve account from signer"); + } + // const pubkey = encodePubkey(encodeSecp256k1Pubkey(accountFromSigner.pubkey)); + const txBodyEncodeObject: TxBodyEncodeObject = { + typeUrl: "/cosmos.tx.v1beta1.TxBody", + value: { + messages: messages, + memo: memo, + }, + }; + const txBodyBytes = this.registry.encode(txBodyEncodeObject); + const signers = [true, true, true, true, true]; + const authInfoBytes = makeAuthInfoBytesForMultisig( + multisigPubkey, + multisigSequence, + fee, + signers, + "direct", + ); + const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, multisigAccountNumber); + const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc); + return TxRaw.fromPartial({ + bodyBytes: signed.bodyBytes, + authInfoBytes: signed.authInfoBytes, + signatures: [fromBase64(signature.signature)], + }); + } + private async signAmino( signerAddress: string, messages: readonly EncodeObject[], From 8a88ec919ac520ce9c564dbcaf859348e84693fb Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 30 Mar 2023 14:09:32 +0200 Subject: [PATCH 3/6] Make multisigSigners configurable --- packages/stargate/src/multisignature.spec.ts | 24 ++++++++++++++++--- .../stargate/src/signingstargateclient.ts | 4 ++-- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 0a022a60c4..48d1a8a931 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -2,10 +2,12 @@ import { createMultisigThresholdPubkey, encodeSecp256k1Pubkey, makeCosmoshubPath, + MultisigThresholdPubkey, pubkeyToAddress, Secp256k1HdWallet, + StdFee, } from "@cosmjs/amino"; -import { coins, DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"; +import { coins, DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing"; import { assert, sleep } from "@cosmjs/utils"; import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx"; @@ -168,6 +170,20 @@ describe("multisignature", () => { }); }); + interface SigningInstructionsAminoJson { + msgs: readonly EncodeObject[]; + chainId: string; + sequence: number; + accountNumber: number; + fee: StdFee; + memo: string; + } + + interface SigningInstructionsDirect extends SigningInstructionsAminoJson { + multisigPubkey: MultisigThresholdPubkey; + signers: boolean[]; + } + describe("makeMultisignedTxBytes", () => { const multisigAccountAddress = "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"; @@ -176,7 +192,7 @@ describe("multisignature", () => { // On the composer's machine signing instructions are created. // The composer does not need to be one of the signers. - const signingInstruction = await (async () => { + const signingInstruction: SigningInstructionsAminoJson = await (async () => { const client = await StargateClient.connect(simapp.tendermintUrl); const accountOnChain = await client.getAccount(multisigAccountAddress); assert(accountOnChain, "Account does not exist on chain"); @@ -279,7 +295,7 @@ describe("multisignature", () => { // On the composer's machine signing instructions are created. // The composer does not need to be one of the signers. - const signingInstruction = await (async () => { + const signingInstruction: SigningInstructionsDirect = await (async () => { const client = await StargateClient.connect(simapp.tendermintUrl); const accountOnChain = await client.getAccount(multisigAccountAddress); assert(accountOnChain, "Account does not exist on chain"); @@ -321,6 +337,7 @@ describe("multisignature", () => { multisigPubkey, accountNumber: accountOnChain.accountNumber, sequence: accountOnChain.sequence, + signers: [true, true, true, true, true], fee: fee, memo: "Use your tokens wisely", }; @@ -348,6 +365,7 @@ describe("multisignature", () => { signingInstruction.multisigPubkey, signingInstruction.sequence, signingInstruction.accountNumber, + signingInstruction.signers, signingInstruction.fee, signingInstruction.memo, ); diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 3042273f4f..1736955cc5 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -358,6 +358,7 @@ export class SigningStargateClient extends StargateClient { multisigPubkey: MultisigThresholdPubkey, multisigSequence: number, multisigAccountNumber: number, + multisigSigners: boolean[], fee: StdFee, memo: string, ): Promise { @@ -377,12 +378,11 @@ export class SigningStargateClient extends StargateClient { }, }; const txBodyBytes = this.registry.encode(txBodyEncodeObject); - const signers = [true, true, true, true, true]; const authInfoBytes = makeAuthInfoBytesForMultisig( multisigPubkey, multisigSequence, fee, - signers, + multisigSigners, "direct", ); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, multisigAccountNumber); From e59d28f943cd9909fc839734493402b179daa520 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 30 Mar 2023 14:37:45 +0200 Subject: [PATCH 4/6] Pull out multisig --- packages/stargate/src/multisignature.spec.ts | 37 +++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 48d1a8a931..7c2868db49 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -187,6 +187,25 @@ describe("multisignature", () => { describe("makeMultisignedTxBytes", () => { const multisigAccountAddress = "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9"; + async function multisig(): Promise { + // In practice we don't need this wallet. Only the pubkeys are needed. + const allPubkeysWallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { + hdPaths: [ + makeCosmoshubPath(0), + makeCosmoshubPath(1), + makeCosmoshubPath(2), + makeCosmoshubPath(3), + makeCosmoshubPath(4), + ], + }); + const multisigPubkey = createMultisigThresholdPubkey( + (await allPubkeysWallet.getAccounts()).map((a) => encodeSecp256k1Pubkey(a.pubkey)), + 2, + ); + expect(pubkeyToAddress(multisigPubkey, "cosmos")).toEqual(multisigAccountAddress); + return multisigPubkey; + } + it("works for Amino JSON sign mode", async () => { pendingWithoutSimapp(); @@ -300,22 +319,6 @@ describe("multisignature", () => { const accountOnChain = await client.getAccount(multisigAccountAddress); assert(accountOnChain, "Account does not exist on chain"); - // In practice we don't need this wallet. Only the pubkeys are needed. - const allPubkeysWallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { - hdPaths: [ - makeCosmoshubPath(0), - makeCosmoshubPath(1), - makeCosmoshubPath(2), - makeCosmoshubPath(3), - makeCosmoshubPath(4), - ], - }); - const multisigPubkey = createMultisigThresholdPubkey( - (await allPubkeysWallet.getAccounts()).map((a) => encodeSecp256k1Pubkey(a.pubkey)), - 2, - ); - expect(pubkeyToAddress(multisigPubkey, "cosmos")).toEqual(multisigAccountAddress); - const msgSend: MsgSend = { fromAddress: multisigAccountAddress, toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", @@ -334,7 +337,7 @@ describe("multisignature", () => { return { msgs: [msg], chainId: await client.getChainId(), - multisigPubkey, + multisigPubkey: await multisig(), accountNumber: accountOnChain.accountNumber, sequence: accountOnChain.sequence, signers: [true, true, true, true, true], From 8223ea477fad030662d6e35e7ae52be975f83838 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 30 Mar 2023 14:40:31 +0200 Subject: [PATCH 5/6] Use multisig() --- packages/stargate/src/multisignature.spec.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 7c2868db49..6d4963411f 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -275,11 +275,7 @@ describe("multisignature", () => { // From here on, no private keys are required anymore. Any anonymous entity // can collect, assemble and broadcast. { - const multisigPubkey = createMultisigThresholdPubkey( - [pubkey0, pubkey1, pubkey2, pubkey3, pubkey4], - 2, - ); - expect(pubkeyToAddress(multisigPubkey, "cosmos")).toEqual(multisigAccountAddress); + const multisigPubkey = await multisig(); const address0 = pubkeyToAddress(pubkey0, "cosmos"); const address1 = pubkeyToAddress(pubkey1, "cosmos"); From a65e011b49ff71b648d8a91b66ea0acb718a5b6f Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Thu, 30 Mar 2023 15:22:00 +0200 Subject: [PATCH 6/6] Fix and test sign modes --- packages/stargate/src/multisignature.spec.ts | 182 +++++++++++++++++- packages/stargate/src/multisignature.ts | 19 +- .../stargate/src/signingstargateclient.ts | 4 +- 3 files changed, 196 insertions(+), 9 deletions(-) diff --git a/packages/stargate/src/multisignature.spec.ts b/packages/stargate/src/multisignature.spec.ts index 6d4963411f..54fada1f2f 100644 --- a/packages/stargate/src/multisignature.spec.ts +++ b/packages/stargate/src/multisignature.spec.ts @@ -206,7 +206,7 @@ describe("multisignature", () => { return multisigPubkey; } - it("works for Amino JSON sign mode", async () => { + it("works for Amino JSON sign mode 5/5", async () => { pendingWithoutSimapp(); // On the composer's machine signing instructions are created. @@ -303,7 +303,98 @@ describe("multisignature", () => { } }); - it("works for Direct sign mode", async () => { + it("works for Amino JSON sign mode 2/5", async () => { + pendingWithoutSimapp(); + + // On the composer's machine signing instructions are created. + // The composer does not need to be one of the signers. + const signingInstruction: SigningInstructionsAminoJson = await (async () => { + const client = await StargateClient.connect(simapp.tendermintUrl); + const accountOnChain = await client.getAccount(multisigAccountAddress); + assert(accountOnChain, "Account does not exist on chain"); + + const msgSend: MsgSend = { + fromAddress: multisigAccountAddress, + toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", + amount: coins(1234, "ucosm"), + }; + const msg: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const gasLimit = 200000; + const fee = { + amount: coins(2000, "ucosm"), + gas: gasLimit.toString(), + }; + + return { + accountNumber: accountOnChain.accountNumber, + sequence: accountOnChain.sequence, + chainId: await client.getChainId(), + msgs: [msg], + fee: fee, + memo: "Use your tokens wisely", + }; + })(); + + const [[pubkey0, signature0, bodyBytes], [pubkey3, signature3]] = await Promise.all( + [0, 3].map(async (i) => { + // Signing environment. Secp256k1HdWallet only supports Amino JSON signing. + const wallet = await Secp256k1HdWallet.fromMnemonic(faucet.mnemonic, { + hdPaths: [makeCosmoshubPath(i)], + }); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const signerData: SignerData = { + accountNumber: signingInstruction.accountNumber, + sequence: signingInstruction.sequence, + chainId: signingInstruction.chainId, + }; + const { bodyBytes: bb, signatures } = await signingClient.sign( + address, + signingInstruction.msgs, + signingInstruction.fee, + signingInstruction.memo, + signerData, + ); + return [pubkey, signatures[0], bb] as const; + }), + ); + + const multisigPubkey = await multisig(); + + // From here on, no private keys are required anymore. Any anonymous entity + // can collect, assemble and broadcast. + { + const address0 = pubkeyToAddress(pubkey0, "cosmos"); + // const address1 = pubkeyToAddress(pubkey1, "cosmos"); + // const address2 = pubkeyToAddress(pubkey2, "cosmos"); + const address3 = pubkeyToAddress(pubkey3, "cosmos"); + // const address4 = pubkeyToAddress(pubkey4, "cosmos"); + + const broadcaster = await StargateClient.connect(simapp.tendermintUrl); + const signedTx = makeMultisignedTxBytes( + multisigPubkey, + signingInstruction.sequence, + signingInstruction.fee, + bodyBytes, + new Map([ + [address0, signature0], + // [address1, signature1], + // [address2, signature2], + [address3, signature3], + // [address4, signature4], + ]), + ); + // ensure signature is valid + const result = await broadcaster.broadcastTx(signedTx); + assertIsDeliverTxSuccess(result); + } + }); + + it("works for Direct sign mode 5/5", async () => { pendingWithoutSimapp(); await sleep(500); @@ -401,5 +492,92 @@ describe("multisignature", () => { assertIsDeliverTxSuccess(result); } }); + + it("works for Direct sign mode 2/5", async () => { + pendingWithoutSimapp(); + + await sleep(500); + + // On the composer's machine signing instructions are created. + // The composer does not need to be one of the signers. + const signingInstruction: SigningInstructionsDirect = await (async () => { + const client = await StargateClient.connect(simapp.tendermintUrl); + const accountOnChain = await client.getAccount(multisigAccountAddress); + assert(accountOnChain, "Account does not exist on chain"); + + const msgSend: MsgSend = { + fromAddress: multisigAccountAddress, + toAddress: "cosmos19rvl6ja9h0erq9dc2xxfdzypc739ej8k5esnhg", + amount: coins(1234, "ucosm"), + }; + const msg: MsgSendEncodeObject = { + typeUrl: "/cosmos.bank.v1beta1.MsgSend", + value: msgSend, + }; + const gasLimit = 200000; + const fee = { + amount: coins(2000, "ucosm"), + gas: gasLimit.toString(), + }; + + return { + msgs: [msg], + chainId: await client.getChainId(), + multisigPubkey: await multisig(), + accountNumber: accountOnChain.accountNumber, + sequence: accountOnChain.sequence, + signers: [true, false, false, true, false], + fee: fee, + memo: "Use your tokens wisely", + }; + })(); + + const [[pubkey0, signature0, bodyBytes], [pubkey3, signature3]] = await Promise.all( + [0, 3].map(async (i) => { + // Signing environment. DirectSecp256k1HdWallet only supports Direct signing. + const wallet = await DirectSecp256k1HdWallet.fromMnemonic(faucet.mnemonic, { + hdPaths: [makeCosmoshubPath(i)], + }); + const pubkey = encodeSecp256k1Pubkey((await wallet.getAccounts())[0].pubkey); + const address = (await wallet.getAccounts())[0].address; + const signingClient = await SigningStargateClient.offline(wallet); + const { bodyBytes: bb, signatures } = await signingClient.signDirectForMultisig( + address, + signingInstruction.msgs, + signingInstruction.chainId, + signingInstruction.multisigPubkey, + signingInstruction.sequence, + signingInstruction.accountNumber, + signingInstruction.signers, + signingInstruction.fee, + signingInstruction.memo, + ); + return [pubkey, signatures[0], bb] as const; + }), + ); + + // From here on, no private keys are required anymore. Any anonymous entity + // can collect, assemble and broadcast. + { + const address0 = pubkeyToAddress(pubkey0, "cosmos"); + const address3 = pubkeyToAddress(pubkey3, "cosmos"); + + const broadcaster = await StargateClient.connect(simapp.tendermintUrl); + const signedTx = makeMultisignedTxBytes( + signingInstruction.multisigPubkey, + signingInstruction.sequence, + signingInstruction.fee, + bodyBytes, + new Map([ + [address0, signature0], + [address3, signature3], + ]), + "direct", + ); + // ensure signature is valid + const result = await broadcaster.broadcastTx(signedTx); + assertIsDeliverTxSuccess(result); + } + }); }); }); diff --git a/packages/stargate/src/multisignature.ts b/packages/stargate/src/multisignature.ts index 5c8abc2b9f..f64dfec754 100644 --- a/packages/stargate/src/multisignature.ts +++ b/packages/stargate/src/multisignature.ts @@ -27,18 +27,19 @@ export function makeAuthInfoBytesForMultisig( sequence: number, fee: StdFee, signers: boolean[], - signMode: "amino_json" | "direct", + signModes: Array<"amino_json" | "direct">, ): Uint8Array { - const mode: SignMode = - signMode == "direct" ? SignMode.SIGN_MODE_DIRECT : SignMode.SIGN_MODE_LEGACY_AMINO_JSON; const signerInfo: SignerInfo = { publicKey: encodePubkey(multisigPubkey), modeInfo: { multi: { bitarray: makeCompactBitArray(signers), - modeInfos: signers.map((_) => ({ + // This feels needs one entry per actual signature + modeInfos: signModes.map((signMode) => ({ // here we assume the signers themselves are no multisigs - single: { mode }, + single: { + mode: signMode === "direct" ? SignMode.SIGN_MODE_DIRECT : SignMode.SIGN_MODE_LEGACY_AMINO_JSON, + }, })), }, }, @@ -87,7 +88,13 @@ export function makeMultisignedTx( } } - const authInfoBytes = makeAuthInfoBytesForMultisig(multisigPubkey, sequence, fee, signers, signMode); + const authInfoBytes = makeAuthInfoBytesForMultisig( + multisigPubkey, + sequence, + fee, + signers, + signaturesList.map((_sig) => signMode), + ); const signedTx = TxRaw.fromPartial({ bodyBytes: bodyBytes, authInfoBytes: authInfoBytes, diff --git a/packages/stargate/src/signingstargateclient.ts b/packages/stargate/src/signingstargateclient.ts index 1736955cc5..09767364d6 100644 --- a/packages/stargate/src/signingstargateclient.ts +++ b/packages/stargate/src/signingstargateclient.ts @@ -378,12 +378,14 @@ export class SigningStargateClient extends StargateClient { }, }; const txBodyBytes = this.registry.encode(txBodyEncodeObject); + // Make list of sign modes. One entry per signature. + const signModes = multisigSigners.filter((signed) => signed).map((_) => "direct" as const); const authInfoBytes = makeAuthInfoBytesForMultisig( multisigPubkey, multisigSequence, fee, multisigSigners, - "direct", + signModes, ); const signDoc = makeSignDoc(txBodyBytes, authInfoBytes, chainId, multisigAccountNumber); const { signature, signed } = await this.signer.signDirect(signerAddress, signDoc);