Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove support for legacy Sphinx payloads #2190

Merged
merged 2 commits into from
Sep 5, 2022
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,11 @@ import kamon.Kamon
object Monitoring {

object Metrics {
val OnionPayloadFormat = Kamon.counter("crypto.sphinx.onion-payload-format")
val SignTxCount = Kamon.counter("crypto.keymanager.sign.count")
val SignTxDuration = Kamon.timer("crypto.keymanager.sign.duration")
}

object Tags {
val LegacyOnion = "legacy"
val TxOwner = "txOwner"
val TxType = "txType"

Expand Down
37 changes: 15 additions & 22 deletions eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,11 @@ object Sphinx extends Logging {
* Peek at the first bytes of the per-hop payload to extract its length.
*/
def peekPayloadLength(payload: ByteVector): Int = {
payload.head match {
case 0 =>
// The 1.0 BOLT spec used 65-bytes frames inside the onion payload.
// The first byte of the frame (called `realm`) is set to 0x00, followed by 32 bytes of per-hop data, followed by a 32-bytes mac.
Metrics.OnionPayloadFormat.withTag(Tags.LegacyOnion, value = true).increment()
65
case _ =>
// The 1.1 BOLT spec changed the frame format to use variable-length per-hop payloads.
// The first bytes contain a varint encoding the length of the payload data (not including the trailing mac).
// Since messages are always smaller than 65535 bytes, this varint will either be 1 or 3 bytes long.
Metrics.OnionPayloadFormat.withTag(Tags.LegacyOnion, value = false).increment()
MacLength + PaymentOnionCodecs.payloadLengthDecoder.decode(payload.bits).require.value.toInt
}
require(payload.head != 0, "legacy onion format is not supported anymore")
pm47 marked this conversation as resolved.
Show resolved Hide resolved
// Each onion frame contains a variable-length per-hop payload.
// The first bytes contain a varint encoding the length of the payload data (not including the trailing mac).
// Since messages are always smaller than 65535 bytes, this varint will either be 1 or 3 bytes long.
MacLength + PaymentOnionCodecs.payloadLengthDecoder.decode(payload.bits).require.value.toInt
}

/**
Expand Down Expand Up @@ -174,15 +166,16 @@ object Sphinx extends Logging {
// we have to pessimistically generate a long cipher stream.
val stream = generateStream(rho, 2 * packet.payload.length.toInt)
val bin = (packet.payload ++ ByteVector.fill(packet.payload.length)(0)) xor stream

val perHopPayloadLength = peekPayloadLength(bin)
val perHopPayload = bin.take(perHopPayloadLength - MacLength)

val hmac = ByteVector32(bin.slice(perHopPayloadLength - MacLength, perHopPayloadLength))
val nextOnionPayload = bin.drop(perHopPayloadLength).take(packet.payload.length)
val nextPubKey = blind(packetEphKey, computeBlindingFactor(packetEphKey, sharedSecret))

Right(DecryptedPacket(perHopPayload, OnionRoutingPacket(Version, nextPubKey.value, nextOnionPayload, hmac), sharedSecret))
Try(peekPayloadLength(bin)) match {
case Success(perHopPayloadLength) =>
val perHopPayload = bin.take(perHopPayloadLength - MacLength)
val hmac = ByteVector32(bin.slice(perHopPayloadLength - MacLength, perHopPayloadLength))
val nextOnionPayload = bin.drop(perHopPayloadLength).take(packet.payload.length)
val nextPubKey = blind(packetEphKey, computeBlindingFactor(packetEphKey, sharedSecret))
Right(DecryptedPacket(perHopPayload, OnionRoutingPacket(Version, nextPubKey.value, nextOnionPayload, hmac), sharedSecret))
case Failure(_) =>
Left(InvalidOnionVersion(hash(packet)))
}
} else {
Left(InvalidOnionHmac(hash(packet)))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,6 @@ object IncomingPaymentPacket {
}
case _ if add.blinding_opt.isDefined => Left(InvalidOnionPayload(UInt64(12), 0))
// NB: we don't validate the ChannelRelayPacket here because its fees and cltv depend on what channel we'll choose to use.
case payload: PaymentOnion.RelayLegacyPayload => Right(ChannelRelayPacket(add, payload, next, None))
case payload: PaymentOnion.ChannelRelayTlvPayload => Right(ChannelRelayPacket(add, payload, next, None))
}
case Right(DecodedOnionPacket(payload: PaymentOnion.FinalPayload, _)) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package fr.acinq.eclair.wire.protocol

import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.payment.{Bolt11Invoice, Invoice}
import fr.acinq.eclair.payment.Bolt11Invoice
import fr.acinq.eclair.wire.protocol.CommonCodecs._
import fr.acinq.eclair.wire.protocol.OnionRoutingCodecs.{ForbiddenTlv, MissingRequiredTlv}
import fr.acinq.eclair.wire.protocol.TlvCodecs._
Expand Down Expand Up @@ -187,25 +187,25 @@ object PaymentOnion {
/*
* We use the following architecture for payment onion payloads:
*
* PerHopPayload
* |
* |
* +--------------------------+------------------------------+
* | |
* | |
* RelayPayload FinalPayload
* | |
* | |
* +-------------------------+---------------------------+ +--------+---------+
* | | | |
* | | | |
* ChannelRelayPayload | | |
* | | | |
* | | | |
* +--------------------------+--------------------------+ | | |
* | | | | | |
* | | | | | |
* RelayLegacyPayload ChannelRelayTlvPayload BlindedChannelRelayPayload NodeRelayPayload FinalTlvPayload BlindedFinalPayload
* PerHopPayload
* |
* |
* +--------------------------+---------------------+
* | |
* | |
* RelayPayload FinalPayload
* | |
* | |
* +---------------------------------------+ +--------+---------+
* | | | |
* | | | |
* ChannelRelayPayload | | |
* | | | |
* | | | |
* +---------------------------------+ | | |
* | | | | |
* | | | | |
* ChannelRelayTlvPayload BlindedChannelRelayPayload NodeRelayPayload FinalTlvPayload BlindedFinalPayload
*
* We also introduce additional traits to separate payloads based on the type of onion packet they can be used with (PacketType).
*/
Expand All @@ -223,7 +223,9 @@ object PaymentOnion {
sealed trait TrampolinePacket extends PacketType

/** Per-hop payload from an HTLC's payment onion (after decryption and decoding). */
sealed trait PerHopPayload
sealed trait PerHopPayload {
def records: TlvStream[OnionPaymentPayloadTlv]
}

/** Per-hop payload for an intermediate node. */
sealed trait RelayPayload extends PerHopPayload
Expand All @@ -245,8 +247,6 @@ object PaymentOnion {
val expiry: CltvExpiry
}

case class RelayLegacyPayload(outgoingChannelId: ShortChannelId, amountToForward: MilliSatoshi, outgoingCltv: CltvExpiry) extends ChannelRelayPayload with ChannelRelayData

case class ChannelRelayTlvPayload(records: TlvStream[OnionPaymentPayloadTlv]) extends ChannelRelayPayload with ChannelRelayData {
override val amountToForward = records.get[AmountToForward].get.amount
override val outgoingCltv = records.get[OutgoingCltv].get.cltv
Expand Down Expand Up @@ -410,28 +410,20 @@ object PaymentOnionCodecs {

val tlvPerHopPayloadCodec: Codec[TlvStream[OnionPaymentPayloadTlv]] = TlvCodecs.lengthPrefixedTlvStream[OnionPaymentPayloadTlv](onionTlvCodec).complete

private val legacyRelayPerHopPayloadCodec: Codec[RelayLegacyPayload] = (
("realm" | constant(ByteVector.fromByte(0))) ::
("short_channel_id" | shortchannelid) ::
("amt_to_forward" | millisatoshi) ::
("outgoing_cltv_value" | cltvExpiry) ::
("unused_with_v0_version_on_header" | ignore(8 * 12))).as[RelayLegacyPayload]

val channelRelayPerHopPayloadCodec: Codec[ChannelRelayPayload] = fallback(tlvPerHopPayloadCodec, legacyRelayPerHopPayloadCodec).narrow({
case Left(tlvs) => tlvs.get[EncryptedRecipientData] match {
case Some(_) if tlvs.get[AmountToForward].isDefined => Attempt.failure(ForbiddenTlv(UInt64(2)))
case Some(_) if tlvs.get[OutgoingCltv].isDefined => Attempt.failure(ForbiddenTlv(UInt64(4)))
case Some(_) => Attempt.successful(BlindedChannelRelayPayload(tlvs))
case None if tlvs.get[AmountToForward].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(2)))
case None if tlvs.get[OutgoingCltv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4)))
case None if tlvs.get[OutgoingChannelId].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(6)))
case None => Attempt.successful(ChannelRelayTlvPayload(tlvs))
}
case Right(legacy) => Attempt.successful(legacy)
val channelRelayPerHopPayloadCodec: Codec[ChannelRelayPayload] = tlvPerHopPayloadCodec.narrow({
tlvs =>
tlvs.get[EncryptedRecipientData] match {
case Some(_) if tlvs.get[AmountToForward].isDefined => Attempt.failure(ForbiddenTlv(UInt64(2)))
case Some(_) if tlvs.get[OutgoingCltv].isDefined => Attempt.failure(ForbiddenTlv(UInt64(4)))
case Some(_) => Attempt.successful(BlindedChannelRelayPayload(tlvs))
case None if tlvs.get[AmountToForward].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(2)))
case None if tlvs.get[OutgoingCltv].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(4)))
case None if tlvs.get[OutgoingChannelId].isEmpty => Attempt.failure(MissingRequiredTlv(UInt64(6)))
case None => Attempt.successful(ChannelRelayTlvPayload(tlvs))
}
}, {
case legacy: RelayLegacyPayload => Right(legacy)
case ChannelRelayTlvPayload(tlvs) => Left(tlvs)
case BlindedChannelRelayPayload(tlvs) => Left(tlvs)
case ChannelRelayTlvPayload(tlvs) => tlvs
case BlindedChannelRelayPayload(tlvs) => tlvs
})

val nodeRelayPerHopPayloadCodec: Codec[NodeRelayPayload] = tlvPerHopPayloadCodec.narrow({
Expand Down
Loading