Skip to content

Commit

Permalink
Activate support for variable-length onion (#1087)
Browse files Browse the repository at this point in the history
This is now enabled by default.
We forward variable-length onions if we receive some.
We accept variable-length payments.
However for maximum compatibility with the network, we send payments using legacy payloads.
  • Loading branch information
t-bast committed Sep 5, 2019
1 parent 4bea855 commit 0bc77f2
Show file tree
Hide file tree
Showing 35 changed files with 873 additions and 406 deletions.
2 changes: 1 addition & 1 deletion eclair-core/src/main/resources/reference.conf
Expand Up @@ -35,7 +35,7 @@ eclair {
node-alias = "eclair"
node-color = "49daaa"

global-features = ""
global-features = "0200" // variable_length_onion
local-features = "8a" // initial_routing_sync + option_data_loss_protect + option_channel_range_queries
override-features = [ // optional per-node features
# {
Expand Down
11 changes: 6 additions & 5 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Expand Up @@ -29,7 +29,8 @@ import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.{IncomingPayment, NetworkFee, OutgoingPayment, Stats}
import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo}
import fr.acinq.eclair.io.{NodeURI, Peer}
import fr.acinq.eclair.payment.PaymentLifecycle._
import fr.acinq.eclair.payment.PaymentInitiator.SendPaymentRequest
import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse, Router}
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
Expand Down Expand Up @@ -186,7 +187,7 @@ class EclairImpl(appKit: Kit) extends Eclair {
}

override def sendToRoute(route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID] = {
(appKit.paymentInitiator ? SendPaymentToRoute(amount, paymentHash, route, finalCltvExpiryDelta)).mapTo[UUID]
(appKit.paymentInitiator ? SendPaymentRequest(amount, paymentHash, route.last, 1, finalCltvExpiryDelta, route)).mapTo[UUID]
}

override def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, invoice_opt: Option[PaymentRequest], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
Expand All @@ -202,12 +203,12 @@ class EclairImpl(appKit: Kit) extends Eclair {
case Some(invoice) if invoice.isExpired => Future.failed(new IllegalArgumentException("invoice has expired"))
case Some(invoice) =>
val sendPayment = invoice.minFinalCltvExpiryDelta match {
case Some(minFinalCltvExpiryDelta) => SendPayment(amount, paymentHash, recipientNodeId, invoice.routingInfo, minFinalCltvExpiryDelta, maxAttempts = maxAttempts, routeParams = Some(routeParams))
case None => SendPayment(amount, paymentHash, recipientNodeId, invoice.routingInfo, maxAttempts = maxAttempts, routeParams = Some(routeParams))
case Some(minFinalCltvExpiryDelta) => SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, minFinalCltvExpiryDelta, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams))
case None => SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts, assistedRoutes = invoice.routingInfo, routeParams = Some(routeParams))
}
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
case None =>
val sendPayment = SendPayment(amount, paymentHash, recipientNodeId, maxAttempts = maxAttempts, routeParams = Some(routeParams))
val sendPayment = SendPaymentRequest(amount, paymentHash, recipientNodeId, maxAttempts = maxAttempts, routeParams = Some(routeParams))
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
}
}
Expand Down
33 changes: 21 additions & 12 deletions eclair-core/src/main/scala/fr/acinq/eclair/Features.scala
Expand Up @@ -16,10 +16,7 @@

package fr.acinq.eclair


import java.util.BitSet

import scodec.bits.ByteVector
import scodec.bits.{BitVector, ByteVector}

/**
* Created by PM on 13/02/2017.
Expand All @@ -38,18 +35,29 @@ object Features {
val VARIABLE_LENGTH_ONION_MANDATORY = 8
val VARIABLE_LENGTH_ONION_OPTIONAL = 9

def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit)
// Note that BitVector indexes from left to right whereas the specification indexes from right to left.
// This is why we have to reverse the bits to check if a feature is set.

def hasFeature(features: BitVector, bit: Int): Boolean = if (features.sizeLessThanOrEqual(bit)) false else features.reverse.get(bit)

def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit)
def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(features.bits, bit)

/**
* We currently don't distinguish mandatory and optional. Interpreting VARIABLE_LENGTH_ONION_MANDATORY strictly would
* be very restrictive and probably fork us out of the network.
* We may implement this distinction later, but for now both flags are interpreted as an optional support.
*/
def hasVariableLengthOnion(features: ByteVector): Boolean = hasFeature(features, VARIABLE_LENGTH_ONION_MANDATORY) || hasFeature(features, VARIABLE_LENGTH_ONION_OPTIONAL)

/**
* Check that the features that we understand are correctly specified, and that there are no mandatory features that
* we don't understand (even bits)
* we don't understand (even bits).
*/
def areSupported(bitset: BitSet): Boolean = {
val supportedMandatoryFeatures = Set(OPTION_DATA_LOSS_PROTECT_MANDATORY)
for (i <- 0 until bitset.length() by 2) {
if (bitset.get(i) && !supportedMandatoryFeatures.contains(i)) return false
def areSupported(features: BitVector): Boolean = {
val supportedMandatoryFeatures = Set[Long](OPTION_DATA_LOSS_PROTECT_MANDATORY, VARIABLE_LENGTH_ONION_MANDATORY)
val reversed = features.reverse
for (i <- 0L until reversed.length by 2) {
if (reversed.get(i) && !supportedMandatoryFeatures.contains(i)) return false
}

true
Expand All @@ -59,5 +67,6 @@ object Features {
* A feature set is supported if all even bits are supported.
* We just ignore unknown odd bits.
*/
def areSupported(features: ByteVector): Boolean = areSupported(BitSet.valueOf(features.reverse.toArray))
def areSupported(features: ByteVector): Boolean = areSupported(features.bits)

}
Expand Up @@ -32,6 +32,7 @@ import fr.acinq.eclair.router.Rebroadcast
import fr.acinq.eclair.transactions.{IN, OUT}
import fr.acinq.eclair.wire.{TemporaryNodeFailure, UpdateAddHtlc}
import grizzled.slf4j.Logging
import scodec.bits.ByteVector

import scala.util.Success

Expand All @@ -57,7 +58,7 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto
})
val peers = nodeParams.db.peers.listPeers()

checkBrokenHtlcsLink(channels, nodeParams.privateKey) match {
checkBrokenHtlcsLink(channels, nodeParams.privateKey, nodeParams.globalFeatures) match {
case Nil => ()
case brokenHtlcs =>
val brokenHtlcKiller = context.system.actorOf(Props[HtlcReaper], name = "htlc-reaper")
Expand Down Expand Up @@ -165,7 +166,7 @@ object Switchboard extends Logging {
*
* This check will detect this and will allow us to fast-fail HTLCs and thus preserve channels.
*/
def checkBrokenHtlcsLink(channels: Seq[HasCommitments], privateKey: PrivateKey): Seq[UpdateAddHtlc] = {
def checkBrokenHtlcsLink(channels: Seq[HasCommitments], privateKey: PrivateKey, features: ByteVector): Seq[UpdateAddHtlc] = {

// We are interested in incoming HTLCs, that have been *cross-signed* (otherwise they wouldn't have been relayed).
// They signed it first, so the HTLC will first appear in our commitment tx, and later on in their commitment when
Expand All @@ -174,7 +175,7 @@ object Switchboard extends Logging {
.flatMap(_.commitments.remoteCommit.spec.htlcs)
.filter(_.direction == OUT)
.map(_.add)
.map(Relayer.decryptPacket(_, privateKey))
.map(Relayer.decryptPacket(_, privateKey, features))
.collect { case Right(RelayPayload(add, _, _)) => add } // we only consider htlcs that are relayed, not the ones for which we are the final node

// Here we do it differently because we need the origin information.
Expand Down
Expand Up @@ -19,17 +19,18 @@ package fr.acinq.eclair.payment
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket
import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, RemoteFailure, SendPayment}
import fr.acinq.eclair.payment.PaymentInitiator.SendPaymentRequest
import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, RemoteFailure}
import fr.acinq.eclair.router.{Announcements, Data, PublicChannel}
import fr.acinq.eclair.wire.IncorrectOrUnknownPaymentDetails
import fr.acinq.eclair.{LongToBtcAmount, NodeParams, randomBytes32, secureRandom}

import scala.concurrent.duration._

/**
* This actor periodically probes the network by sending payments to random nodes. The payments will eventually fail
* because the recipient doesn't know the preimage, but it allows us to test channels and improve routing for real payments.
*/
* This actor periodically probes the network by sending payments to random nodes. The payments will eventually fail
* because the recipient doesn't know the preimage, but it allows us to test channels and improve routing for real payments.
*/
class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: ActorRef) extends Actor with ActorLogging {

import Autoprobe._
Expand All @@ -54,7 +55,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto
case Some(targetNodeId) =>
val paymentHash = randomBytes32 // we don't even know the preimage (this needs to be a secure random!)
log.info(s"sending payment probe to node=$targetNodeId payment_hash=$paymentHash")
paymentInitiator ! SendPayment(PAYMENT_AMOUNT_MSAT, paymentHash, targetNodeId, maxAttempts = 1)
paymentInitiator ! SendPaymentRequest(PAYMENT_AMOUNT_MSAT, paymentHash, targetNodeId, maxAttempts = 1)
case None =>
log.info(s"could not find a destination, re-scheduling")
scheduleProbe()
Expand Down
Expand Up @@ -17,25 +17,49 @@
package fr.acinq.eclair.payment

import java.util.UUID

import akka.actor.{Actor, ActorLogging, ActorRef, Props}
import fr.acinq.eclair.NodeParams
import fr.acinq.eclair.payment.PaymentLifecycle.GenericSendPayment
import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.channel.Channel
import fr.acinq.eclair.payment.PaymentLifecycle.{SendPayment, SendPaymentToRoute}
import fr.acinq.eclair.payment.PaymentRequest.ExtraHop
import fr.acinq.eclair.router.RouteParams
import fr.acinq.eclair.wire.Onion.FinalLegacyPayload
import fr.acinq.eclair.{CltvExpiryDelta, MilliSatoshi, NodeParams}

/**
* Created by PM on 29/08/2016.
*/
* Created by PM on 29/08/2016.
*/
class PaymentInitiator(nodeParams: NodeParams, router: ActorRef, register: ActorRef) extends Actor with ActorLogging {

override def receive: Receive = {
case c: GenericSendPayment =>
case p: PaymentInitiator.SendPaymentRequest =>
val paymentId = UUID.randomUUID()
// We add one block in order to not have our htlc fail when a new block has just been found.
val finalExpiry = (p.finalExpiryDelta + 1).toCltvExpiry
val payFsm = context.actorOf(PaymentLifecycle.props(nodeParams, paymentId, router, register))
payFsm forward c
// NB: we only generate legacy payment onions for now for maximum compatibility.
p.predefinedRoute match {
case Nil => payFsm forward SendPayment(p.paymentHash, p.targetNodeId, FinalLegacyPayload(p.amount, finalExpiry), p.maxAttempts, p.assistedRoutes, p.routeParams)
case hops => payFsm forward SendPaymentToRoute(p.paymentHash, hops, FinalLegacyPayload(p.amount, finalExpiry))
}
sender ! paymentId
}

}

object PaymentInitiator {

def props(nodeParams: NodeParams, router: ActorRef, register: ActorRef) = Props(classOf[PaymentInitiator], nodeParams, router, register)

case class SendPaymentRequest(amount: MilliSatoshi,
paymentHash: ByteVector32,
targetNodeId: PublicKey,
maxAttempts: Int,
finalExpiryDelta: CltvExpiryDelta = Channel.MIN_CLTV_EXPIRY_DELTA,
predefinedRoute: Seq[PublicKey] = Nil,
assistedRoutes: Seq[Seq[ExtraHop]] = Nil,
routeParams: Option[RouteParams] = None)

}

0 comments on commit 0bc77f2

Please sign in to comment.