txscript: support latest Core tx_invalid semantics
What changed, and why it matters
This commit only changes a test file in btcd. It adds helper functions so that btcd's transaction-script test runner can understand newer Bitcoin Core test vectors marked 'BADTX' (context-free invalid transactions). No production code is modified, so this cannot directly affect live Bitcoin nodes or wallets.
No security action required. Treat as routine test maintenance. Reviewers may optionally verify that checkTxSanity's logic matches blockchain package equivalents, but this is only for test correctness.
Security signals we found
No production code paths changed
Test-only reference-vector alignment with upstream Bitcoin Core semantics
Adds context-free sanity checks duplicated from blockchain package to avoid import cycle in tests
Evidence from the diff
The diff modifies txscript/reference_test.go. It introduces parseTxInvalidTestFlags to parse the ‘BADTX’ marker in Bitcoin Core’s tx_invalid.json vectors, checkTxSanity to perform context-free transaction validation inside the test package without creating an import cycle with blockchain, and a small unit test. The production test loop now routes BADTX vectors through checkTxSanity instead of script execution. This is a test-infrastructure alignment patch.
Changed components
txscript/reference_test.goInspect captured patch +129 / −1
diff --git a/txscript/reference_test.go b/txscript/reference_test.go
index 9f421f0..de9e109 100644
--- a/txscript/reference_test.go
+++ b/txscript/reference_test.go
@@ -22,6 +22,12 @@ import (
"github.com/btcsuite/btcd/wire"
)
+const (
+ // maxTxBaseSize mirrors blockchain.MaxBlockBaseSize so checkTxSanity can
+ // enforce the same stripped-size limit without importing blockchain.
+ maxTxBaseSize = 1000000
+)
+
const (
scriptTestSigScriptOffset = iota
scriptTestPkScriptOffset
@@ -263,6 +269,119 @@ func parseTxValidTestFlags(flagStr string) (ScriptFlags, error) {
return allScriptFlags &^ excluded, nil
}
+// parseTxInvalidTestFlags parses the tx_invalid flag field. Those vectors
+// still use the legacy included-flags convention plus the special BADTX marker
+// for context-free transaction-sanity failures.
+func parseTxInvalidTestFlags(flagStr string) (ScriptFlags, bool, error) {
+ badTx := false
+
+ parts := strings.Split(flagStr, ",")
+ filtered := make([]string, 0, len(parts))
+ for _, part := range parts {
+ if part == "BADTX" {
+ badTx = true
+ continue
+ }
+
+ filtered = append(filtered, part)
+ }
+
+ flags, err := parseScriptFlags(strings.Join(filtered, ","))
+ if err != nil {
+ return 0, false, err
+ }
+
+ return flags, badTx, nil
+}
+
+// checkTxSanity performs the subset of context-free transaction checks needed
+// by the tx_invalid BADTX vectors without importing blockchain and creating an
+// import cycle from txscript tests.
+func checkTxSanity(tx *btcutil.Tx) error {
+ msgTx := tx.MsgTx()
+
+ if len(msgTx.TxIn) == 0 {
+ return errors.New("transaction has no inputs")
+ }
+ if len(msgTx.TxOut) == 0 {
+ return errors.New("transaction has no outputs")
+ }
+ if msgTx.SerializeSizeStripped() > maxTxBaseSize {
+ return errors.New("transaction exceeds max base size")
+ }
+
+ var totalSatoshi int64
+ for _, txOut := range msgTx.TxOut {
+ satoshi := txOut.Value
+ if satoshi < 0 {
+ return errors.New("negative output value")
+ }
+ if satoshi > btcutil.MaxSatoshi {
+ return errors.New("output exceeds max satoshi")
+ }
+
+ totalSatoshi += satoshi
+ if totalSatoshi < 0 || totalSatoshi > btcutil.MaxSatoshi {
+ return errors.New("total output exceeds max satoshi")
+ }
+ }
+
+ seen := make(map[wire.OutPoint]struct{}, len(msgTx.TxIn))
+ for _, txIn := range msgTx.TxIn {
+ if _, ok := seen[txIn.PreviousOutPoint]; ok {
+ return errors.New("duplicate inputs")
+ }
+ seen[txIn.PreviousOutPoint] = struct{}{}
+ }
+
+ if isCoinBaseTx(tx) {
+ slen := len(msgTx.TxIn[0].SignatureScript)
+ if slen < 2 || slen > 100 {
+ return errors.New("coinbase script length out of range")
+ }
+ return nil
+ }
+
+ for _, txIn := range msgTx.TxIn {
+ if isNullOutpoint(&txIn.PreviousOutPoint) {
+ return errors.New("null outpoint in non-coinbase tx")
+ }
+ }
+
+ return nil
+}
+
+func isCoinBaseTx(tx *btcutil.Tx) bool {
+ msgTx := tx.MsgTx()
+ if len(msgTx.TxIn) != 1 {
+ return false
+ }
+
+ return isNullOutpoint(&msgTx.TxIn[0].PreviousOutPoint)
+}
+
+func isNullOutpoint(outpoint *wire.OutPoint) bool {
+ return outpoint.Index == ^uint32(0) && outpoint.Hash == (chainhash.Hash{})
+}
+
+func TestCheckTxSanityTooBig(t *testing.T) {
+ t.Parallel()
+
+ tx := wire.NewMsgTx(wire.TxVersion)
+ outpoint := wire.OutPoint{
+ Hash: chainhash.Hash{0: 1},
+ Index: 0,
+ }
+ tx.AddTxIn(wire.NewTxIn(
+ &outpoint, bytes.Repeat([]byte{0x01}, maxTxBaseSize), nil,
+ ))
+ tx.AddTxOut(wire.NewTxOut(0, nil))
+
+ if err := checkTxSanity(btcutil.NewTx(tx)); err == nil {
+ t.Fatal("expected oversized transaction to fail sanity checks")
+ }
+}
+
// hasTaprootScriptTest returns whether the reference script test is one of the
// newer taproot cases embedded in script_tests.json. Those vectors rely on
// Bitcoin Core-specific placeholder macros, while btcd covers taproot via the
@@ -667,11 +786,20 @@ testloop:
continue
}
- flags, err := parseScriptFlags(verifyFlags)
+ flags, badTx, err := parseTxInvalidTestFlags(verifyFlags)
if err != nil {
t.Errorf("bad test %d: %v", i, err)
continue
}
+ if badTx {
+ if err := checkTxSanity(tx); err != nil {
+ continue
+ }
+
+ t.Errorf("test (%d:%v) passed sanity when should fail",
+ i, test)
+ continue
+ }
prevOutFetcher := NewMultiPrevOutFetcher(nil)
for j, iinput := range inputs {
Why this scored 13/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.