Input-triggered transaction expiry

TLDR

Input-triggered transaction expiry is an interesting primitive that warrants further research. It can be implemented with a minimalist consensus change:

If nSequence enforces a height-based relative timelock R and bit 21 is set, fail if R < 100, nLockTime is time-based, or the BIP68 min-height exceeds nLockTime.

This change is surprisingly powerful. It enables both mempool-free HTLC forwarding and (pseudo) contract-level relative timelocks for LN-Symmetry.

Both these applications would also be possible using classical expiry (see OP_EXPIRE), but unlike classical expiry, input-triggered expiry is immune to free relay.

Background

Plain transaction expiry is a well-researched topic. The most fleshed out proposal appears to be Peter Todd’s OP_EXPIRE, which invalidates a transaction after a certain block height.

The proposal requires several consensus changes:

  1. An nVersion flag that enforces a 100-block maturity rule on all outputs.
  2. A new OP_EXPIRE opcode.
  3. A taproot annex that makes the expiry height explicit.

The primary motivation of OP_EXPIRE was the replacement cycling attack in HTLC forwarding. By expiring the preimage path at the HTLC-timeout, we stop the attack, allowing us to replace local-mempool preimage monitoring with chainstate monitoring.

The primary critique was the potential for free relay. If a valid transaction later becomes invalid, an attacker could spam the P2P network at little expected cost. Todd proposes a relay policy requiring a fee sufficient to be mined in the next block, but the community found this unsatisfactory (presumably because probabilistic free relay is still possible).

Input-triggered transaction expiry

An input-triggered approach to transaction expiry is less powerful, but it is immune to free relay and fits naturally in the existing transaction context. The main idea is to expire a transaction if an input was mined too late.

This is enforceable with only the following consensus change:

If nSequence enforces a height-based relative timelock R and bit 21 is set, fail if R < 100, nLockTime is time-based, or the BIP68 min-height exceeds nLockTime.

For example, to enforce expiry by height H, a transaction would set bit 21 in nSequence, enforce a height-based relative timelock R = 100, and set \text{nLockTime} = H. Mining the input after H-100 will then trigger expiry.

The 100 block minimum relative timelock ensures a valid transaction cannot become invalid without a 100 block reorg, guaranteeing the same level of maturity as a coinbase output and preventing free relay.

The most obvious drawback is that this approach imposes an absolute timelock of H, preventing early publication if the input is mined early. While not ideal, this is desirable in some use cases (such as UC2) and can be entirely avoided using introspection (see this post).

Use case 1: mempool-free HTLC forwarding

Like OP_EXPIRE, input-triggered transaction expiry enables HTLC forwarding without local-mempool preimage monitoring. A detailed construction can be found in this post.

Use case 2: (pseudo) contract-level relative timelocks

Background

In classical LN-Symmetry, each update transaction resets the relative timelock. This delays HTLC-timeout by 2x in a 2-party channel and by a factor of N in an N-party channel, locking up liquidity.

The most efficient solution is a contract-level relative timelock, which enforces a relative timelock on settlement based on the height of a kickoff transaction. Unfortunately, CLRTs appear to require a succinct ancestry proof or a way to propagate the kickoff height. Neither is possible today, and adding either capability would require a significant change to consensus.

Summary

A (pseudo) contract-level relative timelock can be constructed using input-triggered expiry, at the cost of increased interactivity, storage, and periodic refresh. With vector commitments, these additional costs can be eliminated with off-chain computation.

Construction

Consider the following force-closure path in a modified LN-Symmetry (assume CSFS and TEMPLATEHASH):

  1. A kickoff TRUC transaction (Tx1) moves the channel funds to a kickoff output (committed to at setup using TEMPLATEHASH), paired with an ephemeral anchor.

  2. A presigned window TRUC transaction (Tx2) moves the funds to an update output controlled by a window-unique public key P_{H}, with no ephemeral anchor. The transaction sets nLockTime to H and enforces bit 21 of nSequence with a 100 block relative locktime.

  3. One or more update transactions are published, followed by the latest settlement transaction.

  4. The settlement transaction commits to a non-final nSequence and sets nLockTime to H + C, where C is the desired challenge window.

Key idea: Due to bit 21 nSequence enforcement, Tx2 expires if Tx1 is not mined by H-100. This expires all update transactions signed with P_{H}, enforcing a pseudo contract-level relative locktime on the settlement transaction.

This functionality comes at the cost of increased interactivity and storage. With each state update, channel parties sign N update transactions for windows beginning over the next N blocks. If there is no presigned Tx2 for height H, that transaction is signed as well.

Periodically, channel parties also refresh the current state by signing new update transactions committing to the same state outputs. This ensures a force-closure path is always available.

Optimizations

An opcode that enables vector commits (i.e., PAIRCOMMIT, CAT, etc.) enables merkle proof verification, which eliminates the added interactivity and storage requirements and the need to refresh. This involves four changes:

  1. All update outputs are controlled by the same public key P.
  2. The window height H is committed to in the update script.
  3. Parties sign a merkle root approving N windows, where each leaf commits to an update template hash and window height H.
  4. The update script verifies the signed template hash and window via a merkle proof.

With this optimization, channels can trivially support millions of windows, removing the need to refresh in practice.

Force-closure walkthrough

Consider a two-party channel between Alice and Bob, where Alice wishes to force close.

  1. First, Alice publishes the kickoff transaction (Tx1), notifying Bob of the force closure.

  2. Suppose Tx1 is mined at block K. This invalidates every window transaction (Tx2) where \text{nLockTime} < K + 100, invalidating in turn every update transaction committing to a settlement transaction where \text{nLockTime} < K + 100 + C, where C is the agreed-upon challenge window.

  3. After 100 blocks, Alice may choose any Tx2 to publish, provided nLockTime is less than or equal to the current block height.

  4. If Alice is honest, she will publish the Tx2 with the lowest valid locktime. If Alice is malicious:

    a) She will withhold Tx2 and force Bob to publish instead. (Bob must publish a Tx2 before the earliest valid window ends so that he has the opportunity to stop a stale commitment).

    b) She will publish the Tx2 with the highest valid locktime, delaying settlement.

  5. Alice publishes an update transaction, committing to a settlement transaction.

  6. If the update transaction is out-of-date, Bob publishes an update transaction with the latest state.

  7. Alice publishes the settlement transaction after the absolute settlement timelock expires.

Security

This construction inherits most of the security properties of LN-Symmetry with an ideal CLRT, but it is perhaps most interesting with vector commitments, which remove the added refresh and interactivity requirements.

The most significant limitation is the added 100-block delay in the force closure path. This raises the minimum HTLC-timeout, which reduces the maximum payment length. In practice, most payments would be unaffected, as the total CLTV budget is sufficiently high, but the total CLTV budget can be increased by 100 if needed.

Final thoughts

Relay-safe transaction expiry seems like a genuinely useful primitive, and I find it surprising that it can be implemented via a consensus change with such a small surface area, by shifting the focus to input-triggered expiry.

The use cases described above are primarily illustrative, but they seem interesting enough to warrant further research. With respect to CLRTs specifically, there are many potential constructions, but the construction described above was an honest surprise. CSFS + TEMPLATEHASH + nSequence bit 21 + a vector-commit opcode is a fascinating combination.

I’m interested to hear the community’s thoughts on this idea. Is transaction expiry worth revisiting if it can be implemented with strong no-free-relay guarantees?

1 Like

A quick follow up

“Input expiry” may be the best description of this primitive:

  • nLockTime specifies the height the transaction can be mined.
  • nSequence specifies how many blocks earlier the input expires.

Framing the primitive this way makes it much more intuitive.

It also makes it clear that it is orthogonal to pure transaction expiry. It enables similar applications, but the mechanism is clearly different.

I think the difference here is that normal “expiry” proposals say “this transaction is invalid after X happens”, whereas this approach says “this spending path is invalid if this transaction was confirmed after X”.

I think it’s worth comparing this to a generic ability to introspect the confirmation height of the coin being spent; I proposed that in the OP_TX thread on bitcoindev 8 months or so ago.

Introspecting commit height gives you roughly the same functionality in a straightforward way, instead of (nSequence R + bit21, nLockTime H) write the script (<TXSEL_HEIGHT> OP_TX <H-100> LESSTHANOREQUAL VERIFY).

You can somewhat do parent height introspection with this feature, in that 100 CSV 900100 CLTV will verify that the parent was mined prior to block 900000, if nSequence bit 21 is also set.

I don’t see a strong argument for why the consensus-enforced minimum delay here should be 100 blocks rather than 50, 20, 10, 6 or even 1 – for coinbase outputs that makes some sense; coinbases pay directly to miners who are ultimately responsible for whether we see extensive reorgs or not, but for individual transactions, I don’t think this creates a significantly bigger risk in regards to reorgs than regular double-spends.

1 Like

An use case i find interesting is to cut steps on contracts and swaps.

If a contract uses HTLCs and end up with a privkey handover, then the other party already have access to the musig, however, today we need a final tx in order to be certain that no HTLC will be published onchain in the future. With HTLC expiration, we can make swaps 1 tx long, something that can be useful in a high fee environment.

@ademan

I believe he meant in conjunction with OP_TX and math on the stack

No, I just meant if this proposal were implemented, it would allow partial height introspection (in that you could end up with 900,000 on the stack when spending a coin that was confirmed at height 900k or earlier).

2 Likes

The double-delay problem is when Alice, say, doesn’t have the latest update tx fully signed (she sent her psig, waiting to hear back), and she needs to go on-chain. After U-1 blocks Bob then sends the final update tx, which in classical relative timelock ln-symmetry resets the timeout, resulting in a wait of U*2 before settlement can occur. Does your proposed change allow Alice to immediately settle or otherwise mitigate? Am I thinking on the wrong layer?

(I am genuinely very bad at reasoning about timelock semantics in bitcoin validation so bear with me)

edit: good thread linked on the CSV->CLTV transformation, think I missed that one

Ok I was referring to your now-deleted idea, which makes me think it was invalid.

I still have to mull over the other options you state here.

1 Like

I might suggest an alternative framing. The difference here is that this approach establishes an invariant on nLockTime, such that all timelocks expire by nLockTime if every BIP68 input is height-based and enforces bit 21.

This implies what you are suggesting. The transaction becomes invalid if the input transaction was not confirmed before some height.

I agree, and thank you for sharing. I was not previously aware of your post.

As you allude to, script-based coin-height introspection is a legitimate alternative, but I would argue that this approach would be like using a hammer to push down a pin. Two drawbacks come straight to mind:

  1. No scriptless expiry: With the proposed approach, a scriptless spend can enforce input-triggered expiry. This is useful in presigned transactions, key-path spends, and (potential) bare output scripts enforcing a TEMPLATEHASH or CTV commitment.
  2. More powerful than needed: The fundamental capability we care about is an absolute cap on an input’s confirmation height. Coin-height introspection enables other behavior, like a minimum or exact confirmation height, which may prove unwanted.

Other less pressing but legitimate reasons to prefer the described approach include:

  1. Transaction readability: With bit 21, all time-related information remains visible in the txid-commitment. This is useful when displaying transactions to users to sign, creating PSBTs, or writing relay policies that require knowledge of expiry. Script-based enforcement would lack these properties by default.
  2. Separation of concerns: With the proposed approach, the existing separation of concerns between context validation and script validation would remain unchanged. Adding coin-height introspection to script would violate the existing separation of concerns, introduce new code paths, and demand a change in downstream projects like the kernel.

I am uncertain if the script you are suggesting works as you describe. CLTV enforces a minimum nLockTime, and the true nLockTime could be anywhere between that value and the current height. To get the behavior you desire, I think you would need a NONE|ACP presignature which commits to the nSequence and nLockTime values.

For this reason, if script-based max-coin-height enforcement is a goal, I would probably suggest an explicit OP_SEQUENCE and OP_LOCKTIME introspection capability. You could then enforce bit 21 in script and set an explicit maximum value on nLockTime using LESSTHAN.

I think you may be right. I felt it best to be conservative and stick with the same 100-block delay as the OP_EXPIRE proposal, but the more I think about it the more I come to the conclusion that the delay is unnecessary.

I plan to write a dedicated follow-up on this issue.

Short-duration HTLCs is definitely an interesting primitive, and it is good motivation for a shorter delay than the current 100-block minimum. See above.

1 Like

Follow-up

This follow-up is motivated by a few comments. First, @ajtowns suggested that the 100-block delay could be removed from consensus (and perhaps policy too). Second, @instagibbs suggested that the original BIP68-oriented language made the change hard to intuitively understand. To that end, I thought I’d share two frameworks and a short note that help me better understand the proposal, and time in general in the protocol.

Alternative framing #1

The first begins with an ultra-short (not-backward-compatible) plain-english explanation, which we then restrict:

(1a) Fail if a relative timelock remains active after nLockTime,

This sentence describes a much more powerful proposal, which is somehow simpler to state. It simply means that the absolute timelock must be the last timelock to expire. This in turn guarantees the transaction will be invalid if any relative timelock kicks off too late.

(2a) Fail if a height-based relative timelock remains active after a height-based nLockTime,

This sentence restricts (1a) to height-based timelocks, leaving MTP-based timelocks untouched. The reason to do this is because most applications need only height-based expiry. Moreover, MTP-based expiry could incentivize miners to play games with timestamps, which is best to avoid.

(3a) Fail if a height-based relative timelock remains active after a height-based nLockTime and bit 21 of nSequence is set on that input.

This final sentence closely resembles the original proposal, minus the 100-block minimum delay. Unlike (2a), (3a) applies only to a single input that opts in using bit 21. This reduces the risk of accidental confiscation, at the cost of losing enforcement of the invariant across all inputs.

Is cross-input enforcement a desirable property? It may be a direction worth exploring, but for now, it is probably best to limit the surface area of the consensus change as much as possible.

Alternative framing #2

Another approach, which I find helpful, places the confirmation at the center of the description:

(1b) Fail if an input has fewer than nSequence confirmations at nLockTime.

This description is completely ignorant of BIP68 and not-backward-compatible. It is purely illustrative and designed to gain insight into the proper (potential) meaning of nSequence.

(1b) is nearly equivalent in functionality to (1a), except it prohibits a time-based relative timelock. The description also suggests an inverse measure of time, which starts with nLockTime and counts the confirmations of each input backward. This has tradeoffs, but it may be more intuitive for users whose sight begins with “the transaction” and who care about the confirmations the transaction has seen at the postdate, rather than relative timelocks or the “chain.”

(2b) Fail if nLockTime is height-based and an input has fewer than nSequence confirmations at nLockTime.

(2b) is effectively identical to (1b), except we prohibit a time-based nLockTime and ignore BIP68.

Let nSequence signal expiry if bit 21 is set and bits 22 and 31 are unset.

(2c) Fail if nLockTime is height-based,nSequence signals expiry, and the input has fewer than nSequence confirmations at nLockTime.

This description is more involved technically, but it is fundamentally equivalent to (3a), leaves time-based relative timelocks untouched, and respects existing BIP68 usage of nSequence

On free relay

(1a) and (1b) are stated in a manner that boils down the consensus change to the fundamental invariant that is enforced. The one caveat is that (1a) and (1b) no longer naturally prevent free relay, because a 0-confirmation input is potentially allowed.

There are two ways to address this, were (1a) or (1b) enforced. The first is to enforce a minimum 1-block delay in consensus (or a greater delay). The second is to enforce a minimum delay only in policy or to create a package relay policy that ensures the child’s relay is covered by the parent’s fee if the parent has zero confirmations and the child could expire.

Again, this is only relevant to (1a) and (1b) and not the actual proposal, which uses BIP68-oriented language and automatically enforces a 1-confirmation input. That being said, it is useful to think about as an academic exercise.

Offline usage

Input-triggered transaction expiry is perhaps also useful as a way to clarify the meaning of transactions when signed offline. For instance, Alice may find it easier to approve the following statement:

This transaction is good on [this date] if and only if [these inputs] have [these confirmations].

This statement perhaps also clarifies the invariant the proposal could enforce. Without expiry, the “only if” clause is lost. With it, we gain bidirectionally.

Final thoughts

Language is perhaps one the hardest things for new users and developers to pick up, especially the language of time in the protocol. Using non-BIP68 language to describe the consensus change is probably a good idea, especially if it can convey the reason and purpose as well.

@ajtowns suggested language akin to input “coin-heights” or “coin-height ceilings.” This is a viable third approach and worth considering.

I think I didn’t get well the first time, but if I get it now, does this only applies to a double transaction scenario? I mean, the second transaction becomes invalid if the first transaction were confirmed too late.

If it’s that, then I think that’s an easier way to explain it.

1 Like

Yes, you got it. Though I might push back on your suggestion that this is only applicable to double transaction scenarios (meaning applications that plan two transactions in advance).

That was certainly the scenario that motivated the idea, but on further reflection, I believe a case could be made to encourage input expiry on regular payments too. The idea being that we can impose costs on miners who play games and reorg by taking away revenue if a dependency is confirmed later than it was originally.

This in turn could improve chain tip stability in a post-subsidy world.

Expressing Now

Background

“Input-triggered transaction expiry” was a name I developed after a good amount of thought, upon posting Expiring HTLCs without free relay and developing the idea for (pseudo) CLRTs, described here.

The language, at the time, was the best I could do to describe the fundamental primitive, which I considered to be an expiry proposal that resolved the “free relay” problem. @ajtowns then rightly pointed out that the delay could be substantially reduced and the language of the exact mechanism could be described as a cap on the maximum “height” of an input “coin.” In this light, the proposal looks somewhat comparable to the coin-height introspection capability @ajtowns previously discussed, albeit at the context-level rather than in script, which has some benefits.

His comments pushed me hard to think about the exact language of the proposal, because it felt more fundamental than just coin-height introspection outside of script.

This post is a summary of that reflection.

Where is now?

There are multiple frames of reference when thinking about “now” in the protocol:

  • The node sees “now” in terms of the local time, the chain tip, the confirmations, and the set of unspent coins.

  • The wallet sees “now” in terms of the local time, the transactions that have been signed, and (potentially) the confirmations of the unspent coins the wallet can spend.

  • The economic agent sees “now” in terms of the local time and the knowledge of the transaction before them.

(There are other perspectives, but I will stop at these three)

State-contingent claims, pre-dates, and post-dates

Let us imagine two economic agents, Alice and Bob. Alice and Bob are engaged in an economic transaction. They both consent. It is voluntary.

In this economic transaction, Alice and Bob consent to a claim that some virtual coins are held across one or more conditions. This claim is a state-contingent claim, because it is contingent on the state-of-the-world to which Alice and Bob agree.

(I am intentionally adopting the language of Kenneth Arrow, who developed the theory of state-contingent claims and Arrow-Debreu general equilibrium.)

There are three types of states:

  • Pre-dated states (ex: yesterday’s newspaper headline)
  • Present states (ex: Alice gives Bob a baseball right now)
  • Post-dated states (ex: Input transaction T has 6 confirmations)

When seen by an observer, there are only three outcomes:

  • The state that conflicts with what has been seen.
  • The state that has been seen.
  • The state that may be seen.

We must therefore establish the state-contingent claims the Protocol presently supports and then argue for the state-contingent claims that should be supported in the End.

Right now

We have two perspectives of “right now” we have to consider. We have the “right now” of the claim, and those who author it, and we have the “right now” of the witness, who sees the claim and confirms the proper ordering.

For the claim, “right now” is the local contingent state of authorship:

  • The chain has received [this many] confirmations.
  • [These coins] are unspent.
  • [These transactions] have received [these confirmations].
  • The local time is [this].

For the witness, “right now” is the absolute state determined by the confirmations they have seen:

  • The number of confirmations the chain has.
  • The coins that remain unspent.
  • The confirmations that each coin has.
  • The current estimate of global “local” time (median-time-past)
  • The status of each coinbase-created coin.

Presently, “right now” is limited to one type of state that could conflict with what the witness has seen:

Present: A state-contingent claim can conflict with the absolute state if and only if it spends a coin that has already been spent.

Claiming Now

The present proposal describes a significant departure from the status quo because it introduces a second type of state that could conflict with what the witness has seen.

Definition: A conflicting claim is one whose state can never be seen, given the state that has been seen.

OP_EXPIRE is the only other proposal I am aware of that has this property, but for the reasons discussed, it is incompatible with absolute free-relay immunity.

What I would like to do is reframe input-triggered transaction expiry in terms of the fundamental capability, which I believe to be increased local expression of the state “Now.”

Proposal: A state-contingent claim can conflict with the absolute state if and only if it spends a coin that has already been spent OR [these spent coins] lack [these confirmations] by [now].

Evaluation

In this light, the proposal is far easier to evaluate. It looks less like an “expiry” proposal and more like a proposal that tunes the protocol’s expression of “Now.”

Presently, “Now” is limited to a lower bound expressed by users who pre-date each transaction with nLockTime using the most recent measure of time. Relative “Now” is likewise limited (see BIP68).

Is this consensus change secure? It seems so right now.

Arguably, it could even improve security by increasing the expressiveness of “Now,” which could have downstream effects on the ability of miners to profit from large chain reorganizations. If every payment asserted the chain depth and the confirmations of each input by that depth, it would prove far harder to perform deep chain reorganizations profitably.

Open question: Should the proposal be generalized so that nLockTime can be time-based?

This is doable with some additional complexity. The ability to state the elapsed MTP / confirmations relative to a local “Now” seems like a basic primitive, regardless of how “Now” is measured. This could strengthen the security guarantees of MTP, even with timewarp protection, which may address the concerns Peter Todd raised in his OP_EXPIRE proposal.

Reorg Defense

To express this claim visually, let’s imagine the following three transactions:

Each transaction has committed to the state of the previous one as closely as possible to “Now.” For instance, Tx2 commits to Tx1 having 30 confirmations by nLockTime, which is set as closely as possible to the actual confirmation.

Now imagine there is a deep chain reorganization, which intentionally or unintentionally delays Tx1, for metaprotocol or other reasons:

Consequently, the fees of Tx2 are permanently lost, as are the fees of any transaction that is dependent on Tx2. In a real deep chain reorganization, there could be many lost dependencies, imposing a severe cost on the miner who intentionally tries to perform the attack. This cost is most severely felt in a world without subsidy.

Result: Reorg attacks can be made more costly by users enforcing input expiry. This applies to shallow reorgs as well, but only if the 100-block delay is reduced (or removed).

(In the above charts, assume bit 21 is set or input expiry is enforced by default.)

Conclusion

I believe that the fundamental primitive is more than coin-height introspection and could be of relevance to chain tip stability post-subsidy. Expressed succinctly, the primitive gives users the ability to make a statement of “Now” that invalidates the transaction if “Now” changes. This in turn imposes costs on the malicious miner who attempts to reorganize the chain.

I’ll leave this open for discussion. I imagine I may have some follow-up thoughts myself.