Skip to content

Instantly share code, notes, and snippets.

@ddustin
Last active January 18, 2024 17:46
Show Gist options
  • Save ddustin/be6297f1bf7e8bf792cdcafcf49bb924 to your computer and use it in GitHub Desktop.
Save ddustin/be6297f1bf7e8bf792cdcafcf49bb924 to your computer and use it in GitHub Desktop.
Splice Signing Order Woes

Imagine a three node network with two channels

A <-> B <-> C

Let's say B would like to perform two splices:

splice-out 10M sats from A <-> B
splice-out 10M sats from B <-> C

B would then like to merge these two splices into a single transaction. This is done in the standard way, namely:

  1. Initiate splice-out with A, continue process until A finalizes. Do not finalize ourself and pause here.
  2. Initiate splice-out with C, add inputs & outputs from splice attempt (1). Finalize up to the point of signature exchange.
  3. Pass inputs & outputs from splice attempt (2) to A. Finalize up the point of signature exchange.

At this point all nodes have a single matching transation that performs a splice for A <-> B and B <-> C.

By existing tie break rules current de-facto rule, the initiator (B) must sign first.

A is waiting to receive signatures for all inputs, which include input(s) controlled by C

  • It will not send signatures until it receives all of these

C is waiting to receive signatures for all inputs, which include input(s) controlled by A

  • It will not send signatures until it receives all of these

B has it's own signatures but not those for A or C. It cannot send the complete package for either splice, even though it is required to for both.

To break this race condition, the tie break rule must be updated for the accepter to sign first.

@t-bast
Copy link

t-bast commented Jan 18, 2024

By existing tie break rules current de-facto rule, the initiator (B) must sign first.

What? Why? That's not how we've implemented it.

In the A-B channel, each party adds the following inputs:

  • A adds whatever inputs they'd like, which we can summarize as a single input of value amount_a
  • B adds:
    • the current channel funding output for A-B: its value is the channel size before the splice funding_amount_ab
    • the current channel funding output for B-C: its value is the channel size before the splice funding_amount_bc
    • the input C added in the B-C channel splice, of value amount_c

In the B-C channel, each party adds the following inputs:

  • C adds whatever inputs they'd like, which we can summarize as a single input of value amount_c
  • B adds:
    • the current channel funding output for A-B: its value is the channel size before the splice funding_amount_ab
    • the current channel funding output for B-C: its value is the channel size before the splice funding_amount_bc
    • the input A added in the A-B channel splice, of value amount_a

The batching works as long as B doesn't have to sign first in both splices. For B to sign first in both splices, B would have to contribute the least in both splices, which means the following inequations would hold:

  • funding_amount_ab + funding_amount_bc + amount_c < amount_a (B signs first in the A-B channel)
  • funding_amount_ab + funding_amount_bc + amount_a < amount_c (B signs first in the B-C channel)

Those two inequations obviously cannot be both true since all amounts are positive, which means there is at least one splice in which B doesn't sign first.

So everything is working fine with the existing rule?

@ddustin
Copy link
Author

ddustin commented Jan 18, 2024

Okay working through it:

  • B adds zero sats
  • A adds 1M sats
  • C adds 1M sats

Now we have
amount_a: 1M
amount_b: 0
amount_c: 1M

And assume existing channels ab and ac are the same, say 10M

amount_ab: 10M
amount_bc: 10M

So for the signing order on splice_ab we see

  1. B has 11M in incoming inputs
  2. A has 10M in incoming inputs

Since (2) is less, A must sign first

And for the signing order on splice_bc we see

  1. B has 11M in incoming inputs
  2. C has 10M in incoming inputs

Since (2) is less, C must sign first

Which, I believe I see now, makes a tie actually impossible. Nice! 👍

@t-bast
Copy link

t-bast commented Jan 18, 2024

I think there are still a few mistakes, because B adds both current channel funding outputs in every signing session.
With the amounts that you've chosen, the correct statements are:

So for the signing order on splice_ab we see

  1. B has 21M in incoming inputs (the two current channel outputs + C's input)
  2. A has 1M in incoming inputs

Since (2) is less, A must sign first

And for the signing order on splice_bc we see

  1. B has 21M in incoming inputs (the two current channel outputs + A's input)
  2. C has 1M in incoming inputs

Since (2) is less, C must sign first

@t-bast
Copy link

t-bast commented Jan 18, 2024

The only case where there can be a deadlock is when B's node_id is less than A and C's node_ids and:

  • funding_amount_ab + funding_amount_bc + amount_c = amount_a
  • funding_amount_ab + funding_amount_bc + amount_a = amount_c

The only case where those two inequations hold is if amount_a = amount_c and funding_amount_ab = funding_amount_bc = 0 (dual-funding without contributing where both peers use the exact same amount). It should be easy to work around that specific case when it happens or simply abort, as it shouldn't happen "by chance".

@ddustin
Copy link
Author

ddustin commented Jan 18, 2024

I think there are still a few mistakes, because B adds both current channel funding outputs in every signing session. With the amounts that you've chosen, the correct statements are:

...

Oh this is an interesting point. The current spec is vague on which side gets to count the channel input itself for signing order calculations.

Making it explicitly the initiator makes sense to me.

@t-bast
Copy link

t-bast commented Jan 18, 2024

I don't think the spec is vague, it says:

The initiator:
  - MUST `tx_add_input` an input which spends the current funding transaction output.

Since the signing order is then based on the tx_add_input each side sent, this is correctly specified already? The wording is weird though, it should probably be:

  • MUST send tx_add_input for the current funding transaction output.

@ddustin
Copy link
Author

ddustin commented Jan 18, 2024

The only case where there can be a deadlock is when B's node_id is less than A and C's node_ids and:

  • funding_amount_ab + funding_amount_bc + amount_c = amount_a
  • funding_amount_ab + funding_amount_bc + amount_a = amount_c

The only case where those two inequations hold is if amount_a = amount_c and funding_amount_ab = funding_amount_bc = 0 (dual-funding without contributing where both peers use the exact same amount). It should be easy to work around that specific case when it happens or simply abort, as it shouldn't happen "by chance".

Yeah dual having 0 initial channel balances creates this problem but seems easy to work around. I imagine it happening if a user does a double lease with dual for the same amount.

Say a single dual open that is two lease requests that result in this:

B <- A (10M)
B <- C (10M)

We could pop out an error saying lease amounts cannot be exactly equal, and / or automatically jiggle the amount by a sat.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment