1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-12 19:01:39 +01:00

Add scripts for taproot channels (#3016)

Add scripts for taproot channels

These scripts have been generated with miniscript and are different from the ones included in the "simple taproot channels" proposal at  e25132d8de.

The `to-revoke-script` is unchanged and not "miniscript compatible", because it includes a NOOP push of the local delayed payment pubkey, which is needed to spend the anchor output once the 16-blocks CSV delay has passed.
This commit is contained in:
Fabrice Drouin 2025-02-27 09:40:31 +01:00 committed by GitHub
parent 945623643f
commit cae22d71be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 516 additions and 11 deletions

View file

@ -17,15 +17,18 @@
package fr.acinq.eclair.transactions
import fr.acinq.bitcoin.Script.LOCKTIME_THRESHOLD
import fr.acinq.bitcoin.ScriptTree
import fr.acinq.bitcoin.SigHash._
import fr.acinq.bitcoin.TxIn.{SEQUENCE_LOCKTIME_DISABLE_FLAG, SEQUENCE_LOCKTIME_MASK, SEQUENCE_LOCKTIME_TYPE_FLAG}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, XonlyPublicKey}
import fr.acinq.bitcoin.scalacompat.Script._
import fr.acinq.bitcoin.scalacompat._
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
import fr.acinq.eclair.{BlockHeight, CltvExpiry, CltvExpiryDelta}
import scodec.bits.ByteVector
import scala.jdk.CollectionConverters.SeqHasAsJava
/**
* Created by PM on 02/12/2016.
*/
@ -44,15 +47,13 @@ object Scripts {
case _: AnchorOutputsCommitmentFormat => SIGHASH_SINGLE | SIGHASH_ANYONECANPAY
}
def multiSig2of2(pubkey1: PublicKey, pubkey2: PublicKey): Seq[ScriptElt] =
if (LexicographicalOrdering.isLessThan(pubkey1.value, pubkey2.value)) {
Script.createMultiSigMofN(2, Seq(pubkey1, pubkey2))
} else {
Script.createMultiSigMofN(2, Seq(pubkey2, pubkey1))
}
/** Sort public keys using lexicographic ordering. */
def sort(pubkeys: Seq[PublicKey]): Seq[PublicKey] = pubkeys.sortWith { (a, b) => LexicographicalOrdering.isLessThan(a.value, b.value) }
def multiSig2of2(pubkey1: PublicKey, pubkey2: PublicKey): Seq[ScriptElt] = Script.createMultiSigMofN(2, sort(Seq(pubkey1, pubkey2)))
/**
* @return a script witness that matches the msig 2-of-2 pubkey script for pubkey1 and pubkey2
* @return a script witness that matches the [[multiSig2of2]] pubkey script for pubkey1 and pubkey2
*/
def witness2of2(sig1: ByteVector64, sig2: ByteVector64, pubkey1: PublicKey, pubkey2: PublicKey): ScriptWitness =
if (LexicographicalOrdering.isLessThan(pubkey1.value, pubkey2.value)) {
@ -297,4 +298,248 @@ object Scripts {
def witnessHtlcWithRevocationSig(revocationSig: ByteVector64, revocationPubkey: PublicKey, htlcScript: ByteVector) =
ScriptWitness(der(revocationSig) :: revocationPubkey.value :: htlcScript :: Nil)
/**
* Specific scripts for taproot channels
*/
object Taproot {
import KotlinUtils._
implicit def scala2kmpscript(input: Seq[fr.acinq.bitcoin.scalacompat.ScriptElt]): java.util.List[fr.acinq.bitcoin.ScriptElt] = input.map(e => scala2kmp(e)).asJava
/**
* Taproot signatures are usually 64 bytes, unless a non-default sighash is used, in which case it is appended.
*/
def encodeSig(sig: ByteVector64, sighashType: Int = SIGHASH_DEFAULT): ByteVector = sighashType match {
case SIGHASH_DEFAULT | SIGHASH_ALL => sig
case _ => sig :+ sighashType.toByte
}
/**
* Sort and aggregate the public keys of a musig2 session.
*
* @param pubkey1 public key
* @param pubkey2 public key
* @return the aggregated public key
* @see [[fr.acinq.bitcoin.scalacompat.Musig2.aggregateKeys()]]
*/
def musig2Aggregate(pubkey1: PublicKey, pubkey2: PublicKey): XonlyPublicKey = Musig2.aggregateKeys(sort(Seq(pubkey1, pubkey2)))
/**
* "Nothing Up My Sleeve" point, for which there is no known private key.
*/
val NUMS_POINT = PublicKey(ByteVector.fromValidHex("02dca094751109d0bd055d03565874e8276dd53e926b44e3bd1bb6bf4bc130a279"))
// miniscript: older(16)
private val anchorScript: Seq[ScriptElt] = OP_16 :: OP_CHECKSEQUENCEVERIFY :: Nil
val anchorScriptTree = new ScriptTree.Leaf(anchorScript)
/**
* Script used for local or remote anchor outputs.
*
* @param paymentPubkey local or remote payment key.
*/
def anchor(paymentPubkey: PublicKey): Seq[ScriptElt] = {
Script.pay2tr(paymentPubkey.xOnly, Some(anchorScriptTree))
}
/**
* Script that can be spent with the revocation key and reveals the delayed payment key to allow observers to claim
* unused anchor outputs.
*
* miniscript: this is not miniscript compatible
*
* @param localDelayedPaymentPubkey local delayed key
* @param revocationPubkey revocation key
* @return a script that will be used to add a "revocation" leaf to a script tree
*/
private def toRevocationKey(localDelayedPaymentPubkey: PublicKey, revocationPubkey: PublicKey): Seq[ScriptElt] = {
OP_PUSHDATA(localDelayedPaymentPubkey.xOnly) :: OP_DROP :: OP_PUSHDATA(revocationPubkey.xOnly) :: OP_CHECKSIG :: Nil
}
/**
* Script that can be spent by the owner of the commitment transaction after a delay.
*
* miniscript: and_v(v:pk(delayed_key),older(delay))
*
* @param localDelayedPaymentPubkey delayed payment key
* @param toSelfDelay to-self CSV delay
* @return a script that will be used to add a "to local key" leaf to a script tree
*/
private def toLocalDelayed(localDelayedPaymentPubkey: PublicKey, toSelfDelay: CltvExpiryDelta): Seq[ScriptElt] = {
OP_PUSHDATA(localDelayedPaymentPubkey.xOnly) :: OP_CHECKSIGVERIFY :: Scripts.encodeNumber(toSelfDelay.toInt) :: OP_CHECKSEQUENCEVERIFY :: Nil
}
/**
*
* @param revocationPubkey revocation key
* @param toSelfDelay to-self CSV delay
* @param localDelayedPaymentPubkey local delayed payment key
* @return a script tree with two leaves (to self with delay, and to revocation key)
*/
def toLocalScriptTree(revocationPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey): ScriptTree.Branch = {
new ScriptTree.Branch(
new ScriptTree.Leaf(toLocalDelayed(localDelayedPaymentPubkey, toSelfDelay)),
new ScriptTree.Leaf(toRevocationKey(localDelayedPaymentPubkey, revocationPubkey)),
)
}
/**
* Script used for the main balance of the owner of the commitment transaction.
*/
def toLocal(localDelayedPaymentPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, revocationPubkey: PublicKey): Seq[ScriptElt] = {
Script.pay2tr(NUMS_POINT.xOnly, Some(toLocalScriptTree(revocationPubkey, toSelfDelay, localDelayedPaymentPubkey)))
}
/**
* Script that can be spent by the channel counterparty after a 1-block delay.
*
* miniscript: and_v(v:pk(remote_key),older(1))
*
* @param remotePaymentPubkey remote payment key
* @return a script that will be used to add a "to remote key" leaf to a script tree
*/
private def toRemoteDelayed(remotePaymentPubkey: PublicKey): Seq[ScriptElt] = {
OP_PUSHDATA(remotePaymentPubkey.xOnly) :: OP_CHECKSIGVERIFY :: OP_1 :: OP_CHECKSEQUENCEVERIFY :: Nil
}
/**
* Script tree used for the main balance of the remote node in our commitment transaction.
* Note that there is no need for a revocation leaf in that case.
*
* @param remotePaymentPubkey remote key
* @return a script tree with a single leaf (to remote key, with a 1-block CSV delay)
*/
def toRemoteScriptTree(remotePaymentPubkey: PublicKey): ScriptTree.Leaf = {
new ScriptTree.Leaf(toRemoteDelayed(remotePaymentPubkey))
}
/**
* Script used for the main balance of the remote node in our commitment transaction.
*/
def toRemote(remotePaymentPubkey: PublicKey): Seq[ScriptElt] = {
Script.pay2tr(NUMS_POINT.xOnly, Some(toRemoteScriptTree(remotePaymentPubkey)))
}
/**
* Script that can be spent when an offered (outgoing) HTLC times out.
* It is spent using a pre-signed HTLC transaction signed with both keys.
*
* miniscript: and_v(v:pk(local_htlc_key),pk(remote_htlc_key))
*
* @param localHtlcPubkey local HTLC key
* @param remoteHtlcPubkey remote HTLC key
* @return a script used to create a "HTLC timeout" leaf in a script tree
*/
private def offeredHtlcTimeout(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey): Seq[ScriptElt] = {
OP_PUSHDATA(localHtlcPubkey.xOnly) :: OP_CHECKSIGVERIFY :: OP_PUSHDATA(remoteHtlcPubkey.xOnly) :: OP_CHECKSIG :: Nil
}
/**
* Script that can be spent when an offered (outgoing) HTLC is fulfilled.
* It is spent using a signature from the receiving node and the preimage, with a 1-block delay.
*
* miniscript: and_v(v:hash160(H),and_v(v:pk(remote_htlc_key),older(1)))
*
* @param remoteHtlcPubkey remote HTLC key
* @param paymentHash payment hash
* @return a script used to create a "spend offered HTLC" leaf in a script tree
*/
private def offeredHtlcSuccess(remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32): Seq[ScriptElt] = {
// @formatter:off
OP_SIZE :: encodeNumber(32) :: OP_EQUALVERIFY ::
OP_HASH160 :: OP_PUSHDATA(Crypto.ripemd160(paymentHash)) :: OP_EQUALVERIFY ::
OP_PUSHDATA(remoteHtlcPubkey.xOnly) :: OP_CHECKSIGVERIFY ::
OP_1 :: OP_CHECKSEQUENCEVERIFY :: Nil
// @formatter:on
}
/**
* Script tree used for offered HTLCs.
*/
def offeredHtlcScriptTree(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32): ScriptTree.Branch = {
new ScriptTree.Branch(
new ScriptTree.Leaf(offeredHtlcTimeout(localHtlcPubkey, remoteHtlcPubkey)),
new ScriptTree.Leaf(offeredHtlcSuccess(remoteHtlcPubkey, paymentHash)),
)
}
/**
* Script used for offered HTLCs.
*/
def offeredHtlc(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32, revocationPubkey: PublicKey): Seq[ScriptElt] = {
Script.pay2tr(revocationPubkey.xOnly, Some(offeredHtlcScriptTree(localHtlcPubkey, remoteHtlcPubkey, paymentHash)))
}
/**
* Script that can be spent when a received (incoming) HTLC times out.
* It is spent using a signature from the receiving node after an absolute delay and a 1-block relative delay.
*
* miniscript: and_v(v:pk(remote_htlc_key),and_v(v:older(1),after(delay)))
*
* @param remoteHtlcPubkey remote HTLC key
* @param lockTime HTLC expiry
*/
private def receivedHtlcTimeout(remoteHtlcPubkey: PublicKey, lockTime: CltvExpiry): Seq[ScriptElt] = {
// @formatter:off
OP_PUSHDATA(remoteHtlcPubkey.xOnly) :: OP_CHECKSIGVERIFY ::
OP_1 :: OP_CHECKSEQUENCEVERIFY :: OP_VERIFY ::
encodeNumber(lockTime.toLong) :: OP_CHECKLOCKTIMEVERIFY :: Nil
// @formatter:on
}
/**
* Script that can be spent when a received (incoming) HTLC is fulfilled.
* It is spent using a pre-signed HTLC transaction signed with both keys and the preimage.
*
* miniscript: and_v(v:hash160(H),and_v(v:pk(local_key),pk(remote_key)))
*
* @param localHtlcPubkey local HTLC key
* @param remoteHtlcPubkey remote HTLC key
* @param paymentHash payment hash
*/
private def receivedHtlcSuccess(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32): Seq[ScriptElt] = {
// @formatter:off
OP_SIZE :: encodeNumber(32) :: OP_EQUALVERIFY ::
OP_HASH160 :: OP_PUSHDATA(Crypto.ripemd160(paymentHash)) :: OP_EQUALVERIFY ::
OP_PUSHDATA(localHtlcPubkey.xOnly) :: OP_CHECKSIGVERIFY ::
OP_PUSHDATA(remoteHtlcPubkey.xOnly) :: OP_CHECKSIG :: Nil
// @formatter:on
}
/**
* Script tree used for received HTLCs.
*/
def receivedHtlcScriptTree(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32, lockTime: CltvExpiry): ScriptTree.Branch = {
new ScriptTree.Branch(
new ScriptTree.Leaf(receivedHtlcTimeout(remoteHtlcPubkey, lockTime)),
new ScriptTree.Leaf(receivedHtlcSuccess(localHtlcPubkey, remoteHtlcPubkey, paymentHash)),
)
}
/**
* Script used for received HTLCs.
*/
def receivedHtlc(localHtlcPubkey: PublicKey, remoteHtlcPubkey: PublicKey, paymentHash: ByteVector32, lockTime: CltvExpiry, revocationPubkey: PublicKey): Seq[ScriptElt] = {
Script.pay2tr(revocationPubkey.xOnly, Some(receivedHtlcScriptTree(localHtlcPubkey, remoteHtlcPubkey, paymentHash, lockTime)))
}
/**
* Script tree used for the output of pre-signed HTLC 2nd-stage transactions.
*/
def htlcDelayedScriptTree(localDelayedPaymentPubkey: PublicKey, toSelfDelay: CltvExpiryDelta): ScriptTree.Leaf = {
new ScriptTree.Leaf(toLocalDelayed(localDelayedPaymentPubkey, toSelfDelay))
}
/**
* Script used for the output of pre-signed HTLC 2nd-stage transactions.
*
* @param localDelayedPaymentPubkey local delayed payment key
* @param toSelfDelay to-self CSV delay
* @param revocationPubkey revocation key
*/
def htlcDelayed(localDelayedPaymentPubkey: PublicKey, toSelfDelay: CltvExpiryDelta, revocationPubkey: PublicKey): Seq[ScriptElt] = {
Script.pay2tr(revocationPubkey.xOnly, Some(htlcDelayedScriptTree(localDelayedPaymentPubkey, toSelfDelay)))
}
}
}

View file

@ -17,15 +17,16 @@
package fr.acinq.eclair.transactions
import fr.acinq.bitcoin.SigHash._
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, ripemd160, sha256}
import fr.acinq.bitcoin.scalacompat.Crypto._
import fr.acinq.bitcoin.scalacompat.Script.{pay2wpkh, pay2wsh, write}
import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, OP_PUSHDATA, OP_RETURN, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi}
import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliBtcDouble, Musig2, OP_2, OP_CHECKMULTISIG, OP_PUSHDATA, OP_RETURN, OutPoint, Protocol, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, millibtc2satoshi}
import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash}
import fr.acinq.eclair.TestUtils.randomTxId
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
import fr.acinq.eclair.channel.Helpers.Funding
import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc}
import fr.acinq.eclair.transactions.Scripts.{anchor, htlcOffered, htlcReceived, toLocalDelayed}
import fr.acinq.eclair.transactions.Scripts._
import fr.acinq.eclair.transactions.Transactions.AnchorOutputsCommitmentFormat.anchorAmount
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.UpdateAddHtlc
@ -743,6 +744,265 @@ class TransactionsSpec extends AnyFunSuite with Logging {
}
}
test("generate valid commitment and htlc transactions (taproot)") {
import fr.acinq.bitcoin.scalacompat.KotlinUtils._
import fr.acinq.eclair.transactions.Scripts.Taproot
// funding tx sends to musig2 aggregate of local and remote funding keys
val fundingTxOutpoint = OutPoint(randomTxId(), 0)
val fundingOutput = TxOut(Btc(1), Script.pay2tr(Taproot.musig2Aggregate(localFundingPriv.publicKey, remoteFundingPriv.publicKey), None))
// offered HTLC
val preimage = ByteVector32.fromValidHex("01" * 32)
val paymentHash = Crypto.sha256(preimage)
val txNumber = 0x404142434445L
val (sequence, lockTime) = encodeTxNumber(txNumber)
val commitTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(fundingTxOutpoint, Nil, sequence) :: Nil,
txOut = Seq(
TxOut(300.millibtc, Taproot.toLocal(localDelayedPaymentPriv.publicKey, toLocalDelay, localRevocationPriv.publicKey)),
TxOut(400.millibtc, Taproot.toRemote(remotePaymentPriv.publicKey)),
TxOut(330.sat, Taproot.anchor(localDelayedPaymentPriv.publicKey)),
TxOut(330.sat, Taproot.anchor(remotePaymentPriv.publicKey)),
TxOut(25_000.sat, Taproot.offeredHtlc(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash, localRevocationPriv.publicKey)),
TxOut(15_000.sat, Taproot.receivedHtlc(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash, CltvExpiry(300), localRevocationPriv.publicKey))
),
lockTime
)
val (secretLocalNonce, publicLocalNonce) = Musig2.generateNonce(randomBytes32(), localFundingPriv, Seq(localFundingPriv.publicKey))
val (secretRemoteNonce, publicRemoteNonce) = Musig2.generateNonce(randomBytes32(), remoteFundingPriv, Seq(remoteFundingPriv.publicKey))
val publicKeys = Scripts.sort(Seq(localFundingPriv.publicKey, remoteFundingPriv.publicKey))
val publicNonces = Seq(publicLocalNonce, publicRemoteNonce)
val Right(sig) = for {
localPartialSig <- Musig2.signTaprootInput(localFundingPriv, tx, 0, Seq(fundingOutput), publicKeys, secretLocalNonce, publicNonces, None)
remotePartialSig <- Musig2.signTaprootInput(remoteFundingPriv, tx, 0, Seq(fundingOutput), publicKeys, secretRemoteNonce, publicNonces, None)
sig <- Musig2.aggregateTaprootSignatures(Seq(localPartialSig, remotePartialSig), tx, 0, Seq(fundingOutput), publicKeys, publicNonces, None)
} yield sig
tx.updateWitness(0, Script.witnessKeyPathPay2tr(sig))
}
Transaction.correctlySpends(commitTx, Map(fundingTxOutpoint -> fundingOutput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey))
val spendToLocalOutputTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 0), Seq(), sequence = toLocalDelay.toInt) :: Nil,
txOut = TxOut(300.millibtc, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.toLocalScriptTree(localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey)
val sig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(commitTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.getLeft.hash())
val witness = Script.witnessScriptPathPay2tr(Taproot.NUMS_POINT.xOnly, scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(sig)), scriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(spendToLocalOutputTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val mainPenaltyTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(300.millibtc, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.toLocalScriptTree(localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey)
val sig = Transaction.signInputTaprootScriptPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.getRight.hash())
val witness = Script.witnessScriptPathPay2tr(XonlyPublicKey(Taproot.NUMS_POINT), scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(sig)), scriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(mainPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val spendToRemoteOutputTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 1), Nil, sequence = 1) :: Nil,
txOut = TxOut(400.millibtc, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.toRemoteScriptTree(remotePaymentPriv.publicKey)
val sig = Transaction.signInputTaprootScriptPath(remotePaymentPriv, tx, 0, Seq(commitTx.txOut(1)), SigHash.SIGHASH_DEFAULT, scriptTree.hash())
val witness = Script.witnessScriptPathPay2tr(Taproot.NUMS_POINT.xOnly, scriptTree, ScriptWitness(Seq(sig)), scriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(spendToRemoteOutputTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val spendLocalAnchorTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(330.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val sig = Transaction.signInputTaprootKeyPath(localDelayedPaymentPriv, tx, 0, Seq(commitTx.txOut(2)), SigHash.SIGHASH_DEFAULT, Some(Scripts.Taproot.anchorScriptTree))
val witness = Script.witnessKeyPathPay2tr(sig)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(spendLocalAnchorTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val spendLocalAnchorAfterDelayTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = 16) :: Nil,
txOut = TxOut(330.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
// after 16 blocks, anchor outputs can be spent without a signature BUT spenders still need to know the local/remote payment public key
val witness = Script.witnessScriptPathPay2tr(localDelayedPaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree, ScriptWitness.empty, Scripts.Taproot.anchorScriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(spendLocalAnchorAfterDelayTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val spendRemoteAnchorTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 3), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(330.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val sig = Transaction.signInputTaprootKeyPath(remotePaymentPriv, tx, 0, Seq(commitTx.txOut(3)), SigHash.SIGHASH_DEFAULT, Some(Scripts.Taproot.anchorScriptTree))
val witness = Script.witnessKeyPathPay2tr(sig)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(spendRemoteAnchorTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val spendRemoteAnchorAfterDelayTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 3), Nil, sequence = 16) :: Nil,
txOut = TxOut(330.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val witness = Script.witnessScriptPathPay2tr(remotePaymentPriv.xOnlyPublicKey(), Scripts.Taproot.anchorScriptTree, ScriptWitness.empty, Scripts.Taproot.anchorScriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(spendRemoteAnchorAfterDelayTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
// Spend offered HTLC with HTLC-Timeout tx.
val htlcTimeoutTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 4), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(25_000.sat, Taproot.htlcDelayed(localDelayedPaymentPriv.publicKey, toLocalDelay, localRevocationPriv.publicKey)) :: Nil,
lockTime = 300)
val scriptTree = Taproot.offeredHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash)
val sigHash = SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY
val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), sigHash, scriptTree.getLeft.hash()), sigHash)
val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(4)), sigHash, scriptTree.getLeft.hash()), sigHash)
val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree.getLeft.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig, localSig)), scriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(htlcTimeoutTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val offeredHtlcPenaltyTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 4), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.offeredHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash)
val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(4)), SigHash.SIGHASH_DEFAULT, Some(scriptTree))
val witness = Script.witnessKeyPathPay2tr(sig)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(offeredHtlcPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val spendHtlcTimeoutTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(htlcTimeoutTx, 0), Nil, sequence = toLocalDelay.toInt) :: Nil,
txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPriv.publicKey, toLocalDelay)
val localSig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(htlcTimeoutTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.hash())
val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree, ScriptWitness(Seq(localSig)), scriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(spendHtlcTimeoutTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val htlcTimeoutPenaltyTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(htlcTimeoutTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(25_000.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPriv.publicKey, toLocalDelay)
val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(htlcTimeoutTx.txOut(0)), SigHash.SIGHASH_DEFAULT, Some(scriptTree))
val witness = Script.witnessKeyPathPay2tr(sig)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(htlcTimeoutPenaltyTx, Seq(htlcTimeoutTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
// Spend received HTLC with HTLC-Success tx.
val htlcSuccessTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 5), Nil, sequence = 1) :: Nil,
txOut = TxOut(15_000.sat, Taproot.htlcDelayed(localDelayedPaymentPriv.publicKey, toLocalDelay, localRevocationPriv.publicKey)) :: Nil,
lockTime = 0)
val scriptTree = Taproot.receivedHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash, CltvExpiry(300))
val sigHash = SigHash.SIGHASH_SINGLE | SigHash.SIGHASH_ANYONECANPAY
val localSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(localHtlcPriv, tx, 0, Seq(commitTx.txOut(5)), sigHash, scriptTree.getRight.hash()), sigHash)
val remoteSig = Taproot.encodeSig(Transaction.signInputTaprootScriptPath(remoteHtlcPriv, tx, 0, Seq(commitTx.txOut(5)), sigHash, scriptTree.getRight.hash()), sigHash)
val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree.getRight.asInstanceOf[ScriptTree.Leaf], ScriptWitness(Seq(remoteSig, localSig, preimage)), scriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(htlcSuccessTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val receivedHtlcPenaltyTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(commitTx, 5), Nil, sequence = 1) :: Nil,
txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.receivedHtlcScriptTree(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, paymentHash, CltvExpiry(300))
val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(commitTx.txOut(5)), SigHash.SIGHASH_DEFAULT, Some(scriptTree))
val witness = Script.witnessKeyPathPay2tr(sig)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(receivedHtlcPenaltyTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val spendHtlcSuccessTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(htlcSuccessTx, 0), Nil, sequence = toLocalDelay.toInt) :: Nil,
txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPriv.publicKey, toLocalDelay)
val localSig = Transaction.signInputTaprootScriptPath(localDelayedPaymentPriv, tx, 0, Seq(htlcSuccessTx.txOut(0)), SigHash.SIGHASH_DEFAULT, scriptTree.hash())
val witness = Script.witnessScriptPathPay2tr(localRevocationPriv.xOnlyPublicKey(), scriptTree, ScriptWitness(Seq(localSig)), scriptTree)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(spendHtlcSuccessTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
val htlcSuccessPenaltyTx = {
val tx = Transaction(
version = 2,
txIn = TxIn(OutPoint(htlcSuccessTx, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
txOut = TxOut(15_000.sat, finalPubKeyScript) :: Nil,
lockTime = 0)
val scriptTree = Taproot.htlcDelayedScriptTree(localDelayedPaymentPriv.publicKey, toLocalDelay)
val sig = Transaction.signInputTaprootKeyPath(localRevocationPriv, tx, 0, Seq(htlcSuccessTx.txOut(0)), SigHash.SIGHASH_DEFAULT, Some(scriptTree))
val witness = Script.witnessKeyPathPay2tr(sig)
tx.updateWitness(0, witness)
}
Transaction.correctlySpends(htlcSuccessPenaltyTx, Seq(htlcSuccessTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}
test("generate taproot NUMS point") {
val bin = 2.toByte +: Crypto.sha256(ByteVector.fromValidHex("0000000000000002") ++ ByteVector.view("Lightning Simple Taproot".getBytes))
val pub = PublicKey(bin)
assert(pub == Taproot.NUMS_POINT)
}
test("sort public keys using lexicographic ordering") {
val pubkey1 = PublicKey(hex"0277174bdb8e0003a03334f0f5d0be2b9f4c0812ee4097b0c23d29f505b8e9d9f8")
val pubkey2 = PublicKey(hex"03e27a9ca7c8d6348868f8b4a3974e9eb91f7df7d6532f9b0a50f0314cb28c8d31")
assert(Seq(pubkey1, pubkey2) == Scripts.sort(Seq(pubkey1, pubkey2)))
assert(Seq(pubkey1, pubkey2) == Scripts.sort(Seq(pubkey2, pubkey1)))
assert(multiSig2of2(pubkey1, pubkey2) == multiSig2of2(pubkey2, pubkey1))
assert(multiSig2of2(pubkey2, pubkey1) == Seq(OP_2, OP_PUSHDATA(pubkey1.value), OP_PUSHDATA(pubkey2.value), OP_2, OP_CHECKMULTISIG))
assert(Taproot.musig2Aggregate(pubkey1, pubkey2) == Taproot.musig2Aggregate(pubkey2, pubkey1))
assert(Taproot.musig2Aggregate(pubkey2, pubkey1) == Musig2.aggregateKeys(Seq(pubkey1, pubkey2)))
}
test("sort the htlc outputs using BIP69 and cltv expiry") {
val localFundingPriv = PrivateKey(hex"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1")
val remoteFundingPriv = PrivateKey(hex"a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2")