Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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: 12 additions & 1 deletion eclair-core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,21 @@ eclair {
trampoline_payment = disabled
keysend = disabled
}
channel-types {
// The following parameter contains the list of all supported lightning transaction formats (order by preference).
// You can reorder this list or remove entries to change what types of channels can be created.
commitment-format = [
// standard lightning channels with option_static_remotekey applied (simplified funds recovery in case of data loss)
"static_remotekey",
// standard lightning channels (as defined in v1.0 of the LN specification)
"standard"
]
}
override-features = [ // optional per-node features
# {
# nodeid = "02aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
# features { }
# channel-types { }
# }
]
sync-whitelist = [] // a list of public keys; if non-empty, we will only do the initial sync with those peers
Expand Down Expand Up @@ -321,6 +332,6 @@ akka {
backend.min-nr-of-members = 1
frontend.min-nr-of-members = 0
}
seed-nodes = [ "akka://eclair-node@127.0.0.1:25520" ]
seed-nodes = ["akka://eclair-node@127.0.0.1:25520"]
}
}
55 changes: 48 additions & 7 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi}
import fr.acinq.eclair.Setup.Seeds
import fr.acinq.eclair.blockchain.fee._
import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.channel.{Channel, ChannelType}
import fr.acinq.eclair.crypto.Noise.KeyPair
import fr.acinq.eclair.crypto.keymanager.{ChannelKeyManager, NodeKeyManager}
import fr.acinq.eclair.db._
Expand Down Expand Up @@ -52,7 +52,8 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,
color: Color,
publicAddresses: List[NodeAddress],
features: Features,
private val overrideFeatures: Map[PublicKey, Features],
channelTypes: List[ChannelType],
private val overrideFeatures: Map[PublicKey, (Features, List[ChannelType])],
syncWhitelist: Set[PublicKey],
pluginParams: Seq[PluginParams],
dustLimit: Satoshi,
Expand Down Expand Up @@ -100,7 +101,16 @@ case class NodeParams(nodeKeyManager: NodeKeyManager,

def currentBlockHeight: Long = blockCount.get

def featuresFor(nodeId: PublicKey): Features = overrideFeatures.getOrElse(nodeId, features)
def featuresFor(nodeId: PublicKey): Features = overrideFeatures.get(nodeId) match {
case Some((featuresOverride, _)) if featuresOverride.activated.nonEmpty => featuresOverride
case _ => features
}

def channelTypesFor(nodeId: PublicKey): List[ChannelType] = overrideFeatures.get(nodeId) match {
case Some((_, channelTypesOverride)) if channelTypesOverride.nonEmpty => channelTypesOverride
case _ => channelTypes
}

}

object NodeParams extends Logging {
Expand Down Expand Up @@ -247,6 +257,28 @@ object NodeParams extends Logging {
val features = Features.fromConfiguration(config)
validateFeatures(features)

def parseChannelTypes(config: Config): List[ChannelType] = {
if (!config.hasPath("channel-types")) {
Nil
} else {
config.getStringList("channel-types.commitment-format").asScala.toList.map {
case "standard" => ChannelType(Features.empty)
case "static_remotekey" => ChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Optional))
case "anchor_outputs" => ChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional))
case unknown => throw new RuntimeException(s"unsupported channel type: $unknown")
}
}
}

def validateChannelTypes(features: Features, channelTypes: List[ChannelType]): Unit = {
channelTypes.foreach(channelType => channelType.features.activated.keys.foreach(f =>
require(features.hasFeature(f), s"feature $f is necessary for channel-type $channelType: you must either enable $f or disable $channelType")
))
}

val channelTypes = parseChannelTypes(config)
validateChannelTypes(features, channelTypes)

require(pluginMessageParams.forall(_.feature.mandatory > 128), "Plugin mandatory feature bit is too low, must be > 128")
require(pluginMessageParams.forall(_.feature.mandatory % 2 == 0), "Plugin mandatory feature bit is odd, must be even")
require(pluginMessageParams.flatMap(_.messageTags).forall(_ > 32768), "Plugin messages tags must be > 32768")
Expand All @@ -256,11 +288,19 @@ object NodeParams extends Logging {

val coreAndPluginFeatures = features.copy(unknown = features.unknown ++ pluginMessageParams.map(_.pluginFeature))

val overrideFeatures: Map[PublicKey, Features] = config.getConfigList("override-features").asScala.map { e =>
val overrideFeatures: Map[PublicKey, (Features, List[ChannelType])] = config.getConfigList("override-features").asScala.map { e =>
val p = PublicKey(ByteVector.fromValidHex(e.getString("nodeid")))
val f = Features.fromConfiguration(e)
validateFeatures(f)
p -> f.copy(unknown = f.unknown ++ pluginMessageParams.map(_.pluginFeature))
val featuresOverride = Features.fromConfiguration(e) match {
case f if f.activated.nonEmpty => f
case _ => features
}
validateFeatures(featuresOverride)
val channelTypesOverride = parseChannelTypes(e) match {
case ct if ct.nonEmpty => ct
case _ => channelTypes
}
validateChannelTypes(featuresOverride, channelTypesOverride)
p -> (featuresOverride.copy(unknown = featuresOverride.unknown ++ pluginMessageParams.map(_.pluginFeature)), channelTypesOverride)
}.toMap

val syncWhitelist: Set[PublicKey] = config.getStringList("sync-whitelist").asScala.map(s => PublicKey(ByteVector.fromValidHex(s))).toSet
Expand Down Expand Up @@ -309,6 +349,7 @@ object NodeParams extends Logging {
color = Color(color(0), color(1), color(2)),
publicAddresses = addresses,
features = coreAndPluginFeatures,
channelTypes = channelTypes,
pluginParams = pluginParams,
overrideFeatures = overrideFeatures,
syncWhitelist = syncWhitelist,
Expand Down
13 changes: 7 additions & 6 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId)
val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath).publicKey
val channelKeyPath = keyManager.keyPath(localParams, channelVersion)
val channelTypes = channelVersion.filterChannelTypes(nodeParams.channelTypesFor(remoteNodeId))
val open = OpenChannel(nodeParams.chainHash,
temporaryChannelId = temporaryChannelId,
fundingSatoshis = fundingSatoshis,
Expand All @@ -221,7 +222,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
channelFlags = channelFlags,
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script.
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty), OpenChannelTlv.ChannelTypes(channelTypes.map(_.features).toList)))
goto(WAIT_FOR_ACCEPT_CHANNEL) using DATA_WAIT_FOR_ACCEPT_CHANNEL(initFunder, open) sending open

case Event(inputFundee@INPUT_INIT_FUNDEE(_, localParams, remote, _, _), Nothing) if !localParams.isFunder =>
Expand Down Expand Up @@ -362,7 +363,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
firstPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0),
// In order to allow TLV extensions and keep backwards-compatibility, we include an empty upfront_shutdown_script.
// See https://github.com/lightningnetwork/lightning-rfc/pull/714.
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty)))
tlvStream = TlvStream(ChannelTlv.UpfrontShutdownScript(ByteVector.empty), AcceptChannelTlv.ChannelType(channelVersion.channelType.features)))
val remoteParams = RemoteParams(
nodeId = remoteNodeId,
dustLimit = open.dustLimitSatoshis,
Expand All @@ -389,11 +390,11 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
})

when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions {
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, initialRelayFees_opt, localParams, _, remoteInit, _, channelVersion), open)) =>
case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, initialRelayFees_opt, localParams, _, remoteInit, _, initialChannelVersion), open)) =>
log.info(s"received AcceptChannel=$accept")
Helpers.validateParamsFunder(nodeParams, open, accept) match {
Helpers.validateParamsFunder(nodeParams, open, accept, initialChannelVersion) match {
case Left(t) => handleLocalError(t, d, Some(accept))
case _ =>
case Right(finalChannelVersion) =>
val remoteParams = RemoteParams(
nodeId = remoteNodeId,
dustLimit = accept.dustLimitSatoshis,
Expand All @@ -412,7 +413,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId
val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey)))
wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self)
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, initialRelayFees_opt, accept.firstPerCommitmentPoint, channelVersion, open)
goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, initialRelayFees_opt, accept.firstPerCommitmentPoint, finalChannelVersion, open)
}

case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction}
import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.wire.protocol.{AnnouncementSignatures, Error, UpdateAddHtlc}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, UInt64}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, MilliSatoshi, UInt64}

/**
* Created by PM on 11/04/2017.
Expand All @@ -40,6 +40,7 @@ case class InvalidChainHash (override val channelId: Byte
case class InvalidFundingAmount (override val channelId: ByteVector32, fundingAmount: Satoshi, min: Satoshi, max: Satoshi) extends ChannelException(channelId, s"invalid funding_satoshis=$fundingAmount (min=$min max=$max)")
case class InvalidPushAmount (override val channelId: ByteVector32, pushAmount: MilliSatoshi, max: MilliSatoshi) extends ChannelException(channelId, s"invalid pushAmount=$pushAmount (max=$max)")
case class InvalidMaxAcceptedHtlcs (override val channelId: ByteVector32, maxAcceptedHtlcs: Int, max: Int) extends ChannelException(channelId, s"invalid max_accepted_htlcs=$maxAcceptedHtlcs (max=$max)")
case class IncompatibleChannelTypes (override val channelId: ByteVector32, supportedChannelTypes: Seq[Features]) extends ChannelException(channelId, s"incompatible channel types (we support ${supportedChannelTypes.map(_.toByteVector.toHex).mkString(" or ")})")
case class DustLimitTooSmall (override val channelId: ByteVector32, dustLimit: Satoshi, min: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too small (min=$min)")
case class DustLimitTooLarge (override val channelId: ByteVector32, dustLimit: Satoshi, max: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is too large (max=$max)")
case class DustLimitAboveOurChannelReserve (override val channelId: ByteVector32, dustLimit: Satoshi, channelReserve: Satoshi) extends ChannelException(channelId, s"dustLimit=$dustLimit is above our channelReserve=$channelReserve")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import fr.acinq.eclair.router.Announcements
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 fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, FeatureSupport, Features, MilliSatoshi, ShortChannelId, UInt64}
import scodec.bits.{BitVector, ByteVector}

import java.util.UUID
Expand Down Expand Up @@ -483,6 +483,12 @@ object ChannelFlags {
val Empty = 0x00.toByte
}

/** Each channel type is a specific combination of features listed in the RFC (Bolt 2). */
case class ChannelType(features: Features) {
/** True if our main output in the remote commitment is directly sent (without any delay) to one of our wallet addresses. */
def paysDirectlyToWallet: Boolean = features.hasFeature(Features.StaticRemoteKey) && !features.hasFeature(Features.AnchorOutputs)
}

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

Expand All @@ -494,6 +500,23 @@ case class ChannelVersion(bits: BitVector) {
DefaultCommitmentFormat
}

val channelType: ChannelType = {
if (hasAnchorOutputs) {
ChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Optional, Features.AnchorOutputs -> FeatureSupport.Optional))
} else if (hasStaticRemotekey) {
ChannelType(Features(Features.StaticRemoteKey -> FeatureSupport.Optional))
} else {
ChannelType(Features.empty)
}
}

/** Filter channel types to keep only those compatible with the current channel version. */
def filterChannelTypes(channelTypes: Seq[ChannelType]): Seq[ChannelType] = {
// We ensure we don't mix channel types that pay to our wallet with channel types that don't, since they use
// different methods to obtain the payment basepoint.
channelTypes.filter(_.paysDirectlyToWallet == paysDirectlyToWallet)
}

def |(other: ChannelVersion) = ChannelVersion(bits | other.bits)
def &(other: ChannelVersion) = ChannelVersion(bits & other.bits)
def ^(other: ChannelVersion) = ChannelVersion(bits ^ other.bits)
Expand All @@ -503,8 +526,7 @@ case class ChannelVersion(bits: BitVector) {
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
def paysDirectlyToWallet: Boolean = channelType.paysDirectlyToWallet
}

object ChannelVersion {
Expand All @@ -518,6 +540,10 @@ object ChannelVersion {

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

/**
* Pick the channel version that should be applied based on features alone (in case our peer doesn't support explicit
* channel type negotiation).
*/
def pickChannelVersion(localFeatures: Features, remoteFeatures: Features): ChannelVersion = {
if (Features.canUseFeature(localFeatures, remoteFeatures, Features.AnchorOutputs)) {
ANCHOR_OUTPUTS
Expand All @@ -528,6 +554,17 @@ object ChannelVersion {
}
}

/** Pick a channel version that matches the negotiated channel type. */
def pickChannelVersion(channelType: ChannelType): ChannelVersion = {
if (channelType.features.hasFeature(Features.AnchorOutputs)) {
ANCHOR_OUTPUTS
} else if (channelType.features.hasFeature(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
Expand Down
11 changes: 9 additions & 2 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ object Helpers {
/**
* Called by the funder
*/
def validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel): Either[ChannelException, Unit] = {
def validateParamsFunder(nodeParams: NodeParams, open: OpenChannel, accept: AcceptChannel, proposedChannelVersion: ChannelVersion): Either[ChannelException, ChannelVersion] = {
if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))
// only enforce dust limit check on mainnet
if (nodeParams.chainHash == Block.LivenetGenesisBlock.hash) {
Expand All @@ -162,7 +162,14 @@ object Helpers {
val reserveToFundingRatio = accept.channelReserveSatoshis.toLong.toDouble / Math.max(open.fundingSatoshis.toLong, 1)
if (reserveToFundingRatio > nodeParams.maxReserveToFundingRatio) return Left(ChannelReserveTooHigh(open.temporaryChannelId, accept.channelReserveSatoshis, reserveToFundingRatio, nodeParams.maxReserveToFundingRatio))

Right()
accept.channelType_opt match {
case Some(channelType) if !open.channelTypes.contains(channelType) =>
Left(IncompatibleChannelTypes(accept.temporaryChannelId, open.channelTypes))
case Some(channelType) =>
Right(ChannelVersion.pickChannelVersion(ChannelType(channelType)))
case None =>
Right(proposedChannelVersion)
}
}

/**
Expand Down
Loading