mirror of
https://github.com/btcsuite/btcd.git
synced 2026-08-13 12:32:51 +02:00
txscript: reject OP_CODESEPARATOR in unexecuted branches for non-segwit
In this commit, we fix a policy-level divergence with Bitcoin Core when handling OP_CODESEPARATOR inside unexecuted OP_IF branches in non-segwit scripts with the ScriptVerifyConstScriptCode flag. Bitcoin Core's EvalScript (interpreter.cpp:474-476) places the SCRIPT_VERIFY_CONST_SCRIPTCODE check for OP_CODESEPARATOR before the fExec branch-execution gate, causing it to fire unconditionally on every OP_CODESEPARATOR encountered during script iteration -- even inside OP_FALSE OP_IF ... OP_ENDIF envelopes. Previously, btcd's equivalent check lived inside the opcodeCodeSeparator handler, which was never reached for opcodes in unexecuted branches due to the early return in executeOpcode that skips non-conditional opcodes when isBranchExecuting() is false. This meant a script like: OP_FALSE OP_IF OP_CODESEPARATOR OP_ENDIF <validation> would be rejected by Bitcoin Core's mempool but accepted by btcd's. The fix moves the check before the branch-execution gate in executeOpcode, matching Bitcoin Core's structure. This follows the existing pattern in btcd where isOpcodeDisabled and isOpcodeAlwaysIllegal checks already fire regardless of branch execution state. Note: SCRIPT_VERIFY_CONST_SCRIPTCODE is purely a policy flag (included in STANDARD_SCRIPT_VERIFY_FLAGS but not MANDATORY_SCRIPT_VERIFY_FLAGS), so this was not a consensus divergence. Both implementations would accept such transactions if mined in a block. Found via differential fuzzing by Bruno from bitcoinfuzz.
This commit is contained in:
parent
3eacced04e
commit
1d827e0347
2 changed files with 148 additions and 0 deletions
|
|
@ -485,6 +485,18 @@ func (vm *Engine) executeOpcode(op *opcode, data []byte) error {
|
|||
return scriptError(ErrElementTooBig, str)
|
||||
}
|
||||
|
||||
// With ScriptVerifyConstScriptCode, OP_CODESEPARATOR in non-segwit
|
||||
// script is rejected even in an unexecuted branch. This mirrors
|
||||
// Bitcoin Core's behavior where the check fires unconditionally
|
||||
// before the branch execution gate.
|
||||
if op.value == OP_CODESEPARATOR && vm.taprootCtx == nil &&
|
||||
vm.witnessProgram == nil &&
|
||||
vm.hasFlag(ScriptVerifyConstScriptCode) {
|
||||
|
||||
str := "OP_CODESEPARATOR used in non-segwit script"
|
||||
return scriptError(ErrCodeSeparator, str)
|
||||
}
|
||||
|
||||
// Nothing left to do when this is not a conditional opcode and it is
|
||||
// not in an executing branch.
|
||||
if !vm.isBranchExecuting() && !isOpcodeConditional(op.value) {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
package txscript
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
|
|
@ -426,3 +427,138 @@ func TestCheckSignatureEncoding(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCodeSepUnexecutedBranch ensures that OP_CODESEPARATOR is rejected in
|
||||
// non-segwit scripts even when inside an unexecuted OP_IF branch, when the
|
||||
// ScriptVerifyConstScriptCode flag is set. This matches Bitcoin Core's
|
||||
// behavior where the SCRIPT_VERIFY_CONST_SCRIPTCODE check fires
|
||||
// unconditionally before the branch execution gate.
|
||||
func TestCodeSepUnexecutedBranch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// A minimal transaction for script execution.
|
||||
tx := &wire.MsgTx{
|
||||
Version: 1,
|
||||
TxIn: []*wire.TxIn{{
|
||||
PreviousOutPoint: wire.OutPoint{
|
||||
Hash: chainhash.Hash([32]byte{
|
||||
0xc9, 0x97, 0xa5, 0xe5,
|
||||
0x6e, 0x10, 0x41, 0x02,
|
||||
0xfa, 0x20, 0x9c, 0x6a,
|
||||
0x85, 0x2d, 0xd9, 0x06,
|
||||
0x60, 0xa2, 0x0b, 0x2d,
|
||||
0x9c, 0x35, 0x24, 0x23,
|
||||
0xed, 0xce, 0x25, 0x85,
|
||||
0x7f, 0xcd, 0x37, 0x04,
|
||||
}),
|
||||
Index: 0,
|
||||
},
|
||||
SignatureScript: mustParseShortForm("TRUE"),
|
||||
Sequence: 4294967295,
|
||||
}},
|
||||
TxOut: []*wire.TxOut{{
|
||||
Value: 1000000000,
|
||||
PkScript: nil,
|
||||
}},
|
||||
LockTime: 0,
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
script string
|
||||
flags ScriptFlags
|
||||
segwit bool
|
||||
wantErr bool
|
||||
errCode ErrorCode
|
||||
}{
|
||||
{
|
||||
// OP_CODESEPARATOR inside an unexecuted branch with
|
||||
// the const scriptcode flag set should be rejected.
|
||||
name: "codesep in unexecuted IF with const scriptcode",
|
||||
script: "0 IF CODESEPARATOR ENDIF TRUE",
|
||||
flags: ScriptVerifyConstScriptCode,
|
||||
wantErr: true,
|
||||
errCode: ErrCodeSeparator,
|
||||
},
|
||||
{
|
||||
// Without the flag, OP_CODESEPARATOR in an unexecuted
|
||||
// branch should be fine (consensus behavior).
|
||||
name: "codesep in unexecuted IF without const scriptcode",
|
||||
script: "0 IF CODESEPARATOR ENDIF TRUE",
|
||||
flags: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
// OP_CODESEPARATOR in an executed branch with the
|
||||
// const scriptcode flag should also be rejected.
|
||||
name: "codesep in executed branch with const scriptcode",
|
||||
script: "CODESEPARATOR TRUE",
|
||||
flags: ScriptVerifyConstScriptCode,
|
||||
wantErr: true,
|
||||
errCode: ErrCodeSeparator,
|
||||
},
|
||||
{
|
||||
// Nested unexecuted branches should still be caught.
|
||||
name: "codesep in nested unexecuted IF with const scriptcode",
|
||||
script: "0 IF 1 IF CODESEPARATOR ENDIF ENDIF TRUE",
|
||||
flags: ScriptVerifyConstScriptCode,
|
||||
wantErr: true,
|
||||
errCode: ErrCodeSeparator,
|
||||
},
|
||||
{
|
||||
// OP_CODESEPARATOR in a segwit P2WSH witness script
|
||||
// should succeed even with const scriptcode, since
|
||||
// the flag only applies to non-segwit scripts.
|
||||
name: "codesep in segwit P2WSH with const scriptcode",
|
||||
script: "CODESEPARATOR TRUE",
|
||||
flags: ScriptVerifyConstScriptCode | ScriptVerifyWitness | ScriptBip16,
|
||||
segwit: true,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
scriptBytes := mustParseShortForm(test.script)
|
||||
|
||||
pkScript := scriptBytes
|
||||
testTx := tx
|
||||
if test.segwit {
|
||||
hash := sha256.Sum256(scriptBytes)
|
||||
pkScript, _ = NewScriptBuilder().
|
||||
AddOp(OP_0).AddData(hash[:]).Script()
|
||||
testTx = tx.Copy()
|
||||
testTx.TxIn[0].SignatureScript = nil
|
||||
testTx.TxIn[0].Witness = wire.TxWitness{
|
||||
scriptBytes,
|
||||
}
|
||||
}
|
||||
|
||||
vm, err := NewEngine(
|
||||
pkScript, testTx, 0, test.flags, nil, nil,
|
||||
-1, nil,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create engine: %v", err)
|
||||
}
|
||||
|
||||
err = vm.Execute()
|
||||
|
||||
switch {
|
||||
case test.wantErr && err == nil:
|
||||
t.Fatal("expected error but execution " +
|
||||
"succeeded")
|
||||
|
||||
case !test.wantErr && err != nil:
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
|
||||
case test.wantErr && err != nil:
|
||||
if !IsErrorCode(err, test.errCode) {
|
||||
t.Fatalf("expected error code "+
|
||||
"%v, got: %v",
|
||||
test.errCode, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue