txscript: Improve conditional stack.

This commit improves the way the conditional execution stack is handled in
a few ways.

First, the current execution state is now pushed onto the end of the slice
rather than the front of it.  This has been done because it results in
fewer allocations and is therefore more efficient.

Second, the need for allocating and setting an initial true in the
conditional stack has been eliminated.  The vast majority of scripts don't
contain any conditionals, so there is no reason to allocate a slice when
it isn't needed.

Third, a new function has been added to the engine to determine if the
current conditional branch is executing named isBranchExecuting which
handles the fact the conditional execution stack can now be empty and
improves the readability of the code.

Finally, it removes a couple of TODOs which I have verified do not apply.
This commit is contained in:
Dave Collins 2015-04-23 00:46:14 -05:00
parent c0b57def7f
commit d6105893af
2 changed files with 31 additions and 30 deletions

View file

@ -100,6 +100,17 @@ func (vm *Engine) hasFlag(flag ScriptFlags) bool {
return vm.flags&flag == flag
}
// isBranchExecuting returns whether or not the current conditional branch is
// actively executing. For example, when the data stack has an OP_FALSE on it
// and an OP_IF is encountered, the branch is inactive until an OP_ELSE or
// OP_ENDIF is encountered. It properly handles nested conditionals.
func (vm *Engine) isBranchExecuting() bool {
if len(vm.condStack) == 0 {
return true
}
return vm.condStack[len(vm.condStack)-1] == OpCondTrue
}
// Execute will execute all script in the script engine and return either nil
// for successful validation or an error if one occurred.
func (vm *Engine) Execute() (err error) {
@ -195,7 +206,7 @@ func (vm *Engine) Step() (done bool, err error) {
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) != 1 {
if err == nil && len(vm.condStack) != 0 {
return false, ErrStackMissingEndif
}
@ -531,7 +542,6 @@ func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags
vm.tx = *tx
vm.txIdx = txIdx
vm.condStack = []int{OpCondTrue}
return &vm, nil
}

View file

@ -745,13 +745,13 @@ func (pop *parsedOpcode) exec(vm *Engine) error {
// If we are not a conditional opcode and we aren't executing, then
// we are done now.
if vm.condStack[0] != OpCondTrue && !pop.conditional() {
if !vm.isBranchExecuting() && !pop.conditional() {
return nil
}
// Ensure all executed data push opcodes use the minimal encoding when
// the minimal data verification is set.
if vm.dstack.verifyMinimalData && vm.condStack[0] == OpCondTrue &&
if vm.dstack.verifyMinimalData && vm.isBranchExecuting() &&
pop.opcode.value >= 0 && pop.opcode.value <= OP_PUSHDATA4 {
if err := pop.checkMinimalDataPush(); err != nil {
return err
@ -896,22 +896,19 @@ func opcodeNop(op *parsedOpcode, vm *Engine) error {
func opcodeIf(op *parsedOpcode, vm *Engine) error {
// opcodeIf will be executed even if it is on the non-execute side
// of the conditional, this is so proper nesting is maintained
var condval int
if vm.condStack[0] == OpCondTrue {
condVal := OpCondFalse
if vm.isBranchExecuting() {
ok, err := vm.dstack.PopBool()
if err != nil {
return err
}
if ok {
condval = OpCondTrue
condVal = OpCondTrue
}
} else {
condval = OpCondSkip
condVal = OpCondSkip
}
cond := []int{condval}
// push condition to the 'head' of the slice
vm.condStack = append(cond, vm.condStack...)
// TODO(drahn) check if a maximum condtitional stack limit exists
vm.condStack = append(vm.condStack, condVal)
return nil
}
@ -920,37 +917,34 @@ func opcodeIf(op *parsedOpcode, vm *Engine) error {
func opcodeNotIf(op *parsedOpcode, vm *Engine) error {
// opcodeIf will be executed even if it is on the non-execute side
// of the conditional, this is so proper nesting is maintained
var condval int
if vm.condStack[0] == OpCondTrue {
condVal := OpCondFalse
if vm.isBranchExecuting() {
ok, err := vm.dstack.PopBool()
if err != nil {
return err
}
if !ok {
condval = OpCondTrue
condVal = OpCondTrue
}
} else {
condval = OpCondSkip
condVal = OpCondSkip
}
cond := []int{condval}
// push condition to the 'head' of the slice
vm.condStack = append(cond, vm.condStack...)
// TODO(drahn) check if a maximum condtitional stack limit exists
vm.condStack = append(vm.condStack, condVal)
return nil
}
// opcodeElse inverts conditional execution for other half of if/else/endif
func opcodeElse(op *parsedOpcode, vm *Engine) error {
if len(vm.condStack) < 2 {
// intial true cannot be toggled, only pushed conditionals
if len(vm.condStack) == 0 {
return ErrStackNoIf
}
switch vm.condStack[0] {
conditionalIdx := len(vm.condStack) - 1
switch vm.condStack[conditionalIdx] {
case OpCondTrue:
vm.condStack[0] = OpCondFalse
vm.condStack[conditionalIdx] = OpCondFalse
case OpCondFalse:
vm.condStack[0] = OpCondTrue
vm.condStack[conditionalIdx] = OpCondTrue
case OpCondSkip:
// value doesn't change in skip
}
@ -960,14 +954,11 @@ func opcodeElse(op *parsedOpcode, vm *Engine) error {
// opcodeEndif terminates a conditional block, removing the value from the
// conditional execution stack.
func opcodeEndif(op *parsedOpcode, vm *Engine) error {
if len(vm.condStack) < 2 {
// intial true cannot be popped, only pushed conditionals
if len(vm.condStack) == 0 {
return ErrStackNoIf
}
stk := make([]int, len(vm.condStack)-1, len(vm.condStack)-1)
copy(stk, vm.condStack[1:])
vm.condStack = stk
vm.condStack = vm.condStack[:len(vm.condStack)-1]
return nil
}