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動画参照↓
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
の内部的で行っているのは↓
- スクリプトのリストからルートハッシュを計算:
追加されたスクリプトを2つずつのペアにして親ノードを作り、あぶれてペアにならない場合は1つ上の階層に移動するというの繰り返してツリーを構成し、ルートハッシュを計算する。 - ルートハッシュとInternal Keyから
tweak
を生成し、それを秘密鍵として、スクリプトツリーの公開鍵を計算する。 - Internal Keyの公開鍵と2の公開鍵を加算してP2TRの公開鍵を計算する。
- 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
を使ってアンロックする。この場合、プルーフとして必要になるのはscript1
とscript3
のハッシュになる。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は需要があれば作るかもしれない。