Skip to content

Commit

Permalink
Alternative to using a 0-depth watch confirmed
Browse files Browse the repository at this point in the history
Instead of using a watch with minDepth=0, we can directly skip the
wait_for_funding_confirmed state when using 0-conf, which is less hacky.
  • Loading branch information
t-bast committed Jun 13, 2022
1 parent cfd6514 commit 869175b
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 165 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,17 @@ import akka.actor.typed.eventstream.EventStream
import akka.actor.typed.scaladsl.{ActorContext, Behaviors, TimerScheduler}
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}
import fr.acinq.bitcoin.scalacompat._
import fr.acinq.eclair.RealShortChannelId
import fr.acinq.eclair.blockchain.Monitoring.Metrics
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.rpc.BitcoinCoreClient
import fr.acinq.eclair.blockchain.watchdogs.BlockchainWatchdog
import fr.acinq.eclair.wire.protocol.ChannelAnnouncement
import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, ShortChannelId, TimestampSecond}
import fr.acinq.eclair.{BlockHeight, KamonExt, NodeParams, RealShortChannelId, TimestampSecond}

import java.util.concurrent.atomic.AtomicLong
import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Random, Success}
import scala.util.{Failure, Success}

/**
* Created by PM on 21/02/2016.
Expand Down Expand Up @@ -237,11 +236,6 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
case _: WatchConfirmed[_] => // nothing to do
case _: WatchFundingLost => // nothing to do
}
watches
.collect {
case w: WatchFundingConfirmed if w.minDepth == 0 && w.txId == tx.txid =>
checkConfirmed(w)
}
Behaviors.same

case ProcessNewBlock(blockHash) =>
Expand Down Expand Up @@ -412,21 +406,13 @@ private class ZmqWatcher(nodeParams: NodeParams, blockHeight: AtomicLong, client
client.getTxConfirmations(w.txId).flatMap {
case Some(confirmations) if confirmations >= w.minDepth =>
client.getTransaction(w.txId).flatMap { tx =>
w match {
case w: WatchFundingConfirmed if confirmations == 0 =>
// if the tx doesn't have confirmations but we don't require any, we reply with a fake block index
// otherwise, we get the real short id
context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(BlockHeight(0), 0, tx))
Future.successful((): Unit)
case _ =>
client.getTransactionShortId(w.txId).map {
case (height, index) => w match {
case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, index, tx))
case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, index, tx))
case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, index, tx))
case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, index, tx))
}
}
client.getTransactionShortId(w.txId).map {
case (height, index) => w match {
case w: WatchFundingConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchFundingConfirmedTriggered(height, index, tx))
case w: WatchFundingDeeplyBuried => context.self ! TriggerEvent(w.replyTo, w, WatchFundingDeeplyBuriedTriggered(height, index, tx))
case w: WatchTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchTxConfirmedTriggered(height, index, tx))
case w: WatchParentTxConfirmed => context.self ! TriggerEvent(w.replyTo, w, WatchParentTxConfirmedTriggered(height, index, tx))
}
}
}
case _ => Future.successful((): Unit)
Expand Down
14 changes: 7 additions & 7 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -291,11 +291,11 @@ object Helpers {
* wait for one conf, except if the channel has the zero-conf feature (because presumably the peer will send an
* alias in that case).
*/
def minDepthFunder(channelFeatures: ChannelFeatures): Long = {
def minDepthFunder(channelFeatures: ChannelFeatures): Option[Long] = {
if (channelFeatures.hasFeature(Features.ZeroConf)) {
0
None
} else {
1
Some(1)
}
}

Expand All @@ -306,14 +306,14 @@ object Helpers {
* @param fundingSatoshis funding amount of the channel
* @return number of confirmations needed
*/
def minDepthFundee(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingSatoshis: Satoshi): Long = fundingSatoshis match {
case _ if channelFeatures.hasFeature(Features.ZeroConf) => 0 // zero-conf stay zero-conf, whatever the funding amount is
case funding if funding <= Channel.MAX_FUNDING => channelConf.minDepthBlocks
def minDepthFundee(channelConf: ChannelConf, channelFeatures: ChannelFeatures, fundingSatoshis: Satoshi): Option[Long] = fundingSatoshis match {
case _ if channelFeatures.hasFeature(Features.ZeroConf) => None // zero-conf stay zero-conf, whatever the funding amount is
case funding if funding <= Channel.MAX_FUNDING => Some(channelConf.minDepthBlocks)
case funding =>
val blockReward = 6.25 // this is true as of ~May 2020, but will be too large after 2024
val scalingFactor = 15
val blocksToReachFunding = (((scalingFactor * funding.toBtc.toDouble) / blockReward).ceil + 1).toInt
channelConf.minDepthBlocks.max(blocksToReachFunding)
Some(channelConf.minDepthBlocks.max(blocksToReachFunding))
}

def makeFundingInputInfo(fundingTxId: ByteVector32, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -615,30 +615,31 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val

case Event(WatchFundingDeeplyBuriedTriggered(blockHeight, txIndex, fundingTx), d: DATA_NORMAL) if d.channelAnnouncement.isEmpty =>
val finalRealShortId = RealScidStatus.Final(RealShortChannelId(blockHeight, txIndex, d.commitments.commitInput.outPoint.index.toInt))
val shortIds1 = d.shortIds.copy(real = finalRealShortId)
log.info(s"funding tx is deeply buried at blockHeight=$blockHeight txIndex=$txIndex shortChannelId=${finalRealShortId.realScid}")
val shortIds1 = d.shortIds.copy(real = finalRealShortId)
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortIds1, remoteNodeId))
if (d.shortIds.real == RealScidStatus.Unknown) {
// this is a zero-conf channel and it is the first time we know for sure that the funding tx has been confirmed
context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, fundingTx))
}
if (!d.shortIds.real.toOption.contains(finalRealShortId.realScid)) {
log.info(s"setting final real scid: old=${d.shortIds.real} new=${finalRealShortId}")
// we announce the new shortChannelId
context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortIds1, remoteNodeId))
}
val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1)
// if the shortChannelId is different from the one we had before, we need to re-announce it
val channelUpdate1 = if (d.channelUpdate.shortChannelId != scidForChannelUpdate) {
log.info(s"using new scid in channel_update: old=${d.channelUpdate.shortChannelId} new=$scidForChannelUpdate")
// we re-announce the channelUpdate for the same reason
Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments))
} else d.channelUpdate
val localAnnSigs_opt = if (d.commitments.announceChannel) {
} else {
d.channelUpdate
}
if (d.commitments.announceChannel) {
// if channel is public we need to send our announcement_signatures in order to generate the channel_announcement
Some(Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, finalRealShortId.realScid))
} else None
// we use goto() instead of stay() because we want to fire transitions
goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() sending localAnnSigs_opt.toSeq
val localAnnSigs = Helpers.makeAnnouncementSignatures(nodeParams, d.commitments, finalRealShortId.realScid)
// we use goto() instead of stay() because we want to fire transitions
goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing() sending localAnnSigs
} else {
// we use goto() instead of stay() because we want to fire transitions
goto(NORMAL) using d.copy(shortIds = shortIds1, channelUpdate = channelUpdate1) storing()
}

case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel =>
// channels are publicly announced if both parties want it (defined as feature bit)
Expand Down Expand Up @@ -1326,7 +1327,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
Helpers.Funding.minDepthFundee(nodeParams.channelConf, d.commitments.channelFeatures, d.commitments.commitInput.txOut.amount)
}
// we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE
blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth)
require(minDepth.nonEmpty, "min_depth must be set since we're waiting for the funding tx to confirm")
blockchain ! WatchFundingConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth.get)
goto(WAIT_FOR_FUNDING_CONFIRMED)

case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_CHANNEL_READY) =>
Expand Down Expand Up @@ -1618,6 +1620,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
// We only send the channel_update directly to the peer if we are connected AND the channel hasn't been announced
val emitEvent_opt: Option[EmitLocalChannelEvent] = (state, nextState, stateData, nextStateData) match {
case (WAIT_FOR_INIT_INTERNAL, OFFLINE, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("restore", d, sendToPeer = false))
case (WAIT_FOR_FUNDING_CONFIRMED, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("initial", d, sendToPeer = true))
case (WAIT_FOR_CHANNEL_READY, NORMAL, _, d: DATA_NORMAL) => Some(EmitLocalChannelUpdate("initial", d, sendToPeer = true))
case (NORMAL, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("normal->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty))
case (SYNCING, NORMAL, d1: DATA_NORMAL, d2: DATA_NORMAL) if d1.channelUpdate != d2.channelUpdate || d1.channelAnnouncement != d2.channelAnnouncement => Some(EmitLocalChannelUpdate("syncing->normal", d2, sendToPeer = d2.channelAnnouncement.isEmpty))
Expand All @@ -1628,7 +1631,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
}
emitEvent_opt.foreach {
case EmitLocalChannelUpdate(reason, d, sendToPeer) =>
log.info(s"emitting channel update event: reason=$reason enabled=${d.channelUpdate.channelFlags.isEnabled} sendToPeer=${sendToPeer} realScid=${d.shortIds.real} channel_update={} channel_announcement={}", d.channelUpdate, d.channelAnnouncement.map(_ => "yes").getOrElse("no"))
log.info(s"emitting channel update event: reason=$reason enabled=${d.channelUpdate.channelFlags.isEnabled} sendToPeer=$sendToPeer realScid=${d.shortIds.real} channel_update={} channel_announcement={}", d.channelUpdate, d.channelAnnouncement.map(_ => "yes").getOrElse("no"))
val lcu = LocalChannelUpdate(self, d.channelId, d.shortIds, d.commitments.remoteParams.nodeId, d.channelAnnouncement, d.channelUpdate, d.commitments)
context.system.eventStream.publish(lcu)
if (sendToPeer) {
Expand All @@ -1644,7 +1647,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
(stateData, nextStateData) match {
// NORMAL->NORMAL, NORMAL->OFFLINE, SYNCING->NORMAL
case (d1: DATA_NORMAL, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = Some(d1.channelUpdate), d2)
// WAIT_FOR_CHANNEL_READY->NORMAL
// WAIT_FOR_FUNDING_CONFIRMED->NORMAL, WAIT_FOR_CHANNEL_READY->NORMAL
case (_: DATA_WAIT_FOR_FUNDING_CONFIRMED, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2)
case (_: DATA_WAIT_FOR_CHANNEL_READY, d2: DATA_NORMAL) => maybeEmitChannelUpdateChangedEvent(newUpdate = d2.channelUpdate, oldUpdate_opt = None, d2)
case _ => ()
}
Expand Down
Loading

0 comments on commit 869175b

Please sign in to comment.