-
Notifications
You must be signed in to change notification settings - Fork 276
Add high-level helpers for taproot channels #3028
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
Conversation
t-bast
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest I think the changes to Transactions.scala show that our existing architecture doesn't work at all for both segwit and taproot...it's frankly a big red flag that it may be worth rethinking the architecture of those types more in-depth now.
I don't have the solution, but I think we should spend more time prototyping bigger re-works of this architecture to be more future-proof. I'll spend some time next week thinking about it on my side, and it would be good to involve @pm47 as well to make sure we get it right this time, and it will be easy to add the zero-fee commitment variants we want to introduce in the future as well.
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/OnChainFeeConf.scala
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.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
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
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
Outdated
Show resolved
Hide resolved
a544e88 to
51fb974
Compare
eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala
Outdated
Show resolved
Hide resolved
We add a new specific commitment format for taproot channels, and high-level methods for creating and spending taproot channel transactions.
|
Squashed and rebased on master at 650681f |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I know this may feel like bikeshedding, but it really isn't: the type architecture isn't completely satisfying yet, there is some type duplication that shows that we haven't really found the right design to correctly support both segwitv0 and taproot.
I know it's painful to iterate and try various approaches, but it will save us a lot of time in the future: refactoring after the fact will be way more costly because we will need to migrate data and change a lot more code...so I'd rather spend more time now trying out variations of the types until we have something that really "fits" than merging something that has design smells. However the burden shouldn't be only on you: if you want me to spend more time trying out my refactorings until it fully compiles, I can spend time on this, just let me know!
eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala
Outdated
Show resolved
Hide resolved
eclair-core/src/main/scala/fr/acinq/eclair/transactions/Scripts.scala
Outdated
Show resolved
Hide resolved
| // commit tx witness is just a single 64 bytes signature | ||
| override val commitWeight = 960 | ||
| // HTLC output weight remains the same | ||
| override val htlcOutputWeight = 172 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Am I understanding correctly that those two values should be specified in Bolt 3 because that's required for both nodes to compute the same value for the commitment fees? I don't see it in the spec PR, it is worth adding a comment to make sure laolu adds them?
Also, I'm surprised that the htlcOutputWeight is the same for taproot channels: the txOut is now using p2tr which uses 35 bytes, while p2wsh uses 34 bytes? I'm surprised that the unit test passes...am I missing something?
The other values below are not strictly necessary since HTLC txs don't pay any fees in this commitment format, but they're useful for our fee estimation when we're doing CPFP.
| case class HtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-success" } | ||
| case class HtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends HtlcTx { override def desc: String = "htlc-timeout" } | ||
| case class HtlcDelayedTx(input: InputInfo, tx: Transaction) extends TransactionWithInputInfo { override def desc: String = "htlc-delayed" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: why the unnecessary change for all those class definitions below? I thought something had changed in the code itself but then realized it's just formatting? Can you revert to minimize the diff?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 0d4748d
| sealed trait CommitmentOutputLink[T <: CommitmentOutput] { | ||
| val output: TxOut | ||
| val commitmentOutput: T | ||
|
|
||
| def filter[R <: CommitmentOutput : ClassTag]: Option[CommitmentOutputLink[R]] = commitmentOutput match { | ||
| case r: R => Some(this.asInstanceOf[CommitmentOutputLink[R]]) | ||
| case _ => None | ||
| } | ||
| } | ||
|
|
||
| /** Type alias for a collection of commitment output links */ | ||
| type CommitmentOutputs = Seq[CommitmentOutputLink[CommitmentOutput]] | ||
|
|
||
| object CommitmentOutputLink { | ||
| case class SegwitLink[T <: CommitmentOutput : ClassTag](output: TxOut, redeemScript: Seq[ScriptElt], commitmentOutput: T) extends CommitmentOutputLink[T] | ||
|
|
||
| case class TaprootLink[T <: CommitmentOutput : ClassTag](output: TxOut, internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree], commitmentOutput: T) extends CommitmentOutputLink[T] | ||
|
|
||
| def apply[T <: CommitmentOutput : ClassTag](output: TxOut, redeemScript: Seq[ScriptElt], commitmentOutput: T): SegwitLink[T] = SegwitLink(output, redeemScript, commitmentOutput) | ||
|
|
||
| def apply[T <: CommitmentOutput : ClassTag](output: TxOut, internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree], commitmentOutput: T): TaprootLink[T] = TaprootLink(output, internalKey, scriptTree_opt, commitmentOutput) | ||
|
|
||
| def apply[T <: CommitmentOutput : ClassTag](output: TxOut, internalKey: XonlyPublicKey, scriptTree: ScriptTree, commitmentOutput: T): TaprootLink[T] = TaprootLink(output, internalKey, Some(scriptTree), commitmentOutput) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a design smell, we shouldn't have to duplicate all of those classes...it took me a while to figure out what exactly was wrong, here was my thought process:
- the only information that you actually want to abstract away is the "redeem" details for this output
- for pay2wsh this was simply
redeemScript: Seq[ScriptElt] - for taproot is is the internal key and the redeem path (key path or script path, with script tree details)
- for pay2wsh this was simply
- instead of duplicating
CommitmentOutputLink, that means we should simply refactor this field - we actually already did almost that with
InputInfo, but the issue is that it also contains theoutPoint, which we don't know yet when creating the commit-tx, leading to a chicken-and-egg situation - but that just means we didn't factor the right layer when doing that change for
InputInfo(my bad!): we should theoutPointandtxOutfields ofInputInfoended up duplicated inSegwitInputandTaprootInput, which was a hint that this wasn't the right abstraction!
My suggestion is thus to rework the InputInfo hierarchy as follows:
/** This trait contains redeem information necessary to spend different types of segwit inputs. */
sealed trait RedeemInfo
object RedeemInfo {
/**
* @param redeemScript the actual script must be known to redeem p2wsh inputs.
*/
case class P2wsh(redeemScript: ByteVector) extends RedeemInfo
object P2wsh {
def apply(redeemScript: Seq[ScriptElt]): P2wsh = P2wsh(Script.write(redeemScript))
}
/**
* @param internalKey the private key associated with this public key will be used to sign.
* @param scriptTree_opt the script tree must be known if there is one, even when spending via the key path.
*/
case class TaprootKeyPath(internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree]) extends RedeemInfo
/**
* @param internalKey we need the internal key, even if we don't have the private key, to spend via a script path.
* @param scriptTree we need the complete script tree to spend taproot inputs.
* @param leafHash hash of the leaf script we're spending (must belong to the tree).
*/
case class TaprootScriptPath(internalKey: XonlyPublicKey, scriptTree: ScriptTree, leafHash: ByteVector32) extends RedeemInfo {
require(findScript(scriptTree, leafHash).nonEmpty, "script tree must contain the provided leaf")
/**
* TODO: this won't be needed once findScript is added to bitcoin-kmp, remove when updating
* @return the leaf that matches `leafHash`
*/
private def findScript(scriptTree: ScriptTree, leafHash: ByteVector32): Option[ScriptTree.Leaf] = scriptTree match {
case l: ScriptTree.Leaf => if (l.hash() == KotlinUtils.scala2kmp(leafHash)) Some(l) else None
case b: ScriptTree.Branch => findScript(b.getLeft, leafHash) orElse findScript(b.getRight, leafHash)
}
}
}
case class InputInfo(outPoint: OutPoint, txOut: TxOut, redeemInfo: RedeemInfo) {
val isTaproot: Boolean = redeemInfo match {
case _: RedeemInfo.P2wsh => false
case _: RedeemInfo.TaprootKeyPath => true
case _: RedeemInfo.TaprootScriptPath => true
}
}This way the CommitmentOutputLink can simply be changed to:
case class CommitmentOutputLink[T <: CommitmentOutput](output: TxOut, redeemInfo: RedeemInfo, commitmentOutput: T)Note that I haven't tried applying this until everything compiled, so I may have missed something. If you want me to spend more time prototyping this on top of your branch, let me know.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll try and improve these types. The main issue is that a taproot output can be spent in may different ways (through a script leaf, or the key path with the revocation key for example) and this is specified when it is spent, not when it is created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm experimenting with
- a new output type, with subtypes for segwit and taproot
- a new input type, that takes one of the input types defined above and specifies how it should be spent
sealed trait OutputSpendingInfo
object OutputSpendingInfo {
case class Segwit(redeemScript: Seq[ScriptElt]) extends OutputSpendingInfo
case class Taproot(internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree]) extends OutputSpendingInfo
}
sealed trait InputSpendingInfo
object InputSpendingInfo {
case class Segwit(output: OutputSpendingInfo.Segwit) extends InputSpendingInfo
object Segwit {
def apply(redeemScript: Seq[ScriptElt]) = new Segwit(OutputSpendingInfo.Segwit(redeemScript))
def apply(redeemScript: ByteVector) = new Segwit(OutputSpendingInfo.Segwit(Script.parse(redeemScript)))
}
case class TaprootKeyPath(output: OutputSpendingInfo.Taproot) extends InputSpendingInfo
object TaprootKeyPath {
def apply(internalKey: XonlyPublicKey, scriptTree_opt: Option[ScriptTree]) = new TaprootKeyPath(OutputSpendingInfo.Taproot(internalKey, scriptTree_opt))
}
case class TaprootScriptPath(output: OutputSpendingInfo.Taproot, leafHash: ByteVector32) extends InputSpendingInfo {
val scriptTree: ScriptTree = output.scriptTree_opt.getOrElse(throw new IllegalArgumentException("missing taproot script"))
val leaf: ScriptTree.Leaf = TaprootScriptPath.findScript(scriptTree, leafHash).getOrElse(throw new IllegalArgumentException("script tree must contain the provided leaf"))
}
object TaprootScriptPath {
import KotlinUtils._
def apply(internalKey: XonlyPublicKey, scriptTree: ScriptTree, leafHash: ByteVector32) = new TaprootScriptPath(OutputSpendingInfo.Taproot(internalKey, Some(scriptTree)), leafHash)
/**
* TODO: this won't be needed once findScript is added to bitcoin-kmp, remove when updating bitcoin-kmp
* @return the leaf that matches `leafHash`
*/
def findScript(scriptTree: ScriptTree, leafHash: ByteVector32): Option[ScriptTree.Leaf] = scriptTree match {
case l: ScriptTree.Leaf => if (l.hash() == scala2kmp(leafHash)) Some(l) else None
case b: ScriptTree.Branch => findScript(b.getLeft, leafHash) orElse findScript(b.getRight, leafHash)
}
}
}
case class InputInfo(outPoint: OutPoint, txOut: TxOut, spendingInfo: InputSpendingInfo)and for CommitmentOutputLink:
case class CommitmentOutputLink[T <: CommitmentOutput](output: TxOut, outputSpendingInfo: OutputSpendingInfo, commitmentOutput: T)I'll publish a new branch as soon as everything compiles and all tests pass
| case InputInfo.SegwitInput(_, _, redeemScript) => Scripts.witnessToLocalDelayedWithRevocationSig(revocationSig, redeemScript) | ||
| case t: InputInfo.TaprootInput => | ||
| t.redeemPath match { | ||
| case RedeemPath.ScriptPath(scriptTree: ScriptTree.Branch, _) => Script.witnessScriptPathPay2tr(t.internalKey, scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(revocationSig)), scriptTree) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why don't you use the leaf field of ScriptPath directly? This comment applies to all of those addSigs methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 788d492
| //assert(Try(claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, Map.empty)).isFailure) | ||
| //assert(Try(claimAnchorOutputTx.sign(anchorKey, TxOwner.Local, commitmentFormat, walletInputs.take(1))).isFailure) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you restore this? You've got some commented lines in this file that look like they need some clean-up after you experimented with stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done in 0d4748d
| def assertWitnessWeightMatches(witness: ScriptWitness, expectedWeight: Int, commitmentFormat: CommitmentFormat): Unit = | ||
| assertWeightMatches(164 + ScriptWitness.write(witness).size.toInt, expectedWeight, commitmentFormat) | ||
|
|
||
| def generateCommitAndHtlcTxs(commitmentFormat: CommitmentFormat): Unit = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice, having this common function that we can run for all commitment formats is going to be very useful!
|
closed in favour of #3075 |
We add a new specific commitment format for taproot channels, and high-level methods for creating and spending taproot channel transactions.
There are no functional changes: protocol messages and codecs have not been updated to support taproot channels, these new methods are only used in unit tests.