如何自助恢复多签资产?
最后更新:2026-10-28
Ownbit 多签资产保存在链上:BTC 等币种的多签地址、EVM 多签合约、带多签权限的 TRON 账户,或 Solana 多签程序。转出这些资产需要足够多参与方的签名,不需要 Ownbit 服务器。服务器不可用时,每个参与方用 Ownbit App 里的多签恢复工具签名(可以离线使用),然后在电脑上组装交易,通过任意公开节点广播。
本文以 2-3 多签为例,参与方为 A、B、C,由 A 和 B 签名。建议先用小额试一遍,广播前核对每一个地址。不要把助记词或钱包密码告诉任何人,包括自称 Ownbit 客服的人。
开始之前
App 版本
以下功能需要 Ownbit 5.2.0 或更高版本,在本文中标为 Ownbit 5.2.0+,请先更新 App:
需要准备什么
打开多签恢复工具
首页菜单 → 区块链工具 → 多签恢复工具 → 选择币种(BTC、ETH 或 TRX;Ownbit 5.2.0+ 还有 SOL)→ 选择钱包(你的多签钱包)。填写各字段,点 多签签名,输入钱包密码。工具显示签名结果,点击即可复制。工具不需要联网。
在 Ownbit 5.2.0+ 中,每个工具顶部的 「选择多签 ›」 会列出本机保存的、符合条件的多签,选中后自动填写字段。本机没有保存的多签(例如在 Ownbit 服务器不可用时刚恢复的钱包)需要手动填写。
每个签名人填写的内容必须完全一致。部分截图来自旧版本,5.2.0 的工具界面可能略有不同。工具里的字段名在所有语言下都显示英文。
哪些链、用哪个工具、需要哪个版本
| 多签 | 恢复方式 | Ownbit 版本 | 需要 Ownbit 吗? |
|---|---|---|---|
| BTC(bc1… 和 3… 地址)、LTC、DOGE、BCH、DGB | 工具 BTC → 组装 → 广播(第 1 节) | 5.2.0+ | 不需要。m 个参与方签名。 |
| EVM:Ethereum、BNB Smart Chain、Base、HyperEVM、Polygon、Arbitrum One、Optimism | 工具 ETH → 调用合约的 spend()(第 2 节) | 5.2.0+ | 不需要。m 个 owner 签名(企业多签:Signer 权重之和达到门槛)。意外防护:连续 416 天不活跃后,链上门槛自动降低。 |
| TRON(TRX、TRC-20) | 工具 TRX → 加入签名 → 广播(第 3 节) | 5.2.0+ | 不需要。m 个参与方签名(企业多签:Signer 权重之和达到门槛)。 |
| Solana(SOL、SPL 代币) | 工具 SOL → 发起 / 批准 / 执行(第 4 节) | 5.2.0+ | 不需要。m 个参与方批准。 |
| BTC、TRON、Solana 的意外防护多签 | 同上;Ownbit 官方持有一把额外的密钥(第 5 节) | 同上 | 只有在原有密钥剩余不足 m 把时才需要。是否协助签名由 Ownbit 自行决定。 |
| XRP、Stellar | 没有工具;使用 XRP Ledger / Stellar 的标准工具(第 6 节) | 不需要 | 不需要。m 个签名人签名。 |
| DASH、ZEC、Taproot、EVM 合约 v4 / v5 | 本文不涉及(第 7 节) | — | — |
「需要 Ownbit 吗?」指 Ownbit 服务器和作为协助签名方的 Ownbit 官方。参与方自己的密钥始终是必需的。
1. BTC 及其他 UTXO 币种(LTC、DOGE、BCH、DGB)
Ownbit 的 BTC 类多签是标准的 m-of-n 脚本 OP_m <排序后的 pubkeys> OP_n OP_CHECKMULTISIG,没有时间锁。以 bc1 开头的 BTC 地址是 P2WSH,以 3 开头的 BTC 地址是 P2SH;LTC、DOGE、BCH、DGB 多签都是 P2SH。在这些链上,每个参与方的密钥都在其多签钱包的 m/44'/0'/0'/0/0,所以同一个参与方在 BTC、BCH、LTC、DOGE、DGB 上的 pubkey 相同。
a. 第一步,查找 UTXO。在区块浏览器里查询多签地址,例如 BTC 用 mempool.space,LTC、DOGE、BCH、DGB 用 blockchair.com。该地址收到且尚未花费的每一个输出都是一个 UTXO。分别记下交易 Hash、输出序号(第一个输出为 0,第二个为 1,依此类推)和金额(单位 satoshi)。
b. 第二步,构建 Input Output JSON。inputs 是这些 UTXO;outputs 是收款地址,以及没有全部转出时的找零地址(通常就是多签地址本身)。金额单位为 satoshi,inputs 总额减去 outputs 总额就是矿工费。input 的字段取决于地址类型:
| 地址类型 | 每个 input 的字段 |
|---|---|
| BTC bc1…(P2WSH) | "segwit": "D"、"txHash"、"index"、"satoshis" |
| BTC 3…(P2SH) | "txHash"、"index"(不需要 satoshis,也不需要 segwit) |
| LTC、DOGE、BCH、DGB | "txid"、"vout"、"satoshis" |
每个 output 都是 "address" 和 "amount"(satoshi)。JSON 必须合法:不能有注释,也不能有多余的逗号。
bc1 多签示例(第一个 output 是收款,第二个是找零;矿工费为 469,800 − 468,144 = 1,656 satoshi):
{
"inputs": [{
"segwit": "D",
"txHash": "757ef70941ea9170cc918097630a7aa7e4c644e3f8d73a15ff1e98d321bf5b62",
"index": 1,
"satoshis": 13400
}, {
"segwit": "D",
"txHash": "637dfeda367f051d34228badfef09726f0893653fe8baebbe6e150ea68e756f3",
"index": 2,
"satoshis": 456400
}],
"outputs": [{
"address": "bc1q2dzu8fcrr66yq9n8q5u6qhwleslqystahdw923",
"amount": 10000
}, {
"address": "bc1q8dl3vmrqmcs4xpyn884upnedlj2t6uewnfdnp034wqfjghpm5ghqupl9v9",
"amount": 458144
}]
}
3 开头多签示例:
{
"inputs": [{
"txHash": "<UTXO 的交易 Hash>",
"index": 0
}],
"outputs": [{
"address": "<收款地址>",
"amount": 50000
}]
}
LTC 示例(DOGE、BCH、DGB 字段相同):
{
"inputs": [{
"txid": "b830d41437f2f1cb3c5303c6626cc9762979bf88bd9bcfbb5a356150e86ef0ad",
"vout": 1,
"satoshis": 94960
}],
"outputs": [{
"address": "LeDXT3evSeAQyCwa2b4ohjS9YxcV9dx7AC",
"amount": 10000
}, {
"address": "MHosP5QAgWrqLGrEvYDRQVDxDohSoUsZA4",
"amount": 79920
}]
}
c. 第三步,准备 Pubkeys。多签详情页会显示每个参与方的 pubkey(Ownbit 5.2.0+ 的「多签自助恢复」页也有,「选择多签 ›」会自动填入):
填入这个多签的全部 n 个 pubkey(有意外防护的多签也要包括 Ownbit 官方的 pubkey),用冒号(:)分隔。顺序无所谓,工具会自动排序。每个 pubkey 是以 02 或 03 开头、66 个十六进制字符的压缩公钥;工具接受 2 到 9 个。示例:
02810260c48b2b4e78dd70ae59b7a8a8858cfdba204ea28896ff7a4de20e0c2514:032f7e05649fbf0278076ab1e936998eddc81417131ed55b07d42fa2aea43c25f4:<C 的 pubkey>
pubkey 也可以由参与方多签钱包的助记词推导:
助记词(mnemonic,如有 BIP39 密码短语一并使用)
↓
Seed(BIP39)
↓
主密钥(BIP32)
↓
路径 m/44'/0'/0'/0
↓
Index 0 的子公钥(m/44'/0'/0'/0/0 的 pubkey,压缩格式)
d. 第四步,签名。A 和 B 分别在自己的手机上打开 多签恢复工具 → BTC,填写:
| 字段 | 填写内容 |
|---|---|
| 选择网络 | Bitcoin (P2WSH, address bc1)、Bitcoin (P2SH, address 3)、Bitcoin Cash、Litecoin、Dogecoin 或 DigiByte |
| Input Output JSON | 第二步的 JSON |
| Pubkeys (Separated by colon) | 第三步的全部 pubkey |
| Min Signers | 签名门槛 m,例如 2-3 多签填 2 |
点 多签签名。如果 Pubkeys 里没有你自己的 pubkey,工具会拒绝签名。结果中每个 input 一个签名,按 input 顺序排列:sig1:<hex> 对应 input 0,sig2:<hex> 对应 input 1,依此类推。
e. 第五步,组装交易。两条规则:
以下是 bc1(P2WSH)多签的示例,使用 bitcoinjs-lib v6。代码会打印由 pubkeys 和 Min Signers 算出的多签地址,它必须和你的多签地址一致。
const bitcoin = require('bitcoinjs-lib'); // v6
const M = 2; // Min Signers
// 多签的全部 n 个 pubkey(与 "Pubkeys" 中填写的相同),按工具的方式排序
const PUBKEYS = [
'02810260c48b2b4e78dd70ae59b7a8a8858cfdba204ea28896ff7a4de20e0c2514', // A
'032f7e05649fbf0278076ab1e936998eddc81417131ed55b07d42fa2aea43c25f4', // B
// 示例数据来自一个 2-2 多签;2-3 多签请在这里加上 C 的 pubkey
].map(k => k.toLowerCase()).sort();
// 工具给出的签名,按签名人的 pubkey 归类:[sig1(input 0), sig2(input 1), ...]
const SIGS = {
'02810260c48b2b4e78dd70ae59b7a8a8858cfdba204ea28896ff7a4de20e0c2514': [
'3044022041fa6e561e5906e03148b540d47f9bcf77d9b3505fcc15b0b0c51843cb359052022069ffd710386f5f2c968ce5e39715b8a0f3a9667f7486ab6cec08a2afd2bc874901',
'3045022100ee8dea3f7a8c1333c4a0e8a2e59206cb495bd7ecbbfec7122dea32cb90a7f9210220117a280d11d45c71b90047b5a430e2ec0ab18fe24064c911ac61926fee0eb53601'
],
'032f7e05649fbf0278076ab1e936998eddc81417131ed55b07d42fa2aea43c25f4': [
'3044022062d213c22e36d9929de57deb1e5b6649757d28779a63f55d635b0124a9f45e7702205a6c8818a9db1580eadfb39286a399e2508a4fb807963aba8597983183b59dcc01',
'3045022100f1fe8473f8125313e01615ef0db9b6b76dc5328ad2ace90905ce77948a2dbc3f02203a5fd49dfe3a0317cd73cace688e62de001b496894863560a580f1693db166d201'
]
};
// 与 Input Output JSON 相同的 inputs 和 outputs,顺序也相同
const INPUTS = [
{ txHash: '757ef70941ea9170cc918097630a7aa7e4c644e3f8d73a15ff1e98d321bf5b62', index: 1 },
{ txHash: '637dfeda367f051d34228badfef09726f0893653fe8baebbe6e150ea68e756f3', index: 2 }
];
const OUTPUTS = [
{ address: 'bc1q2dzu8fcrr66yq9n8q5u6qhwleslqystahdw923', amount: 10000 },
{ address: 'bc1q8dl3vmrqmcs4xpyn884upnedlj2t6uewnfdnp034wqfjghpm5ghqupl9v9', amount: 458144 }
];
const redeem = bitcoin.payments.p2ms({ m: M, pubkeys: PUBKEYS.map(k => Buffer.from(k, 'hex')) });
const p2wsh = bitcoin.payments.p2wsh({ redeem });
console.log('MultiSig address:', p2wsh.address); // 必须是你的 bc1 多签地址
const tx = new bitcoin.Transaction();
tx.version = 2; // 工具签的是 version 2 的交易(bitcoinjs-lib 默认是 1)
tx.locktime = 0;
INPUTS.forEach(i => tx.addInput(Buffer.from(i.txHash, 'hex').reverse(), i.index, 0xffffffff));
OUTPUTS.forEach(o => tx.addOutput(bitcoin.address.toOutputScript(o.address, bitcoin.networks.bitcoin), o.amount));
// 按排序后的 pubkey 顺序取前 M 个签名人
const signers = PUBKEYS.filter(k => SIGS[k]).slice(0, M);
if (signers.length < M) throw new Error('Not enough signatures');
// 每个 input 的 witness:空元素、按 pubkey 顺序的 M 个签名、脚本
INPUTS.forEach((_, n) => {
tx.setWitness(n, [Buffer.alloc(0), ...signers.map(k => Buffer.from(SIGS[k][n], 'hex')), redeem.output]);
});
console.log(tx.toHex()); // finalHex
3 开头(P2SH)的多签:用 bitcoin.payments.p2sh({ redeem }) 计算地址(必须和你的 3 开头地址一致),并把签名放进 input script,而不是 witness:
INPUTS.forEach((_, n) => {
tx.setInputScript(n, bitcoin.script.compile([
bitcoin.opcodes.OP_0,
...signers.map(k => Buffer.from(SIGS[k][n], 'hex')),
redeem.output
]));
});
工具给出的 BTC 签名末尾已经带有 sighash 类型字节(01)。LTC、DOGE、BCH、DGB 由工具用 bitcore 系列库签名(bitcore-lib-ltc 及其他币种的对应库),签名是不带 sighash 类型字节的 DER。请用同一个库组装这些交易:按 JSON 的顺序用 tx.from({ txId, outputIndex, satoshis, script }, pubkeys, m) 加入每个 input、用 tx.to(address, amount) 加入每个 output,再用 tx.applySignature(...) 加入 m 个签名(sighash 类型 SIGHASH_ALL;BCH 为 SIGHASH_ALL | SIGHASH_FORKID)。
f. 第六步,广播 finalHex。BTC 示例:
curl -X POST https://mempool.space/api/tx \
-H "Content-Type: text/plain" \
-d '<finalHex>'
LTC、DOGE、BCH、DGB 请使用该币种的节点或区块浏览器的广播页面。
2. EVM 多签(原生币、ERC-20 和 NFT)
Ownbit 的 EVM 多签是一个智能合约。有人带着足够多的 owner 签名调用它的 spend() 方法时,资产才会转出。在多签恢复工具(币种 ETH)中,每个参与方生成一个签名(r、s、v);然后你编码 spend() 调用,从任意一个有 gas 的地址发送给合约。每个参与方的签名密钥在其多签钱包的 m/44'/60'/0'/0/0。
工具里的 选择网络 提供 Ethereum(chain ID 1)、BNB Smart Chain(56)、Base(8453)、HyperEVM(999)、Polygon(137)、Arbitrum One(42161)和 Optimism(10)。目前可以在 Ethereum、BNB Smart Chain、Base、Arbitrum、HyperEVM 和 Polygon 上新建多签;Optimism 留在列表里供已有的多签使用。请选择多签所在的网络:chain ID 是签名消息的一部分。
合约版本
| 版本 | 合约 | 签名消息中的 chain ID |
|---|---|---|
| 新版(Ownbit 4.67.1 及以上版本创建) | v10 标准、v11 意外防护、v21 企业 | 所有网络都包含,包括以太坊主网 |
| 旧版 | v6 标准、v8 意外防护、v20 企业 | 只在以太坊主网以外的网络包含 |
两代合约的签名不能混用。旧式合约 v4 / v5(Ownbit 4.37.1 及更早版本)不受工具支持,见第 7 节。
判断多签属于哪一代:
getWalletConnectVersion() 方法(selector 0xb7869bac)。调用返回值的是新版合约;调用失败(revert)或没有返回值的是旧版合约。
curl -X POST <该网络的 RPC 地址> \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"<多签地址>","data":"0xb7869bac"},"latest"]}'
a. 第一步,获取 Spend Nonce。它是合约用有效签名执行过的 spend() 调用次数:失败回滚(revert)的 spend() 不计入;签名有效但内部转账失败的 spend() 也计入。它不是区块浏览器上「从」多签地址发出的交易数。用合约的 getSpendNonce() 读取(selector 0xc6a2a9f1,v6 / v8 / v10 / v11 / v21 都有;新版合约还有 spendNonce(),0x843b1a09):
curl -X POST <该网络的 RPC 地址> \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"to":"0xD0BF699Fb29810bB58b5F6EDb482227b529Dfc5a","data":"0xc6a2a9f1"},"latest"]}'
{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000016"}
结果是十六进制:0x16 = 22,即 Spend Nonce 为 22。企业多签另有一个用于修改 owner / signer 的 config nonce;花费资产时用 spend nonce。
b. 第二步,签名。A 打开 多签恢复工具 → ETH,按要转出的资产类型填写:
| 字段 | 原生币(ETH、BNB、POL…) | ERC-20 代币 | NFT(ERC-721 / ERC-1155) |
|---|---|---|---|
| 选择网络 | 多签所在的网络 | ||
| MS Address | 多签合约地址 | ||
| Destination Address | 收款地址 | ||
| Erc20 Contract Address (0x0 = ETH) | 0x0(代表该网络的原生币) |
代币合约地址,例如以太坊上的 USDT:0xdac17f958d2ee523a2206206994597c13d831ec7 | contractAddress_tokenId,例如 0xfcad2…009f7b_0x0000…00b9e |
| Decimals or ErcType | 18 |
代币精度(0–18;USDT 为 6) | 1 = ERC-721,2 = ERC-1155 |
| Amount | 例如 1.2 | 例如 2000 | NFT 数量,例如 1 |
| Spend Nonce | 第一步得到的值 | ||
5.2.0 之前的版本中第三个字段名为 "Erc20 Contract Address"。在 Ownbit 5.2.0+ 中,「选择多签 ›」会自动填写网络、MS Address、合约和精度。
点 多签签名。签名格式类似 r:0x…,s:0x…,v:0(v 为 0 或 1)。
c. 第三步,B 在自己的手机上重复第二步。B 填写的内容必须与 A 完全一致,包括 Spend Nonce。这样就有了两组签名(r、s、v)。
企业多签(v20 / v21):只有 Signer 能花费。收集 Signer 的签名,直到他们的权重之和达到门槛。Owner(最多 7 个)只负责管理 owner 和 signer;不是 Signer 的 Owner 无法生成有效的花费签名。v21 合约最多有 9 个 Signer,可在链上用 getSigners()(0x94cf795e)、getSignerWeights()(0x9f27a602)和 getSignRequired()(0x3fd3407e)查询。白名单、限额和安全管理员由 Ownbit 服务器和 App 执行,合约不检查,所以自助恢复时不受这些限制:请仔细核对收款地址。
d. 第四步,构建 spend() 的 calldata。合约方法如下:
function spend(address destination, uint256 value, uint8[] vs, bytes32[] rs, bytes32[] ss, bytes data)
// selector 0x7c99df4a
各参数必须与工具签名时完全一致:
| 资产 | destination | value | data |
|---|---|---|---|
| 原生币 | 收款地址 | 金额(单位 wei) | 0x(空,不是 0x0) |
| ERC-20 | 代币合约 | 0 | transfer(收款地址, 以代币最小单位计的金额),以 0xa9059cbb 开头 |
| ERC-721 | NFT 合约 | 0 | safeTransferFrom(多签地址, 收款地址, tokenId),以 0x42842e0e 开头 |
| ERC-1155 | NFT 合约 | 0 | safeTransferFrom(多签地址, 收款地址, tokenId, 数量, 0x),以 0xf242432a 开头 |
vs、rs、ss 是数组,每个签名人一项;同一个签名人的 r、s、v 要放在相同位置。v 按工具显示的原样填写(0 或 1),合约会自己加 27。
web3.js 示例(在以太坊上发送 2000 USDT):
const Web3 = require('web3'); // web3.js 1.x;4.x 请用:const { Web3 } = require('web3');
const web3 = new Web3();
const spendAbi = {
name: 'spend', type: 'function',
inputs: [
{ name: 'destination', type: 'address' },
{ name: 'value', type: 'uint256' },
{ name: 'vs', type: 'uint8[]' },
{ name: 'rs', type: 'bytes32[]' },
{ name: 'ss', type: 'bytes32[]' },
{ name: 'data', type: 'bytes' }
]
};
// ERC-20:data = transfer(收款地址, 2000 USDT = 2000 * 10^6)
const tokenData = web3.eth.abi.encodeFunctionCall(
{ name: 'transfer', type: 'function',
inputs: [{ name: 'to', type: 'address' }, { name: 'amount', type: 'uint256' }] },
['0xe36b7026178f17a52f85b47280cc069840c8fa84', '2000000000']
);
// = 0xa9059cbb000000000000000000000000e36b7026178f17a52f85b47280cc069840c8fa840000000000000000000000000000000000000000000000000000000077359400
const calldata = web3.eth.abi.encodeFunctionCall(spendAbi, [
'0xdac17f958d2ee523a2206206994597c13d831ec7', // destination:USDT 合约
'0', // value:代币填 0
['1', '1'], // vs(A、B),按工具显示的原样
['0xaca5025bd6d2fcd3308aa3589ea7aa419ef67914275ce7be80558584232554f9',
'0x9028a9e0fee249f874d747c0c082b1b9cdde12505cea47a2e8cc19b567c3fea8'], // rs
['0x3a12af0ee1354b709b5312db6976150fc7ebc74f6f94404005593290c6322e26',
'0x310e7534a5a0a7e71566c14462c10cf2022c966a390d0edd144895c0c032d599'], // ss
tokenData // data
]);
console.log(calldata);
// 发送原生币:destination = 收款地址,value = 金额(wei),data = '0x',例如
// web3.eth.abi.encodeFunctionCall(spendAbi, ['<收款地址>', web3.utils.toWei('1.2', 'ether'), vs, rs, ss, '0x'])
e. 第五步,发送 calldata。从任意一个持有该网络原生币(用于 gas)的地址,向多签合约地址发送一笔交易:金额 0,data(hex data)为 calldata。发送方不必是参与方。先估算 gas(eth_estimateGas);估算失败说明交易会回滚,请检查网络、Spend Nonce、合约版本、签名人,以及每个值是否与签名时一致。
意外防护(v8 / v11)
合约本身执行 416 天不活跃规则(MAX_INACTIVE_TIME)。owner 在最后一次有效花费签名或调用 active()(或多签创建)后的 416 天内算作活跃。活跃 owner 不少于 m 个时,使用正常门槛;少于 m 个时,合约只要求活跃 owner 的人数(至少 1 个)。所以一个参与方连续 416 天不活跃后,其余活跃的 owner 不需要 Ownbit 就能花费。签名方式与标准多签完全相同。
签名花费会让每个签名人重新变为活跃,合约随后会再检查一次门槛。如果这让门槛超过了你提供的签名数,spend() 会回滚并提示 "please call active() before calling spend"。这时,原来不活跃的签名人先从自己的 owner 地址(m/44'/60'/0'/0/0,需要 gas)调用多签合约的 active()(selector 0x02fb0c5e),然后再用满足新门槛的签名数发送 spend()。
不用 App 签名(进阶)
工具签名的消息是 keccak256(多签地址 ‖ destination ‖ value (uint256) ‖ data ‖ spendNonce (uint256) ‖ chainId (uint256)),按 abi.encodePacked 拼接,再加以太坊签名消息前缀("\x19Ethereum Signed Message:\n32")签名。只有以太坊主网上的旧版合约(v6 / v8 / v20)不带末尾的 chainId。在离线电脑上用 ethers.js v6:
const { ethers } = require('ethers'); // v6
const hash = ethers.solidityPackedKeccak256(
['address', 'address', 'uint256', 'bytes', 'uint256', 'uint256'],
[msAddress, destination, value, data, spendNonce, chainId]
// 以太坊主网上的旧版合约:去掉最后一个 'uint256' 和 chainId
);
const wallet = ethers.HDNodeWallet.fromPhrase(mnemonic, passphrase || '', "m/44'/60'/0'/0/0");
const sig = ethers.Signature.from(await wallet.signMessage(ethers.getBytes(hash)));
console.log({ r: sig.r, s: sig.s, v: sig.v - 27 }); // v 为 0 / 1,与工具相同
在电脑上输入助记词会让助记词暴露。只在你信任的离线电脑上操作,并且只在无法使用 App 时这样做。
3. TRON 多签(TRX 和 TRC-20)
TRON 多签是一个设置了多签权限的普通 TRON 账户,没有合约。每个参与方用其多签钱包 以太坊路径 m/44'/60'/0'/0/0 上的密钥签名(参与方的 TRON 地址就是这把密钥对应的 T 地址),而不是 m/44'/195'/…。
| 多签类型 | 签名人所在权限 | 构建交易时 |
|---|---|---|
| 普通、意外防护、权重多签 | owner 权限(Permission_id 0) | 不设置 permissionId(默认为 0) |
| 企业多签 | active 权限(Permission_id 2),只有 Signer | permissionId: 2 |
Permission_id 不对,签名就无法上链。企业多签只需要 Signer 签名,权重之和达到门槛即可。
a. 第一步,用 TronWeb 构建未签名交易,发送方为多签地址。TRC-20 示例:
const { TronWeb } = require('tronweb'); // TronWeb 6;5.x 请用:const TronWeb = require('tronweb');
const tronWeb = new TronWeb({ fullHost: 'https://api.trongrid.io' });
(async () => {
const from = 'T...'; // 多签地址
const to = 'T...'; // 收款地址
const contract = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; // USDT TRC-20
const amount = 2000000000; // 2000 USDT(6 位精度)
const options = { feeLimit: 15000000, callValue: 0 };
// 仅企业多签:options.permissionId = 2;
const res = await tronWeb.transactionBuilder.triggerSmartContract(
contract, 'transfer(address,uint256)', options,
[{ type: 'address', value: to }, { type: 'uint256', value: amount }],
from
);
// 给签名人留出时间:把有效期延长 1 小时(TRON 最多允许 24 小时)
const tx = await tronWeb.transactionBuilder.extendExpiration(res.transaction, 3600);
console.log(JSON.stringify(tx));
})();
TRX 示例:
// tronWeb 同上例
(async () => {
const from = 'T...'; // 多签地址
const to = 'T...';
const amount = 2000000; // 2 TRX,单位 sun
let tx = await tronWeb.transactionBuilder.sendTrx(to, amount, from);
// 企业多签:await tronWeb.transactionBuilder.sendTrx(to, amount, from, { permissionId: 2 });
tx = await tronWeb.transactionBuilder.extendExpiration(tx, 3600);
console.log(JSON.stringify(tx));
})();
直接使用打印出的 JSON:顶层包含 txID、raw_data 和 raw_data_hex 的未签名交易(TRC-20 是 res.transaction,不是整个返回结果)。请在任何人签名之前延长有效期:之后再改会改变 txID,已有签名随之失效。
b. 第二步,A 和 B 分别在自己的手机上打开 多签恢复工具 → TRX,把同一个 JSON 粘贴到 Tron TX JSON,点 多签签名。签名结果是一串十六进制字符。
在 Ownbit 5.2.0+ 中,用「选择多签 ›」选中多签后,输入框会显示该多签需要的 Permission_id,例如 "Tron TX JSON (Permission_id = 2)"。5.2.0 还会检查 txID 是否等于 sha256(raw_data_hex)、交易是否只有一个 contract、Permission_id 是否符合多签类型,并拒绝为企业多签中不是 Signer 的人签名。更早的版本不做这些检查,请自己核对 Permission_id。
c. 第三步,把所有签名放进交易 JSON 的 signature 数组:
{
"txID": "4c214a020101e1775dd04036f2040caee7d78d5c2bbabf7fdc0318ddc2d9899f",
"raw_data": { "contract": [ ... ], "ref_block_bytes": "...", "expiration": ..., ... },
"raw_data_hex": "...",
"visible": false,
"signature": [
"ad7373b902f185f9a33c1c13b248f4bf5be003dff9d907fa4683aa54ad73f2c3186b57b32f03e477d909bb286d38993896e8a186c5c39c7e5d9f900eb3cabc271c",
"cf310bb59e38e7edd00d1e6ab6ac216b3ab80ba6ce2db140456744cce6b7ed610d599330669ca98ddf05e4211d02c54ff0e033cd28f82680ba8d50b9baf03b831b"
]
}
d. 第四步,在过期前广播,例如:
curl -X POST https://api.trongrid.io/wallet/broadcasttransaction \
-H "Content-Type: application/json" \
-d '<已签名的交易 JSON>'
也可以用 TronWeb:await tronWeb.trx.sendRawTransaction(signedTx)。
4. Solana 多签(SOL 和 SPL 代币)
Ownbit 的 Solana 多签程序是 7uPTVnLrNM9bAdCGrfckz1ugJeRs5bEGcEgcwzPuFsAK(源码:github.com/bitbill/ownbit-multisig-contracts)。每个多签有一个多签数据账户(multisig data account);资产存放在由它推导出的多签地址(PDA,findProgramAddress([数据账户], 程序 ID))。花费需要三个指令:
每个参与方用自己多签钱包 m/44'/501'/0'/0' 上的 SOL 密钥签名并支付手续费。每个提案账户会占用约 0.0065 SOL 的租金。
使用 SOL 工具 Ownbit 5.2.0+
a. 准备。在「多签自助恢复」页或多签详情页("data:" 后面的值)找到多签数据账户;「选择多签 ›」会自动填入。每个签名人自己的 SOL 地址需要有少量 SOL 支付手续费;5.2.0 的工具会显示由哪个地址签名并付费。
b. 每次签名前获取最新的 blockhash。它大约 1 分钟后过期,所以要在这段时间内签名并广播;过期就重新获取、重新签名。
curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getLatestBlockhash"}'
c. 打开 多签恢复工具 → SOL(「多签恢复工具 · SOL」),在顶部选择步骤:发起、批准 或 执行。各步骤的字段:
| 字段 | 发起 | 批准 | 执行 |
|---|---|---|---|
| Multisig data account | 填写 | 填写 | 填写 |
| Transaction account | — | 提案账户 | 提案账户 |
| Recipient (wallet address) | 填写 | — | 与发起时相同 |
| Amount (e.g. 1.5) | 填写 | — | — |
| Token mint (empty = SOL) | SOL 留空,代币填 mint | — | 与发起时相同 |
| Token decimals | 填了 mint 时 | — | — |
| 「Token-2022 代币」开关 | Token-2022 代币打开 | — | 与发起时相同 |
| 「为收款地址创建代币账户」开关 | — | — | 填了 mint 时 |
| Recent blockhash | 填写 | 填写 | 填写 |
| Priority fee (microLamports, optional) | 可选 | 可选 | 可选 |
| Rent exemption (lamports) | 默认 6458880 | — | — |
d. 广播每一笔已签名交易(发起和批准的交易也要广播,链上才有提案和批准记录):
curl https://api.mainnet-beta.solana.com -X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"sendTransaction","params":["<base64 交易>",{"encoding":"base64"}]}'
不用 SOL 工具(手动)
5.2.0 之前的版本没有 SOL 工具,但你仍然可以自己调用程序;这需要 Solana 开发经验。
m/44'/501'/0'/0' 的密钥(在你信任的离线电脑上操作)。findProgramAddress([数据账户], 7uPTVnLrNM9bAdCGrfckz1ugJeRs5bEGcEgcwzPuFsAK)。create_transaction,其他 owner 调用 approve 直到达到门槛,然后任何人调用 execute_transaction。5. 意外防护与 Ownbit 协助签名(BTC、TRON、Solana)
在 BTC、TRON 和 Solana 上,意外防护多签会把 Ownbit 官方(钱包 ID "ownbit")加为一个额外的参与方,例如 3-3 多签变成 3-4 多签。这些链上没有链上的不活跃规则(与 EVM 不同,见第 2 节)。
6. XRP 和 Stellar 多签
多签恢复工具不支持 XRP 和 Stellar。这两种多签使用账本自带的签名人列表,所以不需要 Ownbit,用标准工具即可。
| XRP Ledger | Stellar | |
|---|---|---|
| 设置 | SignerListSet:每个签名人权重 1,SignerQuorum = m;主密钥已禁用 | 每个签名人权重 1,主密钥权重 0,各级门槛 = m |
| 签名人密钥 | 每个参与方多签钱包的 m/44'/144'/0'/0/0 |
每个参与方多签钱包的 m/44'/148'/0' |
| 多签账户 | 推导自 m/44'/144'/0'/0/i;它自己的密钥已禁用,不需要 |
推导自 m/44'/148'/i';它自己的密钥权重为 0,不需要 |
XRP:从多签账户构建一笔 Payment(例如用 xrpl.js 的 client.autofill(tx, m),手续费会按 m 个签名人计算),由 m 个签名人分别用 wallet.sign(tx, true) 签名,用 xrpl.multisign([...]) 合并后提交。
Stellar:用 Stellar SDK 或 Stellar Laboratory 从多签账户构建一笔付款,加上 m 个签名人的签名,提交到 Horizon。
在你信任的离线电脑上,按上面的路径从每个参与方的多签钱包助记词推导签名人密钥。
7. 本文不涉及的情况
以上情况请联系 Ownbit 客服。