Skip to content

Gate 0-conf splices with counterparty inputs behind splice_minimum_depth - #32

Open
martinsaposnic wants to merge 1 commit into
lsp-0.2.7from
lsp-0.2.7-splice-0conf-counterparty-inputs
Open

martinsaposnic wants to merge 1 commit into
lsp-0.2.7from
lsp-0.2.7-splice-0conf-counterparty-inputs

Conversation

@martinsaposnic

Copy link
Copy Markdown

Problem

splice_minimum_depth (#20) only applied when the counterparty initiated the splice. When the LSP initiates (LSPS4 top-up splice-in), the client can still add its own inputs: it answers splice_ack with a non-zero funding_contribution_satoshis and sends tx_add_input. LDK accepts that. On a 0-conf channel the LSP then sends splice_locked right after tx_signatures.

Attack

  1. LSP starts a splice-in on a 0-conf client channel.
  2. Malicious client adds its own inputs (any implementation can, the protocol allows it).
  3. LSP locks the splice at 0 conf. The client's new balance includes its contribution.
  4. Client pays that balance out through the LSP.
  5. Client double-spends its inputs. The splice tx never confirms. The LSP already paid out.

Upstream LDK (v0.2.7 and main) has no guard here: any splice on a 0-conf channel locks at 0 conf. Upstream treats 0-conf peers as trusted. We don't.

Second problem: channels persisted without splice_minimum_depth (opened before #20, or while unset) read it back as None, so even client-initiated splices on those channels locked at 0 conf.

Proof

New tests in lightning/src/ln/splicing_tests.rs:

  • test_splice_minimum_depth_for_self_initiated_with_counterparty_inputs
  • test_splice_minimum_depth_applies_to_channels_persisted_without_it

LDK 0.2 cannot contribute as splice acceptor, so a test-only hook (TEST_SPLICE_ACCEPTOR_CONTRIBUTION, cfg(test)) makes node B add inputs, like a malicious peer would.

cargo test -p lightning --lib test_splice_minimum_depth

Before fix:

node 0 INFO Sending 0conf splice_locked txid ebc93ab4... to our peer
panicked at lightning/src/ln/splicing_tests.rs:2778:5:
LSP sent 0-conf splice_locked although the counterparty contributed inputs: Some(SpliceLocked { ... })

panicked at lightning/src/ln/splicing_tests.rs:2896:5:
LSP sent 0-conf splice_locked on a channel persisted without splice_minimum_depth

After fix: all 4 test_splice_minimum_depth* tests pass.

Fix

  • on_tx_signatures_exchange: apply the override when the counterparty initiated or added any non-shared input (remote_inputs_count() > 0). If the signing session is somehow missing, apply it (fail safe).
  • ChannelManager read: set each channel's splice_minimum_depth from the live config on load.

Tradeoffs

  • LSP-only-input splices still lock at 0 conf. LSPS4 forwards the HTLC after splice_locked, so applying the depth to every splice would add about an hour to every top-up. Not done.
  • A client that adds inputs to an LSP splice now waits splice_minimum_depth confs. LDK clients can't add inputs today, so honest clients see no change.
  • On load, the config wins over the value stored per channel. Changing or unsetting the config after restart changes it for all existing channels.

Not fixed here (known gap)

Even with only LSP inputs, the client can try to race the splice tx by broadcasting its latest old-funding commitment (it spends the same old funding output). That needs separate analysis.

Test runs

  • cargo test -p lightning splic: 25 passed, 1 failed. The failure, test_splice_locked_conveyed_after_reestablish_signature_retransmission, also fails on the base lsp-0.2.7 (bfc1b29) without this change.
  • cargo test -p lightning-liquidity lsps4: 35 passed
  • cargo test -p lightning-liquidity --test lsps4_integration_tests: 6 passed
  • cargo check --workspace: ok
  • RUSTFLAGS="--cfg=c_bindings" cargo check -p lightning: ok

splice_minimum_depth only applied when the counterparty initiated the
splice. When we initiate, the counterparty can still add its own inputs
via splice_ack + tx_add_input. On a 0-conf channel we then sent
splice_locked at once, so the peer could spend its new balance through
us and double-spend its inputs so the splice tx never confirms.

Apply the override whenever the counterparty added inputs to the
negotiated splice tx (or initiated it). Splices built only from our own
inputs, like the LSPS4 top-up splice-in, still lock at 0-conf.

Also, channels persisted without splice_minimum_depth (opened before
the setting existed, or while it was unset) read it back as None, so
the protection never applied to them. Take the value from the live
config when the ChannelManager loads each channel.

Tests use a test-only hook to make the acceptor contribute inputs,
since LDK 0.2 cannot do that yet but the protocol allows it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant