What changed, and why it matters
This commit only adds a new integration test file for the recently added Pay-to-Anchor (P2A) feature in btcd. It does not change any production code, consensus rules, or network behavior. The test verifies that P2A outputs can be created, spent without a signature, and that policy rules reject invalid variations (non-empty witness and sub-dust outputs). There is no security vulnerability here.
No action required. This is a test-only addition and does not introduce or fix a security issue.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff creates integration/p2a_test.go, a Go test guarded by the rpctest build tag. It sets up a simnet harness with –rejectnonstd, creates a P2A output via btcutil.NewAddressPayToAnchor and txscript.PayToAddrScript, mines it, then spends it with an empty signature script. It also includes two sub-tests asserting mempool rejection of (1) a P2A spend with a non-empty witness and (2) a P2A funding output below the BIP 433 dust threshold of 240 satoshis. No library, daemon, or consensus code is modified.
Changed components
integration/p2a_test.goInspect captured patch +199 / −0
diff --git a/integration/p2a_test.go b/integration/p2a_test.go
new file mode 100644
index 0000000..8ca69d6
--- /dev/null
+++ b/integration/p2a_test.go
@@ -0,0 +1,199 @@
+//go:build rpctest
+// +build rpctest
+
+package integration
+
+import (
+ "testing"
+
+ "github.com/btcsuite/btcd/btcutil"
+ "github.com/btcsuite/btcd/chaincfg"
+ "github.com/btcsuite/btcd/integration/rpctest"
+ "github.com/btcsuite/btcd/txscript"
+ "github.com/btcsuite/btcd/wire"
+)
+
+// TestPayToAnchorSimple tests creating and spending P2A outputs.
+func TestPayToAnchorSimple(t *testing.T) {
+ t.Parallel()
+
+ // Integration tests require a full harness setup which is
+ // resource-intensive.
+ if testing.Short() {
+ t.Skip("Skipping P2A integration test in short mode")
+ }
+
+ // Create a btcd instance for testing P2A functionality in a controlled
+ // environment. The simnet harness accepts non-standard transactions by
+ // default, but the sub-dust and non-empty-witness cases below rely on
+ // standardness checks running, so we start the node with
+ // --rejectnonstd.
+ btcdCfg := []string{"--rejectnonstd"}
+ harness, err := rpctest.New(
+ &chaincfg.SimNetParams, nil, btcdCfg, "",
+ )
+ if err != nil {
+ t.Fatalf("unable to create test harness: %v", err)
+ }
+ defer harness.TearDown()
+
+ // Initialize the test harness with mining enabled to confirm
+ // transactions.
+ err = harness.SetUp(true, 25)
+ if err != nil {
+ t.Fatalf("unable to setup test harness: %v", err)
+ }
+
+ // Create a P2A output using the helper to get a P2A address. This
+ // ensures we're using the same P2A script generation logic.
+ p2aAddr, err := btcutil.NewAddressPayToAnchor(&chaincfg.SimNetParams)
+ if err != nil {
+ t.Fatalf("unable to create P2A address: %v", err)
+ }
+ p2aPkScript, err := txscript.PayToAddrScript(p2aAddr)
+ if err != nil {
+ t.Fatalf("unable to build P2A pkScript: %v", err)
+ }
+
+ // Use the harness to create a transaction that sends to the P2A
+ // address. This handles all the UTXO selection and signing for us.
+ amount := btcutil.Amount(10_000)
+ createP2ATxHash, err := harness.SendOutputs([]*wire.TxOut{
+ wire.NewTxOut(int64(amount), p2aPkScript),
+ }, 10)
+ if err != nil {
+ t.Fatalf("unable to send P2A creation transaction: %v", err)
+ }
+
+ // Mine a block to confirm the P2A creation.
+ blockHashes, err := harness.Client.Generate(1)
+ if err != nil {
+ t.Fatalf("unable to generate block: %v", err)
+ }
+ if len(blockHashes) != 1 {
+ t.Fatalf("expected 1 block hash, got %d", len(blockHashes))
+ }
+
+ // Test spending the P2A output to verify it works as anyone-can-spend.
+ spendP2ATx := wire.NewMsgTx(wire.TxVersion)
+
+ // Reference the P2A output we just created.
+ p2aOutpoint := wire.NewOutPoint(createP2ATxHash, 0)
+ p2aInput := wire.NewTxIn(p2aOutpoint, nil, nil)
+
+ // P2A outputs are designed to be spent without signatures for CPFP fee
+ // bumping. The signature script is completely empty for P2A outputs.
+ p2aInput.SignatureScript = []byte{}
+ spendP2ATx.AddTxIn(p2aInput)
+
+ // Send the P2A funds to a regular address.
+ spendAddr, err := harness.NewAddress()
+ if err != nil {
+ t.Fatalf("unable to get spend address: %v", err)
+ }
+ spendScript, err := txscript.PayToAddrScript(spendAddr)
+ if err != nil {
+ t.Fatalf("unable to create spend script: %v", err)
+ }
+
+ // Deduct a small fee from the P2A output value.
+ spendOut := wire.NewTxOut(int64(amount-100), spendScript)
+ spendP2ATx.AddTxOut(spendOut)
+
+ // Broadcast the spend transaction to verify network acceptance. P2A
+ // outputs are witness programs and are validated through the normal
+ // transaction validation path in the mempool and consensus.
+ spendTxHash, err := harness.Client.SendRawTransaction(spendP2ATx, true)
+ if err != nil {
+ t.Fatalf("unable to send P2A spend transaction: %v", err)
+ }
+
+ // Mine a block to confirm the spend.
+ blockHashes, err = harness.Client.Generate(1)
+ if err != nil {
+ t.Fatalf("unable to generate block after spend: %v", err)
+ }
+
+ // Ensure the spend transaction was actually mined to prove full P2A
+ // support.
+ //
+ block, err := harness.Client.GetBlock(blockHashes[0])
+ if err != nil {
+ t.Fatalf("unable to get block: %v", err)
+ }
+
+ // Confirm the spend transaction exists in the confirmed block.
+ found := false
+ for _, tx := range block.Transactions {
+ txHash := tx.TxHash()
+ if txHash.IsEqual(spendTxHash) {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Errorf("P2A spend transaction not found in block")
+ }
+
+ t.Logf("Successfully created P2A output in tx %v and spent it in tx %v",
+ createP2ATxHash, spendTxHash)
+
+ // As a sanity check on the policy gates, also confirm that a P2A
+ // spend carrying witness data is rejected by the mempool, and that a
+ // sub-dust P2A funding transaction is rejected as non-standard.
+ t.Run("non-empty witness rejected", func(t *testing.T) {
+ // Fund a fresh P2A output we can attempt to spend.
+ fundTxHash, err := harness.SendOutputs([]*wire.TxOut{
+ wire.NewTxOut(int64(amount), p2aPkScript),
+ }, 10)
+ if err != nil {
+ t.Fatalf("unable to fund second P2A output: %v", err)
+ }
+ if _, err := harness.Client.Generate(1); err != nil {
+ t.Fatalf("unable to mine block: %v", err)
+ }
+
+ // Build a spend that attaches a non-empty witness, which must
+ // be rejected since P2A is anyone-can-spend with empty witness
+ // only.
+ badTx := wire.NewMsgTx(wire.TxVersion)
+ outpoint := wire.NewOutPoint(fundTxHash, 0)
+ input := wire.NewTxIn(outpoint, nil, nil)
+ input.Witness = wire.TxWitness{{0x00}}
+ badTx.AddTxIn(input)
+ badTx.AddTxOut(wire.NewTxOut(int64(amount-100), spendScript))
+
+ if _, err := harness.Client.SendRawTransaction(
+ badTx, true,
+ ); err == nil {
+
+ t.Fatal("P2A spend with non-empty witness was accepted; " +
+ "expected mempool rejection")
+ }
+ })
+
+ t.Run("sub-dust output rejected", func(t *testing.T) {
+ // Below the BIP 433 fixed 240-sat P2A dust threshold.
+ const subDust = 100
+
+ // Build a transaction paying sub-dust to P2A from a new
+ // harness-funded input. CreateTransaction performs the
+ // signing for us; the dust gate fires when we try to relay it.
+ dustTx, err := harness.CreateTransaction(
+ []*wire.TxOut{wire.NewTxOut(subDust, p2aPkScript)},
+ 10, true,
+ )
+ if err != nil {
+ t.Fatalf("unable to build sub-dust funding tx: %v", err)
+ }
+
+ if _, err := harness.Client.SendRawTransaction(
+ dustTx, true,
+ ); err == nil {
+
+ t.Fatal("sub-dust P2A output was accepted; expected " +
+ "mempool dust rejection")
+ }
+ })
+}
+
Why this scored 15/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.