Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement multisig with SIGN_MODE_DIRECT signers #1400

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions packages/amino/src/multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
331 changes: 320 additions & 11 deletions packages/stargate/src/multisignature.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import {
createMultisigThresholdPubkey,
encodeSecp256k1Pubkey,
makeCosmoshubPath,
MultisigThresholdPubkey,
pubkeyToAddress,
Secp256k1HdWallet,
StdFee,
} from "@cosmjs/amino";
import { coins } from "@cosmjs/proto-signing";
import { assert } from "@cosmjs/utils";
import { coins, DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing";
import { assert, sleep } from "@cosmjs/utils";
import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx";

import { MsgSendEncodeObject } from "./modules";
Expand Down Expand Up @@ -168,14 +170,48 @@ 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", () => {
it("works", async () => {
const multisigAccountAddress = "cosmos1h90ml36rcu7yegwduzgzderj2jmq49hcpfclw9";

async function multisig(): Promise<MultisigThresholdPubkey> {
// 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 5/5", 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.
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");
Expand Down Expand Up @@ -213,7 +249,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)],
});
Expand All @@ -239,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");
Expand All @@ -270,5 +302,282 @@ describe("multisignature", () => {
assertIsDeliverTxSuccess(result);
}
});

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<string, Uint8Array>([
[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);

// 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, true, true, true, true],
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.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 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<string, Uint8Array>([
[address0, signature0],
[address1, signature1],
[address2, signature2],
[address3, signature3],
[address4, signature4],
]),
"direct",
);
// ensure signature is valid
const result = await broadcaster.broadcastTx(signedTx);
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<string, Uint8Array>([
[address0, signature0],
[address3, signature3],
]),
"direct",
);
// ensure signature is valid
const result = await broadcaster.broadcastTx(signedTx);
assertIsDeliverTxSuccess(result);
}
});
});
});
Loading