Skip to content

Instantly share code, notes, and snippets.

@Ademan
Last active January 5, 2025 02:38
Show Gist options
  • Select an option

  • Save Ademan/4a14614fa850511d63a5b2a9b5104cb7 to your computer and use it in GitHub Desktop.

Select an option

Save Ademan/4a14614fa850511d63a5b2a9b5104cb7 to your computer and use it in GitHub Desktop.
Multiparty Eltoo with bounded settlement and optional penalties

Introduction

Eltoo schemes have desirable properties such as simplicity, constant storage requirements, and compatibility with multi-party channels. However, naive eltoo is vulnerable to an attack where a dishonest party submits old update transactions in an attempt to delay settlement of the channel state until HTLCs have expired.

This proposes a multi-party eltoo scheme that both allows penalizing dishonest behavior, and provides a bounded settlement time. By giving parties only one chance to update the channel state on-chain, this scheme drastically reduces the ability of malicious parties to prevent honest settlement of the channel.

This proposal depends on a way to make signed vector commitments, and check vector committed items against a spending transaction. I call this construct "multi-transaction-signature"s. This proposal also depends on "floating transactions" (as used in other eltoo schemes). Therefore, we will assume the LNHANCE soft fork, consisting of OP_INTERNALKEY (IK), OP_CHECKTEMPLATEVERIFY (CTV), OP_CHECKSIGFROMSTACK (CSFS), and OP_PAIRCOMMIT (PC). In LNHANCE CTV+CSFS enables the "floating transactions" construct, and CTV+CSFS+PC enable this "multi-transaction-signature" construct. I believe this scheme could be adapted to ANYPREVOUT(APO)+CSFS+PC however I'm admittedly less familiar with APO than I ought to be.

Prior Art

This is likely completely non-novel, but unfortunately I'm unfamiliar with most of the literature. My apologies to anyone whose work I almost certainly overlooked.

Acknowledgements

  • Moonsettler for review, tentative approval, and driving the current LNHANCE efforts and propsing an opcode (OP_PAIRCOMMIT) that enables this without all of the aspects of CAT that scare me.
  • ReardenCode for review, comments, and being open to discussion on xitter which encouraged me greatly in getting back into Bitcoin dev.
  • Everyone else who read this mess of a draft and offered comments or encouragement (or not-so-encouragement, you know who you are! lol)
  • The all of the Bitcoin giants out there, I'm not standing on your shoulders but I'm probably stepping on your feet.

Description

This scheme bounds the number of times an update transaction can be submitted for a multi-party channel by limiting each party to submitting a single update. State updates occur similar to Decker, Russell, and Osuntokun eltoo by using CHECKLOCKTIMEVERIFY(CLTV) and an incrementing absolute timelock to ensure that update transactions can only be spent by either the appropriate settlement transaction or a newer update transaction. To remove parties' ability to update more than once, every state update consists of a large set of update transactions arranged into generations. Every generation allows a party to submit an update, but removes that party from the set of parties eligible to submit an update in the future. Each unique update transaction has a unique set of parties that are still eligible to submit updates. Each generation 1 <= K < N has one transaction for every possible subset of length N - K of parties still eligible to submit updates. These update transactions are divided into generations 1 < K < N of N choose K transactions each. Generation 1 prohibits 1 party from updating any further and has N choose 1 transactions in it. Generation 2 prohibits 2 parties from updating any further and has N choose 2 transactions in it. Generation N-1 prohibits all but 1 party from updating. The total number of all of these update transactions is ~2^N - 1 + N which obviously grows very quickly. After an update transaction is accepted either because the last party submitted their update transaction, or by a timelock expiring, a settlement transaction splitting up the channel balance can be submitted as in other eltoo schemes. This settlement transaction can optionally penalize misbehaving parties by dividing their channel balance among the honest parties.

This scheme as described so far, achieves both of the stated goals, bounding settlement time and optionally enabling punishment. It does, however, require sharing 2^N signatures per party.

Using OP_PAIRCOMMIT we can instead sign a commitment which authorizes many transactions, requiring only a merkle proof proving that a given CTV template has been signed, which indicates a valid state transition. This compression from 2^N signatures to one per party enormously reduces network communications required, and also makes it feasible to use musig to produce a single signature instead. However, for the sake of reduced interactivity, this proposal will use one signature per party.

Outputs on each update transaction are Segwit v1, and have P=N-K tap leaves, one for every party still eligible to update.

The witness includes a merkle proof that the transaction has been signed for, as well as signatures on that merkle root from all parties (even ones no longer eligible to update), except for the updating party. The updating party provides a regular SIGHASH_ALL signature which is checked with CHECKSIGVERIFY. This ensures other parties cannot submit an update transaction on behalf of a different party. Each tapscript follows a fixed path in the merkle tree, which binds it to transition to a specific transaction. If parties could submit an arbitrary inclusion proof in their transaction, they could for instance fraudulently submit one that removes a different party from the eligible-to-update set.

To illustrate, here is what a generation 0 tap script (from the commitment TX) might look like for A to update. Because party A is updating, the spending transaction must match the CTV template TX_BCD_TEMPLATE whose output is identical to this transaction, but without a tap leaf for A to submit another update. This removes party A's ability to update further. In addition to this one, generation 0 would have equivalent tap leaves with scripts corresponding to each of the other parties in the channel.

witness: <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <ABD_ABC> <TX_ACD_TEMPLATE> <TX_BCD_TEMPLATE> <sig(A)>
tapscript:
<S+1> CHECKLOCKTIMEVERIFY DROP # <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <ABD_ABC> <TX_ACD_TEMPLATE> <TX_BCD_TEMPLATE> <sig(A)>
<A>                            # <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <ABD_ABC> <TX_ACD_TEMPLATE> <TX_BCD_TEMPLATE> <sig(A)> <A>
CHECKSIGVERIFY                 # <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <ABD_ABC> <TX_ACD_TEMPLATE> <TX_BCD_TEMPLATE>
CHECKTEMPLATEVERIFY            # <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <ABD_ABC> <TX_ACD_TEMPLATE> <TX_BCD_TEMPLATE>
SWAP                           # <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <ABD_ABC> <TX_BCD_TEMPLATE> <TX_ACD_TEMPLATE>
PAIRCOMMIT                     # <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <ABD_ABC> <BCD_ACD>
SWAP                           # <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <BCD_ACD> <ABD_ABC>
PAIRCOMMIT                     # <DSIG> <CSIG> <BSIG> <GEN2_MERKLE_ROOT> <GEN1_ROOT>
SWAP                           # <DSIG> <CSIG> <BSIG> <GEN1_ROOT> <GEN2_MERKLE_ROOT>
PAIRCOMMIT                     # <DSIG> <CSIG> <BSIG> <GEN1_MERKLE_ROOT>
TUCK                           # <DSIG> <CSIG> <GEN1_MERKLE_ROOT> <BSIG> <GEN1_MERKLE_ROOT>
<B>                            # <DSIG> <CSIG> <GEN1_MERKLE_ROOT> <BSIG> <GEN1_MERKLE_ROOT> <B>
CHECKSIGFROMSTACK              # <DSIG> <CSIG> <GEN1_MERKLE_ROOT> <1|0>
VERIFY                         # <DSIG> <CSIG> <GEN1_MERKLE_ROOT>
TUCK                           # <DSIG> <GEN1_MERKLE_ROOT> <CSIG> <GEN1_MERKLE_ROOT>
<C>                            # <DSIG> <GEN1_MERKLE_ROOT> <CSIG> <GEN1_MERKLE_ROOT> <C>
CHECKSIGFROMSTACK              # <DSIG> <GEN1_MERKLE_ROOT> <1|0>
VERIFY                         # <DSIG> <GEN1_MERKLE_ROOT>
<D>                            # <DSIG> <GEN1_MERKLE_ROOT> <D>
CHECKSIGFROMSTACK              # <1|0>

The templates are arranged into a proof tree structured like this:

                                                       GEN1_MERKLE_ROOT
                           GEN1_ROOT                                          GEN2_MERKLE_ROOT
            BCD_ACD                         ABD_ABC                     GEN2_ROOT             GEN3_MERKLE_ROOT
TX_BCD_TEMPLATE TX_ACD_TEMPLATE TX_ABD_TEMPLATE TX_ABC_TEMPLATE             |
                                                                            |
                                                   /---------------------------------------------------------\
                                              AB_AC_AD_BC                                                 BD_CD_DUMMY
                                   AB_AC                         AD_BC                         BD_CD                    DUMMY
                       TX_AB_TEMPLATE TX_AC_TEMPLATE TX_AD_TEMPLATE TX_BC_TEMPLATE TX_BD_TEMPLATE TX_CD_TEMPLATE

Every party signs the merkle root GEN1_MERKLE_ROOT, and that set of N signatures is sufficient for a state update.

The presence of SWAP or NOP opcodes commits to the path in the merkle tree, and prevents invalid state transitions.

Party B's script would instead have the sequence NOP PAIRCOMMIT SWAP PAIRCOMMIT SWAP PAIRCOMMIT since the TX_ACD_TEMPLATE which excludes B from further updates, is at index 1. Party C's script would have the sequence SWAP PAIRCOMMIT NOP PAIRCOMMIT SWAP PAIRCOMMIT since it is at index 2. You can see how the index of the desired template in the ordered list of transaction templates corresponds to the SWAP and NOP opcodes as a little-endian representation of bits, with SWAP representing a 0 and a NOP representing a 1. Later generations are encoded in the lower branches of the tree. NOPs could be eliminated making some scripts shorter than others. I think this would have pretty negligible effects on incentives, but it's also simpler for me to think about with them in.

Without PAIRCOMMIT

This scheme works without PAIRCOMMIT but requires every party to share a signature for every valid state transition. ~2^N signatures, per party, need to be shared to make this scheme work without PAIRCOMMIT.

Transitions must be individually authorized, otherwise a blanket signature authorizing a transition to state S would permit any update transaction in R<S to transition into any update transaction in state S. Consider a case where parties A,B, and C are eligible to update. With blanket signatures from B, and C to state S, A could sign a transaction that transitions to a state where A and B are able to update, thereby stealing C's update opportunity and preserving their own. This is why each valid state transition must be authorized individually.

Practicality

Each party is required to recompute the entire multi-transaction-signature commitment every channel state update, and this involves computing ~2^N transactions and building a merkle tree committing to all of them. This means that there are ~(N - 1) * 2^(N + 1) - 1 SHA256 operations required for a penalty scheme. (Take this math with a grain of salt, I haven't proved it out) A non-penalty scheme is around twice as fast because there is only one settlement transaction for all updates in a state, rather than one settlement transaction for every update in a state, this takes the complexity to ~(N - 1) * 2^N - 1. For a penalty scheme this results in ~18431 SHA256 `operations for N=10 parties. The amount of memory required should be logarithmic or better over N so memory should not be a limiting factor. With a very plausible single core performance of ~1MHash/s on my aging mid-range desktop computer, that means calculating the commitment merkle root will optimistically take ~18ms. State recomputation can be significantly parallelized, so with 12 threads that's a best case scenario of 1.5ms. This is all very optimistic and assumes the other computations in state recalculation take negligible time. The practical limit for this scheme is probably in the 10-20 party range because the exponential growth more than doubles for every party added. Using these very optimistic assumptions, 17 parties, a penalty scheme would take ~743ms to recalculate. I unfortunately don't know how many parties other schemes consider practical, but this scheme is fairly limited due to the exponential size of the number of potential update transactions that need to be computed.

Conclusion

I present a multi-party eltoo scheme with desirable functional properties but exponential computational complexity using one known technique and one likely known technique. What I'm calling a "multi-transaction-signature" permits a single signature to commit to multiple possible transactions representing state transitions. I suspect it's a trivial variant of vector commitments too uninteresting to have a name. This scheme combines these "multi-transaction-signature"s with existing eltoo "floating transactions" to enable a communication-efficient multiparty eltoo channel with or without penalty, and with bounded settlement time. Parties are given one chance to update the on-chain state, and may (optionally) be penalized for publishing old state, which should make attacks economically unattractive. The amount of computation required of parties is exponential on the number of parties N, however I believe that N~=10 is practical in this scheme, and also nearing the practical limit of multi-party channels anyway, due to the liveness requirements of participants, and network latency. I present this scheme mostly to solicit feedback, is this interesting enough to deserve a better writeup? this is obviously very incompletel. Is it interesting enough to warrant an implementation? Maybe it is only rehashing previous work, if please direct me to it so I can familiarize myself. Maybe, (my hope) is it inspires one of you wizards to invent a better or related scheme.

Further Work

  • Can this be safely integrated with a watchtower?
  • Truncating the number of generations at some number M < N could potentially drastically reduce the number of transactions to calculate and hash, as long as there are less malicious users than M it should remain secure.
  • Can this scheme be adapted to tolerate offline users? Each generation of update transaction already partitions the parties into two sets, it might be possible to use something like this to split and merge a multi party pool if parties also sign for a set of merge transactions? Maybe?. It's worth exploring but there's a lot of pitfalls, there might not be a (good) way to do it.
  • Since state update communications are reduced to a single signature per party, is it practical to use musig instead, to reduce the witness size of uncooperative closes? That would require two rounds of communication, however it could still be practical.
  • Is this interesting enough to implement? Maybe just to benchmark state update performance to gauge practical limits.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment