NAME
Bitcoin::Crypto::Manual::Taproot - Taproot support details
DESCRIPTION
This page describes all the various bits and pieces that are used in Bitcoin::Crypto's taproot support.
Taproot extends bitcoin functionality to add one new type of address (P2TR), xonly public keys (32-byte public keys with only X coordinate), schnorr signatures, script trees and tapscripts (along with new OP_CHECKSIGADD). Taproot outputs can be spent using key path (like P2PKH) or script path (like P2SH). There is no visual distiction between key path and script path spending before outputs are spent. In addition, taproot outputs can contain many scripts organized in a tree, and any single script can be used for spending without the need to reveal all scripts in the tree. Overall, taproot increases privacy, increases flexibility of scripts and reduces the size of transactions, which results in lower fees.
Keys and derivation
Taproot distinguishes between two types of keys: internal keys and output keys. Internal keys are like the regular ECC keypair, but the public key is serialized as 32 byte array xonly key (dropping the first byte of regular compressed serialization format). Output keys are internal keys that were tweaked with a taproot-specific procedure. Bitcoin::Crypto uses the same Bitcoin::Crypto::Key::Private and Bitcoin::Crypto::Key::Public instances for both internal and output keys. The difference is that output keys are marked with taproot_output flag and uses schnorr algorithm for signing and verifying.
You can use purpose number 86 or Bitcoin::Crypto::Constants::bip44_taproot_purpose to BIP44-derive extended keys in compilance with BIP86.
Bitcoin::Crypto's taproot handling should be opaque, so that you don't need to manually handle xonly public keys, taproot output keys and schnorr signatures. Standard keypair should be all you need most of the time.
Constructing addresses
"get_address" in Bitcoin::Crypto::Key::Public now returns taproot addresses by default. Method "get_taproot_address" in Bitcoin::Crypto::Key::Public works like other address-generating methods, but can optionally accept an additional argument to support spending via a script. Taproot scripts can simultaneously be spendable using key path (like P2WPKH) or script path (like P2WSH). If you omit the extra argument, coins sent to address will only be spendable with the key path.
To enable script path, the extra argument to the method mentioned above must be an instance of Bitcoin::Crypto::Script::Tree (shortcut "btc_script_tree" in Bitcoin::Crypto). Script trees are explained in BIP341 - they are binary trees with each leaf being a single script. When spending a taproot output, any script from a tree can be selected, and only this script must be revealed. All other scripts in the tree will stay private.
Currently, all leaves in the tree must have leaf_version equal to 0xc0 or Bitcoin::Crypto::Constants::tapscript_leaf_version, according to BIP342. Using this leaf_version means that script in the leaf must be an instance of Bitcoin::Crypto::Tapscript (shortcut "btc_tapscript" in Bitcoin::Crypto). Tapscripts are similar to scripts, but they use a different set of opcodes (Bitcoin::Crypto::Tapscript::Opcode).
To completely disable spending via key path (to force spending with scripts), "Nothing Up My Sleeve" Bitcoin::Crypto::Key::NUMS can be used, which generates public keys that have no known private key counterparts.
Spending taproot outputs
Signing transactions by using key path spend is as simple as calling "sign_transaction" in Bitcoin::Crypto::Key::Private on a transaction with proper signing_index. Signing spend by script path is much harder and currently has no internal implementation. Here's a naive example of witness stack construction that uses sighash_all and assumes no OP_CODESEPARATOR in the script:
my $tree = btc_script_tree->new(tree => [{id => 0, script => $script, leaf_version => 0xc0}]);
my @input_witness;
# first witness element: signature for script [<pubkey> OP_CHECKSIG]
push @input_witness, $privkey->sign_message(
$tx->get_digest(
signing_index => 0,
signing_subscript => $script->to_serialized,
taproot_ext_flag => 1,
taproot_ext => get_taproot_ext(1, script_tree => $tree, leaf_id => 0),
sighash => Bitcoin::Crypto::Constants::sighash_all,
)
) . pack('C', Bitcoin::Crypto::Constants::sighash_all);
# second witness element: serialized script
push @input_witness, $script->to_serialized;
# third witness element: taproot control block
push @input_witness, $tree->get_control_block(0, $pub)->to_serialized;
This example uses "get_taproot_ext" in Bitcoin::Crypto::Util to build extension defined in BIP342. "get_control_block" in Bitcoin::Crypto::Script::Tree returns an instance of Bitcoin::Crypto::Transaction::ControlBlock. After serialization, control block will contain the data required for the transaction to reconstruct the script tree and validate the used script was a part of it.
To be able to spend via script path you need to have the exact copy of script tree saved somewhere. Bitcoin::Crypto::Script::Tree class has no serialization method, but Bitcoin::Crypto::PSBT with PSBT_OUT_TAP_TREE field can be used to store the entire unhashed script tree in base64 format.
Various bits and pieces
Public keys in scripts should be marked as
taproot_outputkeys. These are xonly keys, so they use a different way of serialization. Use sequence$pubkey->get_taproot_output_key->get_xonly_keyto get the tweaked output key as a bytestring. Alternatively, to avoid tweaking and use the key as-is, call$privkey->set_taproot_output(1)before signing and use$pubkey->get_xonly_keyinstead. This may be useful if you obtain a serialized copy of a key that was already tweaked or used for schnorr signatures.To create a public key from 32-byte xonly public key, use "lift_x" in Bitcoin::Crypto::Util by calling
btc_pub->from_serialized(lift_x $bytes). Callset_taproot_outputon it if it is supposed to be a taproot output key (e.g. used in schnorr signatures).Encountering one of the new
OP_SUCCESSopcodes while compiling a tapscript will mark it as unconditionally valid. "operations" in Bitcoin::Crypto::Script of such script will appear empty, but calling "success" in Bitcoin::Crypto::Script::Runner on the runner which compiled it will returntrue.