Develop with pleasure!

福岡でCloudとかBlockchainとか。

Taprootのビルド、署名作成のサポートをするBuilderを実装してみた

Taprootもロックインされたので、Taproot宛に送金するP2TR(Pay to Taproot)の作成や、そのUTXOを使用する際の署名作成のサポートをするBuilderクラスをbitcoinrbに実装してみた*1

※ 2021/11/15追記:以下のBitcoin::Taproot::SimpleBuilder については一部インターフェースを変更したため、最新のbitcoinrbで動作するコードについてはwiki参照。

Taprootのスクリプトツリーでは、任意の2分木の構成を取ることができ、最近マージされたBitcoin CoreのTaprootBuilderでは、リーフノードのスクリプトや中間ノードをツリーの深さを指定して構成するようになっている。ただ、二分木にならないような指定をするとエラーになり、使う側がある程度ツリー構造を考えてリーフノードや中間ノードを配置する必要がある。任意のツリーを構成できる反面、ツリー構造に特別な要求がない場合は面倒なので、bitcoinrbで今回実装したのはまずシンプルに複数のロック条件から自動的にスクリプトをビルドする(ツリーのカスタマイズはできない)SimpleBuilder

https://github.com/chaintope/bitcoinrb/blob/v0.8.0/lib/bitcoin/taproot/simple_builder.rb

※ Taprootはロックインされたけど、実際にアクティベートされるのは11月頃と想定されるブロック709,632で、それまではmainnetで使用するとコインがマイナーに回収されるので、実験する場合は既にアクティベートされているSignetで。

P2TRのビルド

TaprootのscriptPubkeyはsegwit version 1を使用した

OP_1 <32バイトの公開鍵>

形式で、↑の32バイトの公開鍵は、Key-PathのInternal KeyとScript-Pathのマークルルートから計算した鍵を加算した点になる。この辺りの仕組みについてはGBEC動画参照↓

goblockchain.network

SimpleBuilderで、まずはこのP2TRを作成する。↓の例ではInternal Keyに加えて、3つ公開鍵を作成し、それぞれに対して署名検証をする(OP_CHECKSIG)をする3つのロックスクリプトを持つP2TRを生成している。※ なお、Internal KeyおよびTapscriptで署名検証に用いる公開鍵はすべて32バイトで、これまでの33バイトの圧縮公開鍵とは異なるので要注意。

require 'bitcoin'

include Bitcoin::Opcodes

# Internal Keyの鍵を生成(秘密鍵を指定してるけど、公開鍵だけあればいい)
internal_key = Bitcoin::Key.new(priv_key: '98d2f0b8dfcaa7b29933bc78e8d82cd9d7c7a18ddc128ce2bc9dd143804f36f4')

# 3つのロックスクリプト(秘密鍵を指定してるけど、公開鍵だけあればいい)
key1 = Bitcoin::Key.new(priv_key: 'fd0137b05e26f40f8900697b690e11b2eba8abbd0f53c421148a22646b15f96f')
key2 = Bitcoin::Key.new(priv_key: '3b0ce9ef75031f5a1d6679f017fdd8d77460ecdcac1a24d482e1465e1768e22c')
key3 = Bitcoin::Key.new(priv_key: 'df94bce0533b3ff0c6b8ca16d6d2ce08b01350792cb350146cfaba056d5e4bfa')
script1 = Bitcoin::Script.new << key1.xonly_pubkey << OP_CHECKSIG
script2 = Bitcoin::Script.new << key2.xonly_pubkey << OP_CHECKSIG
script3 = Bitcoin::Script.new << key3.xonly_pubkey << OP_CHECKSIG

# Internal Keyと3つのロックスクリプトを持つP2TRを作成
builder = Bitcoin::Taproot::SimpleBuilder.new(internal_key.xonly_pubkey, script1, script2, script3)
script_pubkey = builder.build
script_pubkey.to_addr
=> 'tb1p9uv58mst47h0r9zd8lm9hjlttcskq4wndxfceh8mjknd92mmflzspnsygf'

さくっとP2TRスクリプトが作れる*2。↑のように3つのロック条件がある場合のスクリプトツリーは、

      N0
   /     \
  N1      C
 /  \
A    B

のようになる。スクリプトが増えれば、ツリーも変化していく↓

4つスクリプトがある場合:
      N0
   /     \
  N1      N2
 /  \    /  \
A    B  C    D

5つスクリプトがある場合:
          N0
       /     \
      N1      E
   /     \
  N2      N3
 /  \    /  \
A    B  C    D

SimpleBuilderの内部的で行っているのは↓

  1. スクリプトのリストからルートハッシュを計算:
    追加されたスクリプトを2つずつのペアにして親ノードを作り、あぶれてペアにならない場合は1つ上の階層に移動するというの繰り返してツリーを構成し、ルートハッシュを計算する。
  2. ルートハッシュとInternal Keyからtweakを生成し、それを秘密鍵として、スクリプトツリーの公開鍵を計算する。
  3. Internal Keyの公開鍵と2の公開鍵を加算してP2TRの公開鍵を計算する。
  4. 3で計算した公開鍵からP2TRのscriptPubkeyを生成する。

P2TR UTXOの使用

↑で作成したP2TRのUTXOを今度は使ってみよう。このUTXOは、Key-PathとScript-Pathのいずれかでアンロックできる。

Key-Pathを使う場合

1つの方法は、Internal Keyを使ってアンロックする方法。この場合、witnessとして、P2TRのscriptPubkey内の(↑の3で計算した)公開鍵に対して有効なSchnorr署名を提供すればいい。SimpleBuilderでサポートするのは、この公開鍵に対応する秘密鍵の導出。

# 署名用の秘密鍵を導出
key = builder.tweak_private_key(internal_key)

SimpleBuilder#tweak_private_keyは、Internal Keyの秘密鍵スクリプトからP2TRの公開鍵に対応する秘密鍵を導出する。

あとは、この秘密鍵を使って送金Txに署名すれば良い。bitcoinrbだと↓な感じで実装する:

# Txを作成
tx = Bitcoin::Tx.new
tx.in << Bitcoin::TxIn.new(out_point: Bitcoin::OutPoint.from_txid('9b5dbbe79a8938b9527b0a5f12c9be695ca1dac4e4267529a228c380c0b232bd', 1))
tx.out << Bitcoin::TxOut.new(value: 90_000, script_pubkey: script_pubkey)

# 署名対象のsighashを計算
prevouts = [Bitcoin::TxOut.new(value: 100_000, script_pubkey: script_pubkey)]
sighash = tx.sighash_for_input(0, sig_version: :taproot, prevouts: prevouts, hash_type: Bitcoin::SIGHASH_TYPE[:default])

# Schnorr署名を計算
sig = key.sign(sighash, algo: :schnorr)

# インプットに署名をセット
tx.in[0].script_witness.stack << sig

# Txペイロード
tx.to_hex

なお、SIGHASH_TYPE: allはデフォルトで省略しなければならないので(省略しないとTaprootではエラーになる)、インプットに署名をセットする際は付与していない。

実際にSignetでKey-Pathを使ってアンロックしたTxが2d881433592e893e0dbb079929198fed1ef23e132927dfb97c3ada6f5598ecf3

Script-Pathを使う場合

もう1つのScript-Pathを使ってアンロックする場合は、witnessとしてアンロックに使用するスクリプトと、Internal keyおよびスクリプトがツリーに含まれていることを証明するプルーフを提供する。またそれに加えて、スクリプトをアンロックするために必要な要素。

プルーフは、P2TRの公開鍵のY座標の偶奇を表すparity bitとInternal key、leaf versionと合わせてControl Blockを構成し、それをwitnessにセットすることになる。

今回は↑のscript2を使ってアンロックする。この場合、プルーフとして必要になるのはscript1script3のハッシュになる。Script-Pathを使ってアンロックするトランザクションの作成方法は↓

# Txの作成
tx = Bitcoin::Tx.new
tx.in << Bitcoin::TxIn.new(out_point: Bitcoin::OutPoint.from_txid('3cad3075b2cd448fdae11a9d3bb60d9b71acf6a279df7933dd6c966f29e0469d', 1))
tx.out << Bitcoin::TxOut.new(value: 90_000, script_pubkey: script_pubkey)

# 署名対象のsighashを計算
prevouts = [Bitcoin::TxOut.new(value: 100_000, script_pubkey: script_pubkey)]
opts = {leaf_hash: builder.leaf_hash(script2)} # script pathではleaf hashにもコミットするためオプションで渡す
sighash = tx.sighash_for_input(0, sig_version: :tapscript, prevouts: prevouts, hash_type: Bitcoin::SIGHASH_TYPE[:default], opts: opts)

# Schnorr署名を計算
sig = key2.sign(sighash, algo: :schnorr)

# witnessにアンロックに必要なデータをセット
## sript2のアンロックアイテム(署名)をセット
tx.in[0].script_witness.stack << sig
## script2をセット
tx.in[0].script_witness.stack << script2.to_payload
## script2がツリーに含まれていることを証明するプルーフを含むControl Blockをセット
tx.in[0].script_witness.stack << builder.control_block(script2)

# Txペイロード
tx.to_hex

SimpleBuilderは、#leaf_hashメソッドで使用するスクリプトleaf hashの提供と、#control_blockメソッドでそのスクリプトスクリプトツリーに含まれていることを証明するためのプルーフを含むControl Blockを生成する。このControl Blockは、以下のデータを連結したもの:

<P2TRの公開鍵のY座標の偶奇を表すparity bit + スクリプトのleaf version> + <Internal Key> + <Proof(ツリーのリーフノードからルートまでの兄弟ノードのハッシュ)>

実際にSignetでScript-Pathを使ってアンロックしたTxが3117d2bb1c9962c9d4c7bc3d31f48a13878d346407323bbb316908f3ea6279ae

と、Key-Path、Script-PathでP2TRを使用するトランザクションを構成するサポート機能ができた。任意の構成のスクリプトツリーを構成するようなBuilderは需要があれば作るかもしれない。

*1:ScriptInterpreterのTaproot対応は既に完了している

*2:スクリプトleaf versionはデフォルトで0xc0がセットされる。引数で指定することも可能。