P2SH Multisig script を unlock する

33685 ワード

P2SH の Multisig script に lock された bitcoin を unlock する transaction を作成します。

必要なもの

Bitcoin Core v0.27.0
btcdeb v0.4.22(built on commit 3ba1ec7f4d37f7d2ff0544403465004c6e12036e)
hal v0.7.2

事前準備

  • Alice と Bob の署名が必要な Multisig script に lock するので、それぞれの鍵を生成
  • Multisig に lock する transaction を作るためのアカウント Origin を作成

Bitcoin Core を regtest で走らせる

bitcoind --regtest

以後 bitcoin-cli の --regtest 引数は省略している。

Alice と Bob の鍵

hal で生成します。

Genrate key for Alice
hal key generate --regtest
{
  "raw_private_key": "bdfc1eab7cbc719286a30e48bee298742022707ae065fb8e4098b22408bb57ae",
  "wif_private_key": "cTx1NyYTDatDM4x21LCwHCYzak6h4B9pSW7ugVFL182F1jGJUoQF",
  "public_key": "02a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def747474",
  "uncompressed_public_key": "04a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def74747471c142cd419d0eb4880e8e92e545a4aaae81bd03e0c79942ec6369ba7d9f9990",
  "addresses": {
    "p2pkh": "mjvA7TJgHaNedzaXuKNJtcPaCeraXJmuca",
    "p2wpkh": "bcrt1qxpp77nhqpt38pnn54t6hqgzudz5apaww7r4nqw",
    "p2shwpkh": "2NALukca3k9RjHw52imsRECmnTE7SafhHux"
  }
}
Generate key for Bob
hal key generate --regtest
{
  "raw_private_key": "66683d5ab7d799661fbafe1083d18dc35fe8242a8e09ac425e3b8cbe7512cb81",
  "wif_private_key": "cR1mVEBvgAg3MaMsZEVPFj9iGAtfFPyMkW2qajAqStgBDSWauHdR",
  "public_key": "02b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce1",
  "uncompressed_public_key": "04b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce1a9dbe2705f452dd4e2581e255deca249d1728b813de2c7b158e299ef97b09932",
  "addresses": {
    "p2pkh": "mwBEDz6iJApuxBn9ZhzXXdMTXroLch5Vrw",
    "p2wpkh": "bcrt1q40r0m2wm60uqhzgulfe8l2ghpyvqyuh0ug46uc",
    "p2shwpkh": "2NEqGmJTXZu2443dV9hGKLyviF27ZvnCHwK"
  }
}

アカウント Origin を設定

Origin の鍵を生成

hal bip39 generate --regtest
{
  "mnemonic": "october slab middle deliver sniff aisle ginger double story topple panther earth draft napkin cause fence rug vibrant flight napkin ancient invest office cave",
  "entropy": "99195e311d0cd60b58820dd69ca67f22b41f25c922a8bd1e6d63c97088ebe661",
  "entropy_bits": 256,
  "language": "english",
  "passphrase": "",
  "seed": {
    "seed": "6627863a84060d070baa586791e14df3199bef971b61cc47709233b0fdbf06441d511d693decf80148402280966aa1ee0d2ca979e05eae35b014f664330a1f7b",
    "bip32_xpriv": "tprv8ZgxMBicQKsPegAwNF8vH934pv1utqEM5jo9KHw7c5N2mmA9Vo6vLZPABjvmRfAkGxdHr34S7wUnataE4NT5G9LxFvpLqkvgFb8zLqaw2BA",
    "bip32_xpub": "tpubD6NzVbkrYhZ4Y9CjFtoWgYhBPwXr4ARFf3PvboyR2MARcFQv8BvWX412Mr8sXPDjvS1tKXttiLncCJDdQZJ71qFax6hS46AAsZcr4y8WYWp"
  }
}

Bitcoin Core の Wallet に Origin を登録する

createwallet コマンドで wallet を作成する。
descriptor をインポートするので descriptors=true を設定する。

bitcoin-cli createwallet origin false false "" false true

先程生成した bip32_xprivimportdescriptors コマンドで descriptor としてインポートする

bitcoin-cli importdescriptors '[{ "desc": "wpkh(tprv8ZgxMBicQKsPeEouTKKKxFXWfBzkPVnUphbBGZ1E2UHRjMZnG6LmwYDubJZeCLdHwcmx7znafY2Ub6RQUEw48UiEAYNWpooFQq6RU2FBtFX/84h/1h/0h/0/*)#7swry5a5", "timestamp":"now" }, { "desc": "wpkh(tprv8ZgxMBicQKsPeEouTKKKxFXWfBzkPVnUphbBGZ1E2UHRjMZnG6LmwYDubJZeCLdHwcmx7znafY2Ub6RQUEw48UiEAYNWpooFQq6RU2FBtFX/84h/1h/0h/1/*)#0ytzepdv", "timestamp":"now", "internal": true }]'

importdescriptor に与える descriptor は checksum がついていなければならない。
checksum を getdescriptorinfo コマンドで取得する例は以下。

bitcoin-cli getdescriptorinfo "wpkh(tprv8ZgxMBicQKsPeEouTKKKxFXWfBzkPVnUphbBGZ1E2UHRjMZnG6LmwYDubJZeCLdHwcmx7znafY2Ub6RQUEw48UiEAYNWpooFQq6RU2FBtFX/84h/1h/0h/0/*)"

作成した wallet に bitcoin を入れる。

bitcoin-cli getnewaddress
bcrt1q7uytth5dj2hder5knely34n7w99l072a9nxdk2
bitcoin-cli generatetoaddress 101 bcrt1q7uytth5dj2hder5knely34n7w99l072a9nxdk2

origin に 50BTC 入っていることを確認。

bitcoin-cli getbalance
50.00000000

Multisig script に bitcoin を lock する

スクリプトの作成

今回の redeem script は、Alice と Bob 双方の署名が必ず必要な Multisig script にしようと思うので、以下のようなスクリプトになる。
2 <Alice's pubkey> <Bob's pubkey> 2 CHECKMULTISIG

locking script は P2SH の定義上以下。
OP_HASH160 <HASH160 of redeem script> OP_EQUAL

事前準備で生成したように、Alice と Bob の pubkey はそれぞれ以下です。
Alice pubkey: 02a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def747474
Bob pubkey: 02b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce1

pubkey を当てはめた上で16進数にしたスクリプトが以下通り生成します。
(btcc コマンドは btcdeb についてきます)

btcc OP_2 02a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def747474 02b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce1 OP_2 OP_CHECKMULTISIG
522102a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def7474742102b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce152ae

アドレスの生成

スクリプトを元にアドレスを生成するが、hal にやってもらう。
address create に直前で生成したスクリプトを渡す。
今回は P2SH を使う。

hal address create --script 522102a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def7474742102b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce152ae --regtest
{
  "p2sh": "2NFkLUHixD8AYCBdtKNC2ZMvzF7WpMJeugC",
  "p2wsh": "bcrt1qzytpxsvvl8mz9dke3x009znvt2n8nuyct4ryw60eg0hpgnwt5qlstsndnc",
  "p2shwsh": "2N9PJX2ZDD3BzgV6uDXYp2AYw2FzmaEVfiX"
}

スクリプトに lock する

事前準備で作成した Origin wallet からこのアドレスに送金することで、スクリプト 2 <Alice's pubkey> <Bob's pubkey> 2 CHECKMULTISIG に 1BTC を lock する。

bitcoin-cli sendtoaddress 2NFkLUHixD8AYCBdtKNC2ZMvzF7WpMJeugC 1
f2299acdcc9ca69eb8984200441ca88bd45eff830046a12ea9560c5f2ba2fcef

この transaction では2つの output が作成されているはずで、これまでに作成した Multisig script に lock する output を知る必要がある。
gettransaction コマンドで transaction の16進数形式を取得する。

bitcoin-cli gettransaction 56e9d34f8bff598068df6d3e4c712308500f706b49610f2b245d48a1cc5551e4
{
  "amount": -1.00000000,
  "fee": -0.00002840,
  "confirmations": 0,
  "trusted": true,
  "txid": "56e9d34f8bff598068df6d3e4c712308500f706b49610f2b245d48a1cc5551e4",
  "walletconflicts": [
  ],
  "time": 1650184157,
  "timereceived": 1650184157,
  "bip125-replaceable": "no",
  "details": [
    {
      "address": "2NFkLUHixD8AYCBdtKNC2ZMvzF7WpMJeugC",
      "category": "send",
      "amount": -1.00000000,
      "vout": 1,
      "fee": -0.00002840,
      "abandoned": false
    }
  ],
  "hex": "02000000000101c25c8a04ee5c5f4b2224fb5fa46368584191674bddd9a68fb51b12349f5164320000000000feffffff02e8051024010000001600142f2131af18d7dd549dd825595a9251d24cad1bad00e1f5050000000017a914f6d43de2f22c32e1839fedbe719ceada137f6e8b870247304402202f7e61107280a716a970e502962090cdbdcf5dcbb67e2dab01208bfd72f80c72022027fd54acbd3404a72de373e67a800325d6239656c51cdcb6018d6b1c217d4cb30121039bea2d8c4dbf5facca095f6ca8a897f4acebc9235bc7f6dcd92e8415f7a5d13165000000"
}

hal に渡して、何番目の output であるかを示す vout を確認。

hal tx decode 02000000000101c25c8a04ee5c5f4b2224fb5fa46368584191674bddd9a68fb51b12349f5164320000000000feffffff02e8051024010000001600142f2131af18d7dd549dd825595a9251d24cad1bad00e1f5050000000017a914f6d43de2f22c32e1839fedbe719ceada137f6e8b870247304402202f7e61107280a716a970e502962090cdbdcf5dcbb67e2dab01208bfd72f80c72022027fd54acbd3404a72de373e67a800325d6239656c51cdcb6018d6b1c217d4cb30121039bea2d8c4dbf5facca095f6ca8a897f4acebc9235bc7f6dcd92e8415f7a5d13165000000 --regtest

この例では、2つ目の output が Multisig への lock であることが確認できる。つまり vout は1である。
スクリプト自体は P2SH なので不明だが、value が 1BTC でアドレスも lock 時に指定した 2NFkLUHixD8AYCBdtKNC2ZMvzF7WpMJeugC であることからわかる。

{
  "txid": "56e9d34f8bff598068df6d3e4c712308500f706b49610f2b245d48a1cc5551e4",
  "wtxid": "03c66b49120f85d927c1cc6eded7588298a1632db1f7dcba3ddd878db4b11d14",
  "size": 223,
  "weight": 565,
  "vsize": 141,
  "version": 2,
  "locktime": 101,
  "inputs": [
    {
      "prevout": "3264519f34121bb58fa6d9dd4b679141586863a45ffb24224b5f5cee048a5cc2:0",
      "txid": "3264519f34121bb58fa6d9dd4b679141586863a45ffb24224b5f5cee048a5cc2",
      "vout": 0,
      "script_sig": {
        "hex": "",
        "asm": ""
      },
      "sequence": 4294967294,
      "witness": [
        "304402202f7e61107280a716a970e502962090cdbdcf5dcbb67e2dab01208bfd72f80c72022027fd54acbd3404a72de373e67a800325d6239656c51cdcb6018d6b1c217d4cb301",
        "039bea2d8c4dbf5facca095f6ca8a897f4acebc9235bc7f6dcd92e8415f7a5d131"
      ]
    }
  ],
  "outputs": [
    {
      "value": 4899997160,
      "script_pub_key": {
        "hex": "00142f2131af18d7dd549dd825595a9251d24cad1bad",
        "asm": "OP_0 OP_PUSHBYTES_20 2f2131af18d7dd549dd825595a9251d24cad1bad",
        "type": "p2wpkh",
        "address": "bcrt1q9usnrtcc6lw4f8wcy4v44yj36fx26xad82jlv5"
      }
    },
    {
      "value": 100000000,
      "script_pub_key": {
        "hex": "a914f6d43de2f22c32e1839fedbe719ceada137f6e8b87",
        "asm": "OP_HASH160 OP_PUSHBYTES_20 f6d43de2f22c32e1839fedbe719ceada137f6e8b OP_EQUAL",
        "type": "p2sh",
        "address": "2NFkLUHixD8AYCBdtKNC2ZMvzF7WpMJeugC"
      }
    }
  ]
}

Core に、作成した Multisig スクリプトを wallet としてインポートして、1BTC が lock されているか確認してみる。
multisig という名前で wallet を作成する。privkey はインポートしないので disable_private_keys を true に設定する。

bitcoin-cli createwallet multisig true false "" false true

Multisig スクリプトを descriptor としてインポートする。
事前準備で生成した wallet origin がすでに存在するため、以後 wallet コマンドを叩く場合は -rpcwallet にどの wallet への操作であるかを指定する。

bitcoin-cli -rpcwallet=multisig importdescriptors \
'[{ "desc": "sh(multi(2,02a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def747474,02b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce1))#66xyvqvx", "timestamp":"now" }]'

regtest を使っているので残高を確認する前に 1 Block マイニングしておく。(次のコマンドの引数のアドレスは origin wallet のもの。)

bitcoin-cli generatetoaddress 1 bcrt1q7uytth5dj2hder5knely34n7w99l072a9nxdk2

1BTC lock されていることが確認できる。

bitcoin-cli -rpcwallet=multisig getbalance
1.00000000
bitcoin-cli -rpcwallet=origin getbalance

origin は二回分のマイニング報酬 100BTC から送金下分の 1BTC + 手数料がいくらかが引かれているのがわかる。

98.99997160

Multisig スクリプトから unlock する

unlocking スクリプト

redeem script を復習すると
2 <Alice's pubkey> <Bob's pubkey> 2 CHECKMULTISIG
であった。
P2SH に lock しているので unlocking スクリプトはこのようになる。
<Alice's signature> <Bob's signature> <redeem script>

署名の作成

署名の作成には rust-bitcoin を使うことにする。
サンプルコード全体は Sample code for unlock P2SH multisig transaction of Bitcoin

transaction を組み立てる

次の lock 先を origin wallet にしたいのでアドレスを取得しておく

bitcoin-cli -rpcwallet=origin getnewaddress
bcrt1qnzhemlf4ezzdz4yza3na5nrmv5hq52u58n0fuj
// lock した transaction の ID
let prev_tx_id = "f2299acdcc9ca69eb8984200441ca88bd45eff830046a12ea9560c5f2ba2fcef";
// すでに確認したように vout は 1
let vout = 1;
let outpoint = OutPoint::from_str(&format!("{}:{}", prev_tx_id, vout)).unwrap();

let mut input = TxIn::default();
input.previous_output = outpoint;

let redeem_script = Script::from_hex("522102a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def7474742102b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce152ae").unwrap();

// 先程生成した origin wallet のアドレスに lock する
let output_script = &Address::from_str("bcrt1qnzhemlf4ezzdz4yza3na5nrmv5hq52u58n0fuj").unwrap().script_pubkey();

let output = bitcoin::TxOut {
	value: 9000000, // 0.09BTC を origin wallet に lock する
	script_pubkey: output_script.clone()
};

let mut tx = Transaction{
	version: 1,
	lock_time: 0,
	input: vec![input],
	output: vec![output]
};

署名する

// P2SH では署名時の scriptSig 部分は redeem script
let sighash = tx.signature_hash(vout, &redeem_script, SigHashType::All.as_u32());

let alice_secret_key = SecretKey::from_str("bdfc1eab7cbc719286a30e48bee298742022707ae065fb8e4098b22408bb57ae").unwrap();
let bob_secret_key = SecretKey::from_str("66683d5ab7d799661fbafe1083d18dc35fe8242a8e09ac425e3b8cbe7512cb81").unwrap();

let secp = Secp256k1::new();
let alice_sig = secp.sign(&Message::from_slice(&sighash.into_inner()[..]).unwrap(), &alice_secret_key);
let mut alice_final_signature = Vec::with_capacity(75);
alice_final_signature.extend_from_slice(&alice_sig.serialize_der());
alice_final_signature.push(SigHashType::All.as_u32() as u8);

let bob_sig = secp.sign(&Message::from_slice(&sighash.into_inner()[..]).unwrap(), &bob_secret_key);
let mut bob_final_signature = Vec::with_capacity(75);
bob_final_signature.extend_from_slice(&bob_sig.serialize_der());
bob_final_signature.push(SigHashType::All.as_u32() as u8);

println!("alice sig: {:?}",serialize_hex(&alice_final_signature));
println!("bob sig: {:?}",serialize_hex(&bob_final_signature));
alice sig: "4730440220208b24dda98cef23169a8621a57bc72d49d4e5159f2f614f630fe9ca16e51a1b022016850f9e2b43e2f8b77da3524f4ad71320eaecc3a735f6c3cedf3d5356f40c6c01"
bob sig: "4730440220751972c3fffaba9a574fce825bce2c11763fc970c632c753a32941ed93587fc3022020b1b6c96223aab1fb4e9fc5ebbb1078c9dc61beff7cec90a4c1b7c7007f101601"

transaction を完成させる

生成した署名から scriptSig を完成させる。

let script_sig = Builder::new()
	.push_int(0)
	.push_slice(&alice_final_signature)
	.push_slice(&bob_final_signature)
	.push_slice(&redeem_script.as_bytes())
	.into_script();

tx.input[0].script_sig = script_sig;

let hex_tx = serialize_hex(&tx);
println!("{:?}", &hex_tx);
0100000001e45155cca1485d242b0f61496b700f500823714c3e6ddf688059ff8b4fd3e95601000000d9004730440220208b24dda98cef23169a8621a57bc72d49d4e5159f2f614f630fe9ca16e51a1b022016850f9e2b43e2f8b77da3524f4ad71320eaecc3a735f6c3cedf3d5356f40c6c014730440220751972c3fffaba9a574fce825bce2c11763fc970c632c753a32941ed93587fc3022020b1b6c96223aab1fb4e9fc5ebbb1078c9dc61beff7cec90a4c1b7c7007f10160147522102a83a8b76e592f576f45d2f429139a97e9247dedceabc26773a85638def7474742102b270fc21d62683e28dd1762845e8d800a8b44f1776b45c43d432da8b2c4e1ce152aeffffffff01405489000000000016001498af9dfd35c884d15482ec67da4c7b652e0a2b9400000000

non-standard-tx

scriptSig の組み立ては少しはまった。上記で行ったように redeem script は全体を一つのデータとしてスクリプトに挿入する必要がある。
上のコードでは .push_slice(&redeem_script.as_bytes()) に該当。

BIP で定義されている P2SH の validation ロジックを読むと明確なので引用する。