mirror of
https://github.com/ACINQ/eclair.git
synced 2025-03-12 10:30:45 +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:
parent
945623643f
commit
cae22d71be
2 changed files with 516 additions and 11 deletions
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Reference in a new issue