-
Notifications
You must be signed in to change notification settings - Fork 276
Rework the TransactionWithInputInfo architecture
#3074
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This was referenced May 7, 2025
sstone
reviewed
May 12, 2025
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
Show resolved
Hide resolved
They will be added in the upcoming Taproot preparatory PRs.
sstone
approved these changes
May 12, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
This is (hopefully) the last refactoring step before we can seriously start integrating taproot changes 🤞
Our
TransactionWithInputInfoclass hasn't been refactored in years: the main issue with it (IMO) is that we stored theredeemScriptwith 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 theredeemScriptinInputInfoand 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,SpliceTxandClosingTx) 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 theTxOwnerof aSpliceTx?)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.scalaclass, 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.