diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala index 12606ad9f7..70bfc4ca8d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/OnChainWallet.scala @@ -54,10 +54,13 @@ trait OnChainChannelFunder { */ def publishTransaction(tx: Transaction)(implicit ec: ExecutionContext): Future[TxId] - /** Create a fully signed channel funding transaction with the provided pubkeyScript. */ + /** Create an unsigned channel funding transaction with the provided dummy pubkeyScript. */ def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRate: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] - /** + /** Sign a funding transaction with an updated pubkeyScript. */ + def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] + + /** * Committing *must* include publishing the transaction on the network. * * We need to be very careful here, we don't want to consider a commit 'failed' if we are not absolutely sure that the @@ -155,6 +158,8 @@ object OnChainWallet { final case class MakeFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi) + final case class SignFundingTxResponse(fundingTx: Transaction, fundingTxOutputIndex: Int, fee: Satoshi) + final case class FundTransactionResponse(tx: Transaction, fee: Satoshi, changePosition: Option[Int]) { val amountIn: Satoshi = fee + tx.txOut.map(_.amount).sum } 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 3bb199f9ac..b4082a4353 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 @@ -22,11 +22,10 @@ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.bitcoin.{Bech32, Block, SigHash} import fr.acinq.eclair.ShortChannelId.coordinates import fr.acinq.eclair.blockchain.OnChainWallet -import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher.{GetTxWithMetaResponse, UtxoStatus, ValidateResult} import fr.acinq.eclair.blockchain.fee.{FeeratePerKB, FeeratePerKw} import fr.acinq.eclair.crypto.keymanager.OnChainKeyManager -import fr.acinq.eclair.json.SatoshiSerializer import fr.acinq.eclair.transactions.Transactions import fr.acinq.eclair.wire.protocol.ChannelAnnouncement import fr.acinq.eclair.{BlockHeight, TimestampSecond, TxCoordinates} @@ -310,7 +309,28 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, targetFeerate: FeeratePerKw, feeBudget_opt: Option[Satoshi] = None, maxExcess_opt: Option[Satoshi] = None)(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { - def verifyAndSign(tx: Transaction, fees: Satoshi, requestedFeeRate: FeeratePerKw): Future[MakeFundingTxResponse] = { + val partialFundingTx = Transaction( + version = 2, + txIn = Seq.empty[TxIn], + txOut = TxOut(amount, pubkeyScript) :: Nil, + lockTime = 0) + + for { + // TODO: we should check that mempoolMinFee is not dangerously high + feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(targetFeerate)) + // we ask bitcoin core to add inputs to the funding tx, and use the specified change address + FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate, add_excess_to_recipient_position = None, max_excess = maxExcess_opt), feeBudget_opt = feeBudget_opt) + fundingOutputIndex = Transactions.findPubKeyScriptIndex(tx, pubkeyScript) match { + case Left(_) => return Future.failed(new RuntimeException("cannot find expected funding output: bitcoin core may be malicious")) + case Right(outputIndex) => outputIndex + } + makeFundingTxResponse = MakeFundingTxResponse(tx, fundingOutputIndex, fee) + } yield makeFundingTxResponse + } + + def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = { + + def verifyAndSign(tx: Transaction, fees: Satoshi, requestedFeeRate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = { import KotlinUtils._ val fundingOutputIndex = Transactions.findPubKeyScriptIndex(tx, pubkeyScript) match { @@ -331,22 +351,16 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val onChainKeyManag maxFeerate = requestedFeeRate * 1.5 _ = require(actualFeerate < maxFeerate, s"actual feerate $actualFeerate is more than 50% above requested feerate $targetFeerate") _ = logger.debug(s"created funding txid=${fundingTx.txid} outputIndex=$fundingOutputIndex fee=$fees") - } yield MakeFundingTxResponse(fundingTx, fundingOutputIndex, fees) + } yield SignFundingTxResponse(fundingTx, fundingOutputIndex, fees) } - val partialFundingTx = Transaction( - version = 2, - txIn = Seq.empty[TxIn], - txOut = TxOut(amount, pubkeyScript) :: Nil, - lockTime = 0) + val tx1 = tx.copy(txOut = tx.txOut.updated(outputIndex, tx.txOut(outputIndex).copy(publicKeyScript = pubkeyScript))) for { // TODO: we should check that mempoolMinFee is not dangerously high feerate <- mempoolMinFee().map(minFee => FeeratePerKw(minFee).max(targetFeerate)) - // we ask bitcoin core to add inputs to the funding tx, and use the specified change address - FundTransactionResponse(tx, fee, _) <- fundTransaction(partialFundingTx, FundTransactionOptions(feerate, add_excess_to_recipient_position = None, max_excess = maxExcess_opt), feeBudget_opt = feeBudget_opt) - lockedUtxos = tx.txIn.map(_.outPoint) - signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx, fee, feerate)) + lockedUtxos = tx1.txIn.map(_.outPoint) + signedTx <- unlockIfFails(lockedUtxos)(verifyAndSign(tx1, fee, feerate)) } yield signedTx } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala index dcfd390d66..46d6542e81 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelData.scala @@ -19,6 +19,7 @@ package fr.acinq.eclair.channel import akka.actor.{ActorRef, PossiblyHarmful, typed} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, SatoshiLong, Transaction, TxId, TxOut} +import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw} import fr.acinq.eclair.channel.LocalFundingStatus.DualFundedUnconfirmedFundingTx import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._ @@ -53,6 +54,7 @@ sealed trait ChannelState case object WAIT_FOR_INIT_INTERNAL extends ChannelState // Single-funder channel opening: case object WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL extends ChannelState +case object WAIT_FOR_FUNDING_SIGNED_INTERNAL extends ChannelState case object WAIT_FOR_OPEN_CHANNEL extends ChannelState case object WAIT_FOR_ACCEPT_CHANNEL extends ChannelState case object WAIT_FOR_FUNDING_INTERNAL extends ChannelState @@ -522,17 +524,11 @@ sealed trait ChannelDataWithCommitments extends PersistentChannelData { final case class DATA_WAIT_FOR_OPEN_CHANNEL(initFundee: INPUT_INIT_CHANNEL_NON_INITIATOR) extends TransientChannelData { val channelId: ByteVector32 = initFundee.temporaryChannelId } -final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel) extends TransientChannelData { +final case class DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenChannel, fundingTxResponse: MakeFundingTxResponse) extends TransientChannelData { val channelId: ByteVector32 = initFunder.temporaryChannelId } -final case class DATA_WAIT_FOR_FUNDING_INTERNAL(params: ChannelParams, - fundingAmount: Satoshi, - pushAmount: MilliSatoshi, - commitTxFeerate: FeeratePerKw, - remoteFundingPubKey: PublicKey, - remoteFirstPerCommitmentPoint: PublicKey, - replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData { - val channelId: ByteVector32 = params.channelId +final case class DATA_WAIT_FOR_FUNDING_INTERNAL(input: INPUT_INIT_CHANNEL_INITIATOR) extends TransientChannelData { + val channelId: ByteVector32 = input.temporaryChannelId } final case class DATA_WAIT_FOR_FUNDING_CREATED(params: ChannelParams, fundingAmount: Satoshi, @@ -567,6 +563,11 @@ final case class DATA_WAIT_FOR_OPEN_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANNEL_INITIATOR, lastSent: OpenDualFundedChannel) extends TransientChannelData { val channelId: ByteVector32 = lastSent.temporaryChannelId } +final case class DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL(lastFundingTx: Transaction, params: ChannelParams, fundingAmount: Satoshi, + pushAmount: MilliSatoshi, commitTxFeerate: FeeratePerKw, remoteFundingPubKey: PublicKey, + remoteFirstPerCommitmentPoint: PublicKey, replyTo: akka.actor.typed.ActorRef[Peer.OpenChannelResponse]) extends TransientChannelData { + val channelId: ByteVector32 = params.channelId +} final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32, channelParams: ChannelParams, secondRemotePerCommitmentPoint: PublicKey, 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 673982afa9..e2253098d1 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 @@ -20,7 +20,7 @@ import akka.actor.Status import akka.actor.typed.scaladsl.adapter.actorRefAdapter import akka.pattern.pipe import fr.acinq.bitcoin.scalacompat.{SatoshiLong, Script} -import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse +import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel.LocalFundingStatus.SingleFundedUnconfirmedFundingTx @@ -73,6 +73,14 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { when(WAIT_FOR_INIT_SINGLE_FUNDED_CHANNEL)(handleExceptions { case Event(input: INPUT_INIT_CHANNEL_INITIATOR, _) => + val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey + val dummyPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(fundingPubKey, fundingPubKey))) + wallet.makeFundingTx(dummyPubkeyScript, input.fundingAmount, input.fundingTxFeerate, input.fundingTxFeeBudget_opt, maxExcess_opt = input.maxExcess_opt).pipeTo(self) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(input) + }) + + when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { + case Event(makeFundingTxResponse@MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, _), DATA_WAIT_FOR_FUNDING_INTERNAL(input)) => val fundingPubKey = keyManager.fundingPublicKey(input.localParams.fundingKeyPath, fundingTxIndex = 0).publicKey val channelKeyPath = keyManager.keyPath(input.localParams, input.channelConfig) // In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script if this feature is not used @@ -81,7 +89,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val open = OpenChannel( chainHash = nodeParams.chainHash, temporaryChannelId = input.temporaryChannelId, - fundingSatoshis = input.fundingAmount, + fundingSatoshis = fundingTx.txOut(fundingTxOutputIndex).amount, pushMsat = input.pushAmount_opt.getOrElse(0 msat), dustLimitSatoshis = input.localParams.dustLimit, maxHtlcValueInFlightMsat = UInt64(input.localParams.maxHtlcValueInFlightMsat.toLong), @@ -101,7 +109,28 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { ChannelTlv.UpfrontShutdownScriptTlv(localShutdownScript), ChannelTlv.ChannelTypeTlv(input.channelType) )) - goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open) sending open + goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(input, open, makeFundingTxResponse) sending open + + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + log.error(t, s"wallet returned error: ") + d.input.replyTo ! OpenChannelResponse.Rejected(s"wallet error: ${t.getMessage}") + handleLocalError(ChannelFundingError(d.channelId), d, None) // we use a generic exception and don't send the internal error to the peer + + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + d.input.replyTo ! OpenChannelResponse.Cancelled + handleFastClose(c, d.channelId) + + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + d.input.replyTo ! OpenChannelResponse.RemoteError(e.toAscii) + handleRemoteError(e, d) + + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + d.input.replyTo ! OpenChannelResponse.Disconnected + goto(CLOSED) + + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + d.input.replyTo ! OpenChannelResponse.TimedOut + goto(CLOSED) }) when(WAIT_FOR_OPEN_CHANNEL)(handleExceptions { @@ -162,9 +191,10 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(init, open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(init, open, fundingTxResponse)) => Helpers.validateParamsSingleFundedFunder(nodeParams, init.channelType, init.localParams.initFeatures, init.remoteInit.features, open, accept) match { case Left(t) => + wallet.rollback(fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.Rejected(t.getMessage) handleLocalError(t, d, Some(accept)) case Right((channelFeatures, remoteShutdownScript)) => @@ -186,34 +216,40 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { log.info("remote will use fundingMinDepth={}", accept.minimumDepth) val localFundingPubkey = keyManager.fundingPublicKey(init.localParams.fundingKeyPath, fundingTxIndex = 0) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, accept.fundingPubkey))) - wallet.makeFundingTx(fundingPubkeyScript, init.fundingAmount, init.fundingTxFeerate, init.fundingTxFeeBudget_opt, maxExcess_opt = None).pipeTo(self) + wallet.signFundingTx(fundingTxResponse.fundingTx, fundingPubkeyScript, fundingTxResponse.fundingTxOutputIndex, fundingTxResponse.fee, init.fundingTxFeerate).pipeTo(self) val params = ChannelParams(init.temporaryChannelId, init.channelConfig, channelFeatures, init.localParams, remoteParams, open.channelFlags) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(params, init.fundingAmount, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, d.initFunder.replyTo) + goto(WAIT_FOR_FUNDING_SIGNED_INTERNAL) using DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL(fundingTxResponse.fundingTx, params, open.fundingSatoshis, init.pushAmount_opt.getOrElse(0 msat), init.commitTxFeerate, accept.fundingPubkey, accept.firstPerCommitmentPoint, init.replyTo) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + wallet.rollback(d.fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.Cancelled handleFastClose(c, d.lastSent.temporaryChannelId) case Event(e: Error, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + wallet.rollback(d.fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.RemoteError(e.toAscii) handleRemoteError(e, d) case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + wallet.rollback(d.fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.Disconnected goto(CLOSED) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => + wallet.rollback(d.fundingTxResponse.fundingTx) d.initFunder.replyTo ! OpenChannelResponse.TimedOut goto(CLOSED) }) - when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(params, fundingAmount, pushMsat, commitTxFeerate, remoteFundingPubKey, remoteFirstPerCommitmentPoint, replyTo)) => + when(WAIT_FOR_FUNDING_SIGNED_INTERNAL)(handleExceptions { + case Event(SignFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL(_, params, fundingAmount, pushMsat, commitTxFeerate, remoteFundingPubKey, remoteFirstPerCommitmentPoint, replyTo)) => val temporaryChannelId = params.channelId // let's create the first commitment tx that spends the yet uncommitted funding tx Funding.makeFirstCommitTxs(keyManager, params, localFundingAmount = fundingAmount, remoteFundingAmount = 0 sat, localPushAmount = pushMsat, remotePushAmount = 0 msat, commitTxFeerate, fundingTx.txid, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remoteFirstPerCommitmentPoint = remoteFirstPerCommitmentPoint) match { - case Left(ex) => handleLocalError(ex, d, None) + case Left(ex) => + wallet.rollback(fundingTx) + 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) @@ -233,24 +269,29 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(params1, remoteFundingPubKey, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), fundingCreated, replyTo) sending fundingCreated } - case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) log.error(t, s"wallet returned error: ") d.replyTo ! OpenChannelResponse.Rejected(s"wallet error: ${t.getMessage}") handleLocalError(ChannelFundingError(d.channelId), d, None) // we use a generic exception and don't send the internal error to the peer - case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) d.replyTo ! OpenChannelResponse.Cancelled handleFastClose(c, d.channelId) - case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) d.replyTo ! OpenChannelResponse.RemoteError(e.toAscii) handleRemoteError(e, d) - case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) d.replyTo ! OpenChannelResponse.Disconnected goto(CLOSED) - case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_INTERNAL) => + case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL) => + wallet.rollback(d.lastFundingTx) d.replyTo ! OpenChannelResponse.TimedOut goto(CLOSED) }) @@ -317,7 +358,6 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { val signedLocalCommitTx = Transactions.addSigs(localCommitTx, fundingPubKey.publicKey, remoteFundingPubKey, localSigOfLocalTx, remoteSig) Transactions.checkSpendable(signedLocalCommitTx) match { case Failure(cause) => - // we rollback the funding tx, it will never be published wallet.rollback(fundingTx) d.replyTo ! OpenChannelResponse.Rejected(cause.getMessage) handleLocalError(InvalidCommitmentSignature(d.channelId, fundingTx.txid, fundingTxIndex = 0, localCommitTx.tx), d, Some(msg)) @@ -349,25 +389,21 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers { } case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.Cancelled handleFastClose(c, d.channelId) case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.RemoteError(e.toAscii) handleRemoteError(e, d) case Event(INPUT_DISCONNECTED, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.Disconnected goto(CLOSED) case Event(TickChannelOpenTimeout, d: DATA_WAIT_FOR_FUNDING_SIGNED) => - // we rollback the funding tx, it will never be published wallet.rollback(d.fundingTx) d.replyTo ! OpenChannelResponse.TimedOut goto(CLOSED) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala index 81d6c71b5c..8f0af4f9a6 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/PeerReadyNotifier.scala @@ -179,6 +179,7 @@ object PeerReadyNotifier { case channel.WAIT_FOR_OPEN_CHANNEL => true case channel.WAIT_FOR_ACCEPT_CHANNEL => true case channel.WAIT_FOR_FUNDING_INTERNAL => true + case channel.WAIT_FOR_FUNDING_SIGNED_INTERNAL => true case channel.WAIT_FOR_FUNDING_CREATED => true case channel.WAIT_FOR_FUNDING_SIGNED => true case channel.WAIT_FOR_FUNDING_CONFIRMED => true diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala index 90e07ca162..5074cf9ba9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/json/JsonSerializers.scala @@ -630,6 +630,7 @@ object CustomTypeHints { classOf[DATA_WAIT_FOR_OPEN_CHANNEL], classOf[DATA_WAIT_FOR_ACCEPT_CHANNEL], classOf[DATA_WAIT_FOR_FUNDING_INTERNAL], + classOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL], classOf[DATA_WAIT_FOR_FUNDING_CREATED], classOf[DATA_WAIT_FOR_FUNDING_SIGNED], classOf[DATA_WAIT_FOR_FUNDING_CONFIRMED], diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala index 672747aea4..1b7f4100cf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/DummyOnChainWallet.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.{Crypto, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxId, TxIn, TxOut} import fr.acinq.bitcoin.{Bech32, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.SignTransactionResponse import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.randomKey @@ -65,7 +65,13 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = { val tx = DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, amount) funded += (tx.fundingTx.txid -> tx.fundingTx) - Future.successful(tx) + Future.successful(MakeFundingTxResponse(tx.fundingTx, tx.fundingTxOutputIndex, tx.fee)) + } + + override def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = { + val tx1 = DummyOnChainWallet.makeDummyFundingTx(pubkeyScript, tx.txOut(outputIndex).amount) + funded += (tx1.fundingTx.txid -> tx1.fundingTx) + Future.successful(tx1) } override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = publishTransaction(tx).map(_ => true) @@ -113,6 +119,8 @@ class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw, feeBudget_opt: Option[Satoshi], maxExcess_opt: Option[Satoshi])(implicit ec: ExecutionContext): Future[MakeFundingTxResponse] = Promise().future // will never be completed + override def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = Promise().future // will never be completed + override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) override def getTransaction(txId: TxId)(implicit ec: ExecutionContext): Future[Transaction] = Promise().future // will never be completed @@ -217,8 +225,15 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { val tx = Transaction(2, Nil, Seq(TxOut(amount, pubkeyScript)), 0) for { fundedTx <- fundTransaction(tx, feeRatePerKw, replaceable = true, feeBudget_opt = feeBudget_opt, addExcessToRecipientPosition_opt = Some(0), maxExcess_opt = maxExcess_opt) - signedTx <- signTransaction(fundedTx.tx) - } yield MakeFundingTxResponse(signedTx.tx, 0, fundedTx.fee) + } yield MakeFundingTxResponse(fundedTx.tx, 0, fundedTx.fee) + } + + override def signFundingTx(tx: Transaction, pubkeyScript: ByteVector, outputIndex: Int, fee: Satoshi, targetFeerate: FeeratePerKw)(implicit ec: ExecutionContext): Future[SignFundingTxResponse] = { + val txOut1 = tx.txOut.updated(outputIndex, tx.txOut(outputIndex).copy(publicKeyScript = pubkeyScript)) + val fundingTx1 = tx.copy(txOut = txOut1) + for { + signedTx <- signTransaction(fundingTx1) + } yield SignFundingTxResponse(signedTx.tx, outputIndex, fee) } override def commit(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(true) @@ -254,14 +269,14 @@ object DummyOnChainWallet { val dummyReceiveAddress: String = "bcrt1qwcv8naajwn8fjhu8z59q9e6ucrqr068rlcenux" val dummyReceivePubkey: PublicKey = PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12") - def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = { + def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): SignFundingTxResponse = { val fundingTx = Transaction( version = 2, txIn = TxIn(OutPoint(TxId.fromValidHex("0101010101010101010101010101010101010101010101010101010101010101"), 42), signatureScript = Nil, sequence = SEQUENCE_FINAL) :: Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0 ) - MakeFundingTxResponse(fundingTx, 0, 420 sat) + SignFundingTxResponse(fundingTx, 0, 420 sat) } } \ No newline at end of file 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 fd091a0803..7136591c5c 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 @@ -25,7 +25,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PublicKey, der2compact} import fr.acinq.bitcoin.scalacompat.{Block, Btc, BtcDouble, Crypto, DeterministicWallet, MilliBtcDouble, MnemonicCode, OP_DROP, OP_PUSHDATA, OutPoint, Satoshi, SatoshiLong, Script, ScriptWitness, Transaction, TxId, TxIn, TxOut, addressFromPublicKeyScript, addressToPublicKeyScript, computeBIP84Address, computeP2PkhAddress, computeP2WpkhAddress} import fr.acinq.bitcoin.{Bech32, SigHash, SigVersion} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, OnChainBalance, ProcessPsbtResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.WatcherSpec.{createSpendManyP2WPKH, createSpendP2WPKH} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.{BitcoinReq, SignTransactionResponse} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient._ @@ -60,6 +60,12 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A stopBitcoind() } + def makeFundingTx(bitcoinCoreClient: BitcoinCoreClient, sender: TestProbe, pubkeyScript: ByteVector, amount: Btc, feeratePerKw: FeeratePerKw): Future[SignFundingTxResponse] = { + bitcoinCoreClient.makeFundingTx(pubkeyScript, amount, feeratePerKw).pipeTo(sender.ref) + val res = sender.expectMsgType[MakeFundingTxResponse] + bitcoinCoreClient.signFundingTx(res.fundingTx, pubkeyScript, res.fundingTxOutputIndex, res.fee, feeratePerKw).pipeTo(sender.ref) + } + test("encrypt wallet") { assume(!useEclairSigner) @@ -71,7 +77,7 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A restartBitcoind(sender) val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) - bitcoinClient.makeFundingTx(pubkeyScript, 50 millibtc, FeeratePerKw(10000 sat)).pipeTo(sender.ref) + makeFundingTx(bitcoinClient, sender, pubkeyScript, Btc(0.1), FeeratePerKw(250 sat)) val error = sender.expectMsgType[Failure].cause.asInstanceOf[JsonRPCError].error assert(error.message.contains("Please enter the wallet passphrase with walletpassphrase first")) @@ -382,16 +388,18 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val fundingTxs = for (_ <- 0 to 3) yield { val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) - bitcoinClient.makeFundingTx(pubkeyScript, Satoshi(500), FeeratePerKw(250 sat)).pipeTo(sender.ref) - val fundingTx = sender.expectMsgType[MakeFundingTxResponse].fundingTx + makeFundingTx(bitcoinClient, sender, pubkeyScript, Satoshi(500), FeeratePerKw(250 sat)) + + val fundingTx = sender.expectMsgType[SignFundingTxResponse].fundingTx bitcoinClient.publishTransaction(fundingTx.copy(txIn = Nil)).pipeTo(sender.ref) // try publishing an invalid version of the tx sender.expectMsgType[Failure] bitcoinClient.rollback(fundingTx).pipeTo(sender.ref) // rollback the locked outputs sender.expectMsg(true) // now fund a tx with correct feerate - bitcoinClient.makeFundingTx(pubkeyScript, 50 millibtc, FeeratePerKw(250 sat)).pipeTo(sender.ref) - sender.expectMsgType[MakeFundingTxResponse].fundingTx + makeFundingTx(bitcoinClient, sender, pubkeyScript, 50 millibtc, FeeratePerKw(250 sat)) + + sender.expectMsgType[SignFundingTxResponse].fundingTx } bitcoinClient.listLockedOutpoints().pipeTo(sender.ref) @@ -426,8 +434,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(randomKey().publicKey, randomKey().publicKey))) // 200 sat/kw is below the min-relay-fee - bitcoinClient.makeFundingTx(pubkeyScript, 5 millibtc, FeeratePerKw(200 sat)).pipeTo(sender.ref) - val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse] + makeFundingTx(bitcoinClient, sender, pubkeyScript, 5 millibtc, FeeratePerKw(200 sat)) + val SignFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[SignFundingTxResponse] bitcoinClient.commit(fundingTx).pipeTo(sender.ref) sender.expectMsg(true) @@ -501,8 +509,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A val bitcoinClient = makeBitcoinCoreClient() // create a huge tx so we make sure it has > 2 inputs - bitcoinClient.makeFundingTx(pubkeyScript, 250 btc, FeeratePerKw(1000 sat)).pipeTo(sender.ref) - val MakeFundingTxResponse(fundingTx, outputIndex, _) = sender.expectMsgType[MakeFundingTxResponse] + makeFundingTx(bitcoinClient, sender, pubkeyScript, 250 btc, FeeratePerKw(1000 sat)) + val SignFundingTxResponse(fundingTx, outputIndex, _) = sender.expectMsgType[SignFundingTxResponse] assert(fundingTx.txIn.length > 2) // spend the first 2 inputs @@ -732,8 +740,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A { // test #1: unlock wallet outpoints that are actually locked // create a huge tx so we make sure it has > 1 inputs - bitcoinClient.makeFundingTx(nonWalletScript, 250 btc, FeeratePerKw(1000 sat)).pipeTo(sender.ref) - val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse] + makeFundingTx(bitcoinClient, sender, nonWalletScript, 250 btc, FeeratePerKw(1000 sat)) + val SignFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[SignFundingTxResponse] assert(fundingTx.txIn.size > 2) bitcoinClient.listLockedOutpoints().pipeTo(sender.ref) sender.expectMsg(fundingTx.txIn.map(_.outPoint).toSet) @@ -742,8 +750,8 @@ class BitcoinCoreClientSpec extends TestKitBaseClass with BitcoindService with A } { // test #2: some outpoints are locked, some are unlocked - bitcoinClient.makeFundingTx(nonWalletScript, 250 btc, FeeratePerKw(1000 sat)).pipeTo(sender.ref) - val MakeFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[MakeFundingTxResponse] + makeFundingTx(bitcoinClient, sender, nonWalletScript, 250 btc, FeeratePerKw(1000 sat)) + val SignFundingTxResponse(fundingTx, _, _) = sender.expectMsgType[SignFundingTxResponse] assert(fundingTx.txIn.size > 2) bitcoinClient.listLockedOutpoints().pipeTo(sender.ref) sender.expectMsg(fundingTx.txIn.map(_.outPoint).toSet) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index 3cc68fb47e..abfc9b7929 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -23,7 +23,7 @@ import akka.pattern.pipe import akka.testkit.TestProbe import fr.acinq.bitcoin.scalacompat.{Block, Btc, MilliBtcDouble, OutPoint, SatoshiLong, Script, Transaction, TxId, TxOut} import fr.acinq.eclair.TestUtils.randomTxId -import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse} +import fr.acinq.eclair.blockchain.OnChainWallet.{FundTransactionResponse, MakeFundingTxResponse, SignFundingTxResponse} import fr.acinq.eclair.blockchain.WatcherSpec._ import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.SignTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ @@ -36,6 +36,7 @@ import fr.acinq.eclair.{BlockHeight, RealShortChannelId, TestConstants, TestKitB import grizzled.slf4j.Logging import org.scalatest.BeforeAndAfterAll import org.scalatest.funsuite.AnyFunSuiteLike +import scodec.bits.ByteVector import java.util.UUID import java.util.concurrent.atomic.AtomicLong @@ -174,8 +175,10 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind watcher ! WatchPublished(probe.ref, publishedTx.txid) probe.expectMsg(WatchPublishedTriggered(publishedTx)) - bitcoinClient.makeFundingTx(Script.write(Script.pay2wpkh(randomKey().publicKey)), 150_000 sat, FeeratePerKw(2500 sat)).pipeTo(probe.ref) - val unpublishedTx = probe.expectMsgType[MakeFundingTxResponse].fundingTx + bitcoinClient.makeFundingTx(ByteVector.empty, 150_000 sat, FeeratePerKw(2500 sat)).pipeTo(probe.ref) + val res = probe.expectMsgType[MakeFundingTxResponse] + bitcoinClient.signFundingTx(res.fundingTx, Script.write(Script.pay2wpkh(randomKey().publicKey)), res.fundingTxOutputIndex, res.fee, FeeratePerKw(2500 sat)).pipeTo(probe.ref) + val unpublishedTx = probe.expectMsgType[SignFundingTxResponse].fundingTx watcher ! WatchPublished(probe.ref, unpublishedTx.txid) probe.expectNoMessage(100 millis) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 6a25ac5c40..8330e5fda6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -18,9 +18,10 @@ package fr.acinq.eclair.channel.states.a import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, SatoshiLong} +import fr.acinq.bitcoin.scalacompat.{Btc, ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout @@ -67,6 +68,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) val fundingAmount = if (test.tags.contains(LargeChannel)) Btc(5).toSatoshi else TestConstants.fundingSatoshis alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, fundingAmount, maxExcess_opt = None, dualFunded = false, commitTxFeerate, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + alice ! MakeFundingTxResponse(Transaction(version = 2, Nil, Seq(TxOut(fundingAmount, ByteVector.empty)), 0), 0, 1 sat) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) @@ -82,7 +84,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(accept.upfrontShutdownScript_opt.contains(ByteVector.empty)) assert(accept.channelType_opt.contains(ChannelTypes.StaticRemoteKey())) bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) aliceOpenReplyTo.expectNoMessage() } @@ -91,8 +93,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs())) bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -101,8 +103,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx())) bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -111,8 +113,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputsZeroFeeHtlcTx(scidAlias = true))) bob2alice.forward(alice) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == ZeroFeeHtlcTxAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -123,8 +125,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS // Alice explicitly asked for an anchor output channel. Bob doesn't support explicit channel type negotiation but // they both activated anchor outputs so it is the default choice anyway. bob2alice.forward(alice, accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty)))) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -145,8 +147,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS // Alice asked for a standard channel whereas they both support anchor outputs. assert(accept.channelType_opt.contains(ChannelTypes.Standard())) bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == DefaultCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == DefaultCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -174,14 +176,15 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val bobParams = Bob.channelParams.copy(initFeatures = Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional)) alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.anchorOutputsFeeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, Init(bobParams.initFeatures), channelFlags, channelConfig, ChannelTypes.AnchorOutputs(), replyTo = aliceOpenReplyTo.ref.toTyped) bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, Init(bobParams.initFeatures), channelConfig, ChannelTypes.AnchorOutputs()) + alice ! MakeFundingTxResponse(Transaction(version = 2, Nil, Seq(TxOut(TestConstants.fundingSatoshis, ByteVector.empty)), 0), 0, 1 sat) val open = alice2bob.expectMsgType[OpenChannel] assert(open.channelType_opt.contains(ChannelTypes.AnchorOutputs())) alice2bob.forward(bob, open) val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.channelType_opt.contains(ChannelTypes.AnchorOutputs())) bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.commitmentFormat == UnsafeLegacyAnchorOutputsCommitmentFormat) aliceOpenReplyTo.expectNoMessage() } @@ -303,7 +306,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.minimumDepth == 13) // with large channel tag we create a 5BTC channel bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) aliceOpenReplyTo.expectNoMessage() } @@ -312,8 +315,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val accept = bob2alice.expectMsgType[AcceptChannel] assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.localParams.upfrontShutdownScript_opt.get)) bob2alice.forward(alice, accept) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.remoteParams.upfrontShutdownScript_opt == accept.upfrontShutdownScript_opt) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.remoteParams.upfrontShutdownScript_opt == accept.upfrontShutdownScript_opt) aliceOpenReplyTo.expectNoMessage() } @@ -323,8 +326,8 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS assert(accept.upfrontShutdownScript_opt.contains(bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].params.localParams.upfrontShutdownScript_opt.get)) val accept1 = accept.copy(tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScriptTlv(ByteVector.empty))) bob2alice.forward(alice, accept1) - awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) - assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_INTERNAL].params.remoteParams.upfrontShutdownScript_opt.isEmpty) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + assert(alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].params.remoteParams.upfrontShutdownScript_opt.isEmpty) aliceOpenReplyTo.expectNoMessage() } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index ab0495da97..d47b4ecbb2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -278,6 +278,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui test("recv OpenChannel (upfront shutdown script)", Tag(ChannelStateTestsTags.UpfrontShutdownScript)) { f => import f._ val open = alice2bob.expectMsgType[OpenChannel] + awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) assert(open.upfrontShutdownScript_opt.contains(alice.stateData.asInstanceOf[DATA_WAIT_FOR_ACCEPT_CHANNEL].initFunder.localParams.upfrontShutdownScript_opt.get)) alice2bob.forward(bob, open) awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala index e4565e2f59..c6edd91e0f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingInternalStateSpec.scala @@ -19,8 +19,9 @@ package fr.acinq.eclair.channel.states.b import akka.actor.Status import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps import akka.testkit.{TestFSMRef, TestProbe} -import fr.acinq.bitcoin.scalacompat.ByteVector32 +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Transaction, TxOut} import fr.acinq.eclair.blockchain.NoOpOnChainWallet +import fr.acinq.eclair.blockchain.OnChainWallet.MakeFundingTxResponse import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout @@ -30,6 +31,7 @@ import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{TestConstants, TestKitBaseClass} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -47,22 +49,23 @@ class WaitForFundingInternalStateSpec extends TestKitBaseClass with FixtureAnyFu val channelConfig = ChannelConfig.standard val channelFlags = ChannelFlags(announceChannel = false) val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) - val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) val listener = TestProbe() within(30 seconds) { alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) - bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) - alice2bob.expectMsgType[OpenChannel] - alice2bob.forward(bob) - bob2alice.expectMsgType[AcceptChannel] - bob2alice.forward(alice) awaitCond(alice.stateName == WAIT_FOR_FUNDING_INTERNAL) withFixture(test.toNoArgTest(FixtureParam(alice, aliceOpenReplyTo, alice2bob, bob2alice, alice2blockchain, listener))) } } + test("recv MakeFundingTxResponse (funding success)") { f => + import f._ + alice ! MakeFundingTxResponse(Transaction(version = 2, Nil, Seq(TxOut(TestConstants.fundingSatoshis, ByteVector.empty)), 0), 0, 1 sat) + alice2bob.expectMsgType[OpenChannel] + awaitCond(alice.stateName == WAIT_FOR_ACCEPT_CHANNEL) + } + test("recv Status.Failure (wallet error)") { f => import f._ alice ! Status.Failure(new RuntimeException("insufficient funds")) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala new file mode 100644 index 0000000000..961ba05336 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedInternalStateSpec.scala @@ -0,0 +1,145 @@ +/* + * Copyright 2024 ACINQ SAS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fr.acinq.eclair.channel.states.b + +import akka.actor.Status +import akka.actor.typed.scaladsl.adapter.ClassicActorRefOps +import akka.testkit.{TestFSMRef, TestProbe} +import fr.acinq.bitcoin.scalacompat.{ByteVector32, SatoshiLong, Script, Transaction, TxOut} +import fr.acinq.eclair.blockchain.OnChainWallet.{MakeFundingTxResponse, SignFundingTxResponse} +import fr.acinq.eclair.blockchain.{NoOpOnChainWallet, SingleKeyOnChainWallet} +import fr.acinq.eclair.channel._ +import fr.acinq.eclair.channel.fsm.Channel +import fr.acinq.eclair.channel.fsm.Channel.TickChannelOpenTimeout +import fr.acinq.eclair.channel.states.ChannelStateTestsBase +import fr.acinq.eclair.io.Peer.OpenChannelResponse +import fr.acinq.eclair.transactions.Scripts +import fr.acinq.eclair.wire.protocol._ +import fr.acinq.eclair.{TestConstants, TestKitBaseClass} +import org.scalatest.Outcome +import org.scalatest.funsuite.FixtureAnyFunSuiteLike +import scodec.bits.ByteVector + +import scala.concurrent.duration._ + +/** + * Created by remyers on 05/08/2024. + */ + +class WaitForFundingSignedInternalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with ChannelStateTestsBase { + + case class FixtureParam(alice: TestFSMRef[ChannelState, ChannelData, Channel], + bob: TestFSMRef[ChannelState, ChannelData, Channel], + wallet: NoOpOnChainWallet, + aliceOpenReplyTo: TestProbe, + alice2bob: TestProbe, + bob2alice: TestProbe, + alice2blockchain: TestProbe, + listener: TestProbe) + + override def withFixture(test: OneArgTest): Outcome = { + val walletA = new NoOpOnChainWallet() + val setup = init(wallet_opt = Some(walletA), tags = test.tags) + import setup._ + val channelConfig = ChannelConfig.standard + val channelFlags = ChannelFlags(announceChannel = false) + val (aliceParams, bobParams, channelType) = computeFeatures(setup, test.tags, channelFlags) + val aliceInit = Init(aliceParams.initFeatures) + val bobInit = Init(bobParams.initFeatures) + val listener = TestProbe() + within(30 seconds) { + alice.underlying.system.eventStream.subscribe(listener.ref, classOf[ChannelAborted]) + alice ! INPUT_INIT_CHANNEL_INITIATOR(ByteVector32.Zeroes, TestConstants.fundingSatoshis, maxExcess_opt = None, dualFunded = false, TestConstants.feeratePerKw, TestConstants.feeratePerKw, fundingTxFeeBudget_opt = None, Some(TestConstants.initiatorPushAmount), requireConfirmedInputs = false, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, channelType, replyTo = aliceOpenReplyTo.ref.toTyped) + bob ! INPUT_INIT_CHANNEL_NON_INITIATOR(ByteVector32.Zeroes, None, dualFunded = false, None, bobParams, bob2alice.ref, aliceInit, channelConfig, channelType) + alice ! MakeFundingTxResponse(Transaction(version = 2, Nil, Seq(TxOut(TestConstants.fundingSatoshis, ByteVector.empty)), 0), 0, 1 sat) + alice2bob.expectMsgType[OpenChannel] + alice2bob.forward(bob) + bob2alice.expectMsgType[AcceptChannel] + bob2alice.forward(alice) + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED_INTERNAL) + awaitCond(bob.stateName == WAIT_FOR_FUNDING_CREATED) + withFixture(test.toNoArgTest(FixtureParam(alice, bob, walletA, aliceOpenReplyTo, alice2bob, bob2alice, alice2blockchain, listener))) + } + } + + def makeFundingSignedResponse(alice: TestFSMRef[ChannelState, ChannelData, Channel], bob: TestFSMRef[ChannelState, ChannelData, Channel]): SignFundingTxResponse = { + val aliceFundingPubkey = bob.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_CREATED].remoteFundingPubKey + val bobFundingPubkey = alice.stateData.asInstanceOf[DATA_WAIT_FOR_FUNDING_SIGNED_INTERNAL].remoteFundingPubKey + val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(aliceFundingPubkey, bobFundingPubkey))) + val fundingTx = Transaction(version = 2, Nil, Seq(TxOut(TestConstants.fundingSatoshis, fundingPubkeyScript)), 0) + SignFundingTxResponse(fundingTx, 0, 1 sat) + } + + test("recv SignFundingTxResponse (funding signed success)") { f => + import f._ + alice ! makeFundingSignedResponse(alice, bob) + alice2bob.expectMsgType[FundingCreated] + awaitCond(alice.stateName == WAIT_FOR_FUNDING_SIGNED) + aliceOpenReplyTo.expectNoMessage(100 millis) + assert(wallet.rolledback.isEmpty) + } + + test("recv Status.Failure (wallet error)") { f => + import f._ + alice ! Status.Failure(new RuntimeException("insufficient funds")) + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsgType[OpenChannelResponse.Rejected] + aliceOpenReplyTo.expectNoMessage(100 millis) + awaitCond(wallet.rolledback.size == 1) + } + + test("recv Error") { f => + import f._ + alice ! Error(ByteVector32.Zeroes, "oops") + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsgType[OpenChannelResponse.RemoteError] + awaitCond(wallet.rolledback.length == 1) + } + + test("recv CMD_CLOSE") { f => + import f._ + val sender = TestProbe() + val c = CMD_CLOSE(sender.ref, None, None) + alice ! c + sender.expectMsg(RES_SUCCESS(c, ByteVector32.Zeroes)) + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsg(OpenChannelResponse.Cancelled) + awaitCond(wallet.rolledback.length == 1) + } + + test("recv INPUT_DISCONNECTED") { f => + import f._ + alice ! INPUT_DISCONNECTED + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsg(OpenChannelResponse.Disconnected) + awaitCond(wallet.rolledback.length == 1) + } + + test("recv TickChannelOpenTimeout") { f => + import f._ + alice ! TickChannelOpenTimeout + listener.expectMsgType[ChannelAborted] + awaitCond(alice.stateName == CLOSED) + aliceOpenReplyTo.expectMsg(OpenChannelResponse.TimedOut) + awaitCond(wallet.rolledback.length == 1) + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala index b5d7775a4f..a722abce43 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsBatchValidationSpec.scala @@ -95,8 +95,10 @@ object AnnouncementsBatchValidationSpec { val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(node1BitcoinKey.publicKey, node2BitcoinKey.publicKey))) val fundingTxFuture = bitcoinClient.makeFundingTx(fundingPubkeyScript, amount, FeeratePerKw(10000 sat)) val res = Await.result(fundingTxFuture, 10 seconds) - Await.result(bitcoinClient.publishTransaction(res.fundingTx), 10 seconds) - SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, res.fundingTx, res.fundingTxOutputIndex) + val signedTxFuture = bitcoinClient.signFundingTx(res.fundingTx, fundingPubkeyScript, res.fundingTxOutputIndex, res.fee, FeeratePerKw(10000 sat)) + val res1 = Await.result(signedTxFuture, 10 seconds) + Await.result(bitcoinClient.publishTransaction(res1.fundingTx), 10 seconds) + SimulatedChannel(node1Key, node2Key, node1BitcoinKey, node2BitcoinKey, amount, res1.fundingTx, res1.fundingTxOutputIndex) } def makeChannelAnnouncement(c: SimulatedChannel, bitcoinClient: BitcoinCoreClient)(implicit ec: ExecutionContext): ChannelAnnouncement = {