Skip to content

Conversation

@t-bast
Copy link
Member

@t-bast t-bast commented Apr 30, 2025

This is (hopefully) the last refactoring step before we can seriously start integrating taproot changes 🤞

Our TransactionWithInputInfo class hasn't been refactored in years: the main issue with it (IMO) is that we stored the redeemScript with each transaction, which only work for segwit v0 inputs, and was hiding the actual fields that made sense to include in each transaction class. This PR is changing this to get rid of the redeemScript in InputInfo and update each transaction to include the parameters necessary to rebuild scripts and witnesses at runtime, based only on the commitment format and channel/commitment keys.

Importantly, we split transactions that spend the channel funding output (CommitTx, SpliceTx and ClosingTx) from the force-close transactions. They don't have the same semantics since the first category always needs signatures from both channel participants, whereas the second category can always be unilaterally signed. This lets us get rid of several unused function arguments for some transaction types, which were creating confusion (e.g. why do I need to care about the TxOwner of a SpliceTx?)

This PR should be reviewed commit-by-commit. The first and third commits are the hardest: I didn't find a better way to split them into smaller commits that would still be consistent and make all the unit tests pass, sorry...please read each commit message, which contains instructions on were the important changes are!

It can also be quite useful to start the review by looking at the final state of the Transactions.scala class, without even looking at the diff. The goal is that the final state of this file is easy to read, that each class and function has parameters that obviously make sense, and cannot be misused. Then reviewing commit-by-commit to see how we get there will make more sense.

t-bast added 5 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.
@t-bast t-bast requested a review from sstone April 30, 2025 15:05
t-bast added 2 commits May 2, 2025 15:35
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.
t-bast and others added 6 commits May 5, 2025 17:40
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.
They will be added in the upcoming Taproot preparatory PRs.
@t-bast t-bast merged commit 2ada7e7 into master May 12, 2025
1 check passed
@t-bast t-bast deleted the refactor-tx-signing branch May 12, 2025 14:12
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.

3 participants