Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return local channel alias in payment failures #2323

Merged
merged 6 commits into from
Jul 1, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
13 changes: 7 additions & 6 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ upgrading to this release.

### API changes

- `channelbalances` Retrieves information about the balances of all local channels. (#2196)
- `stop` Stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233)
- `channelbalances`: retrieves information about the balances of all local channels (#2196)
- `stop`: stops eclair. Please note that the recommended way of stopping eclair is simply to kill its process (#2233)
- `channelbalances` and `usablebalances` return a `shortIds` object instead of a single `shortChannelId` (#2323)

### Miscellaneous improvements and bug fixes

Expand All @@ -112,8 +113,8 @@ to your node.
The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings
and defaults:

* `eclair.channel.min-public-funding-satoshis = 100000`
* `eclair.channel.min-private-funding-satoshis = 100000`
- `eclair.channel.min-public-funding-satoshis = 100000`
- `eclair.channel.min-private-funding-satoshis = 100000`

If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these
new settings.
Expand All @@ -124,8 +125,8 @@ Expired incoming invoices that are unpaid will be searched for and purged from t
Thereafter searches for expired unpaid invoices to purge will run once every 24 hours. You can disable this feature, or
change the search interval with two new settings:

* `eclair.purge-expired-invoices.enabled = true
* `eclair.purge-expired-invoices.interval = 24 hours`
- `eclair.purge-expired-invoices.enabled = true
- `eclair.purge-expired-invoices.interval = 24 hours`

## Verifying signatures

Expand Down
41 changes: 21 additions & 20 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256}
import fr.acinq.bitcoin.scalacompat.Script._
import fr.acinq.bitcoin.scalacompat._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain.OnChainAddressGenerator
import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratePerKw}
import fr.acinq.eclair.channel.fsm.Channel
import fr.acinq.eclair.channel.fsm.Channel.{ChannelConf, REFRESH_CHANNEL_UPDATE_INTERVAL}
Expand All @@ -39,7 +38,6 @@ import fr.acinq.eclair.wire.protocol._
import scodec.bits.ByteVector

import scala.concurrent.duration._
import scala.concurrent.{Await, ExecutionContext}
import scala.util.{Failure, Success, Try}

/**
Expand Down Expand Up @@ -186,27 +184,30 @@ object Helpers {
}

/**
* The general rule is that we use remote_alias for our channel_update until the channel is publicly announced, and
* then we use the real scid.
*
* Private channels are handled like public channels that have not yet been announced, there is no special case.
*
* Decision tree:
* - received remote_alias from peer
* - before channel announcement: use remote_alias
* - after channel announcement: use real scid
* - no remote_alias from peer
* - min_depth > 0: use real scid (may change if reorg between min_depth and 6 conf)
* - min_depth = 0 (zero-conf): spec violation, our peer MUST send an alias when using zero-conf
* We use the real scid if the channel has been announced, otherwise we use our local alias.
*/
def scidForChannelUpdate(channelAnnouncement_opt: Option[ChannelAnnouncement], shortIds: ShortIds): ShortChannelId = {
channelAnnouncement_opt.map(_.shortChannelId) // we use the real "final" scid when it is publicly announced
.orElse(shortIds.remoteAlias_opt) // otherwise the remote alias
.orElse(shortIds.real.toOption) // if we don't have a remote alias, we use the real scid (which could change because the funding tx possibly has less than 6 confs here)
.getOrElse(throw new RuntimeException("this is a zero-conf channel and no alias was provided in channel_ready")) // if we don't have a real scid, it means this is a zero-conf channel and our peer must have sent an alias
def scidForChannelUpdate(channelAnnouncement_opt: Option[ChannelAnnouncement], localAlias: Alias): ShortChannelId = {
channelAnnouncement_opt.map(_.shortChannelId).getOrElse(localAlias)
}

def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds)
def scidForChannelUpdate(d: DATA_NORMAL): ShortChannelId = scidForChannelUpdate(d.channelAnnouncement, d.shortIds.localAlias)

/**
* If our peer sent us an alias, that's what we must use in the channel_update we send them to ensure they're able to
* match this update with the corresponding local channel. If they didn't send us an alias, it means we're not using
* 0-conf and we'll use the real scid.
*/
def channelUpdateForDirectPeer(nodeParams: NodeParams, channelUpdate: ChannelUpdate, shortIds: ShortIds): ChannelUpdate = {
shortIds.remoteAlias_opt match {
case Some(remoteAlias) => Announcements.signChannelUpdate(nodeParams.privateKey, channelUpdate.copy(shortChannelId = remoteAlias))
case None => shortIds.real.toOption match {
case Some(realScid) => Announcements.signChannelUpdate(nodeParams.privateKey, channelUpdate.copy(shortChannelId = realScid))
// This case is a spec violation: this is a 0-conf channel, so our peer MUST send their alias.
// They won't be able to match our channel_update with their local channel, too bad for them.
case None => channelUpdate
}
}
}

/**
* Compute the delay until we need to refresh the channel_update for our channel not to be considered stale by
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
// 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))
}
val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1)
val scidForChannelUpdate = Helpers.scidForChannelUpdate(d.channelAnnouncement, shortIds1.localAlias)
// 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")
Expand Down Expand Up @@ -659,7 +659,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
handleLocalError(InvalidAnnouncementSignatures(d.channelId, remoteAnnSigs), d, Some(remoteAnnSigs))
} else {
// we generate a new channel_update because the scid used may change if we were previously using an alias
val scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.shortIds)
val scidForChannelUpdate = Helpers.scidForChannelUpdate(Some(channelAnn), d.shortIds.localAlias)
val channelUpdate = 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))
// we use goto() instead of stay() because we want to fire transitions
goto(NORMAL) using d.copy(channelAnnouncement = Some(channelAnn), channelUpdate = channelUpdate) storing()
Expand Down Expand Up @@ -1640,7 +1640,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder, val
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) {
send(d.channelUpdate)
send(Helpers.channelUpdateForDirectPeer(nodeParams, d.channelUpdate, d.shortIds))
}
case EmitLocalChannelDown(d) =>
log.debug(s"emitting channel down event")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@ trait ChannelOpenSingleFunder extends FundingHandlers with ErrorHandlers {
}
log.info("shortIds: real={} localAlias={} remoteAlias={}", shortIds1.real.toOption.getOrElse("none"), shortIds1.localAlias, shortIds1.remoteAlias_opt.getOrElse("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 scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1)
val scidForChannelUpdate = Helpers.scidForChannelUpdate(channelAnnouncement_opt = None, shortIds1.localAlias)
log.info("using shortChannelId={} for initial channel_update", scidForChannelUpdate)
val relayFees = getRelayFees(nodeParams, remoteNodeId, d.commitments)
val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, scidForChannelUpdate, nodeParams.channelConf.expiryDelta, d.commitments.remoteParams.htlcMinimum, relayFees.feeBase, relayFees.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,10 @@ class ChannelRelay private(nodeParams: NodeParams,
private val nextNodeId_opt = channels.headOption.map(_._2.nextNodeId)

/** channel id explicitly requested in the onion payload */
private val requestedChannelId_opt = channels.find(_._2.channelUpdate.shortChannelId == r.payload.outgoingChannelId).map(_._1)
private val requestedChannelId_opt = channels.collectFirst {
case (channelId, channel) if channel.shortIds.localAlias == r.payload.outgoingChannelId => channelId
case (channelId, channel) if channel.shortIds.real.toOption.contains(r.payload.outgoingChannelId) => channelId
}

/**
* Select a channel to the same node to relay the payment to, that has the lowest capacity and balance and is
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ object ChannelRelayer {
case WrappedLocalChannelUpdate(lcu@LocalChannelUpdate(_, channelId, shortIds, remoteNodeId, _, channelUpdate, commitments)) =>
context.log.debug(s"updating local channel info for channelId=$channelId realScid=${shortIds.real} localAlias=${shortIds.localAlias} remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments)
val prevChannelUpdate = channels.get(channelId).map(_.channelUpdate)
val channel = Relayer.OutgoingChannel(remoteNodeId, channelUpdate, prevChannelUpdate, commitments)
val channel = Relayer.OutgoingChannel(shortIds, remoteNodeId, channelUpdate, prevChannelUpdate, commitments)
val channels1 = channels + (channelId -> channel)
val mappings = lcu.scidsForRouting.map(_ -> channelId).toMap
context.log.debug("adding mappings={} to channelId={}", mappings.keys.mkString(","), channelId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.PendingCommandsDb
import fr.acinq.eclair.payment._
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams, ShortChannelId}
import fr.acinq.eclair.{Logs, MilliSatoshi, NodeParams}
import grizzled.slf4j.Logging

import scala.concurrent.Promise
Expand Down Expand Up @@ -133,9 +133,10 @@ object Relayer extends Logging {
}

case class RelayForward(add: UpdateAddHtlc)
case class ChannelBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean)
case class ChannelBalance(remoteNodeId: PublicKey, shortIds: ShortIds, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean, isEnabled: Boolean)

sealed trait OutgoingChannelParams {
def channelId: ByteVector32
def channelUpdate: ChannelUpdate
def prevChannelUpdate: Option[ChannelUpdate]
}
Expand All @@ -146,11 +147,11 @@ 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, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) extends OutgoingChannelParams {
val channelId: ByteVector32 = commitments.channelId
case class OutgoingChannel(shortIds: ShortIds, nextNodeId: PublicKey, channelUpdate: ChannelUpdate, prevChannelUpdate: Option[ChannelUpdate], commitments: AbstractCommitments) extends OutgoingChannelParams {
override val channelId: ByteVector32 = commitments.channelId
def toChannelBalance: ChannelBalance = ChannelBalance(
remoteNodeId = nextNodeId,
shortChannelId = channelUpdate.shortChannelId,
shortIds = shortIds,
canSend = commitments.availableBalanceForSend,
canReceive = commitments.availableBalanceForReceive,
isPublic = commitments.announceChannel,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ package fr.acinq.eclair.router

import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey, sha256, verifySignature}
import fr.acinq.bitcoin.scalacompat.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering}
import fr.acinq.eclair.RealShortChannelId
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult}
import fr.acinq.eclair.{CltvExpiryDelta, Feature, Features, MilliSatoshi, NodeFeature, RealShortChannelId, ShortChannelId, TimestampSecond, TimestampSecondLong, serializationResult}
import scodec.bits.ByteVector
import shapeless.HNil

Expand Down Expand Up @@ -118,11 +117,8 @@ object Announcements {
def makeChannelUpdate(chainHash: ByteVector32, nodeSecret: PrivateKey, remoteNodeId: PublicKey, shortChannelId: ShortChannelId, cltvExpiryDelta: CltvExpiryDelta, htlcMinimumMsat: MilliSatoshi, feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long, htlcMaximumMsat: MilliSatoshi, enable: Boolean = true, timestamp: TimestampSecond = TimestampSecond.now()): ChannelUpdate = {
val channelFlags = ChannelUpdate.ChannelFlags(isNode1 = isNode1(nodeSecret.publicKey, remoteNodeId), isEnabled = enable)
val htlcMaximumMsatOpt = Some(htlcMaximumMsat)

val witness = channelUpdateWitnessEncode(chainHash, shortChannelId, timestamp, channelFlags, cltvExpiryDelta, htlcMinimumMsat, feeBaseMsat, feeProportionalMillionths, htlcMaximumMsatOpt, TlvStream.empty)
val sig = Crypto.sign(witness, nodeSecret)
ChannelUpdate(
signature = sig,
val u = ChannelUpdate(
signature = ByteVector64.Zeroes,
chainHash = chainHash,
shortChannelId = shortChannelId,
timestamp = timestamp,
Expand All @@ -133,6 +129,13 @@ object Announcements {
feeProportionalMillionths = feeProportionalMillionths,
htlcMaximumMsat = htlcMaximumMsatOpt
)
signChannelUpdate(nodeSecret, u)
}

def signChannelUpdate(nodeSecret: PrivateKey, u: ChannelUpdate): ChannelUpdate = {
val witness = channelUpdateWitnessEncode(u.chainHash, u.shortChannelId, u.timestamp, u.channelFlags, u.cltvExpiryDelta, u.htlcMinimumMsat, u.feeBaseMsat, u.feeProportionalMillionths, u.htlcMaximumMsat, u.tlvStream)
val sig = Crypto.sign(witness, nodeSecret)
u.copy(signature = sig)
}

def checkSigs(ann: ChannelAnnouncement): Boolean = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {

// those features can only be enabled with AnchorOutputsZeroFeeHtlcTxs, this is to prevent incompatible test configurations
if (tags.contains(ChannelStateTestsTags.ZeroConf)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration")
if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(channelType.features.contains(Features.ScidAlias), "invalid test configuration")
if (tags.contains(ChannelStateTestsTags.ScidAlias)) assert(tags.contains(ChannelStateTestsTags.AnchorOutputsZeroFeeHtlcTxs), "invalid test configuration")

implicit val ec: scala.concurrent.ExecutionContext = scala.concurrent.ExecutionContext.global
val aliceParams = Alice.channelParams
Expand Down
Loading