Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

## Major changes

<insert changes>
### Remove support for non-anchor channels

We remove the code used to support legacy channels that don't use anchor outputs or taproot.
If you still have such channels, eclair won't start: you will need to close those channels, and will only be able to update eclair once they have been successfully closed.

### Configuration changes

Expand Down
10 changes: 4 additions & 6 deletions eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ eclair {
// - unlock: eclair will automatically unlock the corresponding utxos
// - ignore: eclair will leave these utxos locked and start
startup-locked-utxos-behavior = "stop"
final-pubkey-refresh-delay = 3 seconds
final-address-refresh-delay = 3 seconds
// If true, eclair will poll bitcoind for 30 seconds at start-up before giving up.
wait-for-bitcoind-up = true
// The txid of a transaction that exists in your custom signet network or "" to skip this check.
Expand Down Expand Up @@ -87,7 +87,7 @@ eclair {
// node that you trust using override-init-features (see below).
option_zeroconf = disabled
keysend = disabled
option_simple_close=optional
option_simple_close = optional
trampoline_payment_prototype = disabled
async_payment_prototype = disabled
on_the_fly_funding = disabled
Expand Down Expand Up @@ -177,8 +177,6 @@ eclair {
channel-opener-whitelist = [] // a list of public keys; we will ignore rate limits on pending channels from these peers
}

accept-incoming-static-remote-key-channels = false // whether we accept new incoming static_remote_key channels (which are obsolete, nodes should use anchor_output now)

quiescence-timeout = 1 minutes // maximum time we will stay quiescent (or wait to reach quiescence) before disconnecting

channel-update {
Expand Down Expand Up @@ -292,8 +290,8 @@ eclair {
max-closing-feerate = 10

feerate-tolerance {
ratio-low = 0.5 // will allow remote fee rates as low as half our local feerate (only enforced when not using anchor outputs)
ratio-high = 10.0 // will allow remote fee rates as high as 10 times our local feerate (for all commitment formats)
ratio-low = 0.5 // will allow remote fee rates as low as half our local feerate for funding/splice transactions
ratio-high = 10.0 // will allow remote fee rates as high as 10 times our local feerate for commitment transactions and funding/splice transactions
// when using anchor outputs, we only need to use a commitment feerate that allows the tx to propagate: we will use CPFP to speed up confirmation if needed.
// the following value is the maximum feerate we'll use for our commit tx (in sat/byte)
anchor-output-max-commit-feerate = 10
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/scala/fr/acinq/eclair/DBChecker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ object DBChecker extends Logging {
case _ => ()
}
channels
case Failure(_) => throw IncompatibleDBException
case Failure(t) => throw IncompatibleDBException(t)
}
}

Expand Down
20 changes: 11 additions & 9 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{Block, BlockHash, Crypto, Satoshi, SatoshiLong}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.ChannelFlags
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.{BalanceThreshold, ChannelConf, UnhandledExceptionStrategy}
import fr.acinq.eclair.channel.{ChannelFlags, ChannelTypes}
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager, OnChainKeyManager}
import fr.acinq.eclair.db._
Expand All @@ -39,7 +39,7 @@ import fr.acinq.eclair.router.Graph.HeuristicsConstants
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.router.{Graph, PathFindingExperimentConf, Router}
import fr.acinq.eclair.tor.Socks5ProxyParams
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions.{PhoenixSimpleTaprootChannelCommitmentFormat, ZeroFeeHtlcTxAnchorOutputsCommitmentFormat, ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat}
import fr.acinq.eclair.wire.protocol._
import grizzled.slf4j.Logging
import scodec.bits.ByteVector
Expand Down Expand Up @@ -129,14 +129,17 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
max = (fundingFeerate * feerateTolerance.ratioHigh).max(minimumFeerate),
)
// We use the most likely commitment format, even though there is no guarantee that this is the one that will be used.
val commitmentFormat = ChannelTypes.defaultFromFeatures(localFeatures, remoteFeatures, announceChannel = false).commitmentFormat
val commitmentFormat = if (Features.canUseFeature(localFeatures, remoteFeatures, Features.SimpleTaprootChannelsPhoenix)) {
PhoenixSimpleTaprootChannelCommitmentFormat
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.SimpleTaprootChannelsStaging)) {
ZeroFeeHtlcTxSimpleTaprootChannelCommitmentFormat
} else {
ZeroFeeHtlcTxAnchorOutputsCommitmentFormat
}
val commitmentFeerate = onChainFeeConf.getCommitmentFeerate(currentBitcoinCoreFeerates, remoteNodeId, commitmentFormat)
val commitmentRange = RecommendedFeeratesTlv.CommitmentFeerateRange(
min = (commitmentFeerate * feerateTolerance.ratioLow).max(minimumFeerate),
max = (commitmentFormat match {
case Transactions.DefaultCommitmentFormat => commitmentFeerate * feerateTolerance.ratioHigh
case _: Transactions.AnchorOutputsCommitmentFormat | _: Transactions.SimpleTaprootChannelCommitmentFormat => (commitmentFeerate * feerateTolerance.ratioHigh).max(feerateTolerance.anchorOutputMaxCommitFeerate)
}).max(minimumFeerate),
min = Seq(commitmentFeerate * feerateTolerance.ratioLow, minimumFeerate).max,
max = Seq(commitmentFeerate * feerateTolerance.ratioHigh, feerateTolerance.anchorOutputMaxCommitFeerate, minimumFeerate).max,
)
RecommendedFeerates(chainHash, fundingFeerate, commitmentFeerate, TlvStream(fundingRange, commitmentRange))
}
Expand Down Expand Up @@ -599,7 +602,6 @@ object NodeParams extends Logging {
quiescenceTimeout = FiniteDuration(config.getDuration("channel.quiescence-timeout").getSeconds, TimeUnit.SECONDS),
balanceThresholds = config.getConfigList("channel.channel-update.balance-thresholds").asScala.map(conf => BalanceThreshold(Satoshi(conf.getLong("available-sat")), Satoshi(conf.getLong("max-htlc-sat")))).toSeq,
minTimeBetweenUpdates = FiniteDuration(config.getDuration("channel.channel-update.min-time-between-updates").getSeconds, TimeUnit.SECONDS),
acceptIncomingStaticRemoteKeyChannels = config.getBoolean("channel.accept-incoming-static-remote-key-channels")
),
onChainFeeConf = OnChainFeeConf(
feeTargets = feeTargets,
Expand Down
19 changes: 5 additions & 14 deletions eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -268,24 +268,17 @@ 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)
finalAddressRefreshDelay = FiniteDuration(config.getDuration("bitcoind.final-address-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.
// 2) there is an `eclair-signer.conf` file in Eclair's data directory, but the name of the wallet set in `eclair-signer.conf` does not match the `eclair.bitcoind.wallet` setting in `eclair.conf`.
// Eclair will use the wallet set in `eclair.conf` and will not manage Bitcoin core keys (here we don't set an optional onchain key manager in our bitcoin client) BUT its API will return bitcoin core descriptors.
// This is how you would create a new bitcoin wallet whose private keys are managed by Eclair.
// 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[OnChainAddressRefresher.Command] = system.spawn(Behaviors.supervise(OnChainAddressRefresher(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 ! OnChainAddressRefresher.RenewPubkey
key
}
bitcoinClient = new BitcoinCoreClient(bitcoin, nodeParams.liquidityAdsConfig.lockUtxos, if (bitcoin.wallet == onChainKeyManager_opt.map(_.walletName)) onChainKeyManager_opt else None) with OnChainAddressCache {
val refresher: typed.ActorRef[OnChainAddressRefresher.Command] = system.spawn(Behaviors.supervise(OnChainAddressRefresher(this, finalPubkeyScript, finalAddressRefreshDelay)).onFailure(typed.SupervisorStrategy.restart), name = "on-chain-address-manager")

override def getReceivePublicKeyScript(renew: Boolean): Seq[ScriptElt] = {
val script = finalPubkeyScript.get()
Expand All @@ -294,8 +287,6 @@ class Setup(val datadir: File,
}
}
_ = if (bitcoinClient.useEclairSigner) logger.info("using eclair to sign bitcoin core transactions")
initialPubkey <- bitcoinClient.getP2wpkhPubkey()
_ = finalPubkey.set(initialPubkey)
// We use the default address type configured on the Bitcoin Core node.
initialPubkeyScript <- bitcoinClient.getReceivePublicKeyScript(addressType_opt = None)
_ = finalPubkeyScript.set(initialPubkeyScript)
Expand Down Expand Up @@ -494,7 +485,7 @@ case class Kit(nodeParams: NodeParams,
postman: typed.ActorRef[Postman.Command],
offerManager: typed.ActorRef[OfferManager.Command],
defaultOfferHandler: typed.ActorRef[OfferManager.HandlerCommand],
wallet: OnChainWallet with OnChainPubkeyCache)
wallet: OnChainWallet with OnChainAddressCache)

object Kit {

Expand All @@ -518,7 +509,7 @@ case class BitcoinWalletNotLoadedException(wallet: String, loaded: List[String])

case object EmptyAPIPasswordException extends RuntimeException("must set a password for the json-rpc api")

case object IncompatibleDBException extends RuntimeException("database is not compatible with this version of eclair")
case class IncompatibleDBException(t: Throwable) extends RuntimeException(s"database is not compatible with this version of eclair: ${t.getMessage}")

case object IncompatibleNetworkDBException extends RuntimeException("network database is not compatible with this version of eclair")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
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, ScriptElt, Transaction, TxId}
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient.AddressType
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
Expand Down Expand Up @@ -120,18 +119,10 @@ trait OnChainAddressGenerator {
/** Generate the public key script for a new wallet address. */
def getReceivePublicKeyScript(addressType_opt: 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]

}

/** A caching layer for [[OnChainAddressGenerator]] that provides synchronous access to wallet addresses and keys. */
trait OnChainPubkeyCache {

/**
* @param renew applies after requesting the current pubkey, and is asynchronous.
*/
def getP2wpkhPubkey(renew: Boolean): PublicKey
trait OnChainAddressCache {

/**
* @param renew applies after requesting the current script, and is asynchronous.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ 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.{Script, ScriptElt}
import fr.acinq.eclair.blockchain.OnChainAddressGenerator

Expand All @@ -19,26 +18,23 @@ object OnChainAddressRefresher {

// @formatter:off
sealed trait Command
case object RenewPubkey extends Command
case object RenewPubkeyScript extends Command
private case class SetPubkey(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], finalPubkeyScript: AtomicReference[Seq[ScriptElt]], delay: FiniteDuration): Behavior[Command] = {
def apply(generator: OnChainAddressGenerator, finalPubkeyScript: AtomicReference[Seq[ScriptElt]], delay: FiniteDuration): Behavior[Command] = {
Behaviors.setup { context =>
Behaviors.withTimers { timers =>
val refresher = new OnChainAddressRefresher(generator, finalPubkey, finalPubkeyScript, context, timers, delay)
val refresher = new OnChainAddressRefresher(generator, finalPubkeyScript, context, timers, delay)
refresher.idle()
}
}
}
}

private class OnChainAddressRefresher(generator: OnChainAddressGenerator,
finalPubkey: AtomicReference[PublicKey],
finalPubkeyScript: AtomicReference[Seq[ScriptElt]],
context: ActorContext[OnChainAddressRefresher.Command],
timers: TimerScheduler[OnChainAddressRefresher.Command], delay: FiniteDuration) {
Expand All @@ -47,13 +43,6 @@ private class OnChainAddressRefresher(generator: OnChainAddressGenerator,

/** In that state, we're ready to renew our on-chain address whenever requested. */
def idle(): Behavior[Command] = Behaviors.receiveMessage {
case RenewPubkey =>
context.log.debug("renewing pubkey (current={})", finalPubkey.get())
context.pipeToSelf(generator.getP2wpkhPubkey()) {
case Success(pubkey) => SetPubkey(pubkey)
case Failure(reason) => Error(reason)
}
renewing()
case RenewPubkeyScript =>
context.log.debug("renewing script (current={})", Script.write(finalPubkeyScript.get()).toHex)
context.pipeToSelf(generator.getReceivePublicKeyScript()) {
Expand All @@ -68,44 +57,28 @@ private class OnChainAddressRefresher(generator: OnChainAddressGenerator,

/** We ignore concurrent requests while waiting for bitcoind to respond. */
private def renewing(): Behavior[Command] = Behaviors.receiveMessage {
case SetPubkey(pubkey) =>
timers.startSingleTimer(Done, delay)
delaying(Some(pubkey), None)
case SetPubkeyScript(script) =>
timers.startSingleTimer(Done, delay)
delaying(None, Some(script))
delaying(script)
case Error(reason) =>
context.log.error("cannot renew public key or script", reason)
context.log.error("cannot renew script", reason)
idle()
case cmd =>
context.log.debug("ignoring command={} while waiting for bitcoin core's response", cmd)
Behaviors.same
}

/**
* After receiving our new script or pubkey from bitcoind, we wait before updating our current values.
* After receiving our new address from bitcoind, we wait before updating our current value.
* While waiting, we ignore additional requests to renew.
*
* This ensures that a burst of requests during a mass force-close use the same final on-chain address instead of
* creating a lot of address churn on our bitcoin wallet.
*
* Note that while we're updating our final script, we will ignore requests to update our final public key (and the
* other way around). This is fine, since the public key is only used:
* - when opening static_remotekey channels, which is disabled by default
* - when closing channels with peers that don't support shutdown_anysegwit (which should be widely supported)
*
* In practice, we most likely always use [[RenewPubkeyScript]].
*/
private def delaying(nextPubkey_opt: Option[PublicKey], nextScript_opt: Option[Seq[ScriptElt]]): Behavior[Command] = Behaviors.receiveMessage {
private def delaying(nextScript: Seq[ScriptElt]): Behavior[Command] = Behaviors.receiveMessage {
case Done =>
nextPubkey_opt.foreach { nextPubkey =>
context.log.info("setting pubkey to {}", nextPubkey)
finalPubkey.set(nextPubkey)
}
nextScript_opt.foreach { nextScript =>
context.log.info("setting script to {}", Script.write(nextScript).toHex)
finalPubkeyScript.set(nextScript)
}
context.log.info("setting script to {}", Script.write(nextScript).toHex)
finalPubkeyScript.set(nextScript)
idle()
case cmd =>
context.log.debug("rate-limiting command={}", cmd)
Expand Down
Loading