LoginSignup
1
1

More than 1 year has passed since last update.

bitcoinjs-libでNative Segwitのマルチシグアドレス(P2WSH)を作って送受金してみる

Last updated at Posted at 2021-12-07

今回の記事ではJavascriptのビットコインライブラリであるbitcoinjs-libを使ってP2WSHの2-of-2マルチシグアドレスを作り、その後これに署名して送金してみます。検証のためRegtestモードでbitcoin-cliを使う場面があります。

事故っても私には責任が取れないので、mainnetで下記のコードを動かさないで下さい。また下記の鍵を再利用しないでください。
あと、もし間違っているところがあれば教えてください。

ビットコインのマルチシグアドレスの形式

ビットコインでマルチシグを実現できるアドレス形式として古いものからP2MS、P2SH、P2WSHがあります。

  • P2MS (raw multisig): 一番古い方法で、3-of-3が限界。入金時点でOP_CHECKMULTISIGを使うスクリプトを公開し、必要な署名を集めることで出金できる。アドレスは3から始まる
  • P2SH: チェーンに公開するデータ量を削減したことで鍵を15個まで扱える。入金時にはredeem scriptのハッシュ、出金時はredeem scriptを公開し必要な署名を集める。アドレスは3から始まる
  • P2WSH: P2SHのSegwit版。redeem scriptがwitness scriptとなった。アドレスはbc1qから始まる

今日はbitcoinjs-libを使ってP2WSHで2-of-2のマルチシグアドレスを作ることにします。
(厳密にはP2WSHでラッピングされたP2MS)
2-of-2マルチシグはペイメントチャネルのファンディングトランザクションとして利用されますね。

マルチシグアドレスとスクリプトを作る

最初に環境を整えましょう。ECPairの都合上、Nodeはv12, v14, v16のいずれかを使用するようにしてください。
Regtestモードで稼働しているビットコインノードも必要です。

npm init
npm install bitcoinjs-lib ecpair

では本体を準備します。

create.js
const { ECPair } = require('ecpair')
const bitcoin = require('bitcoinjs-lib')
const { regtest } = require('bitcoinjs-lib/src/networks')

const main = () => {
    // 秘密鍵・公開鍵を2組生成します
    const keyPairOne = ECPair.makeRandom()
    const keyPairTwo = ECPair.makeRandom()
    console.log(`Pubkey 1: ${keyPairOne.publicKey.toString('hex')}`)
    console.log(`Privkey 1: ${keyPairOne.privateKey.toString('hex')}`)
    console.log(`Pubkey 2: ${keyPairTwo.publicKey.toString('hex')}`)
    console.log(`Privkey 2: ${keyPairTwo.privateKey.toString('hex')}`)

    // スクリプト、アドレス等を生成します。具体的には、P2MSをP2WSHでラッピングしています。
    const pubkeys = [ keyPairOne.publicKey, keyPairTwo.publicKey ]
    const payment = bitcoin.payments.p2wsh({
        redeem: bitcoin.payments.p2ms({ 
            m: 2, 
            pubkeys, 
            network: regtest
        }),
        network: regtest
    })
    console.log(`Witness script: ${payment.redeem.output.toString('hex')}`)
    console.log(`Address: ${payment.address}`)
    return
}

main()

実行すると、下記のような結果になります。

$ node create.js 
Pubkey 1: 03a71888931b460d0f290916f33095935b4586bf9adc26c5f3bd7b00f9015583dc
Privkey 1: 85782255ac5d2594548aaafbecf82aaa76bfd2ec696976b27ecca0e1332b8c12
Pubkey 2: 0235cc5a6225db280110ec1050abc801078b059fdcd1a881fd4a29fd1f3508d40b
Privkey 2: dfb82ec3db364006b6949619e30341b0f5e8cbfd556425400f71330e221955b5
Witness script: 522103a71888931b460d0f290916f33095935b4586bf9adc26c5f3bd7b00f9015583dc210235cc5a6225db280110ec1050abc801078b059fdcd1a881fd4a29fd1f3508d40b52ae
Address: bcrt1qwxmxc2gnasr8kyr3juutmksnm35d8denw4449dx9ta8k5e8jf0ssfquqtz

スクリプトの中身を確認する

次はWitness scriptにどのような情報が含まれているのか見ていきましょう。
bitcoin-cli decodescriptでスクリプトの内容がデコードできます:

$ ./bitcoin-cli decodescript 522102817bc553767364e7573af37cb1e1c1b212a4cc71aea9dcbe89ce1976fa3a2bb02103e7cef958a1fe0202895136527620ee73714dc213fade56710cc0872c846c50f952ae
{
  "asm": "2 03a71888931b460d0f290916f33095935b4586bf9adc26c5f3bd7b00f9015583dc 0235cc5a6225db280110ec1050abc801078b059fdcd1a881fd4a29fd1f3508d40b 2 OP_CHECKMULTISIG",
  "type": "multisig",
  "p2sh": "2MwGdzYzLRASZB22NV3SxL5XY7cvsj1KCNr",
  "segwit": {
    "asm": "0 71b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be1",
    "hex": "002071b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be1",
    "address": "bcrt1qwxmxc2gnasr8kyr3juutmksnm35d8denw4449dx9ta8k5e8jf0ssfquqtz",
    "type": "witness_v0_scripthash",
    "p2sh-segwit": "2NDY2jtAfGhNzJMRd34kRWPMtgUBcJoZgsu"
  }
}

"asm"からはUnlock Scriptの具体的な内容が確認できます。今回は2、公開鍵A、公開鍵B、2の順でスタックに追加され、OP_CHECKMULTISIGが与えられた公開鍵を使ってトランザクションが2-of-2の条件を満たすかチェックします。
このマルチシグアドレスからの送金時にはこのスクリプトを公開する必要があります。

今回P2WSHアドレスでマルチシグするのに大事なのは"segwit"以下の部分です。(下に抜粋)

"segwit": {
    "asm": "0 71b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be1",
    "hex": "002071b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be1",
    "address": "bcrt1qwxmxc2gnasr8kyr3juutmksnm35d8denw4449dx9ta8k5e8jf0ssfquqtz",
    "type": "witness_v0_scripthash",
    "p2sh-segwit": "2NDY2jtAfGhNzJMRd34kRWPMtgUBcJoZgsu"
  }

"asm"はScript Pubkeyと呼ばれるもので、"hex"はその16進数表記です。これらの0, 0020の後の部分はUnlock Scriptの内容をSHA256した32バイトの値です。マルチシグアドレスへの送金時に利用するので、Locking Scriptともいいます。
"address"はその名の通り、network (mainnetなら"bc"prefixですがregtestなので"bcrt")、segwit version (0)、そしてscript hash ("segwit"."asm"の後半32バイト)から生成されたP2WSHアドレスです。

とりあえずこのアドレスに100 Regtest BTC送金してみました。

$ bitcoin-cli sendtoaddress bcrt1qwxmxc2gnasr8kyr3juutmksnm35d8denw4449dx9ta8k5e8jf0ssfquqtz 100 -fallbackfee=0.0002
3f7ab8c65601833891520415e8b8c7688fe9f4cbe4124ba831db5385782dafc7
$ bitcoin-cli getreceivedbyaddress bcrt1qwxmxc2gnasr8kyr3juutmksnm35d8denw4449dx9ta8k5e8jf0ssfquqtz
100.00000000

P2WSHマルチシグアドレスから送金する

Unlock Scriptを使ってマルチシグアドレスから送金する手順を見ていきましょう。先程の送金時のTXIDが必要です。

sign.js
const { ECPair } = require('ecpair')
const bitcoin = require('bitcoinjs-lib')
const { regtest } = require('bitcoinjs-lib/src/networks')

const sign = () => {
    // ビットコイントランザクションを組み立てる
    const psbt = new bitcoin.Psbt({ network: regtest })
        // 入力値として、マルチシグアドレスに先程入金したUTXOを使う
        .addInput({
            // TXID
            hash: "3f7ab8c65601833891520415e8b8c7688fe9f4cbe4124ba831db5385782dafc7",
            // トランザクション内の出力値のインデックス
            index: 0,
            // Segwit UTXOなので、Script Pubkey (Locking script)および金額
            witnessUtxo: {
                script: Buffer.from("002071b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be1", "hex"),
                value: 100e8 // 100*10^8 sats = 100 BTC
            },
            // Unlock script (Witness script)
            witnessScript: Buffer.from("522103a71888931b460d0f290916f33095935b4586bf9adc26c5f3bd7b00f9015583dc210235cc5a6225db280110ec1050abc801078b059fdcd1a881fd4a29fd1f3508d40b52ae", "hex")
        })
        // 出力値として、自分のアドレスに1 BTC送金する
        .addOutput({
            address: "bcrt1q4mzl7xjx9q99z2hhn8fwexj5zkj8msr7pxs4jp",
            value: 1e8 // 1*10^8 sats = 1 BTC
        })
        // お釣りとして98.999 BTCをマルチシグアドレスに戻す。残りの0.0001 BTCが手数料になる
        .addOutput({
            address: "bcrt1qwxmxc2gnasr8kyr3juutmksnm35d8denw4449dx9ta8k5e8jf0ssfquqtz",
            value: 98.999e8 // 98.999*10^8 sats = 98.999 BTC
        })
        // 控えておいた2つの秘密鍵をそれぞれ使って0番目の入力値に署名する
        .signInput(0, ECPair.fromPrivateKey(Buffer.from("85782255ac5d2594548aaafbecf82aaa76bfd2ec696976b27ecca0e1332b8c12", "hex")))
        .signInput(0, ECPair.fromPrivateKey(Buffer.from("dfb82ec3db364006b6949619e30341b0f5e8cbfd556425400f71330e221955b5", "hex")))
        // 仕上げのFinalize処理
        .finalizeAllInputs()

    const tx = psbt.extractTransaction().toHex()
    console.log(tx)
    return
}

sign()

node sign.jsを実行すると、署名済みのトランザクションが16進数の生データとして出力されます。これをbitcoin-cli decoderawtransactionすると

$ bitcoin-cli decoderawtransaction 02000000000101c7af2d788553db31a84b12e4cbf4e98f68c7b8e81504529138830156c6b87a3f0000000000ffffffff0200e1f50500000000160014aec5ff1a46280a512af799d2ec9a5415a47dc07e607c144e0200000022002071b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be10400483045022100a4a174d9ec69a2479e4d0ea37263b3e82e415227931d98d548c58e79af525b3f02201deb21554b062d4bed76877f1db2f8d41c650a14ee41bf506687bfa3cc178cfb01473044022059062f9432eaceb482ae8f9e93b251531838707b1d54dbd73e060665afe74ed702200ead0bd17d9e0c1afd671a71bde23f06f240957d3bd0f08244974f6a8b51d63b0147522103a71888931b460d0f290916f33095935b4586bf9adc26c5f3bd7b00f9015583dc210235cc5a6225db280110ec1050abc801078b059fdcd1a881fd4a29fd1f3508d40b52ae00000000
{
  "txid": "b23f196cf3a7f0acf57213b94a73a5a7e8df147e633b466e32208a766db0a23b",
  "hash": "2c3fa993a8002b23c7e5e9a2ec25de80a76f6e0e1f9539e13f9881075aef391b",
  "version": 2,
  "size": 346,
  "vsize": 181,
  "weight": 721,
  "locktime": 0,
  "vin": [
    {
      "txid": "3f7ab8c65601833891520415e8b8c7688fe9f4cbe4124ba831db5385782dafc7",
      "vout": 0,
      "scriptSig": {
        "asm": "",
        "hex": ""
      },
      "txinwitness": [
        "",
        "3045022100a4a174d9ec69a2479e4d0ea37263b3e82e415227931d98d548c58e79af525b3f02201deb21554b062d4bed76877f1db2f8d41c650a14ee41bf506687bfa3cc178cfb01",
        "3044022059062f9432eaceb482ae8f9e93b251531838707b1d54dbd73e060665afe74ed702200ead0bd17d9e0c1afd671a71bde23f06f240957d3bd0f08244974f6a8b51d63b01",
        "522103a71888931b460d0f290916f33095935b4586bf9adc26c5f3bd7b00f9015583dc210235cc5a6225db280110ec1050abc801078b059fdcd1a881fd4a29fd1f3508d40b52ae"
      ],
      "sequence": 4294967295
    }
  ],
  "vout": [
    {
      "value": 1.00000000,
      "n": 0,
      "scriptPubKey": {
        "asm": "0 aec5ff1a46280a512af799d2ec9a5415a47dc07e",
        "hex": "0014aec5ff1a46280a512af799d2ec9a5415a47dc07e",
        "address": "bcrt1q4mzl7xjx9q99z2hhn8fwexj5zkj8msr7pxs4jp",
        "type": "witness_v0_keyhash"
      }
    },
    {
      "value": 98.99900000,
      "n": 1,
      "scriptPubKey": {
        "asm": "0 71b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be1",
        "hex": "002071b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be1",
        "address": "bcrt1qwxmxc2gnasr8kyr3juutmksnm35d8denw4449dx9ta8k5e8jf0ssfquqtz",
        "type": "witness_v0_scripthash"
      }
    }
  ]
}

入力値に2つの署名やWitness Scriptのデータが見えますし、トランザクションのin/outのデータがはっきり見えますね。
あとはこの署名済みのビットコイントランザクションを配信するだけです。(Regtestなので配信と呼んでいいのかわかりませんが)

$ bitcoin-cli sendrawtransaction 02000000000101c7af2d788553db31a84b12e4cbf4e98f68c7b8e81504529138830156c6b87a3f0000000000ffffffff0200e1f50500000000160014aec5ff1a46280a512af799d2ec9a5415a47dc07e607c144e0200000022002071b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be10400483045022100a4a174d9ec69a2479e4d0ea37263b3e82e415227931d98d548c58e79af525b3f02201deb21554b062d4bed76877f1db2f8d41c650a14ee41bf506687bfa3cc178cfb01473044022059062f9432eaceb482ae8f9e93b251531838707b1d54dbd73e060665afe74ed702200ead0bd17d9e0c1afd671a71bde23f06f240957d3bd0f08244974f6a8b51d63b0147522103a71888931b460d0f290916f33095935b4586bf9adc26c5f3bd7b00f9015583dc210235cc5a6225db280110ec1050abc801078b059fdcd1a881fd4a29fd1f3508d40b52ae00000000
b23f196cf3a7f0acf57213b94a73a5a7e8df147e633b466e32208a766db0a23b
(~Regtestを1ブロック進めた後~)
$ bitcoin-cli gettransaction b23f196cf3a7f0acf57213b94a73a5a7e8df147e633b466e32208a766db0a23b
{
  "amount": 1.00000000,
  "confirmations": 1,
  "blockhash": "37e2d774778a3a2a31da2dd19474d2dc185a2d7ebde09b1ddf1c7bc6d12e8a81",
  "blockheight": 308,
  "blockindex": 1,
  "blocktime": 1638810114,
  "txid": "b23f196cf3a7f0acf57213b94a73a5a7e8df147e633b466e32208a766db0a23b",
  "walletconflicts": [
  ],
  "time": 1638809915,
  "timereceived": 1638809915,
  "bip125-replaceable": "no",
  "details": [
    {
      "involvesWatchonly": true,
      "address": "bcrt1q4mzl7xjx9q99z2hhn8fwexj5zkj8msr7pxs4jp",
      "category": "receive",
      "amount": 1.00000000,
      "label": "",
      "vout": 0
    }
  ],
  "hex": "02000000000101c7af2d788553db31a84b12e4cbf4e98f68c7b8e81504529138830156c6b87a3f0000000000ffffffff0200e1f50500000000160014aec5ff1a46280a512af799d2ec9a5415a47dc07e607c144e0200000022002071b66c2913ec067b10719738bdda13dc68d3b733756b52b4c55f4f6a64f24be10400483045022100a4a174d9ec69a2479e4d0ea37263b3e82e415227931d98d548c58e79af525b3f02201deb21554b062d4bed76877f1db2f8d41c650a14ee41bf506687bfa3cc178cfb01473044022059062f9432eaceb482ae8f9e93b251531838707b1d54dbd73e060665afe74ed702200ead0bd17d9e0c1afd671a71bde23f06f240957d3bd0f08244974f6a8b51d63b0147522103a71888931b460d0f290916f33095935b4586bf9adc26c5f3bd7b00f9015583dc210235cc5a6225db280110ec1050abc801078b059fdcd1a881fd4a29fd1f3508d40b52ae00000000"
}

ちゃんと送金が成功したことが確認できました。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1