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
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package fr.acinq.eclair.db
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Router.{ChannelHop, Hop, NodeHop}
import fr.acinq.eclair.router.Router.{BlindedHop, ChannelHop, Hop, NodeHop}
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId, TimestampMilli}
import scodec.bits.ByteVector

Expand Down Expand Up @@ -227,6 +227,7 @@ object HopSummary {
val shortChannelId = h match {
case ch: ChannelHop => Some(ch.shortChannelId)
case _: NodeHop => None
case _: BlindedHop => None
}
HopSummary(h.nodeId, h.nextNodeId, shortChannelId)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ import fr.acinq.eclair.io.Peer
import fr.acinq.eclair.message.OnionMessages
import fr.acinq.eclair.payment.PaymentFailure.PaymentFailedSummary
import fr.acinq.eclair.payment._
import fr.acinq.eclair.router.Router.{ChannelRelayParams, Route}
import fr.acinq.eclair.router.Router.{BlindedHop, ChannelHop, ChannelRelayParams, Route}
import fr.acinq.eclair.transactions.DirectedHtlc
import fr.acinq.eclair.transactions.Transactions._
import fr.acinq.eclair.wire.protocol.MessageOnionCodecs.blindedRouteCodec
import fr.acinq.eclair.wire.protocol.OfferTypes.PaymentInfo
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Alias, BlockHeight, CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, MilliSatoshi, ShortChannelId, TimestampMilli, TimestampSecond, UInt64, UnknownFeature}
import org.json4s
Expand Down Expand Up @@ -294,9 +295,14 @@ object ColorSerializer extends MinimalSerializer({
})

// @formatter:off
private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: ChannelRelayParams)
private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[ChannelHopJson])
object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops.map(h => ChannelHopJson(h.nodeId, h.nextNodeId, h.params))))
private sealed trait HopJson
private case class ChannelHopJson(nodeId: PublicKey, nextNodeId: PublicKey, source: ChannelRelayParams) extends HopJson
private case class BlindedHopJson(nodeId: PublicKey, nextNodeId: PublicKey, paymentInfo: PaymentInfo) extends HopJson
private case class RouteFullJson(amount: MilliSatoshi, hops: Seq[HopJson])
object RouteFullSerializer extends ConvertClassSerializer[Route](route => RouteFullJson(route.amount, route.hops.map {
case h: ChannelHop => ChannelHopJson(h.nodeId, h.nextNodeId, h.params)
case h: BlindedHop => BlindedHopJson(h.nodeId, h.nextNodeId, h.paymentInfo)
}))

private case class RouteNodeIdsJson(amount: MilliSatoshi, nodeIds: Seq[PublicKey])
object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => {
Expand All @@ -307,8 +313,12 @@ object RouteNodeIdsSerializer extends ConvertClassSerializer[Route](route => {
RouteNodeIdsJson(route.amount, nodeIds)
})

private case class RouteShortChannelIdsJson(amount: MilliSatoshi, shortChannelIds: Seq[ShortChannelId])
object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route => RouteShortChannelIdsJson(route.amount, route.hops.map(_.shortChannelId)))
private case class RouteShortChannelIdsJson(amount: MilliSatoshi, shortChannelIds: Seq[String])
object RouteShortChannelIdsSerializer extends ConvertClassSerializer[Route](route =>
RouteShortChannelIdsJson(route.amount, route.hops.map {
case hop: ChannelHop => hop.shortChannelId.toString
case _: BlindedHop => "blinded"
}))
// @formatter:on

// @formatter:off
Expand Down Expand Up @@ -395,7 +405,7 @@ object InvoiceSerializer extends MinimalSerializer({
case p: Bolt12Invoice =>
val fieldList = List(
JField("amount", JLong(p.amount.toLong)),
JField("nodeId", JString(p.nodeId.toString())),
JField("nodeId", JString(p.signingNodeId.toString())),
JField("paymentHash", JString(p.paymentHash.toString())),
p.description.fold(string => JField("description", JString(string)), hash => JField("descriptionHash", JString(hash.toHex))),
JField("features", Extraction.decompose(p.features)(
Expand All @@ -404,10 +414,10 @@ object InvoiceSerializer extends MinimalSerializer({
FeatureSupportSerializer +
UnknownFeatureSerializer
)),
JField("blindedPaths", JArray(p.blindedPaths.map(path => {
JField("blindedPaths", JArray(p.extraEdges.map(path => {
JObject(List(
JField("introductionNodeId", JString(path.introductionNodeId.toString())),
JField("blindedNodeIds", JArray(path.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList))
JField("introductionNodeId", JString(path.path.introductionNodeId.toString())),
JField("blindedNodeIds", JArray(path.path.blindedNodes.map(n => JString(n.blindedPublicKey.toString())).toList))
))
}).toList)),
JField("createdAt", JLong(p.createdAt.toLong)),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ package fr.acinq.eclair.payment
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32}
import fr.acinq.eclair.{CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload.Partial
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, PerHopPayload}
import fr.acinq.eclair.wire.protocol.{GenericTlv, OnionRoutingPacket, PaymentOnion}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, FeatureSupport, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond, randomBytes32}
import scodec.bits.{BitVector, ByteOrdering, ByteVector}
import scodec.codecs.{list, ubyte}
import scodec.{Codec, Err}
Expand Down Expand Up @@ -129,6 +132,15 @@ case class Bolt11Invoice(prefix: String, amount_opt: Option[MilliSatoshi], creat
val int5s = eight2fiveCodec.decode(data).require.value
Bech32.encode(hrp, int5s.toArray, Bech32.Encoding.Bech32)
}

override def singlePartFinalPayload(amount: MilliSatoshi, expiry: CltvExpiry, userCustomTlvs: Seq[GenericTlv]): FinalPayload.Standard =
FinalPayload.Standard.createSinglePartPayload(amount, expiry, paymentSecret, paymentMetadata, userCustomTlvs)

override def multiPartFinalPayload(totalAmount: MilliSatoshi, expiry: CltvExpiry, userCustomTlvs: Seq[GenericTlv]): FinalPayload.Standard.Partial =
FinalPayload.Standard.createMultiPartPayload(totalAmount, expiry, paymentSecret, paymentMetadata, userCustomTlvs = userCustomTlvs)

override def trampolinePayload(totalAmount: MilliSatoshi, expiry: CltvExpiry, trampolineSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): FinalPayload.Standard.Partial =
FinalPayload.Standard.createTrampolinePayload(totalAmount, expiry, trampolineSecret, trampolinePacket, paymentMetadata)
}

object Bolt11Invoice {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
package fr.acinq.eclair.payment

import fr.acinq.bitcoin.Bech32
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.scalacompat.{Block, ByteVector32, ByteVector64, Crypto}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding
import fr.acinq.eclair.payment.Invoice.BlindedEdge
import fr.acinq.eclair.wire.protocol.OfferTypes._
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{InvalidTlvPayload, MissingRequiredTlv}
import fr.acinq.eclair.wire.protocol.{OfferCodecs, OfferTypes, TlvStream}
import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, TimestampSecond, UInt64, randomBytes32}
import fr.acinq.eclair.wire.protocol.PaymentOnion.{BlindedPerHopPayload, FinalPayload}
import fr.acinq.eclair.wire.protocol.{GenericTlv, OfferCodecs, OfferTypes, OnionRoutingPacket, TlvStream}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, TimestampSecond, UInt64, randomKey}
import scodec.bits.ByteVector

import java.util.concurrent.TimeUnit
Expand All @@ -41,19 +42,20 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {

val amount: MilliSatoshi = records.get[Amount].map(_.amount).get
override val amount_opt: Option[MilliSatoshi] = Some(amount)
override val nodeId: Crypto.PublicKey = records.get[NodeId].get.publicKey
override val nodeId: Crypto.PublicKey = randomKey().publicKey
override val paymentHash: ByteVector32 = records.get[PaymentHash].get.hash
override val paymentSecret: ByteVector32 = randomBytes32()
override val paymentMetadata: Option[ByteVector] = None
override val description: Either[String, ByteVector32] = Left(records.get[Description].get.description)
override val extraEdges: Seq[Invoice.ExtraEdge] = Seq.empty // TODO: the blinded paths need to be converted to graph edges
override val extraEdges: Seq[BlindedEdge] = records.get[Paths].get.paths.zip(records.get[PaymentPathsInfo].get.paymentInfo).map {
case (path, payInfo) => BlindedEdge(path, payInfo, nodeId)
}
override val createdAt: TimestampSecond = records.get[CreatedAt].get.timestamp
override val relativeExpiry: FiniteDuration = FiniteDuration(records.get[RelativeExpiry].map(_.seconds).getOrElse(DEFAULT_EXPIRY_SECONDS), TimeUnit.SECONDS)
override val minFinalCltvExpiryDelta: CltvExpiryDelta = records.get[Cltv].map(_.minFinalCltvExpiry).getOrElse(DEFAULT_MIN_FINAL_EXPIRY_DELTA)
override val features: Features[InvoiceFeature] = records.get[FeaturesTlv].map(_.features.invoiceFeatures()).getOrElse(Features.empty)
val signingNodeId: PublicKey = records.get[NodeId].get.publicKey
val chain: ByteVector32 = records.get[Chain].map(_.hash).getOrElse(Block.LivenetGenesisBlock.hash)
val offerId: Option[ByteVector32] = records.get[OfferId].map(_.offerId)
val blindedPaths: Seq[RouteBlinding.BlindedRoute] = records.get[Paths].get.paths
val issuer: Option[String] = records.get[Issuer].map(_.issuer)
val quantity: Option[Long] = records.get[Quantity].map(_.quantity)
val refundFor: Option[ByteVector32] = records.get[RefundFor].map(_.refundedPaymentHash)
Expand All @@ -67,7 +69,7 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {

// It is assumed that the request is valid for this offer.
def isValidFor(offer: Offer, request: InvoiceRequest): Boolean = {
nodeId == offer.nodeId &&
signingNodeId == offer.nodeId &&
checkSignature() &&
offerId.contains(request.offerId) &&
request.chain == chain &&
Expand All @@ -91,14 +93,22 @@ case class Bolt12Invoice(records: TlvStream[InvoiceTlv]) extends Invoice {
}

def checkSignature(): Boolean = {
verifySchnorr(signatureTag("signature"), rootHash(OfferTypes.removeSignature(records), OfferCodecs.invoiceTlvCodec), signature, OfferTypes.xOnlyPublicKey(nodeId))
verifySchnorr(signatureTag("signature"), rootHash(OfferTypes.removeSignature(records), OfferCodecs.invoiceTlvCodec), signature, OfferTypes.xOnlyPublicKey(signingNodeId))
}

override def toString: String = {
val data = OfferCodecs.invoiceTlvCodec.encode(records).require.bytes
Bech32.encodeBytes(hrp, data.toArray, Bech32.Encoding.Beck32WithoutChecksum)
}

override def singlePartFinalPayload(amount: MilliSatoshi, expiry: CltvExpiry, userCustomTlvs: Seq[GenericTlv]): BlindedPerHopPayload =
FinalPayload.Blinded.createSinglePartPayload(amount, userCustomTlvs)

override def multiPartFinalPayload(totalAmount: MilliSatoshi, expiry: CltvExpiry, userCustomTlvs: Seq[GenericTlv]): FinalPayload.Blinded.Partial =
FinalPayload.Blinded.createMultiPartPayload(totalAmount, userCustomTlvs)

override def trampolinePayload(totalAmount: MilliSatoshi, expiry: CltvExpiry, trampolineSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): FinalPayload.Blinded.Partial =
FinalPayload.Blinded.createTrampolinePayload(totalAmount, trampolinePacket)
}

object Bolt12Invoice {
Expand Down
30 changes: 25 additions & 5 deletions eclair-core/src/main/scala/fr/acinq/eclair/payment/Invoice.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,13 @@ package fr.acinq.eclair.payment

import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.crypto.Sphinx.RouteBlinding.BlindedRoute
import fr.acinq.eclair.payment.relay.Relayer
import fr.acinq.eclair.wire.protocol.ChannelUpdate
import fr.acinq.eclair.{CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond}
import fr.acinq.eclair.wire.protocol.OfferTypes.PaymentInfo
import fr.acinq.eclair.wire.protocol.PaymentOnion.FinalPayload.Partial
import fr.acinq.eclair.wire.protocol.PaymentOnion.PerHopPayload
import fr.acinq.eclair.wire.protocol.{ChannelUpdate, GenericTlv, OnionRoutingPacket}
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId, TimestampSecond}
import scodec.bits.ByteVector

import scala.concurrent.duration.FiniteDuration
Expand All @@ -35,8 +39,6 @@ trait Invoice {

val paymentHash: ByteVector32

val paymentSecret: ByteVector32

val paymentMetadata: Option[ByteVector]

val description: Either[String, ByteVector32]
Expand All @@ -52,19 +54,27 @@ trait Invoice {
def isExpired(): Boolean = createdAt + relativeExpiry.toSeconds <= TimestampSecond.now()

def toString: String

def singlePartFinalPayload(amount: MilliSatoshi, expiry: CltvExpiry, userCustomTlvs: Seq[GenericTlv] = Nil): PerHopPayload

def multiPartFinalPayload(totalAmount: MilliSatoshi, expiry: CltvExpiry, userCustomTlvs: Seq[GenericTlv] = Nil): Partial

def trampolinePayload(totalAmount: MilliSatoshi, expiry: CltvExpiry, trampolineSecret: ByteVector32, trampolinePacket: OnionRoutingPacket): Partial
}

object Invoice {
/** An extra edge that can be used to pay a given invoice and may not be part of the public graph. */
sealed trait ExtraEdge {
// @formatter:off
def sourceNodeId: PublicKey
def targetNodeId: PublicKey
def shortChannelId: ShortChannelId
def feeBase: MilliSatoshi
def feeProportionalMillionths: Long
def cltvExpiryDelta: CltvExpiryDelta
def htlcMinimum: MilliSatoshi
def htlcMaximum_opt: Option[MilliSatoshi]
def relayFees: Relayer.RelayFees = Relayer.RelayFees(feeBase = feeBase, feeProportionalMillionths = feeProportionalMillionths)
final def relayFees: Relayer.RelayFees = Relayer.RelayFees(feeBase = feeBase, feeProportionalMillionths = feeProportionalMillionths)
// @formatter:on
}

Expand All @@ -81,6 +91,16 @@ object Invoice {
def update(u: ChannelUpdate): BasicEdge = copy(feeBase = u.feeBaseMsat, feeProportionalMillionths = u.feeProportionalMillionths, cltvExpiryDelta = u.cltvExpiryDelta)
}

case class BlindedEdge(path: BlindedRoute, payInfo: PaymentInfo, targetNodeId: PublicKey) extends ExtraEdge {
override val sourceNodeId: PublicKey = path.introductionNodeId
override val shortChannelId: ShortChannelId = ShortChannelId.generateLocalAlias()
override val feeBase: MilliSatoshi = payInfo.feeBase
override val feeProportionalMillionths: Long = payInfo.feeProportionalMillionths
override val cltvExpiryDelta: CltvExpiryDelta = payInfo.cltvExpiryDelta
override val htlcMinimum: MilliSatoshi = payInfo.minHtlc
override val htlcMaximum_opt: Option[MilliSatoshi] = Some(payInfo.maxHtlc)
}

def fromString(input: String): Try[Invoice] = {
if (input.toLowerCase.startsWith("lni")) {
Bolt12Invoice.fromString(input)
Expand Down
Loading