Skip to content
Merged
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
61 changes: 61 additions & 0 deletions eclair-core/src/main/scala/fr/acinq/eclair/CltvExpiry.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright 2019 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

/**
* Created by t-bast on 21/08/2019.
*/

/**
* Bitcoin scripts (in particular HTLCs) need an absolute block expiry (greater than the current block count) to work
* with OP_CLTV.
*
* @param underlying the absolute cltv expiry value (current block count + some delta).
*/
case class CltvExpiry(private val underlying: Long) extends Ordered[CltvExpiry] {
// @formatter:off
def +(d: CltvExpiryDelta): CltvExpiry = CltvExpiry(underlying + d.toInt)
def -(d: CltvExpiryDelta): CltvExpiry = CltvExpiry(underlying - d.toInt)
def -(other: CltvExpiry): CltvExpiryDelta = CltvExpiryDelta((underlying - other.underlying).toInt)
override def compare(other: CltvExpiry): Int = underlying.compareTo(other.underlying)
def toLong: Long = underlying
// @formatter:on
}

/**
* Channels advertise a cltv expiry delta that should be used when routing through them.
* This value needs to be converted to a [[fr.acinq.eclair.CltvExpiry]] to be used in OP_CLTV.
*
* CltvExpiryDelta can also be used when working with OP_CSV which is by design a delta.
*
* @param underlying the cltv expiry delta value.
*/
case class CltvExpiryDelta(private val underlying: Int) extends Ordered[CltvExpiryDelta] {

/**
* Adds the current block height to the given delta to obtain an absolute expiry.
*/
def toCltvExpiry = CltvExpiry(Globals.blockCount.get() + underlying)

// @formatter:off
def +(other: Int): CltvExpiryDelta = CltvExpiryDelta(underlying + other)
def +(other: CltvExpiryDelta): CltvExpiryDelta = CltvExpiryDelta(underlying + other.underlying)
def compare(other: CltvExpiryDelta): Int = underlying.compareTo(other.underlying)
def toInt: Int = underlying
// @formatter:on

}
26 changes: 12 additions & 14 deletions eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,20 @@ import akka.pattern._
import akka.util.Timeout
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.TimestampQueryFilters._
import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId}
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._
import fr.acinq.eclair.router.{ChannelDesc, RouteRequest, RouteResponse, Router}
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
import scodec.bits.ByteVector

import scala.concurrent.Future
import scala.concurrent.duration._
import fr.acinq.eclair.payment.{GetUsableBalances, PaymentReceived, PaymentRelayed, PaymentRequest, PaymentSent, UsableBalances}
import fr.acinq.eclair.wire.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement}
import TimestampQueryFilters._
import scala.concurrent.{ExecutionContext, Future}

case class GetInfoResponse(nodeId: PublicKey, alias: String, chainHash: ByteVector32, blockHeight: Int, publicAddresses: Seq[NodeAddress])

Expand Down Expand Up @@ -78,13 +78,13 @@ trait Eclair {

def receivedInfo(paymentHash: ByteVector32)(implicit timeout: Timeout): Future[Option[IncomingPayment]]

def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]
def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiryDelta_opt: Option[CltvExpiryDelta] = None, maxAttempts_opt: Option[Int] = None, feeThresholdSat_opt: Option[Satoshi] = None, maxFeePct_opt: Option[Double] = None)(implicit timeout: Timeout): Future[UUID]

def sentInfo(id: Either[UUID, ByteVector32])(implicit timeout: Timeout): Future[Seq[OutgoingPayment]]

def findRoute(targetNodeId: PublicKey, amount: MilliSatoshi, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty)(implicit timeout: Timeout): Future[RouteResponse]

def sendToRoute(route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID]
def sendToRoute(route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiryDelta: CltvExpiryDelta)(implicit timeout: Timeout): Future[UUID]

def audit(from_opt: Option[Long], to_opt: Option[Long])(implicit timeout: Timeout): Future[AuditResponse]

Expand All @@ -111,7 +111,7 @@ trait Eclair {

class EclairImpl(appKit: Kit) extends Eclair {

implicit val ec = appKit.system.dispatcher
implicit val ec: ExecutionContext = appKit.system.dispatcher

override def connect(target: Either[NodeURI, PublicKey])(implicit timeout: Timeout): Future[String] = target match {
case Left(uri) => (appKit.switchboard ? Peer.Connect(uri)).mapTo[String]
Expand Down Expand Up @@ -186,11 +186,11 @@ class EclairImpl(appKit: Kit) extends Eclair {
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amount, assistedRoutes)).mapTo[RouteResponse]
}

override def sendToRoute(route: Seq[PublicKey], amount: MilliSatoshi, paymentHash: ByteVector32, finalCltvExpiry: Long)(implicit timeout: Timeout): Future[UUID] = {
(appKit.paymentInitiator ? SendPaymentToRoute(amount, paymentHash, route, finalCltvExpiry)).mapTo[UUID]
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]
}

override def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiry_opt: Option[Long], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
override def send(recipientNodeId: PublicKey, amount: MilliSatoshi, paymentHash: ByteVector32, assistedRoutes: Seq[Seq[PaymentRequest.ExtraHop]] = Seq.empty, minFinalCltvExpiryDelta_opt: Option[CltvExpiryDelta], maxAttempts_opt: Option[Int], feeThreshold_opt: Option[Satoshi], maxFeePct_opt: Option[Double])(implicit timeout: Timeout): Future[UUID] = {
val maxAttempts = maxAttempts_opt.getOrElse(appKit.nodeParams.maxPaymentAttempts)

val defaultRouteParams = Router.getDefaultRouteParams(appKit.nodeParams.routerConf)
Expand All @@ -199,8 +199,8 @@ class EclairImpl(appKit: Kit) extends Eclair {
maxFeeBase = feeThreshold_opt.map(_.toMilliSatoshi).getOrElse(defaultRouteParams.maxFeeBase)
)

val sendPayment = minFinalCltvExpiry_opt match {
case Some(minCltv) => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiry = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams))
val sendPayment = minFinalCltvExpiryDelta_opt match {
case Some(minCltv) => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, finalCltvExpiryDelta = minCltv, maxAttempts = maxAttempts, routeParams = Some(routeParams))
case None => SendPayment(amount, paymentHash, recipientNodeId, assistedRoutes, maxAttempts = maxAttempts, routeParams = Some(routeParams))
}
(appKit.paymentInitiator ? sendPayment).mapTo[UUID]
Expand Down Expand Up @@ -255,8 +255,6 @@ class EclairImpl(appKit: Kit) extends Eclair {
* Sends a request to a channel and expects a response
*
* @param channelIdentifier either a shortChannelId (BOLT encoded) or a channelId (32-byte hex encoded)
* @param request
* @return
*/
def sendToChannel(channelIdentifier: Either[ByteVector32, ShortChannelId], request: Any)(implicit timeout: Timeout): Future[Any] = channelIdentifier match {
case Left(channelId) => appKit.register ? Forward(channelId, request)
Expand Down
53 changes: 27 additions & 26 deletions eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ import fr.acinq.eclair.router.RouterConf
import fr.acinq.eclair.tor.Socks5ProxyParams
import fr.acinq.eclair.wire.{Color, NodeAddress}
import scodec.bits.ByteVector

import scala.collection.JavaConversions._
import scala.concurrent.duration.FiniteDuration

/**
* Created by PM on 26/02/2017.
*/
* Created by PM on 26/02/2017.
*/
case class NodeParams(keyManager: KeyManager,
alias: String,
color: Color,
Expand All @@ -50,11 +51,11 @@ case class NodeParams(keyManager: KeyManager,
onChainFeeConf: OnChainFeeConf,
maxHtlcValueInFlightMsat: UInt64,
maxAcceptedHtlcs: Int,
expiryDeltaBlocks: Int,
fulfillSafetyBeforeTimeoutBlocks: Int,
expiryDeltaBlocks: CltvExpiryDelta,
fulfillSafetyBeforeTimeoutBlocks: CltvExpiryDelta,
htlcMinimum: MilliSatoshi,
toRemoteDelayBlocks: Int,
maxToLocalDelayBlocks: Int,
toRemoteDelayBlocks: CltvExpiryDelta,
maxToLocalDelayBlocks: CltvExpiryDelta,
minDepthBlocks: Int,
feeBase: MilliSatoshi,
feeProportionalMillionth: Int,
Expand Down Expand Up @@ -90,12 +91,12 @@ object NodeParams {
object ELECTRUM extends WatcherType

/**
* Order of precedence for the configuration parameters:
* 1) Java environment variables (-D...)
* 2) Configuration file eclair.conf
* 3) Optionally provided config
* 4) Default values in reference.conf
*/
* Order of precedence for the configuration parameters:
* 1) Java environment variables (-D...)
* 2) Configuration file eclair.conf
* 3) Optionally provided config
* 4) Default values in reference.conf
*/
def loadConfiguration(datadir: File, overrideDefaults: Config = ConfigFactory.empty()) =
ConfigFactory.parseProperties(System.getProperties)
.withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf")))
Expand All @@ -104,13 +105,13 @@ object NodeParams {

def getSeed(datadir: File): ByteVector = {
val seedPath = new File(datadir, "seed.dat")
seedPath.exists() match {
case true => ByteVector(Files.readAllBytes(seedPath.toPath))
case false =>
datadir.mkdirs()
val seed = randomBytes32
Files.write(seedPath.toPath, seed.toArray)
seed
if (seedPath.exists()) {
ByteVector(Files.readAllBytes(seedPath.toPath))
} else {
datadir.mkdirs()
val seed = randomBytes32
Files.write(seedPath.toPath, seed.toArray)
seed
}
}

Expand Down Expand Up @@ -144,12 +145,12 @@ object NodeParams {
val maxAcceptedHtlcs = config.getInt("max-accepted-htlcs")
require(maxAcceptedHtlcs <= Channel.MAX_ACCEPTED_HTLCS, s"max-accepted-htlcs must be lower than ${Channel.MAX_ACCEPTED_HTLCS}")

val maxToLocalCLTV = config.getInt("max-to-local-delay-blocks")
val offeredCLTV = config.getInt("to-remote-delay-blocks")
val maxToLocalCLTV = CltvExpiryDelta(config.getInt("max-to-local-delay-blocks"))
val offeredCLTV = CltvExpiryDelta(config.getInt("to-remote-delay-blocks"))
require(maxToLocalCLTV <= Channel.MAX_TO_SELF_DELAY && offeredCLTV <= Channel.MAX_TO_SELF_DELAY, s"CLTV delay values too high, max is ${Channel.MAX_TO_SELF_DELAY}")

val expiryDeltaBlocks = config.getInt("expiry-delta-blocks")
val fulfillSafetyBeforeTimeoutBlocks = config.getInt("fulfill-safety-before-timeout-blocks")
val expiryDeltaBlocks = CltvExpiryDelta(config.getInt("expiry-delta-blocks"))
val fulfillSafetyBeforeTimeoutBlocks = CltvExpiryDelta(config.getInt("fulfill-safety-before-timeout-blocks"))
require(fulfillSafetyBeforeTimeoutBlocks < expiryDeltaBlocks, "fulfill-safety-before-timeout-blocks must be smaller than expiry-delta-blocks")

val nodeAlias = config.getString("node-alias")
Expand Down Expand Up @@ -206,8 +207,8 @@ object NodeParams {
expiryDeltaBlocks = expiryDeltaBlocks,
fulfillSafetyBeforeTimeoutBlocks = fulfillSafetyBeforeTimeoutBlocks,
htlcMinimum = MilliSatoshi(config.getInt("htlc-minimum-msat")),
toRemoteDelayBlocks = config.getInt("to-remote-delay-blocks"),
maxToLocalDelayBlocks = config.getInt("max-to-local-delay-blocks"),
toRemoteDelayBlocks = CltvExpiryDelta(config.getInt("to-remote-delay-blocks")),
maxToLocalDelayBlocks = CltvExpiryDelta(config.getInt("max-to-local-delay-blocks")),
minDepthBlocks = config.getInt("mindepth-blocks"),
feeBase = MilliSatoshi(config.getInt("fee-base-msat")),
feeProportionalMillionth = config.getInt("fee-proportional-millionths"),
Expand All @@ -231,7 +232,7 @@ object NodeParams {
routerBroadcastInterval = FiniteDuration(config.getDuration("router.broadcast-interval").getSeconds, TimeUnit.SECONDS),
randomizeRouteSelection = config.getBoolean("router.randomize-route-selection"),
searchMaxRouteLength = config.getInt("router.path-finding.max-route-length"),
searchMaxCltv = config.getInt("router.path-finding.max-cltv"),
searchMaxCltv = CltvExpiryDelta(config.getInt("router.path-finding.max-cltv")),
searchMaxFeeBase = Satoshi(config.getLong("router.path-finding.fee-threshold-sat")),
searchMaxFeePct = config.getDouble("router.path-finding.max-fee-pct"),
searchHeuristicsEnabled = config.getBoolean("router.path-finding.heuristics-enable"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import fr.acinq.eclair.router.RouteResponse
import fr.acinq.eclair.transactions.Direction
import fr.acinq.eclair.transactions.Transactions.{InputInfo, TransactionWithInputInfo}
import fr.acinq.eclair.wire._
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, UInt64}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshi, ShortChannelId, UInt64}
import org.json4s.JsonAST._
import org.json4s.{CustomKeySerializer, CustomSerializer, TypeHints, jackson}
import scodec.bits.ByteVector
Expand Down Expand Up @@ -65,6 +65,14 @@ class MilliSatoshiSerializer extends CustomSerializer[MilliSatoshi](format => ({
case x: MilliSatoshi => JInt(x.amount)
}))

class CltvExpirySerializer extends CustomSerializer[CltvExpiry](format => ({ null }, {
case x: CltvExpiry => JLong(x.toLong)
}))

class CltvExpiryDeltaSerializer extends CustomSerializer[CltvExpiryDelta](format => ({ null }, {
case x: CltvExpiryDelta => JInt(x.toInt)
}))

class ShortChannelIdSerializer extends CustomSerializer[ShortChannelId](format => ({ null }, {
case x: ShortChannelId => JString(x.toString())
}))
Expand Down Expand Up @@ -154,7 +162,7 @@ class PaymentRequestSerializer extends CustomSerializer[PaymentRequest](format =
}, {
case p: PaymentRequest => {
val expiry = p.expiry.map(ex => JField("expiry", JLong(ex))).toSeq
val minFinalCltvExpiry = p.minFinalCltvExpiry.map(mfce => JField("minFinalCltvExpiry", JLong(mfce))).toSeq
val minFinalCltvExpiry = p.minFinalCltvExpiryDelta.map(mfce => JField("minFinalCltvExpiry", JInt(mfce.toInt))).toSeq
val amount = p.amount.map(msat => JField("amount", JLong(msat.toLong))).toSeq

val fieldList = List(JField("prefix", JString(p.prefix)),
Expand Down Expand Up @@ -193,6 +201,8 @@ object JsonSupport extends Json4sSupport {
new UInt64Serializer +
new SatoshiSerializer +
new MilliSatoshiSerializer +
new CltvExpirySerializer +
new CltvExpiryDeltaSerializer +
new ShortChannelIdSerializer +
new StateSerializer +
new ShaChainSerializer +
Expand Down
12 changes: 6 additions & 6 deletions eclair-core/src/main/scala/fr/acinq/eclair/api/Service.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ import akka.stream.scaladsl.{BroadcastHub, Flow, Keep, Source}
import akka.stream.{ActorMaterializer, OverflowStrategy}
import akka.util.Timeout
import com.google.common.net.HostAndPort
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{ByteVector32, Satoshi}
import fr.acinq.eclair.api.FormParamExtractors._
import fr.acinq.eclair.api.JsonSupport.CustomTypeHints
import fr.acinq.eclair.io.NodeURI
import fr.acinq.eclair.payment.PaymentLifecycle.PaymentFailed
import fr.acinq.eclair.payment.{PaymentReceived, PaymentRequest, _}
import fr.acinq.eclair.{Eclair, MilliSatoshi, ShortChannelId}
import fr.acinq.eclair.{CltvExpiryDelta, Eclair, MilliSatoshi}
import grizzled.slf4j.Logging
import org.json4s.jackson.Serialization
import scodec.bits.ByteVector
Expand Down Expand Up @@ -224,9 +224,9 @@ trait Service extends ExtraDirectives with Logging {
path("payinvoice") {
formFields(invoiceFormParam, amountMsatFormParam.?, "maxAttempts".as[Int].?, "feeThresholdSat".as[Satoshi].?, "maxFeePct".as[Double].?) {
case (invoice@PaymentRequest(_, Some(amount), _, nodeId, _, _), None, maxAttempts, feeThresholdSat_opt, maxFeePct_opt) =>
complete(eclairApi.send(nodeId, amount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
complete(eclairApi.send(nodeId, amount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiryDelta, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case (invoice, Some(overrideAmount), maxAttempts, feeThresholdSat_opt, maxFeePct_opt) =>
complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiry, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
complete(eclairApi.send(invoice.nodeId, overrideAmount, invoice.paymentHash, invoice.routingInfo, invoice.minFinalCltvExpiryDelta, maxAttempts, feeThresholdSat_opt, maxFeePct_opt))
case _ => reject(MalformedFormFieldRejection("invoice", "The invoice must have an amount or you need to specify one using the field 'amountMsat'"))
}
} ~
Expand All @@ -236,8 +236,8 @@ trait Service extends ExtraDirectives with Logging {
}
} ~
path("sendtoroute") {
formFields(amountMsatFormParam, paymentHashFormParam, "finalCltvExpiry".as[Long], "route".as[List[PublicKey]](pubkeyListUnmarshaller)) { (amountMsat, paymentHash, finalCltvExpiry, route) =>
complete(eclairApi.sendToRoute(route, amountMsat, paymentHash, finalCltvExpiry))
formFields(amountMsatFormParam, paymentHashFormParam, "finalCltvExpiry".as[Int], "route".as[List[PublicKey]](pubkeyListUnmarshaller)) { (amountMsat, paymentHash, finalCltvExpiry, route) =>
complete(eclairApi.sendToRoute(route, amountMsat, paymentHash, CltvExpiryDelta(finalCltvExpiry)))
}
} ~
path("getsentinfo") {
Expand Down
Loading