Skip to content

Commit

Permalink
txscript: add more detailed taproot errors
Browse files Browse the repository at this point in the history
  • Loading branch information
Roasbeef committed Mar 16, 2022
1 parent 6ab97a3 commit 99e4e00
Show file tree
Hide file tree
Showing 7 changed files with 87 additions and 19 deletions.
2 changes: 1 addition & 1 deletion txscript/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ func (t *taprootExecutionCtx) tallysigOp() error {
t.sigOpsBudget -= sigOpsDelta

if t.sigOpsBudget < 0 {
return fmt.Errorf("max sig ops exceeded")
return scriptError(ErrTaprootMaxSigOps, "")
}

return nil
Expand Down
51 changes: 51 additions & 0 deletions txscript/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,47 @@ const (
// bytes.
ErrDiscourageUpgradeablePubKeyType

// ErrTaprootSigInvalid is returned when an invalid taproot key spend
// signature is encountered.
ErrTaprootSigInvalid

// ErrTaprootMerkleProofInvalid is returned when the revealed script
// merkle proof for a taproot spend is found to be invalid.
ErrTaprootMerkleProofInvalid

// ErrTaprootOutputKeyParityMismatch is returned when the control block
// proof is valid, but the parity of the y-coordinate of the derived
// key doesn't match the value encoded in the control block.
ErrTaprootOutputKeyParityMismatch

// ErrControlBlockTooSmall is returned when a parsed control block is
// less than 33 bytes.
ErrControlBlockTooSmall

// ErrControlBlockTooLarge is returned when the control block is larger
// than the largest possible proof for a merkle script tree.
ErrControlBlockTooLarge

// ErrControlBlockInvalidLength is returned when the control block,
// without the public key isn't a multiple of 32.
ErrControlBlockInvalidLength

// ErrWitnessHasNoAnnex is returned when a caller attempts to extract
// an annex, but the witness has no annex present.
ErrWitnessHasNoAnnex

// ErrInvalidTaprootSigLen is returned when taproot signature isn't 64
// or 65 bytes.
ErrInvalidTaprootSigLen

// ErrTaprootPubkeyIsEmpty is returned when a signature checking op
// code encounters an empty public key.
ErrTaprootPubkeyIsEmpty

// ErrTaprootMaxSigOps is returned when the number of allotted sig ops
// is exceeded during taproot execution.
ErrTaprootMaxSigOps

// numErrorCodes is the maximum error code number used in tests. This
// entry MUST be the last entry in the enum.
numErrorCodes
Expand Down Expand Up @@ -443,6 +484,16 @@ var errorCodeStrings = map[ErrorCode]string{
ErrDiscourageUpgradeableTaprootVersion: "ErrDiscourageUpgradeableTaprootVersion",
ErrTapscriptCheckMultisig: "ErrTapscriptCheckMultisig",
ErrDiscourageUpgradeablePubKeyType: "ErrDiscourageUpgradeablePubKeyType",
ErrTaprootSigInvalid: "ErrTaprootSigInvalid",
ErrTaprootMerkleProofInvalid: "ErrTaprootMerkleProofInvalid",
ErrTaprootOutputKeyParityMismatch: "ErrTaprootOutputKeyParityMismatch",
ErrControlBlockTooSmall: "ErrControlBlockTooSmall",
ErrControlBlockTooLarge: "ErrControlBlockTooLarge",
ErrControlBlockInvalidLength: "ErrControlBlockInvalidLength",
ErrWitnessHasNoAnnex: "ErrWitnessHasNoAnnex",
ErrInvalidTaprootSigLen: "ErrInvalidTaprootSigLen",
ErrTaprootPubkeyIsEmpty: "ErrTaprootPubkeyIsEmpty",
ErrTaprootMaxSigOps: "ErrTaprootMaxSigOps",
}

// String returns the ErrorCode as a human-readable name.
Expand Down
10 changes: 10 additions & 0 deletions txscript/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@ func TestErrorCodeStringer(t *testing.T) {
{ErrTapscriptCheckMultisig, "ErrTapscriptCheckMultisig"},
{ErrDiscourageUpgradableWitnessProgram, "ErrDiscourageUpgradableWitnessProgram"},
{ErrDiscourageUpgradeablePubKeyType, "ErrDiscourageUpgradeablePubKeyType"},
{ErrTaprootSigInvalid, "ErrTaprootSigInvalid"},
{ErrTaprootMerkleProofInvalid, "ErrTaprootMerkleProofInvalid"},
{ErrTaprootOutputKeyParityMismatch, "ErrTaprootOutputKeyParityMismatch"},
{ErrControlBlockTooSmall, "ErrControlBlockTooSmall"},
{ErrControlBlockTooLarge, "ErrControlBlockTooLarge"},
{ErrControlBlockInvalidLength, "ErrControlBlockInvalidLength"},
{ErrWitnessHasNoAnnex, "ErrWitnessHasNoAnnex"},
{ErrInvalidTaprootSigLen, "ErrInvalidTaprootSigLen"},
{ErrTaprootPubkeyIsEmpty, "ErrTaprootPubkeyIsEmpty"},
{ErrTaprootMaxSigOps, "ErrTaprootMaxSigOps"},
{0xffff, "Unknown ErrorCode (65535)"},
}

Expand Down
10 changes: 5 additions & 5 deletions txscript/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -2038,14 +2038,14 @@ func opcodeCheckSig(op *opcode, data []byte, vm *Engine) error {
}
}

// Empty public keys immeidately cause execution to fail.
// Empty public keys immediately cause execution to fail.
if len(pkBytes) == 0 {
return fmt.Errorf("nil pub key")
return scriptError(ErrTaprootPubkeyIsEmpty, "")
}

// If this is tapscript execution, and the signature was
// actually an empty vector, then we push on an empty vector
// and continue execution from ther, but only if the pubkey
// and continue execution from there, but only if the pubkey
// isn't empty.
if len(fullSigBytes) == 0 {
vm.dstack.PushByteArray([]byte{})
Expand Down Expand Up @@ -2143,9 +2143,9 @@ func opcodeCheckSigAdd(op *opcode, data []byte, vm *Engine) error {
}
}

// Empty public keys immeidately cause execution to fail.
// Empty public keys immediately cause execution to fail.
if len(pubKeyBytes) == 0 {
return fmt.Errorf("nil pubkey")
return scriptError(ErrTaprootPubkeyIsEmpty, "")
}

// If the signature is empty, then we'll just push the value N back
Expand Down
7 changes: 3 additions & 4 deletions txscript/sigvalidate.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,8 @@ func parseTaprootSigAndPubKey(pkBytes, rawSig []byte,

// Otherwise, this is an invalid signature, so we need to bail out.
default:
// TODO(roasbeef): do proper error here
return nil, nil, 0, fmt.Errorf("invalid sig len: %v", len(rawSig))
str := fmt.Sprintf("invalid sig len: %v", len(rawSig))
return nil, nil, 0, scriptError(ErrInvalidTaprootSigLen, str)
}

return pubKey, sig, sigHashType, nil
Expand Down Expand Up @@ -402,8 +402,7 @@ func newBaseTapscriptSigVerifier(pkBytes, rawSig []byte,
// If the public key is zero bytes, then this is invalid, and will fail
// immediately.
case 0:
// TODO(roasbeef): better erro
return nil, fmt.Errorf("pubkey is zero bytes")
return nil, scriptError(ErrTaprootPubkeyIsEmpty, "")

// If the public key is 32 byte as we expect, then we'll parse things
// as normal.
Expand Down
3 changes: 1 addition & 2 deletions txscript/standard.go
Original file line number Diff line number Diff line change
Expand Up @@ -482,8 +482,7 @@ func isAnnexedWitness(witness wire.TxWitness) bool {
// witness doesn't contain an annex, then an error is returned.
func extractAnnex(witness [][]byte) ([]byte, error) {
if !isAnnexedWitness(witness) {
// TODO(roasbeef): make into actual type
return nil, fmt.Errorf("no witness annex")
return nil, scriptError(ErrWitnessHasNoAnnex, "")
}

lastElement := witness[len(witness)-1]
Expand Down
23 changes: 16 additions & 7 deletions txscript/taproot.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,7 @@ func VerifyTaprootKeySpend(witnessProgram []byte, rawSig []byte, tx *wire.MsgTx,
return nil
}

// TODO(roasbeef): add proper error
return fmt.Errorf("invalid sig")
return scriptError(ErrTaprootSigInvalid, "")
}

// ControlBlock houses the structured witness input for a taproot spend. This
Expand Down Expand Up @@ -189,18 +188,24 @@ func ParseControlBlock(ctrlBlock []byte) (*ControlBlock, error) {
// The control block must minimally have 33 bytes for the internal
// public key and script leaf version.
case len(ctrlBlock) < ControlBlockBaseSize:
return nil, fmt.Errorf("invalid control block size")
str := fmt.Sprintf("min size is %v bytes, control block "+
"is %v bytes", ControlBlockBaseSize, len(ctrlBlock))
return nil, scriptError(ErrControlBlockTooSmall, str)

// The control block can't be larger than a proof for the largest
// possible tapscript merkle tree with 2^128 leaves.
case len(ctrlBlock) > ControlBlockMaxSize:
return nil, fmt.Errorf("invalid max block size")
str := fmt.Sprintf("max size is %v, control block is %v bytes",
ControlBlockMaxSize, len(ctrlBlock))
return nil, scriptError(ErrControlBlockTooLarge, str)

// Ignoring the fixed sized portion, we expect the total number of
// remaining bytes to be a multiple of the node size, which is 32
// bytes.
case (len(ctrlBlock)-ControlBlockBaseSize)%ControlBlockNodeSize != 0:
return nil, fmt.Errorf("invalid max block size")
str := fmt.Sprintf("control block proof is not a multiple "+
"of 32: %v", len(ctrlBlock)-ControlBlockBaseSize)
return nil, scriptError(ErrControlBlockInvalidLength, str)
}

// With the basic sanity checking complete, we can now parse the
Expand Down Expand Up @@ -347,15 +352,19 @@ func VerifyTaprootLeafCommitment(controlBlock *ControlBlock,
// program passed in.
expectedWitnessProgram := schnorr.SerializePubKey(taprootKey)
if !bytes.Equal(expectedWitnessProgram, taprootWitnessProgram) {
return fmt.Errorf("invalid witness commitment")

return scriptError(ErrTaprootMerkleProofInvalid, "")
}

// Finally, we'll verify that the parity of the y coordinate of the
// public key we've derived matches the control block.
derivedYIsOdd := (taprootKey.SerializeCompressed()[0] ==
secp.PubKeyFormatCompressedOdd)
if controlBlock.OutputKeyYIsOdd != derivedYIsOdd {
return fmt.Errorf("invalid witness commitment")
str := fmt.Sprintf("control block y is odd: %v, derived "+
"parity is odd: %v", controlBlock.OutputKeyYIsOdd,
derivedYIsOdd)
return scriptError(ErrTaprootOutputKeyParityMismatch, str)
}

// Otherwise, if we reach here, the commitment opening is valid and
Expand Down

0 comments on commit 99e4e00

Please sign in to comment.