Skip to content

Instantly share code, notes, and snippets.

@instagibbs
Last active January 6, 2024 12:59
Show Gist options
  • Save instagibbs/f729399d571504d51d0143824f00098b to your computer and use it in GitHub Desktop.
Save instagibbs/f729399d571504d51d0143824f00098b to your computer and use it in GitHub Desktop.
Opt-in transaction policies for anti-pinning

"Free Relay"

In transaction relay we are trying to avoid "free relay" which is roughly defined as:

total_bytes_relayed / (total_sats_in_mempool + total_sats_mined) ?< minrelay

Did the sum total of bytes we've been accepting to our mempool and propagating across the network pay for the "bandwidth" to do so, dubbed minrelay which is a static value at 1 sat/vbyte?

Therefore, if we for example remove bip125 rule#3 in a naive fashion, this can be violated simply by filling the bottom of the mempool with 1 sat/vbyte junk, then double-spending all of with minimum sized transactions at slightly higher rates, e.g., 2 sat/vbyte.

This can be problematic in adversarial and time-sensitive scenarios such as Lightning Network tranasctions, both "commitment" and "HTLC" transactions which each have timelines to be mined safely. The counter-party can intentionally slow down mining of these transactions, and not pay for the privelage to do so, by putting these rule#3 pins at the bottom of the mempool that can become evicted.

Opt-in policies

Instead of working around rule#3 directly, we can instead allow wallets to add "prior restraint" to features of the transaction that may mitigate these issues.

"V3"

The "V3" transaction type is proposed specifically as this kind of policy. If nVersion==3, this means the topology for this transaction in the mempool is highly restricted. The tx may have up to one ancestor and one descendant total, which implies a strict upper-bound parent-child relationship of size two. The child transaction is additionally restricted to 1 kvB. This is deemed large enough that a child can bring reasonable amounts of funds to do package RBF, but two orders of magnitude smaller than the upper-bound possible (101kvB) in packages. This reduces the pin effect by roughly 250-500 times, making pins hopefully impractical.

Very importantly, the parent transaction size is unrestricted.

This transaction type works well specifically for things such as commitment transactions, in which utxos are locked into smart contracts between only the authorized spending parties, and no ANYONECANPAY like behavior is allowed. This precludes its usage in today's LN HTLC transactions, without a significant rewrite of how they work to include (Ephemeral?) anchors, and the resulting additional vbytes in the commone non-pinning case.

Ephemeral anchors additionally allow:

  1. 0-value anchors
  2. "sibling eviction", which allows other outputs of transactions to not require "1 CSV" hacks that are common in things such as LN BOLTs to avoid package limit pinning. This saves bytes, and allows these outputs to be directly spent immdaitely as fees. But relies on "V3" for its topological restrictions to ensure (2).

See more discussion on Ephemeral anchors and its relation to SIGHASH_GROUP fore more background: https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2023-January/021334.html

Cluster Mempool

With the cluster mempool, we have the ability to efficiently order the entire mempool, both in eviction and the mining. This can be efficiently updated incrementally for each transaction added and removed from the mempool. Since the mempool is completely ordered for mining at each step, we can efficiently simulate mining N blocks, and give the necessary "mining scores" required for each chunk to be included in that range of the mempool.

Transaction Priority Opt-in

Once you have a mapping from chunk mining score to "top N blocks", the system is fairly straight forward.

Users commit to the "N" in their transaction input(?). The proposed transaction is relayed, a node simulates adding the transaction to the cluster, runs linearization algorithm, has the resulting mining score, checks that the "N" computed is higher than any committed to in the transaction. If so, it's let into the mempool. If not, it's simply rejected.

Subsequent descendants to that tranasaction must also conform to the most restrictive ancestor's "top N block" chunk restriction, or they will be rejected. Therefore both "ancestor junk" and "descendant junk" are prevented.

The downside to this method is if the pinner somehow gets lucky when the mempool is empty, adds their top-of-mempool tx that is "too large", the mempool fills, then the initial transaction is now a pin. f the attacker can predict when a mempool will naturally fill, they could perhaps time this, but they risk getting mined, which defeats the purpose of the attack. I think in practice this is not a realistic concern, but requires further consideration.

Prior restraint via "V3" like means don't have the same issue, but the "V3" topological restrictions are also more weakly motivated from a wallet usage perspective, so it's something that has to be weighed.

Don't forget that lack of topological restrictions means batch CPFP is back on the table.

Takeaway

"V3" by itself may be subsumed by priority transactions, but in conjunction with Ephemeral Anchors still can make a lot of sense for:

  1. sibling eviction usage when the base transaction cannot be directly conflicted
  2. and when signers are committing to multiple "state" outputs and don't want to "sap" value from the smart contract itself (mixing funds and fees).

Bonus? SIGHASH_GROUP

This method seems to naturally slot into SIGHASH_GROUP anti-pinning. A transaction input commits to the "top N blocks" policy, and the rest of the constructed transaction is otherwise unrestricted.

Alternatively, restricting based on "max size of cluster" means you're putting prior restraint on yourself with respect to BYOF, batched bumps, etc. Similar to V3 constraints.

Thought experiment: Are "V3" and Epehemeral anchors use-cases entirely subsumed by SIGHASH_GROUP + priority?

@darosior
Copy link

darosior commented Jan 6, 2024

checks that the "N" computed is higher than any committed to in the transaction

nit: is lower, right? It's okay to commit to 5 and be in the top 3 blocks i presume.

the mempool fills, then the initial transaction is now a pin

We could also imagine for this case a replace-by-feerate for top-of-mempool transactions but i'm still not convinced it wouldn't allow for too much free relay. Hmm actually maybe it could work if the replaced transaction did commit to top-N block. It means an attacker can't simply RBF from the bottom of the mempool. Yeah this seems sensible:

  • If the to-be-replaced transaction committed to be in top-N blocks
  • If the to-be-replaced transaction isn't in top-N blocks anymore, not even top-N*x
  • If the replacement transaction commits to be in top-N blocks
  • Then the replacement is accepted

I think in practice this is not a realistic concern, but requires further consideration.

Yeah it's quite unlikely. But how much $-unlikely? Without getting in handwavy network-level attacks, If there is a lot of value at stake it doesn't seem too far-fetched to me for an attacker to wait for a real world event leading to a filled mempool. It doesn't cost them to wait for the next popular ordinal or BRC20 whatever mint to broadcast their large transaction right when it starts taking off.

The cost of the attacker is ~fixed: the fees. If the probability of success is non-null as the potential reward increases it becomes a winning strategy?

@darosior
Copy link

darosior commented Jan 6, 2024

To expand on why i think priority transactions reasonably bound free relay. Let's take N is 3 (priority txs commit to be in top-3 blocks) and x is 10 (replace by feerate is only possible for priority transactions which are 3 * 10 blocks deep or more).

The only way to get free relay here would be:

  • you broadcast an almost next block tx
  • somehow 30 blocks worth of higher-paying txs are broadcast
  • you replace it with a top-3 blocks tx

But repeating this is extremely unrealistic. Presumably one wouldn't pay for 30 worth of top-mempool feerate so you'd have to hope other users of the network would do it.

I'll think about how bad free relay can be in this case and edit this comment. My intuition would be the worst case is an empty mempool and a DoS attacker with deep enough pockets to continuously replace 30 blocks worth of priority transactions. Will give more thoughts.

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