From b0716aea8839bb0593293e19fb6397a578365989 Mon Sep 17 00:00:00 2001 From: Anton Kumaigorodski Date: Mon, 19 Oct 2020 11:04:50 +0300 Subject: [PATCH] Make `Commitments` a trait (#1542) This allows easier implementation of Hosted Channels as a plugin. --- .../fr/acinq/eclair/channel/Channel.scala | 26 ++--- .../acinq/eclair/channel/ChannelEvents.scala | 6 +- .../fr/acinq/eclair/channel/Commitments.scala | 101 +++++++++++------- .../fr/acinq/eclair/channel/Register.scala | 2 +- .../acinq/eclair/payment/PaymentPacket.scala | 14 ++- .../eclair/payment/relay/ChannelRelay.scala | 4 +- .../relay/PostRestartHtlcCleaner.scala | 2 +- .../acinq/eclair/payment/relay/Relayer.scala | 2 +- .../scala/fr/acinq/eclair/router/Router.scala | 4 +- .../fr/acinq/eclair/router/Validation.scala | 6 +- .../eclair/transactions/CommitmentSpec.scala | 2 - .../channel/states/e/NormalStateSpec.scala | 5 +- .../eclair/payment/PaymentPacketSpec.scala | 3 +- .../fr/acinq/eclair/gui/GUIUpdater.scala | 6 +- .../controllers/ChannelPaneController.scala | 7 +- 15 files changed, 112 insertions(+), 78 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index b22b91b69c..dfc56d1458 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -193,7 +193,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(INPUT_RESTORED(data), _) => log.info(s"restoring channel channelId=${data.channelId}") - context.system.eventStream.publish(ChannelRestored(self, peer, remoteNodeId, data.commitments.localParams.isFunder, data.channelId, data)) + context.system.eventStream.publish(ChannelRestored(self, data.channelId, peer, remoteNodeId, data.commitments.localParams.isFunder, data.commitments)) data match { //NB: order matters! case closing: DATA_CLOSING if Closing.nothingAtStake(closing) => @@ -246,7 +246,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down val candidateChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, normal.channelUpdate.shortChannelId, nodeParams.expiryDelta, - normal.commitments.remoteParams.htlcMinimum, normal.channelUpdate.feeBaseMsat, normal.channelUpdate.feeProportionalMillionths, normal.commitments.localCommit.spec.totalFunds, enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) + normal.commitments.remoteParams.htlcMinimum, normal.channelUpdate.feeBaseMsat, normal.channelUpdate.feeProportionalMillionths, normal.commitments.capacity.toMilliSatoshi, enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) val channelUpdate1 = if (Announcements.areSame(candidateChannelUpdate, normal.channelUpdate)) { // if there was no configuration change we keep the existing channel update normal.channelUpdate @@ -460,8 +460,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // NB: we don't send a ChannelSignatureSent for the first commit log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") val fundingMinDepth = Helpers.minDepthForFunding(nodeParams, fundingAmount) - blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? - blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, fundingMinDepth, BITCOIN_FUNDING_DEPTHOK) + blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? + blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, fundingMinDepth, BITCOIN_FUNDING_DEPTHOK) context.system.scheduler.scheduleOnce(FUNDING_TIMEOUT_FUNDEE, self, BITCOIN_FUNDING_TIMEOUT) goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, now, None, Right(fundingSigned)) storing() sending fundingSigned } @@ -499,8 +499,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val now = System.currentTimeMillis.milliseconds.toSeconds context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") - blockchain ! WatchSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? - blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) + blockchain ! WatchSpent(self, commitInput.outPoint.txid, commitInput.outPoint.index.toInt, commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT) // TODO: should we wait for an acknowledgment from the watcher? + blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) log.info(s"committing txid=${fundingTx.txid}") // we will publish the funding tx only after the channel state has been written to disk because we want to @@ -598,7 +598,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, nodeParams.feeBase, nodeParams.feeProportionalMillionth, commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, nodeParams.feeBase, nodeParams.feeProportionalMillionth, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.schedule(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, interval = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None) storing() @@ -898,7 +898,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we need to re-announce this shortChannelId context.system.eventStream.publish(ShortChannelIdAssigned(self, d.channelId, shortChannelId, Some(d.shortChannelId))) // we re-announce the channelUpdate for the same reason - Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) + Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, 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) { // if channel is public we need to send our announcement_signatures in order to generate the channel_announcement @@ -945,14 +945,14 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => log.info("updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we use GOTO instead of stay because we want to fire transitions c.replyTo ! RES_SUCCESS(c, d.channelId) goto(NORMAL) using d.copy(channelUpdate = channelUpdate) storing() case Event(BroadcastChannelUpdate(reason), d: DATA_NORMAL) => val age = System.currentTimeMillis.milliseconds - d.channelUpdate.timestamp.seconds - val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = Helpers.aboveReserve(d.commitments)) + val channelUpdate1 = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) reason match { case Reconnected if Announcements.areSame(channelUpdate1, d.channelUpdate) && age < REFRESH_CHANNEL_UPDATE_INTERVAL => // we already sent an identical channel_update not long ago (flapping protection in case we keep being disconnected/reconnected) @@ -976,7 +976,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // if we have pending unsigned htlcs, then we cancel them and advertise the fact that the channel is now disabled val d1 = if (d.commitments.localChanges.proposed.collectFirst { case add: UpdateAddHtlc => add }.isDefined) { log.info(s"updating channel_update announcement (reason=disabled)") - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = false) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) d.commitments.localChanges.proposed.collect { case add: UpdateAddHtlc => relayer ! RES_ADD_SETTLED(d.commitments.originChannels(add.id), add, HtlcResult.Disconnected(channelUpdate)) } @@ -1445,7 +1445,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = false) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state c.replyTo ! RES_SUCCESS(c, d.channelId) stay using d.copy(channelUpdate = channelUpdate) storing() @@ -1981,7 +1981,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId if (Announcements.isEnabled(d.channelUpdate.channelFlags)) { // if the channel isn't disabled we generate a new channel_update log.info(s"updating channel_update announcement (reason=disabled)") - val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.localCommit.spec.totalFunds, enable = false) + val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, d.channelUpdate.feeBaseMsat, d.channelUpdate.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) // then we update the state and replay the request self forward c // we use goto to fire transitions diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala index 7d1d47eed1..b62cf8bf4f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelEvents.scala @@ -32,13 +32,13 @@ trait ChannelEvent case class ChannelCreated(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, temporaryChannelId: ByteVector32, initialFeeratePerKw: FeeratePerKw, fundingTxFeeratePerKw: Option[FeeratePerKw]) extends ChannelEvent -case class ChannelRestored(channel: ActorRef, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, channelId: ByteVector32, currentData: HasCommitments) extends ChannelEvent +case class ChannelRestored(channel: ActorRef, channelId: ByteVector32, peer: ActorRef, remoteNodeId: PublicKey, isFunder: Boolean, currentData: AbstractCommitments) extends ChannelEvent case class ChannelIdAssigned(channel: ActorRef, remoteNodeId: PublicKey, temporaryChannelId: ByteVector32, channelId: ByteVector32) extends ChannelEvent case class ShortChannelIdAssigned(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, previousShortChannelId: Option[ShortChannelId]) extends ChannelEvent -case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: Commitments) extends ChannelEvent +case class LocalChannelUpdate(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey, channelAnnouncement_opt: Option[ChannelAnnouncement], channelUpdate: ChannelUpdate, commitments: AbstractCommitments) extends ChannelEvent case class LocalChannelDown(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, remoteNodeId: PublicKey) extends ChannelEvent @@ -53,7 +53,7 @@ case class ChannelErrorOccurred(channel: ActorRef, channelId: ByteVector32, remo case class NetworkFeePaid(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, tx: Transaction, fee: Satoshi, txType: String) extends ChannelEvent // NB: this event is only sent when the channel is available -case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, commitments: Commitments) extends ChannelEvent +case class AvailableBalanceChanged(channel: ActorRef, channelId: ByteVector32, shortChannelId: ShortChannelId, commitments: AbstractCommitments) extends ChannelEvent case class ChannelPersisted(channel: ActorRef, remoteNodeId: PublicKey, channelId: ByteVector32, data: Data) extends ChannelEvent diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 46616561cf..43cb2d9c60 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -18,16 +18,17 @@ package fr.acinq.eclair.channel import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto} +import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi} import fr.acinq.eclair.blockchain.fee.{FeeratePerKw, OnChainFeeConf} import fr.acinq.eclair.channel.Monitoring.Metrics -import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain, Sphinx} +import fr.acinq.eclair.crypto.{Generators, KeyManager, ShaChain} +import fr.acinq.eclair.payment.OutgoingPacket import fr.acinq.eclair.payment.relay.Relayer import fr.acinq.eclair.transactions.DirectedHtlc._ import fr.acinq.eclair.transactions.Transactions._ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{MilliSatoshi, _} +import fr.acinq.eclair._ import scala.util.{Failure, Success, Try} @@ -46,6 +47,30 @@ case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: ByteVector32, r case class WaitingForRevocation(nextRemoteCommit: RemoteCommit, sent: CommitSig, sentAfterLocalCommitIndex: Long, reSignAsap: Boolean = false) // @formatter:on +trait AbstractCommitments { + def getOutgoingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] + + def getIncomingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] + + def timedOutOutgoingHtlcs(blockheight: Long): Set[UpdateAddHtlc] + + def localNodeId: PublicKey + + def remoteNodeId: PublicKey + + def capacity: Satoshi + + def availableBalanceForReceive: MilliSatoshi + + def availableBalanceForSend: MilliSatoshi + + def originChannels: Map[Long, Origin] + + def channelId: ByteVector32 + + def announceChannel: Boolean +} + /** * about remoteNextCommitInfo: * we either: @@ -63,12 +88,10 @@ case class Commitments(channelVersion: ChannelVersion, originChannels: Map[Long, Origin], // for outgoing htlcs relayed through us, details about the corresponding incoming htlcs remoteNextCommitInfo: Either[WaitingForRevocation, PublicKey], commitInput: InputInfo, - remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) { + remotePerCommitmentSecrets: ShaChain, channelId: ByteVector32) extends AbstractCommitments { require(channelVersion.paysDirectlyToWallet == localParams.walletStaticPaymentBasepoint.isDefined, s"localParams.walletStaticPaymentBasepoint must be defined only for commitments that pay directly to our wallet (version=$channelVersion)") - val commitmentFormat: CommitmentFormat = channelVersion.commitmentFormat - def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty && remoteNextCommitInfo.isRight def hasPendingOrProposedHtlcs: Boolean = !hasNoPendingHtlcs || @@ -83,6 +106,22 @@ case class Commitments(channelVersion: ChannelVersion, remoteNextCommitInfo.left.toSeq.flatMap(_.nextRemoteCommit.spec.htlcs.collect(incoming).filter(expired).toSet) } + def getOutgoingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] = for { + localSigned <- remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(remoteCommit).spec.findIncomingHtlcById(htlcId) + remoteSigned <- localCommit.spec.findOutgoingHtlcById(htlcId) + } yield { + require(localSigned.add == remoteSigned.add) + localSigned.add + } + + def getIncomingHtlcCrossSigned(htlcId: Long): Option[UpdateAddHtlc] = for { + localSigned <- remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(remoteCommit).spec.findOutgoingHtlcById(htlcId) + remoteSigned <- localCommit.spec.findIncomingHtlcById(htlcId) + } yield { + require(localSigned.add == remoteSigned.add) + localSigned.add + } + /** * HTLCs that are close to timing out upstream are potentially dangerous. If we received the preimage for those HTLCs, * we need to get a remote signed updated commitment that removes those HTLCs. @@ -99,8 +138,16 @@ case class Commitments(channelVersion: ChannelVersion, def addRemoteProposal(proposal: UpdateMessage): Commitments = Commitments.addRemoteProposal(this, proposal) + val commitmentFormat: CommitmentFormat = channelVersion.commitmentFormat + + val localNodeId: PublicKey = localParams.nodeId + + val remoteNodeId: PublicKey = remoteParams.nodeId + val announceChannel: Boolean = (channelFlags & 0x01) != 0 + val capacity: Satoshi = commitInput.txOut.amount + // NB: when computing availableBalanceForSend and availableBalanceForReceive, the funder keeps an extra buffer on top // of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see // https://github.com/lightningnetwork/lightning-rfc/issues/728 for details). @@ -325,24 +372,8 @@ object Commitments { commitments1 } - def getOutgoingHtlcCrossSigned(commitments: Commitments, htlcId: Long): Option[UpdateAddHtlc] = for { - localSigned <- commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments.remoteCommit).spec.findIncomingHtlcById(htlcId) - remoteSigned <- commitments.localCommit.spec.findOutgoingHtlcById(htlcId) - } yield { - require(localSigned.add == remoteSigned.add) - localSigned.add - } - - def getIncomingHtlcCrossSigned(commitments: Commitments, htlcId: Long): Option[UpdateAddHtlc] = for { - localSigned <- commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit).getOrElse(commitments.remoteCommit).spec.findOutgoingHtlcById(htlcId) - remoteSigned <- commitments.localCommit.spec.findIncomingHtlcById(htlcId) - } yield { - require(localSigned.add == remoteSigned.add) - localSigned.add - } - def sendFulfill(commitments: Commitments, cmd: CMD_FULFILL_HTLC): Try[(Commitments, UpdateFulfillHtlc)] = - getIncomingHtlcCrossSigned(commitments, cmd.id) match { + commitments.getIncomingHtlcCrossSigned(cmd.id) match { case Some(htlc) if alreadyProposed(commitments.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Failure(UnknownHtlcId(commitments.channelId, cmd.id)) @@ -355,30 +386,20 @@ object Commitments { } def receiveFulfill(commitments: Commitments, fulfill: UpdateFulfillHtlc): Try[(Commitments, Origin, UpdateAddHtlc)] = - getOutgoingHtlcCrossSigned(commitments, fulfill.id) match { + commitments.getOutgoingHtlcCrossSigned(fulfill.id) match { case Some(htlc) if htlc.paymentHash == sha256(fulfill.paymentPreimage) => Try((addRemoteProposal(commitments, fulfill), commitments.originChannels(fulfill.id), htlc)) case Some(_) => Failure(InvalidHtlcPreimage(commitments.channelId, fulfill.id)) case None => Failure(UnknownHtlcId(commitments.channelId, fulfill.id)) } def sendFail(commitments: Commitments, cmd: CMD_FAIL_HTLC, nodeSecret: PrivateKey): Try[(Commitments, UpdateFailHtlc)] = - getIncomingHtlcCrossSigned(commitments, cmd.id) match { + commitments.getIncomingHtlcCrossSigned(cmd.id) match { case Some(htlc) if alreadyProposed(commitments.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Failure(UnknownHtlcId(commitments.channelId, cmd.id)) case Some(htlc) => // we need the shared secret to build the error packet - Sphinx.PaymentPacket.peel(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket) match { - case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => - val reason = cmd.reason match { - case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret) - case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure) - } - val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason) - val commitments1 = addLocalProposal(commitments, fail) - Success((commitments1, fail)) - case Left(_) => Failure(CannotExtractSharedSecret(commitments.channelId, htlc)) - } + OutgoingPacket.buildHtlcFailure(nodeSecret, cmd, htlc).map(fail => (addLocalProposal(commitments, fail), fail)) case None => Failure(UnknownHtlcId(commitments.channelId, cmd.id)) } @@ -387,7 +408,7 @@ object Commitments { if ((cmd.failureCode & FailureMessageCodecs.BADONION) == 0) { Failure(InvalidFailureCode(commitments.channelId)) } else { - getIncomingHtlcCrossSigned(commitments, cmd.id) match { + commitments.getIncomingHtlcCrossSigned(cmd.id) match { case Some(htlc) if alreadyProposed(commitments.localChanges.proposed, htlc.id) => // we have already sent a fail/fulfill for this htlc Failure(UnknownHtlcId(commitments.channelId, cmd.id)) @@ -401,7 +422,7 @@ object Commitments { } def receiveFail(commitments: Commitments, fail: UpdateFailHtlc): Try[(Commitments, Origin, UpdateAddHtlc)] = - getOutgoingHtlcCrossSigned(commitments, fail.id) match { + commitments.getOutgoingHtlcCrossSigned(fail.id) match { case Some(htlc) => Try((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id), htlc)) case None => Failure(UnknownHtlcId(commitments.channelId, fail.id)) } @@ -411,7 +432,7 @@ object Commitments { if ((fail.failureCode & FailureMessageCodecs.BADONION) == 0) { Failure(InvalidFailureCode(commitments.channelId)) } else { - getOutgoingHtlcCrossSigned(commitments, fail.id) match { + commitments.getOutgoingHtlcCrossSigned(fail.id) match { case Some(htlc) => Try((addRemoteProposal(commitments, fail), commitments.originChannels(fail.id), htlc)) case None => Failure(UnknownHtlcId(commitments.channelId, fail.id)) } @@ -727,4 +748,4 @@ object Commitments { |${commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit.spec.htlcs.map(h => s" ${h.direction} ${h.add.id} ${h.add.cltvExpiry}").mkString("\n")).getOrElse("N/A")}""".stripMargin } -} \ No newline at end of file +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala index f491c4f716..674e77a7be 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Register.scala @@ -40,7 +40,7 @@ class Register extends Actor with ActorLogging { context.watch(channel) context become main(channels + (temporaryChannelId -> channel), shortIds, channelsTo + (temporaryChannelId -> remoteNodeId)) - case ChannelRestored(channel, _, remoteNodeId, _, channelId, _) => + case ChannelRestored(channel, channelId, _, remoteNodeId, _, _) => context.watch(channel) context become main(channels + (channelId -> channel), shortIds, channelsTo + (channelId -> remoteNodeId)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala index b56e52e983..1bbf7f30fd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentPacket.scala @@ -22,7 +22,7 @@ import akka.actor.ActorRef import akka.event.LoggingAdapter import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.eclair.channel.{CMD_ADD_HTLC, Origin} +import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractSharedSecret, Origin} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop} import fr.acinq.eclair.wire._ @@ -31,6 +31,7 @@ import scodec.bits.ByteVector import scodec.{Attempt, DecodeResult} import scala.reflect.ClassTag +import scala.util.{Failure, Success, Try} /** * Created by t-bast on 08/10/2019. @@ -249,4 +250,15 @@ object OutgoingPacket { CMD_ADD_HTLC(replyTo, firstAmount, paymentHash, firstExpiry, onion.packet, Origin.Hot(replyTo, upstream), commit = true) -> onion.sharedSecrets } + def buildHtlcFailure(nodeSecret: PrivateKey, cmd: CMD_FAIL_HTLC, add: UpdateAddHtlc): Try[UpdateFailHtlc] = { + Sphinx.PaymentPacket.peel(nodeSecret, add.paymentHash, add.onionRoutingPacket) match { + case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => + val reason = cmd.reason match { + case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret) + case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure) + } + Success(UpdateFailHtlc(add.channelId, cmd.id, reason)) + case Left(_) => Failure(CannotExtractSharedSecret(add.channelId, add)) + } + } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala index add74f869c..195ff41930 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/ChannelRelay.scala @@ -207,7 +207,7 @@ class ChannelRelay private(nodeParams: NodeParams, candidateChannels .map { case (shortChannelId, channelInfo) => val relayResult = relayOrFail(Some(channelInfo.channelUpdate)) - context.log.debug(s"candidate channel: shortChannelId={} balanceMsat={} capacitySat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo.commitments.availableBalanceForSend, channelInfo.commitments.commitInput.txOut.amount, channelInfo.channelUpdate, relayResult) + context.log.debug(s"candidate channel: shortChannelId={} balanceMsat={} capacitySat={} channelUpdate={} relayResult={}", shortChannelId, channelInfo.commitments.availableBalanceForSend, channelInfo.commitments.capacity, channelInfo.channelUpdate, relayResult) (shortChannelId, channelInfo, relayResult) } .collect { @@ -218,7 +218,7 @@ class ChannelRelay private(nodeParams: NodeParams, // we want to use the channel with: // - the lowest available capacity to ensure we keep high-capacity channels for big payments // - the lowest available balance to increase our incoming liquidity - .sortBy { case (_, commitments) => (commitments.commitInput.txOut.amount, commitments.availableBalanceForSend) } + .sortBy { case (_, commitments) => (commitments.capacity, commitments.availableBalanceForSend) } .headOption match { case Some((preferredShortChannelId, commitments)) if preferredShortChannelId != requestedShortChannelId => context.log.info("replacing requestedShortChannelId={} by preferredShortChannelId={} with availableBalanceMsat={}", requestedShortChannelId, preferredShortChannelId, commitments.availableBalanceForSend) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala index 9948a57a15..ebc9b75c24 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/PostRestartHtlcCleaner.scala @@ -79,7 +79,7 @@ class PostRestartHtlcCleaner(nodeParams: NodeParams, register: ActorRef, initial val acked = brokenHtlcs.notRelayed .filter(_.add.channelId == data.channelId) // only consider htlcs coming from this channel .filter { - case IncomingHtlc(htlc, preimage_opt) if Commitments.getIncomingHtlcCrossSigned(data.commitments, htlc.id).isDefined => + case IncomingHtlc(htlc, preimage_opt) if data.commitments.getIncomingHtlcCrossSigned(htlc.id).isDefined => // this htlc is cross signed in the current commitment, we can settle it preimage_opt match { case Some(preimage) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index 70345fec09..2da5f668ce 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -121,7 +121,7 @@ object Relayer extends Logging { * @param enabledOnly if true, filter out disabled channels. */ case class GetOutgoingChannels(enabledOnly: Boolean = true) - case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, commitments: Commitments) { + case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, commitments: AbstractCommitments) { def toUsableBalance: UsableBalance = UsableBalance( remoteNodeId = nextNodeId, shortChannelId = channelUpdate.shortChannelId, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 0bd080380b..e3b553209d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -294,7 +294,7 @@ object Router { def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] = if (Announcements.isNode1(u.channelFlags)) meta_opt.map(_.balance1) else meta_opt.map(_.balance2) def updateChannelUpdateSameSideAs(u: ChannelUpdate): PublicChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) - def updateBalances(commitments: Commitments): PublicChannel = if (commitments.localParams.nodeId == ann.nodeId1) { + def updateBalances(commitments: AbstractCommitments): PublicChannel = if (commitments.localNodeId == ann.nodeId1) { copy(meta_opt = Some(ChannelMeta(commitments.availableBalanceForSend, commitments.availableBalanceForReceive))) } else { copy(meta_opt = Some(ChannelMeta(commitments.availableBalanceForReceive, commitments.availableBalanceForSend))) @@ -312,7 +312,7 @@ object Router { def getChannelUpdateSameSideAs(u: ChannelUpdate): Option[ChannelUpdate] = if (Announcements.isNode1(u.channelFlags)) update_1_opt else update_2_opt def getBalanceSameSideAs(u: ChannelUpdate): Option[MilliSatoshi] = if (Announcements.isNode1(u.channelFlags)) Some(meta.balance1) else Some(meta.balance2) def updateChannelUpdateSameSideAs(u: ChannelUpdate): PrivateChannel = if (Announcements.isNode1(u.channelFlags)) copy(update_1_opt = Some(u)) else copy(update_2_opt = Some(u)) - def updateBalances(commitments: Commitments): PrivateChannel = if (commitments.localParams.nodeId == nodeId1) { + def updateBalances(commitments: AbstractCommitments): PrivateChannel = if (commitments.localNodeId == nodeId1) { copy(meta = ChannelMeta(commitments.availableBalanceForSend, commitments.availableBalanceForReceive)) } else { copy(meta = ChannelMeta(commitments.availableBalanceForReceive, commitments.availableBalanceForSend)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 376a7e1b1d..959890baa5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -472,12 +472,12 @@ object Validation { } def handleAvailableBalanceChanged(d: Data, e: AvailableBalanceChanged)(implicit log: LoggingAdapter): Data = { - val desc = ChannelDesc(e.shortChannelId, e.commitments.localParams.nodeId, e.commitments.remoteParams.nodeId) + val desc = ChannelDesc(e.shortChannelId, e.commitments.localNodeId, e.commitments.remoteNodeId) val (publicChannels1, graph1) = d.channels.get(e.shortChannelId) match { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("public channel balance updated: {}", pc1) - val update_opt = if (e.commitments.localParams.nodeId == pc1.ann.nodeId1) pc1.update_1_opt else pc1.update_2_opt + val update_opt = if (e.commitments.localNodeId == pc1.ann.nodeId1) pc1.update_1_opt else pc1.update_2_opt val graph1 = update_opt.map(u => d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(d.graph) (d.channels + (e.shortChannelId -> pc1), graph1) case None => @@ -487,7 +487,7 @@ object Validation { case Some(pc) => val pc1 = pc.updateBalances(e.commitments) log.debug("private channel balance updated: {}", pc1) - val update_opt = if (e.commitments.localParams.nodeId == pc1.nodeId1) pc1.update_1_opt else pc1.update_2_opt + val update_opt = if (e.commitments.localNodeId == pc1.nodeId1) pc1.update_1_opt else pc1.update_2_opt val graph2 = update_opt.map(u => graph1.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u))).getOrElse(graph1) (d.privateChannels + (e.shortChannelId -> pc1), graph2) case None => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala index 228a1e243a..ae285b5a93 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/transactions/CommitmentSpec.scala @@ -75,8 +75,6 @@ final case class CommitmentSpec(htlcs: Set[DirectedHtlc], feeratePerKw: FeerateP def findIncomingHtlcById(id: Long): Option[IncomingHtlc] = htlcs.collectFirst { case htlc: IncomingHtlc if htlc.add.id == id => htlc } def findOutgoingHtlcById(id: Long): Option[OutgoingHtlc] = htlcs.collectFirst { case htlc: OutgoingHtlc if htlc.add.id == id => htlc } - - val totalFunds: MilliSatoshi = toLocal + toRemote + htlcs.toSeq.map(_.add.amountMsat).sum } object CommitmentSpec { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index 3d9f37042e..daf1909217 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -31,6 +31,7 @@ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.channel.{ChannelErrorOccurred, _} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.io.Peer +import fr.acinq.eclair.payment.OutgoingPacket import fr.acinq.eclair.payment.relay.Relayer._ import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc.{incoming, outgoing} @@ -1356,7 +1357,9 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - bob ! CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure)) + val cmd = CMD_FAIL_HTLC(htlc.id, Right(PermanentChannelFailure)) + assert(OutgoingPacket.buildHtlcFailure(Bob.nodeParams.privateKey, cmd, htlc).get.id === htlc.id) + bob ! cmd val fail = bob2alice.expectMsgType[UpdateFailHtlc] awaitCond(bob.stateData == initialState.copy( commitments = initialState.commitments.copy( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala index 6b53125a4b..21d0ca9df5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentPacketSpec.scala @@ -387,8 +387,9 @@ object PaymentPacketSpec { def makeCommitments(channelId: ByteVector32, testAvailableBalanceForSend: MilliSatoshi = 50000000 msat, testAvailableBalanceForReceive: MilliSatoshi = 50000000 msat, testCapacity: Satoshi = 100000 sat): Commitments = { val params = LocalParams(null, null, null, null, null, null, null, 0, isFunder = true, null, None, null) + val remoteParams = RemoteParams(randomKey.publicKey, null, null, null, null, null, maxAcceptedHtlcs = 0, null, null, null, null, null, null) val commitInput = InputInfo(OutPoint(randomBytes32, 1), TxOut(testCapacity, Nil), Nil) - new Commitments(ChannelVersion.STANDARD, params, null, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, commitInput, null, channelId) { + new Commitments(ChannelVersion.STANDARD, params, remoteParams, 0.toByte, null, null, null, null, 0, 0, Map.empty, null, commitInput, null, channelId) { override lazy val availableBalanceForSend: MilliSatoshi = testAvailableBalanceForSend.max(0 msat) override lazy val availableBalanceForReceive: MilliSatoshi = testAvailableBalanceForReceive.max(0 msat) } diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala index 069b5e312f..a64d72a016 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala @@ -79,16 +79,16 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging runInGuiThread(() => mainController.channelBox.getChildren.addAll(root)) context.become(main(m + (channel -> channelPaneController))) - case ChannelRestored(channel, peer, remoteNodeId, isFunder, channelId, currentData) => + case ChannelRestored(channel, channelId, peer, remoteNodeId, isFunder, currentData: Commitments) => // We are specifically interested in normal Commitments with funding txid here context.watch(channel) val (channelPaneController, root) = createChannelPanel(channel, peer, remoteNodeId, isFunder, channelId) - channelPaneController.updateBalance(currentData.commitments) + channelPaneController.updateBalance(currentData) val m1 = m + (channel -> channelPaneController) val totalBalance = m1.values.map(_.getBalance).sum runInGuiThread(() => { channelPaneController.refreshBalance() mainController.refreshTotalBalance(totalBalance) - channelPaneController.txId.setText(currentData.commitments.commitInput.outPoint.txid.toHex) + channelPaneController.txId.setText(currentData.commitInput.outPoint.txid.toHex) mainController.channelBox.getChildren.addAll(root) }) context.become(main(m1)) diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala index 7722f34afb..0df97641d2 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/ChannelPaneController.scala @@ -18,9 +18,8 @@ package fr.acinq.eclair.gui.controllers import akka.actor.ActorRef import com.google.common.base.Strings -import fr.acinq.eclair.MilliSatoshi -import fr.acinq.eclair.CoinUtils -import fr.acinq.eclair.channel.{CMD_CLOSE, CMD_FORCECLOSE, Commitments} +import fr.acinq.eclair._ +import fr.acinq.eclair.channel.{CMD_CLOSE, CMD_FORCECLOSE, AbstractCommitments, Commitments} import fr.acinq.eclair.gui.FxApp import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction} import grizzled.slf4j.Logging @@ -130,7 +129,7 @@ class ChannelPaneController(val channelRef: ActorRef, val peerNodeId: String) ex def updateBalance(commitments: Commitments) { balance = commitments.localCommit.spec.toLocal - capacity = commitments.localCommit.spec.totalFunds + capacity = commitments.capacity.toMilliSatoshi } def refreshBalance(): Unit = {