<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>deadmanoz.xyz</title>
        <link>https://deadmanoz.xyz</link>
        <description>deadmanoz's website</description>
        <lastBuildDate>Wed, 13 May 2026 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <image>
            <title>deadmanoz.xyz</title>
            <url>https://deadmanoz.xyz/favicon/apple-touch-icon.png</url>
            <link>https://deadmanoz.xyz</link>
        </image>
        <copyright>© 2026 deadmanoz</copyright>
        <item>
            <title><![CDATA[Merge mining and AuxPoW: how it works]]></title>
            <link>https://deadmanoz.xyz/posts/2026/merge-mining</link>
            <guid isPermaLink="false">https://deadmanoz.xyz/posts/2026/merge-mining</guid>
            <pubDate>Wed, 13 May 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[A walkthrough of the AuxPoW mechanism: how auxiliary chains like Namecoin reuse Bitcoin's proof-of-work as their own]]></description>
            <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Merge mining is a technique that lets a chain other than Bitcoin reuse Bitcoin's <em>proof-of-work</em> as its own.
It lets Bitcoin miners use the same hashing effort to mine one or more <em>auxiliary chains</em> (Namecoin being the original and most well-known example), with no reduction in Bitcoin hashrate and only the additional requirement that miners or pools run auxiliary-chain software (e.g., a Namecoin node).
The arrangement is one-sided: an auxiliary chain's nodes carry the code to recognise and validate the shared work, while Bitcoin itself is unchanged and unaware any of it is happening.</p>
<p>Beginning with a brief history of where the idea came from, this post walks through how Bitcoin and auxiliary-chain blocks are linked.
It covers the merkle structure that lets multiple auxiliary chains piggyback on individual Bitcoin blocks, the <em>Auxiliary Proof-of-Work (AuxPoW)</em> record carried by each auxiliary block, and the cryptographic checks an auxiliary node performs to validate a merge-mined auxiliary block.
A <a href="./merge-mining-chains-and-pools">companion post</a> catalogues the chains that have adopted AuxPoW with Bitcoin as their parent and examines which mining pools embed merge-mining commitments in their coinbases.
Not every Bitcoin-merge-mined chain, however, uses AuxPoW; some commit through a different mechanism that lives in a coinbase output rather than the coinbase <code>scriptSig</code> (<em><code>OP_RETURN</code> tag-based merge mining</em>).
We touch on that alternative approach in <a href="#op_return-tag-based-merge-mining">a later section</a>, though the bulk of this post focuses on AuxPoW merge mining.</p>
<p>This piece is a bit of a detour from my planned treatment of Bitcoin network monitoring topics, such as a continued examination of the <a href="https://github.com/peer-observer">peer observer ecosystem</a> and a grounding in the network's important properties, but there is good reason for it.
Each AuxPoW chain that has merge-mined with Bitcoin holds, in its own permanent record, a copy of the Bitcoin block header whose proof-of-work was reused for each of its merge-mined blocks, including headers that Bitcoin's own canonical chain discarded.
More specifically, those AuxPoW chains are a side-channel into block propagation (one of those important network properties) and the stale blocks those propagation delays help create.
A follow-up post uses that side-channel to put numbers on a long-standing theoretical claim: that smaller mining pools produce more stale blocks, relative to their hashrate, than larger ones.
This post is the groundwork.</p>
<h2 id="a-brief-history">A brief history</h2>
<p>Merge mining is a Satoshi-era idea, first sketched on BitcoinTalk in late 2010 and realised a year later as part of Namecoin.
In mid-November 2010, a BitcoinTalk thread titled <a href="https://bitcointalk.org/index.php?topic=1790.0">"BitDNS and Generalizing Bitcoin"</a> opened a discussion about whether Bitcoin's chain should host a DNS-like service for human-readable identifiers.
The first half of that title was the specific proposal; the second half, the broader question the thread quickly turned to: what else could a proof-of-work chain be used for, and where should those services live, on Bitcoin's chain or separately?</p>
<p>Satoshi weighed in <a href="https://bitcointalk.org/index.php?topic=1790.msg28696#msg28696">on 9 December 2010</a>, arguing that BitDNS should be a completely separate network with its own block chain, but that the two could share their proof-of-work:</p>
<blockquote>
<p>I think it would be possible for BitDNS to be a completely separate network and separate block chain, yet share CPU power with Bitcoin.
The only overlap is to make it so miners can search for proof-of-work for both networks simultaneously.</p>
<p>The networks wouldn't need any coordination.
Miners would subscribe to both networks in parallel.
They would scan SHA such that if they get a hit, they potentially solve both at once.
A solution may be for just one of the networks if one network has a lower difficulty.</p>
<p>[...]</p>
<p>Instead of fragmentation, networks share and augment each other's total CPU power.
This would solve the problem that if there are multiple networks, they are a danger to each other if the available CPU power gangs up on one.
Instead, all networks in the world would share combined CPU power, increasing the total strength.
It would make it easier for small networks to get started by tapping into a ready base of miners.</p>
</blockquote>
<p>This idea was the nexus for what became merge mining.</p>
<p><a href="https://www.namecoin.org/">Namecoin</a> launched in April 2011 as the first chain to come out of the BitDNS thread.
Its early months were a case study in the problem Satoshi's reply had warned about: the chain ran with its own independent SHA-256d hashrate, small enough to be vulnerable on its own.
About six months later, in October 2011, the <a href="https://web.archive.org/web/20211220001040/https://forum.namecoin.org/viewtopic.php?t=217">merge-mining hard fork</a> activated at Namecoin block 19,200.
In practice every Namecoin block from that height onwards has carried an AuxPoW record committing it to a Bitcoin parent block.
The <a href="https://en.bitcoin.it/wiki/Merged_mining_specification">specification that emerged from that work</a>, written primarily by Namecoin's early contributors, became the baseline for Bitcoin-parent AuxPoW chains.</p>
<h2 id="two-chains-one-hash">Two chains, one hash</h2>
<p>Under that specification, two chains advance in parallel from the same Bitcoin hashing effort, each with its own independent difficulty target.
Bitcoin's <code>parent_target</code> is the threshold a candidate Bitcoin block header's hash must clear for the block to qualify as a valid Bitcoin block.
The auxiliary chain's <code>aux_target</code> is set independently of <code>parent_target</code> and almost always numerically much larger (i.e., easier to satisfy), so the chain produces blocks at its own intended rate.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/bitcoin-header-bytes.png" alt="Figure 1: Layout of the 80-byte Bitcoin block header: 4 bytes version, 32 bytes prev_block_hash, 32 bytes merkle_root, 4 bytes timestamp, 4 bytes nBits (the compact-encoded parent_target), and 4 bytes nonce. Both Bitcoin and the auxiliary chain check the SHA-256d of these 80 bytes against their respective targets."></p>
<p>Each candidate Bitcoin block has its 80-byte header (Figure 1) put through a single SHA-256d evaluation, and the resulting 32-byte digest is compared independently against both targets (Figure 2).
For a merge-mining miner, three outcomes are possible.</p>
<ol>
<li>If the hash exceeds <code>aux_target</code> (and therefore the lower <code>parent_target</code> too), the candidate is discarded and neither chain extends.</li>
<li>If the hash falls to or below <code>aux_target</code> but still above <code>parent_target</code>, the candidate is a valid auxiliary block, but Bitcoin rejects it.</li>
<li>If the hash is at or below <code>parent_target</code>, both bars are cleared at once: the candidate is a valid Bitcoin block (assuming the rest of consensus passes) and a valid auxiliary block.</li>
</ol>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/merge-mining-dual-target-check.png" alt="Figure 2: Three scenarios for a single SHA-256d hash output compared against both targets. Left panel: hash exceeds aux_target, valid for neither chain. Middle panel: hash falls between parent_target and aux_target, valid for the auxiliary chain only. Right panel: hash is at or below parent_target, valid for both Bitcoin and the auxiliary chain. Example hex values illustrate the inequality chain through their leading-zero counts."></p>
<p>The third outcome splits further on Bitcoin's side: those candidates either become canonical Bitcoin blocks or stales that lost a chain race.
Because every accepted auxiliary block stores its parent header in an AuxPoW record (detailed in the next section), there are three categories of parent headers stored in the auxiliary chain (figure):</p>
<ol>
<li>Canonical Bitcoin blocks (won their chain race; e.g., Block <code>B</code>)</li>
<li>Bitcoin stales (met <code>parent_target</code> but lost their chain race; e.g., the stale that lost to Block <code>B+1</code>)</li>
<li>Aux-only parent headers (hash met only <code>aux_target</code>; e.g., the parent header for NMC block <code>N+2</code>)</li>
</ol>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/merge-mining-flow.png" alt="Schematic flow of merge-mining attempts, adapted from Zamyatin&#x27;s 2017 thesis, Merged Mining: Analysis of Effects and Implications. Candidate parent headers either fail both targets, clear only aux_target, or clear parent_target. Of those that clear parent_target, one becomes a canonical Bitcoin block (Block B) while another loses the chain race to Block B+1 and becomes a stale. All three retained outcomes (canonical, stale, aux-only) appear in AuxPoW records on the auxiliary chain."></p>
<p>The stales are what make AuxPoW chains a side-channel into Bitcoin's PoW history.
This is the premise of Stifter <em>et al.</em>'s 2018 paper <a href="https://eprint.iacr.org/2018/1134.pdf">Echoes of the Past: Recovering Blockchain Metrics From Merged Mining</a>, which used the AuxPoW records on merge-mined chains to reconstruct Bitcoin's historical stale block and fork rates, surfacing a non-negligible portion of stale blocks absent from the live monitoring record (this work will be revisited in detail in a future post).</p>
<h2 id="the-coinbase-commitment">The coinbase commitment</h2>
<p>On the Bitcoin (parent) side, the AuxPoW marker is a 44-byte blob inside the [[coinbase's <code>scriptSig</code>||Bitcoin consensus <a href="https://github.com/bitcoin/bitcoin/blob/master/src/consensus/tx_check.cpp#L49-L50">caps it at 100 bytes total.</a>]] (coinbases without merge-mining simply don't carry it).
Thus Bitcoin commits to AuxPoW data (if present) indirectly through the block header's <code>merkle_root</code>; see the <a href="#scriptsig-path">Reminder: from block header to coinbase scriptSig</a> section below for the full details.</p>
<p><strong>Reminder: from block header to coinbase scriptSig</strong></p>
<p>{#scriptsig-path}
The block header is the 80-byte object whose hash is tested against <code>parent_target</code> and, when merge-mining, against <code>aux_target</code> too.
One of its six fields is <code>merkle_root</code>: the root of the merkle tree built from every transaction txid in the block body.
The coinbase transaction's txid is leaf 0 in that tree, and the coinbase transaction's first input contains the <code>scriptSig</code> field (Figure 3).
This is a "free-form" metadata area that miners use to record:</p>
<ul>
<li>The block height, as required by <a href="https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki">BIP 34</a>,</li>
<li>An extranonce that the mining hardware iterates on,</li>
<li>The AuxPoW marker (optional; only when merge-mining), and</li>
<li>The pool-identifying tags and any additional miner data (optional).</li>
</ul>
<p>For more on the extranonce mechanism, see <a href="https://learnmeabitcoin.com/technical/block/nonce/">learnmeabitcoin.com's excellent interactive nonce explainer</a>.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/coinbase-scriptsig-path.png" alt="Figure 3: Commitment path from the coinbase input scriptSig into the Bitcoin block header. The scriptSig sits inside input 0 of the serialised coinbase transaction, the coinbase transaction is the first transaction in the block body, the coinbase txid and all following transaction txids are merkle leaves, and the resulting merkle root is stored in the 80-byte block header."></p>
<p>The AuxPoW marker within the coinbase <code>scriptSig</code> has four fields as illustrated in Figure 4.
The first 4 bytes are the <code>0xFA BE 6D 6D</code> magic, a fixed identifier that lets auxiliary-chain nodes easily locate the marker without ambiguity.
The next 32 bytes are the <code>aux_merkle_root</code>, the commitment that links this Bitcoin block to one or more auxiliary blocks.
The final 8 bytes are two little-endian <code>uint32</code> values: <code>merkle_size</code> and <code>merkle_nonce</code>.
Auxiliary-chain nodes use those two values to locate each chain's commitment within the merkle tree.
What that tree looks like on the auxiliary-chain side is the subject of <a href="#the-merkle-slot-tree">the next section</a>.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/auxpow-marker.png" alt="Figure 4: Layout of the 44-byte AuxPoW marker in the parent coinbase&#x27;s scriptSig. Four fields: a 4-byte magic constant 0xFA BE 6D 6D, a 32-byte aux merkle root committing to one or more auxiliary block hashes, a 4-byte merkle_size declaring how many slots the merkle tree contains, and a 4-byte merkle_nonce used in the slot-mapping calculation. The marker sits alongside other coinbase scriptSig data such as the BIP 34 block height, the extranonce, and any pool-identifying tags."></p>
<p>From Bitcoin's side, none of these 44 bytes are consensus-relevant.
Bitcoin's nodes never read the marker and have no way of knowing the block is also a valid merge-mined block somewhere else.
That is the whole reason merge mining is one-sided: an auxiliary chain can leverage Bitcoin without Bitcoin ever knowing about it.</p>
<h2 id="the-merkle-slot-tree">The merkle slot tree</h2>
<p>The <em>merkle slot tree</em> is the structure that lets multiple auxiliary chains piggyback on the same parent Bitcoin block at once.
Each participating chain occupies one position in a fixed-size merkle tree, and the single root of that tree is committed in the parent coinbase's AuxPoW marker as the 32-byte <code>aux_merkle_root</code> introduced in the previous section.
A "slot" is one such leaf position, indexed from 0 to <code>merkle_size - 1</code>, where <code>merkle_size</code> is a power of two also declared in the marker (Figure 5).</p>
<p>Each slot holds a 32-byte value: for an occupied slot, that value is the auxiliary block hash; for an unused slot, it is just <a href="https://en.bitcoin.it/wiki/Merged_mining_specification#Aux_work_merkle_tree">arbitrary 32-byte filler</a>, not another meaningful commitment.
The miner locks in the filler choice before computing the root, and the same filled-in tree backs every chain's commitment into this parent block.
The <a href="#worked-example">Namecoin worked example</a> later in this post shows an actual filler value picked by AntPool.
The tree then hashes upwards, Bitcoin-style, using double SHA-256.
In the four-slot example of Figure 5, <code>h01 = SHA-256d(s0 || s1)</code>, <code>h23 = SHA-256d(s2 || s3)</code>, and <code>aux_merkle_root = SHA-256d(h01 || h23)</code>.
At each level, two 32-byte children are concatenated into a 64-byte input, and the hash output is again 32 bytes.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/merge-mining-slot-tree.png" alt="Figure 5: The merkle slot tree, with a 4-slot example. The root, aux_merkle_root, is committed in the parent coinbase&#x27;s AuxPoW marker (reproduced at top). Each slot is 32 bytes. Slot 1 holds Elastos&#x27;s block hash (chain_id 1224); slot 2 holds Namecoin&#x27;s (chain_id 1); the other slots are unused 32-byte filler. Each internal node, and finally the root, is SHA-256d over two 32-byte child values."></p>
<p>Which slot a chain occupies is determined by its <code>chain_id</code> combined with the <code>merkle_nonce</code>.
The <code>chain_id</code> is fixed by the chain; there is no central <code>chain_id</code> registry, and apparently no real coordination on assignment either.
The closest thing is <a href="https://bitcointalk.org/index.php?topic=769073.0">BitcoinTalk thread 769073</a>, opened in 2014, three years after Namecoin's AuxPoW fork, as a catalogue of <code>chain_id</code>s (though it listed Namecoin's <code>chain_id</code> incorrectly until July 2015).
In practice this doesn't seem to have been a problem: the population of active AuxPoW chains is small, and a <code>chain_id</code> clash only bites when a single parent block commits to both colliding chains at once.</p>
<p><code>merkle_nonce</code> was meant to be the miner's tuning knob for avoiding slot collisions when committing to multiple auxiliary chains at once, but the published formula only shifts colliding chains together.
This failure is called out in the <a href="https://en.bitcoin.it/wiki/Merged_mining_specification#Aux_work_merkle_tree">Bitcoin Wiki Aux work merkle tree notes</a> and in the <a href="https://bitcointalk.org/index.php?topic=51069.0">Nonce for merged mining chain merkle tree index</a> BitcoinTalk thread.</p>
<p>The mapping process uses a deterministic linear congruential generator (LCG) to decide placement (see the <a href="#slot-mapping-details">Slot mapping details</a> section below).
It does not derive any auxiliary block hashes; the 32-byte values come from the auxiliary chains' block headers, and those hashes get committed into the slots.</p>
<p><strong>Slot mapping details</strong></p>
<p>{#slot-mapping-details}
The slot index is computed from <code>merkle_nonce</code> and <code>chain_id</code>, with all arithmetic on 32-bit unsigned integers (multiplications wrap modulo (2<sup>{32})).
Each <code>rand = rand * 1103515245 + 12345</code> line is one LCG step, using the multiplier (<code>1103515245</code>) and increment (<code>12345</code>) from <a href="https://en.wikipedia.org/wiki/Linear_congruential_generator">ANSI C's <code>rand()</code></a>, but with 32-bit unsigned wraparound rather than ANSI C's mod 2</sup>31.
The <code>chain_id</code> is mixed in between two iterations, and the final modulo reduces the 32-bit output to a slot index:</p>
<pre><code>rand = merkle_nonce
rand = rand * 1103515245 + 12345
rand = rand + chain_id
rand = rand * 1103515245 + 12345
slot = rand mod merkle_size
</code></pre>
<p>The result is a slot index between 0 and <code>merkle_size - 1</code>.</p>
<p>However, if two chains' <code>chain_id</code>s collide for a given power-of-two <code>merkle_size</code>, changing <code>merkle_nonce</code> does not fix the collision; it only moves both chains together.
The miner must use a larger <code>merkle_size</code>, omit one chain, or rely on chain IDs that do not collide modulo the chosen tree size.
And because each chain's verifier independently runs the LCG to find its expected slot, a miner cannot silently put two chains at the same leaf: a verifier whose slot is occupied by another chain's commitment rejects the AuxPoW.</p>
<p>The simplest case is <code>merkle_size = 1</code>, where a miner is committing to only one auxiliary chain.
The tree collapses to a single leaf: the chain's block hash sits at the root directly, and the <code>merkle_nonce</code> is irrelevant (the LCG always returns slot 0).</p>
<p>In practice, miners committing to multiple AuxPoW chains do so via a single shared slot tree: the same hashing effort can earn a block reward from every chain in the tree whose <code>aux_target</code> the resulting hash clears.
A pool mining Namecoin alongside Elastos and other AuxPoW chains picks a <code>merkle_size</code> to fit the chains it commits to, with each occupying the slot its <code>chain_id</code> resolves to.</p>
<p>Each participating chain ends up with a different proof through this one shared tree: its own slot, its own sibling hashes, its own bit-path up to the root.
The parent block, the coinbase marker, and the <code>aux_merkle_root</code> are common to all of them; the per-chain proof is what differs.
The <a href="#worked-example">worked example</a> later in this post walks through such a tree in detail.</p>
<h2 id="the-auxpow-record">The AuxPoW record</h2>
<p>The <a href="https://en.bitcoin.it/wiki/Merged_mining_specification#Aux_proof-of-work_block">standard AuxPoW format</a>, originally specified by Namecoin and adopted byte-for-byte by most Bitcoin-parent AuxPoW chains, puts an AuxPoW record between the 80-byte auxiliary header and the block body's transaction list.
This AuxPoW record is a variable-sized structure with five fields, as shown in Figure 6.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/merge-mining-auxpow-record.png" alt="Figure 6: Layout of the AuxPoW record carried by every auxiliary block. coinbase_txn and parent_block_header are copied parent-side data; parent_block_hash is a redundant derived hash. The other two fields (coinbase_branch, blockchain_branch) are merkle paths that let the auxiliary chain&#x27;s verifier reconstruct the link to the parent&#x27;s proof-of-work."></p>
<p>Three fields are parent-side data carried into the auxiliary block.
<code>coinbase_txn</code> is the full serialised parent coinbase transaction; the verifier scans its <code>scriptSig</code> for the <code>0xFA BE 6D 6D</code> magic and reads the <code>aux_merkle_root</code> out of the bytes that follow.
[[<code>parent_block_hash</code>||This field is unnecessary for validation because the full parent header is already present in the same AuxPoW record; the <a href="https://en.bitcoin.it/wiki/Merged_mining_specification#Aux_proof-of-work_block">Bitcoin Wiki</a> flags it as such.]] is a 32-byte convenience value that the verifier ignores.
Early Namecoin AuxPoW blocks even shipped it in inconsistent byte order (some in standard little-endian internal byte order, some reversed) without breaking validation.
<code>parent_block_header</code>, by contrast, is the field that actually drives validation: the verifier computes SHA-256d over its full 80 bytes and tests the result against <code>aux_target</code> to confirm the auxiliary block's proof-of-work.</p>
<p>The remaining two fields are compact merkle proofs, not copies of the full parent transaction tree or the full auxiliary slot tree.
They give the verifier just enough sibling hashes to fold a known starting hash up to the parent's transaction <code>merkle_root</code> (for <code>coinbase_branch</code>) and the <code>aux_merkle_root</code> (for <code>blockchain_branch</code>).</p>
<p>For <code>coinbase_branch</code>, the start hash is <code>txid(coinbase_txn)</code> and the expected root is the Bitcoin transaction <code>merkle_root</code> from <code>parent_block_header</code>.
This proves that the proof-of-work in <code>parent_block_header</code> was performed over a transaction merkle root consistent with a tree containing <code>coinbase_txn</code>. That coinbase carries the AuxPoW marker that commits to this auxiliary chain (and to any others sharing the same slot tree).</p>
<p>For <code>blockchain_branch</code>, the start hash is this auxiliary block's hash and the expected root is the <code>aux_merkle_root</code> found in the parent coinbase's AuxPoW marker.
This proves that the coinbase marker commits to this auxiliary block's hash (alongside any other chains' block hashes in the shared slot tree).</p>
<p>The two branch payloads carry the "middle part" of those two checks: the sibling hashes and the side masks needed to fold the start hash up to the expected root (see the <a href="#merkle-branch-details">Merkle branch details</a> section below).
How these fields combine into a verification check is the subject of <a href="#verification">the next section</a>.</p>
<p><strong>Merkle branch details</strong></p>
<p>{#merkle-branch-details}
Both branches use the same three-input fold pattern:</p>
<ul>
<li>A <em>start hash</em>, fetched from surrounding AuxPoW data (it is never stored inside the branch payload).</li>
<li>A <em>branch payload</em>: a CompactSize sibling count, the sibling hashes themselves, and a 4-byte <em>side mask</em> whose role differs per branch.</li>
<li>An <em>expected root</em>, also fetched from surrounding AuxPoW data.</li>
</ul>
<p>The fold sets <code>current</code> to the start hash, then walks <code>branch_hash[]</code> one sibling at a time, reading the matching bit from the side mask starting at bit 0.
If the bit is 0, <code>current</code> sat on the left at that tree level and the step is <code>current = SHA-256d(current || sibling)</code>; if the bit is 1, <code>current</code> sat on the right and the step is <code>current = SHA-256d(sibling || current)</code>.
Read from bit 0 upward, the side mask is therefore the leaf-to-root path of the value being proved.
After the last sibling, <code>current</code> must equal the expected root.</p>
<p>The two branches differ in what each input refers to and in what the side mask encodes.</p>
<p><strong><code>coinbase_branch</code></strong></p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/merge-mining-coinbase-branch.png" alt="Figure 7: coinbase_branch shown as a merkle proof through the parent Bitcoin transaction tree. The branch field carries the CompactSize length, the sibling hashes, and the 4-byte side mask; the coinbase txid and the expected parent header merkle_root come from surrounding AuxPoW data."></p>
<ul>
<li><strong>Start hash</strong>: <code>txid(coinbase_txn)</code>, the SHA-256d of the serialised parent coinbase transaction carried in this same AuxPoW record.</li>
<li><strong>Expected root</strong>: the Bitcoin transaction <code>merkle_root</code> from <code>parent_block_header</code>.</li>
<li><strong>Side mask</strong>: all four bytes are zero, because the coinbase is always at leaf 0 of the parent transaction tree, putting <code>current</code> on the left at every level.</li>
</ul>
<p>A successful fold ends with <code>current</code> equal to the parent header's <code>merkle_root</code>.</p>
<p><strong><code>blockchain_branch</code></strong></p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/merge-mining-blockchain-branch.png" alt="Figure 8: blockchain_branch shown as a merkle proof through the auxiliary merkle slot tree. The branch field carries the CompactSize length, the sibling hashes, and the 4-byte side mask (this chain&#x27;s slot index); the auxiliary block hash and expected aux_merkle_root come from surrounding AuxPoW data."></p>
<ul>
<li><strong>Start hash</strong>: this auxiliary block's header hash.</li>
<li><strong>Expected root</strong>: the <code>aux_merkle_root</code> embedded in the parent coinbase's AuxPoW marker.</li>
<li><strong>Side mask</strong>: the four bytes hold <em>this chain's</em> slot index in the slot tree, little-endian, so its binary digits are already the per-level left/right pattern the fold needs (bit 0 at the leaf level).
The mask is chain-specific: every other auxiliary chain merge-mined into the same parent Bitcoin block carries its own <code>blockchain_branch</code> (own start hash, own sibling hashes, own slot index), and they all fold up to the same shared <code>aux_merkle_root</code>.</li>
</ul>
<p>The verifier also independently derives the expected slot index from <code>chain_id</code>, <code>merkle_nonce</code>, and <code>merkle_size</code>, and rejects the proof if it differs from the side-mask value.
When the slot tree has only one leaf (<code>merkle_size = 1</code>), the payload reduces to 5 bytes of zeros: [[<code>0x00 00 00 00 00</code>||One byte of CompactSize length (zero siblings) followed by a 4-byte side mask of all zeros. The Bitcoin Wiki <a href="https://en.bitcoin.it/wiki/Merged_mining_specification#Example">worked example</a> shows this exact pattern.]].
The fold has no siblings to process, so the start hash is already the expected root.
A successful fold ends with <code>current</code> equal to the <code>aux_merkle_root</code> from the parent coinbase marker.</p>
<h2 id="verification">Verification</h2>
<p>For an auxiliary chain's node, verification answers a single question: does this candidate AuxPoW block actually inherit the parent's proof-of-work?
The PoW link verification can be summarised as five checks, and the PoW link is valid only if all of them pass (Figure 9).</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/merge-mining-verification.png" alt="Figure 9: Five-step verification flowchart that an auxiliary chain node walks for each candidate AuxPoW block: (1) locate the AuxPoW marker in coinbase_txn&#x27;s scriptSig and extract aux_merkle_root, merkle_size, and merkle_nonce, (2) cross-check that the LCG-derived slot for this chain matches blockchain_branch&#x27;s side mask, (3) fold this aux block&#x27;s hash up through blockchain_branch and confirm the reconstructed root matches the marker&#x27;s aux_merkle_root, (4) fold the coinbase txid up through coinbase_branch and confirm the reconstructed root matches parent_block_header.merkle_root, (5) verify SHA-256d(parent_block_header) ≤ aux_target. All five must pass."></p>
<p>The order below establishes identity first (steps 1 and 2) before walking the chain of commitments from the auxiliary block out to the parent's proof-of-work (steps 3 through 5).
Real implementations (e.g., Elastos's <a href="https://github.com/elastos/Elastos.ELA/blob/master/auxpow/auxpow.go"><code>AuxPow.Check</code></a>, following the <a href="https://en.bitcoin.it/wiki/Merged_mining_specification">Bitcoin Wiki specification</a>) sequence the same checks differently and also perform additional structural validation of the AuxPoW record.</p>
<ol>
<li><strong>Locate the AuxPoW marker inside the parent coinbase</strong>: [[scan||When the magic is present it must appear only once in the <code>scriptSig</code>, blocking an "equivocation attack" where a miner embeds different markers for different chains. The <a href="#the-coinbase-commitment">legacy no-magic form</a> (aux merkle root in the first 20 bytes) is also permitted but rarely seen.]] <code>coinbase_txn</code>'s <code>scriptSig</code> for the <code>0xFA BE 6D 6D</code> magic, and read the embedded <code>aux_merkle_root</code>, <code>merkle_size</code>, and <code>merkle_nonce</code> from the bytes that follow.</li>
<li><strong>Cross-check the slot index</strong>: independently derive the expected slot by running the <a href="#slot-mapping-details">LCG</a> over <code>chain_id</code> and the marker's <code>merkle_nonce</code> and <code>merkle_size</code>, and confirm it matches the slot encoded in <code>blockchain_branch</code>'s side mask.</li>
<li><strong>Reconstruct <code>aux_merkle_root</code></strong>: starting from this auxiliary block's hash (SHA-256d of its 80-byte header), fold the hash up through <code>blockchain_branch</code> (using the side-mask bits validated in step 2 to position <code>current</code> left or right at each level), and confirm the resulting root matches the <code>aux_merkle_root</code> extracted from the marker in step 1.</li>
<li><strong>Reconstruct the parent block's transaction <code>merkle_root</code></strong>: compute the coinbase txid (SHA-256d of <code>coinbase_txn</code>) and fold it up through <code>coinbase_branch</code>, and confirm the resulting root matches the <code>merkle_root</code> field inside <code>parent_block_header</code>.</li>
<li><strong>Verify the proof-of-work</strong>: compute SHA-256d of <code>parent_block_header</code> and check that the resulting hash is at or below <code>aux_target</code>.</li>
</ol>
<p>Step 5's <code>aux_target</code> test, looser than Bitcoin's <code>parent_target</code>, is what admits aux-only parent headers (those clearing <code>aux_target</code> but not <code>parent_target</code>) into the auxiliary chain's record.
Combined with the canonical blocks and stales that clear both targets, the record collects all <a href="#two-chains-one-hash">three populations of parent headers</a> the post identified earlier.</p>
<p><strong>Worked example: Namecoin block 823,506</strong></p>
<p>{#worked-example}
We walk through verification on a recent Namecoin block: <a href="https://chainz.cryptoid.info/nmc/block.dws?3e78ac3489c58c087428a1536b1b93c656f846419dd78190d2fd05889c9f6d88.htm">block 823,506</a>, mined by AntPool.
This is a typical modern AuxPoW block:</p>
<ul>
<li><code>merkle_size = 16</code>: multiple AuxPoW chains committed via the slot tree,</li>
<li>A 4-sibling <code>blockchain_branch</code>: one sibling per merkle level, (\log_2(16) = 4), and</li>
<li>A 13-sibling <code>coinbase_branch</code>.</li>
</ul>
<p>The relevant fields from the block JSON (annotated; the full record is at the link above; <code>chainindex</code> is the JSON name for the side mask):</p>
<pre><code>{
  "hash":       "3e78ac34...9c9f6d88",                     // aux block (Namecoin) hash
  "auxpow": {
    "chainindex": 11,                                      // Namecoin's slot in the parent's slot tree
    "chainmerklebranch": [                                 // blockchain_branch siblings (4)
      "000000000000000000000000000000000000000000000000000000000000000a",
      "8ccfc263c108561c4e1c6c30b49fdab67c4ac12090291de80b9f07725c72717c",
      "ace26173348b3779777fdfd38835f76b2e5e0cf0f1e32b3c01fd9c6822c7c9b1",
      "a697adc4c5f4b32251e5ecd1aa16d038256e3750b8639c51df780745cc2db842"
    ],
    "merklebranch": [ /* 13 hashes - coinbase_branch siblings, omitted */ ],
    "tx": {
      "txid":      "c6cd5ec9...14e37cd5",                  // parent coinbase txid
      "vin":       [{"coinbase": "03ab770e...00000000"}],  // dissected below
      "blockhash": "00000000...20906c38"                   // parent Bitcoin block hash
    },
    "parentblock": "00000420...61514998"                   // serialised 80-byte parent header
  }
}
</code></pre>
<p>The parent coinbase <code>scriptSig</code>, dissected into fields:</p>
<pre><code>"03ab770e"                                                         // BIP 34 push: parent height 948,139
"19" "4d696e656420627920416e74506f6f6c20"                          // 25-byte push containing a 17-byte tag: "Mined by AntPool "
     "87003601907790c0"                                            //   ...and 8 bytes pool extranonce
"fabe6d6d"                                                         // AuxPoW magic
"5a9c0198abca4e8c94a1ce47b0f09cf843e33a6a4d3aaf6b9240b08061d68a42" // aux_merkle_root (matches step 3 fold)
"10000000"                                                         // merkle_size = 16 (LE u32)
"00000000"                                                         // merkle_nonce = 0
"0000e66cf007010000000000"                                         // trailing scriptSig bytes (pool extras)
</code></pre>
<p><strong>Step 1 (locate marker in parent coinbase).</strong> Inside the parent coinbase's <code>scriptSig</code>, the magic <code>fa be 6d 6d</code> sits at byte offset 30, after a BIP 34 height push and a 25-byte push containing the 17-byte ASCII tag <code>Mined by AntPool </code> plus 8 bytes of pool extranonce.
The 32 bytes following the magic are <code>5a9c0198...61d68a42</code>, which we'll match against the fold result in step 3.
The trailing <code>10 00 00 00</code> and <code>00 00 00 00</code> give <code>merkle_size = 16</code> and <code>merkle_nonce = 0</code>.</p>
<p><strong>Step 2 (cross-check slot index).</strong> Running the LCG over <code>chain_id = 1</code> (Namecoin), <code>merkle_nonce = 0</code>, and <code>merkle_size = 16</code> (the latter two from the marker bytes in step 1) returns 11, matching the side mask carried in <code>blockchain_branch</code>'s <code>chainindex</code> field.</p>
<p><strong>Step 3 (reconstruct <code>aux_merkle_root</code>).</strong> Namecoin's slot in this block is 11 (validated in step 2), carried in the AuxPoW record's <code>chainindex</code> field (the JSON-friendly form of the 4-byte side mask at the tail of <code>blockchain_branch</code>).
That slot index doubles as the side mask for the four-sibling replay: slot 11 in binary is <code>1011</code>, so reading bit 0 (the LSB) first, the side bits at levels 0, 1, 2, 3 are <code>1, 1, 0, 1</code>.</p>
<p>We start <code>current</code> at the aux block header hash from the JSON's <code>hash</code> field, then apply four SHA-256d steps, each pairing <code>current</code> with the next sibling from <code>chainmerklebranch[]</code>:</p>
<pre><code>current = 3e78ac34...9c9f6d88   (aux block header hash)

level 0 (bit 1, current on the right):
  current = SHA-256d(00000000...0000000a ‖ 3e78ac34...9c9f6d88)
          = 1ef8737d...e74dc89f

level 1 (bit 1, current on the right):
  current = SHA-256d(8ccfc263...5c72717c ‖ 1ef8737d...e74dc89f)
          = 3c11d081...44b313cf

level 2 (bit 0, current on the left):
  current = SHA-256d(3c11d081...44b313cf ‖ ace26173...22c7c9b1)
          = fe16c528...4a10d456

level 3 (bit 1, current on the right):
  current = SHA-256d(a697adc4...cc2db842 ‖ fe16c528...4a10d456)
          = 5a9c0198...61d68a42
</code></pre>
<p>The final <code>current</code>, <code>5a9c0198...61d68a42</code>, is the reconstructed <code>aux_merkle_root</code>, matching the <code>aux_merkle_root</code> extracted from the marker in step 1.</p>
<p><strong>Step 4 (reconstruct parent <code>merkle_root</code> and match).</strong> The start hash is the coinbase txid, <code>c6cd5ec9...14e37cd5</code>.
The side mask is <code>0x00000000</code>, so at every level <code>current</code> is on the left and the sibling on the right (the coinbase is always leaf 0 regardless of how many transactions sit beside it).
The branch carries 13 siblings, consistent with a parent transaction merkle tree of 4,097 to 8,192 leaves. Folding all 13 in yields <code>d63208d5...3d0aa08d</code>, matching the <code>merkle_root</code> field of <code>parent_block_header</code>.</p>
<p><strong>Step 5 (verify PoW).</strong> SHA-256d of <code>parent_block_header</code> reproduces the parent block hash.
Namecoin's <code>aux_target</code> at this height has compact form <code>0x1703e336</code>, which expands to a 32-byte target with nine leading zero bytes followed by <code>0x03 e3 36 00 ...</code>.
Lining up the hash and target as big-endian byte strings:</p>
<pre><code>hash:    00 00 00 00 00 00 00 00 00 02 ...
target:  00 00 00 00 00 00 00 00 00 03 e3 36 ...
</code></pre>
<p>The first nine bytes are equal; at the tenth byte, the hash (<code>0x02</code>) is below the target (<code>0x03</code>), so the hash is below <code>aux_target</code> and the parent's work clears it.</p>
<p>That same hash, however, exceeds Bitcoin's <code>parent_target</code> at this height (compact <code>bits = 0x17021ff0</code>), making this parent <em>aux-only</em> (the third category from <a href="#two-chains-one-hash">Two chains, one hash</a>).
Bitcoin's canonical block at height 948,139 is a different hash (<code>000000000000000000018551...87613a79</code>); only Namecoin's AuxPoW record preserves the parent header here.</p>
<p><strong>Aside on the slot 10 filler.</strong> The level-0 sibling, <code>00000000...0000000a</code>, is the value occupying slot 10 in this AntPool block's slot tree.
A genuinely occupied slot would carry another auxiliary chain's 32-byte block hash (high entropy throughout); this near-zero pattern is consistent with an unused slot.</p>
<h2 id="op_return-tag-based-merge-mining"><code>OP_RETURN</code> tag-based merge mining</h2>
<p>AuxPoW is not the only way for a chain to reuse Bitcoin's proof-of-work.
Some chains instead embed a chain-specific marker in a dedicated <code>OP_RETURN</code> output in the parent's coinbase.
Unlike AuxPoW, where the data is embedded in the parent's coinbase transaction input (<code>scriptSig</code>), these markers live in the coinbase transaction outputs (<code>scriptPubKey</code>).</p>
<p>There doesn't appear to be a settled name for the technique, so we'll refer to it as <em><code>OP_RETURN</code> tag-based merge mining</em>.
The <a href="https://research.mempool.space/merge-mining-report/">Mempool Research merge-mining report</a> describes the common form as a zero-value <code>OP_RETURN</code> carrying a tag (usually ASCII) unique to the merge-mined chain.
A bare tag only marks the parent block as co-mined; it is only when the tag is followed by a payload (a hash of the aux chain's block, or similar) that the aux chain can verify Bitcoin's PoW for that specific block.</p>
<p>This should not be confused with <em>blind merge mining</em> (BMM), the Drivechain (<a href="https://github.com/bitcoin/bips/blob/master/bip-0300.mediawiki">BIP 300</a>, <a href="https://github.com/bitcoin/bips/blob/master/bip-0301.mediawiki">BIP 301</a>) concept where the miner does not run the sidechain node.
BIP 301 also uses an <code>OP_RETURN</code> commitment, so the difference is not the embedding location but the miner's relationship to the sidechain.</p>
<p>In <code>OP_RETURN</code> tag-based merge mining, each output is a coinbase <code>txout</code> whose <code>scriptPubKey</code> is <code>OP_RETURN &#x3C;chain tag> &#x3C;payload></code>, where the tag is a short ASCII identifier and the payload's contents vary widely between chains: an aux block hash for direct PoW binding (e.g., <a href="https://ips.rootstock.io/IPs/RSKIP92.html">RSK</a>, tag <code>RSKBLOCK:</code>), accounting metadata for hashrate delegation (e.g., CoreDAO, tag <code>CORE</code>), an aux block hash and height tag for fork-aware cross-chain bridging (e.g., <a href="https://github.com/syscoin/syscoin/blob/master/doc/release-notes/release-notes-5.0.0.md#4-auxpow-tags">Syscoin</a>, tag <code>sys</code>), or other chain-specific data.</p>
<p>Since SegWit activated in 2017, almost every Bitcoin coinbase has already carried one <code>OP_RETURN</code> by default: per <a href="https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#commitment-structure">BIP 141</a>, every coinbase whose block contains a witness-bearing transaction must include an <code>OP_RETURN</code> with magic prefix <code>0xaa21a9ed</code> followed by a 32-byte commitment to the witness merkle root.
Bitcoin coinbases can carry multiple <code>OP_RETURN</code> outputs, so tag-based merge-mining commitments simply add to that mandatory one.
The coinbase of <a href="https://mempool.space/block/00000000000000000001144d36d598ad54dc664e6387ce1aae928117c44435dc">block 948,433</a>, mined by SpiderPool, is one such case (figure):</p>
<pre><code>OP_RETURN aa21a9ed7a379f...        SegWit witness commitment (BIP 141, mandatory)
OP_RETURN CORE 01 64db24a6...      CoreDAO hash-delegation metadata
OP_RETURN EXSAT 01 120f0803...     exSat synchronizer metadata
OP_RETURN RSKBLOCK: 6e48ff1e...    RSK merge-mining commitment
OP_RETURN sys 615097dc...          Syscoin merge-mining commitment
</code></pre>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/mempool-space-block-948433.png" alt="The coinbase of block 948,433 as it appears on mempool.space: multiple zero-value OP_RETURN outputs in the scriptPubKeys carrying the SegWit witness commitment, merge-mining commitments, and other project/protocol metadata, while the input scriptSig carries an AuxPoW marker, showing that this block is also a parent for AuxPoW chains (dissected below)."></p>
<p>Compared with AuxPoW, tag-based approaches avoid the shared tree, chain-ID coordination, the broken <code>merkle_nonce</code> collision-avoidance scheme, and the marker placement rule.
A miner participating in multiple such systems simply emits one coinbase <code>OP_RETURN</code> output for each system.</p>
<p>What these approaches give up is the shared verification model.
Each tag-based chain defines its own rule for what counts as a valid Bitcoin parent and how the parent header or Bitcoin block metadata is carried in its own format.
There is also no single specification document equivalent to the <a href="https://en.bitcoin.it/wiki/Merged_mining_specification">Merged mining specification</a>; each chain defines its own.</p>
<p>The two mechanisms coexist in the same Bitcoin coinbase because they live in different fields: AuxPoW commits via the input <code>scriptSig</code>, <code>OP_RETURN</code> tag-based approaches commit via output <code>scriptPubKey</code>s.
Bitcoin block <a href="https://mempool.space/block/00000000000000000001144d36d598ad54dc664e6387ce1aae928117c44435dc">948,433</a> as examined above illustrates this (figure): alongside five <code>OP_RETURN</code> outputs (the SegWit witness commitment plus four project/protocol tags, including RSK/Syscoin merge-mining commitments and Core/exSat coordination metadata), the coinbase <code>scriptSig</code> carries an AuxPoW marker that, with <code>merkle_size = 8</code>, commits to up to eight auxiliary chains:</p>
<pre><code>"03d1780e"                                                         // BIP 34 push: parent height 948,433
"045899fd69"                                                       // 4-byte push: pool extranonce (LE u32 = 0x69fd9958)
"537069646572506f6f6c2f3134372f"                                   // 15-byte ASCII tag: "SpiderPool/147/"
"fabe6d6d"                                                         // AuxPoW magic
"40c91d098550760af551762fa506da60fb1ed54256c9d5f9123816d8967111f9" // aux_merkle_root
"08000000"                                                         // merkle_size = 8 (LE u32)
"b9035e3f"                                                         // merkle_nonce (LE u32 = 0x3f5e03b9)
"b6f68743200090c94e12000000000000"                                 // trailing scriptSig bytes (pool extras)
</code></pre>
<p>That is, AuxPoW and tag-based commitments can reuse the same Bitcoin proof-of-work in the same parent block.
On the Namecoin side of this commitment, <a href="https://chainz.cryptoid.info/nmc/block.dws?9f982998fb47a657f7a7310de94bc932b03d6c9dfa500fe92ebfbb06a86b258a.htm">Namecoin block 823,793</a> carries Bitcoin <a href="https://mempool.space/block/00000000000000000001144d36d598ad54dc664e6387ce1aae928117c44435dc">948,433's</a> 80-byte header inside its AuxPoW record at slot 4 of the marker's 8-slot tree, exactly the side-channel introduced earlier in the post.
The full <a href="https://mempool.space/block/00000000000000000001144d36d598ad54dc664e6387ce1aae928117c44435dc">mempool.space</a> view of this coinbase, with the AuxPoW magic highlighted in the <code>scriptSig</code>, is shown in the <a href="#mempool-space-block-948433-details">full coinbase breakdown</a> below for reference.</p>
<p><strong>Full coinbase breakdown of Bitcoin block 948,433</strong></p>
<p>{#mempool-space-block-948433-details}</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/merge-mining/mempool-space-block-948433-details.png" alt="Block 948,433 coinbase on mempool.space: scriptSig on the left (with the fabe6d6d AuxPoW magic highlighted), P2PKH reward output and five OP_RETURN outputs on the right."></p>
<h2 id="conclusion">Conclusion</h2>
<p>AuxPoW gives an auxiliary chain a way to reuse Bitcoin's proof-of-work without any participation from Bitcoin itself.
This satisfies the idea Satoshi proposed in 2010: the parent network needs no coordination with any auxiliary chains, yet many chains can share the parent's hashing effort through a single coinbase commitment to a shared merkle slot tree of auxiliary block hashes.</p>
<p>A single SHA-256d evaluation of the parent header serves both sides.
The hash is tested against <code>parent_target</code> on Bitcoin's side and against <code>aux_target</code> on the auxiliary chain's side (almost always the easier threshold), with three possible outcomes: rejected, aux-only valid, or both parent and aux valid.</p>
<p>The AuxPoW specification makes this verifiable on the auxiliary side using three pieces.
A 44-byte AuxPoW marker in the parent coinbase <code>scriptSig</code> binds the parent block to that shared merkle slot tree of auxiliary block hashes.
An AuxPoW record on the auxiliary chain carries the parent coinbase, the parent header, and the merkle paths needed to verify that commitment.
A five-step verification rule then validates the PoW link regardless of whether Bitcoin's canonical chain ever accepts the parent block.</p>
<p>Together the three pieces let the auxiliary chain treat any candidate hash that clears <code>aux_target</code> as proof-of-work for its block, whether or not Bitcoin accepts the parent.
Every AuxPoW record also retains its full parent header, including headers Bitcoin's canonical chain discarded, making AuxPoW chains a side-channel into Bitcoin's PoW history.</p>
<p><code>OP_RETURN</code> tag-based merge mining sits alongside AuxPoW as a lighter, per-chain alternative: a coinbase <code>OP_RETURN</code> output per chain rather than a shared <code>scriptSig</code> marker, with each chain free to define its own tag, payload format, and commitment semantics (e.g., PoW binding like AuxPoW, or another purpose such as delegation accounting or fork-aware bridge notarisation).
The two embedding locations coexist in the same coinbase without interfering (figure): AuxPoW commits via the input <code>scriptSig</code>, <code>OP_RETURN</code> tag-based approaches via the output <code>scriptPubKey</code>s.</p>
<p>A <a href="./merge-mining-chains-and-pools">companion post</a> catalogues every chain that has actually deployed AuxPoW with Bitcoin as its parent, and examines which Bitcoin mining pools embed merge-mining commitments in their coinbases.</p>
<p>A separate follow-up post will use the side-channel AuxPoW creates (auxiliary chains preserving parent headers Bitcoin discarded as stale) to put numbers on a long-standing claim about block propagation: that smaller mining pools produce more stale blocks, relative to their hashrate, than larger ones.</p>
<h2 id="references">References</h2>
<p><strong>Specifications and primary sources</strong></p>
<ul>
<li><a href="https://en.bitcoin.it/wiki/Merged_mining_specification">Merged mining specification (Bitcoin Wiki)</a></li>
<li><a href="https://bitcointalk.org/index.php?topic=769073.0">Community catalogue of merge-mined chain IDs (BitcoinTalk thread 769073)</a></li>
<li><a href="https://bitcointalk.org/index.php?topic=1790.0">BitDNS and Generalizing Bitcoin (BitcoinTalk, November 2010)</a></li>
<li><a href="https://bitcointalk.org/index.php?topic=1790.msg28696#msg28696">Satoshi: BitDNS as a separate chain sharing proof-of-work (BitcoinTalk, 9 December 2010)</a></li>
<li><a href="https://web.archive.org/web/20211220001040/https://forum.namecoin.org/viewtopic.php?t=217">Namecoin merge-mining hard fork announcement, block 19,200 (Namecoin Forum, 2021 Wayback snapshot; the live forum has been down since May 2025)</a></li>
<li><a href="https://bitcointalk.org/index.php?topic=51069.0">Nonce for merged mining chain merkle tree index (BitcoinTalk)</a></li>
<li><a href="https://www.namecoin.org/">Namecoin project site</a></li>
<li><a href="https://github.com/bitcoin/bips/blob/master/bip-0034.mediawiki">BIP 34: Block v2, Height in Coinbase</a></li>
<li><a href="https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki">BIP 141: Segregated Witness (Consensus layer)</a></li>
<li><a href="https://github.com/bitcoin/bips/blob/master/bip-0300.mediawiki">BIP 300: Hashrate Escrows (Drivechains)</a></li>
<li><a href="https://github.com/bitcoin/bips/blob/master/bip-0301.mediawiki">BIP 301: Blind Merged Mining</a></li>
<li><a href="https://ips.rootstock.io/IPs/RSKIP92.html">RSKIP-92: Merkle proof serialization for merge-mined RSK blocks (Rootstock)</a></li>
<li><a href="https://github.com/syscoin/syscoin/blob/master/doc/release-notes/release-notes-5.0.0.md#4-auxpow-tags">Syscoin 5.0.0 release notes: AuxPoW tags</a></li>
</ul>
<p><strong>Papers and long-form explainers</strong></p>
<ul>
<li><a href="https://eprint.iacr.org/2018/1134.pdf">Stifter, Schindler, Judmayer, Zamyatin, Kern &#x26; Weippl. Echoes of the Past: Recovering Blockchain Metrics From Merged Mining (FC 2019)</a></li>
<li><a href="https://repositum.tuwien.at/bitstream/20.500.12708/5239/2/Zamyatin%20Alexei%20-%202017%20-%20Merged%20mining%20analysis%20of%20effects%20and%20implications.pdf">Zamyatin, A. Merged Mining: Analysis of Effects and Implications (TU Wien diploma thesis, 2017)</a></li>
<li><a href="https://eprint.iacr.org/2017/791.pdf">Judmayer, Zamyatin, Stifter, Voyiatzis &#x26; Weippl. Merged Mining: Curse or Cure? (CBT 2017)</a></li>
<li><a href="https://tlu.tarilabs.com/mining/MergedMiningIntroduction">Tari Labs University: Merged Mining Introduction</a></li>
<li><a href="https://research.mempool.space/merge-mining-report/">Mempool Research: Merge-mining report</a></li>
<li><a href="https://blog.bitmex.com/the-growth-of-bitcoin-merge-mining/">BitMEX Research: The Growth of Bitcoin Merge Mining (2020)</a></li>
<li><a href="https://bitslog.com/2022/11/22/merged-mining-part-i/">Sergio Demian Lerner: Merged mining (Part I) (Bitslog, 2022)</a></li>
<li><a href="https://learnmeabitcoin.com/technical/block/nonce/">learnmeabitcoin: Nonce (interactive extranonce explainer)</a></li>
<li><a href="https://en.wikipedia.org/wiki/Linear_congruential_generator">Linear congruential generator (Wikipedia)</a></li>
</ul>
<p><strong>Reference implementations and worked-example data</strong></p>
<ul>
<li><a href="https://github.com/namecoin/namecoin-core/blob/master/src/auxpow.cpp">Namecoin Core: <code>src/auxpow.cpp</code> (the canonical C++ AuxPoW verification)</a></li>
<li><a href="https://github.com/elastos/Elastos.ELA/blob/master/auxpow/auxpow.go">Elastos.ELA: <code>auxpow/auxpow.go</code> (Go port of the same verification logic)</a></li>
<li><a href="https://github.com/bitcoin/bitcoin/blob/master/src/consensus/tx_check.cpp#L49-L50">Bitcoin Core: <code>src/consensus/tx_check.cpp</code> (coinbase <code>scriptSig</code> length limit)</a></li>
<li><a href="https://chainz.cryptoid.info/nmc/block.dws?3e78ac3489c58c087428a1536b1b93c656f846419dd78190d2fd05889c9f6d88.htm">Namecoin block 823,506 (chainz.cryptoid.info)</a></li>
<li><a href="https://mempool.space/block/00000000000000000001144d36d598ad54dc664e6387ce1aae928117c44435dc">Bitcoin block 948,433 (mempool.space)</a></li>
<li><a href="https://chainz.cryptoid.info/nmc/block.dws?9f982998fb47a657f7a7310de94bc932b03d6c9dfa500fe92ebfbb06a86b258a.htm">Namecoin block 823,793 (chainz.cryptoid.info)</a>, the aux-chain block parented by Bitcoin block 948,433</li>
</ul>]]></content:encoded>
            <category>bitcoin</category>
            <category>merge-mining</category>
            <category>auxpow</category>
            <category>explainer</category>
        </item>
        <item>
            <title><![CDATA[Bitcoin node roles and network topology]]></title>
            <link>https://deadmanoz.xyz/posts/2026/bitcoin-node-roles-p2p</link>
            <guid isPermaLink="false">https://deadmanoz.xyz/posts/2026/bitcoin-node-roles-p2p</guid>
            <pubDate>Thu, 02 Apr 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[An examination of the roles, configuration, and connection management of Bitcoin nodes, and the resultant topology of the network]]></description>
            <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>Bitcoin's peer-to-peer (P2P) network is a distributed system that allows network participants (nodes) to interact without relying on a central authority.
For the network to operate as efficiently and as decentralised as possible, Bitcoin nodes can operate in a variety of modes, fulfilling different roles.
This post examines the various roles and modes of Bitcoin nodes and the topology of the Bitcoin network that this differentiation has led to.</p>
<h2 id="node-reachability">Node reachability</h2>
<p>One way of categorising nodes on the Bitcoin network is whether they are <strong>reachable</strong> or not.
In essence, a node is reachable if other nodes on the network can initiate inbound connections to it.</p>
<p>More concretely, a node is likely to be reachable when it is configured to accept inbound connections (<code>-listen=1</code>, the default), binds to an appropriate network interface, and its listening port (default 8333 for mainnet) is accessible from the outside, either because the machine has a public IP directly, or because NAT/firewall rules forward traffic appropriately.
There are also other considerations such as number of available connection slots for inbound peers, and whether the necessary plumbing is in place for alternative networks (e.g., a Tor hidden service, an I2P service, or a CJDNS address).</p>
<p><strong>NAT, CGNAT, and node reachability</strong></p>
<p>When the ISP assigns a public IP address (static or dynamic) directly to a customer's router, the router uses <strong>Network Address Translation (NAT)</strong> to share that address across devices on the customer's local network.
Outbound connections work transparently, the router tracks the mapping and routes replies back, but unsolicited inbound connections have no existing mapping and are dropped.
Since the router holds a public IP, this is straightforward to solve: a <strong>port forwarding</strong> rule on the router can direct inbound traffic on the listening port (e.g. 8333) to the node's private LAN address.</p>
<p>Some routers support <strong>NAT-PMP</strong> (NAT Port Mapping Protocol) or <strong>PCP</strong> (Port Control Protocol) to automate this.
Bitcoin Core ships a built-in PCP/NAT-PMP implementation (since <a href="https://bitcoincore.org/en/releases/29.0/">v29.0</a> with <a href="https://github.com/bitcoin/bitcoin/pull/30043">PR #30043</a>) and enables it by default (since <a href="https://bitcoincore.org/en/releases/30.0/">v30.0</a> with <a href="https://github.com/bitcoin/bitcoin/pull/33004">PR #33004</a>), so a listening node behind a compatible router will automatically map its port without manual configuration.
Earlier versions relied on UPnP via the miniupnpc library, but that was disabled by default in <a href="https://bitcoincore.org/en/releases/0.11.1/">v0.11.1</a> after <a href="https://bitcoincore.org/en/2024/07/03/disclose_upnp_rce/">multiple security vulnerabilities</a> and <a href="https://bitcoincore.org/en/releases/29.0/">dropped entirely in v29.0</a>.</p>
<p>However, a growing share of connections never receive a public IP at all.
<strong>Carrier-Grade NAT (CGNAT)</strong> places multiple customers behind a shared public IP at the ISP level, so port forwarding on the home router is useless.
Mobile networks use CGNAT <a href="https://blog.cloudflare.com/detecting-cgn-to-reduce-collateral-damage/">almost universally</a>, and fixed-line adoption is growing as <a href="https://www.potaroo.net/tools/ipv4/">all five Regional Internet Registries have exhausted their IPv4 free pools</a>.
For node operators behind CGNAT, the main workarounds are overlay networks: <strong>Tor</strong> hidden services (.onion), <strong>I2P</strong> (supported since Bitcoin Core <a href="https://bitcoincore.org/en/releases/22.0/">v22.0</a> with <a href="https://github.com/bitcoin/bitcoin/pull/20685">PR #20685</a>), or <strong>CJDNS</strong> (since <a href="https://bitcoincore.org/en/releases/23.0/">v23.0</a> with <a href="https://github.com/bitcoin/bitcoin/pull/23077">PR #23077</a>), all of which bypass NAT entirely.</p>
<p>So the Bitcoin network consists of reachable nodes, openly connectable and therefore straightforward to survey, and unreachable nodes, which cannot accept inbound connections (due to not satisfying one or more of the criteria outlined above) and are therefore much harder to catalogue through network observation alone.
The primary way of learning about unreachable nodes is through <code>addr</code> messages gossiped across the network, with the caveat that an address appearing in gossip only confirms that a node <em>existed</em> at some point, not that it is still running.</p>
<p>As of April 2026, the long-running <a href="https://bitnodes.io/">Bitnodes project</a>,
which aims to estimate "the size of the Bitcoin peer-to-peer network by finding
all reachable and unreachable nodes", puts the total count at roughly 70,000,
with around 23,000 (about a third) being reachable.
That two-thirds are unreachable is unsurprising given the barriers facing home node operators: many sit behind NAT without port forwarding configured, and a growing share are behind CGNAT (see <a href="#node-reachability">above</a>), where inbound connections are impossible regardless of router settings.
Node-in-a-box products (Umbrel, Start9, RaspiBlitz) sidestep this by defaulting to Tor, which makes them reachable within the .onion network but not from the clearnet.</p>
<h2 id="node-connection-management">Node connection management</h2>
<p>Every node on the network automatically maintains a small set of outbound connections to peers it has selected itself.
Reachable nodes additionally allocate slots for inbound connections - peers that have discovered and connected to them.
In total, under a default configuration, a node has a maximum of <a href="https://github.com/bitcoin/bitcoin/blob/master/doc/reduce-traffic.md">125 automatic peer connections</a> (<a href="https://github.com/bitcoin/bitcoin/blob/master/src/net.h"><code>DEFAULT_MAX_PEER_CONNECTIONS</code></a>) to maintain (Figure 1).
The way these connection slots are budgeted, and the distinct roles assigned to different connection types, are deliberate design decisions that balance node resource constraints against the network's need for robust connectivity between nodes.
Note that, as explored in <a href="#other-connection-types">other connection types</a>, some special connection types have their own separate slot budgets and are not counted against this limit.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/bitcoin-node-roles-p2p/bitcoin-connection-budget.png" alt="Figure 1: Bitcoin Core connection slot budget showing the 125-slot breakdown: 8 full-relay outbound, 2 block-relay-only outbound, 1 feeler, and 114 inbound slots, with the trust asymmetry between outbound and inbound connections."></p>
<h3 id="outbound-connections">Outbound connections</h3>
<p>In a default configuration, a node has 10 persistent outbound peers, and one short-lived feeler, for <a href="https://github.com/bitcoin/bitcoin/blob/master/doc/reduce-traffic.md">11 outbound peers</a>.
The 10 persistent outbound peers consist of 8 outbound full-relay connections and 2 block-relay-only connections.
The full-relay connections exchange all message types - addresses, transactions and blocks - with a default maximum of 8 specified by <a href="https://github.com/bitcoin/bitcoin/commit/c2fa70ddfd7711d514a701b3a7c8adb561acc3ff">Satoshi in 2010</a>.
The block-relay-only connections, introduced in Bitcoin Core <a href="https://bitcoincore.org/en/releases/0.19.0.1/">v0.19.0.1</a> (in 2019 with <a href="https://github.com/bitcoin/bitcoin/pull/15759">PR #15759</a>), are only for the exchange of block headers and blocks.</p>
<p>Feeler connections are short-lived outbound connections opened approximately every 2 minutes to test whether addresses in the node's peer database are reachable, promoting them from the <code>new</code> table (learned via gossip, untested) to the <code>tried</code> table (successfully connected to at least once) on success.
These tables are part of the node's <strong>address manager</strong> (<code>AddrMan</code>), which tracks known peer addresses and is complex enough that it will be covered in a future post.</p>
<h3 id="inbound-connections">Inbound connections</h3>
<p>Unlike outbound connections, a reachable node has no say in who connects to it; any other node can claim one of its inbound slots.
Inbound connections are therefore less trusted.
A node relies on its outbound peers for its best approximation of the true network state.
Inbound peers still contribute by relaying blocks and transactions to the wider network, and may even be the first to deliver them.</p>
<p>With a default cap of 125 automatic connections, 11 of which are reserved for outbound, there are up to 114 inbound slots.
When a new peer attempts a connection, a node doesn't immediately refuse it.
Instead, it evaluates the existing inbound peers and evicts "the least useful" (which could turn out to be the new peer itself!).
Exploring the eviction logic is beyond the scope of this work, suffice to say it's a process that eliminates peers from being eviction candidates via multiple evaluation criteria, with the final remaining peer being the one that is dropped.</p>
<h3 id="inbound-vs-outbound-asymmetry">Inbound vs Outbound asymmetry</h3>
<p>Since anyone can cheaply claim inbound slots, they are inherently vulnerable to <strong>Sybil attacks</strong> that manipulate the node's view of the network.
Outbound connections, being self-selected from the address database with limited slots and randomised selection, are far harder to subvert.
Several design decisions reinforce this asymmetry.</p>
<p><strong>What's a Sybil attack?</strong></p>
<p>A Sybil attack is when a single adversary creates many fake identities (in Bitcoin's case, many seemingly independent nodes) to gain disproportionate influence over a target.
Since Bitcoin's P2P network is permissionless and there is little cost to spinning up new nodes, an attacker can flood a victim with connections that all appear to be distinct peers but are actually controlled by the same entity.
If a node's connections are dominated by Sybil peers, those peers can collaborate to feed it a distorted view of the network: withholding blocks, censoring transactions, or manipulating relay timing.
The outbound/inbound asymmetry described here is one of the primary structural defences against this class of attack.</p>
<p>Transaction <code>inv</code> messages  are batched on a timer, with outbound peers receiving announcements every 2 seconds and inbound peers every 5 seconds.
The concern is transaction origin inference: a spy opening many inbound connections could correlate <code>inv</code> timing to deduce that the node originated a transaction.
The longer inbound interval ensures all inbound peers from the same network see the same <code>inv</code> simultaneously, eliminating differential timing signals regardless of how many connections the attacker opens.
Outbound peers pose a much lower Sybil risk, so the faster interval trades a small amount of privacy for better propagation speed.
That is, the batching makes the attack's effectiveness independent of how many inbound slots the adversary holds, adding more inbound (Sybil) connections yields no additional timing signal.</p>
<p>Outbound peers are also strongly preferred as block sources.
With inbound slots being cheap to Sybil, an attacker dominating a node's inbound connections could withhold new blocks, e.g. keeping the node on a stale chain tip, or could selectively delay block announcements to gain a timing advantage.
To guard against this, each outbound peer is marked as a preferred peer, meaning blocks are requested from outbound peers first, with inbound peers only used as a fallback when no preferred peers are available.</p>
<p>The same preference applies to header synchronisation: initial headers-first sync is only initiated from a preferred peer, with inbound peers as a fallback.</p>
<p>Finally, only inbound peers are subject to the eviction process described above.
Outbound peers are not part of the inbound eviction process, since losing a self-selected peer would weaken the node's trusted view of the network.
They can, however, still be disconnected for other peer-management reasons, such as stalling detection, extra block-relay peer rotation, or the network-diversity rebalancing discussed in <a href="#bridge-nodes">bridge nodes</a> below.</p>
<h3 id="address-bootstrapping">Address bootstrapping</h3>
<p>There is a bootstrapping problem that a special class of outbound connection, <code>ConnectionType::ADDR_FETCH</code>, exists to solve: how does a node populate its address book when it doesn't yet know any peers?
On first startup, or [[under various other circumstances||Such as after <code>peers.dat</code> has been deleted or corrupted, after being offline long enough for all cached addresses to exceed <a href="https://github.com/bitcoin/bitcoin/blob/master/src/addrman.h"><code>ADDRMAN_HORIZON</code></a> (30 days), or after enough connection failures to mark all addresses "terrible" (<a href="https://github.com/bitcoin/bitcoin/blob/master/src/addrman.h"><code>ADDRMAN_RETRIES</code>/<code>ADDRMAN_MAX_FAILURES</code></a>).]], <code>AddrMan</code> will be empty and the node will have nobody to connect to.</p>
<p>The primary bootstrap mechanism is <strong>DNS seeds</strong>: the node queries hardcoded DNS hostnames (see below) with a service-bit filter prefix that resolve to lists of active node IP addresses.
If the requesting node is behind a proxy where direct DNS isn't possible, or if a seed doesn't support the requested service-bit filter and returns no results, the node falls back to opening an <code>ADDR_FETCH</code> connection directly to the seed.
This is a short-lived connection that completes a version handshake, sends a <code>getaddr</code> message, receives an <code>addr</code> response, feeds those addresses into <code>AddrMan</code>, and disconnects.
Note that the same mechanism serves <code>-seednode</code> peers: operator-specified peers that the node tries as a trusted bootstrapping source before falling back to the hardcoded DNS seeds.</p>
<p><strong>Mainnet DNS seeds (Bitcoin Core v31)</strong></p>
<p>As of v31, Bitcoin Core's mainnet configuration includes eight hardcoded DNS seeds in <a href="https://github.com/bitcoin/bitcoin/blob/v31.0rc2/src/kernel/chainparams.cpp#L144-L151"><code>src/kernel/chainparams.cpp</code></a>:</p>
<table>
<thead>
<tr>
<th>Hostname</th>
<th>Operator</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>seed.bitcoin.sipa.be</code></td>
<td>Pieter Wuille</td>
<td>Supports x1, x5, x9, xd</td>
</tr>
<tr>
<td><code>dnsseed.bluematt.me</code></td>
<td>Matt Corallo</td>
<td>Only supports x9</td>
</tr>
<tr>
<td><code>seed.bitcoin.jonasschnelli.ch</code></td>
<td>Jonas Schnelli</td>
<td>Supports x1, x5, x9, xd</td>
</tr>
<tr>
<td><code>seed.btc.petertodd.net</code></td>
<td>Peter Todd</td>
<td>Supports x1, x5, x9, xd</td>
</tr>
<tr>
<td><code>seed.bitcoin.sprovoost.nl</code></td>
<td>Sjors Provoost</td>
<td></td>
</tr>
<tr>
<td><code>dnsseed.emzy.de</code></td>
<td>Stephan Oeste</td>
<td></td>
</tr>
<tr>
<td><code>seed.bitcoin.wiz.biz</code></td>
<td>Jason Maurice</td>
<td></td>
</tr>
<tr>
<td><code>seed.mainnet.achownodes.xyz</code></td>
<td>Ava Chow</td>
<td>Supports x1, x5, x9, x49, x809, x849, xd, x400, x404, x408, x448, xc08, xc48, x40c</td>
</tr>
</tbody>
</table>
<p>The <code>x</code> prefixed service bit filters (e.g. x9 = <code>NODE_NETWORK | NODE_WITNESS</code>) allow seeds to return only nodes advertising specific service flags.</p>
<p><code>ADDR_FETCH</code> connections are short-lived (they disconnect as soon as addresses are received) and use the shared automatic outbound pool on a best-effort basis, so they do not displace the node's persistent outbound peers and may simply be skipped when no outbound slot is free.</p>
<h3 id="other-connection-types">Other connection types</h3>
<p>Beyond the connection types discussed so far, Bitcoin Core defines two more outbound connection types: operator-specified peers (<code>ConnectionType::MANUAL</code>) and privacy-preserving transaction relay (<code>ConnectionType::PRIVATE_BROADCAST</code>).
Each has its own slot accounting so they don't interfere with the automatic outbound budget.</p>
<p><code>MANUAL</code> connections are created via the <code>-addnode</code> configuration option or the <code>addnode</code> RPC, and have their own separate 8-slot pool.
That is, adding manual peers never displaces any (automatic) outbound connections, they are separately accounted for.
Unlike automatic connections, manual peers are not subject to the same rotation and eviction logic, a node with manual peers will persistently try to maintain connections to them.</p>
<p><code>PRIVATE_BROADCAST</code> is new in v31, enabled via the opt-in <a href="https://github.com/bitcoin/bitcoin/pull/29415"><code>-privatebroadcast</code></a> flag (off by default).
These connections use a dedicated 64-slot budget, completely separate from the regular outbound pool.
They open short-lived connections exclusively over privacy networks (Tor, I2P) to relay transactions submitted locally via RPC, then close (such that transaction broadcast over privacy networks never competes with regular outbound connection slots).</p>
<p><strong>The complete set of connection types in Bitcoin Core</strong></p>
<p>All seven connection types that have been discussed are defined in the <code>ConnectionType</code> enum in <a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/connection_types.h"><code>src/node/connection_types.h</code></a>.</p>
<pre><code class="hljs language-cpp"><span class="hljs-keyword">enum class</span> <span class="hljs-title class_">ConnectionType</span> {
    INBOUND,
    OUTBOUND_FULL_RELAY,
    MANUAL,
    FEELER,
    BLOCK_RELAY,
    ADDR_FETCH,
    PRIVATE_BROADCAST,
};
</code></pre>
<h2 id="node-operating-modes">Node operating modes</h2>
<p>Beyond reachability and connection management, nodes also differ in what they store, what they relay, and how much of the chain they keep after validating it.
All of the configurations discussed in this section are <strong>full nodes</strong>: they independently validate every block against the consensus rules and maintain the UTXO set needed to do so, rather than trusting proof-of-work or any peer's assessment as a proxy for validity.
<a href="#light-clients">Light clients</a>, discussed separately below, are not nodes in this sense; they are wallet software that connects to full nodes as a client rather than participating as a peer.</p>
<p>The first distinction among full nodes is whether they retain a complete copy of the blockchain or not, that is, whether a node is a <strong>full archival node</strong> or a <strong>pruned node</strong>.</p>
<h3 id="archival-nodes">Archival nodes</h3>
<p>Full archival nodes validate and retain every block from the genesis block onwards.
This means they can serve any historical block to peers that request it, most importantly to new nodes that must download and validate the entire chain from scratch (<strong>initial block download</strong> - IBD).
A full archival node should advertise as such by signalling <code>NODE_NETWORK</code> in the service flags in the <code>version</code> message it sends to peers during the version handshake.</p>
<p><strong>The <code>version</code> handshake</strong></p>
<p>When two Bitcoin nodes first connect, they exchange <code>version</code> messages before any other communication.
The <code>version</code> message carries the node's protocol version, service flags (like <code>NODE_NETWORK</code> or <code>NODE_NETWORK_LIMITED</code>), current block height, a nonce for self-connection detection, and the <code>fRelay</code> flag indicating whether the sender wants transaction relay on this connection.
The receiving node responds with a <code>verack</code> (version acknowledgement) message, and once both sides have sent and received <code>verack</code>, the connection is considered established.
Only after this handshake completes will either peer process further messages.
The service flags advertised in <code>version</code> are how peers learn each other's capabilities, though, as noted above, these are self-reported and unauthenticated.</p>
<h3 id="pruned-nodes">Pruned nodes</h3>
<p>Not every operator is willing or able to store the full block history - as of early 2026, the raw block data alone occupies roughly 775 GB, with the full data directory closer to 830 GB before any optional indexes.
Pruned nodes (<code>-prune=&#x3C;N></code>) also download and fully validate every block, and maintain the full UTXO set, but discard old block data after validation.
A pruned node relays new blocks and transactions normally, it simply can't serve historical blocks to peers performing IBD.</p>
<p>Regardless of the pruning target, a pruned node always retains at least the most recent [[288 blocks||<code>MIN_BLOCKS_TO_KEEP</code> in <code>src/validation.h</code>, with the value proposed by <a href="https://github.com/bitcoin/bitcoin/pull/4701">Gregory Maxwell during the original pruning design discussions</a> as "a minimum number I'd consider acceptable as an absolute minimum for the purpose of reorgs."]], which is approximately two days' worth given the 10-minute block interval.
The minimum pruning target of <code>-prune=550</code> (MiB) is derived from this requirement, accounting for the 288 blocks themselves, undo data overhead, orphan block rate, and block file granularity.
Pruned nodes advertise <code>NODE_NETWORK_LIMITED</code> rather than <code>NODE_NETWORK</code> in their service flags, signalling to peers that they can serve recent blocks but cannot be relied upon for historical data during IBD.</p>
<h3 id="blocks-only-mode">Blocks-only mode</h3>
<p>Orthogonal to the archival/pruned distinction is whether a node participates in <strong>transaction relay</strong>.
Running with <code>-blocksonly</code> disables ordinary transaction relay, so a node in this mode won't accept transactions from normal network peers.
Because transaction relay dominates a typical node's traffic, blocks-only mode can <a href="https://github.com/bitcoin/bitcoin/blob/master/doc/reduce-traffic.md">reduce overall bandwidth consumption by as much as 88%</a>.
The trade-off is that the node sees far less unconfirmed transaction activity, so its mempool remains sparse, fee estimation is disabled, and it cannot make full use of the mempool-assisted fast path in <strong>compact block relay</strong>.</p>
<p><strong>Compact block relay</strong></p>
<p><a href="https://github.com/bitcoin/bips/blob/master/bip-0152.mediawiki">BIP 152</a> compact block relay reduces block propagation bandwidth by exploiting the fact that a node's mempool already contains most of a new block's transactions.
Instead of transmitting a full block, compact block relay uses a <code>cmpctblock</code> message containing the block header, a list of short transaction IDs, and any transactions the sender predicts the receiver won't have (the "prefilled" transactions, which always includes the coinbase).
The receiver matches the short IDs against its mempool and, if any are missing, requests them individually via <code>getblocktxn</code>/<code>blocktxn</code> round-trips.
A typical block compresses to a compact block message of <a href="https://bitcoincore.org/en/2016/06/07/compact-blocks-faq/">roughly 9 KB</a> (the header, short transaction IDs, and prefilled transactions); roughly two orders of magnitude smaller than the full block.</p>
<p>BIP 152 defines two modes.
In <strong>low-bandwidth mode</strong> (the default for most connections), the sender first announces the block via <code>headers</code> (or falls back to <code>inv</code>) and only sends the compact block after the receiver requests it with <code>getdata</code>.
In <strong>high-bandwidth mode</strong>, which a node requests from up to three peers via <code>sendcmpct</code>, the sender pushes the <code>cmpctblock</code> immediately upon validation, without waiting for a request, potentially even before fully validating the block itself.
This shaves off a full round-trip and is the primary mechanism by which new blocks propagate quickly across the network.</p>
<p>The impact on propagation times has been dramatic: <a href="https://www.dsn.kastel.kit.edu/bitcoin/publications/bitcoin_network_characterization.pdf">KIT DSN measurements</a> observed block propagation drop from over 6 seconds (2015, pre-compact blocks) to under 1 second (2018) at the 50th percentile, with the 90th percentile falling from over 15 seconds to roughly 2 seconds.</p>
<p>The effectiveness of compact blocks depends directly on mempool overlap: the more transactions a node has already seen and validated, the fewer it needs to request after receiving a compact block.
Monitoring by <a href="https://delvingbitcoin.org/t/stats-on-compact-block-reconstructions/1052">0xB10C</a> shows that under normal mempool conditions, over 80% of compact blocks reconstruct without any additional round-trips, though this can drop below 50% during periods of high mempool congestion (such as around the April 2024 halving).
A blocks-only node, which maintains only a small mempool (5 MB by default vs the normal 300 MB) and receives no transaction relay, rarely has the transactions it needs, reconstructs compact blocks less efficiently, and often needs many missing transactions via <code>getblocktxn</code>, reducing or sometimes erasing the bandwidth benefit of the protocol.</p>
<p>A blocks-only node signals its preference during the version handshake by requesting no transaction relay.
Peers that receive this instruction should not send <code>inv</code> messages for transactions to that connection (the same mechanism used by the block-relay-only connections discussed earlier, though there it is applied per-connection rather than node-wide).
Note that <code>-blocksonly</code> is independent of <code>-prune</code>: a node can be archival and blocks-only (full history, sparse mempool), or pruned and full-relay (limited history, active mempool), or any other combination.</p>
<p><strong><code>-blocksonly</code> vs block-relay-only connections</strong></p>
<p>These two mechanisms are easy to confuse because they sound similar and both set <code>fRelay=false</code> in the version handshake, but they operate at different scopes and serve different purposes.</p>
<p><code>-blocksonly</code> is a <strong>node-wide configuration</strong>.
When enabled, the node disables transaction relay across <em>all</em> of its connections: it does not announce transactions via <code>inv</code>, and it will ignore (and may disconnect peers that persist in sending) unsolicited transaction messages.
The motivation is bandwidth reduction; on a typical node, transaction relay dominates traffic, so disabling it can cut bandwidth consumption dramatically (e.g. 88%).
A <code>-blocksonly</code> node still participates in address relay on its connections.</p>
<p>Block-relay-only connections (<code>ConnectionType::BLOCK_RELAY</code>) are a <strong>per-connection policy</strong>.
A node in a default configuration opens exactly 2 of these outbound connections alongside its 8 full-relay outbound connections.
These connections exchange block headers and blocks but never relay transactions or participate in address gossip (<code>addr</code>/<code>getaddr</code>).
As aforementioned, the motivation is to help obfuscate the topology of the network.</p>
<p>Both advertise <code>fRelay=false</code>, but they are still distinguishable: block-relay-only connections do not participate in address gossip, whereas <code>-blocksonly</code> connections still can.</p>
<h3 id="light-clients">Light clients</h3>
<p>Not every user needs or is able to bear the storage, bandwidth, and processing costs of full validation.
<strong>Light clients</strong> are wallet software that connects to full nodes as a client rather than participating in the P2P network as a peer.
Light clients do not validate blocks or enforce consensus rules.
Instead, they only download block headers (and potentially full blocks) and use various protocols to learn about transactions relevant to them, trusting that the longest proof-of-work chain contains only valid transactions.</p>
<p>The two main protocols for light client data retrieval illustrate different trade-offs in how this trust model is implemented.
The original approach, <strong>bloom filters</strong> (<a href="https://github.com/bitcoin/bips/blob/master/bip-0037.mediawiki">BIP 37</a>), had the client send a filter describing its addresses of interest to a full node, which would then return only matching transactions with Merkle proofs of inclusion.
This had well-documented privacy and DoS problems: the filter inherently leaked which addresses the client cared about (<a href="https://eprint.iacr.org/2014/763">Gervais et al., 2014</a>), and serving filter requests was computationally expensive for the full node with no way for the client to compensate.
Bloom filter serving is now largely deprecated in Bitcoin Core and disabled by default since <a href="https://bitcoincore.org/en/releases/0.19.0.1/">v0.19.0.1</a>.</p>
<p><strong>Compact block filters</strong> (<a href="https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki">BIP 157</a> / <a href="https://github.com/bitcoin/bips/blob/master/bip-0158.mediawiki">BIP 158</a>, not to be confused with <a href="#compact-block-relay">compact block <em>relay</em></a>) invert the model: the full node pre-computes a compact filter for each block, and the client downloads these filters and evaluates them locally.
Since every client downloads the same filters, the downloads reveal nothing about which addresses the client cares about.
When a filter matches, the client downloads the full block to check (with <a href="https://github.com/bitcoin/bips/blob/master/bip-0157.mediawiki">BIP 157</a> recommending that matched blocks be fetched from random peers to limit the information leaked by those requests).
This is the approach used by Neutrino-compatible wallets (such as those in the Lightning ecosystem) and supported by the <a href="https://github.com/bitcoindevkit/bdk">Bitcoin Dev Kit</a>.</p>
<p><strong>What about Electrum?</strong></p>
<p>Not all light wallets use the P2P layer via direct connections to Bitcoin Core nodes.
Electrum connects to dedicated indexing servers (<a href="https://github.com/spesmilo/electrumx">ElectrumX</a>, <a href="https://github.com/cculianu/Fulcrum">Fulcrum</a>, etc.) over the <a href="https://electrumx.readthedocs.io/en/latest/protocol.html">Electrum protocol</a> rather than speaking to Bitcoin Core peers directly.
The server sits on top of a fully-validated node, maintains a full transaction index, and handles address lookups on the client's behalf.
This shifts the trust model from "trust proof-of-work" to "trust the server" (unless the user runs their own).</p>
<h2 id="network-topology">Network topology</h2>
<p>The various node types and operating modes described above produce a network that is far from homogeneous.
The Bitcoin P2P network has a structure shaped by the asymmetry between reachable and unreachable nodes, the different capabilities advertised by each node, and the deliberate connection management strategies implemented in Bitcoin Core.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/bitcoin-node-roles-p2p/bitcoin-p2p-network-topology.png" alt="Figure 2: Bitcoin P2P network topology showing the densely interconnected reachable core and the unreachable periphery, with connection directionality and slot budget detail."></p>
<h3 id="the-reachable-core">The reachable core</h3>
<p>Reachable nodes form a densely connected core that serves as <a href="https://arxiv.org/abs/1905.10518">"the backbone of the Bitcoin network"</a>.
Every node on the network makes outbound connections, and those connections necessarily target reachable nodes.
As a result, this <strong>reachable core</strong> collectively absorbs all outbound connection attempts from the entire network.
A reachable node with 114 inbound slots might serve as a connection point for dozens of unreachable nodes simultaneously, while maintaining its own 10 outbound connections to other reachable peers.</p>
<p>This creates a hub-and-spoke dynamic (Figure 2) where the ~23,000 reachable nodes form the backbone through which up to ~47,000 unreachable nodes access the network.
The unreachable nodes are on the periphery, consuming connectivity from the reachable core but unable to provide it to others.
This is not a design flaw; it is an inevitable consequence of many node operators being unable to configure inbound access (e.g., NAT, CGNAT, firewalls).</p>
<h3 id="block-and-transaction-propagation">Block and transaction propagation</h3>
<p>The heterogeneous mix of relay policies and connection types, e.g., full-relay connections, block-relay-only connections, and connections involving blocks-only nodes, creates distinct overlay networks layered on top of the same physical topology.
Blocks propagate across all connection types, giving them a rich, redundant set of paths through the network.
Transactions, by contrast, only flow over full-relay connections, meaning the transaction relay graph is a subset of the block relay graph (Figure 3).</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/bitcoin-node-roles-p2p/bitcoin-block-tx-propagation-overlays.png" alt="Figure 3: Block and transaction propagation overlays showing that full-relay connections carry both block relay (solid blue) and transaction relay (dashed yellow), while block-relay-only connections and connections to blocks-only nodes carry only block relay, making the transaction relay graph a strict subset of the block relay graph."></p>
<p>This layering is intentional.
The block-relay-only connections provide additional paths for blocks to propagate without revealing the node's full connection topology through address and transaction relay side-channels.
An attacker who probes or analyses transaction relay behaviour may be able to infer parts of a node's connection topology, including some outbound peers; the block-relay-only connections are invisible to this kind of analysis, providing a hidden set of paths that make eclipse attacks meaningfully harder to execute.</p>
<h3 id="bridge-nodes">Bridge nodes</h3>
<p>The Bitcoin network spans multiple overlay networks: IPv4, IPv6, Tor, I2P, and CJDNS (Figure 4).
Most nodes are only reachable on one or two of these, meaning peers within one overlay can become isolated from peers on another if there aren't enough nodes straddling both.
Nodes that maintain connections across multiple network types act as <strong>bridge nodes</strong>, forwarding blocks and transactions between overlay networks that would otherwise have limited or no connectivity to each other.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/bitcoin-node-roles-p2p/bitcoin-bridge-nodes-overlay-networks.png" alt="Figure 4: Simplified illustration of Bitcoin P2P overlay networks (IPv4, IPv6, Tor, I2P, CJDNS) with bridge nodes in zone overlaps connecting across network boundaries. Actual network topology is far denser and more interconnected."></p>
<p>Bridge nodes are critical for preventing <strong>network partitioning</strong> along transport boundaries.
If, for example, the Tor-only segment of the network lost all connections to IPv4 peers, those Tor nodes would effectively be on a separate network, unable to see new blocks or transactions from the IPv4 majority, and vulnerable to eclipse attacks within their isolated segment.
Bridge nodes prevent this by ensuring that block and transaction propagation can cross network boundaries.</p>
<p>Since <a href="https://bitcoincore.org/en/releases/26.0/">Bitcoin Core v26.0</a> (<a href="https://github.com/bitcoin/bitcoin/pull/27213">PR #27213</a>), Bitcoin Core actively tries to improve outbound network-type diversity.
Once all 8 outbound full-relay slots are filled, the node periodically invokes <code>MaybePickPreferredNetwork()</code> to check whether any reachable network has zero current outbound connections despite having known addresses in <code>AddrMan</code>.
If such a network is found, the node briefly opens a 9th outbound full-relay connection targeting that network, then evicts a peer from an over-represented network to bring the count back to 8 with improved diversity.
For example, a node reachable on both IPv4 and Tor, whose 8 initial slots all landed on IPv4 addresses, will open a 9th connection to a Tor peer and drop one of the IPv4 peers to rebalance.</p>
<h3 id="resilience-through-diversity">Resilience through diversity</h3>
<p>The Bitcoin network's resilience emerges from the combination of all roles working together.
Archival nodes ensure that new nodes can always bootstrap from their blank slate.
Pruned nodes reduce the barrier to running a validating node, thus expanding the set of participants who independently enforce the consensus rules.
Blocks-only nodes further lower the resource floor while still contributing to block propagation, and because they do not relay third-party transactions by default, they reduce one avenue by which transaction-origin information can leak, though transactions submitted locally via RPC can still be broadcast by the node.
Bridge nodes knit the overlay networks together, preventing partitioning along transport boundaries.</p>
<p>The design philosophy throughout is one of graceful degradation: each operating mode sacrifices some capability in exchange for reduced resource requirements, but every full node still independently enforces the consensus rules on every block it processes.</p>
<h2 id="conclusion">Conclusion</h2>
<p>The Bitcoin P2P network is a collection of heterogeneous nodes with different capabilities and configurations that collectively maintain the properties the network needs: censorship-resistant transaction relay, fast block propagation, the ability for new nodes to join and sync, and independent consensus validation by every full node operator.</p>
<p>Understanding these roles and how they interact is a prerequisite for reasoning about the network's behaviour under adversarial conditions, resource requirements as the chain grows, and the design decisions behind protocol improvements.
In a future post, we'll look more closely at <code>AddrMan</code>, the address manager that determines <em>which</em> peers a node connects to in the first place, and the design choices that make peer selection both supportive of efficient P2P network operations and resilient to adversarial conditions.</p>
<h2 id="reference">Reference</h2>
<p><strong>Configuration options, code references, service flags, connection types, permissions, P2P messages, RPC commands, and source files referenced in this post (not exhaustive).</strong></p>
<h3 id="configuration-options">Configuration options</h3>
<table>
<thead>
<tr>
<th>Option</th>
<th>Default</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>-listen</code></td>
<td><code>1</code></td>
<td>Accept inbound connections from peers. Required for a node to be reachable.</td>
</tr>
<tr>
<td><code>-prune=&#x3C;N></code></td>
<td>Off</td>
<td>Discard old block data after validation, retaining the most recent blocks. Minimum value is <code>550</code> MiB.</td>
</tr>
<tr>
<td><code>-blocksonly</code></td>
<td>Off</td>
<td>Disable transaction relay entirely. Blocks are still relayed normally. Reduces bandwidth by up to ~88%.</td>
</tr>
<tr>
<td><code>-peerbloomfilters</code></td>
<td><code>0</code></td>
<td>Serve BIP 37 bloom-filtered data to peers. Disabled by default since v0.19.0.1 due to privacy and DoS concerns.</td>
</tr>
<tr>
<td><code>-blockfilterindex</code></td>
<td><code>0</code></td>
<td>Build and maintain BIP 158 compact block filter indexes locally.</td>
</tr>
<tr>
<td><code>-peerblockfilters</code></td>
<td><code>0</code></td>
<td>Serve BIP 157/158 compact block filters to peers. Requires <code>-blockfilterindex=1</code>.</td>
</tr>
<tr>
<td><code>-addnode</code></td>
<td>-</td>
<td>Manually specify a peer to persistently connect to. Uses a separate 8-slot pool that doesn't compete with automatic outbound connections. Also available as an RPC.</td>
</tr>
<tr>
<td><code>-seednode</code></td>
<td>-</td>
<td>Specify a peer to connect to for address bootstrapping on first startup, tried before hardcoded DNS seeds.</td>
</tr>
<tr>
<td><code>-privatebroadcast</code></td>
<td>Off</td>
<td>Opt-in (v31+). Relay locally-submitted transactions over short-lived Tor/I2P connections using a dedicated 64-slot budget, separate from regular outbound slots.</td>
</tr>
</tbody>
</table>
<h3 id="code-references">Code references</h3>
<table>
<thead>
<tr>
<th>Constant</th>
<th>File</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>DEFAULT_MAX_PEER_CONNECTIONS</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net.h"><code>src/net.h</code></a></td>
<td>Maximum total peer connections (default 125).</td>
</tr>
<tr>
<td><code>FEELER_INTERVAL</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net.h"><code>src/net.h</code></a></td>
<td>Interval between feeler connection attempts (~2 minutes).</td>
</tr>
<tr>
<td><code>OUTBOUND_INVENTORY_BROADCAST_INTERVAL</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net_processing.cpp"><code>src/net_processing.cpp</code></a></td>
<td>Transaction <code>inv</code> batching interval for outbound peers (2 seconds).</td>
</tr>
<tr>
<td><code>INBOUND_INVENTORY_BROADCAST_INTERVAL</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net_processing.cpp"><code>src/net_processing.cpp</code></a></td>
<td>Transaction <code>inv</code> batching interval for inbound peers (5 seconds). Longer to prevent timing-based transaction origin inference from Sybil inbound connections.</td>
</tr>
<tr>
<td><code>MIN_BLOCKS_TO_KEEP</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/validation.h"><code>src/validation.h</code></a></td>
<td>Minimum number of recent blocks a pruned node retains (288, ~2 days).</td>
</tr>
<tr>
<td><code>MAX_BLOCKFILE_SIZE</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/blockstorage.h"><code>src/node/blockstorage.h</code></a></td>
<td>Maximum size of a single block file on disk (128 MiB). Factors into the minimum prune target calculation.</td>
</tr>
<tr>
<td><code>BLOCK_STALLING_TIMEOUT_DEFAULT</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net_processing.cpp"><code>src/net_processing.cpp</code></a></td>
<td>Default time a peer must stall block download progress before being disconnected (2 seconds). Doubles on each stalling disconnection up to <code>BLOCK_STALLING_TIMEOUT_MAX</code> (64 seconds).</td>
</tr>
<tr>
<td><code>EXTRA_BLOCK_RELAY_ONLY_PEER_INTERVAL</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net.h"><code>src/net.h</code></a></td>
<td>Interval for the extra block-relay-only peer rotation loop (5 minutes). After IBD, the node periodically connects to a new block-relay-only peer and evicts the least useful existing one.</td>
</tr>
<tr>
<td><code>fPreferredDownload</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net_processing.cpp"><code>src/net_processing.cpp</code></a></td>
<td>Per-peer flag marking outbound peers (and <code>NoBan</code> inbound peers) as preferred sources for block downloads and header sync.</td>
</tr>
<tr>
<td><code>fRelay</code></td>
<td><code>version</code> message</td>
<td>Field in the <code>version</code> handshake message. When <code>false</code>, signals the sender does not want transaction relay on this connection (used by <code>-blocksonly</code> nodes and block-relay-only connections).</td>
</tr>
<tr>
<td><code>ADDRMAN_HORIZON</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/addrman.h"><code>src/addrman.h</code></a></td>
<td>Maximum age (30 days) before an address in <code>AddrMan</code> is considered too old. One trigger for DNS seed bootstrapping.</td>
</tr>
<tr>
<td><code>ADDRMAN_RETRIES</code> / <code>ADDRMAN_MAX_FAILURES</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/addrman.h"><code>src/addrman.h</code></a></td>
<td>Thresholds for marking an address as "terrible" after repeated connection failures. When all addresses are terrible, bootstrapping is triggered.</td>
</tr>
<tr>
<td><code>MaybePickPreferredNetwork()</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net.cpp"><code>src/net.cpp</code></a></td>
<td>Checks whether any reachable network has zero outbound connections despite having known addresses, and if so, sets it as the preferred network for the next outbound attempt. Drives bridge node diversity rebalancing.</td>
</tr>
<tr>
<td><code>EvictExtraOutboundPeers()</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net_processing.cpp"><code>src/net_processing.cpp</code></a></td>
<td>Evicts an outbound peer from an over-represented network after a 9th diversity connection is established. Selects the peer that least recently announced a new block, but protects sole connections on any network type.</td>
</tr>
</tbody>
</table>
<h3 id="service-flags">Service flags</h3>
<table>
<thead>
<tr>
<th>Flag</th>
<th>File</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>NODE_NETWORK</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/protocol.h"><code>src/protocol.h</code></a></td>
<td>Advertises that the node stores the full block history and can serve any historical block to peers.</td>
</tr>
<tr>
<td><code>NODE_NETWORK_LIMITED</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/protocol.h"><code>src/protocol.h</code></a></td>
<td>Advertises that the node is pruned: it can serve recent blocks (at least the last 288) but not the full history.</td>
</tr>
<tr>
<td><code>NODE_WITNESS</code></td>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/protocol.h"><code>src/protocol.h</code></a></td>
<td>Advertises that the node supports segregated witness (SegWit). Used in DNS seed service-bit filter prefixes (e.g. <code>x9</code> = <code>NODE_NETWORK</code> | <code>NODE_WITNESS</code>).</td>
</tr>
</tbody>
</table>
<h3 id="connection-types">Connection types</h3>
<p>Defined in <a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/connection_types.h"><code>src/node/connection_types.h</code></a>, with string representations in <a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/connection_types.cpp"><code>src/node/connection_types.cpp</code></a>.</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Direction</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>OUTBOUND_FULL_RELAY</code></td>
<td>Outbound</td>
<td>Full-relay outbound connection (8 by default). Exchanges addresses, transactions, and blocks.</td>
</tr>
<tr>
<td><code>BLOCK_RELAY</code></td>
<td>Outbound</td>
<td>Block-relay-only outbound connection (2 by default). Exchanges only block headers and blocks, hiding the connection from transaction and address relay side-channels.</td>
</tr>
<tr>
<td><code>FEELER</code></td>
<td>Outbound</td>
<td>Short-lived connection (~every 2 min) to test whether addresses in the peer database are reachable. Promotes peers from the <code>new</code> to <code>tried</code> table.</td>
</tr>
<tr>
<td><code>INBOUND</code></td>
<td>Inbound</td>
<td>Connection initiated by a remote peer. Up to 114 slots by default. Subject to eviction.</td>
</tr>
<tr>
<td><code>MANUAL</code></td>
<td>Outbound</td>
<td>Manually added peer via <code>-addnode</code> or <code>addnode</code> RPC. Not subject to automatic eviction.</td>
</tr>
<tr>
<td><code>ADDR_FETCH</code></td>
<td>Outbound</td>
<td>Short-lived connection opened solely to solicit addresses from a peer.</td>
</tr>
<tr>
<td><code>PRIVATE_BROADCAST</code></td>
<td>Outbound</td>
<td>Connection used to broadcast a specific transaction privately, without entering the normal relay pipeline.</td>
</tr>
</tbody>
</table>
<h3 id="net-permissions">Net permissions</h3>
<p>Defined in <a href="https://github.com/bitcoin/bitcoin/blob/master/src/net_permissions.h"><code>src/net_permissions.h</code></a>.</p>
<table>
<thead>
<tr>
<th>Permission</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Relay</code></td>
<td>Peer is allowed to send transactions to a <code>-blocksonly</code> node, overriding the global relay disable for incoming transaction acceptance.</td>
</tr>
<tr>
<td><code>ForceRelay</code></td>
<td>Peer can force automatic broadcast/rebroadcast of transactions, distinct from merely accepting them via <code>Relay</code>. Granted via <code>whitelistforcerelay</code>.</td>
</tr>
<tr>
<td><code>NoBan</code></td>
<td>Peer is exempt from being banned or discouraged. Also grants <code>fPreferredDownload</code> status (treated as preferred for block downloads, like an outbound peer).</td>
</tr>
</tbody>
</table>
<h3 id="p2p-messages">P2P messages</h3>
<table>
<thead>
<tr>
<th>Message</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>version</code></td>
<td>Sent during the version handshake. Carries protocol version, service flags, block height, nonce, and <code>fRelay</code> flag.</td>
</tr>
<tr>
<td><code>verack</code></td>
<td>Version acknowledgement. Sent in response to <code>version</code>; connection is established once both sides have exchanged <code>version</code>/<code>verack</code>.</td>
</tr>
<tr>
<td><code>inv</code></td>
<td>Inventory announcement. Advertises that the sender has new transactions or blocks available for download.</td>
</tr>
<tr>
<td><code>getdata</code></td>
<td>Requests full data (transaction or block) for items previously announced via <code>inv</code>.</td>
</tr>
<tr>
<td><code>addr</code> / <code>addrv2</code></td>
<td>Address gossip. Propagates known peer addresses across the network. <code>addrv2</code> (<a href="https://github.com/bitcoin/bips/blob/master/bip-0155.mediawiki">BIP 155</a>) extends support to Tor v3, I2P, and CJDNS addresses.</td>
</tr>
<tr>
<td><code>filterload</code></td>
<td>BIP 37 bloom filter loading. A light client sends this to a full node to request filtered transaction delivery. Largely deprecated.</td>
</tr>
<tr>
<td><code>getaddr</code></td>
<td>Requests a list of known peer addresses from a connected peer. Used by <code>ADDR_FETCH</code> connections during address bootstrapping.</td>
</tr>
<tr>
<td><code>cmpctblock</code></td>
<td>BIP 152 compact block message. Contains the block header, short transaction IDs, and prefilled transactions, allowing the receiver to reconstruct the block from its mempool.</td>
</tr>
<tr>
<td><code>getblocktxn</code> / <code>blocktxn</code></td>
<td>BIP 152 compact block round-trip. <code>getblocktxn</code> requests specific transactions missing after a <code>cmpctblock</code>; <code>blocktxn</code> delivers them.</td>
</tr>
<tr>
<td><code>sendcmpct</code></td>
<td>BIP 152 compact block preference message. <code>sendcmpct(1)</code> asks a peer to announce new blocks with unsolicited <code>cmpctblock</code> messages; <code>sendcmpct(0)</code> indicates low-bandwidth mode, where compact blocks are served on request.</td>
</tr>
</tbody>
</table>
<h3 id="source-files-referenced">Source files referenced</h3>
<table>
<thead>
<tr>
<th>File</th>
<th>Purpose</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net.h"><code>src/net.h</code></a></td>
<td>Core networking constants and connection management.</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net_processing.cpp"><code>src/net_processing.cpp</code></a></td>
<td>P2P message processing logic, inventory broadcast intervals, preferred download.</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/protocol.h"><code>src/protocol.h</code></a></td>
<td>Protocol constants including service flags (<code>NODE_NETWORK</code>, <code>NODE_NETWORK_LIMITED</code>).</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/validation.h"><code>src/validation.h</code></a></td>
<td>Validation constants including <code>MIN_BLOCKS_TO_KEEP</code>.</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/blockstorage.h"><code>src/node/blockstorage.h</code></a></td>
<td>Block storage constants including <code>MAX_BLOCKFILE_SIZE</code>.</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/connection_types.h"><code>src/node/connection_types.h</code></a> / <a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/connection_types.cpp"><code>.cpp</code></a></td>
<td><code>ConnectionType</code> enum definition and string representation.</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/eviction.h"><code>src/node/eviction.h</code></a> / <a href="https://github.com/bitcoin/bitcoin/blob/master/src/node/eviction.cpp"><code>.cpp</code></a></td>
<td>Inbound peer eviction logic (<code>SelectNodeToEvict</code>, protection heuristics).</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net_permissions.h"><code>src/net_permissions.h</code></a></td>
<td>Net permission flags (<code>Relay</code>, <code>NoBan</code>, etc.).</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/net.cpp"><code>src/net.cpp</code></a></td>
<td>Connection management implementation, including <code>MaybePickPreferredNetwork()</code> for bridge node diversity.</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/addrman.h"><code>src/addrman.h</code></a> / <a href="https://github.com/bitcoin/bitcoin/blob/master/src/addrman.cpp"><code>.cpp</code></a></td>
<td>Address manager constants (<code>ADDRMAN_HORIZON</code>, <code>ADDRMAN_RETRIES</code>, <code>ADDRMAN_MAX_FAILURES</code> in <code>.h</code>) and implementation.</td>
</tr>
<tr>
<td><a href="https://github.com/bitcoin/bitcoin/blob/master/src/kernel/chainparams.cpp"><code>src/kernel/chainparams.cpp</code></a></td>
<td>Chain parameters including hardcoded DNS seed hostnames.</td>
</tr>
</tbody>
</table>
<h3 id="rpc-commands">RPC commands</h3>
<table>
<thead>
<tr>
<th>Command</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>addnode</code></td>
<td>Manually add or remove a peer connection. Creates a <code>MANUAL</code> connection type with its own separate 8-slot pool.</td>
</tr>
<tr>
<td><code>sendrawtransaction</code></td>
<td>Submit a raw transaction to the network. When <code>-privatebroadcast</code> is enabled, these transactions are relayed over dedicated Tor/I2P connections.</td>
</tr>
<tr>
<td><code>getblockchaininfo</code></td>
<td>Returns information about the blockchain, including current chain height and disk usage.</td>
</tr>
</tbody>
</table>]]></content:encoded>
            <category>bitcoin</category>
            <category>p2p</category>
            <category>network-topology</category>
            <category>explainer</category>
        </item>
        <item>
            <title><![CDATA[NixOS & Peer Observer on a VPS]]></title>
            <link>https://deadmanoz.xyz/posts/2026/nixos-peer-observer-vps</link>
            <guid isPermaLink="false">https://deadmanoz.xyz/posts/2026/nixos-peer-observer-vps</guid>
            <pubDate>Tue, 13 Jan 2026 00:00:00 GMT</pubDate>
            <description><![CDATA[My experience setting up NixOS and Peer Observer on a VPS for Bitcoin monitoring]]></description>
            <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>I've recently started contributing to the <a href="https://github.com/peer-observer/peer-observer">peer-observer</a> ecosystem.
peer-observer is a set of tooling developed by <a href="https://b10c.me/">B10C</a> for monitoring the Bitcoin network "<em>for P2P anomalies and attacks using well-behaving, passive Bitcoin Core honeynodes (honeypot nodes).</em>"
This was, in part, motivated by the "call to action" put out by B10C in <a href="https://b10c.me/projects/024-peer-observer/">his post from July 2025</a>, and the idea of forming a "Bitcoin Network Operations Collective" (BNOC):</p>
<blockquote>
<p>"<em>A loose, decentralized group of people who share the interest of monitoring the Bitcoin Network. A collective to enable sharing of ideas, discussion, data, tools, insights, and more... A place where a Bitcoin network incident could be analyzed, discussed, and ideally resolved...</em>"</p>
</blockquote>
<p>For a month or two I had been tinkering with a bit of a Frankenstein's monster peer-observer setup running on my home server, cobbled together with a pre-existing Bitcoin Core node and <a href="https://github.com/ClubeBitcoinUnB/peer-observer-docker">some Docker containers for the peer-observer stack</a>, but had a number of issues.</p>
<p>Firstly, this was fragile and it was difficult to develop and test against.
Secondly, my home server is on a residential CGNAT connection to the internet.
As such, the Bitcoin Core node was "unreachable" and could not accept inbound connections, severely limiting the usefulness of such a peer-observer setup.</p>
<p>So it was obvious that I needed a more robust and production-like setup.
I needed to set up an instance of peer-observer with a reachable Bitcoin Core node.
And I wanted to do this on a NixOS foundation, given the pervasive use of Nix across all of B10C's projects (as well as across many other Bitcoin infrastructure projects).
After asking B10C and some other members of the fledgling BNOC for recommendations, I settled on setting up a VPS on <a href="https://www.ovhcloud.com/en-au/">OVHcloud</a>.</p>
<p>I was initially hoping to find a VPS provider that would support NixOS "out-of-the-box" but I found that this was likely to be a fool's errand <a href="https://www.youtube.com/live/bKTbis4elR8?si=5p4tLUu77Pw1PiYK&#x26;t=5780">as highlighted by Carl Dong in his presentation at bitcoin++ 2023 (nix-edition)</a>:</p>
<blockquote>
<p>"nobody supports NixOS... VPS providers don't have first class support for NixOS".</p>
</blockquote>
<p>Edouard Paris's post (<a href="https://edouard.paris/notes/install-nixos-on-an-ovh-vps-with-nixos-anywhere/">Install NixOS on an OVH VPS with nixos-anywhere</a>), which is where I found the link to the above talk, helped me understand that I could install NixOS on a VPS that initially came with another distro installed (e.g. Debian).
So I provisioned an Ubuntu 25.04 VPS with the following specs:</p>
<ul>
<li>6 vCores</li>
<li>12GB RAM</li>
<li>100GB SSD NVMe</li>
</ul>
<p>This cost around US$7.32/month (~AU$11.00/month at time of writing) for 6 months prepaid.
Note that 100GB is sufficient with pruning enabled and only storing a few days of logs (default is 4); if you want a full archival node you'll need significantly more storage (I'm aiming to cover this in a future post).</p>
<h2 id="installing-nixos">Installing NixOS</h2>
<p>With my Ubuntu VPS provisioned and accessible, I began the work to set up NixOS.
I more or less followed along with the earlier post (<a href="https://edouard.paris/notes/install-nixos-on-an-ovh-vps-with-nixos-anywhere/">Install NixOS on an OVH VPS with nixos-anywhere</a>).</p>
<p>To perform some of the operations required for <code>nixos-anywhere</code>, you'll need root SSH access to your VPS.
And don't make the same mistake I did in trying to prematurely lock down the VPS before you've got NixOS installed!
That is, I initially:</p>
<ol>
<li>Disabled password login</li>
<li>Enabled SSH key-only authentication</li>
<li>Changed the SSH port</li>
</ol>
<p>But doing these things caused problems during installation - fortunately I had multiple open SSH sessions to the VPS so could intervene when necessary.</p>
<blockquote>
<p><strong>Lock down the VPS <em><em>after</em></em> you've installed NixOS</strong></p>
</blockquote>
<blockquote>
<p><strong>Establish a few SSH sessions so you can intervene if things don't go smoothly</strong></p>
</blockquote>
<h2 id="customising-nixos-configuration">Customising NixOS Configuration</h2>
<p>With the barebones NixOS installed, I then set about customising the setup:</p>
<ul>
<li>Set up SSH key authentication</li>
<li>Changed the SSH port</li>
<li>Configured firewall rules to allow only necessary ports</li>
<li>Set up a non-root user with sudo privileges</li>
<li>Set up fail2ban - I managed to ban my IP during this process, again, multiple SSH sessions helped!</li>
<li>Added Home Manager for clean separation of user-level configuration</li>
</ul>
<p>Of course, all of this is easily done via <code>.nix</code> configuration files, which is one of the great strengths of NixOS.
Note that in using the flake-based remote deployment approach of <code>nixos-anywhere</code>, the configuration files are stored locally and pushed to the VPS when commands like the following are run from your local machine:</p>
<pre><code class="hljs language-bash">nix run github:nix-community/nixos-anywhere -- --flake .#&#x3C;flake-ref> &#x3C;root@vps-host>

nixos-rebuild switch --flake .#&#x3C;flake-ref> --target-host <span class="hljs-string">"root@&#x3C;vps-host>"</span>
</code></pre>
<p>That is, you won't have <code>.nix</code> files where you might expect them on the VPS (e.g. <code>/etc/nixos/configuration.nix</code>), but rather on your local machine.
This might be a problem if you need auto-upgrades or have issues with the process (like I did!), in which case you might want to consider copying your configuration files to the VPS itself (and keep them updated as necessary).</p>
<blockquote>
<p><strong>With <code>nixos-anywhere</code>, the target machine (VPS) has no source configuration, it only has the built system derivation</strong></p>
</blockquote>
<h2 id="spinning-up-peer-observer-et-al">Spinning up Peer Observer <em>et al.</em></h2>
<p>B10C has done an excellent job of providing a <a href="https://github.com/peer-observer/infra-library">NixOS flake for running peer-observer instances</a>, so much of the heavy lifting was already done for me.
Specifically, the <code>infra-library</code> provisions:</p>
<ul>
<li>A Bitcoin Core node with appropriate configuration for monitoring, e.g. built with USDT/tracepoints enabled, pruning enabled (~4GB of recent blocks retained),</li>
<li>peer-observer extractors, including <a href="https://github.com/peer-observer/peer-observer/tree/master/extractors/ebpf">eBPF</a>, <a href="https://github.com/peer-observer/peer-observer/tree/master/extractors/rpc">RPC</a>, <a href="https://github.com/peer-observer/peer-observer/tree/master/extractors/p2p">P2P</a> and <a href="https://github.com/peer-observer/peer-observer/tree/master/extractors/log">log</a> extractors,</li>
<li>peer-observer tools such as <a href="https://github.com/peer-observer/peer-observer/tree/master/tools/logger">logger</a>, <a href="https://github.com/peer-observer/peer-observer/blob/master/tools/metrics">metrics</a> and <a href="https://github.com/peer-observer/peer-observer/tree/master/tools/websocket">websocket</a>,</li>
<li>NATS Server for extractors to publish their data to, and for tools to subscribe to the data from,</li>
<li>WireGuard VPN for connectivity between peer-observer instances and a central data collector (which is where Prometheus/Grafana would live in a multi-instance setup),</li>
<li>age and agenix for secrets management, covered further in <a href="#keys-and-secrets-management">keys and secrets management</a>.</li>
</ul>
<p>The main customisations I made involved:</p>
<ul>
<li>Host name and user account,</li>
<li>All of the secrets and key setup,</li>
<li>Light vim customisation, bash aliases, etc. via Home Manager,</li>
<li>Bumping Bitcoin Core to the v30.2 tag commit.</li>
</ul>
<p>Once everything is installed and customised, we can start all of the services.
The <code>infra-library</code> orchestrates quite a few systemd services:</p>
<ul>
<li><code>bitcoind-mainnet.service</code> - the Bitcoin Core node, compiled with USDT/tracepoints enabled</li>
<li><code>peer-observer-ebpf-extractor.service</code> - attaches to Bitcoin Core's USDT tracepoints via eBPF</li>
<li><code>peer-observer-rpc-extractor.service</code> - polls Bitcoin Core's RPC for chain/mempool state</li>
<li><code>peer-observer-p2p-extractor.service</code> - monitors P2P message traffic</li>
<li><code>peer-observer-tool-metrics.service</code> - exposes metrics for prometheus scraping</li>
<li><code>peer-observer-tool-websocket.service</code> - provides real-time event streaming</li>
<li><code>fork-observer.service</code> - monitors chain tip and detects forks</li>
<li><code>addrman-observer-proxy.service</code> - exposes address manager data</li>
<li><code>nats.service</code> - message broker connecting extractors to tools</li>
<li><code>nginx.service</code> - reverse proxy for metrics endpoints</li>
<li><code>tor.service</code> - Tor proxy for anonymous peer connections (I enabled this)</li>
<li><code>prometheus-node-exporter.service</code> - system metrics (CPU, memory, disk)</li>
<li><code>prometheus-wireguard-exporter.service</code> - VPN tunnel metrics</li>
<li><code>prometheus-process-exporter.service</code> - per-process resource usage</li>
</ul>
<p>I use <a href="https://github.com/casey/just"><code>just</code></a> as a command runner, so I have a <code>justfile</code> with commands to start, stop, restart and check the status and logs of all of the above services.</p>
<p>Be prepared for IBD to take a day or two, even with pruning enabled!</p>
<p>For more instructive details on how to set up peer-observer with NixOS in the manner I did, I'll be contributing to the <a href="https://github.com/peer-observer/infra-library/issues/1">peer-observer/infra-library documentation</a>.</p>
<h2 id="keys-and-secrets-management">Keys and Secrets Management</h2>
<p><a href="https://github.com/FiloSottile/age">Age</a> was completely unfamiliar to me, so I wrote the following explanation of what it is and how it's used in the context of peer-observer deployment.</p>
<p>age is a simple, modern, and secure encryption tool designed to be a simpler alternative to GPG.
It is used in the peer-observer deployment with agenix to manage secrets such as WireGuard keys.</p>
<p>More specifically, we generate a WireGuard private key on our administration/deployment coordinator machine, and then encrypt it with the SSH public keys of all deploy targets.
This generates a single <code>.age</code> file containing the secret encrypted for all recipients.
When deployed via agenix, each target uses its own SSH private key to decrypt and access the WireGuard private key.</p>
<p>This is basically the same as PGP, that is, hybrid encryption and multiple "recipients", just with different algorithms (X25519/ChaCha20 vs PGP's RSA/AES) and without all of the extra complexity of PGP (key servers, web of trust, etc.).</p>
<h2 id="multi-instance-deployment-architecture">Multi-Instance Deployment Architecture</h2>
<p>Although my setup currently consists of a single peer-observer node (with no central web-server yet), it's instructive to consider what a complete multi-node deployment might look like.
Figure 1 shows a setup with two peer-observer nodes and a central web-server.
Each node runs its own Bitcoin Core instance and peer-observer extractors/tools, with all nodes connected via WireGuard VPN.</p>
<p>Note that the extractors use protobuf to serialise and publish data to the NATS Server they are co-located with.
The tools then subscribe to this data from the NATS Server.
This means that in a multi-node setup, each peer-observer node is self-contained with its own NATS Server instance, and the tools on each node only see data from their local extractors.
Prometheus/Grafana on the central web-server scrape metrics from each peer-observer node, and if one peer-observer node goes down or is unreachable, the rest of the setup continues to operate normally.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/2026/nixos-peer-observer-vps/multi-node-deployment-illustration.png" alt="Figure 1: Peer Observer multi-node architecture"></p>
<h2 id="resources">Resources</h2>
<ul>
<li><a href="https://bnoc.xyz/">Bitcoin Network Operations Collective (BNOC)</a></li>
<li><a href="https://github.com/peer-observer">Peer Observer GitHub Organisation</a></li>
<li><a href="https://github.com/ClubeBitcoinUnB/peer-observer-docker">Peer Observer Docker</a></li>
<li><a href="https://b10c.me/projects/024-peer-observer/">B10C - peer-observer: A tool and infrastructure for monitoring the Bitcoin P2P network for attacks and anomalies</a></li>
<li><a href="https://nixos.wiki/wiki/NixOS_friendly_hosters">NixOS Wiki - NixOS friendly hosters</a></li>
<li><a href="https://edouard.paris/blog/install-nixos-on-an-ovh-vps-with-nixos-anywhere">Edouard Paris - Install NixOS on an OVH VPS with nixos-anywhere</a></li>
<li><a href="https://www.youtube.com/watch?v=bKTbis4elR8&#x26;t=5519s">Carl Dong at bitcoin++ 2023 nix-edition - The Dark Arts of NixOS Deployments</a></li>
<li><a href="https://nix-community.github.io/nixos-anywhere/quickstart.html">Quickstart Guide: nixos-anywhere</a></li>
</ul>]]></content:encoded>
            <category>bitcoin</category>
            <category>peer-observer</category>
            <category>nixos</category>
            <category>guide</category>
        </item>
        <item>
            <title><![CDATA[P2MS Data Carry Part 2: UTXO set analysis]]></title>
            <link>https://deadmanoz.xyz/posts/p2ms-data-carry-2</link>
            <guid isPermaLink="false">https://deadmanoz.xyz/posts/p2ms-data-carry-2</guid>
            <pubDate>Thu, 04 Dec 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Exploring P2MS for data carriage in a snapshot of the UTXO set]]></description>
            <content:encoded><![CDATA[<h2 id="tldr">tl;dr</h2>
<p>Analysis of 2.42 million P2MS UTXOs at block height 918,997 (October 2025) reveals that P2MS has been almost entirely co-opted for data carriage rather than its intended multisig purpose.
Over 99.98% of P2MS outputs serve data embedding protocols, with only 0.02% (552 outputs) appearing to be legitimate multisig usage.</p>
<p>Bitcoin Stamps dominates with 73.57% of all P2MS outputs, followed by Counterparty (22.86%) and Omni Layer (1.78%).
Unlike Bitcoin Stamps, both Counterparty and Omni maintain at least one valid public key per output, preserving spendability.</p>
<p>Spendability analysis reveals that 74.37% of all P2MS outputs are unspendable, with Bitcoin Stamps responsible for 98.9% of these through deliberate use of fake public keys.
Only 10.6% of P2MS outputs ever created have been spent.
The historical trend is stark: outputs created before 2023 were predominantly spendable (>95%), while those created since Bitcoin Stamps launched in early 2023 are ~99.5% unspendable.</p>
<p>The value distribution is notably inverted: unspendable outputs contain only 20.54% of the total value (~14.3 BTC), while ~55.2 BTC in spendable outputs remains theoretically recoverable.
Only 69.47 BTC is locked in P2MS outputs in total, just 0.00035% of total supply, yet users have paid over 281 BTC in fees to embed this data.</p>
<p>Content analysis shows that JSON data is present in 72.64% of all P2MS outputs, driven by Bitcoin Stamps' SRC-20, SRC-721, and SRC-101 sub-protocols.
Notably, Bitcoin Stamps hasn't created any "Classic Stamps" (images), the original motivation for unspendable P2MS outputs, since March 2024, with current usage entirely JSON-based.
All told, P2MS data carriage has left a 252.2 MB footprint (~2.2% of total UTXO set size).</p>
<p>With Bitcoin Core v30.0 (October 2025) removing <code>OP_RETURN</code> size limits, Bitcoin Stamps now has a viable alternative for its JSON payloads that doesn't impose permanent costs on the network.
Bitcoin Stamps' original rationale, UTXO set permanence for art, no longer applies when the protocol is embedding only JSON.
Combined with P2MS being almost entirely unused for its intended multisig purpose, there is now a reasonable case for deprecating P2MS output creation entirely.</p>
<h2 id="introduction">Introduction</h2>
<p>The post follows on from <a href="./p2ms-data-carry-1">P2MS Data Carry Part 1: Fundamentals and Examples</a>, which explored the technical mechanics of how Bitcoin Stamps, Counterparty, Omni, and other protocols embed arbitrary data into Pay-to-Multisig (P2MS) transaction outputs.
This post shifts focus to analysing a recent snapshot of the UTXO set to examine how P2MS has been used for data carriage by these protocols, quantify the magnitude of each protocol's contribution to the UTXO set and generally examine various facets of the use of P2MS.</p>
<p>There is perhaps an unnecessary level of detail in some of the sections below; this is in recognition of the fact that the content will (primarily?) be scraped and re-presented by LLMs and AI tools.
The more context and detail, the better, in our brave new world.</p>
<h3 id="data-and-methodology">Data and methodology</h3>
<p>The following analysis is based on a snapshot of the UTXO set at block height <a href="https://mempool.space/block/00000000000000000000bd33bb70d3d9f967b25bddc254ec4bf05655adba119e">918,997</a> (14 October 2025).
A UTXO set snapshot was used rather than full blockchain history because around 90% of all P2MS outputs ever created have never been spent, with very little spending activity in recent times.</p>
<p>The UTXO set was dumped using the <a href="https://github.com/in3rsha/bitcoin-utxo-dump"><code>bitcoin-utxo-dump</code></a> tool, and then processed using the code of the <a href="https://github.com/deadmanoz/data-carry-research"><code>data-carry-research</code></a> companion repository.
Note that the UTXO set as dumped using the <a href="https://github.com/in3rsha/bitcoin-utxo-dump"><code>bitcoin-utxo-dump</code></a> tool generates a CSV that is ~31GB at the time of <del>writing</del> dumping (block height <a href="https://mempool.space/block/00000000000000000000bd33bb70d3d9f967b25bddc254ec4bf05655adba119e">918,997</a>, 14 October 2025).
Note that the findings in this post are fully reproducible by using the <a href="https://github.com/in3rsha/bitcoin-utxo-dump"><code>bitcoin-utxo-dump</code></a> and <a href="https://github.com/deadmanoz/data-carry-research"><code>data-carry-research</code></a> tools.</p>
<h3 id="protocol-classification">Protocol classification</h3>
<p>The <a href="https://github.com/deadmanoz/data-carry-research"><code>data-carry-research</code></a> tool identifies and classifies P2MS outputs into the following categories:</p>
<table>
<thead>
<tr>
<th>Classification</th>
<th>Detection Method</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Bitcoin Stamps</strong></td>
<td>ARC4-obfuscated message analysis with <code>stamp:</code> (or variant) prefix</td>
</tr>
<tr>
<td><strong>Counterparty</strong></td>
<td>ARC4-obfuscated message analysis with <code>CNTRPRTY</code> prefix</td>
</tr>
<tr>
<td><strong>Omni Layer</strong></td>
<td>Presence of Exodus address (<a href="https://mempool.space/address/1EXoDusjGwvnjZUyKkxZ4UHEf77z6A5S4P"><code>1EXoDusj...</code></a>) as transaction output</td>
</tr>
<tr>
<td><strong>Chancecoin</strong></td>
<td><code>CHANCECO</code> identifier present in ASCII interpretation of P2MS outputs</td>
</tr>
<tr>
<td><strong>PPk</strong></td>
<td>Marker pubkey detection (<code>0320a0de...3e12</code>)</td>
</tr>
<tr>
<td><strong>ASCII Identifier Protocols</strong></td>
<td>Unobfuscated ASCII identifiers in P2MS outputs (e.g., <code>TB0001</code>, <code>TEST01</code>, <code>METROXMN</code>)</td>
</tr>
<tr>
<td><strong>OP_RETURN Signalled</strong></td>
<td>Protocol identifiers present in accompanying <code>OP_RETURN</code> outputs</td>
</tr>
<tr>
<td><strong>Data Storage</strong></td>
<td>Pattern matching for known data (WikiLeaks Cablegate, Bitcoin whitepaper) and file signatures</td>
</tr>
<tr>
<td><strong>Likely Data Storage</strong></td>
<td>Heuristic-based: dust-level values, high output counts, or invalid EC points</td>
</tr>
<tr>
<td><strong>Likely Legitimate Multisig</strong></td>
<td>All valid EC points, reasonable values, no protocol markers</td>
</tr>
</tbody>
</table>
<p><strong>Table 1:</strong> Classification methodology</p>
<p>Protocols are checked in a specific precedence order to avoid misclassification.
This ordering is important because some protocols use others as transport mechanisms, e.g., early Bitcoin Stamps transactions used Counterparty as a transport layer, requiring Bitcoin Stamps detection to occur before Counterparty classification.</p>
<h2 id="analysing-p2ms-utxos">Analysing P2MS UTXOs</h2>
<p>The snapshot of the UTXO set at block height <a href="https://mempool.space/block/00000000000000000000bd33bb70d3d9f967b25bddc254ec4bf05655adba119e">918,997</a> (14 October 2025) contains 2,423,456 P2MS transaction outputs created between block height 170,741 (transaction <a href="https://mempool.space/tx/947539645c59e6ab0cda61826cbacb55ef97a8178f012f8c18abe504bf66d4ce"><code>94753964...</code></a>) through to the snapshot height, across 1,330,493 transactions.
The ~2.4M P2MS transaction outputs account for 1.46% of the total transaction outputs in the UTXO set (166,127,819), yet only 0.00034851% of the total Bitcoin supply is encumbered in P2MS outputs (69.47467961 BTC).
Table 2 summarises some of the various high-level stats pertaining to P2MS outputs as of block height <a href="https://mempool.space/block/00000000000000000000bd33bb70d3d9f967b25bddc254ec4bf05655adba119e">918,997</a>.</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th align="right">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td># of unspent P2MS outputs</td>
<td align="right">2,423,456</td>
</tr>
<tr>
<td># of unspent total outputs</td>
<td align="right">166,127,819</td>
</tr>
<tr>
<td>P2MS % of total outputs</td>
<td align="right">1.46%</td>
</tr>
<tr>
<td># of transactions unspent P2MS outputs come from</td>
<td align="right">1,330,493</td>
</tr>
<tr>
<td>Value of P2MS outputs</td>
<td align="right">69.47467961 BTC</td>
</tr>
<tr>
<td>Total BTC supply</td>
<td align="right">19,934,368.75 BTC</td>
</tr>
<tr>
<td>P2MS % of total supply</td>
<td align="right">0.00034851%</td>
</tr>
<tr>
<td>Average value P2MS output</td>
<td align="right">0.00002867 BTC per output (2,867 sats)</td>
</tr>
<tr>
<td>Minimum value P2MS output</td>
<td align="right">0.00000001 BTC (1 sat)</td>
</tr>
<tr>
<td>Maximum value P2MS output</td>
<td align="right">1.85690258 BTC</td>
</tr>
</tbody>
</table>
<p><strong>Table 2:</strong> High-level P2MS stats as of block height 918,997 (14 October 2025).</p>
<blockquote>
<p><strong>Only 0.00035% of the total Bitcoin supply is encumbered in P2MS outputs (~69.5 BTC).</strong></p>
</blockquote>
<h3 id="top-level-classification-breakdown">Top-level classification breakdown</h3>
<p>Table 3 presents a classification breakdown of the P2MS transaction outputs.
The combined contribution of the three dominant protocols in Bitcoin Stamps, Counterparty and Omni, is 98.21% of all P2MS outputs.
If we include the other data carrying classifications of Chancecoin, PPk, OP_RETURN Signalled, ASCII Identifier Protocols, Data Storage and Likely Data Storage, then this value rises to 99.98%.
The remaining 0.02% represents what appears to be Likely Legitimate Multisig usage rather than data carriage.</p>
<blockquote>
<p><strong>Approximately 99.98% of all P2MS transaction outputs in the UTXO set are used for data carrying purposes.</strong></p>
</blockquote>
<table>
<thead>
<tr>
<th>Protocol/Use</th>
<th align="right">Transactions</th>
<th align="right">P2MS Outputs</th>
<th align="right">% of Total P2MS Outputs</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bitcoin Stamps</td>
<td align="right">969,868</td>
<td align="right">1,782,916</td>
<td align="right">73.57%</td>
</tr>
<tr>
<td>Counterparty</td>
<td align="right">303,677</td>
<td align="right">553,981</td>
<td align="right">22.86%</td>
</tr>
<tr>
<td>Omni Layer</td>
<td align="right">40,571</td>
<td align="right">43,077</td>
<td align="right">1.78%</td>
</tr>
<tr>
<td>Data Storage</td>
<td align="right">5,273</td>
<td align="right">28,831</td>
<td align="right">1.19%</td>
</tr>
<tr>
<td>Chancecoin</td>
<td align="right">2,647</td>
<td align="right">5,051</td>
<td align="right">0.21%</td>
</tr>
<tr>
<td>PPk</td>
<td align="right">4,706</td>
<td align="right">4,728</td>
<td align="right">0.20%</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td align="right">1,208</td>
<td align="right">2,152</td>
<td align="right">0.09%</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td align="right">1,342</td>
<td align="right">1,352</td>
<td align="right">0.06%</td>
</tr>
<tr>
<td>ASCII Identifier Protocols</td>
<td align="right">677</td>
<td align="right">816</td>
<td align="right">0.03%</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td align="right">524</td>
<td align="right">552</td>
<td align="right">0.02%</td>
</tr>
</tbody>
</table>
<p><strong>Table 3:</strong> Classification breakdown of P2MS outputs as of block height 918,997 (14 October 2025).</p>
<blockquote>
<p><strong>Bitcoin Stamps dominates unspent P2MS outputs, accounting for 73.57% of all such outputs.</strong></p>
</blockquote>
<p>Table 4 presents a breakdown by value encumbered in P2MS outputs.
Despite Bitcoin Stamps dominating by output count (73.57%), Counterparty leads by value with 35.87 BTC (51.6%) across its 553,981 outputs.
Bitcoin Stamps, with nearly four times as many outputs, holds only 14.19 BTC (20.4%) due to its dust-level output values.
The distribution of output value is examined in the <a href="#value-distribution">UTXO value breakdown section</a>.</p>
<table>
<thead>
<tr>
<th>Protocol/Use</th>
<th align="right">Outputs</th>
<th align="right">Total BTC</th>
<th align="right">Avg BTC/Output</th>
<th align="right">Min BTC</th>
<th align="right">Max BTC</th>
</tr>
</thead>
<tbody>
<tr>
<td>Counterparty</td>
<td align="right">553,981</td>
<td align="right">35.86506687</td>
<td align="right">0.00006474</td>
<td align="right">0.00000625</td>
<td align="right">0.38732200</td>
</tr>
<tr>
<td>Bitcoin Stamps</td>
<td align="right">1,782,916</td>
<td align="right">14.18754855</td>
<td align="right">0.00000796</td>
<td align="right">0.00000546</td>
<td align="right">0.00007800</td>
</tr>
<tr>
<td>Data Storage</td>
<td align="right">28,831</td>
<td align="right">10.18052541</td>
<td align="right">0.00035311</td>
<td align="right">0.00000001</td>
<td align="right">1.85690258</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td align="right">552</td>
<td align="right">6.49722937</td>
<td align="right">0.01177034</td>
<td align="right">0.00001004</td>
<td align="right">1.00916635</td>
</tr>
<tr>
<td>Omni Layer</td>
<td align="right">43,077</td>
<td align="right">2.32399710</td>
<td align="right">0.00005395</td>
<td align="right">0.00000007</td>
<td align="right">0.11066900</td>
</tr>
<tr>
<td>Chancecoin</td>
<td align="right">5,051</td>
<td align="right">0.15158820</td>
<td align="right">0.00003001</td>
<td align="right">0.00000780</td>
<td align="right">0.00010860</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td align="right">1,352</td>
<td align="right">0.10793465</td>
<td align="right">0.00007983</td>
<td align="right">0.00000013</td>
<td align="right">0.00084570</td>
</tr>
<tr>
<td>ASCII Identifier Protocols</td>
<td align="right">816</td>
<td align="right">0.07778934</td>
<td align="right">0.00009533</td>
<td align="right">0.00000780</td>
<td align="right">0.00130000</td>
</tr>
<tr>
<td>PPk</td>
<td align="right">4,728</td>
<td align="right">0.04827642</td>
<td align="right">0.00001021</td>
<td align="right">0.00001000</td>
<td align="right">0.00005757</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td align="right">2,152</td>
<td align="right">0.03472370</td>
<td align="right">0.00001614</td>
<td align="right">0.00000001</td>
<td align="right">0.00303350</td>
</tr>
</tbody>
</table>
<p><strong>Table 4:</strong> Value by classification breakdown of P2MS outputs as of block height 918,997 (14 October 2025).</p>
<blockquote>
<p><strong>The average value of Bitcoin Stamps classified P2MS outputs is just 796 sats.</strong></p>
</blockquote>
<h3 id="multisig-configuration-breakdown">Multisig configuration breakdown</h3>
<h4 id="standardness-rules">Standardness rules</h4>
<p>P2MS supports up to n=3 public keys for <strong>standard</strong> m-of-n multisig configurations (where m≤n).
However, <a href="https://bitcoin.stackexchange.com/questions/23893/what-are-the-limits-of-m-and-n-in-m-of-n-multisig-addresses">consensus rules</a> allow for any m-of-n combination where 1≤m≤n≤20.
That is, m-of-n combinations outside of the <strong>standard</strong> multisig range are considered <strong>non-standard</strong> by policy, failing Bitcoin Core's <code>IsStandard</code> evaluation (see <a href="https://github.com/bitcoin/bitcoin/blob/439e58c4d8194ca37f70346727d31f52e69592ec/src/policy/policy.cpp#L53-L74">policy.cpp</a>):</p>
<pre><code class="hljs language-c++"><span class="hljs-comment">// Support up to x-of-3 multisig txns as standard</span>
<span class="hljs-keyword">if</span> (n &#x3C; <span class="hljs-number">1</span> || n > <span class="hljs-number">3</span>)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
<span class="hljs-keyword">if</span> (m &#x3C; <span class="hljs-number">1</span> || m > n)
    <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
<span class="hljs-comment">///</span>
</code></pre>
<p>The classification produced by the <a href="https://github.com/in3rsha/bitcoin-utxo-dump/blob/9b1c015308f779ac529083ed7922cc551b8ddb53/utxodump.go#L514-L522"><code>bitcoin-utxo-dump</code></a> tool is more permissive than that of Bitcoin Core (see snippet from <code>bitcoin-utxo-dump</code> below).
Specifically, non-standard scripts that are at least 37 bytes in length and end with the <code>OP_CHECKMULTISIG</code> opcode are marked as <code>multisig</code> (P2MS).
This, however, does not pose a significant discrepancy - only 48 UTXOs match this criteria in the analysed UTXO set snapshot.</p>
<pre><code class="hljs language-go"><span class="hljs-comment">// P2MS</span>
<span class="hljs-comment">// if there is a script, it's at least 37 bytes in length (min size for a P2MS)</span>
<span class="hljs-comment">// and if the last opcode is OP_CHECKMULTISIG (174) (0xae)</span>
<span class="hljs-keyword">case</span> <span class="hljs-built_in">len</span>(script) >= <span class="hljs-number">37</span> &#x26;&#x26; script[<span class="hljs-built_in">len</span>(script)<span class="hljs-number">-1</span>] == <span class="hljs-number">174</span>: 
    scriptType = <span class="hljs-string">"p2ms"</span>
    scriptTypeCount[<span class="hljs-string">"p2ms"</span>] += <span class="hljs-number">1</span>

<span class="hljs-comment">// Non-Standard</span>
(<span class="hljs-keyword">if</span> the script <span class="hljs-keyword">type</span> hasn<span class="hljs-string">'t been identified and set then it remains as an unknown "non-standard" script)
default:
  scriptType = "non-standard"
    scriptTypeCount["non-standard"] += 1
</span></code></pre>
<h4 id="observed-configurations">Observed configurations</h4>
<p>Table 5 shows the prevalence of different multisig configurations in the P2MS outputs in the UTXO set at the analysis block height.</p>
<table>
<thead>
<tr>
<th>Multisig Configuration</th>
<th align="right">Count</th>
<th align="right">Percentage</th>
</tr>
</thead>
<tbody>
<tr>
<td>1-of-3</td>
<td align="right">2,213,925</td>
<td align="right">91.35%</td>
</tr>
<tr>
<td>1-of-2</td>
<td align="right">204,619</td>
<td align="right">8.44%</td>
</tr>
<tr>
<td>2-of-2</td>
<td align="right">3,262</td>
<td align="right">0.13%</td>
</tr>
<tr>
<td>1-of-1</td>
<td align="right">1,121</td>
<td align="right">0.05%</td>
</tr>
<tr>
<td>2-of-3</td>
<td align="right">509</td>
<td align="right">0.02%</td>
</tr>
<tr>
<td>3-of-3</td>
<td align="right">20</td>
<td align="right">0.0%</td>
</tr>
</tbody>
</table>
<p><strong>Table 5:</strong> Prevalence of different multisig configurations observed in the UTXO set of block height 918,997 (14 October 2025).</p>
<p>The 1-of-3 multisig configuration dominates, accounting for 2,213,925 outputs or 91.35% of all P2MS outputs in the UTXO set.
This dominance is almost entirely attributable to Bitcoin Stamps, which exclusively uses 1-of-3 configurations, though Counterparty and Data Storage also favour 1-of-3.</p>
<p>The 1-of-2 multisig configuration accounts for 8.44%, largely due to Counterparty.
90.16% of Omni P2MS outputs use 1-of-2, and Chancecoin exclusively uses this configuration.</p>
<p>The remaining configurations are marginal: 2-of-2 represents just 0.13% (3,262 outputs), while 1-of-1, 2-of-3, and 3-of-3 collectively account for less than 0.1% of the UTXO set.
Table 6 gives the full breakdown by protocol.</p>
<p><strong>TABLE: Multisig configuration by protocol breakdown</strong></p>
<table>
<thead>
<tr>
<th>Protocol</th>
<th>Multisig Type</th>
<th align="right">Outputs</th>
<th align="right">% of Protocol</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bitcoin Stamps</td>
<td>1-of-3</td>
<td align="right">1,782,916</td>
<td align="right">100.0%</td>
</tr>
<tr>
<td>Counterparty</td>
<td>1-of-3</td>
<td align="right">399,412</td>
<td align="right">72.1%</td>
</tr>
<tr>
<td>Counterparty</td>
<td>1-of-2</td>
<td align="right">153,930</td>
<td align="right">27.79%</td>
</tr>
<tr>
<td>Counterparty</td>
<td>2-of-2</td>
<td align="right">625</td>
<td align="right">0.11%</td>
</tr>
<tr>
<td>Counterparty</td>
<td>3-of-3</td>
<td align="right">8</td>
<td align="right">0.0%</td>
</tr>
<tr>
<td>Counterparty</td>
<td>2-of-3</td>
<td align="right">6</td>
<td align="right">0.0%</td>
</tr>
<tr>
<td>Omni Layer</td>
<td>1-of-2</td>
<td align="right">38,839</td>
<td align="right">90.16%</td>
</tr>
<tr>
<td>Omni Layer</td>
<td>1-of-3</td>
<td align="right">4,236</td>
<td align="right">9.83%</td>
</tr>
<tr>
<td>Omni Layer</td>
<td>1-of-1</td>
<td align="right">2</td>
<td align="right">0.0%</td>
</tr>
<tr>
<td>Data Storage</td>
<td>1-of-3</td>
<td align="right">23,307</td>
<td align="right">80.84%</td>
</tr>
<tr>
<td>Data Storage</td>
<td>1-of-2</td>
<td align="right">4,590</td>
<td align="right">15.92%</td>
</tr>
<tr>
<td>Data Storage</td>
<td>2-of-2</td>
<td align="right">418</td>
<td align="right">1.45%</td>
</tr>
<tr>
<td>Data Storage</td>
<td>1-of-1</td>
<td align="right">368</td>
<td align="right">1.28%</td>
</tr>
<tr>
<td>Data Storage</td>
<td>2-of-3</td>
<td align="right">137</td>
<td align="right">0.48%</td>
</tr>
<tr>
<td>Data Storage</td>
<td>3-of-3</td>
<td align="right">11</td>
<td align="right">0.04%</td>
</tr>
<tr>
<td>Chancecoin</td>
<td>1-of-2</td>
<td align="right">5,051</td>
<td align="right">100.0%</td>
</tr>
<tr>
<td>PPk</td>
<td>1-of-3</td>
<td align="right">3,330</td>
<td align="right">70.43%</td>
</tr>
<tr>
<td>PPk</td>
<td>1-of-2</td>
<td align="right">1,398</td>
<td align="right">29.57%</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td>2-of-2</td>
<td align="right">854</td>
<td align="right">39.68%</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td>1-of-1</td>
<td align="right">697</td>
<td align="right">32.39%</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td>1-of-3</td>
<td align="right">299</td>
<td align="right">13.89%</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td>2-of-3</td>
<td align="right">291</td>
<td align="right">13.52%</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td>1-of-2</td>
<td align="right">11</td>
<td align="right">0.51%</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td>2-of-2</td>
<td align="right">1,137</td>
<td align="right">84.1%</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td>1-of-2</td>
<td align="right">146</td>
<td align="right">10.8%</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td>1-of-3</td>
<td align="right">62</td>
<td align="right">4.59%</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td>2-of-3</td>
<td align="right">5</td>
<td align="right">0.37%</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td>3-of-3</td>
<td align="right">1</td>
<td align="right">0.07%</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td>1-of-1</td>
<td align="right">1</td>
<td align="right">0.07%</td>
</tr>
<tr>
<td>ASCII Identifier Protocols</td>
<td>1-of-2</td>
<td align="right">516</td>
<td align="right">63.24%</td>
</tr>
<tr>
<td>ASCII Identifier Protocols</td>
<td>1-of-3</td>
<td align="right">300</td>
<td align="right">36.76%</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td>2-of-2</td>
<td align="right">228</td>
<td align="right">41.30%</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td>1-of-2</td>
<td align="right">138</td>
<td align="right">25.00%</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td>2-of-3</td>
<td align="right">70</td>
<td align="right">12.68%</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td>1-of-3</td>
<td align="right">63</td>
<td align="right">11.41%</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td>1-of-1</td>
<td align="right">53</td>
<td align="right">9.60%</td>
</tr>
</tbody>
</table>
<p><strong>Table 6:</strong> Breakdown of multisig configurations by protocol as of block height 918,997 (14 October 2025).</p>
<h3 id="spendability-breakdown">Spendability breakdown</h3>
<p>In the context of P2MS, a spendable output contains at least one valid public key with a known corresponding private key, meaning the output could theoretically be spent to recover the encumbered bitcoin.
An unspendable output either contains no valid public keys or uses keys for which no private key is known to exist (such as Bitcoin Stamps' Key Burn addresses), permanently locking the bitcoin in the UTXO set.</p>
<p>As explored in <a href="./p2ms-data-carry-1#fake-keys">Part 1: Fundamentals and Examples - Fake Keys</a>, we can assess pubkeys to determine if they are invalid.
More specifically, we can check whether a given public key is a valid point on the ECDSA secp256k1 curve. If it is not, we know it is a "fake" public key.
However, if it is on the curve, it could be a true key or just "data" that happens to correspond to a point on the curve.
This property is one component in the analysis of the spendability of the P2MS outputs in the UTXO set.</p>
<p>The other component in this analysis is leveraging what is known about the various protocols that use P2MS outputs.
Again, as covered in <a href="./p2ms-data-carry-1#summarising-the-main-techniques">Part 1: Fundamentals and Examples - Summarising the main techniques</a>, we know that:</p>
<ul>
<li>Bitcoin Stamps exclusively uses Key Burn keys and data keys to ensure unspendability.</li>
<li>Counterparty, Omni and Chancecoin maintain at least one valid public key per output, ensuring spendability and avoiding permanent UTXO set pollution.</li>
</ul>
<p>Combining these two components, we can classify P2MS outputs as either spendable or unspendable.</p>
<p>Table 7 presents the top-level spendability breakdown.
Of the 2.4M P2MS outputs in the UTXO set, 1.8M (74.37%) are unspendable and will remain in the UTXO set forever.</p>
<table>
<thead>
<tr>
<th>Status</th>
<th align="right">Count</th>
<th align="right">% of Outputs</th>
<th align="right">Total BTC</th>
<th align="right">% of BTC Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Spendable</td>
<td align="right">621,202</td>
<td align="right">25.63%</td>
<td align="right">55.20673574</td>
<td align="right">79.46%</td>
</tr>
<tr>
<td>Unspendable</td>
<td align="right">1,802,254</td>
<td align="right">74.37%</td>
<td align="right">14.26794387</td>
<td align="right">20.54%</td>
</tr>
</tbody>
</table>
<p><strong>Table 7:</strong> Spendability breakdown of P2MS outputs in the UTXO set, as of block height 918,997 (14 October 2025).</p>
<p>Notably, the value distribution is inverted: although 74.37% of outputs are unspendable, they contain only 20.54% of the total BTC value (~14.3 BTC).
The remaining 79.46% of value (~55.2 BTC) resides in spendable outputs.
This inversion occurs because Bitcoin Stamps outputs use minimal dust-level amounts (546-1000 sats), while legitimate multisig and older Counterparty/Omni transactions encumber higher amounts.</p>
<blockquote>
<p><strong>~79.5% of the total value of P2MS outputs is spendable (~55.2 BTC), only 20.5% (~14.3 BTC) is unspendable.</strong></p>
</blockquote>
<p>Table 8 breaks down spendability by protocol.
Bitcoin Stamps is responsible for 1,782,916 (98.9%) of the unspendable P2MS outputs in the UTXO set.
In contrast, Counterparty, Omni, Chancecoin, PPk, and legitimate multisig outputs are 100% spendable.</p>
<table>
<thead>
<tr>
<th>Protocol</th>
<th align="right">Spendable</th>
<th align="right">Unspendable</th>
<th align="right">% Spendable</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bitcoin Stamps</td>
<td align="right">0</td>
<td align="right">1,782,916</td>
<td align="right">0.00%</td>
</tr>
<tr>
<td>Counterparty</td>
<td align="right">553,981</td>
<td align="right">0</td>
<td align="right">100.00%</td>
</tr>
<tr>
<td>Omni Layer</td>
<td align="right">43,077</td>
<td align="right">0</td>
<td align="right">100.00%</td>
</tr>
<tr>
<td>Data Storage</td>
<td align="right">9,597</td>
<td align="right">19,234</td>
<td align="right">33.29%</td>
</tr>
<tr>
<td>Chancecoin</td>
<td align="right">5,051</td>
<td align="right">0</td>
<td align="right">100.00%</td>
</tr>
<tr>
<td>PPk</td>
<td align="right">4,728</td>
<td align="right">0</td>
<td align="right">100.00%</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td align="right">2,149</td>
<td align="right">3</td>
<td align="right">99.86%</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td align="right">1,251</td>
<td align="right">101</td>
<td align="right">92.53%</td>
</tr>
<tr>
<td>ASCII Identifier Protocols</td>
<td align="right">816</td>
<td align="right">0</td>
<td align="right">100.00%</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td align="right">552</td>
<td align="right">0</td>
<td align="right">100.00%</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>621,202</strong> (25.63%)</td>
<td align="right"><strong>1,802,254</strong> (74.37%)</td>
<td align="right"></td>
</tr>
</tbody>
</table>
<p><strong>Table 8:</strong> Spendability of P2MS outputs in the UTXO set, separated by protocol classification, as of block height 918,997 (14 October 2025).</p>
<blockquote>
<p><strong>Almost 75% of P2MS outputs in the UTXO set are unspendable, with Bitcoin Stamps responsible for virtually all of them.</strong></p>
</blockquote>
<h4 id="spendability-over-time">Spendability over time</h4>
<p>The 74.37% unspendability figure reflects the accumulated history of P2MS usage since 2012.
However, recent years show an even more pronounced trend: almost all P2MS outputs created since 2023 are unspendable due to Bitcoin Stamps' design.
Figure 1 visualises this dramatic shift.</p>
<p><strong>[Interactive plot - view on website]</strong></p>
<p>P2MS output spendability over time, showing the percentage of spendable (green) vs unspendable (red) outputs created each month. As per the UTXO set as of block height 918,997 (14 October 2025).</p>
<h4 id="spendability-reasons">Spendability reasons</h4>
<p>Breaking down the underlying reasons for spendability provides additional granularity (Table 9):</p>
<table>
<thead>
<tr>
<th>Reason</th>
<th align="right">Count</th>
<th align="right">% of Total</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Unspendable</strong> - Mixed Key Burn and data</td>
<td align="right">1,782,916</td>
<td align="right">73.57%</td>
</tr>
<tr>
<td><strong>Unspendable</strong> - all data keys</td>
<td align="right">19,338</td>
<td align="right">0.80%</td>
</tr>
<tr>
<td><strong>Spendable</strong> - Contains real pubkey</td>
<td align="right">620,650</td>
<td align="right">25.61%</td>
</tr>
<tr>
<td><strong>Spendable</strong> - All valid EC points</td>
<td align="right">552</td>
<td align="right">0.02%</td>
</tr>
</tbody>
</table>
<p><strong>Table 9:</strong> Classified reasons for P2MS UTXO spendability, as of block height 918,997 (14 October 2025).</p>
<p>The "mixed Key Burn and data" classification (73.57%) represents Bitcoin Stamps outputs that deliberately combine Key Burn addresses with data-carrying keys, making them effectively unspendable.
"All data keys" (0.80%) encompasses outputs where all keys are used purely for data carriage with none representing valid, spendable public keys - primarily early Data Storage efforts.
"Contains real pubkey" (25.61%) indicates outputs that include at least one valid public key that could theoretically be used to spend the output, covering Counterparty, Omni, Chancecoin, and PPk transactions.
"All valid EC points" (0.02%) represents outputs where all keys are valid points on the ECDSA secp256k1 curve, suggesting legitimate multisig usage.</p>
<h3 id="utxo-age-distribution">UTXO age distribution</h3>
<p>In considering the block height at which each unspent P2MS output was created alongside the protocol classification, we can plot the age distribution as per Figure 2.
This groups the age by month and shows annotations for when:</p>
<ul>
<li>P2MS was made standard (March 2012)</li>
<li>Omni was launched (July 2013)</li>
<li>Counterparty was launched (January 2014)</li>
<li>Bitcoin Stamps was launched (April 2023)</li>
</ul>
<p><strong>[Interactive plot - view on website]</strong></p>
<p>Distribution of the age (per-month) of P2MS outputs in the UTXO set, separated by protocol, as of block height 918,997 (14 October 2025).</p>
<p>There are a few interesting observations to be made from this visualisation:</p>
<ul>
<li>We can see the rise and fall in the popularity of Omni (2014-2016)</li>
<li>We can see the rise, fall (2014-2016) and later muted resurgence of Counterparty (2023), with the resurgence likely relating to the launch of Bitcoin Stamps in April 2023.</li>
<li>We can see that in April 2013 there was a peak in Data Storage P2MS outputs, due to both the "WikiLeaks Cablegate" data and the "Bitcoin Whitepaper" being embedded in P2MS outputs in this month.</li>
</ul>
<h3 id="data-content-type-breakdown">Data content type breakdown</h3>
<p>With support for the detection, deobfuscation, parsing and interpretation of various protocols implemented in <a href="https://github.com/deadmanoz/data-carry-research"><code>data-carry-research</code></a>, we can consider the type of data that is embedded in P2MS UTXOs.
Table 10 shows the distribution of detected content types across P2MS transactions and outputs.</p>
<table>
<thead>
<tr>
<th>Content Type</th>
<th align="right">Transactions</th>
<th align="right">% of total transactions</th>
<th align="right">Outputs</th>
<th align="right">% of total outputs</th>
</tr>
</thead>
<tbody>
<tr>
<td>application/json</td>
<td align="right">966,455</td>
<td align="right">72.64%</td>
<td align="right">1,708,561</td>
<td align="right">71.83%</td>
</tr>
<tr>
<td>application/octet-stream</td>
<td align="right">353,953</td>
<td align="right">26.60%</td>
<td align="right">590,289</td>
<td align="right">24.82%</td>
</tr>
<tr>
<td>image/png</td>
<td align="right">3,343</td>
<td align="right">0.25%</td>
<td align="right">49,806</td>
<td align="right">2.09%</td>
</tr>
<tr>
<td>image/svg+xml</td>
<td align="right">133</td>
<td align="right">0.01%</td>
<td align="right">9,681</td>
<td align="right">0.41%</td>
</tr>
<tr>
<td>image/gif</td>
<td align="right">603</td>
<td align="right">0.05%</td>
<td align="right">9,610</td>
<td align="right">0.40%</td>
</tr>
<tr>
<td>text/plain</td>
<td align="right">3,018</td>
<td align="right">0.23%</td>
<td align="right">4,105</td>
<td align="right">0.17%</td>
</tr>
<tr>
<td>image/jpeg</td>
<td align="right">60</td>
<td align="right">&#x3C;0.01%</td>
<td align="right">2,634</td>
<td align="right">0.11%</td>
</tr>
<tr>
<td>application/zlib</td>
<td align="right">1,062</td>
<td align="right">0.08%</td>
<td align="right">2,144</td>
<td align="right">0.09%</td>
</tr>
<tr>
<td>text/html</td>
<td align="right">25</td>
<td align="right">&#x3C;0.01%</td>
<td align="right">1,035</td>
<td align="right">0.04%</td>
</tr>
<tr>
<td>image/webp</td>
<td align="right">19</td>
<td align="right">&#x3C;0.01%</td>
<td align="right">530</td>
<td align="right">0.02%</td>
</tr>
<tr>
<td>image/bmp</td>
<td align="right">25</td>
<td align="right">&#x3C;0.01%</td>
<td align="right">151</td>
<td align="right">0.01%</td>
</tr>
<tr>
<td>text/x-python</td>
<td align="right">1</td>
<td align="right">&#x3C;0.01%</td>
<td align="right">23</td>
<td align="right">&#x3C;0.01%</td>
</tr>
<tr>
<td>text/javascript</td>
<td align="right">2</td>
<td align="right">&#x3C;0.01%</td>
<td align="right">14</td>
<td align="right">&#x3C;0.01%</td>
</tr>
<tr>
<td>application/gzip</td>
<td align="right">1</td>
<td align="right">&#x3C;0.01%</td>
<td align="right">2</td>
<td align="right">&#x3C;0.01%</td>
</tr>
</tbody>
</table>
<p><strong>Table 10:</strong> Content type distribution across P2MS transactions and outputs as of block height 918,997 (14 October 2025).</p>
<p>Takeaways from this breakdown include:</p>
<ul>
<li>The dominant content type is <code>application/json</code> (~72%), primarily from the "SRC-20", "SRC-721", and "SRC-101" sub-protocols of Bitcoin Stamps, all of which use JSON-formatted messages.</li>
<li>Raw binary data (<code>application/octet-stream</code>) accounts for ~25%, representing binary data formats like Counterparty, Omni Layer, Chancecoin and PPk.</li>
<li>Plain text (<code>text/plain</code>) represents less than 0.25%, encompassing human-readable messages embedded in P2MS outputs.</li>
<li>Image formats (<code>image/png</code>, <code>image/svg+xml</code>, <code>image/gif</code>, <code>image/jpeg</code>, <code>image/webp</code>, <code>image/bmp</code>) show significant divergence between transaction and output counts.
PNG images appear in only 3,343 transactions yet span 49,806 outputs, reflecting how images are encoded across many P2MS outputs per transaction.
Collectively, image formats represent approximately 3% of outputs but only 0.31% of transactions.</li>
</ul>
<h3 id="data-size-breakdown">Data size breakdown</h3>
<p>Given the commentary that appears whenever discussing the UTXO set, another natural question to ask about the P2MS outputs is "what is the data size of the P2MS outputs"?
At the highest level, we can answer this question by simply considering the full size of each P2MS script.
For example, a "typical" 1-of-3 P2MS output script with 33-byte compressed public keys has a size of 105 bytes:</p>
<ul>
<li><code>OP_1</code> - 1 byte</li>
<li><code>OP_PUSHBYTES_33 &#x3C;33-byte pubkey></code> - 1 + 33 bytes</li>
<li><code>OP_PUSHBYTES_33 &#x3C;33-byte pubkey></code> - 1 + 33 bytes</li>
<li><code>OP_PUSHBYTES_33 &#x3C;33-byte pubkey></code> - 1 + 33 bytes</li>
<li><code>OP_3</code> - 1 byte</li>
<li><code>OP_CHECKMULTISIG</code> - 1 byte</li>
</ul>
<p>1 + 33 + 1 + 33 + 1 + 33 + 1 + 1 = 105 bytes.</p>
<table>
<thead>
<tr>
<th>M-of-N</th>
<th>Keys</th>
<th align="right">Script size</th>
<th align="right">Outputs</th>
<th align="right">Total Data Size</th>
<th align="right">% of Total</th>
</tr>
</thead>
<tbody>
<tr>
<td>1-of-3</td>
<td>CCC</td>
<td align="right">105 B</td>
<td align="right">2,123,648</td>
<td align="right">223.0 MB</td>
<td align="right">87.63%</td>
</tr>
<tr>
<td>1-of-2</td>
<td>CC</td>
<td align="right">71 B</td>
<td align="right">177,783</td>
<td align="right">12.6 MB</td>
<td align="right">7.34%</td>
</tr>
<tr>
<td>1-of-3</td>
<td>CCU</td>
<td align="right">137 B</td>
<td align="right">73,426</td>
<td align="right">10.1 MB</td>
<td align="right">3.03%</td>
</tr>
<tr>
<td>1-of-3</td>
<td>UUU</td>
<td align="right">201 B</td>
<td align="right">16,835</td>
<td align="right">3.4 MB</td>
<td align="right">0.69%</td>
</tr>
<tr>
<td>1-of-2</td>
<td>CU</td>
<td align="right">103 B</td>
<td align="right">26,690</td>
<td align="right">2.7 MB</td>
<td align="right">1.10%</td>
</tr>
<tr>
<td>2-of-2</td>
<td>CC</td>
<td align="right">71 B</td>
<td align="right">3,241</td>
<td align="right">230 KB</td>
<td align="right">0.13%</td>
</tr>
<tr>
<td>1-of-1</td>
<td>U</td>
<td align="right">69 B</td>
<td align="right">752</td>
<td align="right">52 KB</td>
<td align="right">0.03%</td>
</tr>
<tr>
<td>2-of-3</td>
<td>CCC</td>
<td align="right">105 B</td>
<td align="right">482</td>
<td align="right">51 KB</td>
<td align="right">0.02%</td>
</tr>
<tr>
<td>1-of-2</td>
<td>UU</td>
<td align="right">135 B</td>
<td align="right">146</td>
<td align="right">20 KB</td>
<td align="right">0.01%</td>
</tr>
<tr>
<td>1-of-1</td>
<td>C</td>
<td align="right">37 B</td>
<td align="right">369</td>
<td align="right">14 KB</td>
<td align="right">0.02%</td>
</tr>
<tr>
<td>2-of-3</td>
<td>UUU</td>
<td align="right">201 B</td>
<td align="right">22</td>
<td align="right">4.4 KB</td>
<td align="right">&#x3C;0.01%</td>
</tr>
<tr>
<td>1-of-3</td>
<td>CUU</td>
<td align="right">169 B</td>
<td align="right">16</td>
<td align="right">2.7 KB</td>
<td align="right">&#x3C;0.01%</td>
</tr>
<tr>
<td>3-of-3</td>
<td>CCC</td>
<td align="right">105 B</td>
<td align="right">20</td>
<td align="right">2.1 KB</td>
<td align="right">&#x3C;0.01%</td>
</tr>
<tr>
<td>2-of-2</td>
<td>UU</td>
<td align="right">135 B</td>
<td align="right">11</td>
<td align="right">1.5 KB</td>
<td align="right">&#x3C;0.01%</td>
</tr>
<tr>
<td>2-of-2</td>
<td>CU</td>
<td align="right">103 B</td>
<td align="right">10</td>
<td align="right">1.0 KB</td>
<td align="right">&#x3C;0.01%</td>
</tr>
<tr>
<td>2-of-3</td>
<td>CCU</td>
<td align="right">137 B</td>
<td align="right">5</td>
<td align="right">685 B</td>
<td align="right">&#x3C;0.01%</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td></td>
<td align="right"></td>
<td align="right"><strong>2,423,456</strong></td>
<td align="right"><strong>252.2 MB</strong></td>
<td align="right"><strong>100.00%</strong></td>
</tr>
</tbody>
</table>
<p><strong>Table 11:</strong> P2MS multisig configurations in the UTXO set showing key combinations, script sizes, and total data footprint. C = compressed keys (33 bytes), U = uncompressed keys (65 bytes).</p>
<p>Table 11 reveals that the total data size of all P2MS outputs in the UTXO set is 252.2 MB, with 1-of-3 multisig with compressed keys accounting for 223 MB (87.63%) of this data footprint alone.
Note that the entire UTXO set at block height 918,997 is approximately 11.4 GB in size, meaning P2MS outputs account for ~2.2% of the total UTXO set size, despite only representing 1.46% of the total outputs.</p>
<p>As a point of reference, according to BitMEX Research on <a href="https://www.bitmex.com/blog/ordinals-impact-on-node-runners">Ordinals - Impact on Node Runners</a>, Ordinal images and other data take up around 30GB of blockchain space as of September 2025, with an additional ~27GB used by BRC-20 related transactions.
So the contribution of P2MS outputs to the overall UTXO set size is relatively minor in comparison, though the effectively unspendable nature of much of the P2MS data does raise questions about UTXO set bloat and long-term sustainability.</p>
<h3 id="transaction-size-distribution">Transaction size distribution</h3>
<p>While the previous section examined the size of individual P2MS outputs, it's also informative to consider the size of entire transactions that contain P2MS outputs.
Transaction size directly determines block space consumption and, consequently, the fees paid by users of these data-carrying protocols.</p>
<p>Figure 3 shows the distribution of transaction sizes across the 1.33 million transactions relating to the unspent P2MS outputs.
The overwhelming majority of transactions (1.17M, or 88%) fall within the 250-500 byte range, reflecting the typical size of P2MS transactions used by Bitcoin Stamps and Counterparty.</p>
<p><strong>[Interactive plot - view on website]</strong></p>
<p>Distribution of P2MS transaction sizes, as of block height 918,997 (14 October 2025).</p>
<h3 id="utxo-value-breakdown">UTXO value breakdown</h3>
<h4 id="value-distribution">Value distribution</h4>
<p>Figure 4 shows the distribution of P2MS output values across different ranges, with protocol-specific breakdowns available via the legend.
This figure reveals the following insights:</p>
<ul>
<li><strong>A mode at 546-1K sats</strong>: 1.82M outputs (75% of all P2MS outputs), almost entirely attributable to Bitcoin Stamps.
This is just above the 546 sat dust threshold, obviously motivated by ensuring transaction standardness while minimising the cost of data embedding.</li>
<li><strong>A mode at 5K-10K sats</strong>: a secondary peak of ~402K outputs, dominated by Counterparty transactions.</li>
<li><strong>Sub-dust range (0-546 sats) contains 16,861 outputs</strong>: Data Storage accounting for nearly all of them (16,847).
This is explored in the following.</li>
</ul>
<p><strong>[Interactive plot - view on website]</strong></p>
<p>P2MS UTXO Value Distribution by protocol and value range, as of block height 918,997 (14 October 2025).</p>
<h4 id="dust-threshold-analysis">Dust threshold analysis</h4>
<p>The value of UTXOs is relevant in the context of dust limits, as outputs below the dust threshold are non-standard and would not be relayed by most Bitcoin nodes were they to appear in a transaction.
It might seem that the dust limit for P2MS outputs depends on the multisig configuration and key types used, but this is only true for the creation, and not spending, of P2MS outputs.
The following unpacks how Bitcoin Core calculates the dust threshold for outputs; we'll find that the dust threshold to spend P2MS outputs is 546 sats when spending to a non-segwit output (e.g., P2PKH), or 294 sats when spending to a segwit output (e.g., P2WPKH), <em><strong>regardless of the multisig configuration or key types used</strong></em>.</p>
<p><strong>Bitcoin Core Policy &#x26; Dust Threshold Calculation</strong></p>
<p>Each output is checked whether it is dust via <a href="https://github.com/bitcoin/bitcoin/blob/a14e7b9dee9145920f93eab0254ce92942bd1e5e/src/policy/policy.cpp#L65"><code>IsDust</code></a>, with the output value evaluated against the dust threshold (<a href="https://github.com/bitcoin/bitcoin/blob/a14e7b9dee9145920f93eab0254ce92942bd1e5e/src/policy/policy.cpp#L26"><code>GetDustThreshold</code></a>).
Both of these methods require a value for the <code>dustRelayFeeIn</code> argument; this is <code>DUST_RELAY_TX_FEE</code> which has a current value of 3000 sat/kvB (set <a href="https://github.com/bitcoin/bitcoin/blob/a14e7b9dee9145920f93eab0254ce92942bd1e5e/src/policy/policy.h#L64">here</a>).</p>
<pre><code class="hljs language-c++"><span class="hljs-function">CAmount <span class="hljs-title">GetDustThreshold</span><span class="hljs-params">(<span class="hljs-type">const</span> CTxOut&#x26; txout, <span class="hljs-type">const</span> CFeeRate&#x26; dustRelayFeeIn)</span>
</span>{
    <span class="hljs-comment">// "Dust" is defined in terms of dustRelayFee,</span>
    <span class="hljs-comment">// which has units satoshis-per-kilobyte.</span>
    <span class="hljs-comment">// If you'd pay more in fees than the value of the output</span>
    <span class="hljs-comment">// to spend something, then we consider it dust.</span>
    <span class="hljs-comment">// A typical spendable non-segwit txout is 34 bytes big, and will</span>
    <span class="hljs-comment">// need a CTxIn of at least 148 bytes to spend:</span>
    <span class="hljs-comment">// so dust is a spendable txout less than</span>
    <span class="hljs-comment">// 182*dustRelayFee/1000 (in satoshis).</span>
    <span class="hljs-comment">// 546 satoshis at the default rate of 3000 sat/kvB.</span>
    <span class="hljs-comment">// A typical spendable segwit P2WPKH txout is 31 bytes big, and will</span>
    <span class="hljs-comment">// need a CTxIn of at least 67 bytes to spend:</span>
    <span class="hljs-comment">// so dust is a spendable txout less than</span>
    <span class="hljs-comment">// 98*dustRelayFee/1000 (in satoshis).</span>
    <span class="hljs-comment">// 294 satoshis at the default rate of 3000 sat/kvB.</span>
    <span class="hljs-keyword">if</span> (txout.scriptPubKey.<span class="hljs-built_in">IsUnspendable</span>())
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

    <span class="hljs-type">size_t</span> nSize = <span class="hljs-built_in">GetSerializeSize</span>(txout);
    <span class="hljs-type">int</span> witnessversion = <span class="hljs-number">0</span>;
    std::vector&#x3C;<span class="hljs-type">unsigned</span> <span class="hljs-type">char</span>> witnessprogram;

    <span class="hljs-comment">// Note this computation is for spending a Segwit v0 P2WPKH output (a 33 bytes</span>
    <span class="hljs-comment">// public key + an ECDSA signature). For Segwit v1 Taproot outputs the minimum</span>
    <span class="hljs-comment">// satisfaction is lower (a single BIP340 signature) but this computation was</span>
    <span class="hljs-comment">// kept to not further reduce the dust level.</span>
    <span class="hljs-comment">// See discussion in https://github.com/bitcoin/bitcoin/pull/22779 for details.</span>
    <span class="hljs-keyword">if</span> (txout.scriptPubKey.<span class="hljs-built_in">IsWitnessProgram</span>(witnessversion, witnessprogram)) {
        <span class="hljs-comment">// sum the sizes of the parts of a transaction input</span>
        <span class="hljs-comment">// with 75% segwit discount applied to the script size.</span>
        nSize += (<span class="hljs-number">32</span> + <span class="hljs-number">4</span> + <span class="hljs-number">1</span> + (<span class="hljs-number">107</span> / WITNESS_SCALE_FACTOR) + <span class="hljs-number">4</span>);
    } <span class="hljs-keyword">else</span> {
        nSize += (<span class="hljs-number">32</span> + <span class="hljs-number">4</span> + <span class="hljs-number">1</span> + <span class="hljs-number">107</span> + <span class="hljs-number">4</span>); <span class="hljs-comment">// the 148 mentioned above</span>
    }

    <span class="hljs-keyword">return</span> dustRelayFeeIn.<span class="hljs-built_in">GetFee</span>(nSize);
}
</code></pre>
<p>As can be observed in the code snippet, <code>GetDustThreshold</code> calculates the serialised size of the output and then adds 148 to it, to establish a size for fee calculation.
As the comment suggests, this 148 bytes is an assumption of the size of the <code>scriptSig</code> needed to spend the output and represents the spending of a typical P2PKH input:</p>
<ul>
<li>32 bytes: <code>txid</code> (references the previous output's transaction)</li>
<li>4 bytes: <code>vout</code> (index of the output within that transaction)</li>
<li>1 byte: <code>scriptSig</code> length</li>
<li>107 bytes: typical <code>scriptSig</code> size, e.g. <code>OP_PUSHBYTES_72</code> <code>&#x3C;72-byte-sig></code> <code>OP_PUSHBYTES_33</code> <code>&#x3C;33-byte-compressed-key></code></li>
<li>4 bytes: <code>sequence</code></li>
</ul>
<p>Although this fixed input size is used for all non-segwit inputs, it's worth considering what the input size would look like to spend a P2MS output.
We'd have the common elements of <code>txid</code> (32 bytes), <code>vout</code> (4 bytes), <code>scriptSig</code> length (1 byte), and <code>sequence</code> (4 bytes), for 41 bytes.
The <code>scriptSig</code> is where we'd see variation depending on the number of required signatures (m) in the m-of-n multisig configuration.
Note that there is an <code>OP_0</code> dummy element to address the extra stack element consumed by <code>OP_CHECKMULTISIG</code> and <code>OP_CHECKMULTISIGVERIFY</code>, as per <a href="https://github.com/bitcoin/bips/blob/master/bip-0147.mediawiki">BIP-147</a>, and we assume the mid-point 72-bytes for a signature.</p>
<table>
<thead>
<tr>
<th>Multisig configuration</th>
<th><code>scriptSig</code> breakdown</th>
<th align="right"><code>scriptSig</code> size (bytes)</th>
<th align="right">Total size (bytes)</th>
</tr>
</thead>
<tbody>
<tr>
<td>1-of-n</td>
<td>1 (<code>OP_0</code>) + 1 (<code>OP_PUSHBYTES_72</code>) + 72 (<code>&#x3C;signature></code>)</td>
<td align="right">74</td>
<td align="right">115</td>
</tr>
<tr>
<td>2-of-n</td>
<td>1 (<code>OP_0</code>) + 2 * (1 (<code>OP_PUSHBYTES_72</code>) + 72 (<code>&#x3C;signature></code>))</td>
<td align="right">147</td>
<td align="right">188</td>
</tr>
<tr>
<td>3-of-n</td>
<td>1 (<code>OP_0</code>) + 3 * (1 (<code>OP_PUSHBYTES_72</code>) + 72 (<code>&#x3C;signature></code>))</td>
<td align="right">220</td>
<td align="right">261</td>
</tr>
</tbody>
</table>
<p>As for a serialised P2MS output, we would have something like the following in the typical case (involving compressed public keys):</p>
<ul>
<li>8 bytes: <code>amount</code></li>
<li>1 byte: <code>scriptPubKey</code> length</li>
<li>1 byte: <code>OP_m</code>, for the m in m-of-n</li>
<li>34 * n bytes: n * (1 (<code>OP_PUSHBYTES_33</code>) + 33 (<code>&#x3C;33-byte-compressed-key></code>))</li>
<li>1 byte: <code>OP_n</code>, for the n in m-of-n</li>
<li>1 byte: <code>OP_CHECKMULTISIG</code></li>
</ul>
<p>Because the dust threshold is calculated based on the total size of the output plus an assumed fixed size of 148 bytes for the input, the dust threshold only depends on n, the number of keys in the multisig configuration, and NOT on m, the number of required signatures.</p>
<table>
<thead>
<tr>
<th>Multisig configuration</th>
<th align="right">Output size (vbytes)</th>
<th align="right"><code>nSize</code> (vbytes)</th>
<th align="right">Dust Threshold (sats)</th>
</tr>
</thead>
<tbody>
<tr>
<td>m-of-1</td>
<td align="right">46</td>
<td align="right">194</td>
<td align="right">582</td>
</tr>
<tr>
<td>m-of-2</td>
<td align="right">80</td>
<td align="right">228</td>
<td align="right">684</td>
</tr>
<tr>
<td>m-of-3</td>
<td align="right">114</td>
<td align="right">262</td>
<td align="right">786</td>
</tr>
</tbody>
</table>
<p>When spending P2MS UTXOs, the minimum output value <strong>is determined by the destination output type</strong>—546 sats for P2PKH or 294 sats for P2WPKH—regardless of the P2MS configuration being spent.
This is because the dust threshold calculation uses the fixed 148-byte input size assumption.
For example, spending a P2MS UTXO to create a P2PKH output requires the P2MS UTXO to have sufficient value to cover the 546 sat minimum plus transaction fees.</p>
<p>Having established the theoretical dust thresholds, we can now examine how many P2MS outputs actually fall below these thresholds. Table 12 shows the breakdown by protocol.</p>
<table>
<thead>
<tr>
<th>Protocol</th>
<th align="right">Total</th>
<th align="right">&#x3C;294 sats</th>
<th align="right">&#x3C;546 sats</th>
<th align="right">≥546 sats</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bitcoin Stamps</td>
<td align="right">1,782,916</td>
<td align="right">0 (0%)</td>
<td align="right">0 (0%)</td>
<td align="right">1,782,916 (100%)</td>
</tr>
<tr>
<td>Counterparty</td>
<td align="right">553,981</td>
<td align="right">0 (0%)</td>
<td align="right">0 (0%)</td>
<td align="right">553,981 (100%)</td>
</tr>
<tr>
<td>Omni Layer</td>
<td align="right">43,077</td>
<td align="right">3 (0%)</td>
<td align="right">3 (0%)</td>
<td align="right">43,074 (100%)</td>
</tr>
<tr>
<td>Data Storage</td>
<td align="right">28,831</td>
<td align="right">16,847 (58%)</td>
<td align="right">16,847 (58%)</td>
<td align="right">11,984 (42%)</td>
</tr>
<tr>
<td>Chancecoin</td>
<td align="right">5,051</td>
<td align="right">0 (0%)</td>
<td align="right">0 (0%)</td>
<td align="right">5,051 (100%)</td>
</tr>
<tr>
<td>PPk</td>
<td align="right">4,728</td>
<td align="right">0 (0%)</td>
<td align="right">0 (0%)</td>
<td align="right">4,728 (100%)</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td align="right">2,152</td>
<td align="right">10 (&#x3C;1%)</td>
<td align="right">10 (&#x3C;1%)</td>
<td align="right">2,142 (99%)</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td align="right">1,352</td>
<td align="right">1 (0%)</td>
<td align="right">1 (0%)</td>
<td align="right">1,351 (100%)</td>
</tr>
<tr>
<td>ASCII Identifier Protocols</td>
<td align="right">816</td>
<td align="right">0 (0%)</td>
<td align="right">0 (0%)</td>
<td align="right">816 (100%)</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td align="right">552</td>
<td align="right">0 (0%)</td>
<td align="right">0 (0%)</td>
<td align="right">552 (100%)</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>2,423,456</strong></td>
<td align="right"><strong>16,861 (0.7%)</strong></td>
<td align="right"><strong>16,861 (0.7%)</strong></td>
<td align="right"><strong>2,406,595 (99.3%)</strong></td>
</tr>
</tbody>
</table>
<p><strong>Table 12:</strong> Dust threshold analysis of P2MS UTXOs as of block height 918,997 (14 October 2025).</p>
<p>The results reveal that 99.3% of P2MS outputs are at or above the 546 sat threshold, meaning they are not considered dust for spending purposes.
Only 16,861 outputs (0.7%) fall below the dust threshold, and notably all of these are below 294 sats (dust for all destination types).</p>
<p>The Data Storage category is the clear outlier, with 58% of its outputs (16,847) below the dust threshold.
These sub-dust outputs largely correspond to the data embedding efforts of 2013.</p>
<h3 id="fee-breakdown">Fee breakdown</h3>
<p>Beyond the value locked in P2MS outputs, users have paid substantial transaction fees to embed data using this script type. Table 13 shows the total fees paid by each protocol classification, for all transactions creating unspent P2MS outputs.</p>
<table>
<thead>
<tr>
<th>Protocol</th>
<th align="right">Fees (BTC)</th>
<th align="right">% of Total</th>
<th align="right">Avg Fee/TX (sats)</th>
<th align="right">Avg Fee/Byte (sat/byte)</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bitcoin Stamps</td>
<td align="right">218.50505161</td>
<td align="right">77.67%</td>
<td align="right">22,529</td>
<td align="right">61.06</td>
</tr>
<tr>
<td>Counterparty</td>
<td align="right">47.58227325</td>
<td align="right">16.91%</td>
<td align="right">15,669</td>
<td align="right">32.64</td>
</tr>
<tr>
<td>Omni Layer</td>
<td align="right">8.87842992</td>
<td align="right">3.16%</td>
<td align="right">21,884</td>
<td align="right">48.74</td>
</tr>
<tr>
<td>Data Storage</td>
<td align="right">5.54695739</td>
<td align="right">1.97%</td>
<td align="right">105,195</td>
<td align="right">29.17</td>
</tr>
<tr>
<td>Likely Data Storage</td>
<td align="right">0.22754496</td>
<td align="right">0.08%</td>
<td align="right">18,837</td>
<td align="right">23.80</td>
</tr>
<tr>
<td>ASCII Identifier Protocols</td>
<td align="right">0.14692917</td>
<td align="right">0.05%</td>
<td align="right">21,703</td>
<td align="right">51.78</td>
</tr>
<tr>
<td>Likely Legitimate Multisig</td>
<td align="right">0.14583943</td>
<td align="right">0.05%</td>
<td align="right">27,832</td>
<td align="right">80.91</td>
</tr>
<tr>
<td>Chancecoin</td>
<td align="right">0.11400933</td>
<td align="right">0.04%</td>
<td align="right">4,307</td>
<td align="right">10.36</td>
</tr>
<tr>
<td>OP_RETURN Signalled</td>
<td align="right">0.08389254</td>
<td align="right">0.03%</td>
<td align="right">6,251</td>
<td align="right">15.54</td>
</tr>
<tr>
<td>PPk</td>
<td align="right">0.06092428</td>
<td align="right">0.02%</td>
<td align="right">1,295</td>
<td align="right">3.75</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>281.29185188</strong></td>
<td align="right"><strong>100%</strong></td>
<td align="right"><strong>21,143</strong></td>
<td align="right"></td>
</tr>
</tbody>
</table>
<p><strong>Table 13:</strong> Fee breakdown of P2MS UTXOs by protocol classification as of block height 918,997 (14 October 2025).</p>
<p>Bitcoin Stamps dominates fee expenditure at 218.5 BTC (77.7% of all P2MS-related fees), reflecting both its transaction volume and its emergence during higher fee environments in 2023.
Figure 5 shows the weekly distribution of Bitcoin Stamps fees since the protocol's launch.</p>
<blockquote>
<p><strong>Over 281 BTC has been spent on transaction fees to embed data in P2MS outputs, with Bitcoin Stamps accounting for ~78%.</strong></p>
</blockquote>
<p>The Data Storage category shows the highest average fee per transaction at 105,195 sats due to the larger transaction sizes required for bulk data embedding (e.g., "WikiLeaks Cablegate" files, "Bitcoin Whitepaper").
Conversely, PPk transactions paid the lowest fees at just 1,295 sats average, a result of both the protocol's age (operating during lower fee periods) and its small payload sizes.</p>
<h2 id="p2ms-utxos-by-protocoluse">P2MS UTXOs by protocol/use</h2>
<p>The following sections provide deeper analysis of the major protocols, including covering the various sub-protocols or variants where applicable.
If you don't care for the details, e.g., of the various Counterparty or Omni message types, I recommend just reading the Bitcoin Stamps section immediately following, and then skip ahead to the <a href="#what-to-make-of-all-this">summary section - "What to make of all this?"</a>.</p>
<h3 id="bitcoin-stamps">Bitcoin Stamps</h3>
<p>As briefly covered in <a href="./p2ms-data-carry-1#bitcoin-stamps-sub-protocols">Part 1</a>, the primary indicator of a Bitcoin Stamp is the presence of designated "Key Burn" in the third pubkey position of a 1-of-3 multisig.
In the classification system, after Key Burn detection, data is extracted from the first two pubkeys and ARC4-decrypted using the first input's txid as the key.
The decrypted payload must contain a <code>stamp:</code> (or variant) signature to confirm validity.</p>
<p>Variant classification then proceeds in priority order: compressed data (ZLIB/GZIP) is identified first, followed by image formats (PNG, GIF, JPEG, WebP, SVG, BMP, PDF) which constitute the "Classic" variant.
JSON payloads are parsed for protocol markers such as <code>"p":"src-20"</code> ("SRC-20" - fungible tokens), <code>"p":"src-721"</code> ("SRC-721" - non-fungible tokens) and <code>"p":"src-101"</code> ("SRC-101" - naming service). HTML documents and generic binary data fall into subsequent categories.</p>
<p>The system also distinguishes between "Pure" Bitcoin Stamps (direct P2MS encoding) and those embedded within Counterparty transport, which exhibit both <code>CNTRPRTY</code> and <code>stamp:</code> signatures in the decrypted payload.</p>
<p>Table 14 shows the composition of P2MS outputs associated with Bitcoin Stamps in the UTXO set.</p>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">P2MS outputs</th>
<th align="right">Avg outputs/TX</th>
<th align="right">% of total outputs</th>
</tr>
</thead>
<tbody>
<tr>
<td>SRC-20</td>
<td align="right">933,026</td>
<td align="right">1,604,238</td>
<td align="right">1.72</td>
<td align="right">89.98%</td>
</tr>
<tr>
<td>SRC-721</td>
<td align="right">29,289</td>
<td align="right">88,550</td>
<td align="right">3.02</td>
<td align="right">4.97%</td>
</tr>
<tr>
<td>Classic (images)</td>
<td align="right">4,181</td>
<td align="right">72,243</td>
<td align="right">17.28</td>
<td align="right">4.05%</td>
</tr>
<tr>
<td>SRC-101</td>
<td align="right">2,135</td>
<td align="right">13,767</td>
<td align="right">6.45</td>
<td align="right">0.77%</td>
</tr>
<tr>
<td>Compressed</td>
<td align="right">1,059</td>
<td align="right">2,134</td>
<td align="right">2.02</td>
<td align="right">0.12%</td>
</tr>
<tr>
<td>HTML</td>
<td align="right">25</td>
<td align="right">1,035</td>
<td align="right">41.40</td>
<td align="right">0.06%</td>
</tr>
<tr>
<td>Data</td>
<td align="right">82</td>
<td align="right">820</td>
<td align="right">10.00</td>
<td align="right">0.05%</td>
</tr>
<tr>
<td>Unknown</td>
<td align="right">71</td>
<td align="right">129</td>
<td align="right">1.82</td>
<td align="right">0.01%</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>969,868</strong></td>
<td align="right"><strong>1,782,916</strong></td>
<td align="right"><strong>1.84</strong></td>
<td align="right"><strong>100.00%</strong></td>
</tr>
</tbody>
</table>
<p><strong>Table 14:</strong> Bitcoin Stamps sub-protocol composition as of block height 918,997 (14 October 2025).</p>
<p>"SRC-20" tokens dominate at ~90% of Bitcoin Stamps P2MS outputs, reflecting the protocol's primary use for "fungible token operations". "SRC-20" is a JSON-only protocol, so all "SRC-20" P2MS outputs contain JSON data like the following (as decoded in <a href="./p2ms-data-carry-1#data-carrying-in-p2ms-a-bitcoin-stamps-example">Part 1</a>):</p>
<pre><code class="hljs language-json">stamp<span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
	<span class="hljs-attr">"p"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"src-20"</span><span class="hljs-punctuation">,</span>
	<span class="hljs-attr">"op"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"transfer"</span><span class="hljs-punctuation">,</span>
	<span class="hljs-attr">"tick"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"BMWK"</span><span class="hljs-punctuation">,</span>
	<span class="hljs-attr">"amt"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"1000"</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>"SRC-721", accounting for ~5% of Bitcoin Stamps P2MS outputs, is also JSON-only, as is "SRC-101" (~0.8% of outputs).
With "SRC-20", "SRC-721", and "SRC-101" all being JSON-based, the vast majority of Bitcoin Stamps P2MS outputs contain JSON data, explaining why <code>application/json</code> dominates the overall content type distribution at 72.64% (as covered in the <a href="#data-content-type-breakdown">content type breakdown</a>).</p>
<p>"Classic Stamps", the original Bitcoin Stamps protocol that encodes images directly into P2MS outputs, accounts for ~4% of Bitcoin Stamps P2MS outputs.
Approximately 4,200 images, predominantly PNGs and GIFs have been embedded using "Classic Stamps", though there are many more images stored on-chain by Bitcoin Stamps using other techniques and script types such as OLGA and P2TR.</p>
<p><strong>"Classic Stamps"</strong>:</p>
<ul>
<li>PNG images: 3,342 (80%)</li>
<li>GIF animations: 603 (14%)</li>
<li>SVG graphics: 130 (3%)</li>
<li>JPEG images: 60 (1%)</li>
<li>Other formats: 44 (1%)</li>
</ul>
<p>Bitcoin Stamps utilises two distinct transport mechanisms, as summarised in Table 15.
Counterparty was the original transport layer for Bitcoin Stamps, but given the significant overhead, a native Bitcoin Stamps transport mechanism was later developed and introduced.
The overheads of Counterparty were explored in <a href="./p2ms-data-carry-1#summarising-the-main-techniques">Part 1</a>.</p>
<table>
<thead>
<tr>
<th>Transport Method</th>
<th align="right">Transactions</th>
<th align="right">Percentage</th>
</tr>
</thead>
<tbody>
<tr>
<td>Counterparty</td>
<td align="right">78,263</td>
<td align="right">8.1%</td>
</tr>
<tr>
<td>"Pure" Bitcoin Stamps</td>
<td align="right">891,605</td>
<td align="right">91.9%</td>
</tr>
</tbody>
</table>
<p><strong>Table 15:</strong> Bitcoin Stamps transport mechanism breakdown.</p>
<p>Despite the relatively low value locked in outputs (14.19 BTC), Bitcoin Stamps users have demonstrated their willingness to pay for the "permanence guarantee".
For example, with the protocol having emerged during the 2023 Ordinals/Inscriptions hype, the fact that
people chose to use Bitcoin Stamps over Ordinals/Inscriptions can perhaps be seen as a preference for permanence over lower cost.</p>
<p>Although the average value per Bitcoin Stamps P2MS output is just 796 sats (Table 16), with the average size of a Classic Stamp being 17.28 outputs per transaction (Table 14), the average cost per Classic Stamp transaction is ~13,760 sats just for the outputs alone, plus transaction fees.
On the matter of fees, Bitcoin Stamps users have paid approximately 218.50 BTC in transaction fees to embed data using P2MS outputs since the protocol's inception.</p>
<table>
<thead>
<tr>
<th>Metric</th>
<th align="right">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Total BTC in P2MS outputs</td>
<td align="right">~14.19 BTC</td>
</tr>
<tr>
<td>Average value per output</td>
<td align="right">796 sats</td>
</tr>
<tr>
<td>Minimum value</td>
<td align="right">546 sats (dust limit)</td>
</tr>
<tr>
<td>Maximum value</td>
<td align="right">7,800 sats</td>
</tr>
<tr>
<td>Total fees paid</td>
<td align="right">~218.50 BTC</td>
</tr>
</tbody>
</table>
<p><strong>Table 16:</strong> Economic metrics for Bitcoin Stamps P2MS outputs.</p>
<p><strong>[Interactive plot - view on website]</strong></p>
<p>Bitcoin Stamps weekly fee expenditure from protocol inception (April 2023) through October 2025. The average sats/vbyte trace (green) is available via the legend.</p>
<p>Figure 5 shows the weekly distribution of these fees over time.
The chart displays total weekly fees (bars) alongside average fee per transaction (blue line) on a secondary axis.</p>
<p>This data reveals:</p>
<ul>
<li><strong>Peak activity in late 2023</strong>: Weekly fees peaked at over 51 BTC during the week of 14 December 2023, coinciding with broader network congestion and high demand for block space (see figure).
During this period, average fees per transaction reached approximately 167,500 sats (~US$70 at the time!).</li>
<li><strong>Majority of fees paid over a handful of weeks</strong>: ~66% of all Bitcoin Stamps fees were paid during just 10 weeks between November 2023 and February 2024, so simply considering the total fees paid can be misleading without temporal context.</li>
</ul>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/mempool-graph-3y-1764302242.svg" alt="Figure: Bitcoin mempool size (in MvB) from November 2022 to November 2025, showing periods of elevated congestion. From mempool.space"></p>
<p>The "Stamp" in Bitcoin Stamps is (presumably) a backronym: Secure Tradeable Artifacts Maintained Permanently.
And the original motivation was <a href="https://github.com/mikeinspace/stamps/blob/main/BitcoinStamps.md"><em>"Storing 'Art on the Blockchain' as a method of achieving permanence"</em></a>.
Yet we've seen that the dominant use case for Bitcoin Stamps is fungible token operations ("SRC-20"), which arguably diverges from the original intent of "art".</p>
<p>Figure 6 explores how the distribution of Bitcoin Stamps variants (those utilising P2MS) has evolved over time.
It's clear that the "art" use case ("Classic Stamps") has not been seen since March 2024, with only "SRC-20" and "SRC-101" seeing use in 2025.
This is important context if one were to consider if the continued use of P2MS outputs by Bitcoin Stamps is justified, especially given their deliberately unspendable design.
This is discussed further in the <a href="#what-to-make-of-all-this">summary section</a>.</p>
<p><strong>[Interactive plot - view on website]</strong></p>
<p>Weekly distribution of Bitcoin Stamps variants by output count.</p>
<h3 id="counterparty">Counterparty</h3>
<p>Counterparty is the second largest contributor to P2MS outputs in the UTXO set, accounting for ~23% of all such outputs.
Unlike Bitcoin Stamps' deliberately unspendable outputs, every Counterparty P2MS output contains, in theory, at least one valid public key, ensuring spendability and avoiding permanent UTXO set inclusion.
~35.86 BTC is currently locked in Counterparty P2MS outputs.</p>
<p>The 8-byte <code>CNTRPRTY</code> prefix identifies Counterparty messages after successful ARC4 decryption (again, the full process has been explored in <a href="./p2ms-data-carry-1#counterparty">Part 1</a>).
Although there are 20+ different Counterparty protocol message types, they have been consolidated into 7 semantically meaningful high-level variants for the purposes of analysis:</p>
<ol>
<li><strong>Counterparty Issuance</strong> - Issuance, Fair Minter, Fair Mint</li>
<li><strong>Counterparty Transfer</strong> - Send, Enhanced Send, Multi-Party, Multi-Asset (MPMA), Sweep, Dividend</li>
<li><strong>Counterparty DEX</strong> - Decentralised Exchange: Order, BTC Pay, Dispenser, Cancel</li>
<li><strong>Counterparty Oracle</strong> - Broadcast</li>
<li><strong>Counterparty Gaming</strong> - Bet, Rock-Paper-Scissors (RPS), RPS Resolve</li>
<li><strong>Counterparty Utility</strong> - UTXO, Attach, Detach</li>
<li><strong>Counterparty Destruction</strong> - Asset destruction: Destroy, Burn</li>
</ol>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">P2MS Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Counterparty Transfer</td>
<td align="right">188,423</td>
<td align="right">195,174</td>
<td>Token transfers between addresses</td>
</tr>
<tr>
<td>Counterparty Issuance</td>
<td align="right">72,835</td>
<td align="right">283,160</td>
<td>Creating new tokens/assets</td>
</tr>
<tr>
<td>Counterparty DEX</td>
<td align="right">33,298</td>
<td align="right">52,542</td>
<td>Decentralised exchange operations</td>
</tr>
<tr>
<td>Counterparty Oracle</td>
<td align="right">7,811</td>
<td align="right">20,293</td>
<td>Price feeds and data broadcasts</td>
</tr>
<tr>
<td>Counterparty Gaming</td>
<td align="right">1,242</td>
<td align="right">2,519</td>
<td>Betting and game-related operations</td>
</tr>
<tr>
<td>Counterparty Utility</td>
<td align="right">60</td>
<td align="right">263</td>
<td>UTXO management (attach/detach)</td>
</tr>
<tr>
<td>Counterparty Destruction</td>
<td align="right">8</td>
<td align="right">30</td>
<td>Burning/destroying tokens</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>303,677</strong></td>
<td align="right"><strong>553,981</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 17:</strong> Breakdown of Counterparty variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p>Counterparty uses two primary multisig configurations:</p>
<ul>
<li>1-of-3: 72.10% of outputs</li>
<li>1-of-2: 27.79% of outputs</li>
</ul>
<p>The remaining configurations (2-of-2, 2-of-3, 3-of-3) account for 0.11% of all Counterparty P2MS UTXOs.</p>
<h3 id="omni">Omni</h3>
<p>Omni is a distant third in terms of P2MS UTXO contribution, accounting for ~1.8% of all such outputs.
Similar to Counterparty, every Omni P2MS output contains at least one valid public key, ensuring spendability and avoiding permanent UTXO set inclusion.
All 43,077 Omni P2MS outputs are spendable and ~2.32 BTC is currently locked in Omni P2MS outputs.</p>
<p>Omni transactions are identified by the presence of the Exodus address (<a href="https://mempool.space/address/1EXoDusjGwvnjZUyKkxZ4UHEf77z6A5S4P"><code>1EXoDusj...</code></a>) as a transaction output.
Once the Exodus address is confirmed, the Omni payload is extracted from the P2MS outputs using the process described in <a href="./p2ms-data-carry-1#omni-formerly-mastercoin">Part 1</a>.
If deobfuscation is not successful, the transaction and P2MS outputs are marked as "Omni Failed Deobfuscation", otherwise the message type is parsed and classified accordingly.</p>
<p>Like Counterparty, Omni has 20+ message types, which can be consolidated into a smaller number of high-level variants for analysis:</p>
<ol>
<li><strong>Omni Transfer</strong> - SimpleSend, RestrictedSend, SendAll, SendNonFungible (types 0, 2, 4, 5)</li>
<li><strong>Omni Issuance</strong> - CreatePropertyFixed, CreatePropertyVariable, PromoteProperty, CreatePropertyManual, GrantPropertyTokens (types 50, 51, 52, 54, 55)</li>
<li><strong>Omni DEX</strong> - TradeOffer, AcceptOfferBTC, MetaDEXTrade, MetaDEXCancelPrice, MetaDEXCancelPair, MetaDEXCancelEcosystem (types 20, 22, 25-28)</li>
<li><strong>Omni Failed Deobfuscation</strong> - Exodus address present but deobfuscation failed</li>
<li><strong>Omni Destruction</strong> - RevokePropertyTokens (type 56)</li>
<li><strong>Omni Administration</strong> - CloseCrowdsale, ChangeIssuerAddress, EnableFreezing, DisableFreezing, FreezePropertyTokens, UnfreezePropertyTokens (types 53, 70, 71, 72, 185, 186)</li>
<li><strong>Omni Distribution</strong> - SendToOwners (type 3)</li>
</ol>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Omni Transfer</td>
<td align="right">37,421</td>
<td align="right">37,432</td>
<td>Token transfers between addresses</td>
</tr>
<tr>
<td>Omni Issuance</td>
<td align="right">1,198</td>
<td align="right">3,689</td>
<td>Creating new tokens/assets</td>
</tr>
<tr>
<td>Omni DEX</td>
<td align="right">1,857</td>
<td align="right">1,857</td>
<td>Decentralised exchange operations</td>
</tr>
<tr>
<td>Omni Failed Deobfuscation</td>
<td align="right">52</td>
<td align="right">56</td>
<td>Transactions that couldn't be decoded</td>
</tr>
<tr>
<td>Omni Destruction</td>
<td align="right">24</td>
<td align="right">24</td>
<td>Burning/destroying tokens</td>
</tr>
<tr>
<td>Omni Administration</td>
<td align="right">17</td>
<td align="right">17</td>
<td>Token management operations</td>
</tr>
<tr>
<td>Omni Distribution</td>
<td align="right">2</td>
<td align="right">2</td>
<td>Dividend/airdrop distributions</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>40,571</strong></td>
<td align="right"><strong>43,077</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 18:</strong> Breakdown of Omni variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p>Omni primarily uses the 1-of-2 multisig configuration, accounting for 90.16% of outputs, with the remaining 9.83% using 1-of-3 (though there are 2 Omni UTXOs that use the odd 1-of-1 configuration).</p>
<h3 id="chancecoin">Chancecoin</h3>
<p>Chancecoin was a gambling-focused protocol that operated on Bitcoin from 2014-2015, using P2MS outputs for its message encoding.
Like Counterparty and Omni, Chancecoin maintains at least one valid public key per output, ensuring spendability.
~0.15 BTC remains locked in Chancecoin P2MS outputs, all of which appears spendable.</p>
<p>Chancecoin message types can be consolidated into the following variants:</p>
<ol>
<li><strong>Chancecoin Roll</strong> - Dice roll results (ID 14)</li>
<li><strong>Chancecoin Bet</strong> - Gambling bets (ID 40/41)</li>
<li><strong>Chancecoin Send</strong> - Token transfers (ID 0)</li>
<li><strong>Chancecoin Order</strong> - DEX order placement (ID 10)</li>
<li><strong>Chancecoin Cancel</strong> - Order cancellation (ID 70)</li>
<li><strong>Chancecoin BTCPay</strong> - BTC payment for DEX trades (ID 11)</li>
</ol>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Chancecoin Roll</td>
<td align="right">1,912</td>
<td align="right">3,824</td>
<td>Dice roll results</td>
</tr>
<tr>
<td>Chancecoin Bet</td>
<td align="right">368</td>
<td align="right">736</td>
<td>Gambling bets placed</td>
</tr>
<tr>
<td>Chancecoin Send</td>
<td align="right">252</td>
<td align="right">252</td>
<td>Token transfers between addresses</td>
</tr>
<tr>
<td>Chancecoin Order</td>
<td align="right">65</td>
<td align="right">130</td>
<td>DEX order placement</td>
</tr>
<tr>
<td>Chancecoin Cancel</td>
<td align="right">41</td>
<td align="right">82</td>
<td>Order cancellation</td>
</tr>
<tr>
<td>Chancecoin BTCPay</td>
<td align="right">9</td>
<td align="right">27</td>
<td>BTC payment for DEX trades</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>2,647</strong></td>
<td align="right"><strong>5,051</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 19:</strong> Breakdown of Chancecoin variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p>Chancecoin exclusively uses the 1-of-2 multisig configuration (100% of outputs).</p>
<h3 id="ppk">PPk</h3>
<p>PPk was a decentralised identity and naming protocol that operated on Bitcoin, using P2MS and <code>OP_RETURN</code> outputs for message encoding.
PPk aimed to provide a decentralised alternative to DNS and digital identity systems.</p>
<p>PPk transactions are identified by a distinctive marker pubkey (<code>0320a0de...3e12</code>) that must appear in the second position of a P2MS output.
Once detected, variant classification examines the combined P2MS and <code>OP_RETURN</code> data:</p>
<ul>
<li><strong>PPk Profile</strong>: transactions carry JSON payloads using a "RT" (Resource Tag) and a type–length–value (TLV) structure</li>
<li><strong>PPk Registration</strong>: transactions contain quoted numeric strings like "315"</li>
<li><strong>PPk Message</strong>: transactions contain promotional text with "PPk" substrings or ≥80% printable ASCII</li>
<li><strong>PPk Unknown</strong>: Unrecognised PPk message types that don't fit the above patterns.</li>
</ul>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>PPk Message</td>
<td align="right">2,031</td>
<td align="right">2,051</td>
<td>General protocol messages</td>
</tr>
<tr>
<td>PPk Profile</td>
<td align="right">2,003</td>
<td align="right">2,003</td>
<td>Identity/profile data storage</td>
</tr>
<tr>
<td>PPk Unknown</td>
<td align="right">478</td>
<td align="right">480</td>
<td>Unrecognised PPk message types</td>
</tr>
<tr>
<td>PPk Registration</td>
<td align="right">194</td>
<td align="right">194</td>
<td>Name/identity registration</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>4,706</strong></td>
<td align="right"><strong>4,728</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 20:</strong> Breakdown of PPk variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p>PPk uses two multisig configurations:</p>
<ul>
<li>1-of-3: 70.43% of outputs</li>
<li>1-of-2: 29.57% of outputs</li>
</ul>
<p>All 4,728 PPk P2MS outputs appear spendable and only a small amount of value, ~0.05 BTC, is locked in them.</p>
<h3 id="op_return-signalled">OP_RETURN Signalled</h3>
<p>The OP_RETURN Signalled classification captures transactions where an <code>OP_RETURN</code> output contains a protocol identifier, but the transaction also includes P2MS outputs.
Often the <code>OP_RETURN</code> serves as a protocol identifier, with P2MS outputs providing additional data capacity for the protocol's payload.</p>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Protocol47930</td>
<td align="right">742</td>
<td align="right">742</td>
<td>Unknown protocol with identifier <code>47930</code></td>
</tr>
<tr>
<td>Generic ASCII</td>
<td align="right">362</td>
<td align="right">372</td>
<td>ASCII text identifiers without known protocol mapping</td>
</tr>
<tr>
<td><code>CLIPPERZ</code></td>
<td align="right">238</td>
<td align="right">238</td>
<td>Clipperz password manager backup protocol</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>1,342</strong></td>
<td align="right"><strong>1,352</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 21:</strong> Breakdown of OP_RETURN Signalled variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p>The largest category is "Protocol47930" (54.9%), representing transactions featuring the <code>0xbb3a</code> marker (47930 in decimal).
"Generic ASCII" (27.5%) captures various ASCII-based identifiers that don't match known protocols, examples include <code>CC</code> (138 outputs), <code>8EC=</code> (104 outputs) and <code>DEVCHA</code> (30 outputs).
"CLIPPERZ" (17.6%) corresponds to the <a href="https://clipperz.is/">Clipperz</a> open-source password manager, which at some point in the past seems to have used Bitcoin as a backup storage feature.</p>
<p>Unlike dedicated data-carrying protocols, OP_RETURN Signalled transactions show an odd mix of multisig configurations:</p>
<ul>
<li>2-of-2: 84.1% of outputs</li>
<li>1-of-2: 10.8% of outputs</li>
<li>1-of-3: 4.6% of outputs</li>
</ul>
<p>The prevalence of 2-of-2 configurations (rather than the 1-of-n patterns typical of data-carrying protocols) is notable, since these require multiple valid signatures to spend.
Analysis of EC-point validity shows that 92.53% of these OP_RETURN Signalled P2MS outputs are actually spendable, with ~0.11 BTC currently locked across all OP_RETURN Signalled P2MS outputs.</p>
<h3 id="ascii-identifier-protocols">ASCII Identifier Protocols</h3>
<p>The ASCII Identifier Protocols classification captures P2MS transactions where the embedded data begins with a recognisable ASCII string identifier.
That is, the ASCII string identifier is in the P2MS data (as opposed to OP_RETURN Signalled where the identifier is in the <code>OP_RETURN</code> output).
These represent various experimental or short-lived protocols that used P2MS for data storage.</p>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">P2MS Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>TB0001</code></td>
<td align="right">323</td>
<td align="right">342</td>
<td>Unknown protocol with <code>TB0001</code> identifier</td>
</tr>
<tr>
<td><code>METROXMN</code></td>
<td align="right">158</td>
<td align="right">181</td>
<td>Associated with Metronotes XMN</td>
</tr>
<tr>
<td><code>TEST01</code></td>
<td align="right">175</td>
<td align="right">179</td>
<td>Transactions with <code>TEST01</code> marker</td>
</tr>
<tr>
<td>Other ASCII Protocol</td>
<td align="right">21</td>
<td align="right">114</td>
<td>Various other ASCII-prefixed data</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>677</strong></td>
<td align="right"><strong>816</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 22:</strong> Breakdown of ASCII identifier protocol variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p><code>TB0001</code> (41.9%) is the most common variant, though the protocol's purpose remains unidentified. <code>METROXMN</code> (22.2%) is associated with <a href="https://bitcointalk.org/index.php?topic=974486.0">Metronotes XMN</a>, which appears to be a scam. <code>TEST01</code> (21.9%) likely represents testing activity during protocol development or experimentation.
The "Other ASCII Protocol" category (14.0%) is almost entirely <code>NEWBCOIN</code> (113 of 114 outputs), an unknown protocol from late 2014.
Approximately half of the <code>NEWBCOIN</code> transactions (11 of 20) embed gzip-compressed data across multiple P2MS outputs per transaction (9–10 outputs each), while the remainder are single-output transactions without compression.
The single non-<code>NEWBCOIN</code> output is <code>PRVCY</code> from March 2015.</p>
<p>ASCII Identifier Protocols use two multisig configurations:</p>
<ul>
<li>1-of-2: 63.24% of outputs</li>
<li>1-of-3: 36.76% of outputs</li>
</ul>
<p>All 816 ASCII Identifier Protocols P2MS outputs are spendable.</p>
<h3 id="data-storage">Data Storage</h3>
<p>The Data Storage classification captures P2MS transactions where embedded data does not match any known protocol identifier or pattern.
These represent direct data embedding without protocol structure or ASCII identifiers.</p>
<p>The classifier searches for known file signatures (PNG, JPEG, GIF, PDF, ZIP, RAR, GZIP, ZLIB, TAR, and others) via magic byte detection, as well as text content analysis.
However, in practice, the vast majority of data storage outputs contain generic binary or text data without recognisable file signatures; the only file format detected with any frequency is ZLIB-compressed data (90 outputs).
This suggests that early data embedders typically stored raw text, compressed archives, or custom binary formats rather than standard file types like images or PDFs.</p>
<p>Additionally, the classifier identifies proof-of-burn patterns (such as all <code>0xFF</code> or <code>0x00</code> byte pubkeys, or other unspendable patterns) and file metadata patterns (URLs, filenames, archive extensions).</p>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>WikiLeaks Cablegate</td>
<td align="right">134</td>
<td align="right">13,362</td>
<td>Diplomatic cables from the WikiLeaks release</td>
</tr>
<tr>
<td>Embedded Data</td>
<td align="right">1,130</td>
<td align="right">10,513</td>
<td>Generic embedded data</td>
</tr>
<tr>
<td>Proof of Burn</td>
<td align="right">4,004</td>
<td align="right">4,004</td>
<td>Outputs used for proof-of-burn mechanisms</td>
</tr>
<tr>
<td>Bitcoin Whitepaper</td>
<td align="right">1</td>
<td align="right">946</td>
<td>Satoshi's Bitcoin whitepaper PDF</td>
</tr>
<tr>
<td>File Metadata</td>
<td align="right">3</td>
<td align="right">5</td>
<td>File metadata or headers embedded in outputs</td>
</tr>
<tr>
<td>Null Data</td>
<td align="right">1</td>
<td align="right">1</td>
<td>Outputs containing null/empty data patterns</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>5,273</strong></td>
<td align="right"><strong>28,831</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 23:</strong> Breakdown of Data Storage variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p>The "WikiLeaks Cablegate" files dominate this category at 46.3%, representing diplomatic cables embedded across thousands of P2MS outputs.
As explored in <a href="./p2ms-data-carry-1#generic-data-storage">Part 1</a>, this is one of the most famous examples of data storage in P2MS, alongside the Bitcoin whitepaper PDF, which itself was also embedded in April 2013 across 946 P2MS outputs (3.3%).
"Embedded Data" (36.5%) captures various other data embedding efforts.
"Proof of Burn" (13.9%) represents outputs created specifically to demonstrate destruction of bitcoin value.</p>
<p>Data Storage shows diverse multisig configurations, reflecting its ad-hoc nature:</p>
<ul>
<li>1-of-3: 80.84% of outputs</li>
<li>1-of-2: 15.92% of outputs</li>
<li>2-of-2: 1.45% of outputs</li>
<li>1-of-1: 1.28% of outputs</li>
<li>2-of-3: 0.48% of outputs</li>
<li>3-of-3: 0.04% of outputs</li>
</ul>
<p>Only 33.29% of Data Storage P2MS outputs are spendable, with the majority (66.71%) being unspendable due to all keys being used for data carriage or proof-of-burn patterns. ~10.18 BTC is currently locked in Data Storage P2MS outputs.</p>
<h3 id="likely-data-storage">Likely Data Storage</h3>
<p>The Likely Data Storage classification captures P2MS transactions that exhibit characteristics suggesting data storage but lack definitive protocol identifiers or clear data patterns.
Unlike Data Storage (which has confirmed data patterns), Likely Data Storage represents a heuristic-based classification where characteristics such as dust-level values, high output counts, or invalid public keys indicate probable data carriage.</p>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Dust Amount</td>
<td align="right">1,133</td>
<td align="right">1,367</td>
<td>Outputs with dust-level values suggesting data storage</td>
</tr>
<tr>
<td>High Output Count</td>
<td align="right">67</td>
<td align="right">776</td>
<td>Transactions with many P2MS outputs suggesting bulk data embedding</td>
</tr>
<tr>
<td>Invalid EC Point</td>
<td align="right">8</td>
<td align="right">9</td>
<td>Outputs containing keys that are not valid EC points</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>1,208</strong></td>
<td align="right"><strong>2,152</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 24:</strong> Breakdown of Likely Data Storage variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p>The dominant variant is "Dust Amount" (63.52%), capturing outputs where the encumbered value is less than 1000 sats.
"High Output Count" (36.06%) identifies transactions that feature 5+ P2MS outputs, a pattern perhaps indicative of bulk data embedding rather than normal multisig usage.
"Invalid EC Point" (0.42%) represents outputs where at least one pubkey is provably not a valid point on the secp256k1 curve, again indicative of data carrying rather than legitimate multisig.</p>
<p>The Likely Data Storage has the following multisig configuration profile:</p>
<ul>
<li>2-of-2: 39.68% of outputs</li>
<li>1-of-1: 32.39% of outputs</li>
<li>1-of-3: 13.89% of outputs</li>
<li>2-of-3: 13.52% of outputs</li>
<li>1-of-2: 0.51% of outputs</li>
</ul>
<p>This distribution is notably different from both Data Storage (e.g., 80.84% 1-of-3) and established data-carrying protocols.
Specifically, 2-of-2 at 39.68% is more commonly associated with legitimate multisig arrangements as opposed to data carriage.</p>
<p>99.86% of Likely Data Storage P2MS outputs are spendable, with only 3 outputs being unspendable.
~0.03 BTC is currently locked in Likely Data Storage outputs.</p>
<h3 id="likely-legitimate-multisig">Likely Legitimate Multisig</h3>
<p>The Likely Legitimate Multisig classification represents P2MS outputs that appear to be genuine multisig arrangements for securing funds rather than data carriage.
These outputs exhibit characteristics consistent with legitimate multisig usage: valid public keys, reasonable value amounts, and multisig configurations that make practical sense for custody arrangements (valid EC point keys).</p>
<table>
<thead>
<tr>
<th>Variant</th>
<th align="right">Transactions</th>
<th align="right">Outputs</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Legitimate Multisig</td>
<td align="right">512</td>
<td align="right">540</td>
<td>Standard multisig outputs with valid keys and reasonable values</td>
</tr>
<tr>
<td>Legitimate Multisig (Null-Padded)</td>
<td align="right">7</td>
<td align="right">7</td>
<td>Multisig outputs with null-padded public keys</td>
</tr>
<tr>
<td>Legitimate Multisig (Duplicate Keys)</td>
<td align="right">5</td>
<td align="right">5</td>
<td>Multisig outputs containing duplicate public keys</td>
</tr>
<tr>
<td><strong>Total</strong></td>
<td align="right"><strong>524</strong></td>
<td align="right"><strong>552</strong></td>
<td></td>
</tr>
</tbody>
</table>
<p><strong>Table 25:</strong> Breakdown of Likely Legitimate Multisig variants observed in P2MS UTXOs, as of block height 918,997 (14 October 2025).</p>
<p>The vast majority (97.8%) are standard legitimate multisig outputs.
The "Null-Padded" variant (1.3%) represents outputs where public keys contain null padding, potentially indicating older wallet software or non-standard key generation.
"Duplicate Keys" (0.9%) captures the unusual case where the same public key appears multiple times in a multisig script, which while technically valid, suggests implementation bugs.</p>
<p>Likely Legitimate Multisig shows the most diverse multisig configuration profile of any classification:</p>
<ul>
<li>2-of-2: 41.30% of outputs</li>
<li>1-of-2: 25.00% of outputs</li>
<li>2-of-3: 12.68% of outputs</li>
<li>1-of-3: 11.41% of outputs</li>
<li>1-of-1: 9.60% of outputs</li>
</ul>
<p>All 552 Likely Legitimate Multisig P2MS outputs are spendable (100%), as would be expected.
The total value locked in these outputs is approximately 6.50 BTC which accounts for ~9.4% of the total value encumbered in P2MS outputs.
This gives an average value of ~0.12 BTC (e.g., 1.2 million sats) per output, which is orders of magnitude higher than Bitcoin Stamps (796 sats) or other data-carrying protocols, clearly a reflection of their use to secure holdings rather than store data.</p>
<p>The fact that only 552 outputs (0.02% of all P2MS UTXOs) appear to represent legitimate multisig usage underscores how completely P2MS has been co-opted for data carriage purposes.</p>
<h2 id="what-to-make-of-all-this">What to make of all this?</h2>
<p>The objective of this analysis has been to present comprehensive data on P2MS usage as observed from P2MS outputs in the UTXO set.
The findings are stark: over 99.98% of P2MS UTXOs serve data embedding protocols, 74.37% are provably unspendable, and just 0.02% (552 of 2.4M outputs) appear to represent legitimate multisig usage.
This represents a fundamental departure from the script type's intended purpose of enabling multisig custody arrangements and is largely driven by one protocol: Bitcoin Stamps.</p>
<h3 id="p2ms-usage-over-time">P2MS usage over time</h3>
<p>Though the content here is centred on P2MS UTXOs, it would be remiss to not consider the totality of P2MS usage, including P2MS outputs that have already been spent.
Figure 7 shows the cumulative number of P2MS outputs created (purple line) versus the cumulative number of P2MS inputs spent (blue line) over time.</p>
<p>Of the ~2.71M P2MS outputs created through to 14 October 2025, approximately 288K have ever been spent as inputs, yielding a spend rate of just 10.6%.
Only ~8% of the spent P2MS outputs were spent in the last ~6 years, with the remaining ~92% having been spent in the ~8 years prior (2012–2020).
Since Bitcoin Stamps launched, the creation of P2MS outputs has vastly outpaced their spending, leading to a growing accumulation of P2MS outputs in the UTXO set.</p>
<p><strong>[Interactive plot - view on website]</strong></p>
<p>Cumulative P2MS inputs (spent) and outputs (created) over time. The dramatic divergence since Bitcoin Stamps launched in early 2023 illustrates that P2MS outputs are being created far faster than they are being spent. Data sourced from <a href="https://mainnet.observer">mainnet.observer</a>.</p>
<h3 id="bitcoin-stamps-permanence">Bitcoin Stamps permanence</h3>
<p>When Bitcoin Stamps launched in early 2023, its creators justified using P2MS on the grounds of permanence and "art": art encoded in deliberately unspendable outputs would remain in the UTXO set indefinitely.
This rationale was controversial, with objection to deliberate UTXO set pollution, but the protocol forged ahead regardless.</p>
<p>It is true that different data carriage techniques offer different persistence guarantees.
Witness data, used by Ordinals/Inscriptions, is not part of the UTXO set and can be pruned by full nodes running in pruned mode.
<code>OP_RETURN</code> outputs are provably unspendable and never enter the UTXO set.
P2MS outputs, however, must be retained by all full nodes because they <em>might</em> be spendable - a node cannot know whether a given public key has a corresponding private key.</p>
<p>Bitcoin Stamps exploits this property by deliberately creating P2MS outputs that are unspendable but appear potentially spendable to the network.
By using invalid Key Burn keys alongside data-carrying keys, Bitcoin Stamps ensures its data remains in the UTXO set of every full node indefinitely.
Each unspendable output adds to the UTXO set size that every full node (archival and pruned) must maintain in fast-access memory.
This is a cost imposed on the entire network.</p>
<p>However, the "permanence guarantee" offered by UTXO set storage is overstated.
While pruned nodes discard block data (including <code>OP_RETURN</code> outputs), over 90% of Bitcoin full nodes (<a href="https://bitnodes.io/nodes/">as of late 2025</a>) run in archival mode, retaining the complete blockchain history.
Data embedded via <code>OP_RETURN</code> is therefore preserved across the vast majority of the network.
Forcing data into the UTXO set, rather than block storage, comes at disproportionate cost to the network with no practical permanence benefit.</p>
<h3 id="bitcoin-core-v30-and-op_return-unbounding">Bitcoin Core v30 and <code>OP_RETURN</code> unbounding</h3>
<p>Bitcoin Core v30.0, released in October 2025, <a href="https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-30.0.md#updated-settings">removed the standardness restrictions</a> that previously limited <code>OP_RETURN</code> outputs to 80 bytes of data.
Transactions with <code>OP_RETURN</code> outputs of any size (up to the transaction size limit) are now relayed and mined by default.
This fundamentally changes the data carriage landscape on Bitcoin.</p>
<h3 id="bitcoin-stamps-current-usage-doesnt-justify-p2ms">Bitcoin Stamps' current usage doesn't justify P2MS</h3>
<p>The data shows that Bitcoin Stamps' use case has evolved significantly from its original "art" focus.
As shown in Figure 6, no P2MS "Classic Stamps" (images) have been created since March 2024, and only the JSON-based sub-protocols of Bitcoin Stamps (SRC-20, SRC-101) have leveraged P2MS in 2025.</p>
<p>Bitcoin Stamps' current P2MS usage is entirely for simple JSON payloads, not art.
These JSON payloads do not inherently require the permanence guarantee that P2MS provides.
They could function identically using <code>OP_RETURN</code> outputs, which:</p>
<ul>
<li>Do not pollute the UTXO set (they are provably unspendable and pruned from the UTXO set)</li>
<li>Are now limited in size only by the transaction size limit following the release of Bitcoin Core v30.0</li>
<li>Are the standard, intended mechanism for data carriage on Bitcoin</li>
</ul>
<p>It's worth noting that Bitcoin Stamps already uses multiple data carriage techniques beyond P2MS.
The continued use of P2MS specifically for JSON payloads, when <code>OP_RETURN</code> is now a viable alternative, is difficult to justify.</p>
<h3 id="the-case-for-deprecating-p2ms">The case for deprecating P2MS</h3>
<p>Given the data presented in this analysis, there is a reasonable case for deprecating the creation of new P2MS outputs. That is, introducing a soft fork to make the creation of new P2MS outputs invalid by consensus.</p>
<p>The arguments in favour of deprecation include:</p>
<ol>
<li>
<p><strong>P2MS is not used for its intended purpose.</strong>
The data is unambiguous: 99.98% of P2MS UTXOs serve data embedding protocols, not multisig custody.
Legitimate multisig users migrated to P2SH and P2WSH years ago, which offer better privacy, lower fees, and broader wallet support.
Modern wallets largely do not support P2MS at all; as Bitcoin Core maintainer Ava Chow <a href="https://github.com/bitcoin/bitcoin/pull/28217#issuecomment-1666620826">noted in August 2023</a>: <em>"Bare multisigs are generally unusable to the vast majority of wallet software, if not all of them. They do not have an address type so the vast majority of users are completely unable to send to them."</em></p>
</li>
<li>
<p><strong>The primary user no longer needs P2MS.</strong>
As detailed above, Bitcoin Stamps' current usage is entirely JSON-based sub-protocols that don't require UTXO set permanence.
With <code>OP_RETURN</code> now effectively unbounded, there is no reason for Bitcoin Stamps to continue using P2MS.</p>
</li>
<li>
<p><strong>Reduced maintenance burden.</strong>
Deprecating P2MS would allow Bitcoin Core developers to remove support for a script type that clearly no longer serves its original multisig custody purpose, simplifying the codebase and reducing maintenance overhead.</p>
</li>
<li>
<p><strong>Network resource preservation.</strong>
Preventing new unspendable P2MS outputs would halt the ongoing UTXO set growth from data carriage protocols that now have viable alternatives.</p>
</li>
</ol>
<p>The arguments against deprecation are primarily procedural rather than technical:</p>
<ul>
<li>
<p><strong>Bitcoin's conservatism regarding consensus changes.</strong> Any change that invalidates previously valid transactions requires careful consideration, even if the affected use cases are not the intended purpose.</p>
</li>
<li>
<p><strong>Precedent concerns.</strong> Some argue that restricting how people use Bitcoin, even for purposes such as data carrying via deliberately unspendable transaction outputs, sets a problematic precedent.</p>
</li>
<li>
<p><strong>Existing outputs remain.</strong>
Deprecating new P2MS outputs does nothing about the 2.4M outputs already in the UTXO set.
The pollution that has already occurred is permanent until other change happens in Bitcoin, e.g., a future UTXO set pruning mechanism.</p>
</li>
</ul>
<h3 id="prior-proposals-and-discussions">Prior proposals and discussions</h3>
<p>There have been several proposals to address P2MS misuse, though none have been implemented.</p>
<p>In September 2023, portlandhodl opened Bitcoin Core PR <a href="https://github.com/bitcoin/bitcoin/pull/28400">#28400</a> titled "Make provably unsignable standard P2PK and P2MS outpoints unspendable" to remove provably unspendable P2PK and P2MS transaction outputs from the UTXO set.
The PR received generally positive feedback, with contributors acknowledging the UTXO set pollution problem.</p>
<p>However, it was ultimately closed by the author in March 2024 with the comment: <em>"Closing because the fragility of this PR does not justify its limited impact."</em>
The challenges included determining which outputs are truly provably unspendable, managing consensus implications, and the relatively small impact relative to overall UTXO set size.</p>
<p>A related discussion occurred around PR <a href="https://github.com/bitcoin/bitcoin/pull/28217">#28217</a>, which proposed limiting bare multisig to only <code>OP_CHECKMULTISIG</code> and not <code>OP_CHECKMULTISIGVERIFY</code>.
Developer Jimmy Song <a href="https://twitter.com/jimmysong/status/1735439599055356071">commented in December 2023</a> about the broader question of whether bare multisig should be deprecated entirely, noting the data-carrying abuse.
Various discussions on platforms like <a href="https://stacker.news/items/352806">Stacker News</a> have explored whether P2MS should be made non-standard or removed from consensus, though no such changes have been implemented.</p>
<h3 id="conclusion">Conclusion</h3>
<p>This analysis provides a comprehensive baseline for understanding P2MS usage as of late 2025.
The data demonstrates conclusively that P2MS is no longer serving its intended purpose with data carriage, particularly via Bitcoin Stamps, the dominant use case.
The transition from predominantly spendable outputs before Bitcoin Stamps launched to ~75% unspendable in just ~2.5 years represents a dramatic shift in P2MS usage.</p>
<p>With Bitcoin Core v30.0 removing <code>OP_RETURN</code> size limits, the technical justification for using P2MS as a data carriage mechanism has largely evaporated.
Bitcoin Stamps now has a viable alternative for its JSON-based payloads that doesn't impose permanent costs on the network.
Whether the Bitcoin community chooses to deprecate P2MS, maintain the status quo, or pursue other approaches, the data presented here makes it unambiguously clear that the current state of P2MS usage is far removed from its original design intent.</p>
<h2 id="references">References</h2>
<ul>
<li><a href="https://research.mempool.space/utxo-set-report/">UTXO Set Report (Mempool Research)</a></li>
<li><a href="https://www.bitmex.com/blog/ordinals-impact-on-node-runners">Ordinals - Impact on Node Runners</a></li>
<li><a href="https://bitcoin.stackexchange.com/questions/23893/what-are-the-limits-of-m-and-n-in-m-of-n-multisig-addresses">What are the limits of m and n in m-of-n multisig addresses?</a></li>
<li><a href="https://github.com/bitcoin/bitcoin/blob/master/doc/release-notes/release-notes-30.0.md">Bitcoin Core v30.0 Release Notes</a></li>
<li><a href="https://github.com/bitcoin/bitcoin/pull/28400">Bitcoin Core PR #28400: Make provably unsignable standard P2PK and P2MS outpoints unspendable</a></li>
<li><a href="https://github.com/bitcoin/bitcoin/pull/28217">Bitcoin Core PR #28217: Limit bare multisig to OP_CHECKMULTISIG</a></li>
<li><a href="https://twitter.com/jimmysong/status/1735439599055356071">Jimmy Song on P2MS deprecation</a></li>
<li><a href="https://stacker.news/items/352806">Stacker News discussion on P2MS standardness</a></li>
</ul>]]></content:encoded>
            <category>bitcoin</category>
            <category>p2ms</category>
            <category>data-carry</category>
            <category>utxo-set</category>
            <category>research</category>
        </item>
        <item>
            <title><![CDATA[P2MS Data Carry Part 1: Fundamentals and Examples]]></title>
            <link>https://deadmanoz.xyz/posts/p2ms-data-carry-1</link>
            <guid isPermaLink="false">https://deadmanoz.xyz/posts/p2ms-data-carry-1</guid>
            <pubDate>Thu, 16 Oct 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Examining the techniques of P2MS data carriage]]></description>
            <content:encoded><![CDATA[<h2 id="tldr">tl;dr</h2>
<p>Pay-to-Multisig (P2MS) is Bitcoin's original multisig format, since superseded by more efficient alternatives like P2SH and P2WSH.
Even so, it persisted - and still persists - as a vehicle for data carriage, with multiple protocols embedding payloads into fake pubkeys inside Bitcoin transactions.
Three major protocols dominate this practice: Bitcoin Stamps, Counterparty and Omni.</p>
<p>Bitcoin Stamps employs ARC4 obfuscation (TXID-keyed) and can be identified by distinctive Key Burn patterns in the original pub keys (<code>0x0222...</code>, <code>0x0333...</code>, etc).
A protocol identifier, e.g., <code>stamp:</code>, appears only once in the deobfuscated key data.
Bitcoin Stamps uses 1-of-3 P2M outputs, with one Key Burn pubkey and two data pubkeys per output, so no private keys exist that can spend the output.</p>
<p>Counterparty also uses ARC4 obfuscation (TXID-keyed) but is identified by the string <code>CNTRPRTY</code> being present in the deobfuscated key data.
The identifier appears once per ARC4-obfuscated output, so multi-output transactions sacrifice data carrying efficiency.
Counterparty maintains at least one real pubkey per output in its 1-of-2 or 1-of-3 configurations, so most Counterparty-related P2MS outputs remain spendable in theory (apart from <a href="#bitcoin-stamps---classic-stamp-image-counterparty-transport">Classic Stamps</a>).</p>
<p>Omni distinguishes itself through obfuscation based on SHA-256+XOR, keyed to the sender's address, and identification via the Exodus address (<code>1EXoDusj...</code>) in adjacent transaction outputs.
Omni also maintains valid pubkeys in its 1-of-2 or 1-of-3 structures, so all Omni P2MS outputs can also be spent (again, in theory).</p>
<p>A number of minor protocols also leverage P2MS for data carrying purposes, including protocols using identifiers such as <code>CHANCECO</code> (Chancecoin), <code>TB0001</code>, <code>TEST01</code> and <code>METROXMN</code>.
Additionally, several protocols employ hybrid approaches that use P2MS alongside <code>OP_RETURN</code>.
PPk (PPkPub) uses the combined data-carrying capacity of P2MS outputs + <code>OP_RETURN</code>, with a distinctive marker pubkey for identification, whereas others use <code>OP_RETURN</code> outputs for explicit protocol signalling with the larger storage capacity of P2MS outputs used for the data payload (e.g., "Protocol 47930", <code>CLIPPERZ</code>).
Beyond protocol-based approaches, P2MS has also been used for generic data storage, with notable examples including the Bitcoin whitepaper PDF and Wikileaks Cablegate files.</p>
<h2 id="introduction">Introduction</h2>
<p>This post is the first in a series of posts that will explore how the Bitcoin blockchain is used for data carriage.
Data carriage refers to the practice of embedding arbitrary, non-financial data into Bitcoin transactions and storing it permanently on the blockchain.
Data carriage is a controversial topic in the Bitcoin community, with some viewing it as a legitimate use case for the blockchain, while others see it as an abuse of the system that adds blockchain bloat and raises costs for everyone.</p>
<p>While there are many ways to embed data in Bitcoin transactions, this and the following posts will explore the use of Pay-to-Multisig (P2MS) script types for data carriage.
This instalment explains the fundamentals of P2MS and how it's used for data carriage while <a href="./p2ms-data-carry-2">Part 2</a> examines the magnitude of P2MS data carriage via an analysis of a snapshot of the UTXO set.
Note that this post goes into detail in breaking down examples of the various protocols; these examples are collapsed by default but can be expanded.</p>
<p>The use of P2MS script types seemed like a good place to start because:</p>
<ul>
<li>It's the legacy script type for multisig that has long been superseded by other script types that enable multisig, including P2SH and P2WSH, with these newer script types being more economical to use.</li>
<li>Given the above, there's no real reason for anyone to use P2MS for multisig, yet it <em>is</em> still used, for data carriage.</li>
<li>There's been a significant increase in P2MS UTXOs since early 2023 as per Figure 1, yet the encumbered value remains <strong>tiny</strong>.</li>
<li>P2MS data carrying is regularly used, albeit much less so than other approaches (e.g., P2TR-based approaches), but it remains understudied compared to other approaches.</li>
</ul>
<p>There have been a number of analyses of data carriage in Bitcoin, but they generally focus on the other data carriage methods.
Pay-to-Fake-Multisig (P2FMS) as the use of P2MS for data carriage is sometimes called, is often just a side note.
See the <a href="#references">References</a> section for links to some of these other analyses.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/p2ms_analysis.png" alt="Figure 1: the number of P2MS UTXOs and encumbered value over time (block height increments of 50,000)."></p>
<h2 id="p2ms-fundamentals">P2MS fundamentals</h2>
<p>P2MS is a script type that allows users to lock bitcoins to multiple (n) public keys, and require signatures for some or all (m) of those public keys to unlock and spend; an m-of-n multisig.
P2MS is sometimes referred to as raw or bare multisig, as the public keys used to create the lock are directly accessible in the locking script (the <code>ScriptPubKey</code>).
This is in contrast to the more modern multisig constructs of P2SH and P2WSH where the public keys used to create the lock are obfuscated by hashing before being input into the locking script.</p>
<p>Here's an example of a recent transaction from block height <a href="https://mempool.space/block/00000000000000000000708a6447d56220de4d4b2ac7462a6f533d3609320be5">903,379</a> (June 2025) that features two P2MS outputs that are relevant to the following content: <a href="https://mempool.space/tx/eb96a65e4a332f2c84cb847268f614c037e038d2c386eb08d49271966c1b0000">eb96a65e...</a></p>
<p>The <code>ScriptPubKey</code> of the first P2MS output is:</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/mempool.space-p2ms-output.png" alt="Figure 2: the first P2MS output as shown on mempool.space."></p>
<pre><code>512103d587bbd682a301f2933de3efd59ec3aa0e5a305d3b4597a8d71880895ebd9f002102660224cd2ffbf92fada23aa883f0c51f2d55ae13394a40d6538ff2a63d0dce002102020202020202020202020202020202020202020202020202020202020202020253ae
</code></pre>
<p>This <code>ScriptPubKey</code> is 105 bytes, comprised of the following:</p>
<ul>
<li>OP_1 (<code>51</code>), this is m in the m-of-n multisig</li>
<li>3x sets of OP_PUSHBYTES_33 (<code>21</code>) followed by 33-byte public keys
<ul>
<li><code>21 03d587bbd682a301f2933de3efd59ec3aa0e5a305d3b4597a8d71880895ebd9f00</code></li>
<li><code>21 02660224cd2ffbf92fada23aa883f0c51f2d55ae13394a40d6538ff2a63d0dce00</code></li>
<li><code>21 020202020202020202020202020202020202020202020202020202020202020202</code></li>
</ul>
</li>
<li>OP_3 (<code>53</code>), this is n in m-of-n, so it's a 1-of-3 multisig</li>
<li>OP_CHECKMULTISIG (<code>ae</code>)</li>
</ul>
<p>Those first two public keys on the surface look like they could be proper public keys, but that 3rd key looks "fake".
Indeed, even <a href="https://mempool.space/tx/eb96a65e4a332f2c84cb847268f614c037e038d2c386eb08d49271966c1b0000">mempool.space</a> indicates as such, what's the deal with that?</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/mempool.space-p2ms-fake-pubkey.png" alt="Figure 3: mempool.space marking the P2MS as having a fake pubkey."></p>
<h2 id="fake-keys">Fake keys</h2>
<p>The 3rd key is indeed a fake key in the sense that it's not a public key that has a known corresponding private key.
This fake key, <code>020202020202020202020202020202020202020202020202020202020202020202</code>, is actually one of the <em>Key Burn</em> addresses that are used by Bitcoin Stamps, one of the leading data carrying protocols that use P2MS.</p>
<p>The ability to generate predetermined patterns of public keys such as Key Burn addresses in this instance, or burn addresses in general, is almost impossible because it requires generating a private key that leads to the desired public key.
Said another way, the probability of arriving at a pre-determined pattern for an ECC-256 public key is "infinitesimally small to the point where a computer would need to grind away at keys for billions of years in order to produce a valid private key" Bitcoin Stamps.</p>
<p>Because of the near impossibility of generating a private key that leads to a predetermined pattern in a public key, the existence of a highly improbable patterned public key is accepted as evidence that there is no corresponding private key... and that the key cannot be used to spend the output!</p>
<p>According to the <a href="https://github.com/mikeinspace/stamps/blob/main/Key-Burn.md">official Bitcoin Stamps protocol documentation</a>, there are 4 Key Burn addresses/patterns/public keys:</p>
<ul>
<li><code>022222222222222222222222222222222222222222222222222222222222222222</code></li>
<li><code>033333333333333333333333333333333333333333333333333333333333333333</code></li>
<li><code>020202020202020202020202020202020202020202020202020202020202020202</code></li>
<li><code>030303030303030303030303030303030303030303030303030303030303030303</code></li>
</ul>
<p>The Key Burn technique assigns one of the above Key Burn keys to last key position in the 1-of-3 multisig, leaving the first two keys as possibilities to spend the output.</p>
<p>The first two keys, however, are actually the data-carrying component (as we shall later learn from understanding how Bitcoin Stamps works), although we can't always definitively prove that they represent "fake" public keys.
That is, we can run a check to see if a given public key is a valid point on the ECDSA secp256k1 curve, if it is not, we know that it is truly a fake public key, yet if it is a valid point, then it could either be a true key or just "data" that happens to correspond to a point on the curve!</p>
<p>For example, using the 1st and 2nd keys above:</p>
<ul>
<li>1st key: <code>03d587bbd682a301f2933de3efd59ec3aa0e5a305d3b4597a8d71880895ebd9f00</code> is a valid point</li>
<li>2nd key: <code>02660224cd2ffbf92fada23aa883f0c51f2d55ae13394a40d6538ff2a63d0dce00</code> is NOT a valid point</li>
</ul>
<p><strong>CODE: Python code to validate a public key on the secp256k1 curve</strong></p>
<pre><code class="hljs language-python"><span class="hljs-comment"># Validate public key on secp256k1 curve</span>
<span class="hljs-comment"># Returns True if point (x,y) satisfies: y² = x³ + 7 (mod p)</span>

p = <span class="hljs-number">0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F</span>

<span class="hljs-keyword">def</span> <span class="hljs-title function_">is_valid_pubkey</span>(<span class="hljs-params">pubkey_hex</span>):
    <span class="hljs-string">"""Check if public key is valid point on secp256k1"""</span>
    pub_bytes = <span class="hljs-built_in">bytes</span>.fromhex(pubkey_hex)

    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(pub_bytes) == <span class="hljs-number">33</span>:  <span class="hljs-comment"># Compressed</span>
        prefix, x = pub_bytes[<span class="hljs-number">0</span>], <span class="hljs-built_in">int</span>.from_bytes(pub_bytes[<span class="hljs-number">1</span>:], <span class="hljs-string">'big'</span>)
        y_squared = (<span class="hljs-built_in">pow</span>(x, <span class="hljs-number">3</span>, p) + <span class="hljs-number">7</span>) % p
        y = <span class="hljs-built_in">pow</span>(y_squared, (p + <span class="hljs-number">1</span>) // <span class="hljs-number">4</span>, p)

        <span class="hljs-keyword">if</span> <span class="hljs-built_in">pow</span>(y, <span class="hljs-number">2</span>, p) != y_squared:
            <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>

        <span class="hljs-comment"># For any valid x, there are two valid y values: y and p - y</span>
        <span class="hljs-comment"># If calculated y doesn't match prefix parity, use the other root</span>
        <span class="hljs-keyword">if</span> (y % <span class="hljs-number">2</span> == <span class="hljs-number">1</span>) == (prefix == <span class="hljs-number">0x02</span>):
            y = p - y

        <span class="hljs-keyword">return</span> (y % <span class="hljs-number">2</span> == <span class="hljs-number">0</span>) == (prefix == <span class="hljs-number">0x02</span>)

    <span class="hljs-keyword">elif</span> <span class="hljs-built_in">len</span>(pub_bytes) == <span class="hljs-number">65</span>:  <span class="hljs-comment"># Uncompressed</span>
        x = <span class="hljs-built_in">int</span>.from_bytes(pub_bytes[<span class="hljs-number">1</span>:<span class="hljs-number">33</span>], <span class="hljs-string">'big'</span>)
        y = <span class="hljs-built_in">int</span>.from_bytes(pub_bytes[<span class="hljs-number">33</span>:], <span class="hljs-string">'big'</span>)
        <span class="hljs-keyword">return</span> (<span class="hljs-built_in">pow</span>(y, <span class="hljs-number">2</span>, p) - <span class="hljs-built_in">pow</span>(x, <span class="hljs-number">3</span>, p) - <span class="hljs-number">7</span>) % p == <span class="hljs-number">0</span>

    <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>

<span class="hljs-comment"># Usage: is_valid_pubkey("03d587bbd682a301f2933de3efd59ec3aa0e5a305d3b4597a8d71880895ebd9f00")</span>
</code></pre>
<p>Suffice it to say, checking whether a given public key is a valid point on the ECDSA secp256k1 curve is insufficient to definitely prove that the key is data or otherwise - sometimes a public key will be a valid point yet it just represents data (as with the 1st key above).</p>
<p>We have yet to see how keys can represent data; let’s examine how data is embedded.</p>
<h2 id="data-carrying-in-p2ms---a-bitcoin-stamps-example">Data carrying in P2MS - A Bitcoin Stamps example</h2>
<p>Note that the following is provided to give a concrete example of how Bitcoin Stamps embeds arbitrary, non-financial data into Bitcoin transactions; the aim is not to cover the minutiae of how such protocols operate at a higher level (e.g., deploying, minting, transferring etc.).</p>
<p>Bitcoin Stamps inserts data into 1-of-3 P2MS transaction outputs, with the data encoded to look like public keys for the first two keys ("data keys"), and a Key Burn address used for the third.
Setting the Key Burn address aside, with compressed public keys being 33 bytes, the first and last bytes are stripped from each of the data keys, leaving 31 bytes apiece.
Concatenate the byte strings into a single 62-byte string:</p>
<pre><code>d587bbd682a301f2933de3efd59ec3aa0e5a305d3b4597a8d71880895ebd9f + 
660224cd2ffbf92fada23aa883f0c51f2d55ae13394a40d6538ff2a63d0dce
</code></pre>
<p>If the transaction contains two or more P2MS outputs, then the above process is followed for each output: take the first two public keys, disregard the Key Burn, strip bytes and concatenate the data key byte strings.
Then concatenate such strings from each transaction output into a single byte string.</p>
<p>For our example transaction, there's a 2nd multisig output that has the following data keys:</p>
<pre><code>03e34afbaa13450d01f91c6633f7e9cd5893c6096f9087b347a2479c7add951700
02ae587815be570ac6344c49be194daa07e4b8de982f16c8175a981cd0ff4d2200
</code></pre>
<p>Stripped:</p>
<pre><code>e34afbaa13450d01f91c6633f7e9cd5893c6096f9087b347a2479c7add9517 +
ae587815be570ac6344c49be194daa07e4b8de982f16c8175a981cd0ff4d22
</code></pre>
<p>Concatenating the byte string from the first and second output yields the following 124-byte string:</p>
<pre><code>d587bbd682a301f2933de3efd59ec3aa0e5a305d3b4597a8d71880895ebd9f + 
660224cd2ffbf92fada23aa883f0c51f2d55ae13394a40d6538ff2a63d0dce +
e34afbaa13450d01f91c6633f7e9cd5893c6096f9087b347a2479c7add9517 +
ae587815be570ac6344c49be194daa07e4b8de982f16c8175a981cd0ff4d22
</code></pre>
<p>The resulting byte string can now be decoded into meaningful data.
Bitcoin Stamps uses the ARC4 (RC4) stream cipher to obfuscate the original data before embedding it, with a key that is the transaction ID (TXID) corresponding to the first transaction input (<code>vin[0]</code>).
For our example transaction <a href="https://mempool.space/tx/eb96a65e4a332f2c84cb847268f614c037e038d2c386eb08d49271966c1b0000">eb96a65e...</a>, the TXID corresponding to the first transaction input is:</p>
<pre><code>7568f57ecf417e19edefc810f9bbd34d2a62eb770d8492396ceffed3c5dc7348
</code></pre>
<p>Via:</p>
<pre><code>bitcoin-cli getrawtransaction eb96a65e4a332f2c84cb847268f614c037e038d2c386eb08d49271966c1b0000 | xargs bitcoin-cli decoderawtransaction | jq '.vin[0].txid'
</code></pre>
<p>Using <code>7568f57e...</code> as the deobfuscation key, the byte string is deobfuscated to the following.
Note that ARC4 being a stream cipher means that deobfuscation preserves length - 124 bytes in, 124 bytes out.</p>
<pre><code>003f7374616d703a7b2270223a227372632d3230222c226f70223a227472616e73666572222c227469636b223a22424d574b222c22616d74223a2231303030227d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
</code></pre>
<p>The first two bytes, <code>003f</code>, is the length of the decoded data in hex - this is just <code>63</code>.
That is, the 63 bytes following the initial two bytes are the meaningful data, with the remaining data being meaningless (the remaining bytes are zero padding).</p>
<p>Decoding the meaningful 63 bytes with UTF-8 gives:</p>
<pre><code>'stamp:{"p":"src-20","op":"transfer","tick":"BMWK","amt":"1000"}'
</code></pre>
<p>Note that in the above there are no spaces - <a href="https://github.com/stampchain-io/stamps_sdk/blob/main/docs/src20specs.md"><em>"in order to minimize the transaction size spaces are not used in the serialized JSON string which is constructed by the SRC-20 reference wallet"</em></a>.
Prettier formats to the following.</p>
<pre><code class="hljs language-json">stamp<span class="hljs-punctuation">:</span> <span class="hljs-punctuation">{</span>
	<span class="hljs-attr">"p"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"src-20"</span><span class="hljs-punctuation">,</span>
	<span class="hljs-attr">"op"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"transfer"</span><span class="hljs-punctuation">,</span>
	<span class="hljs-attr">"tick"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"BMWK"</span><span class="hljs-punctuation">,</span>
	<span class="hljs-attr">"amt"</span><span class="hljs-punctuation">:</span><span class="hljs-string">"1000"</span>
<span class="hljs-punctuation">}</span>
</code></pre>
<p>So we've taken the two P2MS transaction outputs and transformed it to the above data.
But we've still got some other aspects of the transaction, such as the two other non-P2MS transaction outputs and the output values, that we should briefly consider.</p>
<h3 id="non-p2ms-transaction-outputs-and-output-values">Non-P2MS transaction outputs and output values</h3>
<p>The first transaction output, <code>vout[0]</code>, with an address <a href="https://mempool.space/address/bc1qmgl3u9m7geanw8tlcydrd8qd44awh2660mpg5a">bc1qmgl3u9m7...</a> is the transfer destination.
Transfer because this is what the <code>op</code> was in this example; in other cases it might be the "minter" or "deployer" etc.
The main point is that <code>vout[0]</code> is always some real address.</p>
<p>The 4th transaction output, <code>vout[3]</code>, with an address <a href="https://mempool.space/address/bc1qdmsmn42qajk7ucmkhcszmcd6g7e3svq0jyx3l5">bc1qdmsmn42q...</a>, is the change address.
In this example, it's actually just the same address that was the single transaction input to this transaction - change is going back to original spending address.</p>
<p>The transaction details in Figure 4 show that the first three outputs have a value of 790 sats each.
I don't think this value has any significance other than it safely exceeds all dust limits - <a href="https://bitcoin.stackexchange.com/questions/10986/what-is-meant-by-bitcoin-dust">the highest being 546 sats for P2PKH for the default configuration of 3 sat/vByte</a>.
That is, these values ensure that each output and thus the overall transaction is valid, yet is low enough to have low monetary value.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/mempool.space-p2ms-tx-details.png" alt="Figure 4: the transaction details."></p>
<h3 id="unspendable-p2ms-outputs">Unspendable P2MS outputs</h3>
<p>In the above we established that the P2MS transaction outputs were actually purely for data carrying (or were Key Burn) and did not involve real public keys.
As such, these P2MS outputs are effectively unspendable outputs, and, given the current design of Bitcoin, they'll remain in the UTXO set of every Bitcoin node.
Each time there's a transaction that embeds data in P2MS outputs in the manner described above, there will be at least one, but possibly more, new unspendable P2MS UTXOs added to the UTXO set.</p>
<p>Note that this is, perhaps obviously, intentional.
As is noted in the <a href="https://github.com/mikeinspace/stamps/blob/main/BitcoinStamps.md">Bitcoin Stamps documentation</a>:</p>
<blockquote>
<p>"By doing so, the data is preserved in such a manner that is impossible to prune from a full Bitcoin Node, preserving the data immutably forever."</p>
</blockquote>
<p>I think many would consider this wasteful - every time there is some form of individual activity on Bitcoin Stamps, such as the transfer operation from the above example, there's a one-to-one mapping to activity on Bitcoin, tracked forever, by all Bitcoin nodes!
It's probably worth examining Bitcoin Stamps in a bit more detail to better understand the what and why.</p>
<h2 id="bitcoin-stamps">Bitcoin Stamps</h2>
<p>Bitcoin Stamps were developed in response to most NFTs being <em>"merely image pointers to centralized hosting or stored on-chain in prunable witness data"</em>.
They were a means to achieve permanence in <em>"storing art on the blockchain"</em> and indeed STAMP is an acronym for Secure, Tradeable Art Maintained Securely.
The <a href="https://github.com/mikeinspace/stamps/blob/main/BitcoinStamps.md">original spec</a> also stated that Bitcoin Stamps encode:</p>
<blockquote>
<p><em>"an image's binary content to a base64 string, placing this string as a suffix to <code>STAMP:</code> in a transaction's description key, and then broadcasting it using the Counterparty protocol onto the Bitcoin ledger.</em>
<em>The length of the string means that Counterparty defaults to bare multisig, thereby chunking the data into outputs rather than using the limited (and prunable) <code>OP_RETURN</code>.</em>
<em>By doing so, the data is preserved in such a manner that is impossible to prune from a full Bitcoin Node, preserving the data immutably forever."</em></p>
</blockquote>
<p>The above examination of the <a href="https://mempool.space/tx/eb96a65e4a332f2c84cb847268f614c037e038d2c386eb08d49271966c1b0000"><code>eb96a65e...</code></a> transaction is but just one example of Bitcoin Stamps embedding arbitrary data in P2MS outputs.
In fact, Bitcoin Stamps has used a variety of techniques since the first transactions, <a href="https://jpja.github.io/Electrum-Counterparty/decode_tx.html?tx=17686488353b65b128d19031240478ba50f1387d0ea7e5f188ea7fda78ea06f4"><code>17686488...</code></a> and <a href="https://jpja.github.io/Electrum-Counterparty/decode_tx.html?tx=eb3da8146e626b5783f4359fb1510729f4aad923dfac45b6f1f3a2063907147c"><code>eb3da814...</code></a>, were included in Block <a href="https://mempool.space/block/00000000000000000002ea8eb5df114c3f198c7ef5851435e8a4d8e7bd33121c">779,652</a>.</p>
<p>As the above text alluded to, Bitcoin Stamps started out leveraging Counterparty (which has been around since 2014 and is covered in the <a href="#counterparty">Counterparty</a> section).
Specifically, Bitcoin Stamps ("Classic Stamps") were initially a numerical asset on Counterparty, with data encoded via either P2MS or P2WSH.
P2MS Classic Stamps were encoded in the almost the same manner as the example explored above:</p>
<ul>
<li>ARC4 obfuscation/deobfuscation keyed with the TXID of the first transaction input</li>
<li>Key Burn alongside data keys, no real pubkey present</li>
</ul>
<p>However, the deobfuscated data must first contain the <code>CNTRPRTY</code> prefix, followed by some Counterparty message fields, before a <code>STAMP:</code> prefix variant and Bitcoin Stamps specific data is found.
That is, Classic Bitcoin Stamps used Counterparty transactions as a transport mechanism to embed data in Bitcoin transactions.
See <a href="#bitcoin-stamps---classic-stamp-image-counterparty-transport">Bitcoin Stamps - Classic Stamp image (Counterparty transport) example</a> for a breakdown of such a transaction.</p>
<h3 id="bitcoin-stamps-sub-protocols">Bitcoin Stamps sub-protocols</h3>
<p>Since then, Bitcoin Stamps has become or leveraged a suite of sub-protocols or formats, including SRC-20, SRC-101, SRC-721, SRC-721r, OLGA, with each purportedly serving a different purpose.
Note that not all of these forms use P2MS, for example the <a href="https://github.com/mikeinspace/stamps/blob/main/OLGA.md">OLGA Stamp uses P2WSH outputs</a>.
As the interest here is P2MS, we'll only focus on the aspect of Bitcoin Stamps that leverage P2MS: Classic Stamps, SRC-20, SRC-101, SRC-721.</p>
<p>The initial example of Bitcoin Stamps using P2MS above (<a href="#data-carrying-in-p2ms---a-bitcoin-stamps-example">Data carrying in P2MS - A Bitcoin Stamps example</a>) is an example of SRC-20.
SRC-20 was developed in response to the BRC-20 craze of early 2023, with SRC-20 offering stronger permanence guarantees due to use of non-witness transaction data.
Aside: if you're interested in knowing more about BRC-20 and the history of Ordinals and Inscriptions be sure to check out Binance Research's <a href="https://research.binance.com/static/pdf/BRC-20%20Tokens%20-%20A%20Primer.pdf">"BRC-20 Tokens: A Primer" (May 2023)</a> piece.
Most Bitcoin Stamps related Bitcoin transactions involve SRC-20.</p>
<p><a href="https://bitname.gitbook.io/bitname/src-101">SRC-101 is a domain name system native to Bitcoin Stamps</a>, similar to something like the Ethereum Name Service (ENS).
Such systems are, for example, used to replace standard Bitcoin addresses like <code>bc1q34eaj4rz9yxupzxwza2epvt3qv2nvcc0ujqqpl</code> with simple, human-readable names like <code>alice.btc</code>.
The permanence of P2MS, and the guarantee of unspendability via the use of no real pubkeys, were purportedly the rationale for developing SRC-101.</p>
<p>All this is to say that, even within a single ecosystem (Bitcoin Stamps), there are a number of permutations and variants of how data is encoding for data carrying purpose that we need to consider for classification and analysis purposes.</p>
<h3 id="bitcoin-stamps---classic-stamp-image-counterparty-transport">Bitcoin Stamps - Classic Stamp image (Counterparty transport)</h3>
<p><strong>EXAMPLE: Bitcoin Stamps - Classic Stamp image (Counterparty transport)</strong></p>
<p><a href="https://mempool.space/tx/54fdeda90c4573f8a93fa45251a3c6214bcc79aa8549728dfb08ffe3e7dd3d81"><code>54fdeda9...</code></a> is a transaction from block height <a href="https://mempool.space/block/000000000000000000048a022a551b879bd87160c55b22e1f3b9a3d3b2410094">809,193</a> with 79 outputs, 77 of which are P2MS outputs.
A review of the P2MS outputs shows that all have the <code>022222222222222222222222222222222222222222222222222222222222222222</code> Key Burn pattern and the TXID of <code>vin[0]</code> is :</p>
<pre><code>3b2b5e1de60ba341b8ba85e35b09800edb118dc7bee246d54b11420f01aabac5
</code></pre>
<p>Armed with this as the deobfuscation key, we can attempt to decode the data embedded in the 77 P2MS outputs.
Let's examine the first three P2MS outputs to illustrate the process.</p>
<h4 id="1st-p2ms-output">1st P2MS output</h4>
<p>Pubkeys:</p>
<pre><code>02e6e725b168f3eeafa527053d43d06c9569a393c34d0e5c2dad2236b785eaed70
02acb6c9432134cad7242dbbd531c44e5ec5918e31d65adab1b2ffb2d3588c7d36
</code></pre>
<p>Stripped and concatenated:</p>
<pre><code>e6e725b168f3eeafa527053d43d06c9569a393c34d0e5c2dad2236b785eaed +
acb6c9432134cad7242dbbd531c44e5ec5918e31d65adab1b2ffb2d3588c7d
</code></pre>
<p>Deobfuscated with TXID of <code>vin[0]</code>:</p>
<pre><code>3d434e54525052545914575ed3597b0aa71d00000000000000230001005354 +
414d503a52306c474f446c686f41436741504d5041487a4741502f2f2f7777
</code></pre>
<p>ASCII interpretation (some characters replaced with <code>.</code>):</p>
<pre><code>.CNTRPRTY.......STAMP:R0lGODlhoACgAPMPAHzGAP///ww
</code></pre>
<h4 id="2nd-p2ms-output">2nd P2MS output</h4>
<p>Pubkeys:</p>
<pre><code>03e6e725b168f3eeafa5621322e3c853da83f6d2802a4f136cec6c79f0d0e1f8c4
02a690d838397ce3d1252bbffc0eae7961e2b5ba20c759cfb7a384f6cb10ba4bb6
</code></pre>
<p>Stripped and concatenated:</p>
<pre><code>e6e725b168f3eeafa5621322e3c853da83f6d2802a4f136cec6c79f0d0e1f8 +
a690d838397ce3d1252bbffc0eae7961e2b5ba20c759cfb7a384f6cb10ba4b
</code></pre>
<p>Deobfuscated with TXID of <code>vin[0]</code>:</p>
<pre><code>3d434e545250525459514141734144454d48414367414f41416d4f46555841 +
4b6b41414a7845414e426841502b745866747941504b6f41502b6b37674141
</code></pre>
<p>ASCII interpretation:</p>
<pre><code>=CNTRPRTYQAAsADEMHACgAOAAmOFUXAKkAAJxEANBhAP+tXftyAPKoAP+k7gAA
</code></pre>
<h4 id="3rd-p2ms-output">3rd P2MS output</h4>
<p>Pubkeys:</p>
<pre><code>02e6e725b168f3eeafa572112bbfca27aa88e8d58d095f0a6feb4c5f82f2f8ce20
03a8bad838326c8dc13a2f8dfc1fd54c7accea8834ae65c4b19fdbfca4079750c7
</code></pre>
<p>Stripped and concatenated:</p>
<pre><code>e6e725b168f3eeafa572112bbfca27aa88e8d58d095f0a6feb4c5f82f2f8ce +
a8bad838326c8dc13a2f8dfc1fd54c7accea8834ae65c4b19fdbfca4079750
</code></pre>
<p>Deobfuscated with TXID of <code>vin[0]</code>:</p>
<pre><code>3d434e5452505254594143482f4330354656464e44515642464d6934774177 +
4541414141682b5151465a4141504143482b4b55397764476c746158706c5a
</code></pre>
<p>ASCII interpretation:</p>
<pre><code>=CNTRPRTYACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFZAAPACH+KU9wdGltaXplZ
</code></pre>
<p>Whenever we see <code>434e545250525459</code> in deobfuscated output, we have found the <code>CNTRPRTY</code> identifier.
Focusing on the first P2MS output, because we have the <code>CNTRPRTY</code> prefix, we have to interpret the following bytes according to the Counterparty protocol.
Now, depending on whether the transaction represents a "legacy" Counterparty transaction or a "modern" Counterparty transaction, changes the interpretation of data.
Turns out that this is a "modern" form where the message type is a single byte rather than the 4 bytes for "legacy" Counterparty transactions.
Here's a breakdown of the first P2MS output:</p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex (or ASCII)</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>3d</code></td>
<td>Length prefix: 61</td>
</tr>
<tr>
<td>1-8</td>
<td><code>434e545250525459</code></td>
<td><code>CNTRPRTY</code> prefix</td>
</tr>
<tr>
<td>9</td>
<td><code>14</code></td>
<td>Message type 20 (Issuance)</td>
</tr>
<tr>
<td>10-17</td>
<td><code>575ed3597b0aa71d</code></td>
<td>Asset ID: 6295701710380377885</td>
</tr>
<tr>
<td>18-25</td>
<td><code>0000000000000023</code></td>
<td>Asset issue amount: 35</td>
</tr>
<tr>
<td>26</td>
<td><code>00</code></td>
<td>Divisible: False</td>
</tr>
<tr>
<td>27</td>
<td><code>01</code></td>
<td>Lock: True</td>
</tr>
<tr>
<td>28</td>
<td><code>00</code></td>
<td>Reset: False</td>
</tr>
<tr>
<td>29-61</td>
<td><code>5354414d503a5230...</code></td>
<td>Description: <code>STAMP:R0lGODlhoACgAPMPAHzGAP///ww</code></td>
</tr>
</tbody>
</table>
<p>Also note that whenever we see <code>5354414d503a</code> or <code>5354414d50533a</code> in the decoded output we have found <code>STAMP:</code> or <code>STAMPS:</code>, respectively (with lowercase variants <code>7374616d703a</code> for <code>stamp:</code> and <code>7374616d70733a</code> for <code>stamps:</code>).
It's the description field where further decoding or interpretation is clearly necessary; we see the <code>STAMP:</code> prefix following by a bunch of random characters.
These random characters are actually <a href="https://github.com/mikeinspace/stamps/blob/main/BitcoinStamps.md#bitcoin-stamps">base64 encoded data of an image</a>:</p>
<blockquote>
<p><em>"encoding an image's binary content to a base64 string, placing this string as a suffix to <code>STAMP:</code> in a transaction's description key, and then broadcasting it using the Counterparty protocol onto the Bitcoin ledger... <code>STAMP:&#x3C;base64 data></code>"</em></p>
</blockquote>
<p>Typically, there would be some "magic bytes" or MIME-type and encoding for image data, but the rationale for the absence of this data in Bitcoin Stamps payloads was <a href="https://github.com/mikeinspace/stamps/blob/main/BitcoinStamps.md#absence-of-mime-type-and-encoding">given as</a>:</p>
<blockquote>
<ul>
<li><em>"The fewer the bytes the better."</em></li>
<li><em>"Given the limited scope of acceptable file formats, we are confident that decoding them accurately based on the base64 string alone is trivial."</em></li>
<li><em>"We are only interested in decoding base64, so if the string does not conform to valid base64 it is rejected.</em>
<em>Therefore, specification of the encoding is unnecessary."</em></li>
</ul>
</blockquote>
<p>Anyway, before we deal with the base64 we need to handle the other P2MS outputs.
It can perhaps be inferred from the 2nd and 3rd P2MS outputs above that each P2MS output will have the <code>CNTRPRTY</code> identifier (prefixed by a length prefix).
That is, each P2MS output has 9 bytes (1 length prefix, 8 <code>CNTRPRTY</code>) reserved for the Counterparty protocol, so the true data carrying capacity is only: (62 - 9)/62 = 85.4%.
This is probably one reason why Bitcoin Stamps opted for a new protocol - improve efficiency by not requiring a length prefix and <code>CNTRPRTY</code> identifier in each P2MS output.</p>
<p>Once we've stripped all length prefixes and <code>CNTRPRTY</code> identifiers from all the deobfuscated P2MS outputs and concatenated them, we end up with 4,004 bytes of base64 data which decodes to a 3,003-byte GIF.
The size discrepancy is base64 encoding overhead: every 4 base64 characters is 3 bytes of binary data, so 4,004 / 4 = 1,001 groups x 3 = 3,003 bytes.
The recovered image is shown in Figure 5.</p>
<p>This example has shown how Bitcoin Stamps created 77 P2MS unspendable outputs in transaction <a href="https://mempool.space/tx/54fdeda90c4573f8a93fa45251a3c6214bcc79aa8549728dfb08ffe3e7dd3d81"><code>54fdeda9...</code></a> for the purpose of storing a single 3kB GIF.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/54fdeda90c4573f8a93fa45251a3c6214bcc79aa8549728dfb08ffe3e7dd3d81.gif" alt="Figure 5: the GIF embedded in the 77 P2MS outputs of transaction 54fdeda9...."></p>
<h2 id="counterparty">Counterparty</h2>
<p>We began our exploration of data carrying in P2MS with Bitcoin Stamps because this protocol is both the most prolific user of P2MS for data carrying purposes, and is largely the only protocol still in active use today.
It started, however, with Classic Stamps leveraging Counterparty, which was the first protocol to start using P2MS for data carrying purposes in a significant way back in 2014.</p>
<p>We won't dwell much on the history of Counterparty here, it's incredibly well documented elsewhere, including:</p>
<ul>
<li><a href="https://blog.bitmex.com/battle-of-the-dexes/">BitMEX Research - Battle of the Dexes (September 2020)</a></li>
<li><a href="https://blog.bitmex.com/dapps-or-only-bitcoin-transactions-the-2014-debate/">BitMEX Research - The OP_Return Wars of 2014 – Dapps Vs Bitcoin Transactions (July 2022)</a></li>
</ul>
<p>It is, however perhaps worth noting that Counterparty, via the adoption of P2MS for data carrying, was a key factor in the decision <a href="https://bitcoin.org/en/release/v0.9.0#opreturn-and-data-in-the-block-chain">make <code>OP_RETURN</code> transactions standard in Bitcoin Core 0.9.0 (March 2014)</a>:</p>
<blockquote>
<p>"On <code>OP_RETURN</code>: There was been (sic) some confusion and misunderstanding in the community, regarding the <code>OP_RETURN</code> feature in <code>0.9</code> and data in the blockchain.
This change is not an endorsement of storing data in the blockchain.
The <code>OP_RETURN</code> change creates a provably-prunable output, to avoid data storage schemes – some of which were already deployed – that were storing arbitrary data such as images as forever-unspendable TX outputs, bloating bitcoin’s UTXO database."</p>
</blockquote>
<blockquote>
<p>"Storing arbitrary data in the blockchain is still a bad idea; it is less costly and far more efficient to store non-currency data elsewhere."</p>
</blockquote>
<p>Counterparty was created to enable features like user-created tokens (assets), decentralised exchanges ("dexes"), and other financial primitives without requiring changes to the Bitcoin protocol.
By encoding its protocol messages in Bitcoin transactions, Counterparty leveraged Bitcoin's security and immutability to create a financial platform on top of Bitcoin.
The protocol initially used P2MS outputs for data embedding before transitioning to <code>OP_RETURN</code> outputs once they became <em><strong>standard</strong></em>, though P2MS continued to be used for larger transactions that exceeded <code>OP_RETURN</code>'s (standardness) size limits.
Counterparty's approach was influential in demonstrating both the potential and controversy of using Bitcoin for purposes beyond simple value transfer.</p>
<p>Counterparty is also of some importance in being the single largest example of proof-of-burn in Bitcoin.
Proof-of-burn was seen as a way to bootstrap the Counterparty ecosystem without requiring an ICO or pre-mining, which were common practices at the time.
Proof-of-burn involves sending Bitcoin to an unspendable address, effectively "burning" the Bitcoin, and, in the Counterparty case, receiving Counterparty (XCP) tokens in return.
During January 2014, Counterparty distributed XCP tokens to those who sent Bitcoin to the provably unspendable <a href="https://mempool.space/address/1CounterpartyXXXXXXXXXXXXXXXUWLpVr"><code>1CounterpartyXXXXXXXXXXXXXXXUWLpVr</code></a> address.
To date, 2,130.99165372 Bitcoin has been permanently destroyed in being sent to this address.</p>
<h3 id="embedding-counterparty-transactions-in-bitcoin">Embedding Counterparty transactions in Bitcoin</h3>
<p>In general, to know if a transaction involving P2MS outputs is a Counterparty transaction, we actually have to treat the transaction data in a number of different ways.
The majority of Counterparty data is, like Bitcoin Stamps, obfuscated by ARC4, keyed with the TXID of the first input (<code>vin[0]</code>).
However, unlike Bitcoin Stamps, there is no Key Burn key that provides a simple indication that a transaction is a Counterparty transaction; keys in Counterparty are either valid public keys or are data-carrying.</p>
<p>There is a minority of Counterparty transactions that does not, however, use ARC4 obfuscation, and all that is required is simple ASCII decoding.
An example of such is outlined in <a href="#counterparty---no-arc4-obfuscation">Counterparty - no ARC4 obfuscation</a>.</p>
<p>Regardless of whether there is ARC4 obfuscation or not, the definitive indication that a Bitcoin transaction is a Counterparty transaction is if the string <code>CNTRPRTY</code> is present in the data.
For the majority of Counterparty transactions involving ARC4, the deobfuscation process is necessary before <code>CNTRPRTY</code> can be detected, for those lacking ARC4 obfuscation, it is relatively straightforward to identify the <code>CNTRPRTY</code> prefix via ASCII decoding of P2MS data keys.</p>
<p>In addition to there being a mix of data-encoding techniques, Counterparty also (primarily) uses both 1-of-2 and 1-of-3 P2MS transaction outputs.
Depending on which, and when the Counterparty transaction was constructed, the real pubkey is in a different position:</p>
<ul>
<li>for 1-of-2, the real pubkey is the first of the two keys</li>
<li>for 1-of-3, the real pubkey is either the first (newer) or last (older) of the three keys</li>
</ul>
<p>Being a real pubkey, and with each multisig requiring one key to spend, most Counterparty UTXOs can be spent, and thus removed from the UTXO set.
Contrast this with Bitcoin Stamps, where the keys are either Key Burn or data keys thus no private key exists to spend the UTXO, so they will remain in the UTXO set.</p>
<p>The remaining key(s) in each configuration are the data carrying component.
And again, depending on which particular variant of the Counterparty protocol we're dealing with, we may need to strip the first and last bytes from a pubkey like was necessary for Bitcoin Stamps, or no such stripping is required, and entire key encodes data.</p>
<p>The <a href="https://docs.counterparty.io/docs/advanced/protocol/">current, official Counterparty protocol specification</a> states the following:</p>
<blockquote>
<p><em>For identification purposes, every Counterparty transaction’s ‘data’ field is prefixed by the string <code>CNTRPRTY</code>, encoded in UTF‐8.</em>
<em>This string is long enough that transactions with outputs containing pseudo‐random data cannot be mistaken for valid Counterparty transactions.</em>
<em>In testing (i.e. using the <code>TESTCOIN</code> Counterparty network on any blockchain), this string is ‘XX’.</em></p>
</blockquote>
<blockquote>
<p><em>Counterparty data may be stored in three different types of outputs, or in some combinations of those formats.</em>
<em>All of the data is obfuscated by ARC4 using the transaction identifier (TXID) of the first unspent transaction output (UTXO) as the obfuscation key.</em></p>
</blockquote>
<blockquote>
<p><em>Multi‐signature data outputs are one‐of‐three outputs where the first public key is that of the sender, so that the value of the output is redeemable, and the second two public keys encode the data, zero‐padded and prefixed with a length byte.</em></p>
</blockquote>
<p>The following examples examine a few Counterparty transactions that use some of the various methods described above to encode a variety of data in P2MS transactions.</p>
<h3 id="counterparty---no-arc4-obfuscation">Counterparty - no ARC4 obfuscation</h3>
<p><strong>EXAMPLE: Counterparty - no ARC4 obfuscation</strong></p>
<p>Let's look at an example of a simple Counterparty transaction from block <a href="https://mempool.space/block/0000000000000000c548d152b0873eabd83cfba496bb5dddfb2a639b65bf5e9e">290,929</a>, <a href="https://mempool.space/tx/585f50f12288cd9044705483672fbbddb71dff8198b390b40ab3de30db0a88dd"><code>585f50f1...</code></a> that has a single 1-of-2 P2MS output.
For this transaction, the two keys are:</p>
<ul>
<li><code>02dd842167b625c60c10eda494eadd54df7b30f372d717b946b1912f0ce59dddf6</code></li>
<li><code>1c434e5452505254590000000000000000000000010000000001312d0000000000</code></li>
</ul>
<p>Even from just looking at these keys, it should be pretty obvious that the second is likely not a real pubkey!
If we try to decode this 2nd key as ASCII we can immediately observe <code>CNTRPRTY</code> in the output.
For completeness, breaking it down (using knowledge of Counterparty message structure) we have:</p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>1c</code></td>
<td>Padding/length indicator (28)</td>
</tr>
<tr>
<td>1-8</td>
<td><code>434e545250525459</code></td>
<td><code>CNTRPRTY</code> prefix</td>
</tr>
<tr>
<td>9-12</td>
<td><code>00000000</code></td>
<td>Message Type 0 (Basic Send)</td>
</tr>
<tr>
<td>13-20</td>
<td><code>0000000000000001</code></td>
<td>Asset ID 1 (XCP)</td>
</tr>
<tr>
<td>21-28</td>
<td><code>0000000001312d00</code></td>
<td>Quantity: 20,000,000 = 0.2 XCP</td>
</tr>
<tr>
<td>29-32</td>
<td><code>00000000</code></td>
<td>4 null bytes</td>
</tr>
</tbody>
</table>
<h3 id="counterparty---arc4-obfuscation">Counterparty - ARC4 obfuscation</h3>
<p><strong>EXAMPLE: Counterparty - ARC4 obfuscation</strong></p>
<p>Here's an example of another relatively simple Counterparty transaction, this time from block <a href="https://mempool.space/block/0000000000000000102836b6107448289827c7eba93b7c37bc2b144bfc9cfb51">368,602</a>, <a href="https://mempool.space/tx/541e640fbb527c35e0ee32d724efa4a5506c4c52acfba1ebc3b45949780c08a8"><code>541e640f...</code></a>.
This transaction has two 1-of-3 P2MS outputs.
On the question of which key position represents the real pubkey, upon examination it's clear that the last of the three keys in each P2MS output is the real key as we have the same key present in both outputs: <code>0241e401...ee1f</code>).
This is sender's public key, as can be confirmed by examining the <code>ScriptSig</code> of the transaction input in Figure 6 with the key highlighted in yellow.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/mempool.space-p2ms-counterparty-2.png" alt="Figure 6: Counterparty with ARC4 obfuscation transaction details (541e640f...)."></p>
<p>If we follow a process similar to <a href="#data-carrying-in-p2ms---a-bitcoin-stamps-example">the original Bitcoin Stamps example</a>, we end up with:</p>
<ul>
<li>a deobfuscation key of <code>de3dec665a89228593ffa3c0236dd098a6f9ef6ac698db016f8a3303ce728649</code> (TXID of first input)</li>
<li>a 124-byte string from a concatenation of the stripped keys of <code>026e8c99cf905947...</code> + <code>020b446132ea04f4...</code> + <code>02498c99cf905947...</code> + <code>030b446132ea04f4...</code></li>
</ul>
<p>After ARC4 deobfuscation, each P2MS output contains its own length-prefixed segment with a <code>CNTRPRTY</code> header:</p>
<p><strong>First P2MS output (62 bytes):</strong></p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>3d</code></td>
<td>Length prefix: 61 bytes follow</td>
</tr>
<tr>
<td>1-8</td>
<td><code>434e545250525459</code></td>
<td><code>CNTRPRTY</code> prefix</td>
</tr>
<tr>
<td>9-12</td>
<td><code>00000014</code></td>
<td>Message Type: 20 (Issuance)</td>
</tr>
<tr>
<td>13-20</td>
<td><code>0000036c089131f1</code></td>
<td>Asset ID: 3762535084529</td>
</tr>
<tr>
<td>21-28</td>
<td><code>0000000000000000</code></td>
<td>Quantity (high 8 bytes): 0</td>
</tr>
<tr>
<td>29-36</td>
<td><code>0000000000000000</code></td>
<td>Quantity (low 8 bytes): 0</td>
</tr>
<tr>
<td>37</td>
<td><code>00</code></td>
<td>Divisible: False</td>
</tr>
<tr>
<td>38</td>
<td><code>00</code></td>
<td>Lock/Reset flags</td>
</tr>
<tr>
<td>39-61</td>
<td><code>285341564520555220534f554c2046524f4d2053494e20</code></td>
<td>Description (part 1): "(SAVE UR SOUL FROM SIN " (23 bytes)</td>
</tr>
</tbody>
</table>
<p><strong>Second P2MS output (62 bytes):</strong></p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>62</td>
<td><code>1a</code></td>
<td>Length prefix: 26 bytes follow</td>
</tr>
<tr>
<td>63-70</td>
<td><code>434e545250525459</code></td>
<td><code>CNTRPRTY</code> prefix (repeated)</td>
</tr>
<tr>
<td>71-88</td>
<td><code>262049545320434f4e53455155454e434553</code></td>
<td>Description (part 2): "&#x26; ITS CONSEQUENCES" (18 bytes)</td>
</tr>
<tr>
<td>89-123</td>
<td><code>00...</code></td>
<td>Null padding (35 bytes)</td>
</tr>
</tbody>
</table>
<p>The complete Counterparty message is reconstructed by reading each length-prefixed segment, verifying the <code>CNTRPRTY</code> header, and concatenating the following data.
This structure allows messages to span multiple P2MS outputs while maintaining independent validation of each segment.
Associated with this transaction is a vanity address, <a href="https://mempool.space/address/1SaLvationGodsMarveLousGracaLgYQS"><code>1SaLvationGodsMarveLousGracaLgYQS</code></a>, which is the first transaction output.</p>
<p>And, for sake of completeness, here's some links to <a href="https://tokenscan.io/tx/296339">this transaction</a> and <a href="https://tokenscan.io/asset/SALVATION">"asset" (SALVATION)</a>.</p>
<h2 id="omni-formerly-mastercoin">Omni (formerly Mastercoin)</h2>
<p>The Omni Protocol, originally known as Mastercoin, was the pioneering protocol for building a layer on top of Bitcoin - it predated Counterparty!
Created by J.R. Willett and detailed in his January 2012 whitepaper <a href="https://bitcointalk.org/index.php?topic=56901.0">"The Second Bitcoin Whitepaper"</a>, Mastercoin sought to extend Bitcoin's functionality to enable more complex financial instruments and smart property without requiring any modifications to Bitcoin itself.</p>
<p>The project went live in August 2013 via the Mastercoin Crowdsale with MSC tokens generated during the month of August 2013 when individuals sent bitcoin to the Mastercoin (vanity) "Exodus Address": <a href="https://mempool.space/address/1EXoDusjGwvnjZUyKkxZ4UHEf77z6A5S4P"><code>1EXoDusj...</code></a>.
To date, 6,674.36781069 BTC have been sent to this address.</p>
<p>Mastercoin had a somewhat rocky history, check out the June 2014 Forbes article <a href="https://www.forbes.com/sites/kashmirhill/2014/06/03/mastercoin-maidsafe-crowdsale/">The First 'Bitcoin 2.0' Crowd Sale Was A Wildly Successful $7 Million Disaster</a> for some early context, but despite this, it has had a lasting impact on Bitcoin.
Most prominently Tether (USDT) <a href="https://en.wikipedia.org/wiki/Tether_(cryptocurrency)">launched on the Mastercoin protocol as "Realcoin"</a> and, for years, billions of dollars worth of USDT transactions were embedded in Bitcoin's blockchain via Mastercoin/Omni transactions.
Mastercoin was re-branded to Omni in 2015, likely to <a href="https://en.cryptonomist.ch/2024/04/17/mastercoin-crypto-the-story-of-the-communication-protocol-based-on-bitcoin-which-later-became-omni/">cast off some of the negative connotations associated with the Mastercoin project</a>.</p>
<h3 id="embedding-omni-transactions-in-bitcoin">Embedding Omni transactions in Bitcoin</h3>
<p>The Omni protocol specifies three different ways to embed data in the Bitcoin blockchain:</p>
<ol>
<li>Class A transactions - use fake addresses</li>
<li>Class B transactions - use multi-signature transactions</li>
<li>Class C transactions - use <code>OP_RETURN</code> such that Omni Protocol data is completely prunable.
This class was introduced with re-introduction of <code>OP_RETURN</code> in Bitcoin Core in version 0.9.0 (March 2014).</li>
</ol>
<p>The focus here is then on Class B Omni transactions, which are still used to this day for large transactions that do not fit within the (previous) default <code>OP_RETURN</code> policy limit specified by a majority of the network (e.g., 80 bytes).</p>
<p>According to the <a href="https://github.com/OmniLayer/spec/blob/master/OmniSpecification.adoc#64-class-b-transactions-multisig-method">Omni specification</a>, the first pubkey in each Omni transaction P2MS transaction output <em>"should be a valid public key address designated by the sender which may be used to reclaim the bitcoin assigned to the output"</em>.
The remaining key (in a 1-of-2) or keys (in a 1-of-3) must be compressed public keys, where each 33-byte compressed public key encapsulates an Omni Protocol packet.</p>
<p>After stripping the first and last bytes, the first byte of each 31-byte "packet", is a sequence number which is used to order the packets.
This can range between 1 and 255, which implies:</p>
<ul>
<li>There's 30 usable bytes per-packet (per data key) for Omni Protocol transaction data</li>
<li>There's 255 x 30 = 7,650 bytes maximum data storage capacity for each Class B Omni transaction</li>
</ul>
<p>To encode data in each packet, the sender's address is used, where the sender's address is the address that contributed the most input value.
The sender's address must be a P2PKH address, and:</p>
<blockquote>
<p><em>"Obfuscation is performed by SHA256 hashing the sender's address S times (where S is the sequence number) and taking the first 31 bytes of the resulting hash and XORing with the 31-byte Omni packet.</em>
<em>Multiple SHA256 passes are performed against an uppercase hex representation of the previous hash."</em></p>
</blockquote>
<h3 id="omni---single-packet">Omni - Single Packet</h3>
<p><strong>EXAMPLE: Omni - Single Packet</strong></p>
<p><a href="https://mempool.space/tx/0000297bd516c501aa9b143a5eac8adaf457fa78431e844092a7112815411d03"><code>0000297b...</code></a> is an Omni transaction with a single, 1-of-2 P2MS output in block <a href="https://mempool.space/block/00000000000000000f8cd15fe9923051dd231eb46e09c6e082dd859678df8eba">323,250</a>.
It has an uncompressed (65-byte) key as the first pubkey, and a compressed (33-byte) key as the second pubkey.
The first pubkey is the valid pubkey, so this (currently unspent) P2MS output of 5,678 sats could presumably be spent at some point.
The second pubkey encapsulates the single Omni Protocol packet for this Omni transaction.</p>
<p>There are 7 transaction inputs, all are contributed by the <a href="https://mempool.space/address/1MaStErt4XsYHPwfrN9TpgdURLhHTdMenH"><code>1MaStErt4XsYHPwfrN9TpgdURLhHTdMenH</code></a> address, so this is the sender's address.</p>
<p>To decode, we take the sender's address, apply SHA256 once, and obtain the first 31 bytes of the hash result:</p>
<p><code>2c7f68ac457d834fb57a112f3571d63bb6365d4c0523f9f74b298096160a02</code></p>
<p>Taking the second, data carrying pubkey and stripping the first and last bytes yields:</p>
<p><code>2d7f68ac457d834fb67a112f3571da0eb6365d4c0523f9f74b298096160a02</code></p>
<p>XORing these two 31-byte strings results in:</p>
<p><code>01000000000000000300000000000c35000000000000000000000000000000</code></p>
<p>This breaks down to:</p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>01</code></td>
<td>Packet sequence number: 1</td>
</tr>
<tr>
<td>1-2</td>
<td><code>0000</code></td>
<td>Transaction version: 0</td>
</tr>
<tr>
<td>3-4</td>
<td><code>0000</code></td>
<td>Transaction type: 0 (simple send)</td>
</tr>
<tr>
<td>5-8</td>
<td><code>00000003</code></td>
<td>Currency ID: 3 (MaidSafeCoin)</td>
</tr>
<tr>
<td>9-16</td>
<td><code>00000000000c3500</code></td>
<td>Amount: 800,000</td>
</tr>
<tr>
<td>17-30</td>
<td><code>00000000000000000000000000000000</code></td>
<td>Padding (unused)</td>
</tr>
</tbody>
</table>
<p><a href="https://omniexplorer.info/properties/production">This is the official list</a> for the mapping between Currency ID value and name.</p>
<h3 id="omni---multi-packet">Omni - Multi Packet</h3>
<p><strong>EXAMPLE: Omni - Multi Packet</strong></p>
<p>A <em>very</em> recent (25 June 2025!) example of an Omni transaction involving multiple P2MS transaction outputs, and thus multiple Omni packets, is <a href="https://mempool.space/tx/153091863886921ab8bf6a7cc17ea99610795522f48b1824d2e417954e466281"><code>15309186...</code></a>.</p>
<p>The main difference from the single packet example above is that for each subsequent pubkey, we need to apply <code>SHA256</code> once more to the sender's address.
So with a sender's address of <code>1D6oYjFVRAETW1Us9oS36YC71gfRw1omZB</code>, and there being 6 P2MS transaction outputs (the first 5 are 1-of-3, the last is a 1-of-2), we have 11 full 32-byte SHA256 results, with the index indicating the number of SHA256 rounds (remembering to convert to uppercase hex representation before each round):</p>
<ol>
<li>x<code>573a09227032f2dde9d2ecf3ffd930d69276754c3bc4343b01c14e0515741fa0</code></li>
<li><code>bc9197079fb1e344370ab8ee984291f1a3721b37901484dfb2079a158adc5e30</code></li>
<li><code>5439114b4688ba1e4e88ecb1454e7b147c886979b2e817082188b083d21fd871</code></li>
<li><code>cba80dd14016ec8be4cb466affa23d09f2418a361dbad0b3cebffcca473a6e90</code></li>
<li><code>834bdf87e79fd15dd54190a478e4f6e9a82a65e5e1220368405099064ad75dfd</code></li>
<li><code>aa5cfb1d38ad07a94a6986d4101082b673465f528c50efd8710bcf5ace2114dd</code></li>
<li><code>aafcd59c4c16f69343db9aac1091e0aa0b672efc66272b74870e84207986c9f5</code></li>
<li><code>9fb030e442bdfd7ee7cd0c2fbbd801381cb49f04ba11bc1c3ab68a435f8b5806</code></li>
<li><code>43fdd5998b69475c6416789a441cb8b897417a8b55cc0e684fe43e295f682bd7</code></li>
<li><code>a1c0f8d8c283565b31db64d205b403fcd96fbb62a6abe25c60a7bb57e751028e</code></li>
<li><code>b2573c41dd7e13da76fa530f1f48a3a1df4217b362c59a8bcead0135905ac09b</code></li>
</ol>
<p>Dropping the last byte of each hash, and then XORing with the first-and-last-byte stripped compressed keys yields the following.
We can see that the data has been successfully deobfuscated as the leading byte, the sequence number, ranges from 1 (<code>01</code>) through 11 (<code>0b</code>).</p>
<ol>
<li><code>010000003202000200000000456475636174696f6e004f7468657200416e75</code></li>
<li><code>02436f696e0068747470733a2f2f75756367612e636f6d2f616e75636f696e</code></li>
<li><code>037768697465706170657200416e75436f696e206973206120736163726564</code></li>
<li><code>042063757272656e637920666f7220706c616e657461727920726562697274</code></li>
<li><code>05682c206261636b656420627920746865204b756b756c6b616e20436f6465</code></li>
<li><code>06782e204974206272696467657320736f756c20736f7665726569676e7479</code></li>
<li><code>072c20626c6f636b636861696e20696e746567726974792c20616e6420636f</code></li>
<li><code>08736d69632072656d656d6272616e636520666f7220616c6c206265696e67</code></li>
<li><code>09732077616c6b696e67207468652070617468206f66207370697269747561</code></li>
<li><code>0a6c206c6967687420616e642067616c616374696320416e756e6e616b6920</code></li>
<li><code>0b656e6c69676874656e6d656e742e00000000035a4e900000000000000000</code></li>
</ol>
<p>Removing sequence numbers, but still accounting for original byte position in the first packet, this breaks down to:</p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>1-2</td>
<td><code>0000</code></td>
<td>Transaction version: 0</td>
</tr>
<tr>
<td>3-4</td>
<td><code>0032</code></td>
<td>Transaction type: 50 (create fixed property)</td>
</tr>
<tr>
<td>5</td>
<td><code>02</code></td>
<td>Ecosystem: 2 (Test Omni)</td>
</tr>
<tr>
<td>6-7</td>
<td><code>0002</code></td>
<td>Property type: 2 (new divisible currency)</td>
</tr>
<tr>
<td>8-11</td>
<td><code>00000000</code></td>
<td>Previous property ID: 0 (new smart property)</td>
</tr>
<tr>
<td>12-20</td>
<td><code>456475636174696f6e</code></td>
<td>Property Category: "Education"</td>
</tr>
<tr>
<td>21</td>
<td><code>00</code></td>
<td>Null terminator for previous string</td>
</tr>
<tr>
<td>22-26</td>
<td><code>4f74686572</code></td>
<td>Property Subcategory: "Other"</td>
</tr>
<tr>
<td>27</td>
<td><code>00</code></td>
<td>Null terminator for previous string</td>
</tr>
</tbody>
</table>
<p>Continuing with the last 3 bytes (28-30, <code>416e75</code>) concatenated to the second packet (sans sequence number):</p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>28-30, 1-4</td>
<td><code>416e75</code>+<code>436f696e</code></td>
<td>Property Name: "AnuCoin"</td>
</tr>
<tr>
<td>5</td>
<td><code>00</code></td>
<td>Null terminator for previous string</td>
</tr>
</tbody>
</table>
<p>Next up is the Property URL, which is the remainder of the second packet (6-30) and into the third packet (1-10).
We reach the end when we hit the null terminator <code>00</code> (byte 11).</p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>6-30, 1-10</td>
<td><code>68747470...61706572</code></td>
<td>Property URL: "<a href="https://uucga.com/anucoinwhitepaper">https://uucga.com/anucoinwhitepaper</a>"</td>
</tr>
</tbody>
</table>
<p>This is followed by Property Data, which extends from packet 3 through to packet 11, ending at the null terminator (byte 15):</p>
<table>
<thead>
<tr>
<th>Byte Position</th>
<th>Hex</th>
<th>Interpretation</th>
</tr>
</thead>
<tbody>
<tr>
<td>Many</td>
<td><code>416e7543...656e742e</code></td>
<td>Property Data: "AnuCoin is a sacred currency for planetary rebirth, backed by the Kukulkan Codex. It bridges soul sovereignty, blockchain integrity, and cosmic remembrance for all beings walking the path of spiritual light and galactic Anunnaki enlightenment."</td>
</tr>
<tr>
<td>16-23</td>
<td><code>000000035a4e9000</code></td>
<td>Number of coins: 14,400,000,000</td>
</tr>
</tbody>
</table>
<p>As per the <a href="https://github.com/OmniLayer/spec/blob/master/OmniSpecification.adoc#field-number-of-coins">Omni spec for the number of coins</a> <em>"for divisible coins or tokens, the value in this field is to be divided by 100,000,000"</em>, so there are actually only 144 units of this "AnuCoin".
And the sender was perhaps very impatient for the world to hear about "AnuCoin", significantly over-paying to get the transaction confirmed quickly:</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/mempool.space-p2ms-anucoin.png" alt="Figure 7: 200,000 sat transaction fee for AnuCoin."></p>
<p>Again, by no means being any form of endorsement, <a href="https://omniexplorer.info/asset/2147484218">here are full details</a> relating to this property/asset on Omni.</p>
<h2 id="other-variants-or-protocols">Other variants or protocols</h2>
<p>As documented in <a href="./p2ms-data-carry-2">Part 2</a>, Bitcoin Stamps, Counterparty and Omni are the dominant protocols that utilise P2MS outputs for data carrying purposes, combined accounting for over 98% of P2MS outputs in the UTXO set.
Yet there are other approaches and minor protocols that also use P2MS outputs to carry data:</p>
<ol>
<li><strong>Minor standalone protocols</strong>: Protocols with their own identifiers embedded in P2MS data: PPk (PPkPub), Chancecoin (<code>CHANCECO</code>), <code>TB0001</code>, <code>TEST01</code>, <code>METROXMN</code></li>
<li><strong>Hybrid OP_RETURN signalling</strong>: Protocols that use <code>OP_RETURN</code> for identification alongside P2MS for data storage</li>
<li><strong>Generic data storage</strong>: Direct data embedding without protocol layers</li>
</ol>
<p>These other identifier patterns or protocols can often be simply identified by ASCII interpretation of the P2MS key data, or data of other outputs (<code>OP_RETURN</code>), though sometimes they have binary formats for messages.</p>
<h3 id="ppk-ppkpub">PPk (PPkPub)</h3>
<p>PPk (PPkPub) was a blockchain infrastructure protocol developed by researchers at Beijing University of Posts and Telecommunications between 2016-2019, designed to create a decentralized naming and identity system built on Bitcoin (see <a href="https://github.com/ppkpub/docs/tree/master/English">English documentation</a>).
The protocol appears to have been abandoned since 2019.</p>
<p>The protocol used so-called ODIN (Open Data Index Name) identifiers to reference resources stored in Bitcoin transactions.
The naming structure follows the format <code>ppk:[BTC_BLOCK_HEIGHT].[BTC_TRANS_INDEX]/[DSS]</code>.
For example, <code>ppk:426896.1290/message.txt</code> points to TXID <a href="https://mempool.space/tx/a7fcc7391e2db0fe13b3a12d37fdbdc6138b2c76a9a447020fa92071a64dfe0c"><code>a7fcc739...</code></a> at position 1290 in block <a href="https://mempool.space/block/0000000000000000016b4c3097e5cdef14379670e126dc8aab6a9054ba1af499">426,896</a>, with <code>message.txt</code> as the Data Source Suffix (DSS), which is basically an (optional) resource path.</p>
<p>So given <code>ppk:426896.1290/message.txt</code>, anyone can locate block 426,896, find transaction at position 1290, detect the PPk marker, extract the payload, and decode the message.
This design leverages Bitcoin "blockchain coordinates" (block height, transaction index within block) as a naming system: globally unique (no collision risk), deterministic, independently verifiable (anyone can reconstruct it), decentralised (no central registry), and immutable (unchanging once mined).</p>
<p>ODINs are retroactive identifiers constructed from these "blockchain coordinates" after mining (not embedded beforehand).
When created, PPk transactions contain only the JSON data payload, in the form of title registration (<code>RT</code>), registration information, or message text, split across P2MS and <code>OP_RETURN</code> outputs, plus a distinctive marker pubkey (<code>0320a0de...3e12</code>) at position 2 in the P2MS output.
No block height, transaction index, or ODIN appears in the transaction itself.</p>
<p>Decoders construct the ODIN deterministically by detecting the marker pubkey, extracting the payload from P2MS and <code>OP_RETURN</code> outputs, inferring the resource path from payload type (RT becomes <code>profile.json</code>, registration data becomes, for example, <code>reg_315.txt</code>, messages become <code>message.txt</code>), looking up "blockchain coordinates", and constructing <code>ppk:[height].[index]/[path]</code>.
Data is unobfuscated and directly readable and both 1-of-2 and 1-of-3 multisig configurations are used.</p>
<h3 id="chancecoin">Chancecoin</h3>
<p>Chancecoin was a gambling protocol built on Bitcoin, only active for a short period in 2014, that used P2MS outputs for data carrying purposes in some circumstances.
Like Counterparty, users were required to send bitcoin to a specific address, between certain dates, to obtain "Chancecoin tokens".
In this case the address was <a href="https://mempool.space/address/1ChancecoinXXXXXXXXXXXXXXXXXZELUFD"><code>1ChancecoinXXXXXXXXXXXXXXXXXZELUFD</code></a>.
To date, 480.19571581 BTC have been sent to it.</p>
<p>Key characteristics:</p>
<ul>
<li><code>CHANCECO</code> identifier present in ASCII interpretation, data is not obfuscated</li>
<li>Uses 1-of-2 P2MS outputs, data is in the 2nd pubkey, the 1st is a valid pubkey</li>
<li>Each Chancecoin P2MS output has 32 bytes data carrying capability</li>
<li>Large Chancecoin messages are split across multiple P2MS outputs</li>
<li>Message format: [<code>CHANCECO</code>:8][MessageID:4][Data:variable]</li>
</ul>
<h3 id="ascii-identifier-patterns">ASCII identifier patterns</h3>
<p>Other identifier patterns that have been observed in P2MS outputs in the UTXO set by simple ASCII decoding include <code>TB0001</code>, <code>TEST01</code>, <code>METROXMN</code>.
There doesn't appear to be much information online about these identifiers, with the exception of <code>METROXMN</code> which is associated with <a href="https://bitcointalk.org/index.php?topic=974486.0">Metronotes XMN</a>, which unequivocally appears to be a scam.</p>
<p>Although the identifiers are not obfuscated, the data that follows is likely representing some form of protocol or message format like the other data carrying methods.
In the <a href="https://github.com/deadmanoz/data-carry-research">companion repository</a>, unlike the other protocols discussed here, no attempt has been made to interpret or decode the data beyond the identifier.</p>
<h3 id="op_return-signalling">OP_RETURN signalling</h3>
<p>Some protocols employ a hybrid approach, using <code>OP_RETURN</code> outputs to signal or identify the protocol, while simultaneously using P2MS outputs to carry the actual data payload.
This dual-output pattern provides explicit protocol identification through the <code>OP_RETURN</code> marker, while the larger data carrying capacity of P2MS outputs is used for the message content.
Three distinct variants have been identified in the UTXO set using this approach:</p>
<ul>
<li>"Protocol 47930" (<code>0xbb3a</code> marker) uses a 2-byte <code>OP_RETURN</code> prefix (<code>0xbb3a</code>) alongside 2-of-2 multisig outputs.</li>
<li>"RT Protocol" employs an ASCII <code>RT</code> identifier in its <code>OP_RETURN</code> output, paired with 1-of-2 multisig data storage.</li>
<li>"CLIPPERZ" is a password manager service that used Bitcoin's blockchain for encrypted data backup and notarization, creating <code>OP_RETURN</code> outputs containing <code>CLIPPERZ</code> alongside 2-of-2 multisig outputs.
The <code>OP_RETURN</code> serves as an explicit protocol declaration, avoiding ambiguity in classification, while the P2MS outputs function as the primary data carrier.</li>
</ul>
<h2 id="generic-data-storage">Generic Data Storage</h2>
<p>Data carrying in P2MS outputs need not rely on some standardised protocol and indeed there are many files (documents, images, etc.) and text, encoded, with or without any obfuscation, into P2MS pubkeys.</p>
<p>There are many examples of using P2MS outputs for generic data storage, but perhaps the most famous two are:</p>
<ol>
<li>The Bitcoin Whitepaper PDF - a single transaction</li>
<li>The Wikileaks Cablegate data - uses multiple transactions</li>
</ol>
<p>Both of these examples, and many other instances of Bitcoin transaction data being used for generic data storage are very well documented by Ken Shirriff in <a href="http://www.righto.com/2014/02/ascii-bernanke-wikileaks-photographs.html">Hidden surprises in the Bitcoin blockchain and how they are stored: Nelson Mandela, Wikileaks, photos, and Python software</a> and Ciro Santilli in <a href="https://cirosantilli.com/cool-data-embedded-in-the-bitcoin-blockchain">Cool data embedded in the Bitcoin blockchain: Ciro's Bitcoin Inscription Museum</a>.
For the purposes of understanding how P2MS outputs have been used for generic data storage, we'll examine how we can extract the Bitcoin Whitepaper PDF.</p>
<h3 id="the-bitcoin-whitepaper-pdf">The Bitcoin whitepaper PDF</h3>
<p>The following works through the process of extracting the Bitcoin whitepaper PDF from the transaction <a href="https://mempool.space/tx/54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713"><code>54e48e5f...</code></a> in block 230,009 (April 2013).
Note that this is also documented in various places online, including some elegant one-liners on <a href="https://bitcoin.stackexchange.com/questions/35959/how-is-the-whitepaper-decoded-from-the-blockchain-tx-with-1000x-m-of-n-multisi">Bitcoin Stack Exchange</a></p>
<p><strong>EXAMPLE: Generic Data Storage - The Bitcoin Whitepaper PDF</strong></p>
<p>One of the most famous examples of data embedding in Bitcoin is the Bitcoin whitepaper PDF embedded in transaction <a href="https://mempool.space/tx/54e48e5f5c656b26c3bca14a8c95aa583d07ebe84dde3b7dd4a78f4e4186e713"><code>54e48e5f...</code></a>.
This transaction has 948 outputs, 946 of which are 1-of-3 P2MS outputs, with the remaining two outputs being standard P2PKH outputs.
All pubkeys involved are also 65-byte uncompressed pubkeys, presumably to maximise data carrying capacity.
Unlike the protocol-based approaches we've examined (Bitcoin Stamps, Counterparty, Omni), this is a straightforward generic data storage example with no obfuscation or encryption.</p>
<p>Examining the first output (<code>vout[0]</code>) as in Figure 8 we can see that the 1-of-3 P2MS output uses full 65-byte uncompressed pubkeys.
With uncompressed pubkeys, typically starting with an <code>04</code> prefix, yet none of these keys starting with <code>04</code>, we know that none of these keys are valid pubkeys, and thus this P2MS output is unspendable.
If we were to examine the remaining 945 P2MS outputs, we would see the same pattern: all 3 keys in each output are uncompressed pubkeys, and none of which are valid pubkeys.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/mempool.space-p2ms-whitepaper-1.png" alt="Figure 8: details of first P2MS output of the Bitcoin whitepaper PDF transaction  (54e48e5f...)."></p>
<p>The extraction is simpler than protocol-based approaches:</p>
<p><strong>Step 1: Extract all pubkey data</strong></p>
<p>For each of the 946 P2MS outputs, concatenate all three 65-byte chunks:</p>
<ul>
<li>Output 0: <code>e4cf0200...</code> + <code>636f6465...</code> + <code>9ba54728...</code> = 195 bytes</li>
<li>Output 1: <code>f4eb5fde...</code> + <code>1f3fe4ab...</code> + <code>7395c3c8...</code> = 195 bytes</li>
<li>...continue for all 946 outputs</li>
<li>Total concatenated data: 946 × 195 = 184,470 bytes</li>
</ul>
<p><strong>Step 2: Detect the file type</strong></p>
<p>Scanning the concatenated data reveals the PDF magic bytes <code>%PDF</code> (hex: <code>25 50 44 46</code>) at a specific offset:</p>
<pre><code>Byte position 0-7:   e4 cf 02 00 06 7d af 13  (8-byte prefix)
Byte position 8-15:  25 50 44 46 2d 31 2e 34  (%PDF-1.4)
</code></pre>
<p><strong>Step 3: Localise the PDF content</strong></p>
<p>The PDF data is located at the following byte positions in the raw data:</p>
<ul>
<li><strong>Byte 8</strong>: <code>%PDF-1.4\n</code> - PDF header starts</li>
<li><strong>Bytes 184,294-184,298</strong>: <code>%%EOF</code> - End-of-file marker (5 bytes: hex <code>25 25 45 4f 46</code>)</li>
<li><strong>Byte 184,299</strong>: <code>\n</code> - Final newline after EOF (hex <code>0a</code>)</li>
<li><strong>Bytes 184,300-184,469</strong>: Null padding (170 bytes)</li>
</ul>
<p><strong>Step 4: Trim leading and trailing data, extract PDF</strong></p>
<ul>
<li><strong>Total bytes to remove</strong>: 178 bytes (8 byte prefix + 170 bytes null padding)</li>
<li><strong>Byte 0</strong>: <code>%PDF-1.4\n</code> - PDF header (8-byte prefix removed, now at position 0)</li>
<li><strong>Bytes 184,286-184,290</strong>: <code>%%EOF</code> - EOF marker (5 bytes)</li>
<li><strong>Byte 184,291</strong>: <code>\n</code> - Final newline (hex <code>0a</code>)</li>
<li><strong>Total size</strong>: 184,292 bytes</li>
</ul>
<p>The final extracted PDF is exactly <strong>184,292 bytes</strong> and can be saved as a valid PDF file.</p>
<p><img src="https://deadmanoz.xyz/assets/blog/p2ms-data-carry/p2ms-whitepaper-cover.png" alt="Figure 9: the first page of the extracted Bitcoin whitepaper PDF."></p>
<h2 id="summarising-the-main-techniques">Summarising the main techniques</h2>
<p>In the post we've explored the main methods of how data is carried in P2MS transaction outputs, including working through examples that showed how the various protocols or methods operate.
The dominant protocols, Bitcoin Stamps, Counterparty, and Omni, account for over 94% of all P2MS UTXOs (as we shall learn in <a href="./p2ms-data-carry-2">Part 2</a>), with the following summarising the key technical distinctions between them.</p>
<p><strong>Protocol identification</strong>: Bitcoin Stamps can often be identified without deobfuscation via Key Burn patterns, while Counterparty requires attempting deobfuscation to find the <code>CNTRPRTY</code> prefix.
Omni transactions are identified by the presence of the Exodus address as one of the transaction outputs.</p>
<p><strong>Obfuscation techniques</strong>: Bitcoin Stamps and Counterparty both use ARC4 stream cipher obfuscation with the input TXID as the key, whereas Omni uses SHA256 hashing with XOR operations (and generic data storage often uses no obfuscation at all, with the data embedded directly in pubkey).</p>
<p><strong>Spendability vs. permanence</strong>: Bitcoin Stamps deliberately creates unspendable outputs in using no real pubkeys, ensuring the data remains in the UTXO set forever.
Counterparty and Omni, by contrast, include a valid pubkey that allows each output to be spent, theoretically enabling UTXO set cleanup (though, in practice, many remain unspent years after being created).</p>
<p><strong>Data density &#x26; efficiency</strong>: All three protocols achieve roughly similar density per pubkey (30-31 usable bytes), though older non-ARC4 obfuscated Counterparty transactions use all 33 bytes of a 33-byte compressed pubkey.
The efficiency of the Counterparty protocol is lower than the other two due to per-output headers (losing 9 bytes per output), Omni achieves decent efficiency by using 30 of 31 bytes of each stripped pubkey (losing 1 byte per pubkey), and Bitcoin Stamps achieves the highest efficiency by using all bytes in 2nd and subsequent P2MS outputs for data.</p>
<table>
<thead>
<tr>
<th></th>
<th>Bitcoin Stamps</th>
<th>Counterparty</th>
<th>Omni</th>
</tr>
</thead>
<tbody>
<tr>
<td>Obfuscation method</td>
<td>ARC4</td>
<td>ARC4</td>
<td>SHA256+XOR</td>
</tr>
<tr>
<td>To deobfuscate</td>
<td>TXID of first TX input (<code>vin[0].txid</code>)</td>
<td>TXID of first TX input (<code>vin[0].txid</code>)</td>
<td>Sender's address (address contributing most value to input)</td>
</tr>
<tr>
<td>Identifier</td>
<td>Key Burn addresses (<code>0x0222...</code>, <code>0x0333...</code>, etc.)</td>
<td><code>CNTRPRTY</code> prefix (after deobfuscation)</td>
<td>Exodus address (<code>1EXoDusj...</code>) as output</td>
</tr>
<tr>
<td>P2MS configuration</td>
<td>1-of-3</td>
<td>1-of-2 or 1-of-3 (mostly)</td>
<td>1-of-2 or 1-of-3 (only)</td>
</tr>
<tr>
<td>Spendability</td>
<td>Unspendable - Key Burn and data keys only</td>
<td>Spendable - real pubkey present</td>
<td>Spendable - real pubkey present</td>
</tr>
<tr>
<td>Data per pubkey</td>
<td>31 bytes (33-byte compressed, strip first/last)</td>
<td>31 or 33 bytes (varies by variant)</td>
<td>30 bytes (31-byte packet minus sequence)</td>
</tr>
<tr>
<td>Data segment</td>
<td>Transport method dependent</td>
<td>Per P2MS output</td>
<td>Per pubkey</td>
</tr>
<tr>
<td>Carrying efficiency of multi-output</td>
<td>~100%</td>
<td>~85%</td>
<td>~97%</td>
</tr>
<tr>
<td>Active period</td>
<td>2023-present</td>
<td>2014-present</td>
<td>2013-present</td>
</tr>
</tbody>
</table>
<p><strong>Table 1:</strong> Comparison of the technical details of the major P2MS-using protocols.</p>
<p>Beyond these three major protocols, we also see P2MS transaction outputs leveraged by minor protocols.
PPk (PPkPub) uses a distinctive hybrid approach with a marker pubkey in P2MS outputs combined with protocol data in <code>OP_RETURN</code> outputs.
Other minor protocols include Chancecoin and projects with identifiers <code>TB0001</code>, <code>TEST01</code> and <code>METROXMN</code>.
Additional hybrid <code>OP_RETURN</code> + P2MS protocols exist, such as "Protocol 47930" (with <code>0xbb3a</code> marker) and <code>CLIPPERZ</code>.
In addition, P2MS transaction outputs have been used for generic data storage, with prominent examples including the Bitcoin whitepaper PDF and the Wikileaks Cablegate files.
Such examples demonstrate that P2MS can be used for arbitrary file storage without any standardised protocol layer.</p>
<h2 id="whats-next">What's next?</h2>
<p>These technical approaches each make different tradeoffs between efficiency, permanence, and network impact.
But understanding the mechanics is only part of the story - the critical question is: what is the actual scale and cumulative impact of these techniques on the Bitcoin network?
<a href="./p2ms-data-carry-2">Part 2</a> analyses historical trends and a snapshot of the UTXO set to quantify the magnitude of P2MS data carriage in Bitcoin.</p>
<p>Also be sure to check out the <a href="https://github.com/deadmanoz/data-carry-research">companion GitHub repo</a> which includes a <code>decode-txid</code> utility that can be used to decode transactions involving P2MS outputs according to various protocol described above (including some support for generic data storage).</p>
<h2 id="references">References</h2>
<h3 id="general">General</h3>
<ul>
<li><a href="https://github.com/deadmanoz/data-carry-research">deadmanoz Data Carry Research (GitHub companion repository)</a></li>
<li><a href="https://en.wikipedia.org/wiki/RC4">ARC4 (RC4)</a></li>
<li><a href="https://github.com/mikeinspace/stamps/blob/main/BitcoinStamps.md">Bitcoin Stamps Protocol Specification</a></li>
<li><a href="https://github.com/mikeinspace/stamps/blob/main/Key-Burn.md">Bitcoin Stamps: Key Burn</a></li>
<li><a href="https://github.com/stampchain-io/btc_stamps">Bitcoin Stamps Indexer</a></li>
<li><a href="https://github.com/stampchain-io/stamps_sdk">Bitcoin Stamps SDK Documentation</a></li>
<li><a href="https://docs.openstamp.io">OpenStamp</a></li>
<li><a href="https://research.binance.com/static/pdf/BRC-20%20Tokens%20-%20A%20Primer.pdf">BRC-20 Tokens: A Primer</a></li>
<li><a href="https://bitcointalk.org/index.php?topic=395761.0">Counterparty - Pioneering Peer-to-Peer Finance - Official Thread</a></li>
<li><a href="https://docs.counterparty.io/docs/advanced/protocol/">Counterparty Protocol Specification</a></li>
<li><a href="https://github.com/CounterpartyXCP/counterparty-core">Counterparty Core</a></li>
<li><a href="https://jpja.github.io/Electrum-Counterparty/decode_tx">Counterparty Decoder</a></li>
<li><a href="https://jpjanssen.com/how-to-reverse-engineer-counterparty-txs/">How to Reverse Engineer Counterparty TX’s</a></li>
<li><a href="https://bitcointalk.org/index.php?topic=265488.0">MasterCoin: New Protocol Layer Starting From “The Exodus Address”</a></li>
<li><a href="https://github.com/OmniLayer/spec/blob/master/OmniSpecification.adoc">Omni Layer Specification (0.7)</a></li>
<li><a href="https://www.forbes.com/sites/kashmirhill/2014/06/03/mastercoin-maidsafe-crowdsale/">The First 'Bitcoin 2.0' Crowd Sale Was A Wildly Successful $7 Million Disaster</a></li>
<li><a href="https://en.cryptonomist.ch/2024/04/17/mastercoin-crypto-the-story-of-the-communication-protocol-based-on-bitcoin-which-later-became-omni/">Mastercoin crypto: the story of the communication protocol based on Bitcoin, which later became Omni</a></li>
<li><a href="https://en.wikipedia.org/wiki/Tether_(cryptocurrency)">Tether (USDT) History</a></li>
<li><a href="http://www.righto.com/2014/02/ascii-bernanke-wikileaks-photographs.html">Hidden surprises in the Bitcoin blockchain and how they are stored: Nelson Mandela, Wikileaks, photos, and Python software</a></li>
<li><a href="https://cirosantilli.com/cool-data-embedded-in-the-bitcoin-blockchain">Cool data embedded in the Bitcoin blockchain: Ciro's Bitcoin Inscription Museum</a></li>
</ul>
<h3 id="academic-research">Academic Research</h3>
<ul>
<li><a href="https://digitalcommons.augustana.edu/cscfaculty/1/">Data Insertion in Bitcoin's Blockchain (2017)</a></li>
<li><a href="https://fc18.ifca.ai/preproceedings/6.pdf">A Quantitative Analysis of the Impact of Arbitrary Blockchain Content on Bitcoin (2018)</a></li>
<li><a href="https://www.researchgate.net/publication/323642931_Analysing_blockchains_and_smart_contracts_tools_and_techniques">Analysing blockchains and smart contracts: tools and techniques (2018, PhD thesis)</a></li>
<li><a href="https://iris.unica.it/bitstream/11584/261530/1/main.pdf">A journey into Bitcoin metadata (2019)</a></li>
<li><a href="https://www.blockchainresearchlab.org/wp-content/uploads/2020/03/BRL-Working-Paper-No-7-Dominating-OP-Returns.pdf">Dominating OP Returns: The Impact of Omni and Veriblock on Bitcoin (2020)</a></li>
<li><a href="https://www.researchgate.net/publication/351897792_An_Analysis_of_Data_Hidden_in_Bitcoin_Addresses">An Analysis of Data Hidden In Bitcoin Addresses (2021)</a></li>
<li><a href="https://arxiv.org/pdf/2503.14057">Bitcoin Burn Addresses: Unveiling the Permanent Losses and Their Underlying Causes (2025)</a></li>
</ul>
<h2 id="changelog">Changelog</h2>
<ul>
<li>2025-11-24: Added PPk</li>
</ul>]]></content:encoded>
            <category>bitcoin</category>
            <category>p2ms</category>
            <category>data-carry</category>
            <category>explainer</category>
            <category>research</category>
        </item>
    </channel>
</rss>