Skip to content

Add low-level taproot helpers #3086

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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
Open

Add low-level taproot helpers #3086

wants to merge 10 commits into from

Conversation

sstone
Copy link
Member

@sstone sstone commented May 15, 2025

We add a new commitment format for simple taproot channels, and implement creating and verifying musig2 and taproot signatures for these channels.
Wire format and channel creation/update workflows have not been modified, this will be done in another PR.

@sstone sstone requested a review from t-bast May 15, 2025 13:26
@sstone sstone marked this pull request as ready for review May 15, 2025 13:26
Copy link
Member

@t-bast t-bast left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a bit more work to do around taproot scripts and witnesses to make it cleaner by introducing better types and encapsulation of script trees and their corresponding witnesses. My main comment that requires more work is #3086 (comment), the rest is mostly nits for now.

Copy link
Member

@t-bast t-bast left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking much better than the attempts we made before the transactions architecture refactoring, that was worth it! I think we're almost there, but can still simplify it a bit.

@t-bast
Copy link
Member

t-bast commented May 22, 2025

Needs rebase!

sstone added 8 commits May 22, 2025 10:10
There are no functional changes, we just define a new format that is not used anywhere yet, with dummy "not implemented" blocks when needed.
We can create, sign, spend commit and htlc transactions for the new simple taproot channels commitment format.
Wire messages and channel creation/update workflow have not been modified.
We only either single leaves or branches with 2 single leaves (a timeout and a success branch for example), having
specific types for them makes to code easier to read.
@sstone sstone force-pushed the add-taproot-helpers branch from 668da46 to 8fcc0f9 Compare May 22, 2025 08:30
Comment on lines 243 to 246
def assertWeightMatches(actualWeight: Int, expectedWeight: Int, commitmentFormat: CommitmentFormat): Unit = commitmentFormat match {
case DefaultCommitmentFormat | _: AnchorOutputsCommitmentFormat => assert(Math.abs(actualWeight - expectedWeight) < 20)
case SimpleTaprootChannelCommitmentFormat => assert(actualWeight == expectedWeight)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This unused function still hasn't been removed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in c2d5efe

val walletPriv = randomKey()
val walletPub = walletPriv.publicKey
val finalPubKeyScript = Script.write(Script.pay2wpkh(walletPub))
val commitInput = Funding.makeFundingInputInfo(randomTxId(), 0, Btc(1), localFundingPriv.publicKey, remoteFundingPriv.publicKey, UnsafeLegacyAnchorOutputsCommitmentFormat)
// funding tx sends to musig2 aggregate of local and remote funding keys
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is wrong for non-taproot cases, we should probably just remove it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in c2d5efe

localPartialSig <- txInfo.partialSign(localFundingPriv, remoteFundingPriv.publicKey, Map.empty, LocalNonce(secretLocalNonce, publicLocalNonce), publicNonces)
remotePartialSig <- txInfo.partialSign(remoteFundingPriv, localFundingPriv.publicKey, Map.empty, LocalNonce(secretRemoteNonce, publicRemoteNonce), publicNonces)
_ = assert(txInfo.checkRemotePartialSignature(localFundingPriv.publicKey, remoteFundingPriv.publicKey, remotePartialSig, publicLocalNonce))
invalidRemotePartialSig = remotePartialSig.copy(remotePartialSig.partialSig.reverse, remotePartialSig.nonce)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this call to copy is a bit weird, you're not specifying what you're copying?

Suggested change
invalidRemotePartialSig = remotePartialSig.copy(remotePartialSig.partialSig.reverse, remotePartialSig.nonce)
invalidRemotePartialSig = remotePartialSig.copy(partialSig = remotePartialSig.partialSig.reverse)

Or maybe:

Suggested change
invalidRemotePartialSig = remotePartialSig.copy(remotePartialSig.partialSig.reverse, remotePartialSig.nonce)
invalidRemotePartialSig = ChannelSpendSignature.PartialSignatureWithNonce(randomBytes32(), remotePartialSig.nonce)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in c2d5efe

@@ -148,6 +149,29 @@ object Transactions {
*/
case object ZeroFeeHtlcTxAnchorOutputsCommitmentFormat extends AnchorOutputsCommitmentFormat

sealed trait TaprootCommitmentFormat extends CommitmentFormat

case object SimpleTaprootChannelCommitmentFormat extends TaprootCommitmentFormat {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably become a trait since we want two support a variation of the official commitment format where HTLC txs pay the same feerate as the commit tx, like what we do for anchor outputs (for mobile wallets until we're able to move to v3).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in 6ec1d0c

@@ -183,8 +210,10 @@ object Transactions {
* @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 Taproot {
require(Option(scriptTree.findScript(KotlinUtils.scala2kmp(leafHash))).nonEmpty, "script tree must contain the provided leaf")
val redeemScript: ByteVector = KotlinUtils.kmp2scala(scriptTree.findScript(KotlinUtils.scala2kmp(leafHash)).getScript)
import KotlinUtils._
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We import KotlinUtils 11 times in this file, why not do it a single time for the whole file in the main imports?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in c2d5efe

RedeemInfo.P2wsh(redeemScript)
case SimpleTaprootChannelCommitmentFormat =>
val receivedHtlcTree = Taproot.receivedHtlcScriptTree(commitKeys, paymentHash, htlcExpiry)
RedeemInfo.TaprootScriptPath(commitKeys.revocationPublicKey.xOnly, receivedHtlcTree.scriptTree, KotlinUtils.kmp2scala(receivedHtlcTree.success.hash()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can remove the explicit KotlinUtils.kmp2scala if we import KotlinUtils at the file root. Same for other similar calls in this file.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in c2d5efe

Comment on lines 1368 to 1376
case SimpleTaprootChannelCommitmentFormat =>
if (toLocalAmount >= dustLimit || hasHtlcs) {
val redeemInfo = RedeemInfo.TaprootKeyPath(commitmentKeys.localDelayedPaymentPublicKey.xOnly, Some(Taproot.anchorScriptTree))
outputs.append(ToLocalAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, redeemInfo.pubkeyScript)))
}
if (toRemoteAmount >= dustLimit || hasHtlcs) {
val redeemInfo = RedeemInfo.TaprootKeyPath(commitmentKeys.remotePaymentPublicKey.xOnly, Some(Taproot.anchorScriptTree))

outputs.append(ToRemoteAnchor(TxOut(AnchorOutputsCommitmentFormat.anchorAmount, redeemInfo.pubkeyScript)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we group AnchorOutputsCommitmentFormat and SimpleTaprootChannelCommitmentFormat? Most of the logic is the same, it's only the redeemInfo construction that changes, but we already have redeemInfo helpers exposed on the ClaimAnchorOutputTx?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in c2d5efe

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