跳到主要内容

Amino 多重签名交易

Amino 多重签名交易使用传统的 JSON 编码格式,支持多个签名者共同签署交易。本文档介绍如何使用 Amino 格式构建和签名多重签名交易。

基本概念

Amino 编码特点

  • JSON 格式: 人类可读的交易结构
  • 向后兼容: 支持旧版本的 Cosmos SDK
  • 多重签名支持: 内置多重签名功能
  • 离线签名: 支持离线环境签名

环境配置

设置多个助记词

# .env 文件
AARON="first signer mnemonic phrase"
PHCC="second signer mnemonic phrase"
PENG="third signer mnemonic phrase"

初始化环境

require('dotenv').config();

const { AARON, PHCC, PENG } = process.env;
const rpcEndpoint = 'https://rpc.testnet.cosmos.network:443';
const client = await StargateClient.connect(rpcEndpoint);

多重签名账户创建

生成公钥和地址

async function getMnemonicPubKeyAndAddress(mnemonic, prefix) {
const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
hdPaths: [makeCosmoshubPath(0)],
});
const [account] = await wallet.getAccounts();
const secp256k1PubKey = encodeSecp256k1Pubkey(account.pubkey);
const address = pubkeyToAddress(secp256k1PubKey, prefix);

return {
wallet,
secp256k1PubKey,
pubkey: toHex(account.pubkey),
address
};
}

// 并行生成所有密钥
const keys = await Promise.all([
AARON, PHCC, PENG
].map((mnemonic) => getMnemonicPubKeyAndAddress(mnemonic, prefix)));

const secp256k1PubKeys = keys.map((item) => item.secp256k1PubKey);
const pubKeys = keys.map((item) => item.pubkey);

创建多重签名公钥

const threshold = 2;
const prefix = 'cosmos';

var multisigPubkey = createMultisigThresholdPubkey(secp256k1PubKeys, threshold, true);
const multisigAddress = pubkeyToAddress(multisigPubkey, prefix);
console.log('Multisig address:', multisigAddress);

交易构建

查询账户信息

// 查询多重签名账户
const accountOnChain = await client.getAccount(multisigAddress);
assert(accountOnChain, 'Account does not exist on chain');
console.log('Account on chain:', accountOnChain);

// 查询余额
const balance = await client.getBalance(multisigAddress, 'uphoton');
console.log('Balance:', balance);

构建发送消息

const msgSend = {
fromAddress: multisigAddress,
toAddress: receipt,
amount: coins(2000, 'uphoton'),
};

const msg = {
typeUrl: '/cosmos.bank.v1beta1.MsgSend',
value: msgSend,
};

console.log('Message:', JSON.stringify(msg, null, 4));

设置费用和参数

const gasLimit = 200000;
const fee = {
amount: coins(2000, 'uphoton'),
gas: gasLimit.toString(),
};

const chainId = await client.getChainId();
const memo = 'happy';

签名指令创建

创建签名指令

const signingInstruction = {
accountNumber: accountOnChain.accountNumber,
sequence: accountOnChain.sequence,
chainId,
msgs: [msg],
fee,
memo,
};

离线签名

创建签名文档

async function makeAminoSignDoc(messages, fee, memo, signerData) {
const { accountNumber, sequence, chainId } = signerData;
const msgs = messages.map((msg) => aminoTypes.toAmino(msg));
return makeSignDoc(msgs, fee, chainId, memo, accountNumber, sequence);
}

执行签名

async function signInstruction(mnemonic, instruction, rpcEndpoint) {
const wallet = await Secp256k1HdWallet.fromMnemonic(mnemonic, {
hdPaths: [makeCosmoshubPath(0)],
});

const [account] = await wallet.getAccounts();
const pubkey = encodeSecp256k1Pubkey(account.pubkey);

const signerData = {
accountNumber: instruction.accountNumber,
sequence: instruction.sequence,
chainId: instruction.chainId,
};

// 创建签名文档
const signDoc = await makeAminoSignDoc(
instruction.msgs,
instruction.fee,
instruction.memo,
signerData
);

const signMode = SignMode.SIGN_MODE_LEGACY_AMINO_JSON;

// 获取私钥
const [acc] = await wallet.getAccountsWithPrivkeys();
const { privkey } = acc;

// 计算消息哈希
const message = crypto.sha256(serializeSignDoc(signDoc));

// 创建签名
const signature = await crypto.Secp256k1.createSignature(message, privkey);
const signatureBytes = new Uint8Array([
...signature.r(32),
...signature.s(32)
]);

console.log('Public key:', toHex(account.pubkey));
console.log('Message hash:', toHex(message));
console.log('Signature:', toHex(signatureBytes));

const signed = signDoc;

// 编码交易体
const signedTxBody = {
messages: signed.msgs.map((msg) => aminoTypes.fromAmino(msg)),
memo: signed.memo,
};

const signedTxBodyEncodeObject = {
typeUrl: '/cosmos.tx.v1beta1.TxBody',
value: signedTxBody,
};

const signedTxBodyBytes = registry.encode(signedTxBodyEncodeObject);

// 创建认证信息
const signedGasLimit = Int53.fromString(signed.fee.gas).toNumber();
const signedSequence = Int53.fromString(signed.sequence).toNumber();
const signedAuthInfoBytes = makeAuthInfoBytes(
[{ pubkey, sequence: signedSequence }],
signed.fee.amount,
signedGasLimit,
signMode
);

const signatures = [signatureBytes];
const authInfo = AuthInfo.decode(signedAuthInfoBytes);

// 构建交易
const tx = TxRaw.fromPartial({
bodyBytes: signedTxBodyBytes,
authInfoBytes: signedAuthInfoBytes,
signatures,
});

return [pubkey, signatures[0], signedTxBodyBytes, tx];
}

并行签名收集

收集所有签名

const [
[pubkey0, signature0, bodyBytes],
[pubkey1, signature1],
[pubkey2, signature2]
] = await Promise.all(
[AARON, PHCC, PENG].map(async (mnemonic) =>
signInstruction(mnemonic, signingInstruction, rpcEndpoint)
)
);

console.log('Public key 0:', pubkey0);
console.log('Public key 1:', pubkey1);
console.log('Public key 2:', pubkey2);

交易组装

创建多重签名交易

const address0 = pubkeyToAddress(pubkey0, prefix);
const address1 = pubkeyToAddress(pubkey1, prefix);
const address2 = pubkeyToAddress(pubkey2, prefix);

// 重新创建多重签名公钥
var multisigPubkey = createMultisigThresholdPubkey(
[pubkey0, pubkey1, pubkey2],
threshold,
true
);

// 验证地址一致性
assert.equal(
multisigAccountAddress,
pubkeyToAddress(multisigPubkey, prefix),
'should be equal'
);

// 创建多重签名交易
const signedTx = makeMultisignedTx(
multisigPubkey,
signingInstruction.sequence,
signingInstruction.fee,
bodyBytes,
new Map([
[address0, signature0],
[address1, signature1],
// [address2, signature2], // 可选签名
])
);

交易广播

编码和广播

const tx = TxRaw.encode(signedTx).finish();
console.log('Transaction bytes:', toHex(tx));

const result = await client.broadcastTx(tx);
console.log('Broadcast result:', result);
assertIsDeliverTxSuccess(result);

const { transactionHash } = result;
console.log('Transaction URL:', `https://api.testnet.cosmos.network/cosmos/tx/v1beta1/txs/${transactionHash}`);

完整示例

查看 examples/cosmos/multisig/amino.js 文件获取完整的 Amino 多重签名交易示例代码。

注意事项

  • 确保所有签名者使用相同的签名指令
  • 验证多重签名地址的一致性
  • 合理设置阈值和签名者数量
  • 在生产环境中使用硬件钱包
  • 定期验证多重签名配置
  • 备份所有参与者的助记词