Skip to content

Commit

Permalink
txscript: implement OP_CHECKSIG semantics for tapscript validation
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Roasbeef committed Feb 16, 2022
1 parent 3dbb822 commit 568ec1b
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 4 deletions.
55 changes: 54 additions & 1 deletion txscript/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -1986,7 +1986,10 @@ func opcodeCheckSig(op *opcode, data []byte, vm *Engine) error {
// The signature actually needs needs to be longer than this, but at
// least 1 byte is needed for the hash type below. The full length is
// checked depending on the script flags and upon parsing the signature.
if len(fullSigBytes) < 1 {
//
// This only applies if tapscript verification isn't active, as this
// check is done within the sighash itself.
if vm.taprootCtx == nil && len(fullSigBytes) < 1 {
vm.dstack.PushBool(false)
return nil
}
Expand Down Expand Up @@ -2024,13 +2027,55 @@ func opcodeCheckSig(op *opcode, data []byte, vm *Engine) error {
vm.dstack.PushBool(false)
return nil
}

// Otherwise, this is routine tapscript execution.
case vm.taprootCtx == nil:
// If this is tapscript execution, and the signature was
// actually an empty vector, then we just on an empty vector
// and continue execution from there
if len(fullSigBytes) == 0 {
// TODO(roasbeef): re-verify nil vs empty vector here
vm.dstack.PushByteArray([]byte{})
return nil
}

// If the constructor fails immediately, then it's because
// the public key size is zero, so we'll fail all script
// execution.
sigVerifier, err = newBaseTapscriptSigVerifier(
pkBytes, fullSigBytes, vm,
)
if err != nil {
return err
}

// Account for changes in the sig ops budget after this
// execution.
if err := vm.taprootCtx.tallysigOp(); err != nil {
return err
}

default:
// We skip segwit v1 in isolation here, as the v1 rules aren't
// used in script execution (for sig verification) and are only
// part of the top-level key-spend verification which we
// already skipped.
//
// In other words, this path shouldn't ever be reached
//
// TODO(roasbeef): return an error?
}

// TODO(roasbeef): verify NULLFAIL semantics as relates to constructors
// above and empty sig vectors
valid := sigVerifier.Verify()

switch {
// For tapscript, and prior execution with null fail active, if the
// signature is invalid, then this MUST be an empty signature.
case !valid && vm.taprootCtx != nil &&
(len(fullSigBytes) != 0 || len(fullSigBytes) != 1):
fallthrough
case !valid && vm.hasFlag(ScriptVerifyNullFail) && len(fullSigBytes[1:]) > 0:
str := "signature not empty on failed checksig"
return scriptError(ErrNullFail, str)
Expand All @@ -2047,6 +2092,14 @@ func opcodeCheckSig(op *opcode, data []byte, vm *Engine) error {
// Stack transformation: [... signature pubkey] -> [... bool] -> [...]
func opcodeCheckSigVerify(op *opcode, data []byte, vm *Engine) error {
err := opcodeCheckSig(op, data, vm)

// During tapscript execution this op code doesn't modify the stack in
// the case of a nil error. Execution simply picks up from where we
// left off.
if vm.taprootCtx != nil && err == nil {
return nil
}

if err == nil {
err = abstractVerify(op, vm, ErrCheckSigVerify)
}
Expand Down
8 changes: 5 additions & 3 deletions txscript/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -1060,7 +1060,7 @@ func getWitnessSigOps(pkScript []byte, witness wire.TxWitness) int {
}

switch witnessVersion {
case 0:
case BaseSegwitWitnessVersion:
switch {
case len(witnessProgram) == payToWitnessPubKeyHashDataSize:
return 1
Expand All @@ -1070,8 +1070,10 @@ func getWitnessSigOps(pkScript []byte, witness wire.TxWitness) int {
witnessScript := witness[len(witness)-1]
return countSigOpsV0(witnessScript, true)
}
case 1:
// https://github.com/bitcoin/bitcoin/blob/368831371d97a642beb54b5c4eb6eb0fedaa16b4/src/script/interpreter.cpp#L2090

// Taproot signature operations don't count towards the block-wide sig
// op limit, instead a distinct weight-based accounting method is used.
case TaprootWitnessVersion:
return 0
}

Expand Down

0 comments on commit 568ec1b

Please sign in to comment.