1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-13 11:35:47 +01:00

Cache both onchain public key and public key scripts

This commit is contained in:
sstone 2025-03-03 16:09:54 +01:00
parent 2db6229065
commit da7950b836
No known key found for this signature in database
GPG key ID: E04E48E72C205463
11 changed files with 120 additions and 46 deletions

View file

@ -23,7 +23,7 @@ import akka.actor.{ActorRef, ActorSystem, Props, SupervisorStrategy, typed}
import akka.pattern.after import akka.pattern.after
import akka.util.Timeout import akka.util.Timeout
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 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.NodeParams.hashFromChain
import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener} import fr.acinq.eclair.balance.{BalanceActor, ChannelsListener}
@ -264,6 +264,7 @@ class Setup(val datadir: File,
_ <- feeratesRetrieved.future _ <- feeratesRetrieved.future
finalPubkey = new AtomicReference[PublicKey](null) finalPubkey = new AtomicReference[PublicKey](null)
finalPubkeyScript = new AtomicReference[Seq[ScriptElt]](null)
pubkeyRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-pubkey-refresh-delay").getSeconds, TimeUnit.SECONDS) pubkeyRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-pubkey-refresh-delay").getSeconds, TimeUnit.SECONDS)
// there are 3 possibilities regarding onchain key management: // 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. // 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`. // 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. // 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 { 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 = { override def getP2wpkhPubkey(renew: Boolean): PublicKey = {
val key = finalPubkey.get() val key = finalPubkey.get()
if (renew) refresher ! OnchainPubkeyRefresher.Renew if (renew) refresher ! OnchainPubkeyRefresher.Renew
key 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") _ = if (bitcoinClient.useEclairSigner) logger.info("using eclair to sign bitcoin core transactions")
initialPubkey <- bitcoinClient.getP2wpkhPubkey() initialPubkey <- bitcoinClient.getP2wpkhPubkey()
_ = finalPubkey.set(initialPubkey) _ = 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. // 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. // We want to do something about it: we can unlock them automatically, or let the node operator decide what to do.

View file

@ -18,7 +18,7 @@ package fr.acinq.eclair.blockchain
import fr.acinq.bitcoin.psbt.Psbt import fr.acinq.bitcoin.psbt.Psbt
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 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.bitcoind.rpc.BitcoinCoreClient.AddressType
import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import scodec.bits.ByteVector import scodec.bits.ByteVector
@ -117,10 +117,7 @@ trait OnChainChannelFunder {
/** This trait lets users generate on-chain addresses and public keys. */ /** This trait lets users generate on-chain addresses and public keys. */
trait OnChainAddressGenerator { trait OnChainAddressGenerator {
/** def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]]
* @param label used if implemented with bitcoin core, can be ignored by implementation
*/
def getReceiveAddress(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String]
/** Generate a p2wpkh wallet address and return the corresponding public key. */ /** Generate a p2wpkh wallet address and return the corresponding public key. */
def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[PublicKey] def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[PublicKey]
@ -133,6 +130,8 @@ trait OnchainPubkeyCache {
* @param renew applies after requesting the current pubkey, and is asynchronous * @param renew applies after requesting the current pubkey, and is asynchronous
*/ */
def getP2wpkhPubkey(renew: Boolean = true): PublicKey def getP2wpkhPubkey(renew: Boolean = true): PublicKey
def getReceivePubkeyScript(renew: Boolean = true): Seq[ScriptElt]
} }
/** This trait lets users check the wallet's on-chain balance. */ /** This trait lets users check the wallet's on-chain balance. */

View file

@ -4,6 +4,7 @@ package fr.acinq.eclair.blockchain.bitcoind
import akka.actor.typed.Behavior import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler} import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler}
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{BlockHash, ScriptElt, addressToPublicKeyScript}
import fr.acinq.eclair.blockchain.OnChainAddressGenerator import fr.acinq.eclair.blockchain.OnChainAddressGenerator
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -19,27 +20,29 @@ object OnchainPubkeyRefresher {
// @formatter:off // @formatter:off
sealed trait Command sealed trait Command
case object Renew extends Command case object Renew extends Command
case object RenewPubkeyScript extends Command
private case class Set(pubkey: PublicKey) 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 class Error(reason: Throwable) extends Command
private case object Done extends Command private case object Done extends Command
// @formatter:on // @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.setup { context =>
Behaviors.withTimers { timers => 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._ import OnchainPubkeyRefresher._
def idle(): Behavior[Command] = Behaviors.receiveMessagePartial { def idle(): Behavior[Command] = Behaviors.receiveMessagePartial {
case Renew => 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()) { context.pipeToSelf(generator.getP2wpkhPubkey()) {
case Success(pubkey) => Set(pubkey) case Success(pubkey) => Set(pubkey)
case Failure(reason) => Error(reason) case Failure(reason) => Error(reason)
@ -52,12 +55,33 @@ private class OnchainPubkeyRefresher(generator: OnChainAddressGenerator, finalPu
context.log.error("cannot generate new onchain address", reason) context.log.error("cannot generate new onchain address", reason)
Behaviors.same 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 => case Done =>
context.log.info(s"setting final onchain script to $script") context.log.info(s"setting final onchain script to $script")
finalPubkey.set(script) finalPubkeyScript.set(script)
idle() idle()
} }
} }

View file

@ -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 { def getChangeAddress(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[String] = for {
JString(address) <- rpcClient.invoke("getrawchangeaddress", addressType.map(_.bitcoinCoreName).toList: _*) JString(address) <- rpcClient.invoke("getrawchangeaddress", addressType.map(_.bitcoinCoreName).toList: _*)
verifiedAddress <- verifyAddress(address) verifiedAddress <- verifyAddress(address)

View file

@ -23,6 +23,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256}
import fr.acinq.bitcoin.scalacompat.Script._ import fr.acinq.bitcoin.scalacompat.Script._
import fr.acinq.bitcoin.scalacompat._ import fr.acinq.bitcoin.scalacompat._
import fr.acinq.eclair._ import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.OnchainPubkeyCache
import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.fsm.Channel import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL import fr.acinq.eclair.channel.fsm.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL
@ -628,6 +629,16 @@ object Helpers {
object MutualClose { 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 = { def isValidFinalScriptPubkey(scriptPubKey: ByteVector, allowAnySegwit: Boolean, allowOpReturn: Boolean): Boolean = {
Try(Script.parse(scriptPubKey)) match { Try(Script.parse(scriptPubKey)) match {
case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true case Success(OP_DUP :: OP_HASH160 :: OP_PUSHDATA(pubkeyHash, _) :: OP_EQUALVERIFY :: OP_CHECKSIG :: Nil) if pubkeyHash.size == 20 => true

View file

@ -17,7 +17,7 @@
package fr.acinq.eclair.channel.fsm package fr.acinq.eclair.channel.fsm
import akka.actor.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.Features
import fr.acinq.eclair.channel.Helpers.Closing.MutualClose import fr.acinq.eclair.channel.Helpers.Closing.MutualClose
import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel._
@ -110,6 +110,7 @@ trait CommonHandlers {
case d: DATA_NEGOTIATING_SIMPLE => d.localScriptPubKey case d: DATA_NEGOTIATING_SIMPLE => d.localScriptPubKey
case d: DATA_CLOSING => d.finalScriptPubKey case d: DATA_CLOSING => d.finalScriptPubKey
case d => 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 { d.commitments.params.localParams.upfrontShutdownScript_opt match {
case Some(upfrontShutdownScript) => case Some(upfrontShutdownScript) =>
if (data.commitments.params.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) { if (data.commitments.params.channelFeatures.hasFeature(Features.UpfrontShutdownScript)) {
@ -117,21 +118,18 @@ trait CommonHandlers {
upfrontShutdownScript upfrontShutdownScript
} else { } else {
log.info("ignoring pre-generated shutdown script, because option_upfront_shutdown_script is disabled") 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 => case None =>
// normal case: we don't pre-generate shutdown scripts // 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]) = { def startSimpleClose(commitments: Commitments, localShutdown: Shutdown, remoteShutdown: Shutdown, closingFeerates: Option[ClosingFeerates]): (DATA_NEGOTIATING_SIMPLE, Option[ClosingComplete]) = {
val localScript = localShutdown.scriptPubKey val localScript = localShutdown.scriptPubKey
val remoteScript = remoteShutdown.scriptPubKey val remoteScript = remoteShutdown.scriptPubKey

View file

@ -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 = { 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( makeChannelParams(
nodeParams, initFeatures, nodeParams, initFeatures,
if (upfrontShutdownScript) Some(Script.write(Script.pay2wpkh(pubkey_opt.get))) else None, if (upfrontShutdownScript) Some(Script.write(wallet.getReceivePubkeyScript())) else None,
if (channelType.paysDirectlyToWallet) Some(pubkey_opt.get) else None, if (channelType.paysDirectlyToWallet) Some(wallet.getP2wpkhPubkey()) else None,
isChannelOpener = isChannelOpener, isChannelOpener = isChannelOpener,
paysCommitTxFees = paysCommitTxFees, paysCommitTxFees = paysCommitTxFees,
dualFunded = dualFunded, dualFunded = dualFunded,

View file

@ -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 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) 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 doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(false)
override def getP2wpkhPubkey(renew: Boolean): PublicKey = dummyReceivePubkey override def getP2wpkhPubkey(renew: Boolean): PublicKey = dummyReceivePubkey
override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = Script.pay2tr(dummyReceivePubkey.xOnly)
} }
class NoOpOnChainWallet extends OnChainWallet with OnchainPubkeyCache { 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 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) 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 doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(doubleSpent.contains(tx.txid))
override def getP2wpkhPubkey(renew: Boolean): PublicKey = dummyReceivePubkey override def getP2wpkhPubkey(renew: Boolean): PublicKey = dummyReceivePubkey
override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = Script.pay2tr(dummyReceivePubkey.xOnly)
} }
class SingleKeyOnChainWallet extends OnChainWallet with OnchainPubkeyCache { 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 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 { override def getReceivePublicKeyScript(addressType: Option[AddressType] = None)(implicit ec: ExecutionContext): Future[Seq[ScriptElt]] = Future.successful(addressType match {
case Some(AddressType.P2tr) => Future.successful(pubkey.xOnly.pub.p2trAddress(fr.acinq.bitcoin.Block.RegtestGenesisBlock.hash)) case Some(AddressType.P2wpkh) => script84
case _ => Future.successful(Bech32.encodeWitnessAddress("bcrt", 0, pubkey.hash160.toArray)) case _ => script86
} })
override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(pubkey) 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 doubleSpent(tx: Transaction)(implicit ec: ExecutionContext): Future[Boolean] = Future.successful(doubleSpent.contains(tx.txid))
override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey
override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = script86
} }
object DummyOnChainWallet { object DummyOnChainWallet {
val dummyReceiveAddress: String = "bcrt1qwcv8naajwn8fjhu8z59q9e6ucrqr068rlcenux"
val dummyReceivePubkey: PublicKey = PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12") val dummyReceivePubkey: PublicKey = PublicKey(hex"028feba10d0eafd0fad8fe20e6d9206e6bd30242826de05c63f459a00aced24b12")
def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = { def makeDummyFundingTx(pubkeyScript: ByteVector, amount: Satoshi): MakeFundingTxResponse = {

View file

@ -2,7 +2,7 @@ package fr.acinq.eclair.blockchain.bitcoind
import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps import akka.actor.typed.scaladsl.adapter.ClassicActorSystemOps
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 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.OnChainAddressGenerator
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType
import fr.acinq.eclair.{TestKitBaseClass, randomKey} import fr.acinq.eclair.{TestKitBaseClass, randomKey}
@ -15,16 +15,24 @@ import scala.concurrent.{ExecutionContext, Future}
class OnchainPubkeyRefresherSpec extends TestKitBaseClass with AnyFunSuiteLike { class OnchainPubkeyRefresherSpec extends TestKitBaseClass with AnyFunSuiteLike {
test("renew onchain scripts") { test("renew onchain scripts") {
val finalPubkey = new AtomicReference[PublicKey](randomKey().publicKey) val finalPubkey = new AtomicReference[PublicKey](randomKey().publicKey)
val finalPubkeyScript = new AtomicReference[Seq[ScriptElt]](Script.pay2tr(randomKey().xOnlyPublicKey()))
val generator = new OnChainAddressGenerator { 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) override def getP2wpkhPubkey()(implicit ec: ExecutionContext): Future[Crypto.PublicKey] = Future.successful(randomKey().publicKey)
} }
val manager = system.spawnAnonymous(OnchainPubkeyRefresher(generator, finalPubkey, 3 seconds)) val manager = system.spawnAnonymous(OnchainPubkeyRefresher(generator, finalPubkey, finalPubkeyScript, 3 seconds))
// renew pubkey explicitly
// renew script explicitly
val currentPubkey = finalPubkey.get() val currentPubkey = finalPubkey.get()
manager ! OnchainPubkeyRefresher.Renew manager ! OnchainPubkeyRefresher.Renew
awaitCond(finalPubkey.get() != currentPubkey) awaitCond(finalPubkey.get() != currentPubkey)
// renew pubkey explicitly
val currentPubkeyScript = finalPubkeyScript.get()
manager ! OnchainPubkeyRefresher.RenewPubkeyScript
awaitCond(finalPubkeyScript.get() != currentPubkeyScript)
} }
} }

View file

@ -22,7 +22,7 @@ import akka.pattern.pipe
import akka.testkit.{TestFSMRef, TestProbe} import akka.testkit.{TestFSMRef, TestProbe}
import com.softwaremill.quicklens.ModifyPimp import com.softwaremill.quicklens.ModifyPimp
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 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.NotificationsLogger.NotifyNodeOperator
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.BitcoindService
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
@ -129,8 +129,14 @@ class ReplaceableTxPublisherSpec extends TestKitBaseClass with AnyFunSuiteLike w
getP2wpkhPubkey().pipeTo(probe.ref) getP2wpkhPubkey().pipeTo(probe.ref)
probe.expectMsgType[PublicKey] probe.expectMsgType[PublicKey]
} }
val pubkeyScript = {
getReceivePublicKeyScript(None).pipeTo(probe.ref)
probe.expectMsgType[Seq[ScriptElt]]
}
override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey
override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = pubkeyScript
} }
(walletRpcClient, walletClient) (walletRpcClient, walletClient)
@ -1848,8 +1854,14 @@ class ReplaceableTxPublisherWithEclairSignerSpec extends ReplaceableTxPublisherS
getP2wpkhPubkey().pipeTo(probe.ref) getP2wpkhPubkey().pipeTo(probe.ref)
probe.expectMsgType[PublicKey] probe.expectMsgType[PublicKey]
} }
lazy val pubkeyScript = {
getReceivePublicKeyScript(None).pipeTo(probe.ref)
probe.expectMsgType[Seq[ScriptElt]]
}
override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey override def getP2wpkhPubkey(renew: Boolean): PublicKey = pubkey
override def getReceivePubkeyScript(renew: Boolean): Seq[ScriptElt] = pubkeyScript
} }
createEclairBackedWallet(walletRpcClient, keyManager) createEclairBackedWallet(walletRpcClient, keyManager)

View file

@ -24,7 +24,7 @@ import akka.testkit.TestProbe
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.ScriptFlags import fr.acinq.bitcoin.ScriptFlags
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey 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.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, JsonRPCError} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BitcoinCoreClient, JsonRPCError}
import fr.acinq.eclair.channel._ 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))) 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 val dataC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data
assert(dataC.commitments.params.commitmentFormat == commitmentFormat) 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))) 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 val dataF = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data
assert(dataF.commitments.params.commitmentFormat == commitmentFormat) 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) 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 // 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.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]] 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 // we prepare the revoked transactions F will publish
val keyManagerF = nodes("F").nodeParams.channelKeyManager val keyManagerF = nodes("F").nodeParams.channelKeyManager
val channelKeyPathF = keyManagerF.keyPath(commitmentsF.params.localParams, commitmentsF.params.channelConfig) 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))) 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 commitmentsC = sender.expectMsgType[RES_GET_CHANNEL_DATA[DATA_NORMAL]].data.commitments
val fundingOutpoint = commitmentsC.latest.commitInput.outPoint val fundingOutpoint = commitmentsC.latest.commitInput.outPoint
val finalPubKeyScriptC = Script.write(Script.pay2wpkh(nodes("C").wallet.getP2wpkhPubkey(false))) val finalPubKeyScriptC = Helpers.Closing.MutualClose.generateFinalScriptPubKey(nodes("C").wallet, true, false)
val finalPubKeyScriptF = Script.write(Script.pay2wpkh(nodes("F").wallet.getP2wpkhPubkey(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)) fundee.register ! Register.Forward(sender.ref.toTyped[Any], channelId, CMD_CLOSE(sender.ref, None, None))
sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]] sender.expectMsgType[RES_SUCCESS[CMD_CLOSE]]