Skip to content

Add low-level tooling support for taproot channels#3079

Closed
sstone wants to merge 15 commits intomasterfrom
refactor-replaceable-tx-publisher-taproot
Closed

Add low-level tooling support for taproot channels#3079
sstone wants to merge 15 commits intomasterfrom
refactor-replaceable-tx-publisher-taproot

Conversation

@sstone
Copy link
Member

@sstone sstone commented May 7, 2025

This PR:

  • adds a new commitment format for taproot channels
  • extends our transaction signing/verification classes to support this new format

It does not add new wire messages or modify the channel creation/update workflow.

Based on #3075

t-bast and others added 15 commits April 28, 2025 11:23
Most of our instances of `TransactionWithInputInfo` only contained the
input (`InputInfo` instance) and the transaction. This worked because
we stored the (segwit v0) redeem script in the `InputInfo` instance.
But we can actually get rid of that if we store with each transaction
the parameters that allow re-creating the scripts at runtime.

We don't need many parameters, so this is quite simple to add:

- delayed txs simply need the `to_self_delay` value used
- HTLC txs simply need the `payment_hash` and `htlc_expiry`

Based on that we can trivially re-build scripts and redeem info at
runtime using `ChannelKeys`. This ensures that we will only need the
`commitmentFormat` to know the exact script that is being used, instead
of having to encode different redeem information in `InputInfo`. This
will also reduce the size of our channel data, because storing redeem
info can be removed.

In order to handle data that was encoded before this change, we:

- update `ChannelCodecs4.scala`: this is quite a simple change, but this
  file is getting messy since we made many changes to transaction codecs
  so please review carefully
- when re-connecting a channel, if it contains transactions that are
  missing some data, we simply re-create those transactions based on
  the commitment data
We don't need this helper for other transactions: it is only used to
verify the remote signatures received in `commit_sig`.
In this commit, we somewhat heavily re-design `Transactions.scala`. The
main change is that we stop exposing the default `sign` function that
was too abstract for callers, and have each `TransactionWithInputInfo`
expose its dedicated `sign` function, with simpler arguments depending
on each transaction type.

One very important thing to note is that there is a heavy distinction
between transactions that require both a local and a remote signature
(commit-tx, splice-tx, closing-tx and htlc-tx) and transactions that
only need a local signature. This means we can completely remove the
`addSigs` step for transactions that only need a local signature, and
directly create signed signatures, which simplifies the calling code.

This also lets us better encapsulate the `extraUtxos` parameter, which
only makes sense for txs for which we add wallet inputs, which is only
a small subset of our transactions.

The previous commits ensured that we store the data we need for each
transaction type to allow re-deriving and signing everything based
only on the following runtime data:

- the commitment format
- the commitment funding keys
- the commitment keys

This makes the calling code much less error-prone and removes the need
to store the redeem script in the `InputInfo` class, which wouldn't
have worked for taproot transactions. Note that for simplicity's sake,
we currently keep the field (to avoid a painful codecs migration) since
we will introduce v5 codecs when implementing taproot channels, which
will let us clean up this inconsistency.

We also move each transaction's creation function in its companion
object: this makes the diff a bit harder to review, but ensures that
each transaction's implementation is well encapsulated, which makes
it easy to review that each transaction type is correctly implemented.

Instead of using placeholder signatures when evaluating the weight of
transactions, we actually sign them. This means we sign twice, but is
not a performance issue in this case because it only happens once when
we initiate a force-close, and simplifies the code.

We also introduce a `RedeemInfo` trait which makes the code forward
compatible with taproot channels. The changes to `Transactions.scala`
are a lot to review, but unfortunately I didn't find a good way to
split them while having something consistent and all the unit tests
passing...
We also rename this function, which didn't make any sense: we are not
checking that this transaction is spendable (which means that we could
spend its *outputs*), but rather that the transaction is fully signed
and valid.
Transactions that spend the channel funding output don't behave the
same way as transactions that can be unilaterally signed, so it makes
sense to use different traits. This lets us better scope our shared
code and remove unused parameters (e.g. sighash is much simpler for
transactions spending the channel funding output and the concept of
`TxOwner` is unnecessary).

We also take this opportunity to explicitly separate the two distinct
signing cases for `ChannelSpendTransaction`: 2-of-2 multisig and musig2,
which create different signature types. By pattern matching on the
commitment format, callers should decide whether they want to produce
an individual signature or a partial signatures. This may need some
rework to fully adapt to taproot, but it has the benefit of exposing
which part of the code should be concerned about the type of sigs it
creates.
We previously hard-coded the input index to always be 0, which actually
created issues with replaceable txs for which we added wallet inputs.
We don't need to hard-code this value, we can always find the input
index by matching the `outPoint`.
Anchor signing functions will either use the local or remote commitment
keys for taproot commitments, depending on whether the local or remote
commitment has been broadcast.
We can actually use pre-computed weights for every type of force-close
transaction, which simplifies the code and ensures that the tests use
deterministic fees, regardless of the signature size (since ECDSA sigs
are der-encoded, they have a variable size which slightly impacts the
witness weight).

Note that we use per-transaction constants, even though some have the
same weight: this is more future-proof since future commitment formats
may use different script combinations.
Add the actual commit tx output to CommitmentOutput.
Add the 2nd-level HTLC output to InHtlc/OutHtlc.
We remove the re-creation of closing transactions on restart in favor
of a cleaner codec migration. We add test vectors generated from the
`master` branch.
We previously included confirmation targets directly in HTLC and anchor
transactions, which didn't really belong there. We now instead set
confirmation targets at publication time, based on the current state of
the force-close attempt.

We also extract witness data for HTLCs (preimage and remote signature)
when creating the command to publish the transaction, instead of doing
it inside the publisher actors.

We improve the checks done in the pre-publisher actors, which now take
into account the min-depth for commit txs before aborting anchor txs.
The `ReplaceableTxFunder` is also simpler now, thanks to a better
encapsulation of the transaction's contents.
@sstone sstone requested a review from t-bast May 7, 2025 16:09
@t-bast
Copy link
Member

t-bast commented May 12, 2025

This is looking good, I think it works well with the new transaction architecture, what do you think? I don't think this should be merged into #3075 though, this is quite unrelated and should rather be an independent PR that we merge after #3075?

@sstone
Copy link
Member Author

sstone commented May 12, 2025

This is looking good, I think it works well with the new transaction architecture, what do you think? I don't think this should be merged into #3075 though, this is quite unrelated and should rather be an independent PR that we merge after #3075?

Yes it makes sense, I used #3075 to validate that it works well with taproot (and it does!).

@sstone sstone changed the base branch from refactor-replaceable-tx-publisher to master May 12, 2025 07:46
@sstone
Copy link
Member Author

sstone commented May 15, 2025

Replaced by #3086

@sstone sstone closed this May 15, 2025
@sstone sstone deleted the refactor-replaceable-tx-publisher-taproot branch July 1, 2025 08:10
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.

2 participants