Skip to content

Commit

Permalink
Activate anchor outputs (#2134)
Browse files Browse the repository at this point in the history
This feature is now ready to be widely deployed.
The transaction format has been tested for a long time
(between Phoenix iOS and our node) and automatic
fee-bumping has been implemented in #2113.
  • Loading branch information
t-bast authored Jan 19, 2022
1 parent 52a6ee9 commit e2b1b26
Show file tree
Hide file tree
Showing 7 changed files with 67 additions and 36 deletions.
26 changes: 26 additions & 0 deletions docs/release-notes/eclair-vnext.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,32 @@

## Major changes

### Anchor outputs activated by default

Experimental support for anchor outputs channels was first introduced in [eclair v0.4.2](https://github.com/ACINQ/eclair/releases/tag/v0.4.2).
Going from an experimental feature to production-ready required a lot more work than we expected!
What seems to be a simple change in transaction structure had a lot of impact in many unexpected places, and fundamentally changes the mechanisms to ensure funds safety.

When using anchor outputs, you will need to keep utxos available in your `bitcoind` wallet to fee-bump HTLC transactions when channels are force-closed.
Eclair will warn you via the `notifications.log` file when your `bitcoind` wallet balance is too low to protect your funds against malicious channel peers.
Eclair will wait for you to add funds to your wallet and automatically retry, but you may be at risk in the meantime if you had pending payments at the time of the force-close.

We recommend activating debug logs for the transaction publication mechanisms.
This will make it much easier to follow the trail of RBF-ed transactions, and shouldn't be too noisy unless you constantly have a lot of channels force-closing.
To activate these logs, simply add the following line to your `logback.xml`:

```xml
<logger name="fr.acinq.eclair.channel.publish" level="DEBUG"/>
```

You don't even need to restart eclair, it will automatically pick up the logging configuration update after a short while.

If you don't want to use anchor outputs, you can disable the feature in your `eclair.conf`:

```conf
eclair.features.option_anchors_zero_fee_htlc_tx = disabled
```

### Alternate strategy to avoid mass force-close of channels in certain cases

The default strategy, when an unhandled exception or internal error happens, is to locally force-close the channel. Not only is there a delay before the channel balance gets refunded, but if the exception was due to some misconfiguration or bug in eclair that affects all channels, we risk force-closing all channels.
Expand Down
2 changes: 1 addition & 1 deletion eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ eclair {
// NB: option_anchors_zero_fee_htlc_tx should always be preferred to option_anchor_outputs (it's safer).
// Do not enable option_anchor_outputs unless you really know what you're doing.
option_anchor_outputs = disabled
option_anchors_zero_fee_htlc_tx = disabled
option_anchors_zero_fee_htlc_tx = optional
option_shutdown_anysegwit = optional
option_onion_messages = optional
option_channel_type = optional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -457,9 +457,9 @@ abstract class ChannelIntegrationSpec extends IntegrationSpec {
class StandardChannelIntegrationSpec extends ChannelIntegrationSpec {

test("start eclair nodes") {
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091).asJava).withFallback(withAnchorOutputs).withFallback(withWumbo).withFallback(commonConfig))
instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29742, "eclair.api.port" -> 28092).asJava).withFallback(withWumbo).withFallback(commonConfig))
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29740, "eclair.api.port" -> 28090).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29741, "eclair.api.port" -> 28091).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig))
instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29742, "eclair.api.port" -> 28092).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig))
}

test("connect nodes") {
Expand Down Expand Up @@ -785,8 +785,8 @@ class AnchorOutputChannelIntegrationSpec extends AnchorChannelIntegrationSpec {
override val commitmentFormat = Transactions.UnsafeLegacyAnchorOutputsCommitmentFormat

test("start eclair nodes") {
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(withWumbo).withFallback(commonConfig))
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29750, "eclair.api.port" -> 28093).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29751, "eclair.api.port" -> 28094).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig))
instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29753, "eclair.api.port" -> 28095).asJava).withFallback(withAnchorOutputs).withFallback(commonConfig))
}

Expand Down Expand Up @@ -825,8 +825,8 @@ class AnchorOutputZeroFeeHtlcTxsChannelIntegrationSpec extends AnchorChannelInte
override val commitmentFormat = Transactions.ZeroFeeHtlcTxAnchorOutputsCommitmentFormat

test("start eclair nodes") {
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(withWumbo).withFallback(commonConfig))
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29760, "eclair.api.port" -> 28096).asJava).withFallback(withStaticRemoteKey).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29761, "eclair.api.port" -> 28097).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig))
instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 40, "eclair.fulfill-safety-before-timeout-blocks" -> 12, "eclair.server.port" -> 29763, "eclair.api.port" -> 28098).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,33 +87,38 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit
"eclair.router.broadcast-interval" -> "2 seconds",
"eclair.auto-reconnect" -> false,
"eclair.to-remote-delay-blocks" -> 24,
"eclair.multi-part-payment-expiry" -> "20 seconds").asJava).withFallback(ConfigFactory.load())
"eclair.multi-part-payment-expiry" -> "20 seconds",
"eclair.max-funding-satoshis" -> 500000000).asJava).withFallback(ConfigFactory.load())

val commonFeatures = ConfigFactory.parseMap(Map(
private val commonFeatures = ConfigFactory.parseMap(Map(
s"eclair.features.${OptionDataLossProtect.rfcName}" -> "optional",
s"eclair.features.${ChannelRangeQueries.rfcName}" -> "optional",
s"eclair.features.${ChannelRangeQueriesExtended.rfcName}" -> "optional",
s"eclair.features.${VariableLengthOnion.rfcName}" -> "mandatory",
s"eclair.features.${PaymentSecret.rfcName}" -> "mandatory",
s"eclair.features.${BasicMultiPartPayment.rfcName}" -> "optional"
s"eclair.features.${BasicMultiPartPayment.rfcName}" -> "optional",
s"eclair.features.${Wumbo.rfcName}" -> "optional",
s"eclair.features.${ShutdownAnySegwit.rfcName}" -> "optional",
s"eclair.features.${ChannelType.rfcName}" -> "optional",
).asJava)

val withWumbo = commonFeatures.withFallback(ConfigFactory.parseMap(Map(
s"eclair.features.${Wumbo.rfcName}" -> "optional",
"eclair.max-funding-satoshis" -> 500000000
val withDefaultCommitment = commonFeatures.withFallback(ConfigFactory.parseMap(Map(
s"eclair.features.${StaticRemoteKey.rfcName}" -> "disabled",
s"eclair.features.${AnchorOutputs.rfcName}" -> "disabled",
s"eclair.features.${AnchorOutputsZeroFeeHtlcTx.rfcName}" -> "disabled",
).asJava))

val withStaticRemoteKey = commonFeatures.withFallback(ConfigFactory.parseMap(Map(
val withStaticRemoteKey = ConfigFactory.parseMap(Map(
s"eclair.features.${StaticRemoteKey.rfcName}" -> "optional"
).asJava))
).asJava).withFallback(withDefaultCommitment)

val withAnchorOutputs = withStaticRemoteKey.withFallback(ConfigFactory.parseMap(Map(
val withAnchorOutputs = ConfigFactory.parseMap(Map(
s"eclair.features.${AnchorOutputs.rfcName}" -> "optional"
).asJava))
).asJava).withFallback(withStaticRemoteKey)

val withAnchorOutputsZeroFeeHtlcTxs = withAnchorOutputs.withFallback(ConfigFactory.parseMap(Map(
val withAnchorOutputsZeroFeeHtlcTxs = ConfigFactory.parseMap(Map(
s"eclair.features.${AnchorOutputsZeroFeeHtlcTx.rfcName}" -> "optional"
).asJava))
).asJava).withFallback(withStaticRemoteKey)

implicit val formats: Formats = DefaultFormats

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ class MessageIntegrationSpec extends IntegrationSpec {
implicit val timeout: Timeout = FiniteDuration(30, SECONDS)

test("start eclair nodes") {
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 30700, "eclair.api.port" -> 30780, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all", "eclair.onion-messages.reply-timeout" -> "1 minute").asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 30701, "eclair.api.port" -> 30781, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all", "eclair.onion-messages.reply-timeout" -> "1 second").asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.server.port" -> 30702, "eclair.api.port" -> 30782, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all").asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.server.port" -> 30703, "eclair.api.port" -> 30783).asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.server.port" -> 30704, "eclair.api.port" -> 30784, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "channels-only").asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.server.port" -> 30705, "eclair.api.port" -> 30785, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "no-relay").asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.server.port" -> 30700, "eclair.api.port" -> 30780, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all", "eclair.onion-messages.reply-timeout" -> "1 minute").asJava).withFallback(commonConfig))
instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.server.port" -> 30701, "eclair.api.port" -> 30781, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all", "eclair.onion-messages.reply-timeout" -> "1 second").asJava).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.server.port" -> 30702, "eclair.api.port" -> 30782, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "relay-all").asJava).withFallback(commonConfig))
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.server.port" -> 30703, "eclair.api.port" -> 30783).asJava).withFallback(commonConfig))
instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.server.port" -> 30704, "eclair.api.port" -> 30784, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "channels-only").asJava).withFallback(commonConfig))
instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.server.port" -> 30705, "eclair.api.port" -> 30785, s"eclair.features.${Features.OnionMessages.rfcName}" -> "optional", "eclair.onion-messages.relay-policy" -> "no-relay").asJava).withFallback(commonConfig))
}

test("try to reach unknown node") {
Expand Down Expand Up @@ -226,7 +226,7 @@ class MessageIntegrationSpec extends IntegrationSpec {
val probe = TestProbe()
val eventListener = TestProbe()
nodes("C").system.eventStream.subscribe(eventListener.ref, classOf[OnionMessages.ReceiveMessage])
alice.sendOnionMessage(nodes("E").nodeParams.nodeId :: Nil, Left( nodes("C").nodeParams.nodeId), None, hex"710301020375020102").pipeTo(probe.ref)
alice.sendOnionMessage(nodes("E").nodeParams.nodeId :: Nil, Left(nodes("C").nodeParams.nodeId), None, hex"710301020375020102").pipeTo(probe.ref)
assert(probe.expectMsgType[SendOnionMessageResponse].sent)

val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds)
Expand Down Expand Up @@ -302,7 +302,7 @@ class MessageIntegrationSpec extends IntegrationSpec {
val probe = TestProbe()
val eventListener = TestProbe()
nodes("C").system.eventStream.subscribe(eventListener.ref, classOf[OnionMessages.ReceiveMessage])
alice.sendOnionMessage(nodes("B").nodeParams.nodeId :: Nil, Left( nodes("C").nodeParams.nodeId), None, hex"7300").pipeTo(probe.ref)
alice.sendOnionMessage(nodes("B").nodeParams.nodeId :: Nil, Left(nodes("C").nodeParams.nodeId), None, hex"7300").pipeTo(probe.ref)
assert(probe.expectMsgType[SendOnionMessageResponse].sent)

val r = eventListener.expectMsgType[OnionMessages.ReceiveMessage](max = 60 seconds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ import scala.jdk.CollectionConverters._
class PaymentIntegrationSpec extends IntegrationSpec {

test("start eclair nodes") {
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0).asJava).withFallback(commonFeatures).withFallback(commonConfig)) // A's channels are private
instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(withWumbo).withFallback(commonConfig))
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonFeatures).withFallback(commonConfig))
instantiateEclairNode("A", ConfigFactory.parseMap(Map("eclair.node-alias" -> "A", "eclair.expiry-delta-blocks" -> 130, "eclair.server.port" -> 29730, "eclair.api.port" -> 28080, "eclair.channel-flags" -> 0).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig)) // A's channels are private
instantiateEclairNode("B", ConfigFactory.parseMap(Map("eclair.node-alias" -> "B", "eclair.expiry-delta-blocks" -> 131, "eclair.server.port" -> 29731, "eclair.api.port" -> 28081, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig))
instantiateEclairNode("C", ConfigFactory.parseMap(Map("eclair.node-alias" -> "C", "eclair.expiry-delta-blocks" -> 132, "eclair.server.port" -> 29732, "eclair.api.port" -> 28082, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig))
instantiateEclairNode("D", ConfigFactory.parseMap(Map("eclair.node-alias" -> "D", "eclair.expiry-delta-blocks" -> 133, "eclair.server.port" -> 29733, "eclair.api.port" -> 28083, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withDefaultCommitment).withFallback(commonConfig))
instantiateEclairNode("E", ConfigFactory.parseMap(Map("eclair.node-alias" -> "E", "eclair.expiry-delta-blocks" -> 134, "eclair.server.port" -> 29734, "eclair.api.port" -> 28084).asJava).withFallback(withAnchorOutputsZeroFeeHtlcTxs).withFallback(commonConfig))
instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(withWumbo).withFallback(commonConfig))
instantiateEclairNode("F", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F", "eclair.expiry-delta-blocks" -> 135, "eclair.server.port" -> 29735, "eclair.api.port" -> 28085, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig))
instantiateEclairNode("G", ConfigFactory.parseMap(Map("eclair.node-alias" -> "G", "eclair.expiry-delta-blocks" -> 136, "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.relay.fees.public-channels.fee-base-msat" -> 1010, "eclair.relay.fees.public-channels.fee-proportional-millionths" -> 102, "eclair.trampoline-payments-enable" -> true).asJava).withFallback(commonConfig))
}

Expand Down
Loading

0 comments on commit e2b1b26

Please sign in to comment.