So I have found and fundamentally understand how P2PKH is assembled from inputs and outputs to create a bitcoin forth-like script for execution.

I looked at the SegWit BIP examples section and I found them easy enough to understand as well.

But looking at the Taproot BIP, I've likely reached my limit. It's very well written, but I just can't get a handle on it.

Can anyone look at my recent testnet P2TR spend (raw-ish JSON) and show me how the witnessData, scriptPubKey, and sigScript data is used to construct a bitcoin forth-like script for execution and validation? I'm only interested in single-sig Taproot. After I grasp that perhaps I can expand to multisig-taproot. I know the python samples are provided in the BIP, but honestly, still just don't get it.

BTW.. I can provide the testnet privkey if needed, though I might need to rebuild the TXN.

I've XPosted on reddit if you lack enough SE-karma to comment.

  • 1
    In the case of a P2TR key path spend, there are no scripts involved on chain at all (except the scriptPubKey with is just OP_1 <pubkey>, and an empty scriptSig). That's quite possibly what makes it hard to grasp, but also hard to answer your question as stated. Dec 4, 2021 at 20:00
  • 1
    @PieterWuille, fair enough. Perhaps a better formed question: "What data does a node gather to validate a TXN attempting to spend a single-sig-P2TR output, and how does it use that data to do the validation?"
    – Dan
    Dec 4, 2021 at 20:44
  • FWIW, the 'tap' utility in btcdeb lets you create/manipulate -- and 'btcdeb' itself lets you examine details of -- taproot stuff, which may be helpful to learn how things fit together: github.com/bitcoin-core/btcdeb/blob/master/doc/… (disclaimer: I am the author)
    – Kalle
    Dec 8, 2021 at 5:56
  • 1
    @Kalle, Wow... looks great, I'll check it out! I actually just finished hijacking some of the python in the bitcoin-core test_framework python just to replicate the pay-to-key key tweaks: reddit.com/r/Bitcoin/comments/r8wfio/…
    – Dan
    Dec 8, 2021 at 6:01

1 Answer 1


Before we start

I realize your question is only about single-sig scenarios, and you want an answer focused on how validation works. I will address these things, but I think I need to elaborate a bit on how P2TR outputs are created, and how scripts fit in first, as I fear it'd be a lot more confusing otherwise.

In what follows I'm going to make a simplification: instead of describing the actual BIP341 taproot rules, I'm going to describe a variant in which P2TR outputs can only be spendable with exactly one script, rather than with a tree of scripts of variable size. This doesn't affect the answer to your question, but avoids the need to explain the tree structure which would distract from the matter.

Constructing a taproot output

In this simplified version of taproot, every output can be spent by signing with a key (called P), or by satisfying a script (called S). The key P can be a key that (provably) nobody knows the private key to, in case you want a taproot output than can only be spent by a script. The script S can be OP_FALSE or so, if you want a taproot output that can only be spent by a key (as would be the case for a "single-sig" taproot). But in every case, both P and S are something.

In the leading example, I'm going to assume P is a public key which someone knows the private key d to. I'm also going to assume the script S equals <P1> OP_CHECKSIGVERIFY <P2> OP_CHECKSIG, which is a script that enforces a 2-of-2 policy with keys P1 and P2, and consumes two signatures from the stack (one by P1, and one by P2). In other words, our taproot output will have a "spending policy" corresponding to P or (P1 and P2). At spending time, the spender can either use the "key path" (P), or the "script path" (P1 and P2). The ways of doing that will be very different.

The first step however is creating the taproot output. To do so, the receivers first compute the so-called "tweaked pubkey" Q. This is done using a tweak function: Q = tweak(P,S) = P + hash(P || S)*G. It has two important properties:

  • If someone knows the private key d of P, and they know S, they can compute the private key for Q (namely d + hash(P || S)).
  • Given Q, or given P and S, nobody can find another P' and S' for which Q = tweak(P',S').

Q is still a public key in a way, but it also somehow "commits" to the script S in the sense that only one P and S can be revealed for it. The scriptPubKey in the transaction output will be exactly OP_1 <Q>, so it stores Q without any hashing. The OP_1 signifies it is a version 1 witness output with 32-byte push (aka P2TR). In practice, this is done by the receiver(s) encoding Q into a BIP350 (bech32m) address, and giving it to the sender. The sender's wallet then knows how to construct a transaction output with the intended scriptPubKey. This means the sender knows Q, but they don't know (or care) about P and S.

Spending and validating spends

The taproot spending rules now say that any transaction output matching the template OP_1 <32 bytes> is to be treated as a taproot output, and the 32 byte push is to be interpreted as the (tweaked) public key Q. It can be spent in two ways:

  • key path: the witness stack of the transaction input consists of exactly one element, which is interpreted as a BIP340 signature for public key Q.
  • script path: the witness stack of the transaction input consists of two or more elements. The last one is interpreted as the "untweaked" public key P, the penultimate one as the script S, and all the elements before that as the script arguments to script S. To be valid, it must be the case that Q = tweak(P,S) (thereby showing that Q committed to script S, indicating it is permitted to spend), and running the script S with the provided script arguments must return true.

So in the case of a key path spend, the (tweaked) public key Q is just treated as a public key, and spending with it is literally just requiring a valid signature with it. The validation code doesn't know or care that Q is actually tweaked in this case, but the wallet of course does: it doesn't sign with its normal private key d (because that's the private key for P), but with the tweaked private key d + hash(P || s) (the private key for Q). So in this case, the witness stack is exactly [sig(Q)]. Note that by sig(Q) I mean "sign with the private key to Q".

In the case of a script path spend, the signing wallets basically construct a witness that proves "You saw public key Q, but guess what! In fact that wasn't the public key, it's an S-tweaked version of P, and that means I'm also allowed to spend it by satisfying script S!". In our example, that means the witness stack would be [sig(P2),sig(P1),S,P]; the last two elements prove that Q permitted spending by S, and the first two are script arguments to S providing the required signatures. At that point, the validation just proceeds by running the script S = <P1> OP_CHECKSIGVERIFY <P2> OP_CHECKSIG with inputs [sig(P2),sig(P1)].

Why is this useful?

You may now think... ok, that could work, but what's the point of all this?

The idea is that we've discovered various schemes to make a single public key actually do a whole bunch of things, including the possibility of computing an "aggregate" public key out of a set of other keys, such that all individual private keys are needed to sign for the aggregate (e.g. MuSig2).

With that, it is possible for the "inner" untweaked public key P to be an aggregate as well. In more complex spending policies than just single sig (e.g. Lightning) it would then be possible to make P be the aggregate of both sides' public keys, and the "cooperative" case of spending would be just a single signature on-chain, which is very cheap. Moreover, as long as this key path can be followed, the entire construction would be indistinguishable from a normal single sig: in both cases the scriptPubKey is OP_1 <Q> for some Q, and the witness stack is just [sig(Q)].

Some final notes

If you want a single-sig taproot output for public key P, it is in fact perfectly possible to just use Q=P directly, and to sign with its untweaked private key d directly. As the validation code does not know or care about the tweaking aspect in the key path spend, this is perfectly legal. BIP341 recommends against doing that, and suggests still tweaking with the equivalent of an invalid script S instead, to deal with situation where someone lies about their public key secretly being an already-tweaked version of some other key. Tweaking whatever key you're going to use with an invalid script guarantees that no valid script path spend can be constructed with it at all.

In the real BIP341 S is not a single script, but the Merkle root of a tree of scripts, where every leaf can be a different one. The tree can be empty (key path only), consist of a single script (like here), or be a giant tree with lots of different leaves. These leaf scripts also have their own internal version number, which would permit introducing new script features that remain indistinguishable from the current taproot scriptPubKeys, as long as those scripts don't actually need to be used. The last witness stack element in the real BIP341 isn't just the untweaked public key P, but it also includes the internal version number of the chosen script, as well as the Merkle path to prove how the revealed script related to the Merkle root (and the resulting version+key+path structure is called the control block). There are various other changes to the script language (documented in BIP342), to the signature hash (see BIP341), and an additional extension mechanism called the annex, which is so far unused. None of those are particularly important here, but I wanted to mention them to make the link with the actual specification.

Ok, I guess I also haven't actually answered your question of explaining what scripts things translate to. That's because there is no real script change. The taproot spending rules aren't really changing how scripts work at all (apart from BIP342). Instead, they're introducing a layer of spending logic between the transaction validation and script validation. In the case of key path spends, there simply is no script to speak of, and instead there is just a signature. In the case of script path spends, the rules change how it is determined which script is used, and how it is determined to be the correct script, but what that script is nothing more than the claimed script directly.


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.