Skip to content

Commit

Permalink
Add support for zero-conf and scid-alias (#2224)
Browse files Browse the repository at this point in the history
This implements lightning/bolts#910.

Co-authored-by: Bastien Teinturier <31281497+t-bast@users.noreply.github.com>
  • Loading branch information
pm47 and t-bast committed Jun 15, 2022
1 parent e8c9df4 commit e5f5cd1
Show file tree
Hide file tree
Showing 110 changed files with 2,804 additions and 1,382 deletions.
111 changes: 99 additions & 12 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,88 @@

## Major changes

Dropped support for version 2 of Tor protocol. That means
### Add support for channel aliases and zeroconf channels

:information_source: Those features are only supported for channels of type `AnchorOutputsZeroFeeHtlcTx`, which is the
newest channel type and the one enabled by default. If you are opening a channel with a node that doesn't run Eclair,
make sure they support `option_anchors_zero_fee_htlc_tx`.

#### Channel aliases

Channel aliases offer a way to use arbitrary channel identifiers for routing. This feature improves privacy by not
leaking the funding transaction of the channel during payments.

This feature is enabled by default, but your peer has to support it too, and it is not compatible with public channels.

#### Zeroconf channels

Zeroconf channels make it possible to use a newly created channel before the funding tx is confirmed on the blockchain.

:warning: Zeroconf requires the fundee to trust the funder. For this reason it is disabled by default, and you should
only enable it on a peer-by-peer basis.

##### Enabling through features

Below is how to enable zeroconf with a given peer in `eclair.conf`. With this config, your node will _accept_ zeroconf
channels from node `03864e...`.

```eclair.conf
override-init-features = [
{
nodeid = "03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f",
features = {
// these features need to be enabled
var_onion_optin = mandatory
payment_secret = mandatory
option_channel_type = optional
// dependencies of zeroconf
option_static_remotekey = optional
option_anchors_zero_fee_htlc_tx = optional
option_scid_alias = optional
// enable zeroconf
option_zeroconf = optional
}
}
]
```

Note that, as funder, Eclair will happily use an unconfirmed channel if the peer sends an early `channel_ready`, even if
the `option_zeroconf` feature isn't enabled, as long as the peer provides a channel alias.

##### Enabling through channel type

You can enable `option_scid_alias` and `option_zeroconf` features by requesting them in the channel type, even if those
options aren't enabled in your features.

The new channel types variations are:

- `anchor_outputs_zero_fee_htlc_tx+scid_alias`
- `anchor_outputs_zero_fee_htlc_tx+zeroconf`
- `anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf`

Examples using the command-line interface:

- open a public zeroconf channel:

```shell
$ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+zeroconf --announceChannel=true
```

- open a private zeroconf channel with aliases:

```shell
$ ./eclair-cli open --nodeId=03864e... --fundingSatoshis=100000 --channelType=anchor_outputs_zero_fee_htlc_tx+scid_alias+zeroconf --announceChannel=false
```

### Remove support for Tor v2

Dropped support for version 2 of Tor protocol. That means:

- Eclair can't open control connection to Tor daemon version 0.3.3.5 and earlier anymore
- Eclair can't create hidden services for Tor protocol v2 with newer versions of Tor daemon

IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before upgrading to this release.
IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still use Tor v0.3.3.5 or earlier before
upgrading to this release.

### API changes

Expand All @@ -20,25 +96,33 @@ IMPORTANT: You'll need to upgrade your Tor daemon if for some reason you still u

#### Delay enforcement of new channel fees

When updating the relay fees for a channel, eclair can now continue accepting to relay payments using the old fee even if they would be rejected with the new fee.
By default, eclair will still accept the old fee for 10 minutes, you can change it by setting `eclair.relay.fees.enforcement-delay` to a different value.
When updating the relay fees for a channel, eclair can now continue accepting to relay payments using the old fee even
if they would be rejected with the new fee.
By default, eclair will still accept the old fee for 10 minutes, you can change it by
setting `eclair.relay.fees.enforcement-delay` to a different value.

If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the previous fee.
If you want a specific fee update to ignore this delay, you can update the fee twice to make eclair forget about the
previous fee.

#### New minimum funding setting for private channels

New settings have been added to independently control the minimum funding required to open public and private channels to your node.
New settings have been added to independently control the minimum funding required to open public and private channels
to your node.

The `eclair.channel.min-funding-satoshis` setting has been deprecated and replaced with the following two new settings and defaults:
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`

If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these new settings.
If your configuration file changes `eclair.channel.min-funding-satoshis` then you should replace it with both of these
new settings.

#### Expired incoming invoices now purged if unpaid

Expired incoming invoices that are unpaid will be searched for and purged from the database when Eclair starts up. 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:
Expired incoming invoices that are unpaid will be searched for and purged from the database when Eclair starts up.
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`
Expand Down Expand Up @@ -77,13 +161,16 @@ Use the following command to generate the eclair-node package:
mvn clean install -DskipTests
```

That should generate `eclair-node/target/eclair-node-<version>-XXXXXXX-bin.zip` with sha256 checksums that match the one we provide and sign in `SHA256SUMS.asc`
That should generate `eclair-node/target/eclair-node-<version>-XXXXXXX-bin.zip` with sha256 checksums that match the one
we provide and sign in `SHA256SUMS.asc`

(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not tried everything.
(*) You may be able to build the exact same artefacts with other operating systems or versions of JDK 11, we have not
tried everything.

## Upgrading

This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair, upgrade and restart.
This release is fully compatible with previous eclair versions. You don't need to close your channels, just stop eclair,
upgrade and restart.

## Changelog

Expand Down
6 changes: 5 additions & 1 deletion eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,13 @@ eclair {
option_dual_fund = disabled
option_onion_messages = optional
option_channel_type = optional
option_scid_alias = optional
option_payment_metadata = optional
trampoline_payment_prototype = disabled
// By enabling option_zeroconf, you will be trusting your peers as fundee. You will lose funds if they double spend
// their funding tx.
option_zeroconf = disabled
keysend = disabled
trampoline_payment_prototype = disabled
}
override-init-features = [ // optional per-node features
# {
Expand Down
2 changes: 2 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/BlockHeight.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ case class BlockHeight(private val underlying: Long) extends Ordered[BlockHeight
def toInt: Int = underlying.toInt
def toLong: Long = underlying
def toDouble: Double = underlying.toDouble

override def toString() = underlying.toString
// @formatter:on
}

Expand Down
20 changes: 12 additions & 8 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -182,14 +182,18 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], channelType_opt: Option[SupportedChannelType], fundingFeeratePerByte_opt: Option[FeeratePerByte], announceChannel_opt: Option[Boolean], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = {
// we want the open timeout to expire *before* the default ask timeout, otherwise user will get a generic response
val openTimeout = openTimeout_opt.getOrElse(Timeout(20 seconds))
(appKit.switchboard ? Peer.OpenChannel(
remoteNodeId = nodeId,
fundingSatoshis = fundingAmount,
pushMsat = pushAmount_opt.getOrElse(0 msat),
channelType_opt = channelType_opt,
fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)),
channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)),
timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse]
for {
_ <- Future.successful(0)
open = Peer.OpenChannel(
remoteNodeId = nodeId,
fundingSatoshis = fundingAmount,
pushMsat = pushAmount_opt.getOrElse(0 msat),
channelType_opt = channelType_opt,
fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)),
channelFlags = announceChannel_opt.map(announceChannel => ChannelFlags(announceChannel = announceChannel)),
timeout_opt = Some(openTimeout))
res <- (appKit.switchboard ? open).mapTo[ChannelOpenResponse]
} yield res
}

override def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector], closingFeerates_opt: Option[ClosingFeerates])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] = {
Expand Down
16 changes: 14 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Original file line number Diff line number Diff line change
Expand Up @@ -240,11 +240,21 @@ object Features {
val mandatory = 44
}

case object ScidAlias extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
val rfcName = "option_scid_alias"
val mandatory = 46
}

case object PaymentMetadata extends Feature with InvoiceFeature {
val rfcName = "option_payment_metadata"
val mandatory = 48
}

case object ZeroConf extends Feature with InitFeature with NodeFeature with ChannelTypeFeature {
val rfcName = "option_zeroconf"
val mandatory = 50
}

case object KeySend extends Feature with NodeFeature {
val rfcName = "keysend"
val mandatory = 54
Expand Down Expand Up @@ -278,9 +288,11 @@ object Features {
DualFunding,
OnionMessages,
ChannelType,
ScidAlias,
PaymentMetadata,
TrampolinePaymentPrototype,
KeySend
ZeroConf,
KeySend,
TrampolinePaymentPrototype
)

// Features may depend on other features, as specified in Bolt 9.
Expand Down
63 changes: 44 additions & 19 deletions eclair-core/src/main/scala/fr/acinq/eclair/ShortChannelId.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,71 @@

package fr.acinq.eclair

/**
* A short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain.
* See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements
*/
case class ShortChannelId(private val id: Long) extends Ordered[ShortChannelId] {

def toLong: Long = id

def blockHeight = ShortChannelId.blockHeight(this)
import fr.acinq.eclair.ShortChannelId.toShortId

override def toString: String = {
// @formatter:off
sealed trait ShortChannelId extends Ordered[ShortChannelId] {
def toLong: Long
// we use an unsigned long comparison here
override def compare(that: ShortChannelId): Int = (this.toLong + Long.MinValue).compareTo(that.toLong + Long.MinValue)
override def hashCode(): Int = toLong.hashCode()
override def equals(obj: Any): Boolean = obj match {
case scid: ShortChannelId => this.toLong.equals(scid.toLong)
case _ => false
}
def toCoordinatesString: String = {
val TxCoordinates(blockHeight, txIndex, outputIndex) = ShortChannelId.coordinates(this)
s"${blockHeight.toLong}x${txIndex}x$outputIndex"
}

// we use an unsigned long comparison here
override def compare(that: ShortChannelId): Int = (this.id + Long.MinValue).compareTo(that.id + Long.MinValue)
def toHex: String = s"0x${toLong.toHexString}"
}
/** Sometimes we don't know what a scid really is */
case class UnspecifiedShortChannelId(private val id: Long) extends ShortChannelId {
override def toLong: Long = id
override def toString: String = toCoordinatesString // for backwards compatibility, because ChannelUpdate have an unspecified scid
}
case class RealShortChannelId private(private val id: Long) extends ShortChannelId {
override def toLong: Long = id
override def toString: String = toCoordinatesString
def blockHeight: BlockHeight = ShortChannelId.blockHeight(this)
def outputIndex: Int = ShortChannelId.outputIndex(this)
}
case class Alias(private val id: Long) extends ShortChannelId {
override def toLong: Long = id
override def toString: String = toHex
}
// @formatter:on

object ShortChannelId {

def apply(s: String): ShortChannelId = s.split("x").toList match {
case blockHeight :: txIndex :: outputIndex :: Nil => ShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt))
case blockHeight :: txIndex :: outputIndex :: Nil => UnspecifiedShortChannelId(toShortId(blockHeight.toInt, txIndex.toInt, outputIndex.toInt))
case _ => throw new IllegalArgumentException(s"Invalid short channel id: $s")
}

def apply(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int): ShortChannelId = ShortChannelId(toShortId(blockHeight.toInt, txIndex, outputIndex))
def apply(l: Long): ShortChannelId = UnspecifiedShortChannelId(l)

def toShortId(blockHeight: Int, txIndex: Int, outputIndex: Int): Long = ((blockHeight & 0xFFFFFFL) << 40) | ((txIndex & 0xFFFFFFL) << 16) | (outputIndex & 0xFFFFL)

def generateLocalAlias(): Alias = Alias(System.nanoTime()) // TODO: fixme (duplicate, etc.)

@inline
def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.id >> 40) & 0xFFFFFF)
def blockHeight(shortChannelId: ShortChannelId): BlockHeight = BlockHeight((shortChannelId.toLong >> 40) & 0xFFFFFF)

@inline
def txIndex(shortChannelId: ShortChannelId): Int = ((shortChannelId.id >> 16) & 0xFFFFFF).toInt
def txIndex(shortChannelId: ShortChannelId): Int = ((shortChannelId.toLong >> 16) & 0xFFFFFF).toInt

@inline
def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.id & 0xFFFF).toInt
def outputIndex(shortChannelId: ShortChannelId): Int = (shortChannelId.toLong & 0xFFFF).toInt

def coordinates(shortChannelId: ShortChannelId): TxCoordinates = TxCoordinates(blockHeight(shortChannelId), txIndex(shortChannelId), outputIndex(shortChannelId))
}

/**
* A real short channel id uniquely identifies a channel by the coordinates of its funding tx output in the blockchain.
* See BOLT 7: https://github.com/lightningnetwork/lightning-rfc/blob/master/07-routing-gossip.md#requirements
*/
object RealShortChannelId {
def apply(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int): RealShortChannelId = RealShortChannelId(toShortId(blockHeight.toInt, txIndex, outputIndex))
}

case class TxCoordinates(blockHeight: BlockHeight, txIndex: Int, outputIndex: Int)
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ private class BalanceActor(context: ActorContext[Command],
Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainConfirmed).update(result.onChain.confirmed.toMilliBtc.toDouble)
Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.OnchainUnconfirmed).update(result.onChain.unconfirmed.toMilliBtc.toDouble)
Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingConfirmed).update(result.offChain.waitForFundingConfirmed.toMilliBtc.toDouble)
Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForFundingLocked).update(result.offChain.waitForFundingLocked.toMilliBtc.toDouble)
Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.waitForChannelReady).update(result.offChain.waitForChannelReady.toMilliBtc.toDouble)
Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.normal).update(result.offChain.normal.total.toMilliBtc.toDouble)
Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.shutdown).update(result.offChain.shutdown.total.toMilliBtc.toDouble)
Metrics.GlobalBalanceDetailed.withTag(Tags.BalanceType, Tags.BalanceTypes.Offchain).withTag(Tags.OffchainState, Tags.OffchainStates.closingLocal).update(result.offChain.closing.localCloseBalance.total.toMilliBtc.toDouble)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,13 @@ object CheckBalance {
* The overall balance among all channels in all states.
*/
case class OffChainBalance(waitForFundingConfirmed: Btc = 0.sat,
waitForFundingLocked: Btc = 0.sat,
waitForChannelReady: Btc = 0.sat,
normal: MainAndHtlcBalance = MainAndHtlcBalance(),
shutdown: MainAndHtlcBalance = MainAndHtlcBalance(),
negotiating: Btc = 0.sat,
closing: ClosingBalance = ClosingBalance(),
waitForPublishFutureCommitment: Btc = 0.sat) {
val total: Btc = waitForFundingConfirmed + waitForFundingLocked + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment
val total: Btc = waitForFundingConfirmed + waitForChannelReady + normal.total + shutdown.total + negotiating + closing.total + waitForPublishFutureCommitment
}

def updateMainBalance(localCommit: LocalCommit): Btc => Btc = { v: Btc =>
Expand Down Expand Up @@ -201,7 +201,7 @@ object CheckBalance {
channels
.foldLeft(OffChainBalance()) {
case (r, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) => r.modify(_.waitForFundingConfirmed).using(updateMainBalance(d.commitments.localCommit))
case (r, d: DATA_WAIT_FOR_FUNDING_LOCKED) => r.modify(_.waitForFundingLocked).using(updateMainBalance(d.commitments.localCommit))
case (r, d: DATA_WAIT_FOR_CHANNEL_READY) => r.modify(_.waitForChannelReady).using(updateMainBalance(d.commitments.localCommit))
case (r, d: DATA_NORMAL) => r.modify(_.normal).using(updateMainAndHtlcBalance(d.commitments, knownPreimages))
case (r, d: DATA_SHUTDOWN) => r.modify(_.shutdown).using(updateMainAndHtlcBalance(d.commitments, knownPreimages))
case (r, d: DATA_NEGOTIATING) => r.modify(_.negotiating).using(updateMainBalance(d.commitments.localCommit))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ object Monitoring {

object OffchainStates {
val waitForFundingConfirmed = "waitForFundingConfirmed"
val waitForFundingLocked = "waitForFundingLocked"
val waitForChannelReady = "waitForChannelReady"
val normal = "normal"
val shutdown = "shutdown"
val negotiating = "negotiating"
Expand Down
Loading

0 comments on commit e5f5cd1

Please sign in to comment.