Skip to content

Commit

Permalink
Remove backwards-compatibility with 400-bytes payloads
Browse files Browse the repository at this point in the history
We don't need to keep creating 400-bytes trampoline payloads, as nobody
is using eclair to _send_ trampoline payments, only to relay them, and
at that stage it is backwards-compatible to support smaller and bigger
onion sizes.
  • Loading branch information
t-bast committed Jan 12, 2024
1 parent 2dddedc commit d5b78a3
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 68 deletions.
10 changes: 5 additions & 5 deletions eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
Original file line number Diff line number Diff line change
Expand Up @@ -223,21 +223,21 @@ object Sphinx extends Logging {
nextPacket
}

def payloadsTotalSize(payloads: Seq[ByteVector]): Int = payloads.map(_.length + MacLength).sum.toInt

/**
* Create an encrypted onion packet that contains payloads for all nodes in the list.
*
* @param sessionKey session key.
* @param minPacketLength minimum length of the packet's encrypted onion payload (e.g. 1300 for standard payment onions).
* @param maxPacketLength maximum length of the packet's encrypted onion payload (e.g. 1300 for standard payment onions).
* @param packetPayloadLength length of the packet's encrypted onion payload (e.g. 1300 for standard payment onions).
* @param publicKeys node public keys (one per node).
* @param payloads payloads (one per node).
* @param associatedData associated data.
* @return An onion packet with all shared secrets. The onion packet can be sent to the first node in the list, and
* the shared secrets (one per node) can be used to parse returned failure messages if needed.
*/
def create(sessionKey: PrivateKey, minPacketLength: Int, maxPacketLength: Int, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: Option[ByteVector32]): Try[PacketAndSecrets] = Try {
val packetPayloadLength = minPacketLength max payloads.map(_.length + MacLength).sum.toInt
require(packetPayloadLength <= maxPacketLength, s"packet per-hop payloads cannot exceed $maxPacketLength bytes")
def create(sessionKey: PrivateKey, packetPayloadLength: Int, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: Option[ByteVector32]): Try[PacketAndSecrets] = Try {
require(payloadsTotalSize(payloads) <= packetPayloadLength, s"packet per-hop payloads cannot exceed $packetPayloadLength bytes")
val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys)
val filler = generateFiller("rho", packetPayloadLength, sharedsecrets.dropRight(1), payloads.dropRight(1))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ object OnionMessages {
payloadSize.toInt
}
// Since we are setting the packet size based on the payload, the onion creation should never fail (hence the `.get`).
val Sphinx.PacketAndSecrets(packet, _) = Sphinx.create(sessionKey, packetSize, packetSize, route.blindedNodes.map(_.blindedPublicKey), payloads, None).get
val Sphinx.PacketAndSecrets(packet, _) = Sphinx.create(sessionKey, packetSize, route.blindedNodes.map(_.blindedPublicKey), payloads, None).get
Right((route.introductionNodeId, OnionMessage(route.blindingKey, packet)))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.scalacompat.Crypto.{PrivateKey, PublicKey}
import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC, CannotExtractSharedSecret, Origin}
import fr.acinq.eclair.crypto.Sphinx
import fr.acinq.eclair.payment.send.Recipient
import fr.acinq.eclair.router.Router.{BlindedHop, ChannelHop, Route}
import fr.acinq.eclair.router.Router.{BlindedHop, Route}
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, PerHopPayload}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, Feature, Features, MilliSatoshi, ShortChannelId, TimestampMilli, UInt64, randomKey}
Expand Down Expand Up @@ -250,8 +250,12 @@ object OutgoingPaymentPacket {
}
// @formatter:on

/** Build an encrypted onion packet from onion payloads and node public keys. */
def buildOnion(minPacketLength: Int, maxPacketLength: Int, payloads: Seq[NodePayload], associatedData: ByteVector32): Either[OutgoingPaymentError, Sphinx.PacketAndSecrets] = {
/**
* Build an encrypted onion packet from onion payloads and node public keys.
* If packetPayloadLength_opt is provided, the onion will be padded to the requested length.
* In that case, packetPayloadLength_opt must be greater than the actual onion's content.
*/
def buildOnion(payloads: Seq[NodePayload], associatedData: ByteVector32, packetPayloadLength_opt: Option[Int]): Either[OutgoingPaymentError, Sphinx.PacketAndSecrets] = {
val sessionKey = randomKey()
val nodeIds = payloads.map(_.nodeId)
val payloadsBin = payloads
Expand All @@ -260,7 +264,8 @@ object OutgoingPaymentPacket {
case Attempt.Successful(bits) => bits.bytes
case Attempt.Failure(cause) => return Left(CannotCreateOnion(cause.message))
}
Sphinx.create(sessionKey, minPacketLength, maxPacketLength, nodeIds, payloadsBin, Some(associatedData)) match {
val packetPayloadLength = packetPayloadLength_opt.getOrElse(Sphinx.payloadsTotalSize(payloadsBin))
Sphinx.create(sessionKey, packetPayloadLength, nodeIds, payloadsBin, Some(associatedData)) match {
case Failure(f) => Left(CannotCreateOnion(f.getMessage))
case Success(packet) => Right(packet)
}
Expand Down Expand Up @@ -297,7 +302,7 @@ object OutgoingPaymentPacket {
for {
paymentTmp <- recipient.buildPayloads(paymentHash, route)
outgoing <- getOutgoingChannel(privateKey, paymentTmp, route)
onion <- buildOnion(PaymentOnionCodecs.paymentOnionPayloadLength, PaymentOnionCodecs.paymentOnionPayloadLength, outgoing.payment.payloads, paymentHash) // BOLT 2 requires that associatedData == paymentHash
onion <- buildOnion(outgoing.payment.payloads, paymentHash, Some(PaymentOnionCodecs.paymentOnionPayloadLength)) // BOLT 2 requires that associatedData == paymentHash
} yield {
val cmd = CMD_ADD_HTLC(replyTo, outgoing.payment.amount, paymentHash, outgoing.payment.expiry, onion.packet, outgoing.nextBlinding_opt, Origin.Hot(replyTo, upstream), commit = true)
OutgoingPaymentPacket(cmd, outgoing.shortChannelId, onion.sharedSecrets)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import fr.acinq.eclair.payment.Invoice.ExtraEdge
import fr.acinq.eclair.payment.OutgoingPaymentPacket._
import fr.acinq.eclair.payment.{Bolt11Invoice, Bolt12Invoice, OutgoingPaymentPacket, PaymentBlindedRoute}
import fr.acinq.eclair.router.Router._
import fr.acinq.eclair.wire.protocol.PaymentOnion.{EmptyPayload, FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload}
import fr.acinq.eclair.wire.protocol.{GenericTlv, OnionRoutingPacket, PaymentOnionCodecs}
import fr.acinq.eclair.wire.protocol.PaymentOnion.{FinalPayload, IntermediatePayload, OutgoingBlindedPerHopPayload}
import fr.acinq.eclair.wire.protocol.{GenericTlv, OnionRoutingPacket}
import fr.acinq.eclair.{CltvExpiry, Features, InvoiceFeature, MilliSatoshi, MilliSatoshiLong, ShortChannelId}
import scodec.bits.ByteVector

Expand Down Expand Up @@ -230,14 +230,14 @@ case class ClearTrampolineRecipient(invoice: Bolt11Invoice,
val finalPayload = NodePayload(nodeId, FinalPayload.Standard.createPayload(totalAmount, totalAmount, expiry, invoice.paymentSecret, invoice.paymentMetadata, customTlvs))
val trampolinePayload = NodePayload(trampolineHop.nodeId, IntermediatePayload.NodeRelay.Standard(totalAmount, expiry, nodeId))
val payloads = Seq(trampolinePayload, finalPayload)
OutgoingPaymentPacket.buildOnion(PaymentOnionCodecs.trampolineOnionPayloadLength, PaymentOnionCodecs.paymentOnionPayloadLength, payloads, paymentHash)
OutgoingPaymentPacket.buildOnion(payloads, paymentHash, packetPayloadLength_opt = None)
} else {
// The recipient doesn't support trampoline: the trampoline node will convert the payment to a non-trampoline payment.
// The final payload will thus never reach the recipient, so we create the smallest payload possible to avoid overflowing the trampoline onion size.
val dummyFinalPayload = NodePayload(nodeId, EmptyPayload)
val dummyFinalPayload = NodePayload(nodeId, IntermediatePayload.ChannelRelay.Standard(ShortChannelId(0), 0 msat, CltvExpiry(0)))
val trampolinePayload = NodePayload(trampolineHop.nodeId, IntermediatePayload.NodeRelay.Standard.createNodeRelayToNonTrampolinePayload(totalAmount, totalAmount, expiry, nodeId, invoice))
val payloads = Seq(trampolinePayload, dummyFinalPayload)
OutgoingPaymentPacket.buildOnion(PaymentOnionCodecs.trampolineOnionPayloadLength, PaymentOnionCodecs.paymentOnionPayloadLength, payloads, paymentHash)
OutgoingPaymentPacket.buildOnion(payloads, paymentHash, packetPayloadLength_opt = None)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@ package fr.acinq.eclair.wire.protocol

import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.UInt64
import fr.acinq.eclair.wire.protocol.CommonCodecs.{bytes32, varintoverflow}
import fr.acinq.eclair.wire.protocol.CommonCodecs.bytes32
import scodec.Codec
import scodec.bits.ByteVector
import scodec.bits.{BitVector, ByteVector}
import scodec.codecs._
import shapeless.HNil

/**
* Created by t-bast on 05/07/2019.
Expand All @@ -47,17 +46,18 @@ object OnionRoutingCodecs {
("onionPayload" | bytes(payloadLength)) ::
("hmac" | bytes32)).as[OnionRoutingPacket]


/**
* This codec encodes onion packets of variable sizes, and decodes the whole input byte stream into an onion packet.
* When decoding, the caller must ensure that they provide only the bytes that contain the onion packet.
*/
val variableSizeOnionRoutingPacketCodec: Codec[OnionRoutingPacket] = (
variableSizePrefixedBytesLong(varintoverflow.xmap(l => l - 66, l => l + 66),
("version" | uint8) ::
("publicKey" | bytes(33)),
("onionPayload" | bytes)) ::
("hmac" | bytes32)).xmap[OnionRoutingPacket](
{
case shapeless.::((shapeless.::(version, shapeless.::(publicKey, HNil)), payload), shapeless.::(hmac, HNil)) => OnionRoutingPacket(version, publicKey, payload, hmac)
}, {
case OnionRoutingPacket(version, publicKey, payload, hmac) => shapeless.::((shapeless.::(version, shapeless.::(publicKey, HNil)), payload), shapeless.::(hmac, HNil))
}
)
("version" | uint8) ::
("publicKey" | bytes(33)) ::
("onionPayload" | Codec(
// We simply encode the whole payload, nothing fancy here.
(payload: ByteVector) => bytes(payload.length.toInt).encode(payload),
// We stop 32 bytes before the end to avoid reading the hmac.
(bin: BitVector) => bytes((bin.length.toInt / 8) - 32).decode(bin))) ::
("hmac" | bytes32)).as[OnionRoutingPacket]

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.payment.Bolt11Invoice
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv, variableSizeOnionRoutingPacketCodec}
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, InvalidTlvPayload, MissingRequiredTlv}
import fr.acinq.eclair.wire.protocol.TlvCodecs._
import fr.acinq.eclair.{CltvExpiry, Features, MilliSatoshi, MilliSatoshiLong, ShortChannelId, UInt64}
import scodec.bits.{BitVector, ByteVector}
Expand Down Expand Up @@ -217,10 +217,6 @@ object PaymentOnion {
def records: TlvStream[OnionPaymentPayloadTlv]
}

case object EmptyPayload extends PerHopPayload {
override def records: TlvStream[OnionPaymentPayloadTlv] = TlvStream.empty
}

/** Per-hop payload for an intermediate node. */
sealed trait IntermediatePayload extends PerHopPayload

Expand Down Expand Up @@ -478,9 +474,7 @@ object PaymentOnionCodecs {
import scodec.{Codec, DecodeResult, Decoder}

val paymentOnionPayloadLength = 1300
val trampolineOnionPayloadLength = 400
val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = OnionRoutingCodecs.onionRoutingPacketCodec(paymentOnionPayloadLength)
val trampolineOnionPacketCodec: Codec[OnionRoutingPacket] = OnionRoutingCodecs.onionRoutingPacketCodec(trampolineOnionPayloadLength)

/**
* The 1.1 BOLT spec changed the payment onion frame format to use variable-length per-hop payloads.
Expand Down Expand Up @@ -513,7 +507,7 @@ object PaymentOnionCodecs {

private val invoiceRoutingInfo: Codec[InvoiceRoutingInfo] = tlvField(list(listOfN(uint8, Bolt11Invoice.Codecs.extraHopCodec)))

private val trampolineOnion: Codec[TrampolineOnion] = variableSizeOnionRoutingPacketCodec.as[TrampolineOnion]
private val trampolineOnion: Codec[TrampolineOnion] = tlvField(OnionRoutingCodecs.variableSizeOnionRoutingPacketCodec)

private val keySend: Codec[KeySend] = tlvField(bytes32)

Expand Down
Loading

0 comments on commit d5b78a3

Please sign in to comment.