Thread

Asking Claude about OP_RETURN in Bitcoin Core

can you find all references to OP_RETURN data and outline how it's is handled. When is it serialized/deserialized and in what context?

⏺ I'll search for all references to OP_RETURN in the codebase and analyze how it's handled.

⏺ Search(pattern: "OP_RETURN") ⎿  Found 73 files (ctrl+o to expand)

⏺ Search(pattern: "OP_RETURN", output_mode: "content") ⎿  Found 1241 lines (ctrl+o to expand)

⏺ Let me read the key source files to understand how OP_RETURN data is handled:

⏺ Read(src/script/script.h) ⎿  Read 642 lines (ctrl+o to expand)

⏺ Read(src/script/interpreter.cpp) ⎿  Error: File content (27382 tokens) exceeds maximum allowed tokens (25000). Please use offset and limit parameters to read specific portions of the file, or use the GrepTool to search for specific content.

⏺ Read(src/script/solver.cpp) ⎿  Read 229 lines (ctrl+o to expand)

⏺ Read(src/compressor.h) ⎿  Read 119 lines (ctrl+o to expand)

⏺ Read(src/validation.cpp) ⎿  Read 100 lines (ctrl+o to expand)

⏺ Let me search for more specific OP_RETURN handling in validation and script execution:

⏺ Search(pattern: "IsUnspendable", output_mode: "content") ⎿  Found 238 lines (ctrl+o to expand)

⏺ Read(src/script/interpreter.cpp) ⎿  Read 200 lines (ctrl+o to expand)

⏺ Search(pattern: "NULL_DATA", output_mode: "content") ⎿  Found 170 lines (ctrl+o to expand)

⏺ Let me look at how OP_RETURN is handled during script execution and look for where the data is extracted:

⏺ Search(pattern: "case OP_RETURN", output_mode: "content") ⎿  Found 43 lines (ctrl+o to expand)

⏺ Read(src/rpc/rawtransaction_util.cpp) ⎿  Read 100 lines (ctrl+o to expand)

⏺ Search(pattern: "datacarrier", output_mode: "content") ⎿  Found 352 lines (ctrl+o to expand)

⏺ Now let me check how the data is serialized/deserialized in transactions and the compressor:

⏺ Read(src/script/script.cpp) ⎿  Read 100 lines (ctrl+o to expand)

⏺ Search(pattern: "CompressScript|DecompressScript", output_mode: "content") ⎿  Found 326 lines (ctrl+o to expand)

⏺ Now let me read the actual compressor implementation:

⏺ Read(src/compressor.cpp) ⎿  Read 193 lines (ctrl+o to expand)

⏺ Based on my analysis of the Bitcoin Core codebase, here's a comprehensive outline of how OP_RETURN data is handled:

OP_RETURN Data Handling in Bitcoin Core

Definition and Recognition

Script Definition

Location: src/script/script.h:111

  • OP_RETURN = 0x6a - Opcode that marks a script as provably unspendable

IsUnspendable Check

Location: src/script/script.h:573-576

bool IsUnspendable() const {
    return (size() > 0 && *begin() == OP_RETURN) || (size() > MAX_SCRIPT_SIZE);
}
  • Returns true if script starts with OP_RETURN or exceeds maximum size

Transaction Type Classification

TxoutType::NULL_DATA

Location: src/script/solver.h:30

  • Designated transaction output type for unspendable OP_RETURN scripts that carry data
  • Comment: //!< unspendable OP_RETURN script that carries data

Detection Logic

Location: src/script/solver.cpp:185-187

if (scriptPubKey.size() >= 1 && scriptPubKey[0] == OP_RETURN &&
    scriptPubKey.IsPushOnly(scriptPubKey.begin()+1)) {
    return TxoutType::NULL_DATA;
}

Requirements:

  • Script must start with OP_RETURN
  • Everything after the first byte must be push-only operations (data pushes)
  • No other opcodes allowed after OP_RETURN

Script Execution Context

Interpreter Behavior

Location: src/script/interpreter.cpp:666-669

case OP_RETURN:
{
    return set_error(serror, SCRIPT_ERR_OP_RETURN);
}

Behavior:

  • When executed, OP_RETURN immediately fails with SCRIPT_ERR_OP_RETURN
  • This makes the output provably unspendable at the script level
  • Execution never proceeds past this opcode

Serialization/Deserialization

Standard Transaction Serialization

OP_RETURN scripts are serialized as part of transaction outputs (CTxOut) using standard Bitcoin serialization format:

  1. Script length - Encoded as CompactSize/VarInt
  2. Script bytes - Raw bytes including opcode + data pushes

No special handling during normal transaction serialization.

UTXO Set Compression

Location: src/compressor.h:54-96, src/compressor.cpp:55-84

Compression

bool CompressScript(const CScript& script, CompressedScript& out)

Special Cases (codes 0-5):

  • 0x00 - P2PKH (Pay-to-Public-Key-Hash)
  • 0x01 - P2SH (Pay-to-Script-Hash)
  • 0x02/0x03 - Compressed P2PK
  • 0x04/0x05 - Uncompressed P2PK

OP_RETURN Handling:

  • OP_RETURN scripts do NOT receive special compression codes
  • Stored with full script data + length prefix
  • Falls through compression attempts and stored uncompressed

Decompression Error Handling

Location: src/compressor.h:87-90

if (nSize > MAX_SCRIPT_SIZE) {
    // Overly long script, replace with a short invalid one
    script << OP_RETURN;
    s.ignore(nSize);
}
  • If script exceeds MAX_SCRIPT_SIZE during decompression, it's replaced with a simple OP_RETURN to mark as invalid
  • Invalid data is skipped

UTXO Management

AddCoin Filtering

Location: src/coins.cpp:74

void CCoinsViewCache::AddCoin(const COutPoint &outpoint, Coin&& coin, bool possible_overwrite) {
    assert(!coin.IsSpent());
    if (coin.out.scriptPubKey.IsUnspendable()) return;  // Early exit
    // ...
}

Key Behavior:

  • OP_RETURN outputs are never added to the UTXO set
  • Filtered out immediately when adding coins
  • This is the primary pruning mechanism for OP_RETURN data

BIP30 Duplicate Transaction Checking

Location: src/validation.cpp:2334

for (size_t o = 0; o < tx.vout.size(); o++) {
    if (!tx.vout[o].scriptPubKey.IsUnspendable()) {
        // Only check spendable outputs for BIP30
    }
}
  • OP_RETURN outputs are excluded from duplicate transaction ID checks
  • Only spendable outputs matter for BIP30 validation

Policy and Validation

Size Limits

Location: src/policy/policy.h:73-78

static const bool DEFAULT_ACCEPT_DATACARRIER = true;
static const unsigned int MAX_OP_RETURN_RELAY = MAX_STANDARD_TX_WEIGHT / WITNESS_SCALE_FACTOR;
  • Default maximum: ~100KB (entire transaction weight / 4)
  • Configurable via -datacarrier and -datacarriersize options

Standardness Check

Location: src/policy/policy.cpp:144-150

if (whichType == TxoutType::NULL_DATA) {
    unsigned int size = txout.scriptPubKey.size();
    if (size > datacarrier_bytes_left) {
        reason = "datacarrier";
        return false;
    }
    datacarrier_bytes_left -= size;
}

Rules:

  • Multiple OP_RETURN outputs are allowed in a single transaction
  • Aggregate size of all OP_RETURN outputs is checked
  • Individual and total size must not exceed policy limits

Dust Calculation

Location: src/policy/policy.cpp:42-43

if (txout.scriptPubKey.IsUnspendable())
    return 0;  // No dust threshold for unspendable outputs
  • OP_RETURN outputs have no dust threshold
  • Can contain any amount (including zero)

RPC and Burn Protection

Maximum Burn Amount Check

Location: src/rpc/mempool.cpp:85-87

if((out.scriptPubKey.IsUnspendable() || !out.scriptPubKey.HasValidOps()) &&
   out.nValue > max_burn_amount) {
    throw JSONRPCTransactionError(TransactionError::MAX_BURN_EXCEEDED);
}

Parameters:

  • maxburnamount parameter in sendrawtransaction and submitpackage RPCs
  • Default: DEFAULT_MAX_BURN_AMOUNT
  • Protects against accidental destruction of funds via OP_RETURN outputs

Statistics and Reporting

Coin Statistics Tracking

Location: src/kernel/coinstats.h:65-66

// Total cumulative amount of outputs sent to unspendable scripts
// (OP_RETURN for example) up to and including this block
CAmount total_unspendables_scripts{0};

Tracking:

  • Amounts sent to OP_RETURN outputs are tracked separately
  • Part of UTXO set statistics
  • Used for chain analysis and reporting

GetBlockStats RPC

Location: src/rpc/blockchain.cpp:2000

if (out.scriptPubKey.IsUnspendable()) continue;
// Skip unspendable outputs since they are not included in the UTXO set
  • OP_RETURN outputs excluded from UTXO statistics

Key Processing Contexts

1. Transaction Creation

  • Data encoded as push operations following OP_RETURN opcode
  • Created using RPC commands or wallet APIs
  • Standard script serialization applies

2. Mempool Acceptance

Location: src/validation.cpp:795

if (m_pool.m_opts.require_standard &&
    !IsStandardTx(tx, m_pool.m_opts.max_datacarrier_bytes, ...)) {
    return state.Invalid(TxValidationResult::TX_NOT_STANDARD, reason);
}
  • Size checked against -datacarriersize policy limits
  • Burn amount checked against maxburnamount parameter

3. Block Validation

  • Scripts validated for proper formatting
  • Outputs never enter UTXO set (filtered during AddCoin)
  • Amounts tracked in unspendables statistics

4. Blockchain Storage

  • Stored in blocks using normal transaction serialization
  • Full script data preserved in block files
  • No special handling during block storage

5. UTXO Database

  • Completely excluded from chainstate database
  • Never serialized to UTXO set
  • Pruned immediately when processing transactions

6. Chain Analysis

  • Amounts tracked as provably destroyed coins
  • Accessible in block data for analysis
  • Not recoverable or spendable

Summary

OP_RETURN data is handled as follows:

| Stage | Serialization | Context | |-----------|------------------|-------------| | Transaction Creation | Standard serialization (VarInt + bytes) | Encoded in scriptPubKey | | Network Propagation | Standard serialization | Part of transaction messages | | Mempool | Standard serialization | Policy checks applied | | Block Storage | Standard serialization | Stored in block files | | UTXO Set | Not serialized | Filtered out by IsUnspendable() check | | Script Execution | Never executed | Returns SCRIPT_ERR_OP_RETURN |

Key Insight: OP_RETURN data is never deserialized for execution and is never stored in the UTXO set. It exists only in blockchain history for data storage purposes and is provably unspendable.

Replies (0)

No replies yet. Be the first to leave a comment!