Skip to content

Commit

Permalink
txscript: Significantly improve errors.
Browse files Browse the repository at this point in the history
This converts the majority of script errors from generic errors created
via errors.New and fmt.Errorf to use a concrete type that implements the
error interface with an error code and description.

This allows callers to programmatically detect the type of error via
type assertions and an error code while still allowing the errors to
provide more context.

For example, instead of just having an error the reads "disabled opcode"
as would happen prior to these changes when a disabled opcode is
encountered, the error will now read "attempt to execute disabled opcode
OP_FOO".

While it was previously possible to programmatically detect many errors
due to them being exported, they provided no additional context and
there were also various instances that were just returning errors
created on the spot which callers could not reliably detect without
resorting to looking at the actual error message, which is nearly always
bad practice.

Also, while here, export the MaxStackSize and MaxScriptSize constants
since they can be useful for consumers of the package and perform some
minor cleanup of some of the tests.
  • Loading branch information
davecgh committed Jan 12, 2017
1 parent 283706b commit fdc2bc8
Show file tree
Hide file tree
Showing 18 changed files with 1,147 additions and 692 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
ISC License

Copyright (c) 2013-2016 The btcsuite developers
Copyright (c) 2013-2017 The btcsuite developers
Copyright (c) 2015-2016 The Decred developers

Permission to use, copy, modify, and distribute this software for any
Expand Down
11 changes: 7 additions & 4 deletions txscript/doc.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2013-2015 The btcsuite developers
// Copyright (c) 2013-2017 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand Down Expand Up @@ -32,8 +32,11 @@ what conditions must be met in order to spend bitcoins.
Errors
Errors returned by this package are of the form txscript.ErrStackX where X
indicates the specific error. See Variables in the package documentation for a
full list.
Errors returned by this package are of type txscript.Error. This allows the
caller to programmatically determine the specific error by examining the
ErrorCode field of the type asserted txscript.Error while still providing rich
error messages with contextual information. A convenience function named
IsErrorCode is also provided to allow callers to easily check for a specific
error code. See ErrorCode in the package documentation for a full list.
*/
package txscript
140 changes: 97 additions & 43 deletions txscript/engine.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2013-2016 The btcsuite developers
// Copyright (c) 2013-2017 The btcsuite developers
// Use of this source code is governed by an ISC
// license that can be found in the LICENSE file.

Expand Down Expand Up @@ -72,12 +72,12 @@ const (
)

const (
// maxStackSize is the maximum combined height of stack and alt stack
// MaxStackSize is the maximum combined height of stack and alt stack
// during execution.
maxStackSize = 1000
MaxStackSize = 1000

// maxScriptSize is the maximum allowed length of a raw script.
maxScriptSize = 10000
// MaxScriptSize is the maximum allowed length of a raw script.
MaxScriptSize = 10000
)

// halforder is used to tame ECDSA malleability (see BIP0062).
Expand Down Expand Up @@ -123,23 +123,31 @@ func (vm *Engine) isBranchExecuting() bool {
func (vm *Engine) executeOpcode(pop *parsedOpcode) error {
// Disabled opcodes are fail on program counter.
if pop.isDisabled() {
return ErrStackOpDisabled
str := fmt.Sprintf("attempt to execute disabled opcode %s",
pop.opcode.name)
return scriptError(ErrDisabledOpcode, str)
}

// Always-illegal opcodes are fail on program counter.
if pop.alwaysIllegal() {
return ErrStackReservedOpcode
str := fmt.Sprintf("attempt to execute reserved opcode %s",
pop.opcode.name)
return scriptError(ErrReservedOpcode, str)
}

// Note that this includes OP_RESERVED which counts as a push operation.
if pop.opcode.value > OP_16 {
vm.numOps++
if vm.numOps > MaxOpsPerScript {
return ErrStackTooManyOperations
str := fmt.Sprintf("exceeded max operation limit of %d",
MaxOpsPerScript)
return scriptError(ErrTooManyOperations, str)
}

} else if len(pop.data) > MaxScriptElementSize {
return ErrStackElementTooBig
str := fmt.Sprintf("element size %d exceeds max allowed size %d",
len(pop.data), MaxScriptElementSize)
return scriptError(ErrElementTooBig, str)
}

// Nothing left to do when this is not a conditional opcode and it is
Expand Down Expand Up @@ -174,13 +182,15 @@ func (vm *Engine) disasm(scriptIdx int, scriptOff int) string {
// execution, nil otherwise.
func (vm *Engine) validPC() error {
if vm.scriptIdx >= len(vm.scripts) {
return fmt.Errorf("past input scripts %v:%v %v:xxxx",
str := fmt.Sprintf("past input scripts %v:%v %v:xxxx",
vm.scriptIdx, vm.scriptOff, len(vm.scripts))
return scriptError(ErrInvalidProgramCounter, str)
}
if vm.scriptOff >= len(vm.scripts[vm.scriptIdx]) {
return fmt.Errorf("past input scripts %v:%v %v:%04d",
str := fmt.Sprintf("past input scripts %v:%v %v:%04d",
vm.scriptIdx, vm.scriptOff, vm.scriptIdx,
len(vm.scripts[vm.scriptIdx]))
return scriptError(ErrInvalidProgramCounter, str)
}
return nil
}
Expand Down Expand Up @@ -210,7 +220,9 @@ func (vm *Engine) DisasmPC() (string, error) {
// script.
func (vm *Engine) DisasmScript(idx int) (string, error) {
if idx >= len(vm.scripts) {
return "", ErrStackInvalidIndex
str := fmt.Sprintf("script index %d >= total scripts %d", idx,
len(vm.scripts))
return "", scriptError(ErrInvalidIndex, str)
}

var disstr string
Expand All @@ -227,14 +239,18 @@ func (vm *Engine) CheckErrorCondition(finalScript bool) error {
// Check execution is actually done. When pc is past the end of script
// array there are no more scripts to run.
if vm.scriptIdx < len(vm.scripts) {
return ErrStackScriptUnfinished
return scriptError(ErrScriptUnfinished,
"error check when script unfinished")
}
if finalScript && vm.hasFlag(ScriptVerifyCleanStack) &&
vm.dstack.Depth() != 1 {

return ErrStackCleanStack
str := fmt.Sprintf("stack contains %d unexpected items",
vm.dstack.Depth()-1)
return scriptError(ErrCleanStack, str)
} else if vm.dstack.Depth() < 1 {
return ErrStackEmptyStack
return scriptError(ErrEmptyStack,
"stack empty at end of script execution")
}

v, err := vm.dstack.PopBool()
Expand All @@ -249,7 +265,8 @@ func (vm *Engine) CheckErrorCondition(finalScript bool) error {
return fmt.Sprintf("scripts failed: script0: %s\n"+
"script1: %s", dis0, dis1)
}))
return ErrStackScriptFailed
return scriptError(ErrEvalFalse,
"false stack entry at end of script execution")
}
return nil
}
Expand Down Expand Up @@ -278,16 +295,20 @@ func (vm *Engine) Step() (done bool, err error) {

// The number of elements in the combination of the data and alt stacks
// must not exceed the maximum number of stack elements allowed.
if vm.dstack.Depth()+vm.astack.Depth() > maxStackSize {
return false, ErrStackOverflow
combinedStackSize := vm.dstack.Depth() + vm.astack.Depth()
if combinedStackSize > MaxStackSize {
str := fmt.Sprintf("combined stack size %d > max allowed %d",
combinedStackSize, MaxStackSize)
return false, scriptError(ErrStackOverflow, str)
}

// Prepare for next instruction.
vm.scriptOff++
if vm.scriptOff >= len(vm.scripts[vm.scriptIdx]) {
// Illegal to have an `if' that straddles two scripts.
if err == nil && len(vm.condStack) != 0 {
return false, ErrStackMissingEndif
return false, scriptError(ErrUnbalancedConditional,
"end of script reached in conditional execution")
}

// Alt stack doesn't persist.
Expand Down Expand Up @@ -382,7 +403,8 @@ func (vm *Engine) checkHashTypeEncoding(hashType SigHashType) error {

sigHashType := hashType & ^SigHashAnyOneCanPay
if sigHashType < SigHashAll || sigHashType > SigHashSingle {
return fmt.Errorf("invalid hashtype: 0x%x\n", hashType)
str := fmt.Sprintf("invalid hash type 0x%x", hashType)
return scriptError(ErrInvalidSigHashType, str)
}
return nil
}
Expand All @@ -402,7 +424,7 @@ func (vm *Engine) checkPubKeyEncoding(pubKey []byte) error {
// Uncompressed
return nil
}
return ErrStackInvalidPubKey
return scriptError(ErrPubKeyType, "unsupported public key type")
}

// checkSignatureEncoding returns whether or not the passed signature adheres to
Expand Down Expand Up @@ -438,8 +460,9 @@ func (vm *Engine) checkSignatureEncoding(sig []byte) error {
// 0x30 + <1-byte> + 0x02 + 0x01 + <byte> + 0x2 + 0x01 + <byte>
if len(sig) < 8 {
// Too short
return fmt.Errorf("malformed signature: too short: %d < 8",
str := fmt.Sprintf("malformed signature: too short: %d < 8",
len(sig))
return scriptError(ErrSigDER, str)
}

// Maximum length is when both numbers are 33 bytes each. It is 33
Expand All @@ -448,75 +471,88 @@ func (vm *Engine) checkSignatureEncoding(sig []byte) error {
// 0x30 + <1-byte> + 0x02 + 0x21 + <33 bytes> + 0x2 + 0x21 + <33 bytes>
if len(sig) > 72 {
// Too long
return fmt.Errorf("malformed signature: too long: %d > 72",
str := fmt.Sprintf("malformed signature: too long: %d > 72",
len(sig))
return scriptError(ErrSigDER, str)
}
if sig[0] != 0x30 {
// Wrong type
return fmt.Errorf("malformed signature: format has wrong type: 0x%x",
sig[0])
str := fmt.Sprintf("malformed signature: format has wrong "+
"type: 0x%x", sig[0])
return scriptError(ErrSigDER, str)
}
if int(sig[1]) != len(sig)-2 {
// Invalid length
return fmt.Errorf("malformed signature: bad length: %d != %d",
str := fmt.Sprintf("malformed signature: bad length: %d != %d",
sig[1], len(sig)-2)
return scriptError(ErrSigDER, str)
}

rLen := int(sig[3])

// Make sure S is inside the signature.
if rLen+5 > len(sig) {
return fmt.Errorf("malformed signature: S out of bounds")
return scriptError(ErrSigDER,
"malformed signature: S out of bounds")
}

sLen := int(sig[rLen+5])

// The length of the elements does not match the length of the
// signature.
if rLen+sLen+6 != len(sig) {
return fmt.Errorf("malformed signature: invalid R length")
return scriptError(ErrSigDER,
"malformed signature: invalid R length")
}

// R elements must be integers.
if sig[2] != 0x02 {
return fmt.Errorf("malformed signature: missing first integer marker")
return scriptError(ErrSigDER,
"malformed signature: missing first integer marker")
}

// Zero-length integers are not allowed for R.
if rLen == 0 {
return fmt.Errorf("malformed signature: R length is zero")
return scriptError(ErrSigDER,
"malformed signature: R length is zero")
}

// R must not be negative.
if sig[4]&0x80 != 0 {
return fmt.Errorf("malformed signature: R value is negative")
return scriptError(ErrSigDER,
"malformed signature: R value is negative")
}

// Null bytes at the start of R are not allowed, unless R would
// otherwise be interpreted as a negative number.
if rLen > 1 && sig[4] == 0x00 && sig[5]&0x80 == 0 {
return fmt.Errorf("malformed signature: invalid R value")
return scriptError(ErrSigDER,
"malformed signature: invalid R value")
}

// S elements must be integers.
if sig[rLen+4] != 0x02 {
return fmt.Errorf("malformed signature: missing second integer marker")
return scriptError(ErrSigDER,
"malformed signature: missing second integer marker")
}

// Zero-length integers are not allowed for S.
if sLen == 0 {
return fmt.Errorf("malformed signature: S length is zero")
return scriptError(ErrSigDER,
"malformed signature: S length is zero")
}

// S must not be negative.
if sig[rLen+6]&0x80 != 0 {
return fmt.Errorf("malformed signature: S value is negative")
return scriptError(ErrSigDER,
"malformed signature: S value is negative")
}

// Null bytes at the start of S are not allowed, unless S would
// otherwise be interpreted as a negative number.
if sLen > 1 && sig[rLen+6] == 0x00 && sig[rLen+7]&0x80 == 0 {
return fmt.Errorf("malformed signature: invalid S value")
return scriptError(ErrSigDER,
"malformed signature: invalid S value")
}

// Verify the S value is <= half the order of the curve. This check is
Expand All @@ -529,7 +565,9 @@ func (vm *Engine) checkSignatureEncoding(sig []byte) error {
if vm.hasFlag(ScriptVerifyLowS) {
sValue := new(big.Int).SetBytes(sig[rLen+6 : rLen+6+sLen])
if sValue.Cmp(halfOrder) > 0 {
return ErrStackInvalidLowSSignature
return scriptError(ErrSigHighS,
"signature is not canonical due to "+
"unnecessarily high S value")
}
}

Expand Down Expand Up @@ -587,10 +625,21 @@ func (vm *Engine) SetAltStack(data [][]byte) {
func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags, sigCache *SigCache) (*Engine, error) {
// The provided transaction input index must refer to a valid input.
if txIdx < 0 || txIdx >= len(tx.TxIn) {
return nil, ErrInvalidIndex
str := fmt.Sprintf("transaction input index %d is negative or "+
">= %d", txIdx, len(tx.TxIn))
return nil, scriptError(ErrInvalidIndex, str)
}
scriptSig := tx.TxIn[txIdx].SignatureScript

// When both the signature script and public key script are empty the
// result is necessarily an error since the stack would end up being
// empty which is equivalent to a false top element. Thus, just return
// the relevant error now as an optimization.
if len(scriptSig) == 0 && len(scriptPubKey) == 0 {
return nil, scriptError(ErrEvalFalse,
"false stack entry at end of script execution")
}

// The clean stack flag (ScriptVerifyCleanStack) is not allowed without
// the pay-to-script-hash (P2SH) evaluation (ScriptBip16) flag.
//
Expand All @@ -601,13 +650,15 @@ func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags
// it should be.
vm := Engine{flags: flags, sigCache: sigCache}
if vm.hasFlag(ScriptVerifyCleanStack) && !vm.hasFlag(ScriptBip16) {
return nil, ErrInvalidFlags
return nil, scriptError(ErrInvalidFlags,
"invalid flags combination")
}

// The signature script must only contain data pushes when the
// associated flag is set.
if vm.hasFlag(ScriptVerifySigPushOnly) && !IsPushOnlyScript(scriptSig) {
return nil, ErrStackNonPushOnly
return nil, scriptError(ErrNotPushOnly,
"signature script is not push only")
}

// The engine stores the scripts in parsed form using a slice. This
Expand All @@ -617,8 +668,10 @@ func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags
scripts := [][]byte{scriptSig, scriptPubKey}
vm.scripts = make([][]parsedOpcode, len(scripts))
for i, scr := range scripts {
if len(scr) > maxScriptSize {
return nil, ErrStackLongScript
if len(scr) > MaxScriptSize {
str := fmt.Sprintf("script size %d is larger than max "+
"allowed size %d", len(scr), MaxScriptSize)
return nil, scriptError(ErrScriptTooBig, str)
}
var err error
vm.scripts[i], err = parseScript(scr)
Expand All @@ -637,7 +690,8 @@ func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags
if vm.hasFlag(ScriptBip16) && isScriptHash(vm.scripts[1]) {
// Only accept input scripts that push data for P2SH.
if !isPushOnly(vm.scripts[0]) {
return nil, ErrStackP2SHNonPushOnly
return nil, scriptError(ErrNotPushOnly,
"pay to script hash is not push only")
}
vm.bip16 = true
}
Expand Down
Loading

0 comments on commit fdc2bc8

Please sign in to comment.