diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index 0a333cd52..40e4130c8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -23,7 +23,7 @@ import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy, typed} import akka.pattern.after import akka.util.Timeout import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, BlockId, ByteVector32, Satoshi, Script, addressToPublicKeyScript} +import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, BlockId, ByteVector32, Satoshi, Script, ScriptElt, addressToPublicKeyScript} import fr.acinq.eclair.NodeParams.hashFromChain import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} @@ -264,6 +264,7 @@ class Setup(val datadir: File, _ <- feeratesRetrieved.future finalPubkey = new AtomicReference[PublicKey](null) + finalPubkeyScript = new AtomicReference[Seq[ScriptElt]](null) pubkeyRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-pubkey-refresh-delay").getSeconds, TimeUnit.SECONDS) // there are 3 possibilities regarding onchain key management: // 1) there is no `eclair-signer.conf` file in Eclair's data directory, Eclair will not manage Bitcoin core keys, and Eclair's API will not return bitcoin core descriptors. This is the default mode. @@ -273,17 +274,25 @@ class Setup(val datadir: File, // 3) there is an `eclair-signer.conf` file in Eclair's data directory, and the name of the wallet set in `eclair-signer.conf` matches the `eclair.bitcoind.wallet` setting in `eclair.conf`. // Eclair will assume that this is a watch-only bitcoin wallet that has been created from descriptors generated by Eclair, and will manage its private keys, and here we pass the onchain key manager to our bitcoin client. bitcoinClient = new BitcoinCoreClient(bitcoin, nodeParams.liquidityAdsConfig.lockUtxos, if (bitcoin.wallet == onChainKeyManager_opt.map(_.walletName)) onChainKeyManager_opt else None) with OnchainPubkeyCache { - val refresher: typed.ActorRef[OnchainPubkeyRefresher.Command] = system.spawn(Behaviors.supervise(OnchainPubkeyRefresher(this, finalPubkey, pubkeyRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "onchain-address-manager") + val refresher: typed.ActorRef[OnchainPubkeyRefresher.Command] = system.spawn(Behaviors.supervise(OnchainPubkeyRefresher(this, finalPubkey, finalPubkeyScript, pubkeyRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "onchain-address-manager") override def getP2wpkhPubkey(renew: Boolean): PublicKey = { val key = finalPubkey.get() if (renew) refresher ! OnchainPubkeyRefresher.Renew key } + + override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = { + val script = finalPubkeyScript.get() + if (renew) refresher ! OnchainPubkeyRefresher.RenewPubkeyScript + script + } } _ = if (bitcoinClient.useEclairSigner) logger.info("using eclair to sign bitcoin core transactions") initialPubkey <- bitcoinClient.getP2wpkhPubkey() _ = finalPubkey.set(initialPubkey) + initialPubkeyScript <- bitcoinClient.getReceivePublicKeyScript() + _ = finalPubkeyScript.set(initialPubkeyScript) // If we started funding a transaction and restarted before signing it, we may have utxos that stay locked forever. // We want to do something about it: we can unlock them automatically, or let the node operator decide what to do. 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 351406d7d..e3a3359d2 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 @@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, Transaction, TxId} +import fr.acinq.bitcoin.scalacompat.{OutPoint, Satoshi, ScriptElt, Transaction, TxId} import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType import fr.acinq.eclair.blockchain.fee.FeeratePerKw import scodec.bits.ByteVector @@ -117,10 +117,7 @@ trait OnChainChannelFunder { /** This trait lets users generate on-chain addresses and public keys. */ trait OnChainAddressGenerator { - /** - * @param label used if implemented with bitcoin core, can be ignored by implementation - */ - def getReceiveAddress(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] + def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] /** Generate a p2wpkh wallet address and return the corresponding public key. */ def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[PublicKey] @@ -133,6 +130,8 @@ trait OnchainPubkeyCache { * @param renew applies after requesting the current pubkey, and is asynchronous */ def getP2wpkhPubkey(renew: Boolean = true): PublicKey + + def getReceivePubkeyScript(renew: Boolean = true): Seq[ScriptElt] } /** This trait lets users check the wallet's on-chain balance. */ diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresher.scala index 391e8212b..99efbd0b1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresher.scala @@ -4,6 +4,7 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.typed.Behavior import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey +import fr.acinq.bitcoin.scalacompat.{BlockHash, ScriptElt, addressToPublicKeyScript} import fr.acinq.eclair.blockchain.OnChainAddressGenerator import java.util.concurrent.atomic.AtomicReference @@ -19,27 +20,29 @@ object OnchainPubkeyRefresher { // @formatter:off sealed trait Command case object Renew extends Command + case object RenewPubkeyScript extends Command private case class Set(pubkey: PublicKey) extends Command + private case class SetPubkeyScript(script: Seq[ScriptElt]) extends Command private case class Error(reason: Throwable) extends Command private case object Done extends Command // @formatter:on - def apply(generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], delay: FiniteDuration): Behavior[Command] = { + def apply(generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], finalPubkeyScript: AtomicReference[Seq[ScriptElt]], delay: FiniteDuration): Behavior[Command] = { Behaviors.setup { context => Behaviors.withTimers { timers => - new OnchainPubkeyRefresher(generator, finalPubkey, context, timers, delay).idle() + new OnchainPubkeyRefresher(generator, finalPubkey, finalPubkeyScript, context, timers, delay).idle() } } } } -private class OnchainPubkeyRefresher(generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], context: ActorContext[OnchainPubkeyRefresher.Command], timers: TimerScheduler[OnchainPubkeyRefresher.Command], delay: FiniteDuration) { +private class OnchainPubkeyRefresher(generator: OnChainAddressGenerator, finalPubkey: AtomicReference[PublicKey], finalPubkeyScript: AtomicReference[Seq[ScriptElt]], context: ActorContext[OnchainPubkeyRefresher.Command], timers: TimerScheduler[OnchainPubkeyRefresher.Command], delay: FiniteDuration) { import OnchainPubkeyRefresher._ def idle(): Behavior[Command] = Behaviors.receiveMessagePartial { case Renew => - context.log.debug(s"received Renew current script is ${finalPubkey.get()}") + context.log.debug(s"received Renew current pubkey is ${finalPubkey.get()}") context.pipeToSelf(generator.getP2wpkhPubkey()) { case Success(pubkey) => Set(pubkey) case Failure(reason) => Error(reason) @@ -52,12 +55,33 @@ private class OnchainPubkeyRefresher(generator: OnChainAddressGenerator, finalPu context.log.error("cannot generate new onchain address", reason) Behaviors.same } + case RenewPubkeyScript => + context.log.debug(s"received Renew current script is ${finalPubkeyScript.get()}") + context.pipeToSelf(generator.getReceivePublicKeyScript()) { + case Success(script) => SetPubkeyScript(script) + case Failure(reason) => Error(reason) + } + Behaviors.receiveMessagePartial { + case SetPubkeyScript(script) => + timers.startSingleTimer(Done, delay) // wait a bit to avoid generating too many addresses in case of mass channel force-close + waiting(script) + case Error(reason) => + context.log.error("cannot generate new onchain address", reason) + Behaviors.same + } } - def waiting(script: PublicKey): Behavior[Command] = Behaviors.receiveMessagePartial { + def waiting(pubkey: PublicKey): Behavior[Command] = Behaviors.receiveMessagePartial { + case Done => + context.log.info(s"setting final onchain pubkey to $pubkey") + finalPubkey.set(pubkey) + idle() + } + + def waiting(script: Seq[ScriptElt]): Behavior[Command] = Behaviors.receiveMessagePartial { case Done => context.log.info(s"setting final onchain script to $script") - finalPubkey.set(script) + finalPubkeyScript.set(script) idle() } } 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 80d488d1c..3b00ede71 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 @@ -592,6 +592,10 @@ class BitcoinCoreClient(val rpcClient: BitcoinJsonRPCClient, val lockUtxos: Bool } } + def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] = getReceiveAddress(addressType).map { address => + addressToPublicKeyScript(this.rpcClient.chainHash, address).getOrElse(throw new RuntimeException(s"cannot convert $address to a public key script")) + } + def getChangeAddress(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = for { JString(address) <- rpcClient.invoke("getrawchangeaddress", addressType.map(_.bitcoinCoreName).toList: _*) verifiedAddress <- verifyAddress(address) 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 af9b1d341..221a8464c 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 @@ -23,6 +23,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256} import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat._ import fr.acinq.eclair._ +import fr.acinq.eclair.blockchain.OnchainPubkeyCache import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL @@ -628,6 +629,16 @@ object Helpers { object MutualClose { + def generateFinalScriptPubKey(wallet: OnchainPubkeyCache, allowAnySegwit: Boolean, renew: Boolean = true): ByteVector = { + val finalScriptPubkey = if (!allowAnySegwit) { + val finalPubKey = wallet.getP2wpkhPubkey(renew) + Script.write(Script.pay2wpkh(finalPubKey)) + } else { + Script.write(wallet.getReceivePubkeyScript(renew)) + } + finalScriptPubkey + } + def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean, allowOpReturn: Boolean): Boolean = { Try(Script.parse(scriptPubKey)) match { case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala index 5d3bdfba9..c10e770bb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/CommonHandlers.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.channel.fsm import akka.actor.FSM -import fr.acinq.bitcoin.scalacompat.{ByteVector32, Script} +import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.Features import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel._ @@ -110,6 +110,7 @@ trait CommonHandlers { case d: DATA_NEGOTIATING_SIMPLE => d.localScriptPubKey case d: DATA_CLOSING => d.finalScriptPubKey case d => + val allowAnySegwit = Features.canUseFeature(data.commitments.params.localParams.initFeatures, data.commitments.params.remoteParams.initFeatures, Features.ShutdownAnySegwit) d.commitments.params.localParams.upfrontShutdownScript_opt match { case Some(upfrontShutdownScript) => if (data.commitments.params.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { @@ -117,21 +118,18 @@ trait CommonHandlers { upfrontShutdownScript } else { log.info("ignoring pre-generated shutdown script, because option_upfront_shutdown_script is disabled") - generateFinalScriptPubKey() + val finalScriptPubkey = Helpers.Closing.MutualClose.generateFinalScriptPubKey(wallet, allowAnySegwit) + log.info(s"using finalScriptPubkey=$finalScriptPubkey") + finalScriptPubkey } case None => // normal case: we don't pre-generate shutdown scripts - generateFinalScriptPubKey() + val finalScriptPubkey = Helpers.Closing.MutualClose.generateFinalScriptPubKey(wallet, allowAnySegwit) + log.info(s"using finalScriptPubkey=$finalScriptPubkey") + finalScriptPubkey } } - private def generateFinalScriptPubKey(): ByteVector = { - val finalPubKey = wallet.getP2wpkhPubkey() - val finalScriptPubKey = Script.write(Script.pay2wpkh(finalPubKey)) - log.info(s"using finalScriptPubkey=$finalScriptPubKey") - finalScriptPubKey - } - def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = { val localScript = localShutdown.scriptPubKey val remoteScript = remoteShutdown.scriptPubKey diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala index c5da255b7..324a53b19 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/OpenChannelInterceptor.scala @@ -324,11 +324,10 @@ private class OpenChannelInterceptor(peer: ActorRef[Any], } private def createLocalParams(nodeParams: NodeParams, initFeatures: Features[InitFeature], upfrontShutdownScript: Boolean, channelType: SupportedChannelType, isChannelOpener: Boolean, paysCommitTxFees: Boolean, dualFunded: Boolean, fundingAmount: Satoshi, disableMaxHtlcValueInFlight: Boolean): LocalParams = { - val pubkey_opt = if (upfrontShutdownScript || channelType.paysDirectlyToWallet) Some(wallet.getP2wpkhPubkey()) else None makeChannelParams( nodeParams, initFeatures, - if (upfrontShutdownScript) Some(Script.write(Script.pay2wpkh(pubkey_opt.get))) else None, - if (channelType.paysDirectlyToWallet) Some(pubkey_opt.get) else None, + if (upfrontShutdownScript) Some(Script.write(wallet.getReceivePubkeyScript())) else None, + if (channelType.paysDirectlyToWallet) Some(wallet.getP2wpkhPubkey()) else None, isChannelOpener = isChannelOpener, paysCommitTxFees = paysCommitTxFees, dualFunded = dualFunded, 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 f2c1c5849..d2788bda9 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 @@ -49,7 +49,10 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) - override def getReceiveAddress(addressTYpe: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = Future.successful(dummyReceiveAddress) + override def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] = Future.successful(addressType match { + case Some(AddressType.P2tr) => Script.pay2tr(dummyReceivePubkey.xOnly) + case _ => Script.pay2wpkh(dummyReceivePubkey) + }) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) @@ -92,6 +95,8 @@ class DummyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(false) override def getP2wpkhPubkey(renew: Boolean): PublicKey = dummyReceivePubkey + + override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = Script.pay2tr(dummyReceivePubkey.xOnly) } class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { @@ -104,7 +109,10 @@ class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) - override def getReceiveAddress(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = Future.successful(dummyReceiveAddress) + override def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] = Future.successful(addressType match { + case Some(AddressType.P2tr) => Script.pay2tr(dummyReceivePubkey.xOnly) + case _ => Script.pay2wpkh(dummyReceivePubkey) + }) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(dummyReceivePubkey) @@ -137,6 +145,8 @@ class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(doubleSpent.contains(tx.txid)) override def getP2wpkhPubkey(renew: Boolean): PublicKey = dummyReceivePubkey + + override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = Script.pay2tr(dummyReceivePubkey.xOnly) } class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { @@ -164,10 +174,10 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def onChainBalance()(implicit ec: ExecutionContext): Future[OnChainBalance] = Future.successful(OnChainBalance(1105 sat, 561 sat)) - override def getReceiveAddress(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = addressType match { - case Some(AddressType.P2tr) => Future.successful(pubkey.xOnly.pub.p2trAddress(fr.acinq.bitcoin.Block.RegtestGenesisBlock.hash)) - case _ => Future.successful(Bech32.encodeWitnessAddress("bcrt", 0, pubkey.hash160.toArray)) - } + override def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] = Future.successful(addressType match { + case Some(AddressType.P2wpkh) => script84 + case _ => script86 + }) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(pubkey) @@ -280,11 +290,11 @@ class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { override def doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(doubleSpent.contains(tx.txid)) override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey + + override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = script86 } object DummyOnChainWallet { - - val dummyReceiveAddress: String = "bcrt1qwcv8naajwn8fjhu8z59q9e6ucrqr068rlcenux" val dummyReceivePubkey: PublicKey = PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12") def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresherSpec.scala index 82940cc17..48c4f5cfa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/OnchainPubkeyRefresherSpec.scala @@ -2,7 +2,7 @@ package fr.acinq.eclair.blockchain.bitcoind import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, Crypto, computeBIP84Address} +import fr.acinq.bitcoin.scalacompat.{Block, Crypto, Script, ScriptElt, computeBIP84Address} import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType import fr.acinq.eclair.{TestKitBaseClass, randomKey} @@ -15,16 +15,24 @@ import scala.concurrent.{ExecutionContext, Future} class OnchainPubkeyRefresherSpec extends TestKitBaseClass with AnyFunSuiteLike { test("renew onchain scripts") { val finalPubkey = new AtomicReference[PublicKey](randomKey().publicKey) + val finalPubkeyScript = new AtomicReference[Seq[ScriptElt]](Script.pay2tr(randomKey().xOnlyPublicKey())) val generator = new OnChainAddressGenerator { - override def getReceiveAddress(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = Future.successful(computeBIP84Address(randomKey().publicKey, Block.RegtestGenesisBlock.hash)) + override def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] = Future.successful(addressType match { + case Some(AddressType.P2tr) => Script.pay2tr(randomKey().xOnlyPublicKey()) + case _ => Script.pay2wpkh(randomKey().publicKey) + }) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(randomKey().publicKey) } - val manager = system.spawnAnonymous(OnchainPubkeyRefresher(generator, finalPubkey, 3 seconds)) - - // renew script explicitly + val manager = system.spawnAnonymous(OnchainPubkeyRefresher(generator, finalPubkey, finalPubkeyScript, 3 seconds)) + // renew pubkey explicitly val currentPubkey = finalPubkey.get() manager ! OnchainPubkeyRefresher.Renew awaitCond(finalPubkey.get() != currentPubkey) + + // renew pubkey explicitly + val currentPubkeyScript = finalPubkeyScript.get() + manager ! OnchainPubkeyRefresher.RenewPubkeyScript + awaitCond(finalPubkeyScript.get() != currentPubkeyScript) } } 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 9acc21ad4..5e8757774 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, Transaction, TxId} +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._ @@ -129,8 +129,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w getP2wpkhPubkey().pipeTo(probe.ref) probe.expectMsgType[PublicKey] } + val pubkeyScript = { + getReceivePublicKeyScript(None).pipeTo(probe.ref) + probe.expectMsgType[Seq[ScriptElt]] + } override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey + + override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = pubkeyScript } (walletRpcClient, walletClient) @@ -1848,8 +1854,14 @@ class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherS getP2wpkhPubkey().pipeTo(probe.ref) probe.expectMsgType[PublicKey] } + lazy val pubkeyScript = { + getReceivePublicKeyScript(None).pipeTo(probe.ref) + probe.expectMsgType[Seq[ScriptElt]] + } override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey + + override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = pubkeyScript } createEclairBackedWallet(walletRpcClient, keyManager) 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 8b6c93940..18e702cf6 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 @@ -24,7 +24,7 @@ import akka.testkit.TestProbe import com.typesafe.config.ConfigFactory import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey -import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxId, computeBIP84Address} +import fr.acinq.bitcoin.scalacompat.{Block, BtcDouble, ByteVector32, Crypto, OutPoint, SatoshiLong, Script, Transaction, TxId, addressFromPublicKeyScript, computeBIP84Address} import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, JsonRPCError} import fr.acinq.eclair.channel._ @@ -148,11 +148,11 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data assert(dataC.commitments.params.commitmentFormat == commitmentFormat) - val finalAddressC = computeBIP84Address(nodes("C").wallet.getP2wpkhPubkey(false), Block.RegtestGenesisBlock.hash) + val Right(finalAddressC) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, nodes("C").wallet.getReceivePubkeyScript(false)) sender.send(nodes("F").register, Register.Forward(sender.ref.toTyped[Any], htlc.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val dataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data assert(dataF.commitments.params.commitmentFormat == commitmentFormat) - val finalAddressF = computeBIP84Address(nodes("F").wallet.getP2wpkhPubkey(false), Block.RegtestGenesisBlock.hash) + val Right(finalAddressF) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, nodes("F").wallet.getReceivePubkeyScript(false)) ForceCloseFixture(sender, paymentSender, stateListenerC, stateListenerF, paymentId, htlc, preimage, minerAddress, finalAddressC, finalAddressF) } @@ -434,7 +434,7 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec { // we retrieve C's default final address sender.send(nodes("C").register, Register.Forward(sender.ref.toTyped[Any], commitmentsF.channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]] - val finalAddressC = computeBIP84Address(nodes("C").wallet.getP2wpkhPubkey(false), Block.RegtestGenesisBlock.hash) + val Right(finalAddressC) = addressFromPublicKeyScript(Block.RegtestGenesisBlock.hash, nodes("C").wallet.getReceivePubkeyScript(false)) // we prepare the revoked transactions F will publish val keyManagerF = nodes("F").nodeParams.channelKeyManager val channelKeyPathF = keyManagerF.keyPath(commitmentsF.params.localParams, commitmentsF.params.channelConfig) @@ -566,8 +566,8 @@ class StandardChannelIntegrationSpec extends ChannelIntegrationSpec { sender.send(funder.register, Register.Forward(sender.ref.toTyped[Any], channelId, CMD_GET_CHANNEL_DATA(ActorRef.noSender))) val commitmentsC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments val fundingOutpoint = commitmentsC.latest.commitInput.outPoint - val finalPubKeyScriptC = Script.write(Script.pay2wpkh(nodes("C").wallet.getP2wpkhPubkey(false))) - val finalPubKeyScriptF = Script.write(Script.pay2wpkh(nodes("F").wallet.getP2wpkhPubkey(false))) + val finalPubKeyScriptC = Helpers.Closing.MutualClose.generateFinalScriptPubKey(nodes("C").wallet, true, false) + val finalPubKeyScriptF = Helpers.Closing.MutualClose.generateFinalScriptPubKey(nodes("F").wallet, true, false) fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_CLOSE(sender.ref, None, None)) sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]]