Transaction Stacking for Covenant Fee Minimization
As I explore the use of Bitcoin Script for introspection, I am not overly concerned with total script size, because if common usage patterns emerge those can be soft-forked into new opcodes, or new address types. But I have been concerned with constructions which require the use of Child-Pays-For-Parent for fee paying, as that makes the transaction significantly more expensive than using inline fees and Replace-By-Fee.
Lightning uses this kind of “anchor” construction, and although it’s only used in the forced-closure case, it’s wasteful of onchain space when it happens. It also uses a “bring your own fee” construction for HTLC transactions, using SIGHASH_SINGLE|SIGHASH_ANYONECANPAY
which means only the input and outputs are fixed, and the operation of this is much smoother in general.
(It’s not coincidence that my main contribution to the Eltoo construction was to use a similar single-input/output scheme to allow such last-minute fee binding and RBF).
More recently, Peter Todd argues that such inefficient fee bumping is a threat to decentralization as it creates significant incentive to use out-of-band fees, which would have to be paid in advance and thus would favor large miners.
Stacking Transactions: Adding Fees Later
If you carefully construct your covenant to allow addition of a fee input (and usually a change output) later, you can avoid the expense of a child transaction and put the fees inline.
If you’re really clever, you can combine multiple covenant transactions into one transaction, and add a fee input/change output to all of them at once and reduce total costs even more. I call this stacking, and my thesis is that Bitcoin fees will rise and eventually make such joining profitable, normal and necessary.
Note that such stacking requires real engineering work: we’ve seen how long it took Bitcoin exchanges to implement even simple batching! And for full disclosure: stacking like this is already possible with Lightning with anchor outputs and HTLC transactions, which are signed with SIGHASH_SINGLE|SIGHASH_ANYONECANPAY
, and yet I still haven’t implemented stacking in Core Lightning!
I now want to discuss the dangers of doing this incorrectly, and how OP_TXHASH
can support doing it in various scenarios.
Partial Validation Attacks: A Primer
I vaguely recall first learning of this attack in the context of signing devices, but I cannot find a reference. ~I’ll add one when some clever reader points it out!~ Greg Sanders’s post Hardware Wallet attacks by input ownership omission and fix though I may have cribbed it from Bitcoin OpTech (Greg he also mentioned jl2012 may have been involved).
Consider a transaction designed to take a 1BTC input and pay Bob 0.1BTC, with the remaining 0.9BTC going to a change address. Your software asks a signing device to sign the first input. It checks the input, checks the outputs are correct, prompts the user (telling it we’re paying Bob 0.1BTC) and signs it.
Now consider a transaction which has two identical inputs. Our naive signing device, asked to sign the first input, would see it as valid, and sign it. If we then ask it to sign the second input it would also see it as valid, and sign it. But the transaction would actually pay 1BTC to fees!
I call this a “Partial Validation Attack”, and the same problem can occur with stacking! In this case, it’s the covenant checking the input, not the hardware wallet. If it does not check other inputs (because it wants to allow you to add fees and/or stack other transactions together), and it would allow other covenants to validate the same outputs, it is vulnerable.
Partial Validation Exploit: A Covenant Example.
Imagine you want to create a covenant that forces a transaction to pay all its input amount to a given address, and you have OP_TXHASH
and OP_CAT
.
You want it to stack, so you simply validate that output #0 go to the given address, and that the amount match the input amount of the current input. This is fairly easy, you can either use OP_TXHASH
to get the hashed amount from output #0, and again from the input and compare, or require the output supply the amount on the stack, duplicate it and hash it, then call OP_TXHASH
to hash the output #0 amount and the current input amount, and make sure that’s what they provided.
Then when you want to spend it, you can pay fees by adding as many inputs (and outputs) as you need without invalidating the transaction.
Now, you create two 1BTC outputs to this covenant address. Mallory creates a transaction which spends both at once: it pays 1BTC to your required address (output #0) and sends the other 1BTC to their own address, stealing your funds. Both inputs’ covenants check that output #0 pays the full amount to the required address, and are satisfied. Oops!
Avoiding Partial Validation Issues When Stacking Transactions
This can avoided in one of four ways:
- Specify the entire transaction, CTV-style. But then you cannot add fees inline.
- Have each input examine all the other inputs. This is limited since there is no looping in Script.
- Insist the current input also be at index #0, so there can only be one.
- Use relative output addressing, so we examine the output corresponding to the current input.
Of these options, only the final one (relative output addressing) allows stacking, so obviously that’s my preferred approach.
Unfortunately, this stacking is only possible with current OP_TXHASH
if the number of inputs is equal to the number of outputs. This can often be arranged, but any shared UTXO arrangement results in multi-in-single-out and single-in-multi-out. Can we do better?
Stacking Odd-Shaped Transactions
We can imagine OP_TXHASH
supporting an indexing scheme which lets you refer to “output = input-number * N” (I proposed this as a possibility in my BIP review). (We can also imagine OP_TX
which would let you push the current input number on the stack directly, to do this calculation yourself!).
This would let us stack have several 1-input/2-output txs. But it wouldn’t let us stack different topologies, like a 1-in/2-out on top of a 2-in/1-out tx.
I considered adding an “output accumulator” where some OP_TXHASH field selector would increment the “next output” counter. But writing it up I realized that this fails in the presence of OP_SUCCESS
which can cause an input to be skipped; that would be a hard fork!
If we really want to do this in general, we would need to flag how many outputs each input “owns”, such as in the nSequence
field. And then have a “relative to owned outputs” modifier in OP_TXHASH
. As nSequence
bits are limited and this would irreversibly consume some, I am reluctant to propose this unless real world usage of covenants (i.e. after they’re enabled by a soft-fork) shows it would have real onchain benefits.
Side Note: Things That Don’t Work
You can imagine handing the output number(s) in the witness (and changing them when you stack the transactions), but that re-introduces the “partial transaction” bug. Similarly, providing multiple signatures for different stacking cases would expose you to the issue.
Summary
I believe stacking transactions is going to become popular to reduce fees: while this is currently easy for 1-input-1-output transactions, and the OP_TXHASH
proposal makes it possible for N-input-N-outputs, I suspect the N-inputs-1-output an 1-input-N-output cases will be common (shared UTXOs), so we should try to allow those. It would also be nice to design such that we can allow nSequence bits to indicate the number of associated outputs in a future soft fork.