Skip to content

Commit caae108

Browse files
committed
txscript: increase OP_RETURN data limit to 100KB
This commit increases the MaxDataCarrierSize from 80 bytes to 100,000 bytes to align with Bitcoin Core v30, which removes the specific 80-byte OP_RETURN data limit. Rationale: 1. Policy Alignment: Maintains consistency with Bitcoin Core v30, ensuring btcd nodes accept the same set of standard transactions as the majority of the network. 2. Fee Estimation: Filtering large OP_RETURN transactions from the mempool prevents accurate fee estimation by hiding transactions that are competing for blockspace. Full mempool visibility is essential for informed fee rate calculation. 3. Validation Performance: Rejecting transactions that will be mined by other nodes means they must be validated when processing blocks, without cached results. This delays block validation and propagation, harming both the node operator and network health. 4. Decentralization: Slow block propagation advantages large mining operations with preferentially peered nodes, as they get a head start on the next block. Ensuring all likely-to-be-mined transactions are pre-validated helps maintain network decentralization. Key changes: - Increase MaxDataCarrierSize constant from 80 to 100000 bytes - Modify NullDataScript to use AddFullData instead of AddData to bypass MaxScriptElementSize (520 bytes) since OP_RETURN outputs are provably unspendable and never executed - Update AddFullData documentation to reflect legitimate use for OP_RETURN outputs, not just testing The practical limit for OP_RETURN data remains around 75KB due to the maxStandardTxWeight limit of 400,000 weight units (~100KB total transaction size including inputs and outputs). Test coverage includes: - Unit tests for script creation and size validation - Policy tests for transaction standardness (75KB OP_RETURN) - Integration tests for mempool acceptance and size limit enforcement This change maintains backward compatibility while enabling larger data storage in OP_RETURN outputs for applications like inscriptions, timestamps, and data anchoring.
1 parent b7d0706 commit caae108

File tree

5 files changed

+156
-14
lines changed

5 files changed

+156
-14
lines changed

integration/rawtx_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,88 @@ func decodeHex(t *testing.T, txHex string) *wire.MsgTx {
201201

202202
return tx.MsgTx()
203203
}
204+
205+
// TestLargeOPReturnMempool tests that large OP_RETURN transactions (up to 100KB)
206+
// are accepted into the mempool. This validates the Bitcoin Core v30 change that
207+
// removes the 80-byte OP_RETURN limit.
208+
func TestLargeOPReturnMempool(t *testing.T) {
209+
t.Parallel()
210+
211+
// Create a single node for testing mempool acceptance.
212+
// Use --rejectnonstd to enforce standardness rules (including maxStandardTxWeight).
213+
btcdCfg := []string{"--rejectnonstd", "--debuglevel=debug"}
214+
node, err := rpctest.New(&chaincfg.SimNetParams, nil, btcdCfg, "")
215+
require.NoError(t, err)
216+
217+
// Setup the node with mature coinbase outputs.
218+
require.NoError(t, node.SetUp(true, 100))
219+
t.Cleanup(func() {
220+
require.NoError(t, node.TearDown())
221+
})
222+
223+
testCases := []struct {
224+
name string
225+
dataSize int
226+
shouldSucceed bool
227+
failureReason string
228+
}{
229+
{
230+
name: "75KB OP_RETURN (well over old 80 byte/520 byte limits)",
231+
dataSize: 75000,
232+
shouldSucceed: true,
233+
},
234+
{
235+
name: "100KB OP_RETURN (at MaxDataCarrierSize limit)",
236+
dataSize: 100000,
237+
shouldSucceed: false,
238+
failureReason: "transaction size exceeds maxStandardTxWeight (100,000 vbytes)",
239+
},
240+
}
241+
242+
for _, tc := range testCases {
243+
tc := tc
244+
t.Run(tc.name, func(t *testing.T) {
245+
// Create large OP_RETURN script.
246+
largeData := make([]byte, tc.dataSize)
247+
for i := range largeData {
248+
largeData[i] = byte(i % 256)
249+
}
250+
251+
opReturnScript, err := txscript.NullDataScript(largeData)
252+
require.NoError(t, err)
253+
254+
// Create a transaction with OP_RETURN output using the harness wallet.
255+
// The wallet will handle signing and input selection.
256+
opReturnOutput := &wire.TxOut{
257+
Value: 0,
258+
PkScript: opReturnScript,
259+
}
260+
261+
// Create transaction with the OP_RETURN output.
262+
// The wallet automatically adds inputs and a change output if needed.
263+
tx, err := node.CreateTransaction([]*wire.TxOut{opReturnOutput}, 10, true)
264+
require.NoError(t, err)
265+
266+
// Log the actual transaction size.
267+
txSize := tx.SerializeSize()
268+
txWeight := txSize * 4 // Non-segwit: weight = size * 4
269+
t.Logf("Transaction size: %d bytes, weight: %d (limit: 400000)", txSize, txWeight)
270+
271+
// Submit to the mempool.
272+
txHash, err := node.Client.SendRawTransaction(tx, true)
273+
274+
if tc.shouldSucceed {
275+
require.NoError(t, err, "Large OP_RETURN tx should be accepted")
276+
t.Logf("✓ Large OP_RETURN tx (%d bytes data) accepted: %s", tc.dataSize, txHash)
277+
278+
// Verify it's in the mempool.
279+
mempool, err := node.Client.GetRawMempool()
280+
require.NoError(t, err)
281+
require.Contains(t, mempool, txHash, "Transaction should be in mempool")
282+
} else {
283+
require.Error(t, err, "Transaction should be rejected: %s", tc.failureReason)
284+
t.Logf("✓ Transaction correctly rejected (%s): %v", tc.failureReason, err)
285+
}
286+
})
287+
}
288+
}

mempool/policy_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,33 @@ func TestCheckTransactionStandard(t *testing.T) {
462462
height: 300000,
463463
isStandard: true,
464464
},
465+
{
466+
name: "Large nulldata output with 75KB data (standard after v30)",
467+
tx: func() wire.MsgTx {
468+
// Create a large OP_RETURN with 75KB of data
469+
// This is well over previous limits (80 bytes, 520 bytes MaxScriptElementSize)
470+
// but under the new 100KB MaxDataCarrierSize limit
471+
largeData := make([]byte, 75000)
472+
for i := range largeData {
473+
largeData[i] = byte(i % 256)
474+
}
475+
pkScript, err := txscript.NullDataScript(largeData)
476+
if err != nil {
477+
t.Fatalf("NullDataScript: unexpected error: %v", err)
478+
}
479+
return wire.MsgTx{
480+
Version: 1,
481+
TxIn: []*wire.TxIn{&dummyTxIn},
482+
TxOut: []*wire.TxOut{{
483+
Value: 0,
484+
PkScript: pkScript,
485+
}},
486+
LockTime: 0,
487+
}
488+
}(),
489+
height: 300000,
490+
isStandard: true,
491+
},
465492
}
466493

467494
pastMedianTime := time.Now()

txscript/scriptbuilder.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -199,13 +199,19 @@ func (b *ScriptBuilder) addData(data []byte) *ScriptBuilder {
199199
return b
200200
}
201201

202-
// AddFullData should not typically be used by ordinary users as it does not
203-
// include the checks which prevent data pushes larger than the maximum allowed
204-
// sizes which leads to scripts that can't be executed. This is provided for
205-
// testing purposes such as regression tests where sizes are intentionally made
206-
// larger than allowed.
202+
// AddFullData pushes the passed data to the end of the script without
203+
// enforcing MaxScriptElementSize limits. It automatically chooses canonical
204+
// opcodes depending on the length of the data.
207205
//
208-
// Use AddData instead.
206+
// This function bypasses the 520-byte MaxScriptElementSize limit and is
207+
// intended for:
208+
//
209+
// 1. Creating OP_RETURN outputs with data larger than 520 bytes, since they
210+
// are provably unspendable and never executed.
211+
// 2. Testing purposes where scripts need to intentionally exceed limits.
212+
//
213+
// For regular data pushes that will be executed, use AddData instead, which
214+
// enforces size limits to ensure the resulting script can be executed.
209215
func (b *ScriptBuilder) AddFullData(data []byte) *ScriptBuilder {
210216
if b.err != nil {
211217
return b

txscript/standard.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import (
1515
const (
1616
// MaxDataCarrierSize is the maximum number of bytes allowed in pushed
1717
// data to be considered a nulldata transaction
18-
MaxDataCarrierSize = 80
18+
MaxDataCarrierSize = 100000
1919

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

895-
return NewScriptBuilder().AddOp(OP_RETURN).AddData(data).Script()
899+
return NewScriptBuilder().AddOp(OP_RETURN).AddFullData(data).Script()
896900
}
897901

898902
// MultiSigScript returns a valid script for a multisignature redemption where

txscript/standard_test.go

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1022,14 +1022,14 @@ var scriptClassTests = []struct {
10221022
class: NullDataTy,
10231023
},
10241024
{
1025-
// Nulldata with more than max allowed data to be considered
1026-
// standard (so therefore nonstandard)
1027-
name: "nulldata exceed max standard push",
1025+
// Nulldata with 81 bytes is now standard (previously exceeded
1026+
// the old 80 byte limit, but is well under the new 100KB limit)
1027+
name: "nulldata 81 bytes standard",
10281028
script: "RETURN PUSHDATA1 0x51 0x046708afdb0fe5548271967f1a67" +
10291029
"130b7105cd6a828e03909a67962e0ea1f61deb649f6bc3f4cef3" +
10301030
"046708afdb0fe5548271967f1a67130b7105cd6a828e03909a67" +
10311031
"962e0ea1f61deb649f6bc3f4cef308",
1032-
class: NonStandardTy,
1032+
class: NullDataTy,
10331033
},
10341034
{
10351035
// Almost nulldata, but add an additional opcode after the data
@@ -1238,11 +1238,30 @@ func TestNullDataScript(t *testing.T) {
12381238
class: NullDataTy,
12391239
},
12401240
{
1241-
name: "too big",
1241+
name: "81 bytes now standard",
12421242
data: hexToBytes("000102030405060708090a0b0c0d0e0f101" +
12431243
"112131415161718191a1b1c1d1e1f202122232425262" +
12441244
"728292a2b2c2d2e2f303132333435363738393a3b3c3" +
12451245
"d3e3f404142434445464748494a4b4c4d4e4f50"),
1246+
expected: mustParseShortForm("RETURN PUSHDATA1 0x51 " +
1247+
"0x000102030405060708090a0b0c0d0e0f101112131" +
1248+
"415161718191a1b1c1d1e1f20212223242526272829" +
1249+
"2a2b2c2d2e2f303132333435363738393a3b3c3d3e3" +
1250+
"f404142434445464748494a4b4c4d4e4f50"),
1251+
err: nil,
1252+
class: NullDataTy,
1253+
},
1254+
{
1255+
name: "large data - 9995 bytes (under MaxScriptSize)",
1256+
data: make([]byte, 9995),
1257+
// MaxScriptSize (10000) - OP_RETURN (1) - PUSHDATA2 (3) - length field (1) ≈ 9995
1258+
expected: nil, // Not checking exact encoding for large data
1259+
err: nil,
1260+
class: NullDataTy,
1261+
},
1262+
{
1263+
name: "exceeds MaxDataCarrierSize limit",
1264+
data: make([]byte, 100001),
12461265
expected: nil,
12471266
err: scriptError(ErrTooMuchNullData, ""),
12481267
class: NonStandardTy,
@@ -1259,7 +1278,8 @@ func TestNullDataScript(t *testing.T) {
12591278
}
12601279

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

0 commit comments

Comments
 (0)