In this commit, we implement the new checksig semantics as part of
tapscript validation. Namely:
* OP_CHECKSIGVERIFY no longer pops the OP_TRUE off the stack (TODO(roasbeef): verify))
* the new sig ops semantics are added where each sig deducts 50 from a
starting budget of 50+the weight of the witness
* NULLFAIL is always enforced, meaning invalid sigs MUST be an empty sig array
In this commit, we add a new struct to represent the ControlBlock
structure used to feed in the tapscript leaf inclusion proof into the
witness tack. The `ParseControlBlock` parses a would-be control block
and returns an error if it's incorrectly formatted.
In this commit, we implement the new BIP 341+342 taproot sighash digest
computation. The digest is similar, but re-orders some fragments and
also starts to commit to the input values of all the transactions in the
SIGHASH_ALL case. A new implicit sighash flag, SIGHASH_DEFAULT has been
added that allows signatures to always be 64-bytes for the common case.
The hashcache has been updated as well to store both the v0 and v1 mid
state hashes. The v0 hashes are a double-sha of the contents, while the
v1 hash is a single sha. As a result, if a transaction spends both v0
and v1 inputs, then we 're able to re-use all the intermediate hashes.
As the sighash computation needs the input values and scripts, we create
an abstraction: the PrevOutFetcher to give the caller flexibility w.r.t
how this is done. We also create a `CannedPrevOutputFetcher` that holds
the information in a map for a single input.
A series of function options are also added to allow re-use of the same
base sig hash calculation for both BIP 341 and 342.
This introduces a new function named removeOpcodeByDataRaw which accepts
the raw scripts and data to remove versus requiring the parsed opcodes
to both significantly optimize it as well as make it more flexible for
working with raw scripts.
There are several places in the rest of the code that currently only
have access to the parsed opcodes, so this only introduces the function
for use in the future and deprecates the existing one.
Note that, in practice, the script will never actually contain the data
that is intended to be removed since the function is only used during
signature verification to remove the signature itself which would
require some incredibly non-standard code to create.
Thus, as an optimization, it avoids allocating a new script unless there
is actually a match that needs to be removed.
Finally, it updates the tests to use the new function.
This renames the canonicalPush function to isCanonicalPush and converts
it to accept an opcode as a byte and the associate data as a byte slice
instead of the internal parse opcode data struct in order to make it
more flexible for raw script analysis.
It also updates all callers and tests accordingly.
This converts the GetWitnessSigOpCount function to use a combination of
raw script analysis and the new tokenizer instead of the far less
efficeint parseScript, thereby significantly optimizing the funciton.
In particular, it use the recently added countSigOpsv0 in precise mode
to avoid calling paseScript.
This converts the GetPreciseSigOpCount function to use a combination of
raw script analysis and the new tokenizer instead of the far less
efficient parseScript thereby significantly optimizing the function.
In particular it uses the recently converted isScriptHashScript,
IsPushOnlyScript, and countSigOpsV0 functions along with the recently
added finalOpcodeData functions.
It also modifies the comment to explicitly call out the script version
semantics.
The following is a before and after comparison of analyzing a large
script:
benchmark old ns/op new ns/op delta
BenchmarkGetPreciseSigOpCount-8 130223 742 -99.43%
benchmark old allocs new allocs delta
BenchmarkGetPreciseSigOpCount-8 3 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkGetPreciseSigOpCount-8 623367 0 -100.00%
This converts the GetSigOpCount function to make use of the new
tokenizer instead of the far less efficient parseScript thereby
significantly optimizing the function.
A new function named countSigOpsV0 which accepts the raw script is
introduced to perform the bulk of the work so it can be reused for
precise signature operation counting as well in a later commit. It
retains the same semantics in terms of counting the number of signature
operations either up to the first parse error or the end of the script
in the case it parses successfully as required by consensus.
Finally, this also deprecates the getSigOpCount function that requires
opcodes in favor of the new function and modifies the comment on
GetSigOpCount to explicitly call out the script version semantics.
The following is a before and after comparison of analyzing a large
script:
benchmark old ns/op new ns/op delta
BenchmarkGetSigOpCount-8 61051 677 -98.89%
benchmark old allocs new allocs delta
BenchmarkGetSigOpCount-8 1 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkGetSigOpCount-8 311299 0 -100.00%
This converts the IsUnspendable function to make use of a combination of
raw script analysis and the new tokenizer instead of the far less
efficient parseScript thereby significantly optimizing the function.
It is important to note that this new implementation intentionally has a
semantic difference from the existing implementation in that it will now
report scripts that are larger than the max allowed script size are
unspendable as well.
Finally, the comment is modified to explicitly call out the script
version semantics.
Note: this function was recently optimized in master, so the gains here
are less noticable than other optimizations.
The following is a before and after comparison of analyzing a large
script:
benchmark old ns/op new ns/op delta
BenchmarkIsUnspendable-8 656 584 -10.98%
benchmark old allocs new allocs delta
BenchmarkIsUnspendable-8 1 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkIsUnspendable-8 1 0 -100.00%
This converts the IsNullData function to analyze the raw script instead
of using the far less efficient parseScript, thereby significantly
optimizing the function.
The following is a before and after comparison of analyzing a large
script:
benchmark old ns/op new ns/op delta
BenchmarkIsNullDataScript-8 62495 2.65 -100.00%
benchmark old allocs new allocs delta
BenchmarkIsNullDataScript-8 1 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkIsNullDataScript-8 311299 0 -100.00%
This converts the IsPayToWitnessScriptHash function to analyze the raw
script instead of using the far less efficient parseScript, thereby
significantly optimizing the function.
In order to accomplish this, it introduces two new functions. The first
one is named extractWitnessScriptHash and works with the raw script byte
to simultaneously deteremine if the script is a p2wsh script, and in the
case that is is, extract and return the hash. The second new function is
named isWitnessScriptHashScript and is defined in terms of the former.
The extract function approach was chosed because it is common for
callers to want to only extract relevant details from a script if the
script is of the specific type. Extracting those details requires
performing the exact same checks to ensure the script is of the correct
type, so it is more efficient to combine the two into one and define the
type determination in terms of the result, so long as the extraction
does not require allocations.
Finally, this also deprecates the isWitnessScriptHash function that
requires opcodes in favor of the new functions and modifies the comment
on IsPayToWitnessScriptHash to call out the script version semantics.
The following is a before and after comparison of executing
IsPayToWitnessScriptHash on a large script:
benchmark old ns/op new ns/op delta
BenchmarkIsWitnessScriptHash-8 62774 0.63 -100.00%
benchmark old allocs new allocs delta
BenchmarkIsWitnessScriptHash-8 1 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkIsWitnessScriptHash-8 311299 0 -100.00%
This converts the IsPayToWitnessPubKeyHash function to analyze the raw
script instead of the far less efficient parseScript, thereby
significantly optimizing the function.
In order to accomplish this, it introduces two new functions. The first
one is named extractWitnessPubKeyHash and works with the raw script
bytes to simultaneously deteremine if the script is a p2wkh, and in case
it is, extract and return the hash. The second new function is name
isWitnessPubKeyHashScript which is defined in terms of the former.
The extract function is approach was chosen because it is common for
callers to want to only extract relevant details from the script if the
script is of the specific type. Extracting those details requires the
exact same checks to ensure the script is of the correct type, so it is
more efficient to combine the two and define the type determination in
terms of the result so long as the extraction does not require
allocations.
Finally, this deprecates the isWitnessPubKeyHash function that requires
opcodes in favor of the new functions and modifies the comment on
IsPayToWitnessPubKeyHash to explicitly call out the script version
semantics.
The following is a before and after comparison of executing
IsPayToWitnessPubKeyHash on a large script:
benchmark old ns/op new ns/op delta
BenchmarkIsWitnessPubKeyHash-8 68927 0.53 -100.00%
benchmark old allocs new allocs delta
BenchmarkIsWitnessPubKeyHash-8 1 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkIsWitnessPubKeyHash-8 311299 0 -100.00%
This converts the IsPushOnlyScript function to make use of the new
tokenizer instead of the far less efficient parseScript thereby
significantly optimizing the function.
It also deprecates the isPushOnly function that requires opcodes in
favor of the new function and modifies the comment on IsPushOnlyScript
to explicitly call out the script version semantics.
The following is a before and after comparison of analyzing a large
script:
benchmark old ns/op new ns/op delta
BenchmarkIsPushOnlyScript-8 62412 622 -99.00%
benchmark old allocs new allocs delta
BenchmarkIsPushOnlyScript-8 1 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkIsPushOnlyScript-8 311299 0 -100.00%
This converts the IsMultisigSigScript function to analyze the raw script
and make use of the new tokenizer instead of the far less efficient
parseScript thereby significantly optimizing the function.
In order to accomplish this, it first rejects scripts that can't
possibly fit the bill due to the final byte of what would be the redeem
script not being the appropriate opcode or the overall script not having
enough bytes. Then, it uses a new function that is introduced named
finalOpcodeData that uses the tokenizer to return any data associated
with the final opcode in the signature script (which will be nil for
non-push opcodes or if the script fails to parse) and analyzes it as if
it were a redeem script when it is non nil.
It is also worth noting that this new implementation intentionally has
the same semantic difference from the existing implementation as the
updated IsMultisigScript function in regards to allowing zero pubkeys
whereas previously it incorrectly required at least one pubkey.
Finally, the comment is modified to explicitly call out the script
version semantics.
The following is a before and after comparison of analyzing a large
script that is not a multisig script and both a 1-of-2 multisig public
key script (which should be false) and a signature script comprised of a
pay-to-script-hash 1-of-2 multisig redeem script (which should be true):
benchmark old ns/op new ns/op delta
BenchmarkIsMultisigSigScriptLarge-8 69328 2.93 -100.00%
BenchmarkIsMultisigSigScript-8 2375 146 -93.85%
benchmark old allocs new allocs delta
BenchmarkIsMultisigSigScriptLarge-8 5 0 -100.00%
BenchmarkIsMultisigSigScript-8 3 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkIsMultisigSigScriptLarge-8 330035 0 -100.00%
BenchmarkIsMultisigSigScript-8 9472 0 -100.00%
This converts the IsPayToScriptHash function to analyze the raw script
instead of using the far less efficient parseScript thereby
significantly optimizing the function.
In order to accomplish this, it introduces two new functions. The first
one is named extractScriptHash and works with the raw script bytes to
simultaneously determine if the script is a p2sh script, and in the case
it is, extract and return the hash. The second new function is named
isScriptHashScript and is defined in terms of the former.
The extract function approach was chosen because it is common for
callers to want to only extract relevant details from a script if the
script is of the specific type. Extracting those details requires
performing the exact same checks to ensure the script is of the correct
type, so it is more efficient to combine the two into one and define the
type determination in terms of the result so long as the extraction does
not require allocations.
Finally, this also deprecates the isScriptHash function that requires
opcodes in favor of the new functions and modifies the comment on
IsPayToScriptHash to explicitly call out the script version semantics.
The following is a before and after comparison of analyzing a large
script that is not a p2sh script:
benchmark old ns/op new ns/op delta
BenchmarkIsPayToScriptHash-8 62393 0.60 -100.00%
benchmark old allocs new allocs delta
BenchmarkIsPayToScriptHash-8 1 0 -100.00%
benchmark old bytes new bytes delta
BenchmarkIsPayToScriptHash-8 311299 0 -100.00%