Taxonomy of Transaction Fees in Smart Contracts

We’re going to take a look through many of the common fee tropes that wallets and smart contracts use and hopefully figure out a more common language in how we discuss these rather than just “CPFP vs RBF”. For mempool design, this is important as we should be looking to support what users are attempting to make, where reasonable, to avoid out-of-band fee solutions. This becomes even more important as people discuss the design characteristics of covenants proposals, and their potential Bitcoin-scaling benefits.

For this post, “smart contracts” can also simply be understood as logical transactions: people want some state change effectuated, and they may make otherwise-unrelated logical transactions to pay fees for them.

In the below diagrams, green arrows indicate where fees are “coming from”. Squares are bitcoin transactions, circles are outputs. Filled in orange implies the minimal set of transactions that require RBF, so 2+ would imply package RBF is required.

Endogenous fees, single transaction RBF

These bring fees from “inside” the logical transaction which also happens to be a single Bitcoin transaction.

flowchart TD
    Pc([contract]) --> P4:::rbf
    Pc --> Pi4
    Pi4[Pay to state y] <-.->|conflicts| P4[Pay to state x\nEndo fees, Single RBF]
    P4 --> Po4([state x balance minus fees])
    
    classDef rbf fill:#f96
    linkStyle 0 stroke-width:4px,stroke:green
    linkStyle 1 stroke-width:4px,stroke:green

This is the most basic setup, and includes things like simple wallet sends, up to non-anchor ln-penalty channels. They are “maximally” compact in that fee-sizing aside, the transaction itself is quite compact.

Exogenous fees, single transaction RBF

These bring fees from outside the smart contract, but contain it within a single Bitcoin transaction.

flowchart TD
    Pc([contract]) --> P5:::rbf
    Pc --> Pi5
    Pi5[Pay to state y] <-.->|conflicts| P5[Pay to state x\nExo fees, Single RBF]
    Pi5_2([fee input])--> P5
    P5 --> Po5([state x balance])
    P5 --> Co5([change])
    
    classDef rbf fill:#f96
    linkStyle 3 stroke-width:4px,stroke:green
    linkStyle 5 stroke-width:4px,stroke:green

Typically seen with SIGHASH_SINGLE | ANYONCANPAY arrangements.

Examples in LN spec HTLC presigned transactions, ln-symmetry “update” transactions, Murcery Wallet’s statechains, degen NFT trades, and likely more.

Keeping it within a single transaction saves a bit of overhead vs other strategies, but requires additional inputs and possibly an output to be added.

Without additional mitigations it can

Both single-transaction RBF types benefit from simplicity of the RBF case in today’s mempool and relay policies.

Endogenous fees, CPFP

Fees are brought from within the parent transaction, with no requirements to resolve a conflict from the parent’s input set.

flowchart TD
    Pi3([contract])
    Pi3 --> Pc[Pay to state y]
    Pc --> Po3([contract state x])
    Po3 --> C3[Endo fees, CPFP]:::rbf
    C3 --> Co3([state x balance minus fees])
    
    classDef rbf fill:#f96
    linkStyle 0 stroke-width:4px,stroke:green
    linkStyle 1 stroke-width:4px,stroke:green
    linkStyle 2 stroke-width:4px,stroke:green
    linkStyle 3 stroke-width:4px,stroke:green

This is most applicable to when the parent transaction is not under the control of the user making the CPFP, or when replacing it would be too expensive, disallowing RBF.

A common use-case would be users receiving a transaction from someone and the sender unwilling or unable to increase the fee themselves. ln-symmetry settlement transactions have all outputs freely spendable, also allowing endogenous spends. Another would be fee bumping an LN remote transaction using your own balance output in pre-anchor channels.

Exogenous fees, CPFP

Same use-cases as above, but perhaps some outputs in the smart contract are unable to be spent due to locktimes or other factors.

flowchart TD
    Pi([contract]) --> P[Pay to state x]
    P --> Po([state x])
    Po --> C[Exo fees, CPFP]:::rbf
    fee([fee input]) --> C
    C --> Co([change plus state x balance])
    
    classDef rbf fill:#f96
    linkStyle 3 stroke-width:4px,stroke:green
    linkStyle 4 stroke-width:4px,stroke:green

We see this pattern in today’s LN anchor channels which requires all fees to be exogenous to avoid pinning scenarios.

Endogenous fees, Package RBF

“Package RBF” is a term to describe the combination of CPFP and package RBF, where the child is paying for the parent’s conflict.

If the smart contract’s outputs are otherwise unencumbered, fees can be paid for endogenously.

flowchart TD
    Pi3([contract]) --> P3[Pay to state x]:::rbf
    Pi3 --> Pc[Pay to state y] <-.->|conflicts| P3
    P3 --> Po3([contract state x])
    Po3 --> C3[Endo fees, CPFP]:::rbf
    C3 --> Co3([state x balance minus fees])
    
    classDef rbf fill:#f96
    linkStyle 0 stroke-width:4px,stroke:green
    linkStyle 3 stroke-width:4px,stroke:green
    linkStyle 4 stroke-width:4px,stroke:green
    linkStyle 5 stroke-width:4px,stroke:green

Currently this is unavailable to Bitcoin’s various mempool implementations as this requires evaluation of entire packages, but users of this would be LN for commitment transactions plus fee paying child if outputs became otherwise unencumbered by a one block relative timelock.

Exogenous fees, Package RBF

flowchart TD
    Pi([contract]) --> P[Pay to state x]:::rbf
    Pi --> Pc[Pay to state y] <-.->|conflicts| P
    P --> Po([state x])
    Po --> C[Exo fees, CPFP]:::rbf
    fee([fee input]) --> C
    C --> Co([change plus state x balance])
    
    classDef rbf fill:#f96
    linkStyle 5 stroke-width:4px,stroke:green
    linkStyle 6 stroke-width:4px,stroke:green

Today’s LN anchor channels is the primary example.

Composeable Transaction Structures

Not all smart contract paradigms use the parent and child scheme.

For example, there are a number of schemes involving either pre-signed or CTV-encumbered transaction trees.

These transaction trees will end up fitting in these above buckets in different ways, depending on tradeoffs like usual:

  1. Are we able to relay and get package evaluation for the final package?
  2. Are the ultimate (“virtual”)UTXOs immediately spendable, allowing endogenous fees?
  3. Do we need sibling eviction to avoid cluster limits? How would it need to differ from v3-style sibling eviction?

Composing these ideas, you could imagine a Timeout Tree where the leaf nodes use Endogenous, single transaction RBF-compatible channels, and if users want to go to chain, it composes to endogenous fees paying for the CPFP, and exercising package RBF if sibling eviction becomes required or any ancestor input in the chain was conflicted. This unilateral exit could also be used to pay for a separate exogenously-fee-settled smart contract that has failed.

5 Likes

As fees rise, compactness will override all other concerns. (Including security!)

This implies that we’ll will use endogenous fees where possible, and single transaction which stacks as many otherwise-unrelated operations, with a single exogenous fee in/out.

We should be designing for this reality, when considering covenant constructions.

2 Likes

That seems unsound? “I want to save money, therefore I’ll make it easy for someone to steal all my money” If you really want to save on fees, and don’t mind security risks, you don’t go on-chain at all, you use a centralised database, or a more-centralised blockchain than bitcoin.

The risk that makes sense to me here is the one to decentralisation: you’re still going onchain, you’re still keeping your funds secured, but you’re paying fees out of band to the largest miners via a trusted relationship, ignoring the downside that, at scale, this makes smaller miners less profitable, and perhaps unsustainable.

(We’ve got evidence that people will abandon security when trying to claim BRC20 mints and the like, but that’s a lottery in the first place: if your tx isn’t amongst the first N to claim, you’re throwing your money away anyway)

I don’t think the decentralisation risk is avoidable though.

Consider the case where an LSP wants to unilaterally close a channel: then they will need to get a commitment tx on chain, but in a high/variable fee environment cannot rely on the commitment tx to have precommitted sufficient fee payment (for ln-symmetry/eltoo channels, they may not be able to precommit to any fee payment), and because it’s a unilateral close, can’t immediately claim any of their own channel balance to use that for fees.

One approach is to do what we do now: pull in fees from an existing utxo, and add a change address. That can be done via an anchor output and CPFP, or by the commitment tx only being signed with SINGLE|ANYONECANPAY, or by pre-preparing a utxo with the exact fee and an ALL|ANYONECANPAY signature.

But a large LSP that has their own mining operation has another option: they can just prioritise the tx directly, even if it has 0 fee. When that is mined, as a miner they’ll be forgoing fees so it’s not free, but that’s still cheaper: they’re only forgoing X vbytes worth of fees from the bottom of the block, where X is the size of the commitment without any additional fee inputs/change outputs, rather than paying X+Y bytes worth of fees at the top/middle of the block, where Y is the size of the additional fee inputs and change outputs (and any overhead).

In this scenario, the “single exogenous fee” doesn’t make it onchain at all (it’s an internal transfer from the LSP part of the business to the mining part of the business), but presumably all the LSP’s txs are still combined together into a single 0-fee tx at the start of the block.

Adding introspection logic isn’t needed for that setup, and would itself be unnecessary overhead.

With V3 txns and things like it wouldn’t LSPs and others just pay miners out-of-band to mine the tx and let the anchor output just go unspent? Doesn’t seem like a trusted relationship to me because you can fall back to the expensive V3 mechanism. I wouldn’t expect them to care about the UTXO set being polluted with anchors.

Why would they need a mining operation rather than use an accelerator like mempool.space/ViaBTC/Binance/etc?

It just made the example simpler. In both cases it’s centralisation: if it’s the LSP doing the mining, then that LSP can use the bitcoin more cheaply than a smaller LSP; if it’s a trusted accelerator paying miners/pools out of band, then that accelerator can require the LSPs or miners/pools to comply with their T&C before they’ll do business with them. Doesn’t matter in either case if it’s rare and for small amounts. Does matter if it’s common and a non-trivial fraction of block reward.

I found these diagrams a little confusing. Here’s how I think I would draw them; what do you think?

  • Rounded boxes are utxos
  • Square boxes are transactions
  • Green lines emphasise where fee/change amounts come from/go
  • Dashed lines just indicate conflicting txs (due to attempting to spend the same utxo) and are also labelled.
  • If RBFing a previous state, the minimal set of txs that justify the RBF are highlighted in orange. If only one tx is highlighted, it probably works today; if two txs are highlighted, you need to wait for package RBF to be able to do state conflicts.

Endogenous fees, single transaction RBF

flowchart TD
    Pc([contract]) --> P4:::rbf
    Pc --> Pi4
    Pi4[Pay to state y] <-.->|conflicts| P4[Pay to state x\nEndo fees, Single RBF]
    P4 --> Po4([state x balance minus fees])
    
    classDef rbf fill:#f96
    linkStyle 0 stroke-width:4px,stroke:green
    linkStyle 1 stroke-width:4px,stroke:green

Exogenous fees, single transaction RBF

flowchart TD
    Pc([contract]) --> P5:::rbf
    Pc --> Pi5
    Pi5[Pay to state y] <-.->|conflicts| P5[Pay to state x\nExo fees, Single RBF]
    Pi5_2([fee input])--> P5
    P5 --> Po5([state x balance])
    P5 --> Co5([change])
    
    classDef rbf fill:#f96
    linkStyle 3 stroke-width:4px,stroke:green
    linkStyle 5 stroke-width:4px,stroke:green

Endogenous fees, CPFP

This is the same as “endogenous fees, package rbf”, if you note that “Pay to state x” conflicts with some other tx “Pay to state y”, both spending “contract”

flowchart TD
    Pi3([contract]) --> P3[Pay to state x]:::rbf
    Pi3 --> Pc[Pay to state y] <-.->|conflicts| P3
    P3 --> Po3([contract state x])
    Po3 --> C3[Endo fees, CPFP]:::rbf
    C3 --> Co3([state x balance minus fees])
    
    classDef rbf fill:#f96
    linkStyle 0 stroke-width:4px,stroke:green
    linkStyle 3 stroke-width:4px,stroke:green
    linkStyle 4 stroke-width:4px,stroke:green
    linkStyle 5 stroke-width:4px,stroke:green

Exogenous fees, CPFP

Likewise

flowchart TD
    Pi([contract]) --> P[Pay to state x]:::rbf
    Pi --> Pc[Pay to state y] <-.->|conflicts| P
    P --> Po([state x])
    Po --> C[Exo fees, CPFP]:::rbf
    fee([fee input]) --> C
    C --> Co([change plus state x balance])
    
    classDef rbf fill:#f96
    linkStyle 5 stroke-width:4px,stroke:green
    linkStyle 6 stroke-width:4px,stroke:green

Just starting to read the original post; it’s not clear to me what “conflicted” with a dashed arrow means here.

I find these more confusing, but I might be alone here.

An input is detected as conflicting with something in mempool, making it an RBF

fwiw I found these diagrams easier to understand

1 Like

Haha, I suggested the same approach when @instagibbs drew these on the whiteboard here yesterday…

2 Likes

@ajtowns you’re on the hook for redrawing all of them now

1 Like

Sorry to necropost here. From the original OP:

… this is important as we should be looking to support what users are attempting to make, where reasonable, to avoid out-of-band fee solutions.

This conversation is largely around fee inputs (whether exogenous or endogenous). However, there may be another type of transaction fee which we may need to consider: explicit fee outputs.

I am referring to a fee paid to a miner where the fee is represented as a separate output in the transaction. Let us call such fee outputs “explicit” fees, as opposed to usual “implicit” fees ( = sum of inputs - sum of outputs).

Of course, transactions with explicit fee outputs take up more block space, so the explicit fee must account for the miner’s opportunity cost.

But why are out-of-band explicit-fee outputs bad? I think one reason which is often not considered by non-experts is that out-of-band explicit fee outputs are not, unless carefully constructed, subject to the coinbase maturity restrictions.

The coinbase maturity is actually a crucial component of Bitcoin’s security. We should naturally want as many of the block’s fee sats as possible flowing into those coinbase (or equivalently locked – see below) outputs.

I wonder whether, in a post-subsidy bitcoin world, or a world where the chain is being re-organized more than usual, users (especially receivers) may find themselves needing to adopt explicit anyone_can_spend fee outputs which emulate the coinbase maturity semantics. Maybe a simple output script of something like 100 op_csv.

In such a world, I suppose a receiver could use CPFP to attach such an output to their inbound transactions.

What particular problem are you solving with explicit fee outputs? I think Pay2Anchor and their legacy emulations P2SH(OP_TRUE) count as that, with money “free for the taking”, but in practice is burned to fees by a child transaction.

Thanks for your reply. It is not a problem yet, at least not so far as I am aware. I will try to illustrate a contrived example:

  1. Receiver uses CPFP to consume the Pay2Anchor and, rather than implicitly burning to fees, instead pays a miner directly via an explicit output.

  2. The reason the miner likes this is that she does not need to wait 100 blocks to spend those fees.

  3. The reason the Receiver likes this is that he gets his transaction confirmed faster.

I can imagine a world where the receiver just creates a bunch of these CPFP transactions and submits one to each mining pool’s accelerator.

The problem is that if blocks are full and if for some bizarre reason this becomes the “normal” way of doing things, I fear that the receiver will have a false sense of security.

I think it probably only becomes an issue, if ever, when total miner fees in this “explicit output” form significantly exceed the block subsidy and implicit fees for a given block.

Then, in that situation at block height H, miners of the blocks near the tip (blocks H-1, H-2, H-3, …,) may have more incentive to reorg than they do in our current circumstances.

This would not work to incentivize a miner to include your transaction, since they are not guaranteed to find the 100th block after the one containing your transaction. You would need more Script expressiveness to make the output first anyonecanspend but also “carry forward” this condition so it applies to the “second stage” spender. This logic is exactly what @salvatoshi’s OP_CCV enables: OP_CHECKCONTRACTVERIFY and its amount semantic.

Yes, you are of course correct for the example I gave. I should have made it more clear that there I was referring to the situation where a receiver submits a transaction to each miner (or mining pool) for acceleration and has in the transaction an explicit fee output paying that miner.

So the actual explicit output would have been something like (pseudocode) 100 op_csv <miner pubkey> op_checksigverify. Good catch!

The largest systemic drawback would be that this makes mining permissioned in the sense that fees cannot be obtained without wallets knowing about them (and giving them a copy)

1 Like

Exactly! Thankfully so long as the presence of a robust market for implicit fees exists and such implicit fees outweigh what could be obtained by this strange permissioned explicit fee stuff, then we hopefully have nothing to worry about.

For an (albeit hopefully very unlikely!) world where blocks are completely full most of the time and where the subsidy + implicit_fees is much less than the revenue available from these (permissioned) explicit_fees, I merely wanted to raise it here as a possible edge case with a potential adverse side-effect.