btcd/txscript/consensus_divergence_test.go
2026-04-27 08:59:07 +08:00

252 lines
6.7 KiB
Go

// Regression tests for previously-reported btcd vs. Bitcoin Core consensus
// divergences in nested P2SH witness handling.
package txscript
import (
"encoding/hex"
"errors"
"testing"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
)
// consensusBlockFlags matches the flag set used by the consensus-path
// regressions in this file.
func consensusBlockFlags() ScriptFlags {
return ScriptBip16 | ScriptVerifyWitness | ScriptStrictMultiSig
}
// buildSpendingTx creates a minimal transaction whose first input spends the
// supplied scriptSig and witness pair.
func buildSpendingTx(sigScript []byte, witness wire.TxWitness) *wire.MsgTx {
tx := &wire.MsgTx{Version: 1}
tx.AddTxIn(&wire.TxIn{
PreviousOutPoint: wire.OutPoint{Index: 0},
SignatureScript: sigScript,
Witness: witness,
Sequence: 0xffffffff,
})
tx.AddTxOut(&wire.TxOut{Value: 0, PkScript: []byte{0x51}})
return tx
}
// p2shPkScript wraps the provided redeem script in a standard P2SH output.
func p2shPkScript(redeem []byte) []byte {
h := btcutil.Hash160(redeem)
pk := make([]byte, 0, 23)
pk = append(pk, 0xa9, 0x14)
pk = append(pk, h...)
pk = append(pk, 0x87)
return pk
}
// mustExecuteErrorIs runs the VM and asserts the returned script error code.
func mustExecuteErrorIs(t *testing.T, vm *Engine, want ErrorCode,
label string) {
t.Helper()
err := vm.Execute()
if err == nil {
t.Fatalf("%s: expected rejection (code=%v), got success", label, want)
}
var se Error
if !errors.As(err, &se) {
t.Fatalf("%s: expected scriptError (code=%v), got %T: %v",
label, want, err, err)
}
if se.ErrorCode != want {
t.Fatalf("%s: expected error code %v, got %v (%v)",
label, want, se.ErrorCode, err)
}
}
func TestRegression_EmptyNestedP2SHWitness_P2WPKH(t *testing.T) {
keyHash := make([]byte, 20)
for i := range keyHash {
keyHash[i] = 0x42
}
redeem := make([]byte, 0, 22)
redeem = append(redeem, 0x00, 0x14)
redeem = append(redeem, keyHash...)
sigScript := make([]byte, 0, 23)
sigScript = append(sigScript, 0x16)
sigScript = append(sigScript, redeem...)
pkScript := p2shPkScript(redeem)
tx := buildSpendingTx(sigScript, nil)
vm, err := NewEngine(
pkScript, tx, 0, consensusBlockFlags(), nil, nil, 0, nil,
)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
mustExecuteErrorIs(t, vm, ErrWitnessProgramMismatch,
"empty-witness P2SH-P2WPKH")
}
func TestRegression_EmptyNestedP2SHWitness_P2WSH(t *testing.T) {
scriptHash := make([]byte, 32)
for i := range scriptHash {
scriptHash[i] = 0x42
}
redeem := make([]byte, 0, 34)
redeem = append(redeem, 0x00, 0x20)
redeem = append(redeem, scriptHash...)
sigScript := make([]byte, 0, 35)
sigScript = append(sigScript, 0x22)
sigScript = append(sigScript, redeem...)
pkScript := p2shPkScript(redeem)
tx := buildSpendingTx(sigScript, nil)
vm, err := NewEngine(
pkScript, tx, 0, consensusBlockFlags(), nil, nil, 0, nil,
)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
mustExecuteErrorIs(t, vm, ErrWitnessProgramEmpty,
"empty-witness P2SH-P2WSH")
}
func TestRegression_P2SHWitnessPrefix(t *testing.T) {
redeem := make([]byte, 32)
for i := range redeem {
redeem[i] = 0x61
}
// This stays push-only, but the final pushed element is not a witness
// program even though the bytes after the first push opcode resemble one.
sigScript := []byte{0x01, 0x51, 0x20}
sigScript = append(sigScript, redeem...)
pkScript := p2shPkScript(redeem)
witness := wire.TxWitness{{}}
tx := buildSpendingTx(sigScript, witness)
_, err := NewEngine(
pkScript, tx, 0, consensusBlockFlags(), nil, nil, 0, nil,
)
if err == nil {
t.Fatalf("expected NewEngine to reject non-empty witness on non-witness P2SH, got success")
}
var se Error
if !errors.As(err, &se) {
t.Fatalf("expected scriptError, got %T: %v", err, err)
}
if se.ErrorCode != ErrWitnessUnexpected {
t.Fatalf("expected ErrWitnessUnexpected, got %v (%v)",
se.ErrorCode, err)
}
}
func TestRegression_FakeNestedP2WPKHSigopCount(t *testing.T) {
redeem, err := hex.DecodeString("7551616161616161616161616161616161616161")
if err != nil {
t.Fatalf("hex decode: %v", err)
}
// The bytes after the first push look like a v0 key-hash witness program,
// but the actual redeem script is the final pushed element above.
sigScript := []byte{0x01, 0x00, 0x14}
sigScript = append(sigScript, redeem...)
pkScript := p2shPkScript(redeem)
gotSigOps := GetWitnessSigOpCount(sigScript, pkScript, nil)
if gotSigOps != 0 {
t.Fatalf("expected 0 witness sigops, got %d", gotSigOps)
}
tx := buildSpendingTx(sigScript, nil)
vm, err := NewEngine(
pkScript, tx, 0, consensusBlockFlags(), nil, nil, 0, nil,
)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
if err := vm.Execute(); err != nil {
t.Fatalf("expected legacy P2SH spend to succeed, got: %v", err)
}
}
func TestRegression_HonestNestedP2WPKHHappyPath(t *testing.T) {
keyHash := make([]byte, 20)
for i := range keyHash {
keyHash[i] = 0x99
}
redeem := make([]byte, 0, 22)
redeem = append(redeem, 0x00, 0x14)
redeem = append(redeem, keyHash...)
sigScript := make([]byte, 0, 23)
sigScript = append(sigScript, 0x16)
sigScript = append(sigScript, redeem...)
pkScript := p2shPkScript(redeem)
witness := wire.TxWitness{{0x00}, {0x00}}
tx := buildSpendingTx(sigScript, witness)
vm, err := NewEngine(
pkScript, tx, 0, consensusBlockFlags(), nil, nil, 0, nil,
)
if err != nil {
t.Fatalf("NewEngine failed: %v", err)
}
err = vm.Execute()
if err == nil {
return
}
var se Error
if errors.As(err, &se) {
if se.ErrorCode == ErrWitnessProgramMismatch ||
se.ErrorCode == ErrWitnessProgramEmpty ||
se.ErrorCode == ErrWitnessMalleatedP2SH {
t.Fatalf("fix broke the happy path: got %v", err)
}
}
}
func TestRegression_NonCanonicalNestedP2WPKHPush(t *testing.T) {
keyHash := make([]byte, 20)
for i := range keyHash {
keyHash[i] = 0x11
}
redeem := make([]byte, 0, 22)
redeem = append(redeem, 0x00, 0x14)
redeem = append(redeem, keyHash...)
// PUSHDATA1 makes this a non-canonical single push for a 22-byte redeem
// script, so nested witness recognition must reject it as malleated.
sigScript := make([]byte, 0, 24)
sigScript = append(sigScript, 0x4c, 0x16)
sigScript = append(sigScript, redeem...)
pkScript := p2shPkScript(redeem)
tx := buildSpendingTx(sigScript, nil)
_, err := NewEngine(
pkScript, tx, 0, consensusBlockFlags(), nil, nil, 0, nil,
)
if err == nil {
t.Fatalf("expected NewEngine to reject non-canonical nested P2SH witness push, got success")
}
var se Error
if !errors.As(err, &se) {
t.Fatalf("expected scriptError, got %T: %v", err, err)
}
if se.ErrorCode != ErrWitnessMalleatedP2SH {
t.Fatalf("expected ErrWitnessMalleatedP2SH, got %v (%v)",
se.ErrorCode, err)
}
}