Skip to content

Commit

Permalink
Channels data format migration (#1849)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
pm47 committed Jul 15, 2021
1 parent 547d7e7 commit e9df4ee
Show file tree
Hide file tree
Showing 57 changed files with 1,740 additions and 860 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ object CheckBalance {

def computeRemoteCloseBalance(c: Commitments, r: RemoteClose, knownPreimages: Set[(ByteVector32, Long)]): PossiblyPublishedMainAndHtlcBalance = {
import r._
val toLocal = if (c.channelVersion.paysDirectlyToWallet) {
val toLocal = if (c.channelFeatures.paysDirectlyToWallet) {
// If static remote key is enabled, the commit tx directly pays to our wallet
// We use the pubkeyscript to retrieve our output
Transactions.findPubKeyScriptIndex(remoteCommitPublished.commitTx, c.localParams.defaultFinalScriptPubKey) match {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ package fr.acinq.eclair.blockchain.fee

import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.Satoshi
import fr.acinq.eclair.Features
import fr.acinq.eclair.blockchain.CurrentFeerates
import fr.acinq.eclair.channel.ChannelVersion
import fr.acinq.eclair.channel.ChannelFeatures

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

case class FeerateTolerance(ratioLow: Double, ratioHigh: Double, anchorOutputMaxCommitFeerate: FeeratePerKw) {
/**
* @param channelVersion channel version
* @param channelFeatures permanent channel features
* @param networkFeerate reference fee rate (value we estimate from our view of the network)
* @param proposedFeerate fee rate proposed (new proposal through update_fee or previous proposal used in our current commit tx)
* @return true if the difference between proposed and reference fee rates is too high.
*/
def isFeeDiffTooHigh(channelVersion: ChannelVersion, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
if (channelVersion.hasAnchorOutputs) {
def isFeeDiffTooHigh(channelFeatures: ChannelFeatures, networkFeerate: FeeratePerKw, proposedFeerate: FeeratePerKw): Boolean = {
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
proposedFeerate < networkFeerate * ratioLow || anchorOutputMaxCommitFeerate * ratioHigh < proposedFeerate
} else {
proposedFeerate < networkFeerate * ratioLow || networkFeerate * ratioHigh < proposedFeerate
Expand All @@ -60,15 +61,15 @@ case class OnChainFeeConf(feeTargets: FeeTargets, feeEstimator: FeeEstimator, cl
* - otherwise we use a feerate that should get the commit tx confirmed within the configured block target
*
* @param remoteNodeId nodeId of our channel peer
* @param channelVersion channel version
* @param channelFeatures permanent channel features
* @param currentFeerates_opt if provided, will be used to compute the most up-to-date network fee, otherwise we rely on the fee estimator
*/
def getCommitmentFeerate(remoteNodeId: PublicKey, channelVersion: ChannelVersion, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
def getCommitmentFeerate(remoteNodeId: PublicKey, channelFeatures: ChannelFeatures, channelCapacity: Satoshi, currentFeerates_opt: Option[CurrentFeerates]): FeeratePerKw = {
val networkFeerate = currentFeerates_opt match {
case Some(currentFeerates) => currentFeerates.feeratesPerKw.feePerBlock(feeTargets.commitmentBlockTarget)
case None => feeEstimator.getFeeratePerKw(feeTargets.commitmentBlockTarget)
}
if (channelVersion.hasAnchorOutputs) {
if (channelFeatures.hasFeature(Features.AnchorOutputs)) {
networkFeerate.min(feerateToleranceFor(remoteNodeId).anchorOutputMaxCommitFeerate)
} else {
networkFeerate
Expand Down
96 changes: 49 additions & 47 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2021 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.channel

/**
* Created by t-bast on 24/06/2021.
*/

/**
* Internal configuration option impacting the channel's structure or behavior.
* This must be set when creating the channel and cannot be changed afterwards.
*/
trait ChannelConfigOption {
// @formatter:off
def supportBit: Int
def name: String
// @formatter:on
}

case class ChannelConfig(options: Set[ChannelConfigOption]) {

def hasOption(option: ChannelConfigOption): Boolean = options.contains(option)

}

object ChannelConfig {

val standard: ChannelConfig = ChannelConfig(options = Set(FundingPubKeyBasedChannelKeyPath))

def apply(opts: ChannelConfigOption*): ChannelConfig = ChannelConfig(Set.from(opts))

/**
* If set, the channel's BIP32 key path will be deterministically derived from the funding public key.
* It makes it very easy to retrieve funds when channel data has been lost:
* - connect to your peer and use option_data_loss_protect to get them to publish their remote commit tx
* - retrieve the commit tx from the bitcoin network, extract your funding pubkey from its witness data
* - recompute your channel keys and spend your output
*/
case object FundingPubKeyBasedChannelKeyPath extends ChannelConfigOption {
override val supportBit: Int = 0
override val name: String = "funding_pubkey_based_channel_keypath"
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2021 ACINQ SAS
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package fr.acinq.eclair.channel

import fr.acinq.eclair.Features.{AnchorOutputs, StaticRemoteKey, Wumbo}
import fr.acinq.eclair.transactions.Transactions.{AnchorOutputsCommitmentFormat, CommitmentFormat, DefaultCommitmentFormat}
import fr.acinq.eclair.{Feature, Features}

/**
* Created by t-bast on 24/06/2021.
*/

/**
* Subset of Bolt 9 features used to configure a channel and applicable over the lifetime of that channel.
* Even if one of these features is later disabled at the connection level, it will still apply to the channel until the
* channel is upgraded or closed.
*/
case class ChannelFeatures(activated: Set[Feature]) {

/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
val paysDirectlyToWallet: Boolean = {
hasFeature(Features.StaticRemoteKey) && !hasFeature(Features.AnchorOutputs)
}

/** Format of the channel transactions. */
val commitmentFormat: CommitmentFormat = {
if (hasFeature(AnchorOutputs)) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}
}

def hasFeature(feature: Feature): Boolean = activated.contains(feature)

override def toString: String = activated.mkString(",")

}

object ChannelFeatures {

def apply(features: Feature*): ChannelFeatures = ChannelFeatures(Set.from(features))

/** Pick the channel features that should be used based on local and remote feature bits. */
def pickChannelFeatures(localFeatures: Features, remoteFeatures: Features): ChannelFeatures = {
// NB: we don't include features that can be safely activated/deactivated without impacting the channel's operation,
// such as option_dataloss_protect or option_shutdown_anysegwit.
val availableFeatures = Set[Feature](
StaticRemoteKey,
Wumbo,
AnchorOutputs,
).filter(f => Features.canUseFeature(localFeatures, remoteFeatures, f))

ChannelFeatures(availableFeatures)
}

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

import java.util.UUID

Expand Down Expand Up @@ -87,8 +87,14 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32,
remote: ActorRef,
remoteInit: Init,
channelFlags: Byte,
channelVersion: ChannelVersion)
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32, localParams: LocalParams, remote: ActorRef, remoteInit: Init, channelVersion: ChannelVersion)
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures)
case class INPUT_INIT_FUNDEE(temporaryChannelId: ByteVector32,
localParams: LocalParams,
remote: ActorRef,
remoteInit: Init,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures)
case object INPUT_CLOSE_COMPLETE_TIMEOUT // when requesting a mutual close, we wait for as much as this timeout, then unilateral close
case object INPUT_DISCONNECTED
case class INPUT_RECONNECTED(remote: ActorRef, localInit: Init, remoteInit: Init)
Expand Down Expand Up @@ -375,7 +381,8 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32
initialFeeratePerKw: FeeratePerKw,
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
remoteFirstPerCommitmentPoint: PublicKey,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: OpenChannel) extends Data {
val channelId: ByteVector32 = temporaryChannelId
}
Expand All @@ -388,7 +395,8 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32,
initialRelayFees_opt: Option[(MilliSatoshi, Int)],
remoteFirstPerCommitmentPoint: PublicKey,
channelFlags: Byte,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: AcceptChannel) extends Data {
val channelId: ByteVector32 = temporaryChannelId
}
Expand All @@ -402,7 +410,8 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32,
localCommitTx: CommitTx,
remoteCommit: RemoteCommit,
channelFlags: Byte,
channelVersion: ChannelVersion,
channelConfig: ChannelConfig,
channelFeatures: ChannelFeatures,
lastSent: FundingCreated) extends Data
final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments,
fundingTx: Option[Transaction],
Expand Down Expand Up @@ -444,9 +453,9 @@ final case class DATA_CLOSING(commitments: Commitments,
final case class DATA_WAIT_FOR_REMOTE_PUBLISH_FUTURE_COMMITMENT(commitments: Commitments, remoteChannelReestablish: ChannelReestablish) extends Data with HasCommitments

/**
* @param features current connection features, or last features used if the channel is disconnected. Note that these
* features are updated at each reconnection and may be different from the ones that were used when the
* channel was created. See [[ChannelVersion]] for permanent features associated to a channel.
* @param initFeatures current connection features, or last features used if the channel is disconnected. Note that these
* features are updated at each reconnection and may be different from the channel permanent features
* (see [[ChannelFeatures]]).
*/
final case class LocalParams(nodeId: PublicKey,
fundingKeyPath: DeterministicWallet.KeyPath,
Expand All @@ -459,10 +468,10 @@ final case class LocalParams(nodeId: PublicKey,
isFunder: Boolean,
defaultFinalScriptPubKey: ByteVector,
walletStaticPaymentBasepoint: Option[PublicKey],
features: Features)
initFeatures: Features)

/**
* @param features see [[LocalParams.features]]
* @param initFeatures see [[LocalParams.initFeatures]]
*/
final case class RemoteParams(nodeId: PublicKey,
dustLimit: Satoshi,
Expand All @@ -476,61 +485,11 @@ final case class RemoteParams(nodeId: PublicKey,
paymentBasepoint: PublicKey,
delayedPaymentBasepoint: PublicKey,
htlcBasepoint: PublicKey,
features: Features)
initFeatures: Features,
shutdownScript: Option[ByteVector])

object ChannelFlags {
val AnnounceChannel = 0x01.toByte
val Empty = 0x00.toByte
}

case class ChannelVersion(bits: BitVector) {
import ChannelVersion._

require(bits.size == ChannelVersion.LENGTH_BITS, "channel version takes 4 bytes")

val commitmentFormat: CommitmentFormat = if (hasAnchorOutputs) {
AnchorOutputsCommitmentFormat
} else {
DefaultCommitmentFormat
}

def |(other: ChannelVersion) = ChannelVersion(bits | other.bits)
def &(other: ChannelVersion) = ChannelVersion(bits & other.bits)
def ^(other: ChannelVersion) = ChannelVersion(bits ^ other.bits)

def isSet(bit: Int): Boolean = bits.reverse.get(bit)

def hasPubkeyKeyPath: Boolean = isSet(USE_PUBKEY_KEYPATH_BIT)
def hasStaticRemotekey: Boolean = isSet(USE_STATIC_REMOTEKEY_BIT)
def hasAnchorOutputs: Boolean = isSet(USE_ANCHOR_OUTPUTS_BIT)
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
def paysDirectlyToWallet: Boolean = hasStaticRemotekey && !hasAnchorOutputs
}

object ChannelVersion {
import scodec.bits._

val LENGTH_BITS: Int = 4 * 8

private val USE_PUBKEY_KEYPATH_BIT = 0 // bit numbers start at 0
private val USE_STATIC_REMOTEKEY_BIT = 1
private val USE_ANCHOR_OUTPUTS_BIT = 2

def fromBit(bit: Int): ChannelVersion = ChannelVersion(BitVector.low(LENGTH_BITS).set(bit).reverse)

def pickChannelVersion(localFeatures: Features, remoteFeatures: Features): ChannelVersion = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
ANCHOR_OUTPUTS
} else if (Features.canUseFeature(localFeatures, remoteFeatures, Features.StaticRemoteKey)) {
STATIC_REMOTEKEY
} else {
STANDARD
}
}

val ZEROES = ChannelVersion(bin"00000000000000000000000000000000")
val STANDARD = ZEROES | fromBit(USE_PUBKEY_KEYPATH_BIT)
val STATIC_REMOTEKEY = STANDARD | fromBit(USE_STATIC_REMOTEKEY_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY
val ANCHOR_OUTPUTS = STATIC_REMOTEKEY | fromBit(USE_ANCHOR_OUTPUTS_BIT) // PUBKEY_KEYPATH + STATIC_REMOTEKEY + ANCHOR_OUTPUTS
}
// @formatter:on

0 comments on commit e9df4ee

Please sign in to comment.