Skip to content

channeld: report the fundee's full post-splice balance to hsmd in msat - #9591

Open
Amperstrand wants to merge 3 commits into
ElementsProject:masterfrom
Amperstrand:splice-fundee-msat
Open

Amperstrand wants to merge 3 commits into
ElementsProject:masterfrom
Amperstrand:splice-fundee-msat

Conversation

@Amperstrand

@Amperstrand Amperstrand commented Sep 30, 2026 •

Copy link
Copy Markdown

relative_splice_balance_fundee() computes the push_value handed to hsmd via hsmd_setup_channel when a splice creates a new channel era. It is wrong three ways:

  1. Units: it wrapped the satoshi-denominated opener_relative/accepter_relative splicing fields in amount_msat() unchanged, so the value reached hsmd 1000x under-reported.
  2. Role: the contribution was selected by splice role, but the fundee is defined by channel role: the side that did not open the channel. The splice initiator is not necessarily the channel opener, so whenever the channel fundee reports -- whether it initiated the splice or accepted an opener-initiated one -- the splice roles are inverted with respect to the channel roles and the channel opener's contribution was sent instead of the fundee's.
  3. Prior balance: push_value is the fundee's balance at the start of a channel era (at channel open, the pushed amount); for a splice that is pre-splice balance + funding contribution. A fundee already holding balance was under-reported by exactly that balance -- and a fundee holding HTLCs in flight at splice setup was under-reported by those too: a fundee-owned HTLC that fails back raises the fundee's output above its settled at-setup balance.

None of this is caught by the existing tests: stock hsmd signs unconditionally, so no test observes what hsmd was told.

Impact: stock hsmd is unaffected (it signs whatever it is asked to); any hsmd/signer implementation that validates the balances reported to it reads the honest post-splice commitment as an overpayment and refuses to sign it, wedging fundee-side splices. Found while running a validating signer implementation against experimental splicing.

Fix: three commits, each independently test-covered. Units conversion (amount_sat_to_msat, matching every other consumer of these fields, which feed amount_msat_add_sat_s64); then selection by channel opener role plus the fundee's pre-splice settled balance from the channel view (view[].owed[]); then the fundee-owned HTLCs pending at splice setup (bucketed by owner side, the same bucketing check_balances uses; the other side's HTLCs never count). A negative contribution is a splice-out and subtracts; a negative total or an overflowing add is a genuine over-draw and fails the peer. Nothing is ever wrapped or clamped.

Tests: channeld/test/run-splice_fundee_msat.c pins byte-exact through the real static function: the channel/splice role matrix, X-sat -> X*1000 msat (never X msat: the wrap fingerprint), pre-splice balance, splice-out and pending-HTLC cases (fundee-owned HTLCs included, other side's excluded), and refusal of out-of-range and cushioned true-negative totals. It follows the hsmd/test/run-bad-request-close.c pattern of including the daemon .c (with its main renamed) to unit-test statics; channeld had no unit-test home that could otherwise reach this function. test_splice_fundee_with_balance exercises a splice on top of a fundee that already holds 500k sat; the rest of tests/test_splicing.py (25 tests) passes unchanged with stock hsmd.

relative_splice_balance_fundee() wrapped the satoshi-denominated
opener_relative/accepter_relative splicing fields in amount_msat()
unchanged, but the hsmd_setup_channel push_value field it feeds is an
amount_msat: the fundee's post-splice balance reached hsmd
under-reported by 1000x.

A stock hsmd never notices (it signs whatever it is asked to), but any
hsmd/signer implementation that validates the balances reported to it
reads the honest post-splice commitment as an overpayment and refuses
to sign it, wedging fundee-side splices.

Convert with amount_sat_to_msat() instead, matching every other
consumer of these fields (amount_msat_add_sat_s64), and fail the peer
on a negative contribution, which is never valid here.

Add channeld/test/run-splice_fundee_msat.c, which pins byte-exact
through the real static function that an X-sat contribution is
reported as X*1000 msat (never X msat), and that negative and
overflowing contributions fail the peer.

Fixes: 4649bcc
Changelog-Fixed: channeld: splicing was reporting the fundee's post-splice balance to hsmd in satoshis instead of millisatoshis.
Signed-off-by: Amperstrand <amperstrand@users.noreply.github.com>
hsmd_setup_channel's push_value is the fundee's balance at the start
of a channel era: at channel open that is the pushed amount, and for a
splice it is the fundee's pre-splice balance plus its funding
contribution.  Reporting a splice-role-selected contribution alone is
wrong twice over:

- The contribution was selected by SPLICE role, but the fundee is
  defined by CHANNEL role: the side that did not open the channel.
  The splice initiator is not necessarily the channel opener, so
  whenever the channel fundee is reporting -- whether it initiated the
  splice or accepted an opener-initiated one -- the splice roles are
  inverted with respect to the channel roles and the channel opener's
  contribution was reported instead of the fundee's.

- A fundee that already held a balance at splice time was
  under-reported by exactly that balance.

Neither defect is caught by the existing tests: stock hsmd signs
unconditionally, so no test observes what hsmd was told.

Select the contribution by channel opener role and add the fundee's
pre-splice balance from the channel view (view[].owed[], an upper
bound of the fundee's first post-splice commitment output, since
pending HTLCs only reduce it).  A negative contribution is a
splice-out and subtracts; only an out-of-range result fails the peer.

Extend the unit test with the channel/splice role matrix and the
pre-splice balance cases, and add test_splice_fundee_with_balance,
which routes 500k sat to the fundee before splicing in on top, so the
reported balance is exercised on top of a pre-splice balance.

Fixes: 4649bcc
Changelog-Fixed: splicing: hsmd is now given the fundee's full post-splice balance, not just its funding contribution.
Signed-off-by: Amperstrand <amperstrand@users.noreply.github.com>
@Amperstrand Amperstrand changed the title channeld: fix the fundee splice balance reported to hsmd (units, role, prior balance) channeld: report the fundee's full post-splice balance to hsmd in msat Sep 30, 2026
hsmd_setup_channel's push_value is the fundee's balance at the start of
the new channel era, and a fundee may hold HTLCs in flight when the
splice is set up.  The settled owed[] alone under-reports the fundee in
one resolution direction: a fundee-owned HTLC that fails back returns
its escrow to the fundee, raising its output above the settled
at-setup balance.  A signer that validates the reported balance then
reads the honest post-splice commitment as an overpayment and refuses
it, wedging the splice.

Add the HTLCs pending at splice setup and attributable to the fundee
(bucketed by owner side, the same bucketing check_balances uses for
its pending_htlcs; callers run after check_balances so the set is
final) to the total.  The other side's pending HTLCs never count.  A
negative total -- settled + pending + contribution below zero --
remains a genuine over-draw and fails the peer; nothing is wrapped or
clamped.

Rails: owed + fundee-owned pending + contribution lands exactly (both
role shapes, other side's HTLCs excluded; RED against the settled-only
total: 400M vs 500M msat), and a cushioned true-negative total
refuses.

Changelog-Fixed: splicing: the fundee balance reported to hsmd now includes the fundee's pending HTLCs, not just its settled balance.
Signed-off-by: Amperstrand <amperstrand@users.noreply.github.com>

This branch has not been deployed

No deployments
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