diff --git a/txscript/engine.go b/txscript/engine.go index 0fa5cc489..ce130309d 100644 --- a/txscript/engine.go +++ b/txscript/engine.go @@ -84,6 +84,10 @@ const ( // ScriptVerifyDiscourageUpgradeableWitnessProgram makes witness // program with versions 2-16 non-standard. ScriptVerifyDiscourageUpgradeableWitnessProgram + + // ScriptVerifyMinimalIf makes a script with an OP_IF/OP_NOTIF whose + // operand is anything other than empty vector or [0x01] non-standard. + ScriptVerifyMinimalIf ) const ( diff --git a/txscript/opcode.go b/txscript/opcode.go index 327f7639f..c4417dbe5 100644 --- a/txscript/opcode.go +++ b/txscript/opcode.go @@ -918,6 +918,47 @@ func opcodeNop(op *parsedOpcode, vm *Engine) error { return nil } +// popIfBool enforces the "minimal if" policy during script execution if the +// particular flag is set. If so, in order to eliminate an additional source +// of nuisance malleability, post-segwit for version 0 witness programs, we now +// require the following: for OP_IF and OP_NOT_IF, the top stack item MUST +// either be an empty byte slice, or [0x01]. Otherwise, the item at the top of +// the stack will be popped and interpreted as a boolean. +func popIfBool(vm *Engine) (bool, error) { + // When not in witness execution mode, not executing a v0 witness + // program, or the minimal if flag isn't set pop the top stack item as + // a normal bool. + if !vm.witness || !vm.hasFlag(ScriptVerifyMinimalIf) { + return vm.dstack.PopBool() + } + + // At this point, a v0 witness program is being executed and the minimal + // if flag is set, so enforce additional constraints on the top stack + // item. + so, err := vm.dstack.PopByteArray() + if err != nil { + return false, err + } + + // The top element MUST have a length of at least one. + if len(so) > 1 { + str := fmt.Sprintf("minimal if is active, top element MUST "+ + "have a length of at least, instead length is %v", + len(so)) + return false, scriptError(ErrMinimalIf, str) + } + + // Additionally, if the length is one, then the value MUST be 0x01. + if len(so) == 1 && so[0] != 0x01 { + str := fmt.Sprintf("minimal if is active, top stack item MUST "+ + "be an empty byte array or 0x01, is instead: %v", + so[0]) + return false, scriptError(ErrMinimalIf, str) + } + + return asBool(so), nil +} + // opcodeIf treats the top item on the data stack as a boolean and removes it. // // An appropriate entry is added to the conditional stack depending on whether @@ -936,10 +977,11 @@ func opcodeNop(op *parsedOpcode, vm *Engine) error { func opcodeIf(op *parsedOpcode, vm *Engine) error { condVal := OpCondFalse if vm.isBranchExecuting() { - ok, err := vm.dstack.PopBool() + ok, err := popIfBool(vm) if err != nil { return err } + if ok { condVal = OpCondTrue } @@ -969,10 +1011,11 @@ func opcodeIf(op *parsedOpcode, vm *Engine) error { func opcodeNotIf(op *parsedOpcode, vm *Engine) error { condVal := OpCondFalse if vm.isBranchExecuting() { - ok, err := vm.dstack.PopBool() + ok, err := popIfBool(vm) if err != nil { return err } + if !ok { condVal = OpCondTrue }