Skip to content

Commit

Permalink
txscript: Optimize conditional execution mem usage.
Browse files Browse the repository at this point in the history
The existing implementation to handle conditional execution makes use of
a stack to track the state of each nested conditional.  It is already
fairly efficient in terms of execution costs since it only considers the
most recent conditional stack entry and makes use of pushing OpCondSkip
to essentially track the nesting depth in unexecuted branches, however,
using a stack is less efficient in terms of memory usage than is
actually necessary since there is no need to use a stack at all given
that all that is really needed to provide the necessary behavior is the
current conditional nesting depth and the depth at which branch
execution was disabled (if it has been disabled).

Given the above, this optimizes the txscript conditional execution logic
by replacing the condition stack with two int32 fields to track the
aforementioned cases and updates the conditional execution opcode and
logic accordingly.
  • Loading branch information
davecgh committed Dec 30, 2019
1 parent 241afc8 commit aad3f7c
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 39 deletions.
62 changes: 53 additions & 9 deletions txscript/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ const (

// MaxScriptSize is the maximum allowed length of a raw script.
MaxScriptSize = 16384

// noCondDisableDepth is the nesting depth which indicates that no
// conditional opcodes have been encountered that cause the current
// execution state to be disabled.
noCondDisableDepth = -1
)

// halforder is used to tame ECDSA malleability (see BIP0062).
Expand Down Expand Up @@ -129,9 +134,6 @@ type Engine struct {
// astack is the alternate data stack the various opcodes push and pop data
// to and from during execution.
//
// condStack tracks the conditional execution state with support for
// multiple nested conditional execution opcodes.
//
// numOps tracks the total number of non-push operations in a script and is
// primarily used to enforce maximum limits.
scripts [][]byte
Expand All @@ -142,8 +144,52 @@ type Engine struct {
savedFirstStack [][]byte
dstack stack
astack stack
condStack []int
numOps int

// The following fields keep track of the current conditional execution
// state of the engine with support for multiple nested conditional
// execution opcodes.
//
// Each time a conditional opcode is encountered the conditional nesting
// depth is incremented. This is the case even in an unexecuted branch so
// proper nesting is maintained. On the other hand, when a conditional
// branch is terminated, the nesting depth is decremented.
//
// Whenever one of the aforementioned conditional opcodes that indicates
// branch execution needs to be disabled is encountered, execution of any
// opcodes in that branch, and any nested conditional branches, is disabled
// until the disabled conditional branch is terminated.
//
// In other words, only the current nesting depth and the nesting depth that
// caused branch execution to be disabled needs to be tracked and execution
// becomes enabled again once the nesting depth is reduced to that depth.
//
// For example, consider the following script and nesting depth diagram:
//
// TRUE IF FALSE IF <opcodes> TRUE IF <opcodes> ENDIF ENDIF ENDIF <opcodes>
// | | | | | | | |
// | | | ----depth 3---- | | |
// | | ----------depth 2---------------------- | |
// | -------------------depth 1---------------------------- |
// --------------------------depth 0-------------------------------------
//
// The first IF is TRUE, so branch execution is unchanged and the current
// nesting depth is increased from 0 to 1. The second IF is FALSE, so
// branch execution is disabled at nesting depth 1 and the current nesting
// depth is increased from 1 to 2. Branch execution is already disabled for
// the third IF, so its value has no effect, but the current nesting depth
// is increased from 2 to 3. The first ENDIF reduces the current nesting
// depth from 3 to 2. The second ENDIF reduces the current nesting depth
// from 2 to 1 and since the branch execution was disabled at depth 1,
// branch execution is enabled again. The third ENDIF reduces the nesting
// depth from 1 to 0.
//
// condNestDepth is the current conditional execution nesting depth.
//
// condDisableDepth is the nesting depth that caused conditional branch
// execution to be disabled, or the value `noCondDisableDepth`.
condNestDepth int32
condDisableDepth int32
}

// hasFlag returns whether the script engine instance has the passed flag set.
Expand All @@ -156,10 +202,7 @@ func (vm *Engine) hasFlag(flag ScriptFlags) bool {
// 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
return vm.condDisableDepth == noCondDisableDepth
}

// isOpcodeDisabled returns whether or not the opcode is disabled and thus is
Expand Down Expand Up @@ -472,7 +515,7 @@ func (vm *Engine) Step() (done bool, err error) {
vm.opcodeIdx++
if vm.tokenizer.Done() {
// Illegal to have a conditional that straddles two scripts.
if len(vm.condStack) != 0 {
if vm.condNestDepth != 0 {
return false, scriptError(ErrUnbalancedConditional,
"end of script reached in conditional execution")
}
Expand Down Expand Up @@ -957,6 +1000,7 @@ func NewEngine(scriptPubKey []byte, tx *wire.MsgTx, txIdx int, flags ScriptFlags

vm.tx = *tx
vm.txIdx = txIdx
vm.condDisableDepth = noCondDisableDepth

return &vm, nil
}
74 changes: 44 additions & 30 deletions txscript/opcode.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,8 @@ const (
)

// Conditional execution constants.
//
// Deprecated: This will be removed in the next major version bump.
const (
OpCondFalse = 0
OpCondTrue = 1
Expand Down Expand Up @@ -782,21 +784,23 @@ func opcodeNop(op *opcode, data []byte, vm *Engine) error {
// it is on a non-executing branch so proper nesting is maintained.
//
// Data stack transformation: [... bool] -> [...]
// Conditional stack transformation: [...] -> [... OpCondValue]
func opcodeIf(op *opcode, data []byte, vm *Engine) error {
condVal := OpCondFalse
if vm.isBranchExecuting() {
ok, err := vm.dstack.PopBool()
if err != nil {
return err
}
if ok {
condVal = OpCondTrue
if !ok {
// Branch execution is being disabled when it was not previously, so
// mark the current conditional nesting depth as the depth at which
// it was disabled.
vm.condDisableDepth = vm.condNestDepth
}
} else {
condVal = OpCondSkip
}
vm.condStack = append(vm.condStack, condVal)

// Increment the conditional execution nesting depth to account for the
// conditional opcode.
vm.condNestDepth++
return nil
}

Expand All @@ -815,45 +819,51 @@ func opcodeIf(op *opcode, data []byte, vm *Engine) error {
// it is on a non-executing branch so proper nesting is maintained.
//
// Data stack transformation: [... bool] -> [...]
// Conditional stack transformation: [...] -> [... OpCondValue]
func opcodeNotIf(op *opcode, data []byte, vm *Engine) error {
condVal := OpCondFalse
if vm.isBranchExecuting() {
ok, err := vm.dstack.PopBool()
if err != nil {
return err
}
if !ok {
condVal = OpCondTrue
if ok {
// Branch execution is being disabled when it was not previously, so
// mark the current conditional nesting depth as the depth at which
// it was disabled.
vm.condDisableDepth = vm.condNestDepth
}
} else {
condVal = OpCondSkip
}
vm.condStack = append(vm.condStack, condVal)

// Increment the conditional execution nesting depth to account for the
// conditional opcode.
vm.condNestDepth++
return nil
}

// opcodeElse inverts conditional execution for other half of if/else/endif.
//
// An error is returned if there has not already been a matching OP_IF.
//
// Conditional stack transformation: [... OpCondValue] -> [... !OpCondValue]
func opcodeElse(op *opcode, data []byte, vm *Engine) error {
if len(vm.condStack) == 0 {
if vm.condNestDepth == 0 {
str := fmt.Sprintf("encountered opcode %s with no matching "+
"opcode to begin conditional execution", op.name)
return scriptError(ErrUnbalancedConditional, str)
}

conditionalIdx := len(vm.condStack) - 1
switch vm.condStack[conditionalIdx] {
case OpCondTrue:
vm.condStack[conditionalIdx] = OpCondFalse
case OpCondFalse:
vm.condStack[conditionalIdx] = OpCondTrue
case OpCondSkip:
// Value doesn't change in skip since it indicates this opcode
// is nested in a non-executed branch.
conditionalDepth := vm.condNestDepth - 1
switch {
case vm.isBranchExecuting():
// Branch execution is being disabled when it was not previously, so
// mark the most recent conditional nesting depth as the depth at which
// it was disabled.
vm.condDisableDepth = conditionalDepth

case vm.condDisableDepth == conditionalDepth:
// Enable branch execution when it was previously disabled as a result
// of the opcode at the depth that is being toggled.
vm.condDisableDepth = noCondDisableDepth

default:
// No effect since this opcode is nested in a non-executed branch.
}
return nil
}
Expand All @@ -862,16 +872,20 @@ func opcodeElse(op *opcode, data []byte, vm *Engine) error {
// conditional execution stack.
//
// An error is returned if there has not already been a matching OP_IF.
//
// Conditional stack transformation: [... OpCondValue] -> [...]
func opcodeEndif(op *opcode, data []byte, vm *Engine) error {
if len(vm.condStack) == 0 {
if vm.condNestDepth == 0 {
str := fmt.Sprintf("encountered opcode %s with no matching "+
"opcode to begin conditional execution", op.name)
return scriptError(ErrUnbalancedConditional, str)
}

vm.condStack = vm.condStack[:len(vm.condStack)-1]
// Decrement the conditional execution nesting depth and enable branch
// execution if it was previously disabled as a result of the opcode at
// that depth.
vm.condNestDepth--
if vm.condDisableDepth == vm.condNestDepth {
vm.condDisableDepth = noCondDisableDepth
}
return nil
}

Expand Down

0 comments on commit aad3f7c

Please sign in to comment.