如何自助恢復多簽資產?

Ownbit 多簽資產的安全性與可用性並不依賴於 Ownbit 伺服器。透過 Ownbit 多簽恢復工具(該工具可在無網路環境下運行,因此即使 Ownbit 伺服器永久宕機,您仍可自助恢復多簽資產),您始終可以相對輕鬆地、隨時隨地完成多簽資產的自助恢復。本文以 2-3 多簽為例,參與方為 A、B、C 三人。

1. 恢復 BTC 多簽資產

a. 第一步,透過區塊瀏覽器查詢該多簽地址上的可用 UTXO。每種幣別都有對應的瀏覽器,例如 BTC 可使用:https://www.blockchain.com/btc/address/3CkvfWhYd6behUYnrA5rMQJoyr6oLGM6Ed 進行查詢。顯示該地址已接收且尚未花費的交易即為可用 UTXO。請分別記錄該交易的 Hash、該地址在輸出欄位中的位置(第一個位置為 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 的總金額差值即為本筆交易的礦工費。

c. 第三步,準備 Pubkeys。Pubkeys 是所有多簽參與方的公鑰所組成的陣列。您可以在多簽錢包的詳情頁中查看每位參與者的 pubkey,如下圖所示:


將所有參與者的 pubkey 以冒號(:)分隔並串接起來,即為 Pubkeys,示例如下:

02810260c48b2b4e78dd70ae59b7a8a8858cfdba204ea28896ff7a4de20e0c2514:032f7e05649fbf0278076ab1e936998eddc81417131ed55b07d42fa2aea43c25f4
    

pubkey 也可以透過助記詞推導而得,邏輯如下:

        助記詞(mnemonic)
            ↓
        Seed(BIP39)
            ↓
        主密鑰(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 '<hex>'
    

提示:LTC、DOGE 等其他 UTXO 幣種與 BTC 類似,但組建 input/output JSON 的參數略有不同,以下是 LTC 多簽的 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 代幣都有自己的 decimals,例如 USDT 是 6,可在 etherscan.io 查詢
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,並點擊按鈕簽名,獲得各自的簽名結果。如下圖所示:


c. 第三步,將獲得的簽名加入至 txJson 的 signature 欄位中,即可生成完整的 txJson,用於廣播至 TRON 網路。如下示例所示:

        {
          "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 指令。