Skip to content

Commit e9df4ee

Browse files
authored
Channels data format migration (#1849)
There are three otherwise unrelated changes, that we group together to only have one migration: - remove local signatures for local commitments (this PR) - separate internal channel config from channel features (#1848) - upfront shutdown script (#1846) We increase database version number in sqlite and postgres to force a full data migration. The goal of removing local signatures from the channel data is that even if the node database or a backup is compromised, the attacker won't be able to force close channels from the outside.
1 parent 547d7e7 commit e9df4ee

File tree

57 files changed

+1740
-860
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+1740
-860
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/balance/CheckBalance.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ object CheckBalance {
127127

128128
def computeRemoteCloseBalance(c: Commitments, r: RemoteClose, knownPreimages: Set[(ByteVector32, Long)]): PossiblyPublishedMainAndHtlcBalance = {
129129
import r._
130-
val toLocal = if (c.channelVersion.paysDirectlyToWallet) {
130+
val toLocal = if (c.channelFeatures.paysDirectlyToWallet) {
131131
// If static remote key is enabled, the commit tx directly pays to our wallet
132132
// We use the pubkeyscript to retrieve our output
133133
Transactions.findPubKeyScriptIndex(remoteCommitPublished.commitTx, c.localParams.defaultFinalScriptPubKey) match {

eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/FeeEstimator.scala

+8-7
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@ package fr.acinq.eclair.blockchain.fee
1818

1919
import fr.acinq.bitcoin.Crypto.PublicKey
2020
import fr.acinq.bitcoin.Satoshi
21+
import fr.acinq.eclair.Features
2122
import fr.acinq.eclair.blockchain.CurrentFeerates
22-
import fr.acinq.eclair.channel.ChannelVersion
23+
import fr.acinq.eclair.channel.ChannelFeatures
2324

2425
trait FeeEstimator {
2526
// @formatter:off
@@ -32,13 +33,13 @@ case class FeeTargets(fundingBlockTarget: Int, commitmentBlockTarget: Int, mutua
3233

3334
case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw) {
3435
/**
35-
* @param channelVersion channel version
36+
* @param channelFeatures permanent channel features
3637
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
3738
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
3839
* @return true if the difference between proposed and reference fee rates is too high.
3940
*/
40-
def isFeeDiffTooHigh(channelVersion: ChannelVersion, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
41-
if (channelVersion.hasAnchorOutputs) {
41+
def isFeeDiffTooHigh(channelFeatures: ChannelFeatures, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
42+
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
4243
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
4344
} else {
4445
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
@@ -60,15 +61,15 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
6061
* - otherwise we use a feerate that should get the commit tx confirmed within the configured block target
6162
*
6263
* @param remoteNodeId nodeId of our channel peer
63-
* @param channelVersion channel version
64+
* @param channelFeatures permanent channel features
6465
* @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator
6566
*/
66-
def getCommitmentFeerate(remoteNodeId: PublicKey, channelVersion: ChannelVersion, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
67+
def getCommitmentFeerate(remoteNodeId: PublicKey, channelFeatures: ChannelFeatures, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
6768
val networkFeerate = currentFeerates_opt match {
6869
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
6970
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
7071
}
71-
if (channelVersion.hasAnchorOutputs) {
72+
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
7273
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
7374
} else {
7475
networkFeerate

eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala

+49-47
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2021 ACINQ SAS
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fr.acinq.eclair.channel
18+
19+
/**
20+
* Created by t-bast on 24/06/2021.
21+
*/
22+
23+
/**
24+
* Internal configuration option impacting the channel's structure or behavior.
25+
* This must be set when creating the channel and cannot be changed afterwards.
26+
*/
27+
trait ChannelConfigOption {
28+
// @formatter:off
29+
def supportBit: Int
30+
def name: String
31+
// @formatter:on
32+
}
33+
34+
case class ChannelConfig(options: Set[ChannelConfigOption]) {
35+
36+
def hasOption(option: ChannelConfigOption): Boolean = options.contains(option)
37+
38+
}
39+
40+
object ChannelConfig {
41+
42+
val standard: ChannelConfig = ChannelConfig(options = Set(FundingPubKeyBasedChannelKeyPath))
43+
44+
def apply(opts: ChannelConfigOption*): ChannelConfig = ChannelConfig(Set.from(opts))
45+
46+
/**
47+
* If set, the channel's BIP32 key path will be deterministically derived from the funding public key.
48+
* It makes it very easy to retrieve funds when channel data has been lost:
49+
* - connect to your peer and use option_data_loss_protect to get them to publish their remote commit tx
50+
* - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data
51+
* - recompute your channel keys and spend your output
52+
*/
53+
case object FundingPubKeyBasedChannelKeyPath extends ChannelConfigOption {
54+
override val supportBit: Int = 0
55+
override val name: String = "funding_pubkey_based_channel_keypath"
56+
}
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2021 ACINQ SAS
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package fr.acinq.eclair.channel
18+
19+
import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo}
20+
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
21+
import fr.acinq.eclair.{Feature, Features}
22+
23+
/**
24+
* Created by t-bast on 24/06/2021.
25+
*/
26+
27+
/**
28+
* Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel.
29+
* Even if one of these features is later disabled at the connection level, it will still apply to the channel until the
30+
* channel is upgraded or closed.
31+
*/
32+
case class ChannelFeatures(activated: Set[Feature]) {
33+
34+
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
35+
val paysDirectlyToWallet: Boolean = {
36+
hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs)
37+
}
38+
39+
/** Format of the channel transactions. */
40+
val commitmentFormat: CommitmentFormat = {
41+
if (hasFeature(AnchorOutputs)) {
42+
AnchorOutputsCommitmentFormat
43+
} else {
44+
DefaultCommitmentFormat
45+
}
46+
}
47+
48+
def hasFeature(feature: Feature): Boolean = activated.contains(feature)
49+
50+
override def toString: String = activated.mkString(",")
51+
52+
}
53+
54+
object ChannelFeatures {
55+
56+
def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features))
57+
58+
/** Pick the channel features that should be used based on local and remote feature bits. */
59+
def pickChannelFeatures(localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
60+
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
61+
// such as option_dataloss_protect or option_shutdown_anysegwit.
62+
val availableFeatures = Set[Feature](
63+
StaticRemoteKey,
64+
Wumbo,
65+
AnchorOutputs,
66+
).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))
67+
68+
ChannelFeatures(availableFeatures)
69+
}
70+
71+
}

eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala

+22-63
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import fr.acinq.eclair.transactions.CommitmentSpec
2626
import fr.acinq.eclair.transactions.Transactions._
2727
import fr.acinq.eclair.wire.protocol.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFulfillHtlc}
2828
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, ShortChannelId, UInt64}
29-
import scodec.bits.{BitVector, ByteVector}
29+
import scodec.bits.ByteVector
3030

3131
import java.util.UUID
3232

@@ -87,8 +87,14 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32,
8787
remote: ActorRef,
8888
remoteInit: Init,
8989
channelFlags: Byte,
90-
channelVersion: ChannelVersion)
91-
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelVersion: ChannelVersion)
90+
channelConfig: ChannelConfig,
91+
channelFeatures: ChannelFeatures)
92+
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
93+
localParams: LocalParams,
94+
remote: ActorRef,
95+
remoteInit: Init,
96+
channelConfig: ChannelConfig,
97+
channelFeatures: ChannelFeatures)
9298
case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close
9399
case object INPUT_DISCONNECTED
94100
case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init)
@@ -375,7 +381,8 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32
375381
initialFeeratePerKw: FeeratePerKw,
376382
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
377383
remoteFirstPerCommitmentPoint: PublicKey,
378-
channelVersion: ChannelVersion,
384+
channelConfig: ChannelConfig,
385+
channelFeatures: ChannelFeatures,
379386
lastSent: OpenChannel) extends Data {
380387
val channelId: ByteVector32 = temporaryChannelId
381388
}
@@ -388,7 +395,8 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32,
388395
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
389396
remoteFirstPerCommitmentPoint: PublicKey,
390397
channelFlags: Byte,
391-
channelVersion: ChannelVersion,
398+
channelConfig: ChannelConfig,
399+
channelFeatures: ChannelFeatures,
392400
lastSent: AcceptChannel) extends Data {
393401
val channelId: ByteVector32 = temporaryChannelId
394402
}
@@ -402,7 +410,8 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32,
402410
localCommitTx: CommitTx,
403411
remoteCommit: RemoteCommit,
404412
channelFlags: Byte,
405-
channelVersion: ChannelVersion,
413+
channelConfig: ChannelConfig,
414+
channelFeatures: ChannelFeatures,
406415
lastSent: FundingCreated) extends Data
407416
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments,
408417
fundingTx: Option[Transaction],
@@ -444,9 +453,9 @@ final case class DATA_CLOSING(commitments: Commitments,
444453
final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments
445454

446455
/**
447-
* @param features current connection features, or last features used if the channel is disconnected. Note that these
448-
* features are updated at each reconnection and may be different from the ones that were used when the
449-
* channel was created. See [[ChannelVersion]] for permanent features associated to a channel.
456+
* @param initFeatures current connection features, or last features used if the channel is disconnected. Note that these
457+
* features are updated at each reconnection and may be different from the channel permanent features
458+
* (see [[ChannelFeatures]]).
450459
*/
451460
final case class LocalParams(nodeId: PublicKey,
452461
fundingKeyPath: DeterministicWallet.KeyPath,
@@ -459,10 +468,10 @@ final case class LocalParams(nodeId: PublicKey,
459468
isFunder: Boolean,
460469
defaultFinalScriptPubKey: ByteVector,
461470
walletStaticPaymentBasepoint: Option[PublicKey],
462-
features: Features)
471+
initFeatures: Features)
463472

464473
/**
465-
* @param features see [[LocalParams.features]]
474+
* @param initFeatures see [[LocalParams.initFeatures]]
466475
*/
467476
final case class RemoteParams(nodeId: PublicKey,
468477
dustLimit: Satoshi,
@@ -476,61 +485,11 @@ final case class RemoteParams(nodeId: PublicKey,
476485
paymentBasepoint: PublicKey,
477486
delayedPaymentBasepoint: PublicKey,
478487
htlcBasepoint: PublicKey,
479-
features: Features)
488+
initFeatures: Features,
489+
shutdownScript: Option[ByteVector])
480490

481491
object ChannelFlags {
482492
val AnnounceChannel = 0x01.toByte
483493
val Empty = 0x00.toByte
484494
}
485-
486-
case class ChannelVersion(bits: BitVector) {
487-
import ChannelVersion._
488-
489-
require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes")
490-
491-
val commitmentFormat: CommitmentFormat = if (hasAnchorOutputs) {
492-
AnchorOutputsCommitmentFormat
493-
} else {
494-
DefaultCommitmentFormat
495-
}
496-
497-
def |(other: ChannelVersion) = ChannelVersion(bits | other.bits)
498-
def &(other: ChannelVersion) = ChannelVersion(bits & other.bits)
499-
def ^(other: ChannelVersion) = ChannelVersion(bits ^ other.bits)
500-
501-
def isSet(bit: Int): Boolean = bits.reverse.get(bit)
502-
503-
def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT)
504-
def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT)
505-
def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT)
506-
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
507-
def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs
508-
}
509-
510-
object ChannelVersion {
511-
import scodec.bits._
512-
513-
val LENGTH_BITS: Int = 4 * 8
514-
515-
private val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0
516-
private val USE_STATIC_REMOTEKEY_BIT = 1
517-
private val USE_ANCHOR_OUTPUTS_BIT = 2
518-
519-
def fromBit(bit: Int): ChannelVersion = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse)
520-
521-
def pickChannelVersion(localFeatures: Features, remoteFeatures: Features): ChannelVersion = {
522-
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
523-
ANCHOR_OUTPUTS
524-
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
525-
STATIC_REMOTEKEY
526-
} else {
527-
STANDARD
528-
}
529-
}
530-
531-
val ZEROES = ChannelVersion(bin"00000000000000000000000000000000")
532-
val STANDARD = ZEROES | fromBit(USE_PUBKEY_KEYPATH_BIT)
533-
val STATIC_REMOTEKEY = STANDARD | fromBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY
534-
val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | fromBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS
535-
}
536495
// @formatter:on

0 commit comments

Comments
 (0)