Subject: [bitcoin-dev] OP_Expire and Coinbase-Like Behavior: Making HTLCs Safer by Letting Transactions Expire Safely

From: Peter Todd <[email-masked]>
Date: Sat, 21 Oct 2023 00:09:16 +0000

On Mon, Oct 16, 2023 at 05:57:36PM +0100, Antoine Riard via bitcoin-dev wrote:
> Here enter a replacement cycling attack. A malicious channel counterparty
> can broadcast its HTLC-preimage transaction with a higher absolute fee and
> higher feerate than the honest HTLC-timeout of the victim lightning node
> and triggers a replacement. Both for legacy and anchor output channels, a
> HTLC-preimage on a counterparty commitment transaction is malleable, i.e
> additional inputs or outputs can be added. The HTLC-preimage spends an
> unconfirmed and unrelated to the channel parent transaction M and conflicts
> its child.

The basic problem here is after the HTLC-timeout path becomes spendable, the
HTLC-preimage path remains spendable. That's bad, because in this case we want
spending the HTLC-preimage - if possible - to have an urgency attached to it to
ensure that it happens before the previous HTLC-timeout is mined.

So, why can't we make the HTLC-preimage path expire? Traditionally, we've tried
to ensure that transactions - once valid - remain valid forever. We do this
because we don't want transactions to become impossible to mine in the event of
a large reorganization.

A notable example of this design philosophy is seen in Bitcoin's rules around
coinbase outputs: they only become spendable after 100 more blocks have been
found; a 100 block reorg is quite unlikely.

Enter the OP_Expire and the Coinbase Bit soft-fork upgrade.

# Coinbase Bit

By redefining a bit of the nVersion field, eg the most significant bit, we can
apply coinbase-like txout handling to arbitrary transactions. Such a
transaction's outputs would be treated similarly to a coinbase transaction, and
would be spendable only after 100 more blocks had been mined. Due to this
requirement, these transactions will pose no greater risk to reorg safety than
the existing hazard of coinbase transactions themselves becoming invalid.

Note how such a transaction is non-standard right now, ensuring compatibility
with existing nodes in a soft-fork upgrade.

# OP_Expire

Redefining an existing OP_Nop opcode, OP_Expire would terminate script
evaluation with an error if:

1) the Coinbase Bit was not set; or
2) the stack is empty; or
3) the top item on the stack was >= the block height of the containing block

This is conceptually an AntiCheckLockTimeVerify: where CLTV _allows_ a txout to
become spendable in a particular way in the future, Expire _prevents_ a txout
from being spent in a particular way.

Since OP_Expire requires the Coinbase Bit to be set, the reorg security of
OP_Expire-using transactions is no worse than transactions spending miner
coinbases.

# How HTLC's Would Use OP_Expire

Whenever revealing the preimage on-chain is necessary to the secure functioning
of the HTLC-using protocol, we simply add an appropriate OP_Expire to the
pre-image branch of the script along the lines of:

    If
        <expiry height> Expire Drop
        Hash <digest> EqualVerify
        <pubkey> CheckSig
    ElseIf
        # HTLC Expiration conditions
        ...
    EndIf

Now the party receiving the pre-image has a deadline. Either they get a
transaction spending the preimage mined, notifying the other party via the
blockchain itself, or they fail to get the preimage mined in time, reverting
control to the other party who can spend the HTLC output at their leisure,
without strict time constraints.

Since the HTLC-expired branch does *not* execute OP_Expire, the transaction
spending the HTLC-expired branch does *not* need to set the Coinbase Bit. Thus
it can be spent in a perfectly normal transaction, without restrictions.

# Delta Encoding Expiration

Rather than having a specific Coinbase Bit, it may also be feasible to encode
the expiration height as a delta against a block-height nLockTime. In this
variant, OP_Expire would work similarly to OP_CheckLockTimeVerify, by checking
that the absolute expiration height was <= the requested expiration, allowing
multiple HTLC preimage outputs to be spent in one transaction.

If the top 16-bits were used, the maximum period a transaction could be valid
would be:

    2^16 blocks / 144 blocks/day = 455 days

In this variant, a non-zero expiration delta would enable expiration behavior,
as well as the coinbase-like output spending restriction. The remaining 16-bits
of nVersion would remain available for other meanings.

Similar to how CLTV and CSV verified nLockTime and nSequence respectively,
verifying an expiration height encoded in the nVersion has the advantage of
making an expiration height easy to detect without validating scripts.

While Lightning's HTLC-success transactions currently use nLockTime=0, AFAIK
there is no reason why they could not set nLockTime to be valid in the next
block, allowing delta encoding to be used.

## Reusing Time-Based nLockTime

Reusing time-based nLockTime's prior to some pre-2009 genesis point for
expiration is another possibility (similar to how Lightning makes use of
time-based nLockTime for signalling). However I believe this is not as
desirable as delta encoding or a coinbase bit, as it would prevent transactions
from using block nLockTime and expiration at the same time. It would also still
require a coinbase bit or nVersion increase to ensure expiration-using
transactions are non-standard.

# Mempool Behavior

Obviously, mempool logic will need to handle transactions that can expire
differently than non-expiring transactions. One notable consideration is that
nodes should require higher minimum relay fees for transactions close to their
expiration height to ensure we don't waste bandwidth on transactions that have
no potential to be mined. Considering the primary use-case, it is probably
acceptable to always require a fee rate high enough to be mined in the next
block.

-- 
https://petertodd.org 'peter'[:-1]@petertodd.org