Skip to content

Conversation

@Roasbeef
Copy link
Member

In this PR, we add initial recognition of the new P2A output type. It actually overloads the existing Taproot witness version, but with a distinct witness program. We adopt the proposed standardness rules to make only spends without any witness data standard. Utilities to parse the script, make addresses, etc - have also been added.

Note that this doesn't include the logic to support "ephemeral dust", which enables zero fee and zero value outputs under certain conditions.

A replace dir is used as we modified btcutil in this PR.

@coveralls
Copy link

coveralls commented Sep 27, 2025

Pull Request Test Coverage Report for Build 20117557279

Details

  • 101 of 111 (90.99%) changed or added relevant lines in 6 files are covered.
  • 127 unchanged lines in 8 files lost coverage.
  • Overall coverage increased (+0.4%) to 55.171%

Changes Missing Coverage Covered Lines Changed/Added Lines %
txscript/pkscript.go 1 2 50.0%
txscript/sign.go 0 1 0.0%
mempool/policy.go 28 30 93.33%
txscript/standard.go 24 30 80.0%
Files with Coverage Reduction New Missed Lines %
btcutil/bech32/bech32.go 1 99.61%
btcutil/bloom/filter.go 2 96.84%
btcutil/gcs/gcs.go 2 81.25%
txscript/taproot.go 2 97.74%
database/ffldb/blockio.go 4 88.81%
peer/peer.go 15 71.1%
rpcclient/infrastructure.go 38 47.99%
btcec/schnorr/musig2/context.go 63 80.17%
Totals Coverage Status
Change from base Build 20117517710: 0.4%
Covered Lines: 31296
Relevant Lines: 56725

💛 - Coveralls

Copy link
Collaborator

@sputn1ck sputn1ck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

return nil, fmt.Errorf("unable to parse address: %v", err)
}

// P2A scripts don't have addresses.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a stray line, but ExtractPkScriptAddrs should handle that. Will add a test for this.

@Roasbeef Roasbeef requested a review from yyforyongyu October 27, 2025 23:06
@Roasbeef Roasbeef changed the base branch from master to v3-tx-standard December 11, 2025 00:18
nilAddrErrStr)
}
return payToWitnessTaprootScript(addr.ScriptAddress())
case *btcutil.AddressPayToAnchor:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

btcutil.AddressPayToAnchor isn't introduced until commit 3f99cfe0a9b0ed9a33e0c11136127e8b6e2906ea.

So this commit doesn't build.

},
{
name: "invalid - wrong version",
script: []byte{OP_0, OP_DATA_2, 0x4e, 0x73},
Copy link
Collaborator

@kcalvinalvin kcalvinalvin Dec 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a question: is OP_1 a version? Curious as to the test's name

nvm figured it out


go 1.23.2

replace github.com/btcsuite/btcd/btcutil => ./btcutil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we ok with this or is this temporary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is temp. It's needed as we ref the newly added addr type in txscript/standard.go.

#1825 will resolve this circualr dep module issue. After this is merged, I'll be ale to update the module dep, then remove this.

otherNet = &chaincfg.TestNet3Params
}
if addr.IsForNet(otherNet) {
t.Errorf("IsForNet(other) = true, want false")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this testing for? Why do we want to test "othernet-ness"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just that IsForNet properly detects if an address isn't for the target network.

I agree it's a trivial test, tbh I did it just so the test coverage reported is higher.

t.Errorf("IsForNet(other) = true, want false")
}

expectedScript := []byte{0x51, 0x02, 0x4e, 0x73}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's better if I we declare something like the declaration in txscript/standard.go

var PayToAnchorScript = []byte{OP_1, OP_DATA_2, 0x4e, 0x73}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the issue with that again is the module issue I brought up which will be addressed by #1825. This is defined, but the btcutil module wasn't updated (as this was only added in this PR). So I had to add yet another replace to amek things work.

// script bytes: OP_1 <0x4e73>.
func (a *AddressPayToAnchor) ScriptAddress() []byte {
// Return the fixed P2A script bytes.
return []byte{0x51, 0x02, 0x4e, 0x73}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah here too. Would be better if we declared some variable and referred to it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually this won't work, as it causes an import cycle. We can't import txscript from btcutil. The other changed worked at it was in btcutil_test.

In this commit, we modify the execution logic, such that the P2A witness
program doesn't appear as a unknown witness program version. For
execution, we just make sure the script passes.
P2A is standard if the witness and the sigscript is empty. We make a
smol refactor to be able to write a unit test for the input
standardness.
We make sure it'll be accepted into the mempool, and can be spent
without a witness.
@Roasbeef
Copy link
Member Author

@kcalvinalvin PTAL

@Roasbeef Roasbeef changed the base branch from v3-tx-standard to master December 18, 2025 02:15
// - Regtest: bcrt1pfeesnyr2tx
// - Simnet: sb1pfeesxv0pfa
func (a *AddressPayToAnchor) EncodeAddress() string {
// For unknown networks, generate the address from the anchor data.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird comment. Should remove this.

require.Equal(t, tt.wantAddress, addr.String())
require.True(t, addr.IsForNet(tt.net))

// Verify it's not for a different network.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The require.True() does cover this and we don't need this extra check but it's ok.

Claude could be better but heh.

if IsPayToAnchorScript(pkScript) {
// P2A scripts don't have traditional addresses and require no
// signatures (anyone can spend them).
return PayToAnchorTy, nil, 0, nil
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be returning the address here because of this. 2eadd59#r2390989822

class, PayToAnchorTy)
}

if len(addrs) != 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd want 1 address since they P2A has an address

class.String())
}

// Test that regular taproot scripts are still recognized as
Copy link
Collaborator

@kcalvinalvin kcalvinalvin Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Weird thing to test for here but ok

}

// LookupEntry returns the entry for a given outpoint.
func (m *mockUtxoView) LookupEntry(op wire.OutPoint) utxoEntry {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like these mock utxo entry and mock utxoview is only needed for the tests but it really doesn't need this abstraction. Not a blocker but yeah I think simpler is better.

diff --git a/mempool/policy.go b/mempool/policy.go
index 798d6686..46339d33 100644
--- a/mempool/policy.go
+++ b/mempool/policy.go
@@ -79,29 +79,6 @@ func calcMinRequiredTxRelayFee(serializedSize int64, minRelayTxFee btcutil.Amoun
        return minFee
 }
 
-// utxoEntry is an interface that provides access to UTXO data needed for
-// input standardness checks.
-type utxoEntry interface {
-       PkScript() []byte
-}
-
-// utxoView is an interface that provides access to UTXOs needed for
-// input standardness checks.
-type utxoView interface {
-       LookupEntry(wire.OutPoint) utxoEntry
-}      
-       
-// utxoViewpointAdapter wraps a blockchain.UtxoViewpoint to implement the
-// utxoView interface.
-type utxoViewpointAdapter struct {
-       view *blockchain.UtxoViewpoint
-}
-       
-// LookupEntry returns the entry for a given outpoint.
-func (u *utxoViewpointAdapter) LookupEntry(op wire.OutPoint) utxoEntry {
-       return u.view.LookupEntry(op)
-}      
-               
 // checkInputsStandard performs a series of checks on a transaction's inputs
 // to ensure they are "standard".  A standard transaction input within the
 // context of this function is one whose referenced public key script is of a
@@ -113,12 +90,6 @@ func (u *utxoViewpointAdapter) LookupEntry(op wire.OutPoint) utxoEntry {
 // accurately and concisely via the txscript.ScriptVerifyCleanStack and
 // txscript.ScriptVerifySigPushOnly flags.
 func checkInputsStandard(tx *btcutil.Tx, utxoView *blockchain.UtxoViewpoint) error {
-       return checkInputsStandardWithView(tx, &utxoViewpointAdapter{view: utxoView})
-}      
-
-// checkInputsStandardWithView performs input standardness checks using the
-// utxoView interface.
-func checkInputsStandardWithView(tx *btcutil.Tx, utxoView utxoView) error {
        // NOTE: The reference implementation also does a coinbase check here,
        // but coinbases have already been rejected prior to calling this
        // function so no need to recheck.
diff --git a/mempool/policy_test.go b/mempool/policy_test.go
index c3259fd0..3411fa08 100644
--- a/mempool/policy_test.go
+++ b/mempool/policy_test.go
@@ -9,13 +9,13 @@ import (
        "testing"
        "time"  
 
+       "github.com/btcsuite/btcd/blockchain"
        "github.com/btcsuite/btcd/btcec/v2"
        "github.com/btcsuite/btcd/btcutil"
        "github.com/btcsuite/btcd/chaincfg"
        "github.com/btcsuite/btcd/chaincfg/chainhash"
        "github.com/btcsuite/btcd/txscript"
        "github.com/btcsuite/btcd/wire"
-       "github.com/stretchr/testify/mock"
 )
                                        
 // TestCalcMinRequiredTxRelayFee tests the calcMinRequiredTxRelayFee API.
@@ -660,44 +660,28 @@ func TestCheckTransactionStandard(t *testing.T) {
        }
 }                      
                        
-// mockUtxoEntry mocks the utxoEntry interface using testify/mock.
-type mockUtxoEntry struct {
-       mock.Mock
-}
-
-// PkScript returns the public key script.
-func (m *mockUtxoEntry) PkScript() []byte {
-       args := m.Called()
-       return args.Get(0).([]byte)
-}
-
-// mockUtxoView mocks the utxoView interface using testify/mock.
-type mockUtxoView struct {
-       mock.Mock
-}
-
-// LookupEntry returns the entry for a given outpoint.
-func (m *mockUtxoView) LookupEntry(op wire.OutPoint) utxoEntry {
-       args := m.Called(op)
-       if args.Get(0) == nil {
-               return nil
-       }
-       return args.Get(0).(utxoEntry)
-}
-
 // TestP2ASpendingStandardness tests that P2A outputs require empty witness
 // and empty signature script to be considered standard.
 func TestP2ASpendingStandardness(t *testing.T) {
        // Create a previous transaction with a P2A output.
-       prevTxHash, _ := chainhash.NewHashFromStr("0101010101010101010101010101010101010101010101010101010101010101")
-       prevOut := wire.OutPoint{Hash: *prevTxHash, Index: 0}
+       prevTx := wire.NewMsgTx(2)
+       prevTx.AddTxIn(&wire.TxIn{
+               PreviousOutPoint: wire.OutPoint{
+                       Hash:  chainhash.Hash{},
+                       Index: 0,
+               },
+       })
+       prevTx.AddTxOut(&wire.TxOut{
+               Value:    1000,
+               PkScript: txscript.PayToAnchorScript,
+       })
+       prevBtcTx := btcutil.NewTx(prevTx)
 
-       // Create mocked UTXO entry and view.
-       mockEntry := new(mockUtxoEntry)
-       mockEntry.On("PkScript").Return(txscript.PayToAnchorScript)
+       // Create a UTXO view with the P2A output.
+       utxoView := blockchain.NewUtxoViewpoint()
+       utxoView.AddTxOuts(prevBtcTx, 100)
 
-       mockView := new(mockUtxoView)
-       mockView.On("LookupEntry", prevOut).Return(mockEntry)
+       prevOut := wire.OutPoint{Hash: *prevBtcTx.Hash(), Index: 0}
 
        tests := []struct {
                name       string
@@ -756,7 +740,7 @@ func TestP2ASpendingStandardness(t *testing.T) {
                        })
 
                        btcTx := btcutil.NewTx(tx)
-                       err := checkInputsStandardWithView(btcTx, mockView)
+                       err := checkInputsStandard(btcTx, utxoView)
 
                        if test.shouldFail {
                                if err == nil {
@@ -767,10 +751,6 @@ func TestP2ASpendingStandardness(t *testing.T) {
                                        t.Errorf("Unexpected error for valid P2A spend: %v", err)
                                }
                        }
-
-                       // Verify that the mock was called.
-                       mockView.AssertExpectations(t)
-                       mockEntry.AssertExpectations(t)
                })
        }
 }

@kcalvinalvin
Copy link
Collaborator

Most things look good. But ExtractPkScriptAddrs not returning an address for P2A should be fixed before this gets merged.

Things done:

  1. Went through each of the commits again to sanity check the changes.
  2. Checked that the implementation here aligns with BIP-433.
  3. Checked that the tests make sense in each of the commits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants