From 001ad3af8e88df044680eb5c66f8ef20ed3bbdd3 Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 26 Mar 2025 14:10:11 +0100 Subject: [PATCH 1/6] ChannelKeyManager: add optional list of spent outputs to sign() methods This is needed to sign inputs that spend taproot outputs. --- .../eclair/crypto/keymanager/ChannelKeyManager.scala | 8 +++++--- .../eclair/crypto/keymanager/LocalChannelKeyManager.scala | 8 ++++---- .../scala/fr/acinq/eclair/transactions/Transactions.scala | 8 +++++--- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index c0f1e7c725..e5f0f4a66e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, DeterministicWallet, Protocol} +import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, DeterministicWallet, Protocol, TxOut} import fr.acinq.eclair.channel.{ChannelConfig, LocalParams} import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} import scodec.bits.ByteVector @@ -66,7 +66,9 @@ trait ChannelKeyManager { * @param commitmentFormat format of the commitment tx * @return a signature generated with the private key that matches the input extended public key */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]]): ByteVector64 + + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = sign(tx, publicKey, txOwner, commitmentFormat, None) /** * This method is used to spend funds sent to htlc keys/delayed keys @@ -78,7 +80,7 @@ trait ChannelKeyManager { * @param commitmentFormat format of the commitment tx * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]] = None): ByteVector64 /** * Ths method is used to spend revoked transactions, with the corresponding revocation key diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 86975a1ae8..3a773f4aec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.crypto.keymanager import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ -import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet} +import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, TxOut} import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Announcements @@ -106,7 +106,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param commitmentFormat format of the commitment tx * @return a signature generated with the private key that matches the input extended public key */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]]): ByteVector64 = { // NB: not all those transactions are actually commit txs (especially during closing), but this is good enough for monitoring purposes val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.CommitTx) Metrics.SignTxCount.withTags(tags).increment() @@ -126,14 +126,14 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param commitmentFormat format of the commitment tx * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]] = None): ByteVector64 = { // NB: not all those transactions are actually htlc txs (especially during closing), but this is good enough for monitoring purposes val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.HtlcTx) Metrics.SignTxCount.withTags(tags).increment() KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { val privateKey = privateKeys.get(publicKey.path) val currentKey = Generators.derivePrivKey(privateKey.privateKey, remotePoint) - tx.sign(currentKey, txOwner, commitmentFormat) + tx.sign(currentKey, txOwner, commitmentFormat, spentUtxos) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index aa7f8dd854..ee1d13e9d3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -131,11 +131,13 @@ object Transactions { /** Sighash flags to use when signing the transaction. */ def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = SIGHASH_ALL - def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = { - sign(key, sighash(txOwner, commitmentFormat)) + def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]] = None): ByteVector64 = { + sign(key, sighash(txOwner, commitmentFormat), spentUtxos) } - def sign(key: PrivateKey, sighashType: Int): ByteVector64 = input match { + def sign(key: PrivateKey, sighashType: Int): ByteVector64 = sign(key, sighashType, None) + + def sign(key: PrivateKey, sighashType: Int, spentUtxos: Option[Seq[TxOut]]): ByteVector64 = input match { case _: InputInfo.TaprootInput => ByteVector64.Zeroes case InputInfo.SegwitInput(outPoint, txOut, redeemScript) => // NB: the tx may have multiple inputs, we will only sign the one provided in txinfo.input. Bear in mind that the From dd87c3951714fa31da0e0da7d911b6cd6a186ffa Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 26 Mar 2025 15:07:05 +0100 Subject: [PATCH 2/6] Fixup: document new parameters --- .../fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala | 2 ++ .../acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala | 2 ++ 2 files changed, 4 insertions(+) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index e5f0f4a66e..f3da4aedcc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -64,6 +64,7 @@ trait ChannelKeyManager { * @param publicKey extended public key * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx + * @param spentUtxos all outputs spent by this transaction * @return a signature generated with the private key that matches the input extended public key */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]]): ByteVector64 @@ -78,6 +79,7 @@ trait ChannelKeyManager { * @param remotePoint remote point * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx + * @param spentUtxos all outputs spent by this transaction * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]] = None): ByteVector64 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 3a773f4aec..8d50fa1446 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -104,6 +104,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param publicKey extended public key * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx + * @param spentUtxos all outputs spent by this transaction * @return a signature generated with the private key that matches the input extended public key */ override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]]): ByteVector64 = { @@ -124,6 +125,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param remotePoint remote point * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx + * @param spentUtxos all outputs spent by this transaction * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]] = None): ByteVector64 = { From 8b71c7ee4bab38afbb250cd13fc545c915378e4c Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 26 Mar 2025 20:20:34 +0100 Subject: [PATCH 3/6] ChannelKeyManager: add an extra output parameter to sign() methods This outputs are typically wallet inputs added to a transaction to bump it. --- .../eclair/SpendFromChannelAddress.scala | 3 +- .../bitcoind/rpc/BitcoinCoreClient.scala | 2 +- .../fr/acinq/eclair/channel/Commitments.scala | 10 +- .../fr/acinq/eclair/channel/Helpers.scala | 38 +++--- .../channel/fsm/ChannelOpenSingleFunded.scala | 8 +- .../channel/fund/InteractiveTxBuilder.scala | 6 +- .../channel/publish/ReplaceableTxFunder.scala | 58 ++++++--- .../crypto/keymanager/ChannelKeyManager.scala | 12 +- .../keymanager/LocalChannelKeyManager.scala | 17 +-- .../eclair/transactions/Transactions.scala | 20 +-- .../publish/ReplaceableTxPublisherSpec.scala | 55 ++++++++- .../integration/ChannelIntegrationSpec.scala | 6 +- .../eclair/transactions/TestVectorsSpec.scala | 12 +- .../transactions/TransactionsSpec.scala | 115 +++++++++++------- 14 files changed, 236 insertions(+), 126 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala index 6b9746883e..a6cb0c329e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala @@ -48,7 +48,8 @@ trait SpendFromChannelAddress { Transactions.SpliceTx(inputInfo, unsignedTx), // classify as splice, doesn't really matter localFundingPubkey, TxOwner.Local, // unused - DefaultCommitmentFormat // unused + DefaultCommitmentFormat, // unused + Nil ) witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey.publicKey, remoteFundingPubkey) signedTx = unsignedTx.updateWitness(0, witness) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 5dfecb60ce..91b6bcff23 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -305,7 +305,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool }) } - private def utxoUpdatePsbt(psbt: Psbt)(implicit ec: ExecutionContext): Future[Psbt] = { + def utxoUpdatePsbt(psbt: Psbt)(implicit ec: ExecutionContext): Future[Psbt] = { val encoded = Base64.getEncoder.encodeToString(Psbt.write(psbt).toByteArray) rpcClient.invoke("utxoupdatepsbt", encoded).map(json => { val JString(base64) = json diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index a17b2dc145..bbe7e5b58a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -218,10 +218,10 @@ object LocalCommit { case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey) { def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo): CommitSig = { val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Nil) val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Remote, params.commitmentFormat)) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Nil)) CommitSig(params.channelId, sig, htlcSigs.toList) } } @@ -622,11 +622,11 @@ case class Commitment(fundingTxIndex: Long, // remote commitment will include all local proposed changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, remoteCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remoteNextPerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Nil) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = htlcTxs.sortBy(_.input.outPoint.index) val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, params.commitmentFormat)) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Nil)) // NB: IN/OUT htlcs are inverted because this is the remote commit log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(",")) @@ -659,7 +659,7 @@ case class Commitment(fundingTxIndex: Long, /** Return a fully signed commit tx, that can be published as-is. */ def fullySignedLocalCommitTx(params: ChannelParams, keyManager: ChannelKeyManager): CommitTx = { val unsignedCommitTx = localCommit.commitTxAndRemoteSig.commitTx - val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat) + val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat, Nil) val RemoteSignature.FullSignature(remoteSig) = localCommit.commitTxAndRemoteSig.remoteSig val commitTx = addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig) // We verify the remote signature when receiving their commit_sig, so this check should always pass. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 0879042f3b..01bcdabaac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -701,7 +701,7 @@ object Helpers { log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, commitment.specs2String) val dustLimit = commitment.localParams.dustLimit.max(commitment.remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex), TxOwner.Local, commitment.params.commitmentFormat) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex), TxOwner.Local, commitment.params.commitmentFormat, Nil) val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) log.debug(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") @@ -741,9 +741,9 @@ object Helpers { } val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, TlvStream(Set( - closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))), - closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))), - closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat))), + closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil))), + closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil))), + closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil))), ).flatten[ClosingTlv])) Right(closingTxs, closingComplete) } @@ -774,7 +774,7 @@ object Helpers { closingTxsWithSigs.headOption match { case Some((closingTx, remoteSig, sigToTlv)) => val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat) + val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil) val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) Transactions.checkSpendable(signedClosingTx) match { case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) @@ -800,7 +800,7 @@ object Helpers { closingTxsWithSig.headOption match { case Some((closingTx, remoteSig)) => val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat) + val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil) val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) Transactions.checkSpendable(signedClosingTx) match { case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) @@ -882,7 +882,7 @@ object Helpers { // first we will claim our main output as soon as the delay is over val mainDelayedTx = withTxGenerationLog("local-main-delayed") { Transactions.makeClaimLocalDelayedOutputTx(tx, commitment.localParams.dustLimit, localRevocationPubkey, commitment.remoteParams.toSelfDelay, localDelayedPubkey, finalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) Transactions.addSigs(claimDelayed, sig) }) } @@ -947,7 +947,7 @@ object Helpers { if (hash2Preimage.contains(paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-success") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat) + val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) Right(Transactions.addSigs(txInfo, localSig, remoteSig, hash2Preimage(paymentHash), commitment.params.commitmentFormat)) }) } else if (failedIncomingHtlcs.contains(txInfo.htlcId)) { @@ -968,7 +968,7 @@ object Helpers { // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds // back after the timeout. Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-timeout") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat) + val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) Right(Transactions.addSigs(txInfo, localSig, remoteSig, commitment.params.commitmentFormat)) }) }.flatten.toMap @@ -991,7 +991,7 @@ object Helpers { // Note that this will return None if the transaction wasn't one of our HTLC transactions, which may happen // if our peer was able to claim the HTLC output before us (race condition between success and timeout). Transactions.makeHtlcDelayedTx(tx, commitment.localParams.dustLimit, localRevocationPubkey, commitment.remoteParams.toSelfDelay, localDelayedPubkey, finalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) Transactions.addSigs(claimDelayed, sig) }) } @@ -1073,13 +1073,13 @@ object Helpers { params.commitmentFormat match { case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { Transactions.makeClaimP2WPKHOutputTx(tx, params.localParams.dustLimit, localPubkey, finalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, params.commitmentFormat) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, params.commitmentFormat, Nil) Transactions.addSigs(claimMain, localPubkey, sig) }) } case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(tx, params.localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, params.commitmentFormat) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, params.commitmentFormat, Nil) Transactions.addSigs(claimMain, sig) }) } @@ -1126,7 +1126,7 @@ object Helpers { if (hash2Preimage.contains(add.paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-success") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat) + val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) Right(Transactions.addSigs(claimHtlcTx, sig, hash2Preimage(add.paymentHash))) }) } else if (failedIncomingHtlcs.contains(add.id)) { @@ -1152,7 +1152,7 @@ object Helpers { Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, commitment.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, finalScriptPubKey, add, feeratePerKwHtlc, commitment.params.commitmentFormat) }.map(claimHtlcTx => { Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-timeout") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat) + val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) Right(Transactions.addSigs(claimHtlcTx, sig)) }) }) @@ -1220,13 +1220,13 @@ object Helpers { case ct => ct.commitmentFormat match { case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, finalScriptPubKey, feerateMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat, Nil) Transactions.addSigs(claimMain, localPaymentPubkey, sig) }) } case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feerateMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat, Nil) Transactions.addSigs(claimMain, sig) }) } @@ -1236,7 +1236,7 @@ object Helpers { // then we punish them by stealing their main output val mainPenaltyTx = withTxGenerationLog("main-penalty") { Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, finalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePenalty).map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Nil) Transactions.addSigs(txinfo, sig) }) } @@ -1256,7 +1256,7 @@ object Helpers { val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) withTxGenerationLog("htlc-penalty") { Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, finalScriptPubKey, feeratePenalty).map(htlcPenalty => { - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Nil) Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) } @@ -1312,7 +1312,7 @@ object Helpers { val penaltyTxs = Transactions.makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, finalScriptPubKey, feeratePerKwPenalty).flatMap(claimHtlcDelayedOutputPenaltyTx => { withTxGenerationLog("htlc-delayed-penalty") { claimHtlcDelayedOutputPenaltyTx.map(htlcDelayedPenalty => { - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat) + val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Nil) val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) // We need to make sure that the tx is indeed valid. Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 73c56b5ca4..931dafa06a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -216,7 +216,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), TxOwner.Remote, params.commitmentFormat) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), TxOwner.Remote, params.commitmentFormat, Nil) // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( temporaryChannelId = temporaryChannelId, @@ -264,12 +264,12 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => // check remote signature validity val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Nil) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, fundingTxIndex = 0, localCommitTx.tx), d, None) case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, params.commitmentFormat) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, params.commitmentFormat, Nil) val channelId = toLongId(fundingTxId, fundingTxOutputIndex) val fundingSigned = FundingSigned( channelId = channelId, @@ -313,7 +313,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Event(msg@FundingSigned(_, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(params, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, _)) => // we make sure that their sig checks out and that our first commit tx is spendable val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Nil) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 7d40cbbebc..0adedc7d75 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -114,7 +114,7 @@ object InteractiveTxBuilder { override def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction): ByteVector64 = { val localFundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) - keyManager.sign(Transactions.SpliceTx(info, tx), localFundingPubkey, TxOwner.Local, params.commitmentFormat) + keyManager.sign(Transactions.SpliceTx(info, tx), localFundingPubkey, TxOwner.Local, params.commitmentFormat, Nil) } } @@ -847,9 +847,9 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)) => require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!") val fundingPubKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex) - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat, Nil) val localPerCommitmentPoint = keyManager.htlcPoint(keyManager.keyPath(channelParams.localParams, channelParams.channelConfig)) - val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat)).toList + val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat, Nil)).toList val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures) val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index 158b7d661f..a55fab1a4c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{NodeParams, NotificationsLogger} import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.CollectionConverters.ListHasAsScala import scala.util.{Failure, Success} /** @@ -49,7 +50,7 @@ object ReplaceableTxFunder { sealed trait Command case class FundTransaction(replyTo: ActorRef[FundingResult], cmd: TxPublisher.PublishReplaceableTx, tx: Either[FundedTx, ReplaceableTxWithWitnessData], targetFeerate: FeeratePerKw) extends Command - private case class AddInputsOk(tx: ReplaceableTxWithWitnessData, totalAmountIn: Satoshi) extends Command + private case class AddInputsOk(tx: ReplaceableTxWithWitnessData, totalAmountIn: Satoshi, walletUtxos: Seq[TxOut]) extends Command private case class AddInputsFailed(reason: Throwable) extends Command private case class SignWalletInputsOk(signedTx: Transaction) extends Command private case class SignWalletInputsFailed(reason: Throwable) extends Command @@ -248,7 +249,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, val htlcFeerate = cmd.commitment.localCommit.spec.htlcTxFeerate(cmd.commitment.params.commitmentFormat) if (targetFeerate <= htlcFeerate) { log.debug("publishing {} without adding inputs: txid={}", cmd.desc, htlcTx.txInfo.tx.txid) - sign(txWithWitnessData, htlcFeerate, htlcTx.txInfo.amountIn) + sign(txWithWitnessData, htlcFeerate, htlcTx.txInfo.amountIn, Nil) } else { addWalletInputs(htlcTx, targetFeerate) } @@ -260,7 +261,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, replyTo ! FundingFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) Behaviors.stopped case Right(updatedClaimHtlcTx) => - sign(updatedClaimHtlcTx, targetFeerate, updatedClaimHtlcTx.txInfo.amountIn) + sign(updatedClaimHtlcTx, targetFeerate, updatedClaimHtlcTx.txInfo.amountIn, Nil) } } } @@ -274,7 +275,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, Behaviors.stopped case AdjustPreviousTxOutputResult.TxOutputAdjusted(updatedTx) => log.debug("bumping {} fees without adding new inputs: txid={}", cmd.desc, updatedTx.txInfo.tx.txid) - sign(updatedTx, targetFeerate, previousTx.totalAmountIn) + sign(updatedTx, targetFeerate, previousTx.totalAmountIn, Nil) case AdjustPreviousTxOutputResult.AddWalletInputs(tx) => log.debug("bumping {} fees requires adding new inputs (feerate={})", cmd.desc, targetFeerate) // We restore the original transaction (remove previous attempt's wallet inputs). @@ -285,13 +286,13 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, private def addWalletInputs(txWithWitnessData: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw): Behavior[Command] = { context.pipeToSelf(addInputs(txWithWitnessData, targetFeerate, cmd.commitment)) { - case Success((fundedTx, totalAmountIn)) => AddInputsOk(fundedTx, totalAmountIn) + case Success((fundedTx, totalAmountIn, psbt)) => AddInputsOk(fundedTx, totalAmountIn, psbt) case Failure(reason) => AddInputsFailed(reason) } Behaviors.receiveMessagePartial { - case AddInputsOk(fundedTx, totalAmountIn) => + case AddInputsOk(fundedTx, totalAmountIn, walletUtxos) => log.debug("added {} wallet input(s) and {} wallet output(s) to {}", fundedTx.txInfo.tx.txIn.length - 1, fundedTx.txInfo.tx.txOut.length - 1, cmd.desc) - sign(fundedTx, targetFeerate, totalAmountIn) + sign(fundedTx, targetFeerate, totalAmountIn, walletUtxos) case AddInputsFailed(reason) => if (reason.getMessage.contains("Insufficient funds")) { val nodeOperatorMessage = @@ -309,17 +310,24 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } } - private def sign(fundedTx: ReplaceableTxWithWitnessData, txFeerate: FeeratePerKw, amountIn: Satoshi): Behavior[Command] = { + private def sign(fundedTx: ReplaceableTxWithWitnessData, txFeerate: FeeratePerKw, amountIn: Satoshi, walletUtxos: Seq[TxOut]): Behavior[Command] = { val channelKeyPath = keyManager.keyPath(cmd.commitment.localParams, cmd.commitment.params.channelConfig) fundedTx match { case claimAnchorTx: ClaimLocalAnchorWithWitnessData => - val localSig = keyManager.sign(claimAnchorTx.txInfo, keyManager.fundingPublicKey(cmd.commitment.localParams.fundingKeyPath, cmd.commitment.fundingTxIndex), TxOwner.Local, cmd.commitment.params.commitmentFormat) + log.info(s"signing ${claimAnchorTx.txInfo.tx} with wallet utxos $walletUtxos") + val localSig = fundedTx.txInfo.input match { + case _: InputInfo.SegwitInput => + keyManager.sign(claimAnchorTx.txInfo, keyManager.fundingPublicKey(cmd.commitment.localParams.fundingKeyPath, cmd.commitment.fundingTxIndex), TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) + case _: InputInfo.TaprootInput => + val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitment.localCommit.index) + keyManager.sign(claimAnchorTx.txInfo, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) + } val signedTx = claimAnchorTx.copy(txInfo = addSigs(claimAnchorTx.txInfo, localSig)) signWalletInputs(signedTx, txFeerate, amountIn) case htlcTx: HtlcWithWitnessData => val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitment.localCommit.index) val localHtlcBasepoint = keyManager.htlcPoint(channelKeyPath) - val localSig = keyManager.sign(htlcTx.txInfo, localHtlcBasepoint, localPerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat) + val localSig = keyManager.sign(htlcTx.txInfo, localHtlcBasepoint, localPerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) val signedTx = htlcTx match { case htlcSuccess: HtlcSuccessWithWitnessData => htlcSuccess.copy(txInfo = addSigs(htlcSuccess.txInfo, localSig, htlcSuccess.remoteSig, htlcSuccess.preimage, cmd.commitment.params.commitmentFormat)) case htlcTimeout: HtlcTimeoutWithWitnessData => htlcTimeout.copy(txInfo = addSigs(htlcTimeout.txInfo, localSig, htlcTimeout.remoteSig, cmd.commitment.params.commitmentFormat)) @@ -336,7 +344,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, case Some(c) if claimHtlcTx.txInfo.input.outPoint.txid == c.commit.txid => c.commit.remotePerCommitmentPoint case _ => cmd.commitment.remoteCommit.remotePerCommitmentPoint } - val sig = keyManager.sign(claimHtlcTx.txInfo, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat) + val sig = keyManager.sign(claimHtlcTx.txInfo, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) val signedTx = claimHtlcTx match { case claimSuccess: ClaimHtlcSuccessWithWitnessData => claimSuccess.copy(txInfo = addSigs(claimSuccess.txInfo, sig, claimSuccess.preimage)) case legacyClaimHtlcSuccess: LegacyClaimHtlcSuccessWithWitnessData => legacyClaimHtlcSuccess @@ -355,13 +363,17 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, case InputInfo.SegwitInput(_, _, redeemScript) => fr.acinq.bitcoin.Script.parse(redeemScript) case _: InputInfo.TaprootInput => null } + val sigHash = locallySignedTx.txInfo.input match { + case _: InputInfo.TaprootInput => fr.acinq.bitcoin.SigHash.SIGHASH_DEFAULT + case _: InputInfo.SegwitInput => fr.acinq.bitcoin.SigHash.SIGHASH_ALL + } val psbt = new Psbt(locallySignedTx.txInfo.tx) .updateWitnessInput( locallySignedTx.txInfo.input.outPoint, locallySignedTx.txInfo.input.txOut, null, witnessScript, - fr.acinq.bitcoin.SigHash.SIGHASH_ALL, + sigHash, java.util.Map.of(), null, null, @@ -425,10 +437,26 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } } - private def addInputs(tx: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ReplaceableTxWithWalletInputs, Satoshi)] = { + private def addInputs(tx: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ReplaceableTxWithWalletInputs, Satoshi, Seq[TxOut])] = { + import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + + def getWalletUtxos(txInfo: TransactionWithInputInfo): Future[Seq[TxOut]] = { + val psbt = new Psbt(txInfo.tx) + bitcoinClient.utxoUpdatePsbt(psbt).map(updatedPsbt => { + // we skip the first spent (it's the one from txInfo.input), additional inputs are wallet inputs and have been updated by bitcoin core + updatedPsbt.inputs.asScala.tail.map(_.getWitnessUtxo).map(kmp2scala).toSeq + }) + } + tx match { - case anchorTx: ClaimLocalAnchorWithWitnessData => addInputs(anchorTx, targetFeerate, commitment) - case htlcTx: HtlcWithWitnessData => addInputs(htlcTx, targetFeerate, commitment) + case anchorTx: ClaimLocalAnchorWithWitnessData => for { + (fundedTx, amountIn) <- addInputs(anchorTx, targetFeerate, commitment) + spentUtxos <- getWalletUtxos(fundedTx.txInfo) + } yield (fundedTx, amountIn, spentUtxos) + case htlcTx: HtlcWithWitnessData => for { + (fundedTx, amountIn) <- addInputs(htlcTx, targetFeerate, commitment) + spentUtxos <- getWalletUtxos(fundedTx.txInfo) + } yield (fundedTx, amountIn, spentUtxos) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index f3da4aedcc..35a0e709c1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -64,12 +64,11 @@ trait ChannelKeyManager { * @param publicKey extended public key * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param spentUtxos all outputs spent by this transaction + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with the private key that matches the input extended public key */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]]): ByteVector64 + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = sign(tx, publicKey, txOwner, commitmentFormat, None) /** * This method is used to spend funds sent to htlc keys/delayed keys @@ -79,10 +78,10 @@ trait ChannelKeyManager { * @param remotePoint remote point * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param spentUtxos all outputs spent by this transaction + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]] = None): ByteVector64 + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 /** * Ths method is used to spend revoked transactions, with the corresponding revocation key @@ -92,9 +91,10 @@ trait ChannelKeyManager { * @param remoteSecret remote secret * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 /** * Sign a channel announcement message diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 8d50fa1446..952a0ac5a5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -104,16 +104,16 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param publicKey extended public key * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param spentUtxos all outputs spent by this transaction + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with the private key that matches the input extended public key */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]]): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { // NB: not all those transactions are actually commit txs (especially during closing), but this is good enough for monitoring purposes val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.CommitTx) Metrics.SignTxCount.withTags(tags).increment() KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { val privateKey = privateKeys.get(publicKey.path) - tx.sign(privateKey.privateKey, txOwner, commitmentFormat) + tx.sign(privateKey.privateKey, txOwner, commitmentFormat, extraUtxos) } } @@ -125,17 +125,17 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param remotePoint remote point * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param spentUtxos all outputs spent by this transaction + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]] = None): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { // NB: not all those transactions are actually htlc txs (especially during closing), but this is good enough for monitoring purposes val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.HtlcTx) Metrics.SignTxCount.withTags(tags).increment() KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { val privateKey = privateKeys.get(publicKey.path) val currentKey = Generators.derivePrivKey(privateKey.privateKey, remotePoint) - tx.sign(currentKey, txOwner, commitmentFormat, spentUtxos) + tx.sign(currentKey, txOwner, commitmentFormat, extraUtxos) } } @@ -147,15 +147,16 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param remoteSecret remote secret * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx + * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.RevokedTx) Metrics.SignTxCount.withTags(tags).increment() KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { val privateKey = privateKeys.get(publicKey.path) val currentKey = Generators.revocationPrivKey(privateKey.privateKey, remoteSecret) - tx.sign(currentKey, txOwner, commitmentFormat) + tx.sign(currentKey, txOwner, commitmentFormat, extraUtxos) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index ee1d13e9d3..f669ac52ea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.SigVersion._ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey, ripemd160} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.{ScriptFlags, ScriptTree} +import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} import fr.acinq.eclair.transactions.CommitmentOutput._ @@ -131,13 +131,11 @@ object Transactions { /** Sighash flags to use when signing the transaction. */ def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = SIGHASH_ALL - def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, spentUtxos: Option[Seq[TxOut]] = None): ByteVector64 = { - sign(key, sighash(txOwner, commitmentFormat), spentUtxos) + def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + sign(key, sighash(txOwner, commitmentFormat), extraUtxos) } - def sign(key: PrivateKey, sighashType: Int): ByteVector64 = sign(key, sighashType, None) - - def sign(key: PrivateKey, sighashType: Int, spentUtxos: Option[Seq[TxOut]]): ByteVector64 = input match { + def sign(key: PrivateKey, sighashType: Int, extraUtxos: Seq[TxOut]): ByteVector64 = input match { case _: InputInfo.TaprootInput => ByteVector64.Zeroes case InputInfo.SegwitInput(outPoint, txOut, redeemScript) => // NB: the tx may have multiple inputs, we will only sign the one provided in txinfo.input. Bear in mind that the @@ -203,7 +201,15 @@ object Transactions { case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-timeout" } sealed trait ClaimAnchorOutputTx extends TransactionWithInputInfo - case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { override def desc: String = "local-anchor" } + case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { + override def desc: String = "local-anchor" + override def sign(key: PrivateKey, sighashType: Int, extraUtxos: Seq[TxOut]): ByteVector64 = input match { // README: this will be updated with a better type hierarchy for taproot inputs + case _: InputInfo.SegwitInput => + super.sign(key, sighashType, extraUtxos) + case t: InputInfo.TaprootInput => + Transaction.signInputTaprootKeyPath(key, tx, 0, input.txOut +: extraUtxos, sighashType, t.scriptTree_opt) + } + } case class ClaimRemoteAnchorOutputTx(input: InputInfo, tx: Transaction) extends ClaimAnchorOutputTx { override def desc: String = "remote-anchor" } sealed trait ClaimRemoteCommitMainOutputTx extends TransactionWithInputInfo case class ClaimP2WPKHOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { override def desc: String = "remote-main" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index 317b6c0d4b..a3002ba6bf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -22,7 +22,7 @@ import akka.pattern.pipe import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, ScriptElt, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, ByteVector64, Crypto, DeterministicWallet, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, ScriptElt, Transaction, TxId, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -36,18 +36,19 @@ import fr.acinq.eclair.channel.publish.ReplaceableTxPublisher.{Publish, Stop, Up import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.crypto.keymanager.LocalOnChainKeyManager +import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, LocalOnChainKeyManager} import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateFee} import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, NotificationsLogger, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} +import org.json4s.JString import org.scalatest.BeforeAndAfterAll import org.scalatest.Inside.inside import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector import java.util.UUID -import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.{AtomicLong, AtomicReference} import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.DurationInt @@ -143,7 +144,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } // NB: we can't use ScalaTest's fixtures, they would see uninitialized bitcoind fields because they sandbox each test. - private def withFixture(utxos: Seq[BtcAmount], channelType: SupportedChannelType)(testFun: Fixture => Any): Unit = { + private def withFixture(utxos: Seq[BtcAmount], channelType: SupportedChannelType, channelKeyManager_opt: Option[ChannelKeyManager] = None)(testFun: Fixture => Any): Unit = { // Create a unique wallet for this test and ensure it has some btc. val testId = UUID.randomUUID() val (walletRpcClient, walletClient) = createTestWallet(s"lightning-$testId") @@ -160,7 +161,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Setup a valid channel between alice and bob. val blockHeight = new AtomicLong() blockHeight.set(currentBlockHeight(probe).toLong) - val aliceNodeParams = TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight) + val aliceNodeParams = channelKeyManager_opt.map(ckm => TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight, channelKeyManager = ckm)).getOrElse(TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight)) val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), wallet_opt = Some(walletClient)) val testTags = channelType match { case _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) @@ -407,7 +408,36 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate too low, spending anchor output (local commit)") { - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => + //we create a new channel key manager that delegates all call to Alice's key manager but track extra utxos passed to the sign() methods + val walletInputs = new AtomicReference[Seq[TxOut]](Nil) + val channelKeyManagerA = TestConstants.Alice.nodeParams.channelKeyManager + // @formatter:off + val channelKeyManager = new ChannelKeyManager { + override def fundingPublicKey(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.fundingPublicKey(fundingKeyPath, fundingTxIndex) + override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.revocationPoint(channelKeyPath) + override def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.paymentPoint(channelKeyPath) + override def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.delayedPaymentPoint(channelKeyPath) + override def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.htlcPoint(channelKeyPath) + override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PrivateKey = channelKeyManagerA.commitmentSecret(channelKeyPath, index) + override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PublicKey = channelKeyManagerA.commitmentPoint(channelKeyPath, index) + override def newFundingKeyPath(isChannelOpener: Boolean): DeterministicWallet.KeyPath = channelKeyManagerA.newFundingKeyPath(isChannelOpener) + override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + walletInputs.set(extraUtxos) + channelKeyManagerA.sign(tx, publicKey, txOwner, commitmentFormat, extraUtxos) + } + override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + walletInputs.set(extraUtxos) + channelKeyManagerA.sign(tx, publicKey, remotePoint, txOwner, commitmentFormat, extraUtxos) + } + override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remoteSecret: Crypto.PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + walletInputs.set(extraUtxos) + channelKeyManagerA.sign(tx, publicKey, remoteSecret, txOwner, commitmentFormat, extraUtxos) + } + override def signChannelAnnouncement(witness: ByteVector, fundingKeyPath: DeterministicWallet.KeyPath): ByteVector64 = channelKeyManagerA.signChannelAnnouncement(witness, fundingKeyPath) + } + // @formatter:on + + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Some(channelKeyManager)) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -419,10 +449,23 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // NB: we try to get transactions confirmed *before* their confirmation target, so we aim for a more aggressive block target what's provided. setFeerate(targetFeerate, blockTarget = 12) publisher ! Publish(probe.ref, anchorTx) + // wait for the commit tx and anchor tx to be published val mempoolTxs = getMempoolTxs(2) + // check that the wallet input added by the tx funder was passed to the key manager assert(mempoolTxs.map(_.txid).contains(commitTx.tx.txid)) + def getTransaction(txid: TxId): Transaction = { + bitcoinrpcclient.invoke("getrawtransaction", txid).pipeTo(probe.ref) + Transaction.read(probe.expectMsgType[JString].values) + } + + // there are 2 transactions in the mempool, the one that is not the commit tx has to be the anchor tx + val publishedAnchorTx = getTransaction(mempoolTxs.filterNot(_.txid == commitTx.tx.txid).head.txid) + val extraInputs = publishedAnchorTx.txIn.tail.map(input => getTransaction(input.outPoint.txid).txOut(input.outPoint.index.toInt)) + // check that inputs added to the anchor tx match what was passed to our key manager's sign() method + assert(walletInputs.get() == extraInputs) + val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt) val actualFee = mempoolTxs.map(_.fees).sum assert(targetFee * 0.9 <= actualFee && actualFee <= targetFee * 1.1, s"actualFee=$actualFee targetFee=$targetFee") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 1cf810f5d1..63e5efdcae 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -441,17 +441,17 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val localPerCommitmentPointF = keyManagerF.commitmentPoint(channelKeyPathF, commitmentsF.localCommitIndex) val revokedCommitTx = { val commitTx = localCommitF.commitTxAndRemoteSig.commitTx - val localSig = keyManagerF.sign(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex), TxOwner.Local, commitmentFormat) + val localSig = keyManagerF.sign(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex), TxOwner.Local, commitmentFormat, Nil) val RemoteSignature.FullSignature(remoteSig) = localCommitF.commitTxAndRemoteSig.remoteSig Transactions.addSigs(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex).publicKey, commitmentsF.latest.remoteFundingPubKey, localSig, remoteSig).tx } val htlcSuccess = htlcSuccessTxs.zip(Seq(preimage1, preimage2)).map { case (htlcTxAndSigs, preimage) => - val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat) + val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat, Nil) Transactions.addSigs(htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcSuccessTx], localSig, htlcTxAndSigs.remoteSig, preimage, commitmentsF.params.commitmentFormat).tx } val htlcTimeout = htlcTimeoutTxs.map { htlcTxAndSigs => - val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat) + val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat, Nil) Transactions.addSigs(htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcTimeoutTx], localSig, htlcTxAndSigs.remoteSig, commitmentsF.params.commitmentFormat).tx } htlcSuccess.foreach(tx => Transaction.correctlySpends(tx, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index 336ce4cc23..e5334cd986 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -209,9 +209,9 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { remotePaymentBasePoint = Remote.payment_basepoint, localIsChannelOpener = true, outputs = outputs) - val local_sig = tx.sign(Local.funding_privkey, TxOwner.Local, commitmentFormat) + val local_sig = tx.sign(Local.funding_privkey, TxOwner.Local, commitmentFormat, Nil) logger.info(s"# local_signature = ${Scripts.der(local_sig).dropRight(1).toHex}") - val remote_sig = tx.sign(Remote.funding_privkey, TxOwner.Remote, commitmentFormat) + val remote_sig = tx.sign(Remote.funding_privkey, TxOwner.Remote, commitmentFormat, Nil) logger.info(s"remote_signature: ${Scripts.der(remote_sig).dropRight(1).toHex}") Transactions.addSigs(tx, Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig) } @@ -248,8 +248,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val signedTxs = htlcTxs.collect { case tx: HtlcSuccessTx => - val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat) - val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat) + val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat, Nil) + val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat, Nil) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.asInstanceOf[InputInfo.SegwitInput].redeemScript)) val preimage = paymentPreimages.find(p => Crypto.sha256(p) == tx.paymentHash).get val tx1 = Transactions.addSigs(tx, localSig, remoteSig, preimage, commitmentFormat) @@ -260,8 +260,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"htlc_success_tx (htlc #$htlcIndex): ${tx1.tx}") tx1 case tx: HtlcTimeoutTx => - val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat) - val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat) + val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat, Nil) + val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat, Nil) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.asInstanceOf[InputInfo.SegwitInput].redeemScript)) val tx1 = Transactions.addSigs(tx, localSig, remoteSig, commitmentFormat) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 8ae556d089..45323435b9 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -20,9 +20,10 @@ import fr.acinq.bitcoin.SigHash._ 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, 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.bitcoin.{ScriptFlags, ScriptTree, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ +import fr.acinq.eclair.blockchain.fee.ConfirmationPriority.Fast import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc} @@ -291,8 +292,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434445L val commitTx = { val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) + val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat, Nil) Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) } @@ -318,8 +319,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // either party spends local->remote htlc output with htlc timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat) - val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat) + val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat, Nil) val signed = addSigs(htlcTimeoutTx, localSig, remoteSig, DefaultCommitmentFormat) assert(checkSpendable(signed).isSuccess) } @@ -327,7 +328,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc1 timeout tx val Right(htlcDelayed) = makeHtlcDelayedTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit @@ -338,7 +339,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends local->remote htlc1/htlc3 output directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) - val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat) + val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) } @@ -346,8 +347,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(0), paymentPreimage4) :: Nil) { - val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat) - val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat) + val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat, Nil) val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, DefaultCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // check remote sig @@ -357,7 +358,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc2 success tx val Right(htlcDelayed) = makeHtlcDelayedTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc4 success tx because it is below the dust limit @@ -367,35 +368,35 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) + val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends main output val Right(claimP2WPKHOutputTx) = makeClaimP2WPKHOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimP2WPKHOutputTx.sign(remotePaymentPriv, TxOwner.Local, DefaultCommitmentFormat) + val localSig = claimP2WPKHOutputTx.sign(remotePaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signedTx = addSigs(claimP2WPKHOutputTx, remotePaymentPriv.publicKey, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends remote->local htlc output directly in case of timeout val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc2, feeratePerKw, DefaultCommitmentFormat) - val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat) + val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) } { // remote spends local main delayed output with revocation key val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) + val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signed = addSigs(mainPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } { // remote spends htlc1's htlc-timeout tx with revocation key val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) + val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit @@ -410,14 +411,14 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } { // remote spends htlc2's htlc-success tx with revocation key val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) + val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit @@ -432,7 +433,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat) + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -528,8 +529,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434445L val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(UnsafeLegacyAnchorOutputsCommitmentFormat), outputs, UnsafeLegacyAnchorOutputsCommitmentFormat) @@ -564,7 +565,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -576,7 +577,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends main delayed output val Right(claimRemoteDelayedOutputTx) = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimRemoteDelayedOutputTx.sign(remotePaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = claimRemoteDelayedOutputTx.sign(remotePaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signedTx = addSigs(claimRemoteDelayedOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -584,7 +585,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // local spends local anchor val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, localFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -592,28 +593,28 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends remote anchor val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, remoteFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = claimAnchorOutputTx.sign(remoteFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = claimAnchorOutputTx.sign(remoteFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends local main delayed output with revocation key val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signed = addSigs(mainPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } { // local spends received htlc with HTLC-timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) - val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signedTx = addSigs(htlcTimeoutTx, localSig, remoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { - val invalidRemoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, sighash) + val invalidRemoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, sighash, Nil) val invalidTx = addSigs(htlcTimeoutTx, localSig, invalidRemoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(invalidTx).isFailure) } @@ -622,7 +623,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc1 timeout tx val Right(htlcDelayed) = makeHtlcDelayedTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit @@ -632,8 +633,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends offered htlc with HTLC-success tx for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage4) :: (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(2), paymentPreimage2) :: Nil) { - val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) - val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // check remote sig @@ -641,7 +642,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { - val invalidRemoteSig = htlcSuccessTx.sign(remoteHtlcPriv, sighash) + val invalidRemoteSig = htlcSuccessTx.sign(remoteHtlcPriv, sighash, Nil) val invalidTx = addSigs(htlcSuccessTx, localSig, invalidRemoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(invalidTx).isFailure) assert(!invalidTx.checkSig(invalidRemoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat)) @@ -653,7 +654,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Right(htlcDelayedA) = makeHtlcDelayedTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Right(htlcDelayedB) = makeHtlcDelayedTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (htlcDelayed <- Seq(htlcDelayedA, htlcDelayedB)) { - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -665,7 +666,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends local->remote htlc outputs directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) } @@ -673,7 +674,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends htlc1's htlc-timeout tx with revocation key val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit @@ -684,7 +685,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends remote->local htlc output directly in case of timeout for (htlc <- Seq(htlc2a, htlc2b)) { val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) } @@ -694,7 +695,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Seq(Right(claimHtlcDelayedPenaltyTxA)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Seq(Right(claimHtlcDelayedPenaltyTxB)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (claimHtlcSuccessPenaltyTx <- Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB)) { - val sig = claimHtlcSuccessPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val sig = claimHtlcSuccessPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signed = addSigs(claimHtlcSuccessPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } @@ -724,7 +725,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -737,7 +738,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat) + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -834,12 +835,42 @@ class TransactionsSpec extends AnyFunSuite with Logging { 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 claimLocalAnchorOutputTx = ClaimLocalAnchorOutputTx(InputInfo.TaprootInput(OutPoint(commitTx, 2), commitTx.txOut(2), localDelayedPaymentPriv.xOnlyPublicKey(), Some(Scripts.Taproot.anchorScriptTree)), tx, ConfirmationTarget.Priority(Fast)) + val sig = claimLocalAnchorOutputTx.sign(localDelayedPaymentPriv, SigHash.SIGHASH_DEFAULT, Nil) val witness = Script.witnessKeyPathPay2tr(sig) tx.updateWitness(0, witness) } Transaction.correctlySpends(spendLocalAnchorTx, Seq(commitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + { + val walletKey = PrivateKey(ByteVector.fromValidHex("01" * 32)) + val walletInput = Transaction(version = 2, txIn = Nil, txOut = TxOut(5000.sat, Script.pay2wpkh(walletKey.publicKey)) :: Nil, lockTime = 0) + val tx = Transaction( + version = 2, + txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = TxIn.SEQUENCE_FINAL) :: TxIn(OutPoint(walletInput, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, + txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, + lockTime = 0) + // we add a wallet input to our claim anchor tx + val claimLocalAnchorOutputTx = ClaimLocalAnchorOutputTx(InputInfo.TaprootInput(OutPoint(commitTx, 2), commitTx.txOut(2), localDelayedPaymentPriv.xOnlyPublicKey(), Some(Scripts.Taproot.anchorScriptTree)), tx, ConfirmationTarget.Priority(Fast)) + val walletSig = tx.signInput(1, Script.pay2pkh(walletKey.publicKey), SIGHASH_ALL, walletInput.txOut(0).amount, SigVersion.SIGVERSION_WITNESS_V0, walletKey) + + // if we omit the wallet input, we'll generate an invalid signature + val badSig = claimLocalAnchorOutputTx.sign(localDelayedPaymentPriv, SigHash.SIGHASH_DEFAULT, Nil) + val badTx = tx. + updateWitness(0, Script.witnessKeyPathPay2tr(badSig)) + .updateWitness(1, ScriptWitness(walletSig :: walletKey.publicKey.value :: Nil)) + assertThrows[IllegalArgumentException] { + Transaction.correctlySpends(badTx, Seq(commitTx, walletInput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + + // but if we pass in the extra wallet input, signature will be valid + val goodSig = claimLocalAnchorOutputTx.sign(localDelayedPaymentPriv, SigHash.SIGHASH_DEFAULT, walletInput.txOut(0) :: Nil) + val goodTx = tx. + updateWitness(0, Script.witnessKeyPathPay2tr(goodSig)) + .updateWitness(1, ScriptWitness(walletSig :: walletKey.publicKey.value :: Nil)) + Transaction.correctlySpends(goodTx, Seq(commitTx, walletInput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) + } + val spendLocalAnchorAfterDelayTx = { val tx = Transaction( version = 2, @@ -1042,8 +1073,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val (commitTx, outputs, htlcTxs) = { val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat) + val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat, Nil) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw, outputs, DefaultCommitmentFormat) (commitTx, outputs, htlcTxs) From 81782f3a925d5b7461a739513b28561056cc2387 Mon Sep 17 00:00:00 2001 From: sstone Date: Thu, 27 Mar 2025 17:05:29 +0100 Subject: [PATCH 4/6] Fixup: use a map for wallet utxos, remember wallet utxo for funded txs we want to bump --- .../eclair/SpendFromChannelAddress.scala | 2 +- .../fr/acinq/eclair/channel/Commitments.scala | 10 +- .../fr/acinq/eclair/channel/Helpers.scala | 38 +++--- .../channel/fsm/ChannelOpenSingleFunded.scala | 8 +- .../channel/fund/InteractiveTxBuilder.scala | 8 +- .../channel/publish/ReplaceableTxFunder.scala | 62 +++++----- .../crypto/keymanager/ChannelKeyManager.scala | 8 +- .../keymanager/LocalChannelKeyManager.scala | 8 +- .../eclair/transactions/Transactions.scala | 16 +-- .../publish/ReplaceableTxFunderSpec.scala | 16 +-- .../publish/ReplaceableTxPublisherSpec.scala | 10 +- .../integration/ChannelIntegrationSpec.scala | 6 +- .../eclair/transactions/TestVectorsSpec.scala | 12 +- .../transactions/TransactionsSpec.scala | 112 +++++++----------- 14 files changed, 137 insertions(+), 179 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala index a6cb0c329e..8f799def24 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/SpendFromChannelAddress.scala @@ -49,7 +49,7 @@ trait SpendFromChannelAddress { localFundingPubkey, TxOwner.Local, // unused DefaultCommitmentFormat, // unused - Nil + Map.empty ) witness = Scripts.witness2of2(localSig, remoteSig, localFundingPubkey.publicKey, remoteFundingPubkey) signedTx = unsignedTx.updateWitness(0, witness) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index bbe7e5b58a..2bf8cc5e34 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -218,10 +218,10 @@ object LocalCommit { case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: TxId, remotePerCommitmentPoint: PublicKey) { def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo): CommitSig = { val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, index, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remotePerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Nil) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Map.empty) val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Nil)) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Map.empty)) CommitSig(params.channelId, sig, htlcSigs.toList) } } @@ -622,11 +622,11 @@ case class Commitment(fundingTxIndex: Long, // remote commitment will include all local proposed changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, params.channelConfig, params.channelFeatures, remoteCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, remoteNextPerCommitmentPoint, spec) - val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Nil) + val sig = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Remote, params.commitmentFormat, Map.empty) val sortedHtlcTxs: Seq[TransactionWithInputInfo] = htlcTxs.sortBy(_.input.outPoint.index) val channelKeyPath = keyManager.keyPath(params.localParams, params.channelConfig) - val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Nil)) + val htlcSigs = sortedHtlcTxs.map(keyManager.sign(_, keyManager.htlcPoint(channelKeyPath), remoteNextPerCommitmentPoint, TxOwner.Remote, params.commitmentFormat, Map.empty)) // NB: IN/OUT htlcs are inverted because this is the remote commit log.info(s"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(",")) @@ -659,7 +659,7 @@ case class Commitment(fundingTxIndex: Long, /** Return a fully signed commit tx, that can be published as-is. */ def fullySignedLocalCommitTx(params: ChannelParams, keyManager: ChannelKeyManager): CommitTx = { val unsignedCommitTx = localCommit.commitTxAndRemoteSig.commitTx - val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat, Nil) + val localSig = keyManager.sign(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex), TxOwner.Local, params.commitmentFormat, Map.empty) val RemoteSignature.FullSignature(remoteSig) = localCommit.commitTxAndRemoteSig.remoteSig val commitTx = addSigs(unsignedCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex).publicKey, remoteFundingPubKey, localSig, remoteSig) // We verify the remote signature when receiving their commit_sig, so this check should always pass. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 01bcdabaac..24a1448459 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -701,7 +701,7 @@ object Helpers { log.debug("making closing tx with closing fee={} and commitments:\n{}", closingFees.preferred, commitment.specs2String) val dustLimit = commitment.localParams.dustLimit.max(commitment.remoteParams.dustLimit) val closingTx = Transactions.makeClosingTx(commitment.commitInput, localScriptPubkey, remoteScriptPubkey, commitment.localParams.paysClosingFees, dustLimit, closingFees.preferred, commitment.localCommit.spec) - val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex), TxOwner.Local, commitment.params.commitmentFormat, Nil) + val localClosingSig = keyManager.sign(closingTx, keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex), TxOwner.Local, commitment.params.commitmentFormat, Map.empty) val closingSigned = ClosingSigned(commitment.channelId, closingFees.preferred, localClosingSig, TlvStream(ClosingSignedTlv.FeeRange(closingFees.min, closingFees.max))) log.debug(s"signed closing txid=${closingTx.tx.txid} with closing fee=${closingSigned.feeSatoshis}") log.debug(s"closingTxid=${closingTx.tx.txid} closingTx=${closingTx.tx}}") @@ -741,9 +741,9 @@ object Helpers { } val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) val closingComplete = ClosingComplete(commitment.channelId, localScriptPubkey, remoteScriptPubkey, closingFee.fee, currentBlockHeight.toLong, TlvStream(Set( - closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil))), - closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil))), - closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil))), + closingTxs.localAndRemote_opt.map(tx => ClosingTlv.CloserAndCloseeOutputs(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), + closingTxs.localOnly_opt.map(tx => ClosingTlv.CloserOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), + closingTxs.remoteOnly_opt.map(tx => ClosingTlv.CloseeOutputOnly(keyManager.sign(tx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty))), ).flatten[ClosingTlv])) Right(closingTxs, closingComplete) } @@ -774,7 +774,7 @@ object Helpers { closingTxsWithSigs.headOption match { case Some((closingTx, remoteSig, sigToTlv)) => val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil) + val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) Transactions.checkSpendable(signedClosingTx) match { case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) @@ -800,7 +800,7 @@ object Helpers { closingTxsWithSig.headOption match { case Some((closingTx, remoteSig)) => val localFundingPubKey = keyManager.fundingPublicKey(commitment.localParams.fundingKeyPath, commitment.fundingTxIndex) - val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Nil) + val localSig = keyManager.sign(closingTx, localFundingPubKey, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) val signedClosingTx = Transactions.addSigs(closingTx, localFundingPubKey.publicKey, commitment.remoteFundingPubKey, localSig, remoteSig) Transactions.checkSpendable(signedClosingTx) match { case Failure(_) => Left(InvalidCloseSignature(commitment.channelId, signedClosingTx.tx.txid)) @@ -882,7 +882,7 @@ object Helpers { // first we will claim our main output as soon as the delay is over val mainDelayedTx = withTxGenerationLog("local-main-delayed") { Transactions.makeClaimLocalDelayedOutputTx(tx, commitment.localParams.dustLimit, localRevocationPubkey, commitment.remoteParams.toSelfDelay, localDelayedPubkey, finalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) Transactions.addSigs(claimDelayed, sig) }) } @@ -947,7 +947,7 @@ object Helpers { if (hash2Preimage.contains(paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-success") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) + val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) Right(Transactions.addSigs(txInfo, localSig, remoteSig, hash2Preimage(paymentHash), commitment.params.commitmentFormat)) }) } else if (failedIncomingHtlcs.contains(txInfo.htlcId)) { @@ -968,7 +968,7 @@ object Helpers { // claim the output, we will learn the preimage from their transaction, otherwise we will get our funds // back after the timeout. Some(txInfo.input.outPoint -> withTxGenerationLog("htlc-timeout") { - val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) + val localSig = keyManager.sign(txInfo, keyManager.htlcPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) Right(Transactions.addSigs(txInfo, localSig, remoteSig, commitment.params.commitmentFormat)) }) }.flatten.toMap @@ -991,7 +991,7 @@ object Helpers { // Note that this will return None if the transaction wasn't one of our HTLC transactions, which may happen // if our peer was able to claim the HTLC output before us (race condition between success and timeout). Transactions.makeHtlcDelayedTx(tx, commitment.localParams.dustLimit, localRevocationPubkey, commitment.remoteParams.toSelfDelay, localDelayedPubkey, finalScriptPubKey, feeratePerKwDelayed).map(claimDelayed => { - val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) + val sig = keyManager.sign(claimDelayed, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) Transactions.addSigs(claimDelayed, sig) }) } @@ -1073,13 +1073,13 @@ object Helpers { params.commitmentFormat match { case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { Transactions.makeClaimP2WPKHOutputTx(tx, params.localParams.dustLimit, localPubkey, finalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, params.commitmentFormat, Nil) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, params.commitmentFormat, Map.empty) Transactions.addSigs(claimMain, localPubkey, sig) }) } case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(tx, params.localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feeratePerKwMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, params.commitmentFormat, Nil) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, params.commitmentFormat, Map.empty) Transactions.addSigs(claimMain, sig) }) } @@ -1126,7 +1126,7 @@ object Helpers { if (hash2Preimage.contains(add.paymentHash)) { // We immediately spend incoming htlcs for which we have the preimage. Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-success") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) + val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) Right(Transactions.addSigs(claimHtlcTx, sig, hash2Preimage(add.paymentHash))) }) } else if (failedIncomingHtlcs.contains(add.id)) { @@ -1152,7 +1152,7 @@ object Helpers { Transactions.makeClaimHtlcTimeoutTx(remoteCommitTx.tx, outputs, commitment.localParams.dustLimit, localHtlcPubkey, remoteHtlcPubkey, remoteRevocationPubkey, finalScriptPubKey, add, feeratePerKwHtlc, commitment.params.commitmentFormat) }.map(claimHtlcTx => { Some(claimHtlcTx.input.outPoint -> withTxGenerationLog("claim-htlc-timeout") { - val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Nil) + val sig = keyManager.sign(claimHtlcTx, keyManager.htlcPoint(channelKeyPath), remoteCommit.remotePerCommitmentPoint, TxOwner.Local, commitment.params.commitmentFormat, Map.empty) Right(Transactions.addSigs(claimHtlcTx, sig)) }) }) @@ -1220,13 +1220,13 @@ object Helpers { case ct => ct.commitmentFormat match { case DefaultCommitmentFormat => withTxGenerationLog("remote-main") { Transactions.makeClaimP2WPKHOutputTx(commitTx, localParams.dustLimit, localPaymentPubkey, finalScriptPubKey, feerateMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat, Nil) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), remotePerCommitmentPoint, TxOwner.Local, commitmentFormat, Map.empty) Transactions.addSigs(claimMain, localPaymentPubkey, sig) }) } case _: AnchorOutputsCommitmentFormat => withTxGenerationLog("remote-main-delayed") { Transactions.makeClaimRemoteDelayedOutputTx(commitTx, localParams.dustLimit, localPaymentPoint, finalScriptPubKey, feerateMain).map(claimMain => { - val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat, Nil) + val sig = keyManager.sign(claimMain, keyManager.paymentPoint(channelKeyPath), TxOwner.Local, commitmentFormat, Map.empty) Transactions.addSigs(claimMain, sig) }) } @@ -1236,7 +1236,7 @@ object Helpers { // then we punish them by stealing their main output val mainPenaltyTx = withTxGenerationLog("main-penalty") { Transactions.makeMainPenaltyTx(commitTx, localParams.dustLimit, remoteRevocationPubkey, finalScriptPubKey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, feeratePenalty).map(txinfo => { - val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Nil) + val sig = keyManager.sign(txinfo, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) Transactions.addSigs(txinfo, sig) }) } @@ -1256,7 +1256,7 @@ object Helpers { val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript) withTxGenerationLog("htlc-penalty") { Transactions.makeHtlcPenaltyTx(commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, finalScriptPubKey, feeratePenalty).map(htlcPenalty => { - val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Nil) + val sig = keyManager.sign(htlcPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) Transactions.addSigs(htlcPenalty, sig, remoteRevocationPubkey) }) } @@ -1312,7 +1312,7 @@ object Helpers { val penaltyTxs = Transactions.makeClaimHtlcDelayedOutputPenaltyTxs(htlcTx, localParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, finalScriptPubKey, feeratePerKwPenalty).flatMap(claimHtlcDelayedOutputPenaltyTx => { withTxGenerationLog("htlc-delayed-penalty") { claimHtlcDelayedOutputPenaltyTx.map(htlcDelayedPenalty => { - val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Nil) + val sig = keyManager.sign(htlcDelayedPenalty, keyManager.revocationPoint(channelKeyPath), remotePerCommitmentSecret, TxOwner.Local, commitmentFormat, Map.empty) val signedTx = Transactions.addSigs(htlcDelayedPenalty, sig) // We need to make sure that the tx is indeed valid. Transaction.correctlySpends(signedTx.tx, Seq(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala index 931dafa06a..934b9d6d7b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/ChannelOpenSingleFunded.scala @@ -216,7 +216,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Left(ex) => handleLocalError(ex, d, None) case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => require(fundingTx.txOut(fundingTxOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, s"pubkey script mismatch!") - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), TxOwner.Remote, params.commitmentFormat, Nil) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0), TxOwner.Remote, params.commitmentFormat, Map.empty) // signature of their initial commitment tx that pays remote pushMsat val fundingCreated = FundingCreated( temporaryChannelId = temporaryChannelId, @@ -264,12 +264,12 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx)) => // check remote signature validity val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Nil) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Map.empty) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(_) => handleLocalError(InvalidCommitmentSignature(temporaryChannelId, fundingTxId, fundingTxIndex = 0, localCommitTx.tx), d, None) case Success(_) => - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, params.commitmentFormat, Nil) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, params.commitmentFormat, Map.empty) val channelId = toLongId(fundingTxId, fundingTxOutputIndex) val fundingSigned = FundingSigned( channelId = channelId, @@ -313,7 +313,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { case Event(msg@FundingSigned(_, remoteSig, _), d@DATA_WAIT_FOR_FUNDING_SIGNED(params, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, fundingCreated, _)) => // we make sure that their sig checks out and that our first commit tx is spendable val fundingPubKey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex = 0) - val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Nil) + val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, params.commitmentFormat, Map.empty) val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 0adedc7d75..225be50bda 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -105,6 +105,8 @@ object InteractiveTxBuilder { // @formatter:off def info: InputInfo def weight: Int + // we don't need to provide extra inputs here, as this method will only be called for multisig-2-of-2 inputs which only require the output that is spent for signing + // for taproot channels, we'll use a musig2 input that is not signed like this: instead, the signature will be the Musig2 aggregation if a local and remote partial signature def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction): ByteVector64 // @formatter:on } @@ -114,7 +116,7 @@ object InteractiveTxBuilder { override def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction): ByteVector64 = { val localFundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) - keyManager.sign(Transactions.SpliceTx(info, tx), localFundingPubkey, TxOwner.Local, params.commitmentFormat, Nil) + keyManager.sign(Transactions.SpliceTx(info, tx), localFundingPubkey, TxOwner.Local, params.commitmentFormat, Map.empty) } } @@ -847,9 +849,9 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon case Right((localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)) => require(fundingTx.txOut(fundingOutputIndex).publicKeyScript == localCommitTx.input.txOut.publicKeyScript, "pubkey script mismatch!") val fundingPubKey = keyManager.fundingPublicKey(channelParams.localParams.fundingKeyPath, purpose.fundingTxIndex) - val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat, Nil) + val localSigOfRemoteTx = keyManager.sign(remoteCommitTx, fundingPubKey, TxOwner.Remote, channelParams.channelFeatures.commitmentFormat, Map.empty) val localPerCommitmentPoint = keyManager.htlcPoint(keyManager.keyPath(channelParams.localParams, channelParams.channelConfig)) - val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat, Nil)).toList + val htlcSignatures = sortedHtlcTxs.map(keyManager.sign(_, localPerCommitmentPoint, purpose.remotePerCommitmentPoint, TxOwner.Remote, channelParams.commitmentFormat, Map.empty)).toList val localCommitSig = CommitSig(fundingParams.channelId, localSigOfRemoteTx, htlcSignatures) val localCommit = UnsignedLocalCommit(purpose.localCommitIndex, localSpec, localCommitTx, htlcTxs = Nil) val remoteCommit = RemoteCommit(purpose.remoteCommitIndex, remoteSpec, remoteCommitTx.tx.txid, purpose.remotePerCommitmentPoint) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index a55fab1a4c..33fcc2b478 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -20,7 +20,7 @@ import akka.actor.typed.eventstream.EventStream import akka.actor.typed.scaladsl.{ActorContext, Behaviors} import akka.actor.typed.{ActorRef, Behavior} import fr.acinq.bitcoin.psbt.Psbt -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Satoshi, Transaction, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, OutPoint, Satoshi, Transaction, TxOut} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, FeeratesPerKw, OnChainFeeConf} @@ -32,7 +32,6 @@ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.{NodeParams, NotificationsLogger} import scala.concurrent.{ExecutionContext, Future} -import scala.jdk.CollectionConverters.ListHasAsScala import scala.util.{Failure, Success} /** @@ -50,14 +49,14 @@ object ReplaceableTxFunder { sealed trait Command case class FundTransaction(replyTo: ActorRef[FundingResult], cmd: TxPublisher.PublishReplaceableTx, tx: Either[FundedTx, ReplaceableTxWithWitnessData], targetFeerate: FeeratePerKw) extends Command - private case class AddInputsOk(tx: ReplaceableTxWithWitnessData, totalAmountIn: Satoshi, walletUtxos: Seq[TxOut]) extends Command + private case class AddInputsOk(tx: ReplaceableTxWithWitnessData, totalAmountIn: Satoshi, walletUtxos: Map[OutPoint, TxOut]) extends Command private case class AddInputsFailed(reason: Throwable) extends Command private case class SignWalletInputsOk(signedTx: Transaction) extends Command private case class SignWalletInputsFailed(reason: Throwable) extends Command private case object UtxosUnlocked extends Command // @formatter:on - case class FundedTx(signedTxWithWitnessData: ReplaceableTxWithWitnessData, totalAmountIn: Satoshi, feerate: FeeratePerKw) { + case class FundedTx(signedTxWithWitnessData: ReplaceableTxWithWitnessData, totalAmountIn: Satoshi, feerate: FeeratePerKw, walletInputs: Map[OutPoint, TxOut]) { require(signedTxWithWitnessData.txInfo.tx.txIn.nonEmpty, "funded transaction must have inputs") require(signedTxWithWitnessData.txInfo.tx.txOut.nonEmpty, "funded transaction must have outputs") val signedTx: Transaction = signedTxWithWitnessData.txInfo.tx @@ -249,7 +248,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, val htlcFeerate = cmd.commitment.localCommit.spec.htlcTxFeerate(cmd.commitment.params.commitmentFormat) if (targetFeerate <= htlcFeerate) { log.debug("publishing {} without adding inputs: txid={}", cmd.desc, htlcTx.txInfo.tx.txid) - sign(txWithWitnessData, htlcFeerate, htlcTx.txInfo.amountIn, Nil) + sign(txWithWitnessData, htlcFeerate, htlcTx.txInfo.amountIn, Map.empty) } else { addWalletInputs(htlcTx, targetFeerate) } @@ -261,7 +260,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, replyTo ! FundingFailed(TxPublisher.TxRejectedReason.TxSkipped(retryNextBlock = true)) Behaviors.stopped case Right(updatedClaimHtlcTx) => - sign(updatedClaimHtlcTx, targetFeerate, updatedClaimHtlcTx.txInfo.amountIn, Nil) + sign(updatedClaimHtlcTx, targetFeerate, updatedClaimHtlcTx.txInfo.amountIn, Map.empty) } } } @@ -275,7 +274,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, Behaviors.stopped case AdjustPreviousTxOutputResult.TxOutputAdjusted(updatedTx) => log.debug("bumping {} fees without adding new inputs: txid={}", cmd.desc, updatedTx.txInfo.tx.txid) - sign(updatedTx, targetFeerate, previousTx.totalAmountIn, Nil) + sign(updatedTx, targetFeerate, previousTx.totalAmountIn, Map.empty) case AdjustPreviousTxOutputResult.AddWalletInputs(tx) => log.debug("bumping {} fees requires adding new inputs (feerate={})", cmd.desc, targetFeerate) // We restore the original transaction (remove previous attempt's wallet inputs). @@ -310,20 +309,13 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } } - private def sign(fundedTx: ReplaceableTxWithWitnessData, txFeerate: FeeratePerKw, amountIn: Satoshi, walletUtxos: Seq[TxOut]): Behavior[Command] = { + private def sign(fundedTx: ReplaceableTxWithWitnessData, txFeerate: FeeratePerKw, amountIn: Satoshi, walletUtxos: Map[OutPoint, TxOut]): Behavior[Command] = { val channelKeyPath = keyManager.keyPath(cmd.commitment.localParams, cmd.commitment.params.channelConfig) fundedTx match { case claimAnchorTx: ClaimLocalAnchorWithWitnessData => - log.info(s"signing ${claimAnchorTx.txInfo.tx} with wallet utxos $walletUtxos") - val localSig = fundedTx.txInfo.input match { - case _: InputInfo.SegwitInput => - keyManager.sign(claimAnchorTx.txInfo, keyManager.fundingPublicKey(cmd.commitment.localParams.fundingKeyPath, cmd.commitment.fundingTxIndex), TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) - case _: InputInfo.TaprootInput => - val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitment.localCommit.index) - keyManager.sign(claimAnchorTx.txInfo, keyManager.delayedPaymentPoint(channelKeyPath), localPerCommitmentPoint, TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) - } + val localSig = keyManager.sign(claimAnchorTx.txInfo, keyManager.fundingPublicKey(cmd.commitment.localParams.fundingKeyPath, cmd.commitment.fundingTxIndex), TxOwner.Local, cmd.commitment.params.commitmentFormat, walletUtxos) val signedTx = claimAnchorTx.copy(txInfo = addSigs(claimAnchorTx.txInfo, localSig)) - signWalletInputs(signedTx, txFeerate, amountIn) + signWalletInputs(signedTx, txFeerate, amountIn, walletUtxos) case htlcTx: HtlcWithWitnessData => val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, cmd.commitment.localCommit.index) val localHtlcBasepoint = keyManager.htlcPoint(channelKeyPath) @@ -334,9 +326,9 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } val hasWalletInputs = htlcTx.txInfo.tx.txIn.size > 1 if (hasWalletInputs) { - signWalletInputs(signedTx, txFeerate, amountIn) + signWalletInputs(signedTx, txFeerate, amountIn, walletUtxos) } else { - replyTo ! TransactionReady(FundedTx(signedTx, amountIn, txFeerate)) + replyTo ! TransactionReady(FundedTx(signedTx, amountIn, txFeerate, walletUtxos)) Behaviors.stopped } case claimHtlcTx: ClaimHtlcWithWitnessData => @@ -350,12 +342,12 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, case legacyClaimHtlcSuccess: LegacyClaimHtlcSuccessWithWitnessData => legacyClaimHtlcSuccess case claimTimeout: ClaimHtlcTimeoutWithWitnessData => claimTimeout.copy(txInfo = addSigs(claimTimeout.txInfo, sig)) } - replyTo ! TransactionReady(FundedTx(signedTx, amountIn, txFeerate)) + replyTo ! TransactionReady(FundedTx(signedTx, amountIn, txFeerate, walletUtxos)) Behaviors.stopped } } - private def signWalletInputs(locallySignedTx: ReplaceableTxWithWalletInputs, txFeerate: FeeratePerKw, amountIn: Satoshi): Behavior[Command] = { + private def signWalletInputs(locallySignedTx: ReplaceableTxWithWalletInputs, txFeerate: FeeratePerKw, amountIn: Satoshi, walletUtxos: Map[OutPoint, TxOut]): Behavior[Command] = { import fr.acinq.bitcoin.scalacompat.KotlinUtils._ // We create a PSBT with the non-wallet input already signed: @@ -363,10 +355,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, case InputInfo.SegwitInput(_, _, redeemScript) => fr.acinq.bitcoin.Script.parse(redeemScript) case _: InputInfo.TaprootInput => null } - val sigHash = locallySignedTx.txInfo.input match { - case _: InputInfo.TaprootInput => fr.acinq.bitcoin.SigHash.SIGHASH_DEFAULT - case _: InputInfo.SegwitInput => fr.acinq.bitcoin.SigHash.SIGHASH_ALL - } + val sigHash = locallySignedTx.txInfo.sighash(TxOwner.Local, cmd.commitment.params.commitmentFormat) val psbt = new Psbt(locallySignedTx.txInfo.tx) .updateWitnessInput( locallySignedTx.txInfo.input.outPoint, @@ -414,7 +403,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, Behaviors.receiveMessagePartial { case SignWalletInputsOk(signedTx) => val fullySignedTx = locallySignedTx.updateTx(signedTx) - replyTo ! TransactionReady(FundedTx(fullySignedTx, amountIn, txFeerate)) + replyTo ! TransactionReady(FundedTx(fullySignedTx, amountIn, txFeerate, walletUtxos)) Behaviors.stopped case SignWalletInputsFailed(reason) => log.error(s"cannot sign ${cmd.desc}: ", reason) @@ -437,15 +426,20 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } } - private def addInputs(tx: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ReplaceableTxWithWalletInputs, Satoshi, Seq[TxOut])] = { - import fr.acinq.bitcoin.scalacompat.KotlinUtils._ + private def addInputs(tx: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ReplaceableTxWithWalletInputs, Satoshi, Map[OutPoint, TxOut])] = { - def getWalletUtxos(txInfo: TransactionWithInputInfo): Future[Seq[TxOut]] = { - val psbt = new Psbt(txInfo.tx) - bitcoinClient.utxoUpdatePsbt(psbt).map(updatedPsbt => { - // we skip the first spent (it's the one from txInfo.input), additional inputs are wallet inputs and have been updated by bitcoin core - updatedPsbt.inputs.asScala.tail.map(_.getWitnessUtxo).map(kmp2scala).toSeq - }) + def getWalletUtxos(txInfo: TransactionWithInputInfo): Future[Map[OutPoint, TxOut]] = { + val inputs = txInfo.tx.txIn.filterNot(_.outPoint == txInfo.input.outPoint) + val txids = inputs.map(_.outPoint.txid).toSet + for { + txs <- Future.sequence(txids.toSeq.map(bitcoinClient.getTransaction)) + txMap = txs.map(tx => tx.txid -> tx).toMap + } yield { + inputs.map(_.outPoint).map(outPoint => { + require(txMap.contains(outPoint.txid) && txMap(outPoint.txid).txOut.size >= outPoint.index, s"missing wallet input $outPoint") + outPoint -> txMap(outPoint.txid).txOut(outPoint.index.toInt) + }).toMap + } } tx match { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index 35a0e709c1..50f71781d7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.crypto.keymanager import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet.ExtendedPublicKey -import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, DeterministicWallet, Protocol, TxOut} +import fr.acinq.bitcoin.scalacompat.{ByteVector64, Crypto, DeterministicWallet, OutPoint, Protocol, TxOut} import fr.acinq.eclair.channel.{ChannelConfig, LocalParams} import fr.acinq.eclair.transactions.Transactions.{CommitmentFormat, TransactionWithInputInfo, TxOwner} import scodec.bits.ByteVector @@ -67,7 +67,7 @@ trait ChannelKeyManager { * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with the private key that matches the input extended public key */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 /** @@ -81,7 +81,7 @@ trait ChannelKeyManager { * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 /** * Ths method is used to spend revoked transactions, with the corresponding revocation key @@ -94,7 +94,7 @@ trait ChannelKeyManager { * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. */ - def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 + def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 /** * Sign a channel announcement message diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 952a0ac5a5..96e0acded0 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.crypto.keymanager import com.google.common.cache.{CacheBuilder, CacheLoader, LoadingCache} import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.scalacompat.DeterministicWallet._ -import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, ByteVector32, ByteVector64, Crypto, DeterministicWallet, OutPoint, TxOut} import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.Monitoring.{Metrics, Tags} import fr.acinq.eclair.router.Announcements @@ -107,7 +107,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with the private key that matches the input extended public key */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { // NB: not all those transactions are actually commit txs (especially during closing), but this is good enough for monitoring purposes val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.CommitTx) Metrics.SignTxCount.withTags(tags).increment() @@ -128,7 +128,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { // NB: not all those transactions are actually htlc txs (especially during closing), but this is good enough for monitoring purposes val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.HtlcTx) Metrics.SignTxCount.withTags(tags).increment() @@ -150,7 +150,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. */ - override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { val tags = TagSet.Empty.withTag(Tags.TxOwner, txOwner.toString).withTag(Tags.TxType, Tags.TxTypes.RevokedTx) Metrics.SignTxCount.withTags(tags).increment() KamonExt.time(Metrics.SignTxDuration.withTags(tags)) { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index f669ac52ea..cc8dd3b50c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.SigVersion._ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, XonlyPublicKey, ripemd160} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ -import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash} +import fr.acinq.bitcoin.{ScriptFlags, ScriptTree} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} import fr.acinq.eclair.transactions.CommitmentOutput._ @@ -131,11 +131,11 @@ object Transactions { /** Sighash flags to use when signing the transaction. */ def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = SIGHASH_ALL - def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { sign(key, sighash(txOwner, commitmentFormat), extraUtxos) } - def sign(key: PrivateKey, sighashType: Int, extraUtxos: Seq[TxOut]): ByteVector64 = input match { + def sign(key: PrivateKey, sighashType: Int, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = input match { case _: InputInfo.TaprootInput => ByteVector64.Zeroes case InputInfo.SegwitInput(outPoint, txOut, redeemScript) => // NB: the tx may have multiple inputs, we will only sign the one provided in txinfo.input. Bear in mind that the @@ -201,15 +201,7 @@ object Transactions { case class ClaimHtlcSuccessTx(input: InputInfo, tx: Transaction, paymentHash: ByteVector32, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-success" } case class ClaimHtlcTimeoutTx(input: InputInfo, tx: Transaction, htlcId: Long, confirmationTarget: ConfirmationTarget.Absolute) extends ClaimHtlcTx { override def desc: String = "claim-htlc-timeout" } sealed trait ClaimAnchorOutputTx extends TransactionWithInputInfo - case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { - override def desc: String = "local-anchor" - override def sign(key: PrivateKey, sighashType: Int, extraUtxos: Seq[TxOut]): ByteVector64 = input match { // README: this will be updated with a better type hierarchy for taproot inputs - case _: InputInfo.SegwitInput => - super.sign(key, sighashType, extraUtxos) - case t: InputInfo.TaprootInput => - Transaction.signInputTaprootKeyPath(key, tx, 0, input.txOut +: extraUtxos, sighashType, t.scriptTree_opt) - } - } + case class ClaimLocalAnchorOutputTx(input: InputInfo, tx: Transaction, confirmationTarget: ConfirmationTarget) extends ClaimAnchorOutputTx with ReplaceableTransactionWithInputInfo { override def desc: String = "local-anchor" } case class ClaimRemoteAnchorOutputTx(input: InputInfo, tx: Transaction) extends ClaimAnchorOutputTx { override def desc: String = "remote-anchor" } sealed trait ClaimRemoteCommitMainOutputTx extends TransactionWithInputInfo case class ClaimP2WPKHOutputTx(input: InputInfo, tx: Transaction) extends ClaimRemoteCommitMainOutputTx { override def desc: String = "remote-main" } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala index 681859f556..40d369925e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunderSpec.scala @@ -156,16 +156,16 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { commitment.localCommit.returns(localCommit) // We can handle a small feerate update by lowering the change output. - val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitment, commitTx.tx) + val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(5000 sat), commitment, commitTx.tx) assert(feerateUpdate1.txInfo.tx.txIn == previousAnchorTx.txInfo.tx.txIn) assert(feerateUpdate1.txInfo.tx.txOut.length == 1) - val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(6000 sat), commitment, commitTx.tx) + val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(6000 sat), commitment, commitTx.tx) assert(feerateUpdate2.txInfo.tx.txIn == previousAnchorTx.txInfo.tx.txIn) assert(feerateUpdate2.txInfo.tx.txOut.length == 1) assert(feerateUpdate2.txInfo.tx.txOut.head.amount < feerateUpdate1.txInfo.tx.txOut.head.amount) // But if the feerate increase is too large, we must add new wallet inputs. - val AddWalletInputs(previousTx) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(10000 sat), commitment, commitTx.tx) + val AddWalletInputs(previousTx) = adjustPreviousTxOutput(FundedTx(previousAnchorTx, 12000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(10000 sat), commitment, commitTx.tx) assert(previousTx == previousAnchorTx) } @@ -192,11 +192,11 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { )) // We can handle a small feerate update by lowering the change output. - val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitment, commitTx) + val TxOutputAdjusted(feerateUpdate1) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(5000 sat), commitment, commitTx) assert(feerateUpdate1.txInfo.tx.txIn == previousTx.txInfo.tx.txIn) assert(feerateUpdate1.txInfo.tx.txOut.length == 2) assert(feerateUpdate1.txInfo.tx.txOut.head == previousTx.txInfo.tx.txOut.head) - val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(6000 sat), commitment, commitTx) + val TxOutputAdjusted(feerateUpdate2) = adjustPreviousTxOutput(FundedTx(previousTx, 15000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(6000 sat), commitment, commitTx) assert(feerateUpdate2.txInfo.tx.txIn == previousTx.txInfo.tx.txIn) assert(feerateUpdate2.txInfo.tx.txOut.length == 2) assert(feerateUpdate2.txInfo.tx.txOut.head == previousTx.txInfo.tx.txOut.head) @@ -204,7 +204,7 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { // If the previous funding attempt didn't add a change output, we must add new wallet inputs. val previousTxNoChange = previousTx.updateTx(previousTx.txInfo.tx.copy(txOut = Seq(previousTx.txInfo.tx.txOut.head))) - val AddWalletInputs(tx) = adjustPreviousTxOutput(FundedTx(previousTxNoChange, 25000 sat, FeeratePerKw(2500 sat)), FeeratePerKw(5000 sat), commitment, commitTx) + val AddWalletInputs(tx) = adjustPreviousTxOutput(FundedTx(previousTxNoChange, 25000 sat, FeeratePerKw(2500 sat), Map.empty), FeeratePerKw(5000 sat), commitment, commitTx) assert(tx == previousTxNoChange) for (_ <- 1 to 100) { @@ -215,7 +215,7 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { TxOut(changeAmount, Script.pay2wpkh(PlaceHolderPubKey)) ))) val targetFeerate = FeeratePerKw(2500 sat) + FeeratePerKw(Random.nextInt(20000).sat) - adjustPreviousTxOutput(FundedTx(fuzzyPreviousTx, amountIn, FeeratePerKw(2500 sat)), targetFeerate, commitment, commitTx) match { + adjustPreviousTxOutput(FundedTx(fuzzyPreviousTx, amountIn, FeeratePerKw(2500 sat), Map.empty), targetFeerate, commitment, commitTx) match { case AdjustPreviousTxOutputResult.Skip(_) => // nothing do check case AddWalletInputs(tx) => assert(tx == fuzzyPreviousTx) case TxOutputAdjusted(updatedTx) => @@ -238,7 +238,7 @@ class ReplaceableTxFunderSpec extends TestKitBaseClass with AnyFunSuiteLike { var previousAmount = claimHtlc.txInfo.tx.txOut.head.amount for (i <- 1 to 100) { val targetFeerate = FeeratePerKw(250 * i sat) - adjustPreviousTxOutput(FundedTx(claimHtlc, claimHtlc.txInfo.amountIn, FeeratePerKw(2500 sat)), targetFeerate, commitment, commitTx) match { + adjustPreviousTxOutput(FundedTx(claimHtlc, claimHtlc.txInfo.amountIn, FeeratePerKw(2500 sat), Map.empty), targetFeerate, commitment, commitTx) match { case AdjustPreviousTxOutputResult.Skip(_) => assert(targetFeerate >= FeeratePerKw(10000 sat)) case AddWalletInputs(_) => fail("shouldn't add wallet inputs to claim-htlc-tx") case TxOutputAdjusted(updatedTx) => diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index a3002ba6bf..a2fbf82ec3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -409,7 +409,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w test("commit tx feerate too low, spending anchor output (local commit)") { //we create a new channel key manager that delegates all call to Alice's key manager but track extra utxos passed to the sign() methods - val walletInputs = new AtomicReference[Seq[TxOut]](Nil) + val walletInputs = new AtomicReference[Map[OutPoint, TxOut]](Map.empty) val channelKeyManagerA = TestConstants.Alice.nodeParams.channelKeyManager // @formatter:off val channelKeyManager = new ChannelKeyManager { @@ -421,15 +421,15 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PrivateKey = channelKeyManagerA.commitmentSecret(channelKeyPath, index) override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PublicKey = channelKeyManagerA.commitmentPoint(channelKeyPath, index) override def newFundingKeyPath(isChannelOpener: Boolean): DeterministicWallet.KeyPath = channelKeyManagerA.newFundingKeyPath(isChannelOpener) - override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { walletInputs.set(extraUtxos) channelKeyManagerA.sign(tx, publicKey, txOwner, commitmentFormat, extraUtxos) } - override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { walletInputs.set(extraUtxos) channelKeyManagerA.sign(tx, publicKey, remotePoint, txOwner, commitmentFormat, extraUtxos) } - override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remoteSecret: Crypto.PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Seq[TxOut]): ByteVector64 = { + override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remoteSecret: Crypto.PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { walletInputs.set(extraUtxos) channelKeyManagerA.sign(tx, publicKey, remoteSecret, txOwner, commitmentFormat, extraUtxos) } @@ -462,7 +462,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // there are 2 transactions in the mempool, the one that is not the commit tx has to be the anchor tx val publishedAnchorTx = getTransaction(mempoolTxs.filterNot(_.txid == commitTx.tx.txid).head.txid) - val extraInputs = publishedAnchorTx.txIn.tail.map(input => getTransaction(input.outPoint.txid).txOut(input.outPoint.index.toInt)) + val extraInputs = publishedAnchorTx.txIn.tail.map(input => input.outPoint -> getTransaction(input.outPoint.txid).txOut(input.outPoint.index.toInt)).toMap // check that inputs added to the anchor tx match what was passed to our key manager's sign() method assert(walletInputs.get() == extraInputs) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala index 63e5efdcae..71c80329d2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/ChannelIntegrationSpec.scala @@ -441,17 +441,17 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { val localPerCommitmentPointF = keyManagerF.commitmentPoint(channelKeyPathF, commitmentsF.localCommitIndex) val revokedCommitTx = { val commitTx = localCommitF.commitTxAndRemoteSig.commitTx - val localSig = keyManagerF.sign(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex), TxOwner.Local, commitmentFormat, Nil) + val localSig = keyManagerF.sign(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex), TxOwner.Local, commitmentFormat, Map.empty) val RemoteSignature.FullSignature(remoteSig) = localCommitF.commitTxAndRemoteSig.remoteSig Transactions.addSigs(commitTx, keyManagerF.fundingPublicKey(commitmentsF.params.localParams.fundingKeyPath, commitmentsF.latest.fundingTxIndex).publicKey, commitmentsF.latest.remoteFundingPubKey, localSig, remoteSig).tx } val htlcSuccess = htlcSuccessTxs.zip(Seq(preimage1, preimage2)).map { case (htlcTxAndSigs, preimage) => - val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat, Nil) + val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat, Map.empty) Transactions.addSigs(htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcSuccessTx], localSig, htlcTxAndSigs.remoteSig, preimage, commitmentsF.params.commitmentFormat).tx } val htlcTimeout = htlcTimeoutTxs.map { htlcTxAndSigs => - val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat, Nil) + val localSig = keyManagerF.sign(htlcTxAndSigs.htlcTx, keyManagerF.htlcPoint(channelKeyPathF), localPerCommitmentPointF, TxOwner.Local, commitmentFormat, Map.empty) Transactions.addSigs(htlcTxAndSigs.htlcTx.asInstanceOf[Transactions.HtlcTimeoutTx], localSig, htlcTxAndSigs.remoteSig, commitmentsF.params.commitmentFormat).tx } htlcSuccess.foreach(tx => Transaction.correctlySpends(tx, Seq(revokedCommitTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index e5334cd986..81a7111cf4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -209,9 +209,9 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { remotePaymentBasePoint = Remote.payment_basepoint, localIsChannelOpener = true, outputs = outputs) - val local_sig = tx.sign(Local.funding_privkey, TxOwner.Local, commitmentFormat, Nil) + val local_sig = tx.sign(Local.funding_privkey, TxOwner.Local, commitmentFormat, Map.empty) logger.info(s"# local_signature = ${Scripts.der(local_sig).dropRight(1).toHex}") - val remote_sig = tx.sign(Remote.funding_privkey, TxOwner.Remote, commitmentFormat, Nil) + val remote_sig = tx.sign(Remote.funding_privkey, TxOwner.Remote, commitmentFormat, Map.empty) logger.info(s"remote_signature: ${Scripts.der(remote_sig).dropRight(1).toHex}") Transactions.addSigs(tx, Local.funding_pubkey, Remote.funding_pubkey, local_sig, remote_sig) } @@ -248,8 +248,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { val signedTxs = htlcTxs.collect { case tx: HtlcSuccessTx => - val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat, Nil) - val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat, Nil) + val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat, Map.empty) + val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat, Map.empty) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.asInstanceOf[InputInfo.SegwitInput].redeemScript)) val preimage = paymentPreimages.find(p => Crypto.sha256(p) == tx.paymentHash).get val tx1 = Transactions.addSigs(tx, localSig, remoteSig, preimage, commitmentFormat) @@ -260,8 +260,8 @@ trait TestVectorsSpec extends AnyFunSuite with Logging { logger.info(s"htlc_success_tx (htlc #$htlcIndex): ${tx1.tx}") tx1 case tx: HtlcTimeoutTx => - val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat, Nil) - val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat, Nil) + val localSig = tx.sign(Local.htlc_privkey, TxOwner.Local, commitmentFormat, Map.empty) + val remoteSig = tx.sign(Remote.htlc_privkey, TxOwner.Remote, commitmentFormat, Map.empty) val htlcIndex = htlcScripts.indexOf(Script.parse(tx.input.asInstanceOf[InputInfo.SegwitInput].redeemScript)) val tx1 = Transactions.addSigs(tx, localSig, remoteSig, commitmentFormat) Transaction.correctlySpends(tx1.tx, Seq(commitTx.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 45323435b9..6a7bffb52e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -292,8 +292,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434445L val commitTx = { val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat, Nil) + val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) + val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat, Map.empty) Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) } @@ -319,8 +319,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // either party spends local->remote htlc output with htlc timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) - val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat, Nil) + val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) + val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat, Map.empty) val signed = addSigs(htlcTimeoutTx, localSig, remoteSig, DefaultCommitmentFormat) assert(checkSpendable(signed).isSuccess) } @@ -328,7 +328,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc1 timeout tx val Right(htlcDelayed) = makeHtlcDelayedTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit @@ -339,7 +339,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends local->remote htlc1/htlc3 output directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, DefaultCommitmentFormat) - val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) } @@ -347,8 +347,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends remote->local htlc2/htlc4 output with htlc success tx using payment preimage for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(0), paymentPreimage4) :: Nil) { - val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) - val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat, Nil) + val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) + val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, DefaultCommitmentFormat, Map.empty) val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, DefaultCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // check remote sig @@ -358,7 +358,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc2 success tx val Right(htlcDelayed) = makeHtlcDelayedTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc4 success tx because it is below the dust limit @@ -368,35 +368,35 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends main output val Right(claimP2WPKHOutputTx) = makeClaimP2WPKHOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimP2WPKHOutputTx.sign(remotePaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val localSig = claimP2WPKHOutputTx.sign(remotePaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signedTx = addSigs(claimP2WPKHOutputTx, remotePaymentPriv.publicKey, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends remote->local htlc output directly in case of timeout val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, outputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc2, feeratePerKw, DefaultCommitmentFormat) - val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) } { // remote spends local main delayed output with revocation key val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(mainPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } { // remote spends htlc1's htlc-timeout tx with revocation key val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit @@ -411,14 +411,14 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } { // remote spends htlc2's htlc-success tx with revocation key val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc4's htlc-success tx because it is below the dust limit @@ -433,7 +433,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -529,8 +529,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val commitTxNumber = 0x404142434445L val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, UnsafeLegacyAnchorOutputsCommitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, spec.htlcTxFeerate(UnsafeLegacyAnchorOutputsCommitmentFormat), outputs, UnsafeLegacyAnchorOutputsCommitmentFormat) @@ -565,7 +565,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends main delayed output val Right(claimMainOutputTx) = makeClaimLocalDelayedOutputTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = claimMainOutputTx.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signedTx = addSigs(claimMainOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -577,7 +577,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends main delayed output val Right(claimRemoteDelayedOutputTx) = makeClaimRemoteDelayedOutputTx(commitTx.tx, localDustLimit, remotePaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = claimRemoteDelayedOutputTx.sign(remotePaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = claimRemoteDelayedOutputTx.sign(remotePaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signedTx = addSigs(claimRemoteDelayedOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -585,7 +585,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // local spends local anchor val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, localFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -593,28 +593,28 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends remote anchor val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, remoteFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = claimAnchorOutputTx.sign(remoteFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = claimAnchorOutputTx.sign(remoteFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signedTx = addSigs(claimAnchorOutputTx, localSig) assert(checkSpendable(signedTx).isSuccess) } { // remote spends local main delayed output with revocation key val Right(mainPenaltyTx) = makeMainPenaltyTx(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, finalPubKeyScript, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw) - val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val sig = mainPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signed = addSigs(mainPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } { // local spends received htlc with HTLC-timeout tx for (htlcTimeoutTx <- htlcTimeoutTxs) { - val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) - val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = htlcTimeoutTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val remoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signedTx = addSigs(htlcTimeoutTx, localSig, remoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { - val invalidRemoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, sighash, Nil) + val invalidRemoteSig = htlcTimeoutTx.sign(remoteHtlcPriv, sighash, Map.empty) val invalidTx = addSigs(htlcTimeoutTx, localSig, invalidRemoteSig, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(invalidTx).isFailure) } @@ -623,7 +623,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends delayed output of htlc1 timeout tx val Right(htlcDelayed) = makeHtlcDelayedTx(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) // local can't claim delayed output of htlc3 timeout tx because it is below the dust limit @@ -633,8 +633,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // local spends offered htlc with HTLC-success tx for ((htlcSuccessTx, paymentPreimage) <- (htlcSuccessTxs(0), paymentPreimage4) :: (htlcSuccessTxs(1), paymentPreimage2) :: (htlcSuccessTxs(2), paymentPreimage2) :: Nil) { - val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) - val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = htlcSuccessTx.sign(localHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + val remoteSig = htlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signedTx = addSigs(htlcSuccessTx, localSig, remoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(signedTx).isSuccess) // check remote sig @@ -642,7 +642,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // local detects when remote doesn't use the right sighash flags val invalidSighash = Seq(SIGHASH_ALL, SIGHASH_ALL | SIGHASH_ANYONECANPAY, SIGHASH_SINGLE, SIGHASH_NONE) for (sighash <- invalidSighash) { - val invalidRemoteSig = htlcSuccessTx.sign(remoteHtlcPriv, sighash, Nil) + val invalidRemoteSig = htlcSuccessTx.sign(remoteHtlcPriv, sighash, Map.empty) val invalidTx = addSigs(htlcSuccessTx, localSig, invalidRemoteSig, paymentPreimage, UnsafeLegacyAnchorOutputsCommitmentFormat) assert(checkSpendable(invalidTx).isFailure) assert(!invalidTx.checkSig(invalidRemoteSig, remoteHtlcPriv.publicKey, TxOwner.Remote, UnsafeLegacyAnchorOutputsCommitmentFormat)) @@ -654,7 +654,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Right(htlcDelayedA) = makeHtlcDelayedTx(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Right(htlcDelayedB) = makeHtlcDelayedTx(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (htlcDelayed <- Seq(htlcDelayedA, htlcDelayedB)) { - val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = htlcDelayed.sign(localDelayedPaymentPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signedTx = addSigs(htlcDelayed, localSig) assert(checkSpendable(signedTx).isSuccess) } @@ -666,7 +666,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends local->remote htlc outputs directly in case of success for ((htlc, paymentPreimage) <- (htlc1, paymentPreimage1) :: (htlc3, paymentPreimage3) :: Nil) { val Right(claimHtlcSuccessTx) = makeClaimHtlcSuccessTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = claimHtlcSuccessTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signed = addSigs(claimHtlcSuccessTx, localSig, paymentPreimage) assert(checkSpendable(signed).isSuccess) } @@ -674,7 +674,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { { // remote spends htlc1's htlc-timeout tx with revocation key val Seq(Right(claimHtlcDelayedPenaltyTx)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcTimeoutTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) - val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val sig = claimHtlcDelayedPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signed = addSigs(claimHtlcDelayedPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) // remote can't claim revoked output of htlc3's htlc-timeout tx because it is below the dust limit @@ -685,7 +685,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { // remote spends remote->local htlc output directly in case of timeout for (htlc <- Seq(htlc2a, htlc2b)) { val Right(claimHtlcTimeoutTx) = makeClaimHtlcTimeoutTx(commitTx.tx, commitTxOutputs, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat) - val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val localSig = claimHtlcTimeoutTx.sign(remoteHtlcPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signed = addSigs(claimHtlcTimeoutTx, localSig) assert(checkSpendable(signed).isSuccess) } @@ -695,7 +695,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { val Seq(Right(claimHtlcDelayedPenaltyTxA)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(1).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) val Seq(Right(claimHtlcDelayedPenaltyTxB)) = makeClaimHtlcDelayedOutputPenaltyTxs(htlcSuccessTxs(2).tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, finalPubKeyScript, feeratePerKw) for (claimHtlcSuccessPenaltyTx <- Seq(claimHtlcDelayedPenaltyTxA, claimHtlcDelayedPenaltyTxB)) { - val sig = claimHtlcSuccessPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val sig = claimHtlcSuccessPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signed = addSigs(claimHtlcSuccessPenaltyTx, sig) assert(checkSpendable(signed).isSuccess) } @@ -725,7 +725,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -738,7 +738,7 @@ class TransactionsSpec extends AnyFunSuite with Logging { case _ => false }.map(_._2) val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw) - val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Nil) + val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) val signed = addSigs(htlcPenaltyTx, sig, localRevocationPriv.publicKey) assert(checkSpendable(signed).isSuccess) } @@ -835,42 +835,12 @@ class TransactionsSpec extends AnyFunSuite with Logging { txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, lockTime = 0) - val claimLocalAnchorOutputTx = ClaimLocalAnchorOutputTx(InputInfo.TaprootInput(OutPoint(commitTx, 2), commitTx.txOut(2), localDelayedPaymentPriv.xOnlyPublicKey(), Some(Scripts.Taproot.anchorScriptTree)), tx, ConfirmationTarget.Priority(Fast)) - val sig = claimLocalAnchorOutputTx.sign(localDelayedPaymentPriv, SigHash.SIGHASH_DEFAULT, Nil) + 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 walletKey = PrivateKey(ByteVector.fromValidHex("01" * 32)) - val walletInput = Transaction(version = 2, txIn = Nil, txOut = TxOut(5000.sat, Script.pay2wpkh(walletKey.publicKey)) :: Nil, lockTime = 0) - val tx = Transaction( - version = 2, - txIn = TxIn(OutPoint(commitTx, 2), Nil, sequence = TxIn.SEQUENCE_FINAL) :: TxIn(OutPoint(walletInput, 0), Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(330.sat, finalPubKeyScript) :: Nil, - lockTime = 0) - // we add a wallet input to our claim anchor tx - val claimLocalAnchorOutputTx = ClaimLocalAnchorOutputTx(InputInfo.TaprootInput(OutPoint(commitTx, 2), commitTx.txOut(2), localDelayedPaymentPriv.xOnlyPublicKey(), Some(Scripts.Taproot.anchorScriptTree)), tx, ConfirmationTarget.Priority(Fast)) - val walletSig = tx.signInput(1, Script.pay2pkh(walletKey.publicKey), SIGHASH_ALL, walletInput.txOut(0).amount, SigVersion.SIGVERSION_WITNESS_V0, walletKey) - - // if we omit the wallet input, we'll generate an invalid signature - val badSig = claimLocalAnchorOutputTx.sign(localDelayedPaymentPriv, SigHash.SIGHASH_DEFAULT, Nil) - val badTx = tx. - updateWitness(0, Script.witnessKeyPathPay2tr(badSig)) - .updateWitness(1, ScriptWitness(walletSig :: walletKey.publicKey.value :: Nil)) - assertThrows[IllegalArgumentException] { - Transaction.correctlySpends(badTx, Seq(commitTx, walletInput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - - // but if we pass in the extra wallet input, signature will be valid - val goodSig = claimLocalAnchorOutputTx.sign(localDelayedPaymentPriv, SigHash.SIGHASH_DEFAULT, walletInput.txOut(0) :: Nil) - val goodTx = tx. - updateWitness(0, Script.witnessKeyPathPay2tr(goodSig)) - .updateWitness(1, ScriptWitness(walletSig :: walletKey.publicKey.value :: Nil)) - Transaction.correctlySpends(goodTx, Seq(commitTx, walletInput), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - } - val spendLocalAnchorAfterDelayTx = { val tx = Transaction( version = 2, @@ -1073,8 +1043,8 @@ class TransactionsSpec extends AnyFunSuite with Logging { val (commitTx, outputs, htlcTxs) = { val outputs = makeCommitTxOutputs(localPaysCommitTxFees = true, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, remotePaymentPriv.publicKey, localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localFundingPriv.publicKey, remoteFundingPriv.publicKey, spec, DefaultCommitmentFormat) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey, remotePaymentPriv.publicKey, localIsChannelOpener = true, outputs) - val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Nil) - val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat, Nil) + val localSig = txInfo.sign(localPaymentPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty) + val remoteSig = txInfo.sign(remotePaymentPriv, TxOwner.Remote, DefaultCommitmentFormat, Map.empty) val commitTx = Transactions.addSigs(txInfo, localFundingPriv.publicKey, remoteFundingPriv.publicKey, localSig, remoteSig) val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey, toLocalDelay, localDelayedPaymentPriv.publicKey, feeratePerKw, outputs, DefaultCommitmentFormat) (commitTx, outputs, htlcTxs) From 5dcf2b2debdd5f5bec8bb572750ce4728aede30c Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 1 Apr 2025 10:43:25 +0200 Subject: [PATCH 5/6] Fix bitcoin core client test --- .../acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala index 2433abbca3..44c71fdfff 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/BitcoinCoreClientSpec.scala @@ -1141,6 +1141,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A sender.expectMsg(childTx1.txid) bitcoinClient.getMempool().map(txs => txs.map(_.txid).toSet).pipeTo(sender.ref) sender.expectMsg(Set(parentTx1.txid, childTx1.txid)) + generateBlocks(1) } test("send and list transactions") { From c9341fc15f9f4179e03131d690fa2a015b2371ce Mon Sep 17 00:00:00 2001 From: Bastien Teinturier <31281497+t-bast@users.noreply.github.com> Date: Tue, 1 Apr 2025 16:16:12 +0200 Subject: [PATCH 6/6] Stricter requirements on input details for signing (#3052) We verify that details about all inputs are provided to the `sign` function. While this isn't mandatory for segwit v0, it ensures that all of our existing tests exercise this codepath and reduces the risk that we forget to provide some wallet inputs, which would result in an invalid signature which would be hard to investigate. With this change, some of the unit tests started failing, which showed that we weren't correctly setting wallet inputs in the fee-bumping case in `ReplaceableTxFunder`, which we've fixed. We also add a test in `TransactionsSpec.scala` to verify that signing fails when details about some inputs are missing. --- .../bitcoind/rpc/BitcoinCoreClient.scala | 2 +- .../channel/fund/InteractiveTxBuilder.scala | 13 +++-- .../channel/publish/ReplaceableTxFunder.scala | 41 ++++++-------- .../crypto/keymanager/ChannelKeyManager.scala | 6 +- .../keymanager/LocalChannelKeyManager.scala | 6 +- .../eclair/transactions/Transactions.scala | 29 +++++++--- .../publish/ReplaceableTxPublisherSpec.scala | 56 ++++--------------- .../transactions/TransactionsSpec.scala | 35 +++++++++--- 8 files changed, 88 insertions(+), 100 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala index 91b6bcff23..5dfecb60ce 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/rpc/BitcoinCoreClient.scala @@ -305,7 +305,7 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool }) } - def utxoUpdatePsbt(psbt: Psbt)(implicit ec: ExecutionContext): Future[Psbt] = { + private def utxoUpdatePsbt(psbt: Psbt)(implicit ec: ExecutionContext): Future[Psbt] = { val encoded = Base64.getEncoder.encodeToString(Psbt.write(psbt).toByteArray) rpcClient.invoke("utxoupdatepsbt", encoded).map(json => { val JString(base64) = json diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala index 225be50bda..94dec79916 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala @@ -105,18 +105,15 @@ object InteractiveTxBuilder { // @formatter:off def info: InputInfo def weight: Int - // we don't need to provide extra inputs here, as this method will only be called for multisig-2-of-2 inputs which only require the output that is spent for signing - // for taproot channels, we'll use a musig2 input that is not signed like this: instead, the signature will be the Musig2 aggregation if a local and remote partial signature - def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction): ByteVector64 // @formatter:on } case class Multisig2of2Input(info: InputInfo, fundingTxIndex: Long, remoteFundingPubkey: PublicKey) extends SharedFundingInput { override val weight: Int = 388 - override def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction): ByteVector64 = { + def sign(keyManager: ChannelKeyManager, params: ChannelParams, tx: Transaction, spentUtxos: Map[OutPoint, TxOut]): ByteVector64 = { val localFundingPubkey = keyManager.fundingPublicKey(params.localParams.fundingKeyPath, fundingTxIndex) - keyManager.sign(Transactions.SpliceTx(info, tx), localFundingPubkey, TxOwner.Local, params.commitmentFormat, Map.empty) + keyManager.sign(Transactions.SpliceTx(info, tx), localFundingPubkey, TxOwner.Local, params.commitmentFormat, spentUtxos) } } @@ -339,6 +336,8 @@ object InteractiveTxBuilder { val remoteFees: MilliSatoshi = remoteAmountIn - remoteAmountOut // Note that the truncation is a no-op: sub-satoshi balances are carried over from inputs to outputs and cancel out. val fees: Satoshi = (localFees + remoteFees).truncateToSatoshi + // When signing transactions that include taproot inputs, we must provide details about all of the transaction's inputs. + val inputDetails: Map[OutPoint, TxOut] = (sharedInput_opt.toSeq.map(i => i.outPoint -> i.txOut) ++ localInputs.map(i => i.outPoint -> i.txOut) ++ remoteInputs.map(i => i.outPoint -> i.txOut)).toMap def localOnlyNonChangeOutputs: List[Output.Local.NonChange] = localOutputs.collect { case o: Local.NonChange => o } @@ -914,7 +913,9 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon import fr.acinq.bitcoin.scalacompat.KotlinUtils._ val tx = unsignedTx.buildUnsignedTx() - val sharedSig_opt = fundingParams.sharedInput_opt.map(_.sign(keyManager, channelParams, tx)) + val sharedSig_opt = fundingParams.sharedInput_opt.collect { + case i: Multisig2of2Input => i.sign(keyManager, channelParams, tx, unsignedTx.inputDetails) + } if (unsignedTx.localInputs.isEmpty) { context.self ! SignTransactionResult(PartiallySignedSharedTransaction(unsignedTx, TxSignatures(fundingParams.channelId, tx, Nil, sharedSig_opt))) } else { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala index 33fcc2b478..10301aaa8a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/publish/ReplaceableTxFunder.scala @@ -274,7 +274,7 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, Behaviors.stopped case AdjustPreviousTxOutputResult.TxOutputAdjusted(updatedTx) => log.debug("bumping {} fees without adding new inputs: txid={}", cmd.desc, updatedTx.txInfo.tx.txid) - sign(updatedTx, targetFeerate, previousTx.totalAmountIn, Map.empty) + sign(updatedTx, targetFeerate, previousTx.totalAmountIn, previousTx.walletInputs) case AdjustPreviousTxOutputResult.AddWalletInputs(tx) => log.debug("bumping {} fees requires adding new inputs (feerate={})", cmd.desc, targetFeerate) // We restore the original transaction (remove previous attempt's wallet inputs). @@ -426,32 +426,23 @@ private class ReplaceableTxFunder(nodeParams: NodeParams, } } - private def addInputs(tx: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ReplaceableTxWithWalletInputs, Satoshi, Map[OutPoint, TxOut])] = { - - def getWalletUtxos(txInfo: TransactionWithInputInfo): Future[Map[OutPoint, TxOut]] = { - val inputs = txInfo.tx.txIn.filterNot(_.outPoint == txInfo.input.outPoint) - val txids = inputs.map(_.outPoint.txid).toSet - for { - txs <- Future.sequence(txids.toSeq.map(bitcoinClient.getTransaction)) - txMap = txs.map(tx => tx.txid -> tx).toMap - } yield { - inputs.map(_.outPoint).map(outPoint => { - require(txMap.contains(outPoint.txid) && txMap(outPoint.txid).txOut.size >= outPoint.index, s"missing wallet input $outPoint") - outPoint -> txMap(outPoint.txid).txOut(outPoint.index.toInt) - }).toMap + private def getWalletUtxos(txInfo: TransactionWithInputInfo): Future[Map[OutPoint, TxOut]] = { + Future.sequence(txInfo.tx.txIn.filter(_.outPoint != txInfo.input.outPoint).map(txIn => { + bitcoinClient.getTransaction(txIn.outPoint.txid).flatMap { + case inputTx if inputTx.txOut.size <= txIn.outPoint.index => Future.failed(new IllegalArgumentException(s"input ${inputTx.txid}:${txIn.outPoint.index} doesn't exist")) + case inputTx => Future.successful(txIn.outPoint -> inputTx.txOut(txIn.outPoint.index.toInt)) } - } + })).map(_.toMap) + } - tx match { - case anchorTx: ClaimLocalAnchorWithWitnessData => for { - (fundedTx, amountIn) <- addInputs(anchorTx, targetFeerate, commitment) - spentUtxos <- getWalletUtxos(fundedTx.txInfo) - } yield (fundedTx, amountIn, spentUtxos) - case htlcTx: HtlcWithWitnessData => for { - (fundedTx, amountIn) <- addInputs(htlcTx, targetFeerate, commitment) - spentUtxos <- getWalletUtxos(fundedTx.txInfo) - } yield (fundedTx, amountIn, spentUtxos) - } + private def addInputs(tx: ReplaceableTxWithWalletInputs, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ReplaceableTxWithWalletInputs, Satoshi, Map[OutPoint, TxOut])] = { + for { + (fundedTx, amountIn) <- tx match { + case anchorTx: ClaimLocalAnchorWithWitnessData => addInputs(anchorTx, targetFeerate, commitment) + case htlcTx: HtlcWithWitnessData => addInputs(htlcTx, targetFeerate, commitment) + } + spentUtxos <- getWalletUtxos(fundedTx.txInfo) + } yield (fundedTx, amountIn, spentUtxos) } private def addInputs(anchorTx: ClaimLocalAnchorWithWitnessData, targetFeerate: FeeratePerKw, commitment: FullCommitment): Future[(ClaimLocalAnchorWithWitnessData, Satoshi)] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala index 50f71781d7..78a2b981bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/ChannelKeyManager.scala @@ -64,7 +64,7 @@ trait ChannelKeyManager { * @param publicKey extended public key * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) + * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) * @return a signature generated with the private key that matches the input extended public key */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 @@ -78,7 +78,7 @@ trait ChannelKeyManager { * @param remotePoint remote point * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) + * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 @@ -91,7 +91,7 @@ trait ChannelKeyManager { * @param remoteSecret remote secret * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) + * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. */ def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala index 96e0acded0..01be5a95c4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/keymanager/LocalChannelKeyManager.scala @@ -104,7 +104,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param publicKey extended public key * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) + * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) * @return a signature generated with the private key that matches the input extended public key */ override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { @@ -125,7 +125,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param remotePoint remote point * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) + * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) * @return a signature generated with a private key generated from the input key's matching private key and the remote point. */ override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { @@ -147,7 +147,7 @@ class LocalChannelKeyManager(seed: ByteVector, chainHash: BlockHash) extends Cha * @param remoteSecret remote secret * @param txOwner owner of the transaction (local/remote) * @param commitmentFormat format of the commitment tx - * @param extraUtxos extra outputs spent by this transaction (in addition to our [[fr.acinq.eclair.transactions.Transactions.InputInfo]] output, which is assumed to always be the first spent output) + * @param extraUtxos extra outputs spent by this transaction (in addition to [[fr.acinq.eclair.transactions.Transactions.InputInfo]]) * @return a signature generated with a private key generated from the input key's matching private key and the remote secret. */ override def sign(tx: TransactionWithInputInfo, publicKey: ExtendedPublicKey, remoteSecret: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala index cc8dd3b50c..9cb1ea9194 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala @@ -131,18 +131,31 @@ object Transactions { /** Sighash flags to use when signing the transaction. */ def sighash(txOwner: TxOwner, commitmentFormat: CommitmentFormat): Int = SIGHASH_ALL + /** + * @param extraUtxos extra outputs spent by this transaction (in addition to the main [[input]]). + */ def sign(key: PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { sign(key, sighash(txOwner, commitmentFormat), extraUtxos) } - def sign(key: PrivateKey, sighashType: Int, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = input match { - case _: InputInfo.TaprootInput => ByteVector64.Zeroes - case InputInfo.SegwitInput(outPoint, txOut, redeemScript) => - // NB: the tx may have multiple inputs, we will only sign the one provided in txinfo.input. Bear in mind that the - // signature will be invalidated if other inputs are added *afterwards* and sighashType was SIGHASH_ALL. - val inputIndex = tx.txIn.indexWhere(_.outPoint == outPoint) - val sigDER = Transaction.signInput(tx, inputIndex, redeemScript, sighashType, txOut.amount, SIGVERSION_WITNESS_V0, key) - Crypto.der2compact(sigDER) + def sign(key: PrivateKey, sighashType: Int, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { + val inputsMap = extraUtxos + (input.outPoint -> input.txOut) + tx.txIn.foreach(txIn => { + // Note that using a require here is dangerous, because callers don't except this function to throw. + // But we want to ensure that we're correctly providing input details, otherwise our signature will silently be + // invalid when using taproot. We verify this in all cases, even when using segwit v0, to ensure that we have as + // many tests as possible that exercise this codepath. + require(inputsMap.contains(txIn.outPoint), s"cannot sign $desc with txId=${tx.txid}: missing input details for ${txIn.outPoint}") + }) + input match { + case InputInfo.SegwitInput(outPoint, txOut, redeemScript) => + // NB: the tx may have multiple inputs, we will only sign the one provided in txinfo.input. Bear in mind that the + // signature will be invalidated if other inputs are added *afterwards* and sighashType was SIGHASH_ALL. + val inputIndex = tx.txIn.indexWhere(_.outPoint == outPoint) + val sigDER = Transaction.signInput(tx, inputIndex, redeemScript, sighashType, txOut.amount, SIGVERSION_WITNESS_V0, key) + Crypto.der2compact(sigDER) + case _: InputInfo.TaprootInput => ??? + } } def checkSig(sig: ByteVector64, pubKey: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat): Boolean = input match { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala index a2fbf82ec3..64fe89219a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/publish/ReplaceableTxPublisherSpec.scala @@ -22,7 +22,7 @@ import akka.pattern.pipe import akka.testkit.{TestFSMRef, TestProbe} import com.softwaremill.quicklens.ModifyPimp import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, ByteVector64, Crypto, DeterministicWallet, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, ScriptElt, Transaction, TxId, TxOut} +import fr.acinq.bitcoin.scalacompat.{Block, BtcAmount, MilliBtcDouble, MnemonicCode, OutPoint, SatoshiLong, ScriptElt, Transaction, TxId} import fr.acinq.eclair.NotificationsLogger.NotifyNodeOperator import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -36,19 +36,18 @@ import fr.acinq.eclair.channel.publish.ReplaceableTxPublisher.{Publish, Stop, Up import fr.acinq.eclair.channel.publish.TxPublisher.TxRejectedReason._ import fr.acinq.eclair.channel.publish.TxPublisher._ import fr.acinq.eclair.channel.states.{ChannelStateTestsBase, ChannelStateTestsTags} -import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, LocalOnChainKeyManager} +import fr.acinq.eclair.crypto.keymanager.LocalOnChainKeyManager import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.wire.protocol.{CommitSig, RevokeAndAck, UpdateFee} import fr.acinq.eclair.{BlockHeight, MilliSatoshi, MilliSatoshiLong, NodeParams, NotificationsLogger, TestConstants, TestKitBaseClass, TimestampSecond, randomKey} -import org.json4s.JString import org.scalatest.BeforeAndAfterAll import org.scalatest.Inside.inside import org.scalatest.funsuite.AnyFunSuiteLike import scodec.bits.ByteVector import java.util.UUID -import java.util.concurrent.atomic.{AtomicLong, AtomicReference} +import java.util.concurrent.atomic.AtomicLong import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.duration.DurationInt @@ -144,7 +143,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } // NB: we can't use ScalaTest's fixtures, they would see uninitialized bitcoind fields because they sandbox each test. - private def withFixture(utxos: Seq[BtcAmount], channelType: SupportedChannelType, channelKeyManager_opt: Option[ChannelKeyManager] = None)(testFun: Fixture => Any): Unit = { + private def withFixture(utxos: Seq[BtcAmount], channelType: SupportedChannelType)(testFun: Fixture => Any): Unit = { // Create a unique wallet for this test and ensure it has some btc. val testId = UUID.randomUUID() val (walletRpcClient, walletClient) = createTestWallet(s"lightning-$testId") @@ -161,7 +160,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // Setup a valid channel between alice and bob. val blockHeight = new AtomicLong() blockHeight.set(currentBlockHeight(probe).toLong) - val aliceNodeParams = channelKeyManager_opt.map(ckm => TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight, channelKeyManager = ckm)).getOrElse(TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight)) + val aliceNodeParams = TestConstants.Alice.nodeParams.copy(blockHeight = blockHeight) val setup = init(aliceNodeParams, TestConstants.Bob.nodeParams.copy(blockHeight = blockHeight), wallet_opt = Some(walletClient)) val testTags = channelType match { case _: ChannelTypes.AnchorOutputsZeroFeeHtlcTx => Set(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs) @@ -408,36 +407,7 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w } test("commit tx feerate too low, spending anchor output (local commit)") { - //we create a new channel key manager that delegates all call to Alice's key manager but track extra utxos passed to the sign() methods - val walletInputs = new AtomicReference[Map[OutPoint, TxOut]](Map.empty) - val channelKeyManagerA = TestConstants.Alice.nodeParams.channelKeyManager - // @formatter:off - val channelKeyManager = new ChannelKeyManager { - override def fundingPublicKey(fundingKeyPath: DeterministicWallet.KeyPath, fundingTxIndex: Long): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.fundingPublicKey(fundingKeyPath, fundingTxIndex) - override def revocationPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.revocationPoint(channelKeyPath) - override def paymentPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.paymentPoint(channelKeyPath) - override def delayedPaymentPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.delayedPaymentPoint(channelKeyPath) - override def htlcPoint(channelKeyPath: DeterministicWallet.KeyPath): DeterministicWallet.ExtendedPublicKey = channelKeyManagerA.htlcPoint(channelKeyPath) - override def commitmentSecret(channelKeyPath: DeterministicWallet.KeyPath, index: Long): Crypto.PrivateKey = channelKeyManagerA.commitmentSecret(channelKeyPath, index) - override def commitmentPoint(channelKeyPath: DeterministicWallet.KeyPath, index: Long): PublicKey = channelKeyManagerA.commitmentPoint(channelKeyPath, index) - override def newFundingKeyPath(isChannelOpener: Boolean): DeterministicWallet.KeyPath = channelKeyManagerA.newFundingKeyPath(isChannelOpener) - override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - walletInputs.set(extraUtxos) - channelKeyManagerA.sign(tx, publicKey, txOwner, commitmentFormat, extraUtxos) - } - override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remotePoint: PublicKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - walletInputs.set(extraUtxos) - channelKeyManagerA.sign(tx, publicKey, remotePoint, txOwner, commitmentFormat, extraUtxos) - } - override def sign(tx: TransactionWithInputInfo, publicKey: DeterministicWallet.ExtendedPublicKey, remoteSecret: Crypto.PrivateKey, txOwner: TxOwner, commitmentFormat: CommitmentFormat, extraUtxos: Map[OutPoint, TxOut]): ByteVector64 = { - walletInputs.set(extraUtxos) - channelKeyManagerA.sign(tx, publicKey, remoteSecret, txOwner, commitmentFormat, extraUtxos) - } - override def signChannelAnnouncement(witness: ByteVector, fundingKeyPath: DeterministicWallet.KeyPath): ByteVector64 = channelKeyManagerA.signChannelAnnouncement(witness, fundingKeyPath) - } - // @formatter:on - - withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx(), Some(channelKeyManager)) { f => + withFixture(Seq(500 millibtc), ChannelTypes.AnchorOutputsZeroFeeHtlcTx()) { f => import f._ val (commitTx, anchorTx) = closeChannelWithoutHtlcs(f, aliceBlockHeight() + 30) @@ -452,19 +422,13 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w // wait for the commit tx and anchor tx to be published val mempoolTxs = getMempoolTxs(2) - // check that the wallet input added by the tx funder was passed to the key manager assert(mempoolTxs.map(_.txid).contains(commitTx.tx.txid)) - def getTransaction(txid: TxId): Transaction = { - bitcoinrpcclient.invoke("getrawtransaction", txid).pipeTo(probe.ref) - Transaction.read(probe.expectMsgType[JString].values) - } - + // we check that the anchor tx contains additional wallet inputs // there are 2 transactions in the mempool, the one that is not the commit tx has to be the anchor tx - val publishedAnchorTx = getTransaction(mempoolTxs.filterNot(_.txid == commitTx.tx.txid).head.txid) - val extraInputs = publishedAnchorTx.txIn.tail.map(input => input.outPoint -> getTransaction(input.outPoint.txid).txOut(input.outPoint.index.toInt)).toMap - // check that inputs added to the anchor tx match what was passed to our key manager's sign() method - assert(walletInputs.get() == extraInputs) + wallet.getTransaction(mempoolTxs.filterNot(_.txid == commitTx.tx.txid).head.txid).pipeTo(probe.ref) + val publishedAnchorTx = probe.expectMsgType[Transaction] + assert(publishedAnchorTx.txIn.size > 1) val targetFee = Transactions.weight2fee(targetFeerate, mempoolTxs.map(_.weight).sum.toInt) val actualFee = mempoolTxs.map(_.fees).sum diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 6a7bffb52e..b6ce30a205 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -23,7 +23,6 @@ import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, Crypto, MilliBtc, MilliB import fr.acinq.bitcoin.{ScriptFlags, ScriptTree, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId import fr.acinq.eclair._ -import fr.acinq.eclair.blockchain.fee.ConfirmationPriority.Fast import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc} @@ -37,7 +36,7 @@ import scodec.bits._ import java.nio.ByteOrder import scala.io.Source -import scala.util.Random +import scala.util.{Random, Try} /** * Created by PM on 16/12/2016. @@ -489,7 +488,9 @@ class TransactionsSpec extends AnyFunSuite with Logging { } test("generate valid commitment and htlc transactions (anchor outputs)") { - val finalPubKeyScript = Script.write(Script.pay2wpkh(PrivateKey(randomBytes32()).publicKey)) + 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) // htlc1, htlc2a and htlc2b are regular IN/OUT htlcs @@ -582,12 +583,30 @@ class TransactionsSpec extends AnyFunSuite with Logging { assert(checkSpendable(signedTx).isSuccess) } { - // local spends local anchor - val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, localFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))) - assert(checkSpendable(claimAnchorOutputTx).isFailure) - val localSig = claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty) + // local spends local anchor with additional wallet inputs + val walletAmount = 50_000 sat + val walletInputs = Map( + OutPoint(randomTxId(), 3) -> TxOut(walletAmount, Script.pay2wpkh(walletPub)), + OutPoint(randomTxId(), 0) -> TxOut(walletAmount, Script.pay2wpkh(walletPub)), + ) + val Right(claimAnchorOutputTx) = makeClaimLocalAnchorOutputTx(commitTx.tx, localFundingPriv.publicKey, ConfirmationTarget.Absolute(BlockHeight(0))).map(anchorTx => { + val walletTxIn = walletInputs.map { case (outpoint, _) => TxIn(outpoint, ByteVector.empty, 0) } + val unsignedTx = anchorTx.tx.copy(txIn = anchorTx.tx.txIn ++ walletTxIn) + val sig1 = unsignedTx.signInput(1, Script.pay2pkh(walletPub), SIGHASH_ALL, walletAmount, SigVersion.SIGVERSION_WITNESS_V0, walletPriv) + val sig2 = unsignedTx.signInput(2, Script.pay2pkh(walletPub), SIGHASH_ALL, walletAmount, SigVersion.SIGVERSION_WITNESS_V0, walletPriv) + val walletSignedTx = unsignedTx + .updateWitness(1, Script.witnessPay2wpkh(walletPub, sig1)) + .updateWitness(2, Script.witnessPay2wpkh(walletPub, sig2)) + anchorTx.copy(tx = walletSignedTx) + }) + val allInputs = walletInputs + (claimAnchorOutputTx.input.outPoint -> claimAnchorOutputTx.input.txOut) + assert(Try(Transaction.correctlySpends(claimAnchorOutputTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)).isFailure) + // All wallet inputs must be provided when signing. + assert(Try(claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty)).isFailure) + assert(Try(claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, walletInputs.take(1))).isFailure) + val localSig = claimAnchorOutputTx.sign(localFundingPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, walletInputs) val signedTx = addSigs(claimAnchorOutputTx, localSig) - assert(checkSpendable(signedTx).isSuccess) + Transaction.correctlySpends(signedTx.tx, allInputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) } { // remote spends remote anchor