如何自助恢复多签资产?

Ownbit多签资产的安全性和可用性不依赖于Onwbit服务器。借助于 Ownbit 多签恢复工具(多签恢复工具可以在没有网络的环境下运行,因此不论 Ownbit 服务器是否永久宕机,您均可以自助恢复多签资产),您总是可以相对轻松地、随时随地自助恢复多签资产。本文以 2-3 多签为例,参与方分别为:A、B、C 三人。

1. 恢复 BTC 多签资产

a. 第一步,通过浏览器查询出多签地址上的可用UTXO,每个币种都有相应的浏览器,例如 BTC 可用:https://www.blockchain.com/btc/address/3CkvfWhYd6behUYnrA5rMQJoyr6oLGM6Ed 进行查询。显示为该地址接收,并且未花费的交易即为可用UTXO。分别记录下相应交易Hash、该地址在Output中所处的位置(第一个位置为0,第二个为1,依次类推)、对应金额,即分别为:txHash、index 和 satoshi。

b. 第二步,构建 BTC input output JSON, 示例如下:

        {
            "inputs": [{
                "segwit": "D",
                "txHash": "757ef70941ea9170cc918097630a7aa7e4c644e3f8d73a15ff1e98d321bf5b62",
                "index": 1,
                "satoshis": 13400
            }, {
                "segwit": "D",
                "txHash": "637dfeda367f051d34228badfef09726f0893653fe8baebbe6e150ea68e756f3",
                "index": 2,
                "satoshis": 456400
            }],
            "outputs": [{
                "amount": 10000,
                "address": "bc1q2dzu8fcrr66yq9n8q5u6qhwleslqystahdw923" //receiver address
            }, {
                "address": "bc1q8dl3vmrqmcs4xpyn884upnedlj2t6uewnfdnp034wqfjghpm5ghqupl9v9", //change address
                "amount": 458144
            }]
        }
    

bc1 开头的多签地址需要有 segwit = D 的参数,3 开头的多签地址无需该参数。input/output 的 sum amount 差额即为本笔交易的矿工费。

c. 第三步,准备 Pubkeys。Pubkeys 是所有多签参与方的 pubkey 的数组。可以在多签详情里查看每个参与方的 pubkey,如图:


将所有参与方 pubkey 通过 : 分隔相连即得到 Pubkeys,示例如下:

02810260c48b2b4e78dd70ae59b7a8a8858cfdba204ea28896ff7a4de20e0c2514:032f7e05649fbf0278076ab1e936998eddc81417131ed55b07d42fa2aea43c25f4
    

pubkey 也可以通过助记词推导而得,逻辑如下:

        助记词(mnemonic)
            ↓
        Seed(BIP39)
            ↓
        Master Key(BIP32)
            ↓
        BIP44 路径:m/44'/0'/0'/0
            ↓
        扩展公钥(xpub)
            ↓
        Index 0 的子地址公钥(m/44'/0'/0'/0/0 的 pubkey)
    

d. 第四步,分别在 A、B 用户的手机上打开:恢复多签工具(位于:首页菜单 - 区块链工具 - 多签恢复工具),多签恢复工具可以在没有网络的环境下运行,因此不论 Ownbit 服务器是否永久宕机,您均可以自助恢复多签资产。如图:

选择币种:BTC,进入 BTC 多签工具页面,如下图:

参数详解如下:

参数名称 参数内容详解
Network 选择网络,BTC bc1 地址还是 3 地址,LTC、DOGE 等其他网络
Input Output JSON 第一步中生成的 Input/Output JSON
Pubkeys 第三步中生成的 Pubkeys,以 : 分隔
Min Singers 最少签名人数,例如 2-3 多签,最少签名人数为:2

BTC 多签的参数的说明。

e. 第五步,填入相应内容,并点击按钮签名,获得签名结果。


提示:根据 input 的个数会生成相应的签名个数,例如 2 个input会生成 2 个签名结果。

f. 第六步,这时我们可以组装交易生成 finalHex。

        const bitcoin = require('bitcoinjs-lib');
        const ecc = require('tiny-secp256k1');
        bitcoin.initEccLib(ecc);

        // 签名(已从用户A+B获得)
        const sigs = {
          input0: [
            '3044022062d213c22e36d9929de57deb1e5b6649757d28779a63f55d635b0124a9f45e7702205a6c8818a9db1580eadfb39286a399e2508a4fb807963aba8597983183b59dcc01',
            '3044022041fa6e561e5906e03148b540d47f9bcf77d9b3505fcc15b0b0c51843cb359052022069ffd710386f5f2c968ce5e39715b8a0f3a9667f7486ab6cec08a2afd2bc874901'
          ],
          input1: [
            '3045022100f1fe8473f8125313e01615ef0db9b6b76dc5328ad2ace90905ce77948a2dbc3f02203a5fd49dfe3a0317cd73cace688e62de001b496894863560a580f1693db166d201',
            '3045022100ee8dea3f7a8c1333c4a0e8a2e59206cb495bd7ecbbfec7122dea32cb90a7f9210220117a280d11d45c71b90047b5a430e2ec0ab18fe24064c911ac61926fee0eb53601'
          ]
        };

        // 构建 2-of-2 多签脚本
        const pubkey1 = Buffer.from('02810260c48b2b4e78dd70ae59b7a8a8858cfdba204ea28896ff7a4de20e0c2514', 'hex');
        const pubkey2 = Buffer.from('032f7e05649fbf0278076ab1e936998eddc81417131ed55b07d42fa2aea43c25f4', 'hex');
        const multisig = bitcoin.payments.p2ms({ m: 2, pubkeys: [pubkey1, pubkey2] });
        const p2wsh = bitcoin.payments.p2wsh({ redeem: multisig });

        const tx = new bitcoin.Transaction();

        // 添加 input 0
        tx.addInput(
          Buffer.from('757ef70941ea9170cc918097630a7aa7e4c644e3f8d73a15ff1e98d321bf5b62', 'hex').reverse(),
          1,
          0xffffffff
        );

        // 添加 input 1
        tx.addInput(
          Buffer.from('637dfeda367f051d34228badfef09726f0893653fe8baebbe6e150ea68e756f3', 'hex').reverse(),
          2,
          0xffffffff
        );

        // 添加 output 0
        tx.addOutput(
          bitcoin.address.toOutputScript('bc1q2dzu8fcrr66yq9n8q5u6qhwleslqystahdw923', bitcoin.networks.bitcoin),
          10000
        );

        // 添加 output 1
        tx.addOutput(
          bitcoin.address.toOutputScript('bc1q8dl3vmrqmcs4xpyn884upnedlj2t6uewnfdnp034wqfjghpm5ghqupl9v9', bitcoin.networks.bitcoin),
          458144
        );

        // 为每个 input 设置 witness
        [sigs.input0, sigs.input1].forEach((sigList, index) => {
          tx.setWitness(index, [
            Buffer.alloc(0),
            Buffer.from(sigList[0], 'hex'),
            Buffer.from(sigList[1], 'hex'),
            p2wsh.redeem.output
          ]);
        });

        // 输出交易 hex
        const finalHex = tx.toHex();
        console.log('Final Transaction Hex:\n', finalHex);
    

g. 第七步,将 finalHex 广播至网络,即可花费多签资产,简单示例如下:

        curl -X POST https://mempool.space/api/tx \
          -H "Content-Type: text/plain" \
          -d ''
    

提示:LTC、DOGE 等其他 UTXO 币种和 BTC 类似,但是组建 input/output JSON 参数略有不同,下面是一个 LTC 多签的 input/output JSON 例子:

        {
            "inputs": [{
                "vout": 1,
                "satoshis": 94960,
                "txid": "b830d41437f2f1cb3c5303c6626cc9762979bf88bd9bcfbb5a356150e86ef0ad"
            }],
            "outputs": [{
                "address": "LeDXT3evSeAQyCwa2b4ohjS9YxcV9dx7AC",
                "amount": 10000
            }, {
                "address": "MHosP5QAgWrqLGrEvYDRQVDxDohSoUsZA4",
                "amount": 79920
            }]
        }
    

LTC 等其他 UTXO 币种使用 txid、vout 参数(而不是 BTC 的 txHash, index 参数)。

2. 恢复 ETH/ERC20 多签资产

a. 第一步,用户A打开“多签恢复工具”,选择币种:ETH,进入 ETH 多签工具页面,如下图:

根据你要恢复的资产类型填写每个空格,详解如下:

参数名称 参数内容详解
Network 根据您的多签选择相应网络:Ethereum、BNB Smart Chain、Base 等等
MS Address 要恢复资产的多签地址
Destination 您要将资产发送到哪个地址
Erc20 Contract Address 发送 ETH 时填写:0x0
Decimals or ErcType 发送 ETH 时填写:18
Amount 填写要发送的金额,如填写:1.2(表示发送 1.2 ETH)
Spend Nonce 填写当前多签地址的 spend nonce,下面有如何获得 Spend Nonce 的详细解释

发送 ETH 的参数的说明。

b. 第二步,获取 Spend Nonce:Spend Nonce 是您的多签地址发送交易的次数(失败的交易也算在内),您有两个方法获得 Spend Nonce:1. 去浏览器中数从您的多签地址中发出的交易。2. 通过调用 RPC 获得,如下:

% curl -X POST https://mainnet.infura.io/v3/your_api_key \
    -H "Content-Type: application/json" \
    -d '{
           "jsonrpc":"2.0",
           "method":"eth_call",
           "params":[
             {
               "to": "0xD0BF699Fb29810bB58b5F6EDb482227b529Dfc5a",
               "data": "0xc6a2a9f1"
             },
             "latest"
           ],
           "id":1
    }'
    {"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000016"}
    

获得的 result 是十六进制的,需转化成十进制,如例子中 0016 转化成:22。

发送 ERC20 时按以下填写各参数:

参数名称 参数内容详解
Network 根据您的多签选择相应网络:Ethereum、BNB Smart Chain、Base 等等
MS Address 要恢复资产的多签地址
Destination 您要将资产发送到哪个地址
Erc20 Contract Address 相应的 ERC20 地址,如发送 USDT,对应于:0xdac17f95...3d831ec7
Decimals or ErcType 每个 ERC20 token 有自己对应的 decimals,例如 USDT 是 6,可以在 etherscan.io 中查看到每个 token 的 decimals
Amount 填写要发送的金额,如填写:1.2(表示发送 1.2 USDT)
Spend Nonce 见前面解释

发送 ERC20 的参数的说明。

发送 NFT(ERC721/ERC1155)时按以下填写各参数:

参数名称 参数内容详解
Network 根据您的多签选择相应网络:Ethereum、BNB Smart Chain、Base 等等
MS Address 要恢复资产的多签地址
Destination 您要将资产发送到哪个地址
Erc20 Contract Address 填写 contractAddress_tokenId(中间用 _ 分割),如:0xfcad2...009f7b_0x0000...00b9e
Decimals or ErcType ERC721 填写 1,ERC1155 填写 2
Amount 填写要发送的金额,如填写:1
Spend Nonce 见前面解释

发送 ERC721/ERC1155 的参数的说明。

c. 第三步,填写完成后点击签名按钮获得签名(r、s、v),如下图:


d. 第四步,在用户B的手机上重复上述过程,得到B的签名结果(r、s、v),注意,用户B填写的内容应该和A填写的一摸一样。这时,您已经获得两组(r、s、v)签名结果,现在可以开始调用多签合约花费资产了。

下面开始调用 spend 方法进行多签花费,相应的方法结构如下:

//destination is the receiver address for sending ether,  or an ERC20 contract address for sending ERC20.
//value is the wei to be trasnferred to the destination, when sending ERC20 value should be 0.
//data for transfer ether: 0x
//data for transfer erc20 example: 0xa9059cbb000000000000000000000000ac6342a7efb995d63cc91db49f6023e95873d25000000000000000000000000000000000000000000000000000000000000003e8
//data for transfer erc721 example: 0x42842e0e00000000000000000000000097b65ad59c8c96f2dd786751e6279a1a6d34a4810000000000000000000000006cb33e7179860d24635c66850f1f6a5d4f8eee6d0000000000000000000000000000000000000000000000000000000000042134
//data can contain any data to be executed.

function spend(address destination, uint256 value, uint8[] memory vs, bytes32[] memory rs, bytes32[] memory ss, bytes calldata data) external {
    require(destination != address(this), "Not allow sending to yourself");
    require(_validSignature(destination, value, vs, rs, ss, data), "invalid signatures");
    spendNonce = spendNonce + 1;
    //transfer tokens from this contract to the destination address
    (bool sent,) = destination.call{value: value}(data);
    if (sent) {
        emit Spent(destination, value);
    }
}
    

e. 第五步,根据已经有的两组(r、s、v)你可以构建出 spend 方法所需的 calldata,下面是示例代码:

        
        const Web3 = require("web3");
        const web3 = new Web3();

        const abi = [{
          "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" }
          ]
        }];

        const contract = new web3.eth.Contract(abi);
        const calldata = contract.methods.spend(
          "0xdac17f958d2ee523a2206206994597c13d831ec7", //destination
          "0", //ether-value
          [1, 1], //vs
          [
            "0xaca5025bd6d2fcd3308aa3589ea7aa419ef67914275ce7be80558584232554f9",
            "0x9028a9e0fee249f874d747c0c082b1b9cdde12505cea47a2e8cc19b567c3fea8" //rs
          ],
          [
            "0x3a12af0ee1354b709b5312db6976150fc7ebc74f6f94404005593290c6322e26",
            "0x310e7534a5a0a7e71566c14462c10cf2022c966a390d0edd144895c0c032d599" //ss
          ],
          "0xa9059cbb000000000000000000000000e36b7026178f17a52f85b47280cc069840c8fa840000000000000000000000000000000000000000000000000000000077359400" //inner-calldata
        ).encodeABI();

        console.log(calldata);

    

发送 ERC20 spend 的 calldata 构建

发送 ETH spend 的 calldata 构建类似,只将 ether-value 改成相应的 value,并将 inner-calldata 改成 0x0 即可。

f. 第六步,生成了 calldata 您可以发送任何 ETH 交易,将 calldata 设为该交易的 hexdata,即可广播交易,使多签花费生效。

注:其它 EVM 网络(如:BSC、BASE)多签恢复方式和ETH一致。

3. 恢复 TRX/TRC20 多签资产

a. 第一步,生成要发送交易的 txJson。下面是代码示例:

        const TronWeb = require('tronweb');

        const tronWeb = new TronWeb({
          fullHost: 'https://api.trongrid.io'
        });

        (async () => {
          const from = 'TXeDpnE...';
          const to = 'TQwbMLM...';
          const contract = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; // USDT TRC20
          const amount = 2000000000; // 2000 USDT (6 decimals)
          const feeLimit = 15000000;

          const functionSelector = 'transfer(address,uint256)';
          const parameter = [
            { type: 'address', value: to },
            { type: 'uint256', value: amount }
          ];

          const options = {
            feeLimit,
            callValue: 0,
            shouldPollResponse: false
          };

          const tx = await tronWeb.transactionBuilder.triggerSmartContract(
            contract,
            functionSelector,
            options,
            parameter,
            from
          );

          const txJson = JSON.stringify(tx.transaction);
          console.log(txJson);
        })();
    

发送 TRC20 交易的 txJson 生成

        const TronWeb = require('tronweb');

        const tronWeb = new TronWeb({
          fullHost: 'https://api.trongrid.io'
        });

        (async () => {
          const from = 'TXeDpnE...';
          const to = 'TQwbMLM...';
          const amount = 2_000_000; // 2 TRX

          const tx = await tronWeb.transactionBuilder.sendTrx(to, amount, from);

          const txJson = JSON.stringify(tx);
          console.log(txJson);
        })();

    

发送 TRX 交易的 txJson 生成

提示:默认生成的 txJson 的 expiration 时间较短,可适当扩展 expiration 时间,以防止广播交易时已经失效。

b. 第二步,分别在 A、B 用户的手机上打开:恢复多签工具 - TRX,输入 txJson,并点击按钮签名,得到各自的签名结果。如图:


3. 第三步,现在将得到的签名加入到 txJson 的 signature 字段中,即生成完整的 txJson,可用于广播至网络。示例如下:

        {
          "txID": "4c214a020101e1775dd04036f2040caee7d78d5c2bbabf7fdc0318ddc2d9899f",
          "raw_data": {
            "contract": [...],
            "ref_block_bytes": "...",
            ...
          },
          "raw_data_hex": "...",
          "visible": false,
          "signature": [
            "ad7373b902f185f9a33c1c13b248f4bf5be003dff9d907fa4683aa54ad73f2c3186b57b32f03e477d909bb286d38993896e8a186c5c39c7e5d9f900eb3cabc271c",
            "cf310bb59e38e7edd00d1e6ab6ac216b3ab80ba6ce2db140456744cce6b7ed610d599330669ca98ddf05e4211d02c54ff0e033cd28f82680ba8d50b9baf03b831b"
          ]
        }
    

4. 第四步,将完整的 txJson 的广播至 TRON 网络,多签花费即可生效。

4. 恢复 SOL/SPL 多签资产

a. 通过多签助记词生成各参与方的 SOL 地址和签名所需私钥(生成路径:m/44'/501'/0'/0')。

b. 在 多签钱包 -> 多签页面 -> 菜单“多签详情”中获得 /multisig data/ 值(这是在调用合约过程中要用到的 multisig data account。

c. 当你同时拥有多签地址(multisigPda)、multisig data account,以及各参与方的 SOL 地址和签名私钥,就可以根据 Ownbit 多签源码进行调用以花费多签资产,多签源代码地址:https://github.com/bitbill/ownbit-multisig-contracts。

d. 一个完整的调用流程:先由任意参与方发送调用 create_transaction 指令,然后再由其他参与方调用 approve 指令。当达到足够人数时,再由任意一方发送调用 execute_transaction 指令。