Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions integration/rawtx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,88 @@ func decodeHex(t *testing.T, txHex string) *wire.MsgTx {

return tx.MsgTx()
}

// TestLargeOPReturnMempool tests that large OP_RETURN transactions (up to 100KB)
// are accepted into the mempool. This validates the Bitcoin Core v30 change that
// removes the 80-byte OP_RETURN limit.
func TestLargeOPReturnMempool(t *testing.T) {

Choose a reason for hiding this comment

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

Bitcoin Core 30.0 will also allow multiple OP_RETURN outputs on transactions. I’m not sure whether BTCD ever restricted transactions to a single one, but another test that adds two small OP_RETURN outputs would demonstrate that BTCD is compatible, or reveal that additional changes would need to be made to make it compatible.

Copy link
Author

Choose a reason for hiding this comment

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

@murchandamus It's enforced atm see

btcd/mempool/policy.go

Lines 368 to 373 in b7d0706

// A standard transaction must not have more than one output script that
// only carries data.
if numNullDataOutputs > 1 {
str := "more than one transaction output in a nulldata script"
return txRuleError(wire.RejectNonstandard, str)
}

Looking into it now

Copy link
Author

Choose a reason for hiding this comment

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

t.Parallel()

// Create a single node for testing mempool acceptance.
// Use --rejectnonstd to enforce standardness rules (including maxStandardTxWeight).
btcdCfg := []string{"--rejectnonstd", "--debuglevel=debug"}
node, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "")
require.NoError(t, err)

// Setup the node with mature coinbase outputs.
require.NoError(t, node.SetUp(true, 100))
t.Cleanup(func() {
require.NoError(t, node.TearDown())
})

testCases := []struct {
name string
dataSize int
shouldSucceed bool
failureReason string
}{
{
name: "75KB OP_RETURN (well over old 80 byte/520 byte limits)",
dataSize: 75000,
shouldSucceed: true,
},
{
name: "100KB OP_RETURN (at MaxDataCarrierSize limit)",
dataSize: 100000,
shouldSucceed: false,
failureReason: "transaction size exceeds maxStandardTxWeight (100,000 vbytes)",
},
}

for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
// Create large OP_RETURN script.
largeData := make([]byte, tc.dataSize)
for i := range largeData {
largeData[i] = byte(i % 256)
}

opReturnScript, err := txscript.NullDataScript(largeData)
require.NoError(t, err)

// Create a transaction with OP_RETURN output using the harness wallet.
// The wallet will handle signing and input selection.
opReturnOutput := &wire.TxOut{
Value: 0,
PkScript: opReturnScript,
}

// Create transaction with the OP_RETURN output.
// The wallet automatically adds inputs and a change output if needed.
tx, err := node.CreateTransaction([]*wire.TxOut{opReturnOutput}, 10, true)
require.NoError(t, err)

// Log the actual transaction size.
txSize := tx.SerializeSize()
txWeight := txSize * 4 // Non-segwit: weight = size * 4
t.Logf("Transaction size: %d bytes, weight: %d (limit: 400000)", txSize, txWeight)

// Submit to the mempool.
txHash, err := node.Client.SendRawTransaction(tx, true)

if tc.shouldSucceed {
require.NoError(t, err, "Large OP_RETURN tx should be accepted")
t.Logf("✓ Large OP_RETURN tx (%d bytes data) accepted: %s", tc.dataSize, txHash)

// Verify it's in the mempool.
mempool, err := node.Client.GetRawMempool()
require.NoError(t, err)
require.Contains(t, mempool, txHash, "Transaction should be in mempool")
} else {
require.Error(t, err, "Transaction should be rejected: %s", tc.failureReason)
t.Logf("✓ Transaction correctly rejected (%s): %v", tc.failureReason, err)
}
})
}
}
27 changes: 27 additions & 0 deletions mempool/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,33 @@ func TestCheckTransactionStandard(t *testing.T) {
height: 300000,
isStandard: true,
},
{
name: "Large nulldata output with 75KB data (standard after v30)",
tx: func() wire.MsgTx {
// Create a large OP_RETURN with 75KB of data
// This is well over previous limits (80 bytes, 520 bytes MaxScriptElementSize)
// but under the new 100KB MaxDataCarrierSize limit
largeData := make([]byte, 75000)
for i := range largeData {
largeData[i] = byte(i % 256)
}
pkScript, err := txscript.NullDataScript(largeData)
if err != nil {
t.Fatalf("NullDataScript: unexpected error: %v", err)
}
return wire.MsgTx{
Version: 1,
TxIn: []*wire.TxIn{&dummyTxIn},
TxOut: []*wire.TxOut{{
Value: 0,
PkScript: pkScript,
}},
LockTime: 0,
}
}(),
height: 300000,
isStandard: true,
},
}

pastMedianTime := time.Now()
Expand Down
18 changes: 12 additions & 6 deletions txscript/scriptbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,19 @@ func (b *ScriptBuilder) addData(data []byte) *ScriptBuilder {
return b
}

// AddFullData should not typically be used by ordinary users as it does not
// include the checks which prevent data pushes larger than the maximum allowed
// sizes which leads to scripts that can't be executed. This is provided for
// testing purposes such as regression tests where sizes are intentionally made
// larger than allowed.
// AddFullData pushes the passed data to the end of the script without
// enforcing MaxScriptElementSize limits. It automatically chooses canonical
// opcodes depending on the length of the data.
//
// Use AddData instead.
// This function bypasses the 520-byte MaxScriptElementSize limit and is
// intended for:
//
// 1. Creating OP_RETURN outputs with data larger than 520 bytes, since they
// are provably unspendable and never executed.
// 2. Testing purposes where scripts need to intentionally exceed limits.
//
// For regular data pushes that will be executed, use AddData instead, which
// enforces size limits to ensure the resulting script can be executed.
func (b *ScriptBuilder) AddFullData(data []byte) *ScriptBuilder {
if b.err != nil {
return b
Expand Down
8 changes: 6 additions & 2 deletions txscript/standard.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
const (
// MaxDataCarrierSize is the maximum number of bytes allowed in pushed
// data to be considered a nulldata transaction
MaxDataCarrierSize = 80
MaxDataCarrierSize = 100000

// StandardVerifyFlags are the script flags which are used when
// executing transaction scripts to enforce additional checks which
Expand Down Expand Up @@ -885,14 +885,18 @@ func PayToAddrScript(addr btcutil.Address) ([]byte, error) {
// NullDataScript creates a provably-prunable script containing OP_RETURN
// followed by the passed data. An Error with the error code ErrTooMuchNullData
// will be returned if the length of the passed data exceeds MaxDataCarrierSize.
//
// Note: This function uses AddFullData to bypass MaxScriptElementSize since
// OP_RETURN outputs are provably unspendable and never executed. The data size
// is only constrained by MaxDataCarrierSize policy and MaxScriptSize consensus.
func NullDataScript(data []byte) ([]byte, error) {
if len(data) > MaxDataCarrierSize {
str := fmt.Sprintf("data size %d is larger than max "+
"allowed size %d", len(data), MaxDataCarrierSize)
return nil, scriptError(ErrTooMuchNullData, str)
}

return NewScriptBuilder().AddOp(OP_RETURN).AddData(data).Script()
return NewScriptBuilder().AddOp(OP_RETURN).AddFullData(data).Script()
}

// MultiSigScript returns a valid script for a multisignature redemption where
Expand Down
32 changes: 26 additions & 6 deletions txscript/standard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1022,14 +1022,14 @@ var scriptClassTests = []struct {
class: NullDataTy,
},
{
// Nulldata with more than max allowed data to be considered
// standard (so therefore nonstandard)
name: "nulldata exceed max standard push",
// Nulldata with 81 bytes is now standard (previously exceeded
// the old 80 byte limit, but is well under the new 100KB limit)
name: "nulldata 81 bytes standard",
script: "RETURN PUSHDATA1 0x51 0x046708afdb0fe5548271967f1a67" +
"130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3" +
"046708afdb0fe5548271967f1a67130b7105cd6a828e03909a67" +
"962e0ea1f61deb649f6bc3f4cef308",
class: NonStandardTy,
class: NullDataTy,
},
{
// Almost nulldata, but add an additional opcode after the data
Expand Down Expand Up @@ -1238,11 +1238,30 @@ func TestNullDataScript(t *testing.T) {
class: NullDataTy,
},
{
name: "too big",
name: "81 bytes now standard",
data: hexToBytes("000102030405060708090a0b0c0d0e0f101" +
"112131415161718191a1b1c1d1e1f202122232425262" +
"728292a2b2c2d2e2f303132333435363738393a3b3c3" +
"d3e3f404142434445464748494a4b4c4d4e4f50"),
expected: mustParseShortForm("RETURN PUSHDATA1 0x51 " +
"0x000102030405060708090a0b0c0d0e0f101112131" +
"415161718191a1b1c1d1e1f20212223242526272829" +
"2a2b2c2d2e2f303132333435363738393a3b3c3d3e3" +
"f404142434445464748494a4b4c4d4e4f50"),
err: nil,
class: NullDataTy,
},
{
name: "large data - 9995 bytes (under MaxScriptSize)",
data: make([]byte, 9995),
// MaxScriptSize (10000) - OP_RETURN (1) - PUSHDATA2 (3) - length field (1) ≈ 9995
expected: nil, // Not checking exact encoding for large data
err: nil,
class: NullDataTy,
},
{
name: "exceeds MaxDataCarrierSize limit",
data: make([]byte, 100001),
expected: nil,
err: scriptError(ErrTooMuchNullData, ""),
class: NonStandardTy,
Expand All @@ -1259,7 +1278,8 @@ func TestNullDataScript(t *testing.T) {
}

// Check that the expected result was returned.
if !bytes.Equal(script, test.expected) {
// Skip exact comparison if expected is nil (for large data tests).
if test.expected != nil && !bytes.Equal(script, test.expected) {
t.Errorf("NullDataScript: #%d (%s) wrong result\n"+
"got: %x\nwant: %x", i, test.name, script,
test.expected)
Expand Down