From 2c06d41db1f0b01859d6ef242b96d592ebfcbe40 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Fri, 26 Apr 2019 17:45:58 +0200 Subject: [PATCH 01/27] Sphinx: typo in computeBlindingFactor --- .../src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 861746a8b1..cd08d4a973 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -74,7 +74,7 @@ object Sphinx extends Logging { def computeSharedSecret(pub: PublicKey, secret: PrivateKey): ByteVector32 = Crypto.sha256(pub.multiply(secret).value) - def computeblindingFactor(pub: PublicKey, secret: ByteVector): ByteVector32 = Crypto.sha256(pub.value ++ secret) + def computeBlindingFactor(pub: PublicKey, secret: ByteVector): ByteVector32 = Crypto.sha256(pub.value ++ secret) def blind(pub: PublicKey, blindingFactor: ByteVector32): PublicKey = pub.multiply(PrivateKey(blindingFactor)) @@ -90,7 +90,7 @@ object Sphinx extends Logging { def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey]): (Seq[PublicKey], Seq[ByteVector32]) = { val ephemeralPublicKey0 = blind(PublicKey(Crypto.curve.getG), sessionKey.value) val secret0 = computeSharedSecret(publicKeys.head, sessionKey) - val blindingFactor0 = computeblindingFactor(ephemeralPublicKey0, secret0) + val blindingFactor0 = computeBlindingFactor(ephemeralPublicKey0, secret0) computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, Seq(ephemeralPublicKey0), Seq(blindingFactor0), Seq(secret0)) } @@ -101,7 +101,7 @@ object Sphinx extends Logging { else { val ephemeralPublicKey = blind(ephemeralPublicKeys.last, blindingFactors.last) val secret = computeSharedSecret(blind(publicKeys.head, blindingFactors), sessionKey) - val blindingFactor = computeblindingFactor(ephemeralPublicKey, secret) + val blindingFactor = computeBlindingFactor(ephemeralPublicKey, secret) computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys.tail, ephemeralPublicKeys :+ ephemeralPublicKey, blindingFactors :+ blindingFactor, sharedSecrets :+ secret) } } @@ -189,7 +189,7 @@ object Sphinx extends Logging { val hmac = ByteVector32(bin.slice(PayloadLength, PayloadLength + MacLength)) val nextRouteInfo = bin.drop(PayloadLength + MacLength) - val nextPubKey = blind(PublicKey(packet.publicKey), computeblindingFactor(PublicKey(packet.publicKey), sharedSecret)) + val nextPubKey = blind(PublicKey(packet.publicKey), computeBlindingFactor(PublicKey(packet.publicKey), sharedSecret)) ParsedPacket(payload, Packet(Version, nextPubKey.value, hmac, nextRouteInfo), sharedSecret) } From 314e3fc7d93568efa58b6462b845a04ff9f7fd1c Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 29 Apr 2019 16:35:13 +0200 Subject: [PATCH 02/27] Sphinx: implement multi-frame onion proposal --- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 81 +++++++++++++------ .../fr/acinq/eclair/crypto/SphinxSpec.scala | 4 +- 2 files changed, 57 insertions(+), 28 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index cd08d4a973..dc1d632bce 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -41,17 +41,34 @@ object Sphinx extends Logging { // length of a MAC val MacLength = 32 - // length of a payload: 33 bytes (1 bytes for realm, 32 bytes for a realm-specific packet) - val PayloadLength = 33 - // max number of hops val MaxHops = 20 + // A frame is the smallest unit of memory that can be used by a hop to store its payload. + // Each hop may use multiple frames. + // Parts of the frame are fixed: + // - The first byte of the first frame contains the number of frames used by the payload and the realm, which indicates how the payload should be parsed. + // - The last 32 bytes of the last frame contain the HMAC that should be passed to the next hop, or 0 for the last hop. + // - All other bytes can be used to store the hop's payload. + val FrameSize = 65 + + // The maximum size a payload for a single hop can be. This is the worst case scenario of a single hop, consuming all 20 frames. + // We need to know this in order to generate a sufficiently long stream of pseudo-random bytes when encrypting/decrypting the payload. + val MaxPayloadLength = MaxHops * FrameSize + + // length of the obfuscated onion data + val RoutingInfoLength = MaxHops * FrameSize + // onion packet length - val PacketLength = 1 + 33 + MacLength + MaxHops * (PayloadLength + MacLength) + val PacketLength = 1 + 33 + MacLength + RoutingInfoLength // last packet (all zeroes except for the version byte) - val LAST_PACKET = Packet(Version, ByteVector.fill(33)(0), ByteVector32.Zeroes, ByteVector.fill(MaxHops * (PayloadLength + MacLength))(0)) + val LAST_PACKET = Packet(Version, ByteVector.fill(33)(0), ByteVector32.Zeroes, ByteVector.fill(RoutingInfoLength)(0)) + + // The 4 MSB of the first frame of the payload contains the number of frames used. + def payloadFrameCount(payload: ByteVector): Int = { + (payload.head >> 4) + 1 + } def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { val mac = new HMac(new SHA256Digest()) @@ -106,11 +123,15 @@ object Sphinx extends Logging { } } - def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], hopSize: Int, maxNumberOfHops: Int = MaxHops): ByteVector = { - sharedSecrets.foldLeft(ByteVector.empty)((padding, secret) => { + def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], payloads: Seq[ByteVector]): ByteVector = { + require(sharedSecrets.length == payloads.length, "the number of secrets should equal the number of payloads") + + (sharedSecrets zip payloads).foldLeft(ByteVector.empty)((padding, secretAndPayload) => { + val (secret, payload) = secretAndPayload + val payloadLength = FrameSize*payloadFrameCount(payload) val key = generateKey(keyType, secret) - val padding1 = padding ++ ByteVector.fill(hopSize)(0) - val stream = generateStream(key, hopSize * (maxNumberOfHops + 1)).takeRight(padding1.length) + val padding1 = padding ++ ByteVector.fill(payloadLength)(0) + val stream = generateStream(key, RoutingInfoLength + payloadLength).takeRight(padding1.length) padding1.xor(stream) }) } @@ -118,7 +139,7 @@ object Sphinx extends Logging { case class Packet(version: Int, publicKey: ByteVector, hmac: ByteVector32, routingInfo: ByteVector) { require(publicKey.length == 33, "onion packet public key length should be 33") require(hmac.length == MacLength, s"onion packet hmac length should be $MacLength") - require(routingInfo.length == MaxHops * (PayloadLength + MacLength), s"onion packet routing info length should be ${MaxHops * (PayloadLength + MacLength)}") + require(routingInfo.length == RoutingInfoLength, s"onion packet routing info length should be $RoutingInfoLength") def isLastPacket: Boolean = hmac == ByteVector32.Zeroes @@ -130,7 +151,7 @@ object Sphinx extends Logging { val version = in.read val publicKey = new Array[Byte](33) in.read(publicKey) - val routingInfo = new Array[Byte](MaxHops * (PayloadLength + MacLength)) + val routingInfo = new Array[Byte](RoutingInfoLength) in.read(routingInfo) val hmac = new Array[Byte](MacLength) in.read(hmac) @@ -176,7 +197,7 @@ object Sphinx extends Logging { * messages upstream. */ def parsePacket(privateKey: PrivateKey, associatedData: ByteVector, rawPacket: ByteVector): Try[ParsedPacket] = Try { - require(rawPacket.length == PacketLength, s"onion packet length is ${rawPacket.length}, it should be ${PacketLength}") + require(rawPacket.length == PacketLength, s"onion packet length is ${rawPacket.length}, it should be $PacketLength") val packet = Packet.read(rawPacket) val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey) val mu = generateKey("mu", sharedSecret) @@ -184,11 +205,16 @@ object Sphinx extends Logging { require(check == packet.hmac, "invalid header mac") val rho = generateKey("rho", sharedSecret) - val bin = (packet.routingInfo ++ ByteVector.fill(PayloadLength + MacLength)(0)) xor generateStream(rho, PayloadLength + MacLength + MaxHops * (PayloadLength + MacLength)) - val payload = bin.take(PayloadLength) - val hmac = ByteVector32(bin.slice(PayloadLength, PayloadLength + MacLength)) - val nextRouteInfo = bin.drop(PayloadLength + MacLength) + // Since we don't know the length of the hop payload (we will learn it once we decode the first byte), + // we have to pessimistically generate a long cipher stream. + val stream = generateStream(rho, RoutingInfoLength + MaxPayloadLength) + val bin = (packet.routingInfo ++ ByteVector.fill(MaxPayloadLength)(0)) xor stream + val payloadLength = payloadFrameCount(bin)*FrameSize + val payload = bin.take(payloadLength-MacLength) + + val hmac = ByteVector32(bin.slice(payloadLength-MacLength, payloadLength)) + val nextRouteInfo = bin.drop(payloadLength).take(RoutingInfoLength) val nextPubKey = blind(PublicKey(packet.publicKey), computeBlindingFactor(PublicKey(packet.publicKey), sharedSecret)) ParsedPacket(payload, Packet(Version, nextPubKey.value, hmac, nextRouteInfo), sharedSecret) @@ -207,23 +233,24 @@ object Sphinx extends Logging { * Compute the next packet from the current packet and node parameters. * Packets are constructed in reverse order: * - you first build the last packet - * - then you call makeNextPacket(...) until you've build the final onion packet that will be sent to the first node + * - then you call makeNextPacket(...) until you've built the final onion packet that will be sent to the first node * in the route * - * @param payload payload for this packed + * @param payload payload for this packet * @param associatedData associated data - * @param ephemeralPublicKey ephemeral key for this packed + * @param ephemeralPublicKey ephemeral key for this packet * @param sharedSecret shared secret * @param packet current packet (1 + all zeroes if this is the last packet) * @param routingInfoFiller optional routing info filler, needed only when you're constructing the last packet * @return the next packet */ private def makeNextPacket(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: ByteVector, sharedSecret: ByteVector32, packet: Packet, routingInfoFiller: ByteVector = ByteVector.empty): Packet = { - require(payload.length == PayloadLength) + require(payload.length <= MaxPayloadLength-MacLength, s"packet payload cannot exceed ${MaxPayloadLength-MacLength} bytes") + require((payload.length+MacLength) % FrameSize == 0, "the payload and mac should use a discrete number of frames") val nextRoutingInfo = { - val routingInfo1 = payload ++ packet.hmac ++ packet.routingInfo.dropRight(PayloadLength + MacLength) - val routingInfo2 = routingInfo1 xor generateStream(generateKey("rho", sharedSecret), MaxHops * (PayloadLength + MacLength)) + val routingInfo1 = payload ++ packet.hmac ++ packet.routingInfo.dropRight(payload.length + MacLength) + val routingInfo2 = routingInfo1 xor generateStream(generateKey("rho", sharedSecret), RoutingInfoLength) routingInfo2.dropRight(routingInfoFiller.length) ++ routingInfoFiller } @@ -244,8 +271,8 @@ object Sphinx extends Logging { /** * A properly decoded error from a node in the route * - * @param originNode - * @param failureMessage + * @param originNode public key of the node that generated the failure. + * @param failureMessage friendly error message. */ case class ErrorPacket(originNode: PublicKey, failureMessage: FailureMessage) @@ -260,8 +287,10 @@ object Sphinx extends Logging { * shared secrets (one per node) can be used to parse returned error messages if needed */ def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { + require(payloadFrameCount(payloads.last) == 1, "last packet should use a single frame") + val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), PayloadLength + MacLength, MaxHops) + val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) val lastPacket = makeNextPacket(payloads.last, associatedData, ephemeralPublicKeys.last.value, sharedsecrets.last, LAST_PACKET, filler) @@ -336,7 +365,7 @@ object Sphinx extends Logging { /** * - * @param sharedSecret this node's share secret + * @param sharedSecret this node's shared secret * @param packet error packet * @return true if the packet's mac is valid, which means that it has been properly de-obfuscated */ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index f0b8c42f70..893404fe0b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -73,7 +73,7 @@ class SphinxSpec extends FunSuite { */ test("generate filler") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), PayloadLength + MacLength, 20) + val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) assert(filler == hex"c6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") } @@ -93,7 +93,7 @@ class SphinxSpec extends FunSuite { assert(packets(1).hmac == ByteVector32(hex"548e58057ab0a0e6c2d8ad8e855d89f9224279a5652895ea14f60bffb81590eb")) assert(packets(2).hmac == ByteVector32(hex"0daed5f832ef34ea8d0d2cc0699134287a2739c77152d9edc8fe5ccce7ec838f")) assert(packets(3).hmac == ByteVector32(hex"62cc962876e734e089e79eda497077fb411fac5f36afd43329040ecd1e16c6d9")) - // this means that node #4 us the last node + // this means that node #4 is the last node assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } From ac6ec1a104cbcdcf46cd3eeddf62f59fd4ee6100 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Tue, 30 Apr 2019 10:42:59 +0200 Subject: [PATCH 03/27] Sphinx: add tests for multi-frame onions --- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 2 - .../fr/acinq/eclair/crypto/SphinxSpec.scala | 220 ++++++++++++------ 2 files changed, 146 insertions(+), 76 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index dc1d632bce..748aacf605 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -287,8 +287,6 @@ object Sphinx extends Logging { * shared secrets (one per node) can be used to parse returned error messages if needed */ def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { - require(payloadFrameCount(payloads.last) == 1, "last packet should use a single frame") - val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 893404fe0b..257be94d22 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -71,14 +71,23 @@ class SphinxSpec extends FunSuite { /* filler = 0xc6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac */ - test("generate filler") { + test("generate single-frame filler") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) + val filler = generateFiller("rho", sharedsecrets.dropRight(1), singleFramePayloads.dropRight(1)) assert(filler == hex"c6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") } - test("create packet (reference test vector)") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, payloads, associatedData) + /* + filler = 0x77df18cc319848a016c8239c0ebb732bad7978511122520680894c116cc99b15d3acd6b94a39c4ccba3928dc52fed3d50572159847c4772fe43f6812b02ed16b9ce08d4d8d472527872e4697e9d31e451f81e726ed53fdd417ef2ecaf30b6ed8bfc72fc71102a4417b97b9fb1a1af471eb3a4a5a33f1d8b67bba45d89449c91bf5b7346f22dc71b3081de49830f1ccdf14982abd6647169f6dfb3fbf4d12eb66b92370c34d865d6c794072125baa778da953ebe6c4448f3875096e2039d594d14443ecf2b5b9847d59a5d01f72c8162846a67f750796cbb767a42835dd015fe16493f38b9c3b6cd35f9d019d6538e993184b8eeca4e18ae02ec0b42c20749865d4ba3784137e0bc72e3ec1b347b0fad52738fd09e5f76f3f87374c217f6e42b5b5ef7b189d6e38e9a6a649e14bd1ffdd4affaedeac5577ce9b0064e1729c83c4fa313ad59abf83fe8340a09f9cc0b5e2dc78a8507bad79ffbb916d336ecb38bd9ed463855c94bb91349013d90108b10a748ec892782ed9d39322a06f77001b368d9c56fe88a60e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac + */ + test("generate multi-frame filler") { + val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) + val filler = generateFiller("rho", sharedsecrets.dropRight(1), multiFramePayloads.dropRight(1)) + assert(filler == hex"77df18cc319848a016c8239c0ebb732bad7978511122520680894c116cc99b15d3acd6b94a39c4ccba3928dc52fed3d50572159847c4772fe43f6812b02ed16b9ce08d4d8d472527872e4697e9d31e451f81e726ed53fdd417ef2ecaf30b6ed8bfc72fc71102a4417b97b9fb1a1af471eb3a4a5a33f1d8b67bba45d89449c91bf5b7346f22dc71b3081de49830f1ccdf14982abd6647169f6dfb3fbf4d12eb66b92370c34d865d6c794072125baa778da953ebe6c4448f3875096e2039d594d14443ecf2b5b9847d59a5d01f72c8162846a67f750796cbb767a42835dd015fe16493f38b9c3b6cd35f9d019d6538e993184b8eeca4e18ae02ec0b42c20749865d4ba3784137e0bc72e3ec1b347b0fad52738fd09e5f76f3f87374c217f6e42b5b5ef7b189d6e38e9a6a649e14bd1ffdd4affaedeac5577ce9b0064e1729c83c4fa313ad59abf83fe8340a09f9cc0b5e2dc78a8507bad79ffbb916d336ecb38bd9ed463855c94bb91349013d90108b10a748ec892782ed9d39322a06f77001b368d9c56fe88a60e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") + } + + test("create single-frame packet (reference test vector)") { + val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, singleFramePayloads, associatedData) assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) @@ -86,7 +95,7 @@ class SphinxSpec extends FunSuite { val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) - assert(Seq(payload0, payload1, payload2, payload3, payload4) == payloads) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == singleFramePayloads) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) assert(packets(0).hmac == ByteVector32(hex"9b122c79c8aee73ea2cdbc22eca15bbcc9409a4cdd73d2b3fcd4fe26a492d376")) @@ -97,78 +106,119 @@ class SphinxSpec extends FunSuite { assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } + test("create multi-frame packet (reference test vector)") { + val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, multiFramePayloads, associatedData) + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619c4f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2ce49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e5871838590283176751ecc9157ec8c2fd7b27c8819501c575b38b827f3bac85b68ec043499e80606dcf0216855a3e6540745a6c55da121259e09a3053ffea849b6fb32d9640c9e5954088b34249b977d74f21fd3c560cb5ad283ba1385e56a15636d67d88924154590ab474bd51db6d0ab655967e671542f1c5c0c4fd4245dfde83c5bd8251ed8983c280843d97530a72ae7c6fb883df5d645afb046c5cb9ee73b1c473bbf488ad3bc1ddc341296b537a887452f006673e5bb99097ae1599f7d6497afd66f26ea163e276f476a773473ac572f6022a238ccc7d0b7360bf8827de566a8726eb36ca48b0bc8a146f217ad9fbafc602d109ab4742214195087588d76183bbf27b399bd3e0c60849e4d86c446e38fbe37218a501bd3945caa2c2cd656d40e1f5f863c98a30f3ee8dcd7c564a85a9b61d09e2fcb2a47add89bde32cec62d5f102a797f33693d4e285448b7929a89dc0da684b44c8596f1f903245a6d6133b41433f94aac919c579be28c691a134e0e05c7372670c7947f99ce50ee24bc7f97ed12feaba3411a479af84c94610d672f026cbb2c2ad66f28f6aa8c79f2ae99c45c9f17bee5dc0a4137d62aa5886f52efdb81bde49237f6e34774a89780bda053ee4590c622544620b4c40e9a9cf8117e071c26faa85559307d6f504d5af9d291de16886146380ee7590dde7cdfcd7debd91997b62c9a79c0f3e0e7591a255ee6bb240841cfd1e8da49b7ec803c1473115cfa394ddcd11268defde62e80b0f5d639c3e34f15cd50602711b9e8cec2a5edae718390d60072e050cbb62738851fb3fe7918730113619890283982466e49b9509fab5dddc8e0f2a7af6dfa4eac057c4be2c28b0a0f8054a0ecc2ea6711bfc5df639ea53b4947f386b0434926374e2e26e982a43c85eb5b426316c9180fdfae0f2b8d4636c780a395fb0dc7a7dc2d4a08ccb81236ef162a7f72e16e215f1bb5506de64d81917a232ebd3c49063b87d44ea2a769577f538ca8ae7806a0688d00c50592f5c9dc642f19e21bd0f3c459f770fbbbaacdb9de345d65cee4ce2e4b27f8e2d2d1a3596bf87de876d64528951fe472e5c5bd7d9d1270eb7a692ffce5d1a9f799f5905b944d4a138f985434e5df98e819fa337d4dda140e631806598e60a46de98f498fe5750e280719af2b04c7bec6f82c494699113fa26bc8a4c65fdf9ddb857109b95d78526fc329ac3bae8bb6c45f98c35acd7e8294bfc86706aa6d99fa770dfb3f5c0ac43301911d75c91106744d48b139e71cea3e4a83f6a124c6496b5b5177625edd7ebbf003061bdfdc80be4177d68a8f1d1b9b100eb91a638eef34539af4f44e2056831ef278ba13e2574333d0f2f1ba6195e2f1595c95697d90e8664195059b5a88f88e540d84f49c03b60775c17ff61d49967c99be5c0f58427226e4b61342120a7396d8053b730b22ccf70937465be115b47ffa3d9d94909c9b43ff3f40d1c75662e7f1d733cbdafc837e9ef8fc225f8838ce3fe2540caccfa4413f87714f57497fe04a895489b2f3e93b6a14bb734419a270f8352c11ceaecf74802fbb6cfb3d5862482ffca751eaaae92f260c3f0dbfde855ad7507d54e96d6953d107364c94209903f0aab8968e864a7620670c7fd89c3ca0577b9ba64eb8") + + val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) + val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) + val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) + val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) + val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == multiFramePayloads) + + val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) + assert(packets(0).hmac == ByteVector32(hex"5c8d703327feb46f88c4c3f3fb77bcf8d2f2063dd673cf75ae6101f29445b6c6")) + assert(packets(1).hmac == ByteVector32(hex"f9951d3fe5668b2733209163450a2e06aede1d63f4e5c2b7b1d883650dff510d")) + assert(packets(2).hmac == ByteVector32(hex"400dd7fd80fed9963618a6c262e7a7182cc9c8618f69245fe8080173c1faa501")) + assert(packets(3).hmac == ByteVector32(hex"49602e7e652eb2987df05b5ea1e45c797791c9a29f53b1309705175097def065")) + assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) + } + + test("create multi-frame packet filling onion (reference test vector)") { + val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, multiFramePayloadsFull, associatedData) + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619a4f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2ce49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f590c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce4d904939b9b486b68fe24f9430d33c893b039722146164d2aa4b38556279c61ecdfcacbe96df37b4a7cb6b3b88dba165efe681b8fcb5c72febc91b133c6c1a15a5dd2b7ce8a23cf43225931b5967094f4f1c9c88eb8e864a5ac0f8276bc7036f2b36784b2ab4d0435336a8445b64cc7af1b703c8c94b1a83411d0234b1fd3fff0b0dc7992c9f5ab8fc8395d3cd70c5dd6ef9b2408d1b8608f6fff191c2437e8694296e3224b92e3b7a872877b31e5d2dd70ffaa9e8fd70ea552d85fb27289f91e1772794156c87b81bb2c9a470d364945fa1c96816dbc0f6440d3071a4bfc8596012ef384f20776d7384ef196db73b3aac192bf890666f0d5551a4551421558d87bf90218159b96bd0c4aa2acb6755e5db85e5fd41810434f3dd6680be8636190e249f3b97b1f65365922bbe2339bf4d815dc7cb0b588f8d0ef1d5017cff1501135d78d0996ab554fda116eb551529ea7630c102de660692a3dd214f9be933091eac8fb9e2559ea07c65e869179c71ebb0ff002ed8a53ce05e0e6024addbbf3118f34319d75e4850d5e17fbce98ed8458b9e5ab996e00ec697d3752f5a1d6a3ad691d8015f78b10afb9b62bd92e0dbdfb2ed7783771d61b86d8bcb8e167e74919b0f61fd1c63ed6153abeb3d90b8bb6d5932d559dda555c9e0482cf3c953eb69f4c1a58e4dcd31361958666c2fdbe30bc281684c7421b1d23635c7e9176651c2d91db9a07f50dcaa914f1f50e153dafd00381bc868bfbdf84401ad5a22516a91653808d8942190441e600c60ee23358cf56f2e5007314df2f4700c24b27dae11cb45817459a566f582aebcde5bd2ebdee887824e67128f4155832fb08752a5f1cc95cbe70352f5f9d86e23a3fdd97f0a3c3806eb17177d4f07ec4c2d30fb5318b9feb9b055c8571fb7d0dd1782b9eb1b0155564b7fa21998c9c696b5979c81041818c2e774395141ef2283aea4353397d90dd37923aefcadda3a66785cdb335ace25d53a250ab57ff7cc6c43034d8afef7d1fdbb07ca85639586eb939c4c96ee38ca2c8dd5c14e03ddef13b25cdd2af2486f02f73de04571b402f6ead23cf3b27efaa0f3b88df280c77acc16da783c158ebe7fe9c3f02bc7de8edc401f28554aef736dd73eb5a105c4026ac1e799b930ec497f505060501391c1c396121efc0fa152ba6967b551fe13a0ae135a7bf8e347c52858d66bc1a07c379ec7a34654c3afe8443e30ea344c27af96a5d1ff1b94d9744fa1f273320f38c4a9bfffa81e05ae0dbbd2a232e57f4fca93fe588ebcd39bd334a4d26b7223dfeb10c0f8b7d42d2dfbe5264073752ab8e63f5fe65d627bfff286c4bf429cad13a17bc21412038ccddfdb956dfd1bef80d2adc04700bf50adb875f00c4e133e6c6bc88ae152bc2eb59450ce6f2f27522ba6cfa43847495cda022327d63f2116006c7991d615328a8d6fb40b97b957334c84f9a4b003a20") + + val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) + val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) + val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) + val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) + val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == multiFramePayloadsFull) + + val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) + assert(packets(0).hmac == ByteVector32(hex"f797374c534b81bca1e085b7345ee0d582083f59d8658215f7f2a390187e25ea")) + assert(packets(1).hmac == ByteVector32(hex"dc7a0f5eee117736e87344f0c49db15db69ed8acdf28ee8ea75a541f821de6ef")) + assert(packets(2).hmac == ByteVector32(hex"6a97cd6190cea91c9db21b2d0a9a55405935841f679b526e129adecdb744933e")) + assert(packets(3).hmac == ByteVector32(hex"4e2a8b74b82264b9a94934a592c0e77e2fa20b733fdc333fcd9f567683a828f9")) + assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) + } + test("last node replies with an error message") { - // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 - - // origin build the onion packet - val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData) - - // each node parses and forwards the packet - // node #0 - val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) - // node #1 - val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) - // node #2 - val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) - // node #3 - val Success(ParsedPacket(payload3, packet4, sharedSecret3)) = parsePacket(privKeys(3), associatedData, packet3.serialize) - // node #4 - val Success(ParsedPacket(payload4, packet5, sharedSecret4)) = parsePacket(privKeys(4), associatedData, packet4.serialize) - assert(packet5.isLastPacket) - - // node #4 want to reply with an error message - val error = createErrorPacket(sharedSecret4, TemporaryNodeFailure) - assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") - // assert(error == hex"69b1e5a3e05a7b5478e6529cd1749fdd8c66da6f6db42078ff8497ac4e117e91a8cb9168b58f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c2") - // error sent back to 3, 2, 1 and 0 - val error1 = forwardErrorPacket(error, sharedSecret3) - assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") - // assert(error1 == hex"08cd44478211b8a4370ab1368b5ffe8c9c92fb830ff4ad6e3b0a316df9d24176a081bab161ea0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a93") - - val error2 = forwardErrorPacket(error1, sharedSecret2) - assert(error2 == hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") - // assert(error2 == hex"6984b0ccd86f37995857363df13670acd064bfd1a540e521cad4d71c07b1bc3dff9ac25f41addfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd") - - val error3 = forwardErrorPacket(error2, sharedSecret1) - assert(error3 == hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") - // assert(error3 == hex"669478a3ddf9ba4049df8fa51f73ac712b9c20380cda431696963a492713ebddb7dfadbb566c8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8") - - val error4 = forwardErrorPacket(error3, sharedSecret0) - assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") - // assert(error4 == hex"500d8596f76d3045bfdbf99914b98519fe76ea130dc22338c473ab68d74378b13a06a19f891145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366") - - - // origin parses error packet and can see that it comes from node #4 - val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error4, sharedSecrets) - assert(pubkey == publicKeys(4)) - assert(failure == TemporaryNodeFailure) + for (payloads <- Seq(singleFramePayloads, multiFramePayloads, multiFramePayloadsFull)) { + // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + + // origin build the onion packet + val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData) + + // each node parses and forwards the packet + // node #0 + val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) + // node #1 + val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) + // node #2 + val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) + // node #3 + val Success(ParsedPacket(payload3, packet4, sharedSecret3)) = parsePacket(privKeys(3), associatedData, packet3.serialize) + // node #4 + val Success(ParsedPacket(payload4, packet5, sharedSecret4)) = parsePacket(privKeys(4), associatedData, packet4.serialize) + assert(packet5.isLastPacket) + + // node #4 want to reply with an error message + val error = createErrorPacket(sharedSecret4, TemporaryNodeFailure) + assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") + // assert(error == hex"69b1e5a3e05a7b5478e6529cd1749fdd8c66da6f6db42078ff8497ac4e117e91a8cb9168b58f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c2") + // error sent back to 3, 2, 1 and 0 + val error1 = forwardErrorPacket(error, sharedSecret3) + assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") + // assert(error1 == hex"08cd44478211b8a4370ab1368b5ffe8c9c92fb830ff4ad6e3b0a316df9d24176a081bab161ea0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a93") + + val error2 = forwardErrorPacket(error1, sharedSecret2) + assert(error2 == hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") + // assert(error2 == hex"6984b0ccd86f37995857363df13670acd064bfd1a540e521cad4d71c07b1bc3dff9ac25f41addfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd") + + val error3 = forwardErrorPacket(error2, sharedSecret1) + assert(error3 == hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") + // assert(error3 == hex"669478a3ddf9ba4049df8fa51f73ac712b9c20380cda431696963a492713ebddb7dfadbb566c8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8") + + val error4 = forwardErrorPacket(error3, sharedSecret0) + assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") + // assert(error4 == hex"500d8596f76d3045bfdbf99914b98519fe76ea130dc22338c473ab68d74378b13a06a19f891145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366") + + // origin parses error packet and can see that it comes from node #4 + val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error4, sharedSecrets) + assert(pubkey == publicKeys(4)) + assert(failure == TemporaryNodeFailure) + } } test("intermediate node replies with an error message") { - // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 - - // origin build the onion packet - val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData) - - // each node parses and forwards the packet - // node #0 - val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) - // node #1 - val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) - // node #2 - val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) - - // node #2 want to reply with an error message - val error = createErrorPacket(sharedSecret2, InvalidRealm) - - // error sent back to 1 and 0 - val error1 = forwardErrorPacket(error, sharedSecret1) - val error2 = forwardErrorPacket(error1, sharedSecret0) - - // origin parses error packet and can see that it comes from node #2 - val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error2, sharedSecrets) - assert(pubkey == publicKeys(2)) - assert(failure == InvalidRealm) + for (payloads <- Seq(singleFramePayloads, multiFramePayloads, multiFramePayloadsFull)) { + // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + + // origin build the onion packet + val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData) + + // each node parses and forwards the packet + // node #0 + val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) + // node #1 + val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) + // node #2 + val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) + + // node #2 want to reply with an error message + val error = createErrorPacket(sharedSecret2, InvalidRealm) + + // error sent back to 1 and 0 + val error1 = forwardErrorPacket(error, sharedSecret1) + val error2 = forwardErrorPacket(error1, sharedSecret0) + + // origin parses error packet and can see that it comes from node #2 + val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error2, sharedSecrets) + assert(pubkey == publicKeys(2)) + assert(failure == InvalidRealm) + } } } @@ -190,12 +240,34 @@ object SphinxSpec { )) val sessionKey: PrivateKey = PrivateKey(hex"4141414141414141414141414141414141414141414141414141414141414141") - val payloads = Seq( + + // This test vector uses only single-frame payloads. + // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + val singleFramePayloads = Seq( hex"000000000000000000000000000000000000000000000000000000000000000000", hex"000101010101010101000000000000000100000001000000000000000000000000", hex"000202020202020202000000000000000200000002000000000000000000000000", hex"000303030303030303000000000000000300000003000000000000000000000000", hex"000404040404040404000000000000000400000004000000000000000000000000") + // This test vector uses multi-frame payloads intertwined with single-frame payloads. + // origin -> node #0 (3 frames) -> node #1 (1 frame) -> node #2 (2 frames) -> node #3 (1 frame) -> node #4 (1 frame) + val multiFramePayloads = Seq( + hex"21000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hex"000101010101010101000000000000000100000001000000000000000000000000", + hex"1122222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", + hex"013333333333333333333333333333333333333333333333333333333333333333", + hex"000404040404040404000000000000000400000004000000000000000000000000") + + // This test vector uses multi-frame payloads. + // It fills all the frames available in an onion packet. + // origin -> node #0 (5 frames) -> node #1 (5 frames) -> node #2 (4 frames) -> node #3 (3 frames) -> node #4 (3 frames) + val multiFramePayloadsFull = Seq( + hex"4100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hex"4111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + hex"312222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", + hex"21333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + hex"21444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444") + val associatedData = ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242") } From f75ae41546e5ff7f16f5fa245dbcc3b34ee1de61 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Wed, 15 May 2019 14:35:36 +0200 Subject: [PATCH 04/27] Sphinx: use 5 bits to encode number of frames. Add test case for 20-frames single-hop payload. --- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 4 +- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 53 +++++++++++++------ 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 748aacf605..84d5039ef2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -65,9 +65,9 @@ object Sphinx extends Logging { // last packet (all zeroes except for the version byte) val LAST_PACKET = Packet(Version, ByteVector.fill(33)(0), ByteVector32.Zeroes, ByteVector.fill(RoutingInfoLength)(0)) - // The 4 MSB of the first frame of the payload contains the number of frames used. + // The 5 MSB of the first frame of the payload contains the number of frames used. def payloadFrameCount(payload: ByteVector): Int = { - (payload.head >> 4) + 1 + ((payload.head >> 3) & 0x1F) + 1 } def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 257be94d22..ead569e561 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -106,9 +106,9 @@ class SphinxSpec extends FunSuite { assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } - test("create multi-frame packet (reference test vector)") { + test("create multi-frame packet") { val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, multiFramePayloads, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619c4f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2ce49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e5871838590283176751ecc9157ec8c2fd7b27c8819501c575b38b827f3bac85b68ec043499e80606dcf0216855a3e6540745a6c55da121259e09a3053ffea849b6fb32d9640c9e5954088b34249b977d74f21fd3c560cb5ad283ba1385e56a15636d67d88924154590ab474bd51db6d0ab655967e671542f1c5c0c4fd4245dfde83c5bd8251ed8983c280843d97530a72ae7c6fb883df5d645afb046c5cb9ee73b1c473bbf488ad3bc1ddc341296b537a887452f006673e5bb99097ae1599f7d6497afd66f26ea163e276f476a773473ac572f6022a238ccc7d0b7360bf8827de566a8726eb36ca48b0bc8a146f217ad9fbafc602d109ab4742214195087588d76183bbf27b399bd3e0c60849e4d86c446e38fbe37218a501bd3945caa2c2cd656d40e1f5f863c98a30f3ee8dcd7c564a85a9b61d09e2fcb2a47add89bde32cec62d5f102a797f33693d4e285448b7929a89dc0da684b44c8596f1f903245a6d6133b41433f94aac919c579be28c691a134e0e05c7372670c7947f99ce50ee24bc7f97ed12feaba3411a479af84c94610d672f026cbb2c2ad66f28f6aa8c79f2ae99c45c9f17bee5dc0a4137d62aa5886f52efdb81bde49237f6e34774a89780bda053ee4590c622544620b4c40e9a9cf8117e071c26faa85559307d6f504d5af9d291de16886146380ee7590dde7cdfcd7debd91997b62c9a79c0f3e0e7591a255ee6bb240841cfd1e8da49b7ec803c1473115cfa394ddcd11268defde62e80b0f5d639c3e34f15cd50602711b9e8cec2a5edae718390d60072e050cbb62738851fb3fe7918730113619890283982466e49b9509fab5dddc8e0f2a7af6dfa4eac057c4be2c28b0a0f8054a0ecc2ea6711bfc5df639ea53b4947f386b0434926374e2e26e982a43c85eb5b426316c9180fdfae0f2b8d4636c780a395fb0dc7a7dc2d4a08ccb81236ef162a7f72e16e215f1bb5506de64d81917a232ebd3c49063b87d44ea2a769577f538ca8ae7806a0688d00c50592f5c9dc642f19e21bd0f3c459f770fbbbaacdb9de345d65cee4ce2e4b27f8e2d2d1a3596bf87de876d64528951fe472e5c5bd7d9d1270eb7a692ffce5d1a9f799f5905b944d4a138f985434e5df98e819fa337d4dda140e631806598e60a46de98f498fe5750e280719af2b04c7bec6f82c494699113fa26bc8a4c65fdf9ddb857109b95d78526fc329ac3bae8bb6c45f98c35acd7e8294bfc86706aa6d99fa770dfb3f5c0ac43301911d75c91106744d48b139e71cea3e4a83f6a124c6496b5b5177625edd7ebbf003061bdfdc80be4177d68a8f1d1b9b100eb91a638eef34539af4f44e2056831ef278ba13e2574333d0f2f1ba6195e2f1595c95697d90e8664195059b5a88f88e540d84f49c03b60775c17ff61d49967c99be5c0f58427226e4b61342120a7396d8053b730b22ccf70937465be115b47ffa3d9d94909c9b43ff3f40d1c75662e7f1d733cbdafc837e9ef8fc225f8838ce3fe2540caccfa4413f87714f57497fe04a895489b2f3e93b6a14bb734419a270f8352c11ceaecf74802fbb6cfb3d5862482ffca751eaaae92f260c3f0dbfde855ad7507d54e96d6953d107364c94209903f0aab8968e864a7620670c7fd89c3ca0577b9ba64eb8") + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f4f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2ce49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e5871838529d0eb5e1f0e26a68ee6e88eaa81124f360afcfe056ac1d095d40d4f681cf34499e80606dcf0216855a3e6540745a6c55da121259e09a3053ffea849b6fb32d964ebb4433f00240e6dabef9c103667ee4d734daf9c43e38e44fdc1019b13efe8923c154590ab474bd51db6d0ab655967e671542f1c5c0c4fd4245dfde83c5bd8251ed8983c280843d97530a72ae7c6fb883df5d645afb046c5cb9ee73b1c473bbf488ad3bc1ddc341296b537a887452f006673e5bb99097ae1599f7d6497afd66f26ea163e276f476a773473ac572f6022a238ccc7d0b7360bf8827de566a8726eb36ca48b0bc8a146f217ad9fbafc602d109ab4742214195087588d76183bbf27b399bd3e0c60849e4d86c446e38fbe37218a501bd3945caa2c2cd656d40e1f5f863c98a30f3ee8dcd7c564a85a9b61d09e2fcb2a47add89bde32cec62d5f102a797f33693d4e285448b7929a89dc0da684b44c8596f1f903245a6d6133b41433f94aac919c579be28c691a134e0e05c7372670c7947f99ce50ee24bc7f97ed12feaba3411a479af84c94610d672f026cbb2c2ad66f28f6aa8c79f2ae99c45c9f17bee5dc0a4137d62aa5886f52efdb81bde49237f6e34774a89780bda053ee4590c622544620b4c40e9a9cf8117e071c26faa85559307d6f504d5af9d291de16886146380ee7590dde7cdfcd7debd91997b62c9a79c0f3e0e7591a255ee6bb240841cfd1e8da49b7ec803c1473115cfa394ddcd11268defde62e80b0f5d639c3e34f15cd50602711b9e8cec2a5edae718390d60072e050cbb62738851fb3fe7918730113619890283982466e49b9509fab5dddc8e0f2a7af6dfa4eac057c4be2c28b0a0f8054a0ecc2ea6711bfc5df639ea53b4947f386b0434926374e2e26e982a43c85eb5b426316c9180fdfae0f2b8d4636c780a395fb0dc7a7dc2d4a08ccb81236ef162a7f72e16e215f1bb5506de64d81917a232ebd3c49063b87d44ea2a769577f538ca8ae7806a0688d00c50592f5c9dc642f19e21bd0f3c459f770fbbbaacdb9de345d65cee4ce2e4b27f8e2d2d1a3596bf87de876d64528951fe472e5c5bd7d9d1270eb7a692ffce5d1a9f799f5905b944d4a138f985434e5df98e819fa337d4dda140e631806598e60a46de98f498fe5750e280719af2b04c7bec6f82c494699113fa26bc8a4c65fdf9ddb857109b95d78526fc329ac3bae8bb6c45f98c35acd7e8294bfc86706aa6d99fa770dfb3f5c0ac43301911d75c91106744d48b139e71cea3e4a83f6a124c6496b5b5177625edd7ebbf003061bdfdc80be4177d68a8f1d1b9b100eb91a638eef34539af4f44e2056831ef278ba13e2574333d0f2f1ba6195e2f1595c95697d90e8664195059b5a88f88e540d84f49c03b60775c17ff61d49967c99be5c0f58427226e4b61342120a7396d8053b730b22ccf70937465be115b47ffa3d9d94909c9b43ff3f40d1c75662e7f1d733cbdafc837e9ef8fc225f8838ce3fe2540caccfa4413f87714f57497fe04a895489b2f3e93b6a14bb734419a270f8352c11ceaecf74802fbb6cfb3d5862482ffca751eaaae92f260c3f0dbfde855ad7507d54e96dc232991011422efa7f00f4e94e3a2eed2ebd5c195730350dc6987691fae4fb68") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) @@ -116,18 +116,19 @@ class SphinxSpec extends FunSuite { val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) assert(Seq(payload0, payload1, payload2, payload3, payload4) == multiFramePayloads) + assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) - assert(packets(0).hmac == ByteVector32(hex"5c8d703327feb46f88c4c3f3fb77bcf8d2f2063dd673cf75ae6101f29445b6c6")) - assert(packets(1).hmac == ByteVector32(hex"f9951d3fe5668b2733209163450a2e06aede1d63f4e5c2b7b1d883650dff510d")) + assert(packets(0).hmac == ByteVector32(hex"e575aa1b4dee5e5851cea7528644d23ffda8e6948821b682c80fc4e694b541b6")) + assert(packets(1).hmac == ByteVector32(hex"1ebf0754edc9b16e035870078172138ebd58e82d34bc5f76a973979d73776116")) assert(packets(2).hmac == ByteVector32(hex"400dd7fd80fed9963618a6c262e7a7182cc9c8618f69245fe8080173c1faa501")) assert(packets(3).hmac == ByteVector32(hex"49602e7e652eb2987df05b5ea1e45c797791c9a29f53b1309705175097def065")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } - test("create multi-frame packet filling onion (reference test vector)") { + test("create multi-frame packet filling onion") { val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, multiFramePayloadsFull, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619a4f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2ce49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f590c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce4d904939b9b486b68fe24f9430d33c893b039722146164d2aa4b38556279c61ecdfcacbe96df37b4a7cb6b3b88dba165efe681b8fcb5c72febc91b133c6c1a15a5dd2b7ce8a23cf43225931b5967094f4f1c9c88eb8e864a5ac0f8276bc7036f2b36784b2ab4d0435336a8445b64cc7af1b703c8c94b1a83411d0234b1fd3fff0b0dc7992c9f5ab8fc8395d3cd70c5dd6ef9b2408d1b8608f6fff191c2437e8694296e3224b92e3b7a872877b31e5d2dd70ffaa9e8fd70ea552d85fb27289f91e1772794156c87b81bb2c9a470d364945fa1c96816dbc0f6440d3071a4bfc8596012ef384f20776d7384ef196db73b3aac192bf890666f0d5551a4551421558d87bf90218159b96bd0c4aa2acb6755e5db85e5fd41810434f3dd6680be8636190e249f3b97b1f65365922bbe2339bf4d815dc7cb0b588f8d0ef1d5017cff1501135d78d0996ab554fda116eb551529ea7630c102de660692a3dd214f9be933091eac8fb9e2559ea07c65e869179c71ebb0ff002ed8a53ce05e0e6024addbbf3118f34319d75e4850d5e17fbce98ed8458b9e5ab996e00ec697d3752f5a1d6a3ad691d8015f78b10afb9b62bd92e0dbdfb2ed7783771d61b86d8bcb8e167e74919b0f61fd1c63ed6153abeb3d90b8bb6d5932d559dda555c9e0482cf3c953eb69f4c1a58e4dcd31361958666c2fdbe30bc281684c7421b1d23635c7e9176651c2d91db9a07f50dcaa914f1f50e153dafd00381bc868bfbdf84401ad5a22516a91653808d8942190441e600c60ee23358cf56f2e5007314df2f4700c24b27dae11cb45817459a566f582aebcde5bd2ebdee887824e67128f4155832fb08752a5f1cc95cbe70352f5f9d86e23a3fdd97f0a3c3806eb17177d4f07ec4c2d30fb5318b9feb9b055c8571fb7d0dd1782b9eb1b0155564b7fa21998c9c696b5979c81041818c2e774395141ef2283aea4353397d90dd37923aefcadda3a66785cdb335ace25d53a250ab57ff7cc6c43034d8afef7d1fdbb07ca85639586eb939c4c96ee38ca2c8dd5c14e03ddef13b25cdd2af2486f02f73de04571b402f6ead23cf3b27efaa0f3b88df280c77acc16da783c158ebe7fe9c3f02bc7de8edc401f28554aef736dd73eb5a105c4026ac1e799b930ec497f505060501391c1c396121efc0fa152ba6967b551fe13a0ae135a7bf8e347c52858d66bc1a07c379ec7a34654c3afe8443e30ea344c27af96a5d1ff1b94d9744fa1f273320f38c4a9bfffa81e05ae0dbbd2a232e57f4fca93fe588ebcd39bd334a4d26b7223dfeb10c0f8b7d42d2dfbe5264073752ab8e63f5fe65d627bfff286c4bf429cad13a17bc21412038ccddfdb956dfd1bef80d2adc04700bf50adb875f00c4e133e6c6bc88ae152bc2eb59450ce6f2f27522ba6cfa43847495cda022327d63f2116006c7991d615328a8d6fb40b97b957334c84f9a4b003a20") + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619c4f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2ce49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f590c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce4fcdbbfcca2995dccb71d6ccf759220bbbf52b19b0135308490c476816a316a3dbfcacbe96df37b4a7cb6b3b88dba165efe681b8fcb5c72febc91b133c6c1a15a5dd2b7ce8a23cf43225931b5967094f4f1c9c88eb8e864a5ac0f8276bc7036f2b36784b2ab4d0435336a8445b64cc7af1b703c8c94b1a83411d0234b1fd3fff0b0dc7992c9f5ab8fc8395d3cd70c5dd6ef9b2408d1b8608f6fff191c2437e8694296e3224b92e3b7a872877b31e5d2dd70ffaa9e8fd70ea552d85fb27289f91e1772794156c87b81bb2c9a470d364945fa1c96816dbc0f6440d3071a4bfc8596012ef384f20776d7384ef196db73b3aac192bf890666f0d5551a4551421558d87bf90218159b96bd0c4aa2acb6755e5db85e5fd41810434f3dd6680be8636190e249f3b97b1f65365922bbe2339bf4d815dc7cb0b588f8d0ef1d5017cff1501135d78d0996e5c122ce52cd6b49b4aba6222f4ba93c23643be48ddacf552221561720a8dc6b71ea07c65e869179c71ebb0ff002ed8a53ce05e0e6024addbbf3118f34319d75e4850d5e17fbce98ed8458b9e5ab996e00ec697d3752f5a1d6a3ad691d8015f78b10afb9b62bd92e0dbdfb2ed7783771d61b86d8bcb8e167e74919b0f61fd1c63ed6153abeb3d90b8bb6d5932d559dda555c9e0482cf3c953eb69f4c1a58e4dcd31361958666c2fdbe30bc281684c7421b1d23635c7e9176651c2d91db9a07f50dcaa914f1f50e153dafd00381bc868bfbdf84401ad5a22516a91653808d8942190441e600c60ee23358cf56f2e5007314df2f4700c24b27dae11cb45817459a566f582ab3d899d48fa61e8c76526c84994be0281bb3f20494b9bd8973e4169817250803d23a3fdd97f0a3c3806eb17177d4f07ec4c2d30fb5318b9feb9b055c8571fb7d0dd1782b9eb1b0155564b7fa21998c9c696b5979c81041818c2e774395141ef2283aea4353397d90dd37923aefcadda3a66785cdb335ace25d53a250ab57ff7cc6c43034d8afef7d1fdbb07ca85639586eb939c4c96ee38ca2c8dd5c14e03ddef13b25cdd2af2486f02f73de04571b402f6ead23cf3b27efaa0f3b88df280c77acc16ddd5359b06e0e2aef47b98f01a86df40b4974db1c36466ca7208d56e9088709dc499b930ec497f505060501391c1c396121efc0fa152ba6967b551fe13a0ae135a7bf8e347c52858d66bc1a07c379ec7a34654c3afe8443e30ea344c27af96a5d1ff1b94d9744fa1f273320f38c4a9bfffa81e05ae0dbbd2a232e57f4fca93fe588ebcd39bd334a4d26b7223dfeb10c0f8b7d42d2dfbe5264073752ab8e63f5fe65d627bfff286c4bf429cad13a17bc21412038ccddfdb956dfd1bef80d2adc04700bf50adb875f00c4e133e6c6bc88ae152bc2eb59450ce6f2f27522ba6cfa43847495019b0282f6ffdb3efbbe10a70f89ea311652670932218d70a9335163f25c7c45") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) @@ -135,15 +136,25 @@ class SphinxSpec extends FunSuite { val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) assert(Seq(payload0, payload1, payload2, payload3, payload4) == multiFramePayloadsFull) + assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) - assert(packets(0).hmac == ByteVector32(hex"f797374c534b81bca1e085b7345ee0d582083f59d8658215f7f2a390187e25ea")) - assert(packets(1).hmac == ByteVector32(hex"dc7a0f5eee117736e87344f0c49db15db69ed8acdf28ee8ea75a541f821de6ef")) - assert(packets(2).hmac == ByteVector32(hex"6a97cd6190cea91c9db21b2d0a9a55405935841f679b526e129adecdb744933e")) - assert(packets(3).hmac == ByteVector32(hex"4e2a8b74b82264b9a94934a592c0e77e2fa20b733fdc333fcd9f567683a828f9")) + assert(packets(0).hmac == ByteVector32(hex"d2481b1b6a9ab718e8d9103b4cff08fd8d63fce39f46ffbbc385504755d32e3b")) + assert(packets(1).hmac == ByteVector32(hex"92ee624aadb2a92e0e4645b1e7c63587f593c97580e6d865164b93e26a4ea4a1")) + assert(packets(2).hmac == ByteVector32(hex"3282b10831d5591893c491d8bb25a030707d7e6ed97df32e3dc0b8608f3e06bb")) + assert(packets(3).hmac == ByteVector32(hex"34fa139c3dcbb0cad1f207d9d240d774945384c1feac82eb064810c3cb098d3b")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } + test("create 20-frames 1-hop packet") { + val Sphinx.PacketAndSecrets(onion, _) = Sphinx.makePacket(sessionKey, publicKeys.take(1), maxFramesOneHopPayload, associatedData) + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866197cb3011280e52d8070f7a62f005cd4570558fba2feca39adba8b1d99c5cd39783303077c1dcf60f4775a52ec165ea6dbe2d908dfddc293c61ecb228a1d8493ea3990096ea6db60cbccd111b864c2c48248aaf5b55a021930ef7a4e99f1cec7a26842003cf6ffffeabe96690632c8dcbfa6deb511f51cf97ad9ba0f49b9b7c7d2a752a276156843a64b814b7ba0a6e2d2ae8ade755a62ed16a22c08970b1f0c1a33c1c78ee7030710a23abc9d6a0d9e6e87823289e05828cf0935101f998bebbeebf0b0d8ee15c1acc9ef954bba5e31bd22172eb0ac2078b231f7ea05dbee838804f526b509d544ce3a648e473b3b08164f3817a6dcee64e4d14102516cd628da7002f28f59316c721e5200b7d286f02d564d8388f1ac2d204b1aa2d5ddc7af9d1a2e034ea66cd1e6958a41a8961d863eb67b2f6a0470730f3adc318d7d110364847da00644dfdcc83e0ddaa5a772be1acc26ccb6a403740bc3f775370097a5e6e8e92f77b49d3f78df3ee5576377dbac408699a80f85ca22bcfc5f6b67de26f7d97171bd98a9f8bd0c3973803875d3b4fdc6c81a9d3e2bf9695a1090ef7739db5bbd98a732f03f854bb9e2c5657f780faba4e4fc2499263df4ea93e741f1b6c3fcb9d43618fd1e86e4f9e2b5c2978537d9dc7278682955a947f4a422b68636d7dff3c1a388a222a321d065ac498841d5c861cf86776c9526983f0d7c97713d2f41349db56457fcaca8ecad3be9d1a6fefd5a8fc666a9692895b17f690dbf1ce2c130a4af5fda03109d55a20e2d31164bca959fd5dc404666e32180335758671b24fc3c9cb66325fab88ee47126d57bf8fb3730f6c727ce9c02dc1f084d277d581b53565476a190f445d95b763fb42dda62371d97554b6fec20b965c949ed8e3894c0b5d05978071c4e212bdac53ba02d461af159bd95a6a7fdf15b06aa4d5249671181f454a519a3c16a9e014d245a971e9592d469fb54902c96f355862de0dc48536e40812262f108d4346ff46ab876d2c0cc8045db20efc32cb04ce2fee5cdf0f5b5ec9f0e05a137a400d69313bd414a7572712fa826d10241e035728e5e3fae120b9ff8c3ca507f93d3e8337b4f01e6ddf5d0e19832c0ade48eaf31b24c8ea85a561dfec6d0110bb5f7107308faaf0e0b5c7b68af795d41a93b050e6b0900a0a9e310cfe02612e81b6ef83ce06cd41e169db2bf2d9d94f769d84d1d346c65b94c2e7e9164e3885e1b319cd9aac301dd326a72edbcf2827683c88a9780b3778b26fd2b9c1294abd99ccaab4163f8c67fd5118aba7424521f67b8b95853a860946cb4b3be76880c22811f2b3578520fd80630ce4a80492156703143e08642e55c566683646a8ddd0825db489cc86041f2affb2e22c0962579314b8f1086f16940d75ff0b42e2e30ff2e3cee6912fa7a72851e9d8194ab80f7d0861c97b81d2c8298c733493c5720ece121a0c427c115d39e150fd832f0a7a0fbd578f7d29619d69006be0014d02e1817f25e92eee05da7bb8b452b5df264928725caf27ee1b2cff2e9fbbb434bcf9c6c8642772afe7ee03637bdc4e58948e99ba7512e1fa2269325361a0967626652d9f2624699e3b8fddcc0359c4b3848f29ecd2c64be96e0cdfb903e96e0b4f37c652c901be1dc5e3c22b1816e1e8686bb299bc3af891c6e34e7558f80c7e86ee73393a19621ae952308a0659d55ed360772199584238d3ff827c9489eb12942c6f5081de64aa030b54135488777517632d3508796a6bcda3c4e65f4320364d4b76971661e60d5379817456b938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3cc71285778b5a49c2c741b09b61c113eed6dd067d430d9c64f8918f0bc7ee2c0") + + val Success(Sphinx.ParsedPacket(payload, nextPacket, _)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) + assert(payload == maxFramesOneHopPayload.head) + assert(nextPacket.hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) + } + test("last node replies with an error message") { for (payloads <- Seq(singleFramePayloads, multiFramePayloads, multiFramePayloadsFull)) { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 @@ -251,23 +262,31 @@ object SphinxSpec { hex"000404040404040404000000000000000400000004000000000000000000000000") // This test vector uses multi-frame payloads intertwined with single-frame payloads. + // The number of frames (-1) is encoded in the first 5 bits of the first byte. // origin -> node #0 (3 frames) -> node #1 (1 frame) -> node #2 (2 frames) -> node #3 (1 frame) -> node #4 (1 frame) val multiFramePayloads = Seq( - hex"21000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hex"11000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", hex"000101010101010101000000000000000100000001000000000000000000000000", - hex"1122222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", + hex"0922222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", hex"013333333333333333333333333333333333333333333333333333333333333333", hex"000404040404040404000000000000000400000004000000000000000000000000") // This test vector uses multi-frame payloads. // It fills all the frames available in an onion packet. + // The number of frames (-1) is encoded in the first 5 bits of the first byte. // origin -> node #0 (5 frames) -> node #1 (5 frames) -> node #2 (4 frames) -> node #3 (3 frames) -> node #4 (3 frames) val multiFramePayloadsFull = Seq( - hex"4100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - hex"4111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", - hex"312222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", - hex"21333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", - hex"21444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444") + hex"2100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hex"2111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", + hex"192222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", + hex"11333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", + hex"11444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444") + + // This test vector uses a payload containing a single hop filling all the available frames. + // origin -> recipient (20 frames) + val maxFramesOneHopPayload = Seq( + hex"9942424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242" + ) val associatedData = ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242") } From 4014183fff072cf34b19330d888e6a581a251ec0 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Wed, 15 May 2019 15:19:46 +0200 Subject: [PATCH 05/27] SphinxSpec: small clean-up --- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index ead569e561..a30df2cfdf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -96,6 +96,7 @@ class SphinxSpec extends FunSuite { val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) assert(Seq(payload0, payload1, payload2, payload3, payload4) == singleFramePayloads) + assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) assert(packets(0).hmac == ByteVector32(hex"9b122c79c8aee73ea2cdbc22eca15bbcc9409a4cdd73d2b3fcd4fe26a492d376")) @@ -164,15 +165,15 @@ class SphinxSpec extends FunSuite { // each node parses and forwards the packet // node #0 - val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) + val Success(ParsedPacket(_, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) // node #1 - val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) + val Success(ParsedPacket(_, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) // node #2 - val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) + val Success(ParsedPacket(_, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) // node #3 - val Success(ParsedPacket(payload3, packet4, sharedSecret3)) = parsePacket(privKeys(3), associatedData, packet3.serialize) + val Success(ParsedPacket(_, packet4, sharedSecret3)) = parsePacket(privKeys(3), associatedData, packet3.serialize) // node #4 - val Success(ParsedPacket(payload4, packet5, sharedSecret4)) = parsePacket(privKeys(4), associatedData, packet4.serialize) + val Success(ParsedPacket(_, packet5, sharedSecret4)) = parsePacket(privKeys(4), associatedData, packet4.serialize) assert(packet5.isLastPacket) // node #4 want to reply with an error message @@ -212,11 +213,11 @@ class SphinxSpec extends FunSuite { // each node parses and forwards the packet // node #0 - val Success(ParsedPacket(payload0, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) + val Success(ParsedPacket(_, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) // node #1 - val Success(ParsedPacket(payload1, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) + val Success(ParsedPacket(_, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) // node #2 - val Success(ParsedPacket(payload2, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) + val Success(ParsedPacket(_, _, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) // node #2 want to reply with an error message val error = createErrorPacket(sharedSecret2, InvalidRealm) @@ -259,7 +260,8 @@ object SphinxSpec { hex"000101010101010101000000000000000100000001000000000000000000000000", hex"000202020202020202000000000000000200000002000000000000000000000000", hex"000303030303030303000000000000000300000003000000000000000000000000", - hex"000404040404040404000000000000000400000004000000000000000000000000") + hex"000404040404040404000000000000000400000004000000000000000000000000" + ) // This test vector uses multi-frame payloads intertwined with single-frame payloads. // The number of frames (-1) is encoded in the first 5 bits of the first byte. @@ -269,7 +271,8 @@ object SphinxSpec { hex"000101010101010101000000000000000100000001000000000000000000000000", hex"0922222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", hex"013333333333333333333333333333333333333333333333333333333333333333", - hex"000404040404040404000000000000000400000004000000000000000000000000") + hex"000404040404040404000000000000000400000004000000000000000000000000" + ) // This test vector uses multi-frame payloads. // It fills all the frames available in an onion packet. @@ -280,7 +283,8 @@ object SphinxSpec { hex"2111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", hex"192222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", hex"11333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", - hex"11444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444") + hex"11444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444" + ) // This test vector uses a payload containing a single hop filling all the available frames. // origin -> recipient (20 frames) From f2fff4ff2176a07a01666f89e15eeb4946997874 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Wed, 15 May 2019 17:20:05 +0200 Subject: [PATCH 06/27] Features: add multi-frame onion feature flag. Advertise support for this flag in our node announcement. --- .../main/scala/fr/acinq/eclair/Features.scala | 7 +++--- .../acinq/eclair/router/Announcements.scala | 10 ++++---- .../scala/fr/acinq/eclair/FeaturesSpec.scala | 10 ++++++-- .../eclair/router/AnnouncementsSpec.scala | 1 + .../wire/LightningMessageCodecsSpec.scala | 23 ++++++------------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index 2e40b4f4ce..b2f7dc90e5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -21,7 +21,6 @@ import java.util.BitSet import scodec.bits.ByteVector - /** * Created by PM on 13/02/2017. */ @@ -36,12 +35,13 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 + val OPTION_MULTI_FRAME_ONION_MANDATORY = 0 + val OPTION_MULTI_FRAME_ONION_OPTIONAL = 1 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) def hasFeature(features: ByteVector, bit: Int): Boolean = hasFeature(BitSet.valueOf(features.reverse.toArray), bit) - /** * Check that the features that we understand are correctly specified, and that there are no mandatory features that * we don't understand (even bits) @@ -51,7 +51,8 @@ object Features { for (i <- 0 until bitset.length() by 2) { if (bitset.get(i) && !supportedMandatoryFeatures.contains(i)) return false } - return true + + true } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 2ade1fe802..e2f396902e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -19,11 +19,10 @@ package fr.acinq.eclair.router import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey, sha256, verifySignature} import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, LexicographicalOrdering} import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, serializationResult} +import fr.acinq.eclair.{Features, ShortChannelId, serializationResult} import scodec.bits.{BitVector, ByteVector} import shapeless.HNil -import scala.concurrent.duration._ import scala.compat.Platform import scala.concurrent.duration._ @@ -75,8 +74,9 @@ object Announcements { } def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], timestamp: Long = Platform.currentTime.milliseconds.toSeconds): NodeAnnouncement = { - require(alias.size <= 32) - val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, ByteVector.empty, nodeAddresses, unknownFields = ByteVector.empty) + require(alias.length <= 32) + val features = ByteVector.fromByte((1 << Features.OPTION_MULTI_FRAME_ONION_OPTIONAL).byteValue) + val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, nodeAddresses, unknownFields = ByteVector.empty) val sig = Crypto.sign(witness, nodeSecret) NodeAnnouncement( signature = sig, @@ -84,7 +84,7 @@ object Announcements { nodeId = nodeSecret.publicKey, rgbColor = color, alias = alias, - features = ByteVector.empty, + features = features, addresses = nodeAddresses ) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index a0813d961b..79c2a09d75 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -43,12 +43,18 @@ class FeaturesSpec extends FunSuite { assert(areSupported(features) && hasFeature(features, OPTION_DATA_LOSS_PROTECT_OPTIONAL) && hasFeature(features, INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) } + test("'multi_frame_onion' feature") { + assert(hasFeature(hex"01", Features.OPTION_MULTI_FRAME_ONION_MANDATORY)) + assert(hasFeature(hex"02", Features.OPTION_MULTI_FRAME_ONION_OPTIONAL)) + } + test("features compatibility") { assert(areSupported(Protocol.writeUInt64(1l << INITIAL_ROUTING_SYNC_BIT_OPTIONAL, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1l << OPTION_DATA_LOSS_PROTECT_OPTIONAL, ByteOrder.BIG_ENDIAN))) - assert(areSupported(hex"14") == false) - assert(areSupported(hex"0141") == false) + assert(areSupported(Protocol.writeUInt64(1l << OPTION_MULTI_FRAME_ONION_OPTIONAL, ByteOrder.BIG_ENDIAN))) + assert(!areSupported(hex"14")) + assert(!areSupported(hex"0141")) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 432101f27b..34922fa82c 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -49,6 +49,7 @@ class AnnouncementsSpec extends FunSuite { test("create valid signed node announcement") { val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses) + assert(Features.hasFeature(ann.features, Features.OPTION_MULTI_FRAME_ONION_OPTIONAL)) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index 700b552971..5936eae80f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -44,21 +44,12 @@ class LightningMessageCodecsSpec extends FunSuite { def publicKey(fill: Byte) = PrivateKey(ByteVector.fill(32)(fill)).publicKey test("encode/decode live node_announcements") { - val anns = List( - hex"a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce26900005acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607" - //hex"d5bfb0be26412eed9bbab186772bd3885610e289ed305e729869a5bcbd97ea431863b6fa884b021162ed5e66264c4087630e4403669bab29f3c533c4089e508c00005ab521eb030e9226f19cd3ba8a58fb280d00f5f94f3c10f1b4618a5f9bffd43534c966ebd4030e9256495247494e41574f4c465f3200000000000000000000000000000000000000000f03cec0cb03c68094bbb48792002608" - //hex"9746cd4d25a5cf2b04f3d986a073973b0318282e32e2758939b6650cd13cf65e4225ceaa98b02f070614e907661278a1479542afb12b9867511e0d31d995209800005ab646a302dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607", - //hex"a483677744b63d892a85fb7460fd6cb0504f802600956eb18cfaad05fbbe775328e4a7060476d2c0f3b7a6d505bb4de9377a55b27d1477baf14c367287c3de7900005abb440002dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607", - //hex"3ecfd85bcb3bafb5bad14ab7f6323a2df33e161c37c2897e576762fa90ffe46078d231ebbf7dce3eff4b440d091a10ea9d092e698a321bb9c6b30869e2782c9900005abbebe202dc523b9db431de52d7adb79cf81dd3d780002f4ce952706053edc9da30d9b9f702dc5256495247494e41574f4c460000000000000000000000000000000000000000000016031bb5481aa82769f4446e1002260701584473f82607", - //hex"ad40baf5c7151777cc8896bc70ad2d0fd2afff47f4befb3883a78911b781a829441382d82625b77a47b9c2c71d201aab7187a6dc80e7d2d036dcb1186bac273c00005abffc330341f5ff2992997613aff5675d6796232a63ab7f30136219774da8aba431df37c80341f563377a6763723364776d777a7a3261652e6f6e696f6e00000000000000000000000f0317f2614763b32d9ce804fc002607" - ) - - anns.foreach { ann => - val bin = ann.toBitVector - val node = nodeAnnouncementCodec.decode(bin).require.value - val bin2 = nodeAnnouncementCodec.encode(node).require - assert(bin === bin2) - } + val ann = hex"a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce2690001025acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607" + val bin = ann.toBitVector + + val node = nodeAnnouncementCodec.decode(bin).require.value + val bin2 = nodeAnnouncementCodec.encode(node).require + assert(bin === bin2) } test("encode/decode all channel messages") { @@ -77,7 +68,7 @@ class LightningMessageCodecsSpec extends FunSuite { val commit_sig = CommitSig(randomBytes32, randomBytes64, randomBytes64 :: randomBytes64 :: randomBytes64 :: Nil) val revoke_and_ack = RevokeAndAck(randomBytes32, scalar(0), point(1)) val channel_announcement = ChannelAnnouncement(randomBytes64, randomBytes64, randomBytes64, randomBytes64, bin(7, 9), Block.RegtestGenesisBlock.hash, ShortChannelId(1), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey) - val node_announcement = NodeAnnouncement(randomBytes64, bin(0, 0), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) + val node_announcement = NodeAnnouncement(randomBytes64, bin(1, 2), 1, randomKey.publicKey, Color(100.toByte, 200.toByte, 300.toByte), "node-alias", IPv4(InetAddress.getByAddress(Array[Byte](192.toByte, 168.toByte, 1.toByte, 42.toByte)).asInstanceOf[Inet4Address], 42000) :: Nil) val channel_update = ChannelUpdate(randomBytes64, Block.RegtestGenesisBlock.hash, ShortChannelId(1), 2, 42, 0, 3, 4, 5, 6, None) val announcement_signatures = AnnouncementSignatures(randomBytes32, ShortChannelId(42), randomBytes64, randomBytes64) val gossip_timestamp_filter = GossipTimestampFilter(Block.RegtestGenesisBlock.blockId, 100000, 1500) From 02de66a8b65323d3d71eee8241bad0f3ca5ec403 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Wed, 22 May 2019 10:23:47 +0200 Subject: [PATCH 07/27] SphinxSpec: add official test vector. --- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index a30df2cfdf..32f8c3d0c8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -73,21 +73,21 @@ class SphinxSpec extends FunSuite { */ test("generate single-frame filler") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), singleFramePayloads.dropRight(1)) + val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceSingleFramePayloads.dropRight(1)) assert(filler == hex"c6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") } /* - filler = 0x77df18cc319848a016c8239c0ebb732bad7978511122520680894c116cc99b15d3acd6b94a39c4ccba3928dc52fed3d50572159847c4772fe43f6812b02ed16b9ce08d4d8d472527872e4697e9d31e451f81e726ed53fdd417ef2ecaf30b6ed8bfc72fc71102a4417b97b9fb1a1af471eb3a4a5a33f1d8b67bba45d89449c91bf5b7346f22dc71b3081de49830f1ccdf14982abd6647169f6dfb3fbf4d12eb66b92370c34d865d6c794072125baa778da953ebe6c4448f3875096e2039d594d14443ecf2b5b9847d59a5d01f72c8162846a67f750796cbb767a42835dd015fe16493f38b9c3b6cd35f9d019d6538e993184b8eeca4e18ae02ec0b42c20749865d4ba3784137e0bc72e3ec1b347b0fad52738fd09e5f76f3f87374c217f6e42b5b5ef7b189d6e38e9a6a649e14bd1ffdd4affaedeac5577ce9b0064e1729c83c4fa313ad59abf83fe8340a09f9cc0b5e2dc78a8507bad79ffbb916d336ecb38bd9ed463855c94bb91349013d90108b10a748ec892782ed9d39322a06f77001b368d9c56fe88a60e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac + filler = 0xec46532066fd5c8296a1b3ba75fe06c6f8d3c61b6d177512f0ae5a987030ea69a21af42cbc0989a13a1a0721fe7877a3fc7931c3136ad5b2c078600d8867c33e29ef61f91b4fc24bf0405b46efd70aa67c3dfbb63ef0979bdb1536eb103dc15ec63533560e081f45be5c7f3fccc0e50ca9ab4d8a93f9b444779993b1b14eeebdac9bfb0da1641c36a72dd23ccedbaa30f4745c83d6252b18cac443264854eb31904aa1bb140a88c2de41b333eba32efcacf7647316a136e32a893228b2e407456c26ef4b0b4bc24615b73988bbb967e3094d1d20ee19bcee838c1ff8e0cb268f86e22df2fa87ccf96227d0c5088bd6e8fa307f98634f0e96b91eacc86d88eb29a5eed0a0f633ff83ded2cee3ca35079cf0a16fb2303326d37804d20bcdaa1f9283a5e63c936207ddd1c25c712cb8a5f8fd050fb357db9e766b0d60812ced013a312ea325d1e3cd3e966bc8bd87ae3fc141ad89e6c7b61170191567bed291266f4e89225d5e6db93b9cf602674061df818bff34bc99772f96107ccc3839a9f879131299e5b56e3abe0bf55a30771807640ff11f1f9d07dcd6112f6e4c7ba3008ef4b070284524cad49cf9f9d636a7964318742158aa60fb3decd1f353c759beff1194c0fe00d3770e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac */ test("generate multi-frame filler") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), multiFramePayloads.dropRight(1)) - assert(filler == hex"77df18cc319848a016c8239c0ebb732bad7978511122520680894c116cc99b15d3acd6b94a39c4ccba3928dc52fed3d50572159847c4772fe43f6812b02ed16b9ce08d4d8d472527872e4697e9d31e451f81e726ed53fdd417ef2ecaf30b6ed8bfc72fc71102a4417b97b9fb1a1af471eb3a4a5a33f1d8b67bba45d89449c91bf5b7346f22dc71b3081de49830f1ccdf14982abd6647169f6dfb3fbf4d12eb66b92370c34d865d6c794072125baa778da953ebe6c4448f3875096e2039d594d14443ecf2b5b9847d59a5d01f72c8162846a67f750796cbb767a42835dd015fe16493f38b9c3b6cd35f9d019d6538e993184b8eeca4e18ae02ec0b42c20749865d4ba3784137e0bc72e3ec1b347b0fad52738fd09e5f76f3f87374c217f6e42b5b5ef7b189d6e38e9a6a649e14bd1ffdd4affaedeac5577ce9b0064e1729c83c4fa313ad59abf83fe8340a09f9cc0b5e2dc78a8507bad79ffbb916d336ecb38bd9ed463855c94bb91349013d90108b10a748ec892782ed9d39322a06f77001b368d9c56fe88a60e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") + val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceMultiFramePayloads.dropRight(1)) + assert(filler == hex"ec46532066fd5c8296a1b3ba75fe06c6f8d3c61b6d177512f0ae5a987030ea69a21af42cbc0989a13a1a0721fe7877a3fc7931c3136ad5b2c078600d8867c33e29ef61f91b4fc24bf0405b46efd70aa67c3dfbb63ef0979bdb1536eb103dc15ec63533560e081f45be5c7f3fccc0e50ca9ab4d8a93f9b444779993b1b14eeebdac9bfb0da1641c36a72dd23ccedbaa30f4745c83d6252b18cac443264854eb31904aa1bb140a88c2de41b333eba32efcacf7647316a136e32a893228b2e407456c26ef4b0b4bc24615b73988bbb967e3094d1d20ee19bcee838c1ff8e0cb268f86e22df2fa87ccf96227d0c5088bd6e8fa307f98634f0e96b91eacc86d88eb29a5eed0a0f633ff83ded2cee3ca35079cf0a16fb2303326d37804d20bcdaa1f9283a5e63c936207ddd1c25c712cb8a5f8fd050fb357db9e766b0d60812ced013a312ea325d1e3cd3e966bc8bd87ae3fc141ad89e6c7b61170191567bed291266f4e89225d5e6db93b9cf602674061df818bff34bc99772f96107ccc3839a9f879131299e5b56e3abe0bf55a30771807640ff11f1f9d07dcd6112f6e4c7ba3008ef4b070284524cad49cf9f9d636a7964318742158aa60fb3decd1f353c759beff1194c0fe00d3770e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") } test("create single-frame packet (reference test vector)") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, singleFramePayloads, associatedData) + val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceSingleFramePayloads, associatedData) assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) @@ -95,7 +95,7 @@ class SphinxSpec extends FunSuite { val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) - assert(Seq(payload0, payload1, payload2, payload3, payload4) == singleFramePayloads) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceSingleFramePayloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) @@ -107,23 +107,23 @@ class SphinxSpec extends FunSuite { assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } - test("create multi-frame packet") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, multiFramePayloads, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619f4f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2ce49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e5871838529d0eb5e1f0e26a68ee6e88eaa81124f360afcfe056ac1d095d40d4f681cf34499e80606dcf0216855a3e6540745a6c55da121259e09a3053ffea849b6fb32d964ebb4433f00240e6dabef9c103667ee4d734daf9c43e38e44fdc1019b13efe8923c154590ab474bd51db6d0ab655967e671542f1c5c0c4fd4245dfde83c5bd8251ed8983c280843d97530a72ae7c6fb883df5d645afb046c5cb9ee73b1c473bbf488ad3bc1ddc341296b537a887452f006673e5bb99097ae1599f7d6497afd66f26ea163e276f476a773473ac572f6022a238ccc7d0b7360bf8827de566a8726eb36ca48b0bc8a146f217ad9fbafc602d109ab4742214195087588d76183bbf27b399bd3e0c60849e4d86c446e38fbe37218a501bd3945caa2c2cd656d40e1f5f863c98a30f3ee8dcd7c564a85a9b61d09e2fcb2a47add89bde32cec62d5f102a797f33693d4e285448b7929a89dc0da684b44c8596f1f903245a6d6133b41433f94aac919c579be28c691a134e0e05c7372670c7947f99ce50ee24bc7f97ed12feaba3411a479af84c94610d672f026cbb2c2ad66f28f6aa8c79f2ae99c45c9f17bee5dc0a4137d62aa5886f52efdb81bde49237f6e34774a89780bda053ee4590c622544620b4c40e9a9cf8117e071c26faa85559307d6f504d5af9d291de16886146380ee7590dde7cdfcd7debd91997b62c9a79c0f3e0e7591a255ee6bb240841cfd1e8da49b7ec803c1473115cfa394ddcd11268defde62e80b0f5d639c3e34f15cd50602711b9e8cec2a5edae718390d60072e050cbb62738851fb3fe7918730113619890283982466e49b9509fab5dddc8e0f2a7af6dfa4eac057c4be2c28b0a0f8054a0ecc2ea6711bfc5df639ea53b4947f386b0434926374e2e26e982a43c85eb5b426316c9180fdfae0f2b8d4636c780a395fb0dc7a7dc2d4a08ccb81236ef162a7f72e16e215f1bb5506de64d81917a232ebd3c49063b87d44ea2a769577f538ca8ae7806a0688d00c50592f5c9dc642f19e21bd0f3c459f770fbbbaacdb9de345d65cee4ce2e4b27f8e2d2d1a3596bf87de876d64528951fe472e5c5bd7d9d1270eb7a692ffce5d1a9f799f5905b944d4a138f985434e5df98e819fa337d4dda140e631806598e60a46de98f498fe5750e280719af2b04c7bec6f82c494699113fa26bc8a4c65fdf9ddb857109b95d78526fc329ac3bae8bb6c45f98c35acd7e8294bfc86706aa6d99fa770dfb3f5c0ac43301911d75c91106744d48b139e71cea3e4a83f6a124c6496b5b5177625edd7ebbf003061bdfdc80be4177d68a8f1d1b9b100eb91a638eef34539af4f44e2056831ef278ba13e2574333d0f2f1ba6195e2f1595c95697d90e8664195059b5a88f88e540d84f49c03b60775c17ff61d49967c99be5c0f58427226e4b61342120a7396d8053b730b22ccf70937465be115b47ffa3d9d94909c9b43ff3f40d1c75662e7f1d733cbdafc837e9ef8fc225f8838ce3fe2540caccfa4413f87714f57497fe04a895489b2f3e93b6a14bb734419a270f8352c11ceaecf74802fbb6cfb3d5862482ffca751eaaae92f260c3f0dbfde855ad7507d54e96dc232991011422efa7f00f4e94e3a2eed2ebd5c195730350dc6987691fae4fb68") + test("create multi-frame packet (reference test vector)") { + val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceMultiFramePayloads, associatedData) + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7188818bd40634aa515d2a9bc473fbcc3a3896391dbae9ed22b19f037fb2c004e4d10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793edb487b9ae0c8d87ab577dc51e030fc6086c7f5fc26ae7c5735a02c03a64224aee701faad79527c7a161ab08f8e865c873bf6b6ecb367819c43762521351d7964238add9bb94a74f5aba4011c6dd5c6c061e5602acb56969b38a66cc6a9a6f55a1ef8201831b403cc1e6588413b0aecd81b24ee0533a4955c677781485b08291c98001fc08067dc124facc49d211b93b727387b0b3ba423ece2aa6b6ed0ccf5503cbb06853918dc74342166696880645cd465b43f7e2b02676153fb278b85940189216e8bd70d7326363e878ada2dfae7f1b305058a4bffe75cbf5036989f4aaa30d8297cadc148bc603837fc7fe371ab7e466a3ec486d1d6e3e1dd82019bc01882d586514b4e5c9e7a5dd7754f48f7702e99c9ac1b84b811b944f6bb22c58f0bb7a538d7123f1c5b88b03a43f4924512cfc84555da326847824954af989e62f315afc855084a07d3400b04278d5a173bb3a95f7b0832c43c829f57e7469ba63a89cace2a7c15121108ffd40b0c804ab8f6fab44e110bee5e2f01ec9575adc53538582afd6ebe48d6aff8a0eac698d1a1f157e9999f3f3e424d46fb3ffefe420efeac1f7333b92b4eed93f8c32b2b436514375d7f84073f8606efdd5c4a452378a47f50a926675faf980ecfecba991c98351c5a36a27942972e26c5205ad2f5fbc1fe57496b2bcd046fffec92f960b1dc2e57f94408ba34785f9fd5af9c1f6f7368f09510f20e4eef76c144e33c8e312a9a5333fa2ec6615da4e3d79252cc1d0669ef1e8054a0238b92d05aacf00299604f830f6532e01da0518964567accf676e802096fd3adb1ea7af60094b3d82141b9cf1007b13a8f6565d691397fe6eda13fa3ee08bf0a990c3c145a440d7f726bfddbcdb8d065439e124a23f289a13d22320d55b1cddbc661f6587eb844598fa6bc2f2df6257956d142fea702e108bbbe1267d5eecb30e0e9bc33f7dc16853144d70c8d59c8b7c93c999e6b308c7b5092cd3f6bf4c496882639156b261f1f4039a40d0e7ac4717a2e13259a08126d17f48618c57b8005bcc98aa48c6bcd6519c1c37b0bc2a8f953745f7f0b0604c3b91b8905140e6b207517cbaebaac87489925a6d56e0eb5e47178e62e8d6614229ed2e3d831c7f278dfe89dac6ad3bcb54c6df85fc9d757f7168f0006f151a38e916c11abeb93ee3931c583e9afefc26910ae1b95bfe019855ba99a09bf1d84ec7be8783000c172802f63edba24daafda69448a3acf425208c8bd3ae944882e35c78c2610bd0536397bff7c6f3a582a5907bfe09e2b791a6e87380c2ca1d3ca449ab836c6769c1727f051ee02c967f891cb3ad7161c4942e9eb5dac560effd3d913afc2240b50077cb907064291e9792c9a3c0c0f881c5be0b10592263a87d418086e2fd7e050d502a423febd87a0488efb95b25c312b6e46638cd295c944bcf25f5b6bbf798af7fcfecaf8186f516aeb4b50a74439ebad8564ea680d568d948a5ee23dba9f68a867e033ee62eb5268f934ea197729d60e0ebf3c5997cdade0d0cb78cb37dc1555956c0821384fa6760409133fa45e127a8c54c09c99e0b684654530df5686c676708f5640d9d4f31f41dcd20c1a68bd7ffe98b2a7cc0f9341169b5bf94aa611c21a19e76d29a43cefd20119e3ccf04805ba3d95a432f497487b3d64bec0597b20eaf70ba26010691c84fbfb9585f4796ec5e8c4794baedbfca9e8d6f8ef17eb27758c96a6a6d9e62b47c7f0e06") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) - assert(Seq(payload0, payload1, payload2, payload3, payload4) == multiFramePayloads) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceMultiFramePayloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) - assert(packets(0).hmac == ByteVector32(hex"e575aa1b4dee5e5851cea7528644d23ffda8e6948821b682c80fc4e694b541b6")) - assert(packets(1).hmac == ByteVector32(hex"1ebf0754edc9b16e035870078172138ebd58e82d34bc5f76a973979d73776116")) - assert(packets(2).hmac == ByteVector32(hex"400dd7fd80fed9963618a6c262e7a7182cc9c8618f69245fe8080173c1faa501")) - assert(packets(3).hmac == ByteVector32(hex"49602e7e652eb2987df05b5ea1e45c797791c9a29f53b1309705175097def065")) + assert(packets(0).hmac == ByteVector32(hex"c9c4b58b8b161c64453a35906f1f559aa3dca4823a38697e38ffcb207411ac9f")) + assert(packets(1).hmac == ByteVector32(hex"115671e3866f025551ddab0c3a91437daf86a9169d0eed1c447705abed2cfe13")) + assert(packets(2).hmac == ByteVector32(hex"fa8ed50911736b2a4c13c832f9660993440bbc909de4c1c7d3b404820f79d21f")) + assert(packets(3).hmac == ByteVector32(hex"b8e37ddbe2ffdcf8b77c9567331b28b8e77ab5d4fe8caca1091809f36d01f575")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } @@ -157,7 +157,7 @@ class SphinxSpec extends FunSuite { } test("last node replies with an error message") { - for (payloads <- Seq(singleFramePayloads, multiFramePayloads, multiFramePayloadsFull)) { + for (payloads <- Seq(referenceSingleFramePayloads, referenceMultiFramePayloads, multiFramePayloadsFull)) { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 // origin build the onion packet @@ -205,7 +205,7 @@ class SphinxSpec extends FunSuite { } test("intermediate node replies with an error message") { - for (payloads <- Seq(singleFramePayloads, multiFramePayloads, multiFramePayloadsFull)) { + for (payloads <- Seq(referenceSingleFramePayloads, referenceMultiFramePayloads, multiFramePayloadsFull)) { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 // origin build the onion packet @@ -255,7 +255,7 @@ object SphinxSpec { // This test vector uses only single-frame payloads. // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 - val singleFramePayloads = Seq( + val referenceSingleFramePayloads = Seq( hex"000000000000000000000000000000000000000000000000000000000000000000", hex"000101010101010101000000000000000100000001000000000000000000000000", hex"000202020202020202000000000000000200000002000000000000000000000000", @@ -263,14 +263,14 @@ object SphinxSpec { hex"000404040404040404000000000000000400000004000000000000000000000000" ) - // This test vector uses multi-frame payloads intertwined with single-frame payloads. + // This test vector uses a multi-frame payload intertwined with single-frame payloads. // The number of frames (-1) is encoded in the first 5 bits of the first byte. - // origin -> node #0 (3 frames) -> node #1 (1 frame) -> node #2 (2 frames) -> node #3 (1 frame) -> node #4 (1 frame) - val multiFramePayloads = Seq( - hex"11000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // origin -> node #0 (1 frame) -> node #1 (1 frame) -> node #2 (N frames) -> node #3 (1 frame) -> node #4 (1 frame) + val referenceMultiFramePayloads = Seq( + hex"000000000000000000000000000000000000000000000000000000000000000000", hex"000101010101010101000000000000000100000001000000000000000000000000", - hex"0922222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", - hex"013333333333333333333333333333333333333333333333333333333333333333", + hex"20000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000000000000000000000000000000000000000000000000000000000000000000000000", + hex"000303030303030303000000000000000300000003000000000000000000000000", hex"000404040404040404000000000000000400000004000000000000000000000000" ) From 7bd8942e1849c5e4e7919970b4dc42de6dd58e42 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Tue, 28 May 2019 17:55:26 +0200 Subject: [PATCH 08/27] Sphinx: use variable-length payloads. --- .../acinq/eclair/channel/ChannelTypes.scala | 2 +- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 127 +++++++++--------- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 119 ++++++++-------- .../eclair/payment/ChannelSelectionSpec.scala | 4 +- 4 files changed, 133 insertions(+), 119 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 4cfbf66c53..65ef68a6ea 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -107,7 +107,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.LAST_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.EMPTY_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 84d5039ef2..011be97ed9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -21,7 +21,7 @@ import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, Crypto, Protocol} -import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs} +import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs, CommonCodecs} import grizzled.slf4j.Logging import org.spongycastle.crypto.digests.SHA256Digest import org.spongycastle.crypto.macs.HMac @@ -38,37 +38,25 @@ import scala.util.{Failure, Success, Try} object Sphinx extends Logging { val Version = 0.toByte + // length of a public key + val PubKeyLength = 33 + // length of a MAC val MacLength = 32 - // max number of hops - val MaxHops = 20 - - // A frame is the smallest unit of memory that can be used by a hop to store its payload. - // Each hop may use multiple frames. - // Parts of the frame are fixed: - // - The first byte of the first frame contains the number of frames used by the payload and the realm, which indicates how the payload should be parsed. - // - The last 32 bytes of the last frame contain the HMAC that should be passed to the next hop, or 0 for the last hop. - // - All other bytes can be used to store the hop's payload. - val FrameSize = 65 - - // The maximum size a payload for a single hop can be. This is the worst case scenario of a single hop, consuming all 20 frames. - // We need to know this in order to generate a sufficiently long stream of pseudo-random bytes when encrypting/decrypting the payload. - val MaxPayloadLength = MaxHops * FrameSize + // The 1.0 BOLT spec used 20 fixed-size 65-bytes frames inside the onion payload. + // The first byte of the frame (called `realm`) was set to 0x00, followed by 32 bytes of hop data and a 32-bytes mac. + // The 1.1 BOLT spec changed that format to use variable-length per-hop length. + val LegacyFrameSize = 65 - // length of the obfuscated onion data - val RoutingInfoLength = MaxHops * FrameSize + // length of the obfuscated onion payload + val OnionPayloadLength = 1300 // onion packet length - val PacketLength = 1 + 33 + MacLength + RoutingInfoLength + val PacketLength = 1 + PubKeyLength + MacLength + OnionPayloadLength - // last packet (all zeroes except for the version byte) - val LAST_PACKET = Packet(Version, ByteVector.fill(33)(0), ByteVector32.Zeroes, ByteVector.fill(RoutingInfoLength)(0)) - - // The 5 MSB of the first frame of the payload contains the number of frames used. - def payloadFrameCount(payload: ByteVector): Int = { - ((payload.head >> 3) & 0x1F) + 1 - } + // packet construction starts with an empty packet (all zeroes except for the version byte) + val EMPTY_PACKET = Packet(Version, ByteVector.fill(PubKeyLength)(0), ByteVector32.Zeroes, ByteVector.fill(OnionPayloadLength)(0)) def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { val mac = new HMac(new SHA256Digest()) @@ -123,23 +111,42 @@ object Sphinx extends Logging { } } + /** + * Return the amount of bytes that should be read to extract the per-hop payload (data and mac). + */ + def currentHopLength(payload: ByteVector): Int = { + payload.head match { + case 0 => LegacyFrameSize + case _ => + // For non-legacy packets the first bytes are a varint encoding the length of the payload data (not including mac). + // Since the onion packet is only 1300-bytes long, the varint will either be 1 or 3 bytes long. + val dataLength = CommonCodecs.varlong.decode(BitVector(payload.take(3))).require.value.toInt + val varintLength = dataLength match { + case i if i < 253 => 1 + case _ => 3 + } + varintLength + dataLength + MacLength + } + } + def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], payloads: Seq[ByteVector]): ByteVector = { require(sharedSecrets.length == payloads.length, "the number of secrets should equal the number of payloads") (sharedSecrets zip payloads).foldLeft(ByteVector.empty)((padding, secretAndPayload) => { val (secret, payload) = secretAndPayload - val payloadLength = FrameSize*payloadFrameCount(payload) + val payloadLength = currentHopLength(payload) + require(payloadLength == payload.length + MacLength, s"invalid payload: length isn't correctly encoded: $payload") val key = generateKey(keyType, secret) val padding1 = padding ++ ByteVector.fill(payloadLength)(0) - val stream = generateStream(key, RoutingInfoLength + payloadLength).takeRight(padding1.length) + val stream = generateStream(key, OnionPayloadLength + payloadLength).takeRight(padding1.length) padding1.xor(stream) }) } - case class Packet(version: Int, publicKey: ByteVector, hmac: ByteVector32, routingInfo: ByteVector) { - require(publicKey.length == 33, "onion packet public key length should be 33") + case class Packet(version: Int, publicKey: ByteVector, hmac: ByteVector32, onionPayload: ByteVector) { + require(publicKey.length == PubKeyLength, s"onion packet public key length should be $PubKeyLength") require(hmac.length == MacLength, s"onion packet hmac length should be $MacLength") - require(routingInfo.length == RoutingInfoLength, s"onion packet routing info length should be $RoutingInfoLength") + require(onionPayload.length == OnionPayloadLength, s"onion packet payload length should be $OnionPayloadLength") def isLastPacket: Boolean = hmac == ByteVector32.Zeroes @@ -149,13 +156,13 @@ object Sphinx extends Logging { object Packet { def read(in: InputStream): Packet = { val version = in.read - val publicKey = new Array[Byte](33) + val publicKey = new Array[Byte](PubKeyLength) in.read(publicKey) - val routingInfo = new Array[Byte](RoutingInfoLength) - in.read(routingInfo) + val onionPayload = new Array[Byte](OnionPayloadLength) + in.read(onionPayload) val hmac = new Array[Byte](MacLength) in.read(hmac) - Packet(version, ByteVector.view(publicKey), ByteVector32(ByteVector.view(hmac)), ByteVector.view(routingInfo)) + Packet(version, ByteVector.view(publicKey), ByteVector32(ByteVector.view(hmac)), ByteVector.view(onionPayload)) } def read(in: ByteVector): Packet = read(new ByteArrayInputStream(in.toArray)) @@ -163,7 +170,7 @@ object Sphinx extends Logging { def write(packet: Packet, out: OutputStream): OutputStream = { out.write(packet.version) out.write(packet.publicKey.toArray) - out.write(packet.routingInfo.toArray) + out.write(packet.onionPayload.toArray) out.write(packet.hmac.toArray) out } @@ -201,23 +208,23 @@ object Sphinx extends Logging { val packet = Packet.read(rawPacket) val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey) val mu = generateKey("mu", sharedSecret) - val check = mac(mu, packet.routingInfo ++ associatedData) + val check = mac(mu, packet.onionPayload ++ associatedData) require(check == packet.hmac, "invalid header mac") val rho = generateKey("rho", sharedSecret) // Since we don't know the length of the hop payload (we will learn it once we decode the first byte), // we have to pessimistically generate a long cipher stream. - val stream = generateStream(rho, RoutingInfoLength + MaxPayloadLength) - val bin = (packet.routingInfo ++ ByteVector.fill(MaxPayloadLength)(0)) xor stream + val stream = generateStream(rho, 2 * OnionPayloadLength) + val bin = (packet.onionPayload ++ ByteVector.fill(OnionPayloadLength)(0)) xor stream - val payloadLength = payloadFrameCount(bin)*FrameSize - val payload = bin.take(payloadLength-MacLength) + val payloadLength = currentHopLength(bin) + val payload = bin.take(payloadLength - MacLength) - val hmac = ByteVector32(bin.slice(payloadLength-MacLength, payloadLength)) - val nextRouteInfo = bin.drop(payloadLength).take(RoutingInfoLength) + val hmac = ByteVector32(bin.slice(payloadLength - MacLength, payloadLength)) + val nextOnionPayload = bin.drop(payloadLength).take(OnionPayloadLength) val nextPubKey = blind(PublicKey(packet.publicKey), computeBlindingFactor(PublicKey(packet.publicKey), sharedSecret)) - ParsedPacket(payload, Packet(Version, nextPubKey.value, hmac, nextRouteInfo), sharedSecret) + ParsedPacket(payload, Packet(Version, nextPubKey.value, hmac, nextOnionPayload), sharedSecret) } @tailrec @@ -236,30 +243,28 @@ object Sphinx extends Logging { * - then you call makeNextPacket(...) until you've built the final onion packet that will be sent to the first node * in the route * - * @param payload payload for this packet - * @param associatedData associated data + * @param payload payload for this packet + * @param associatedData associated data * @param ephemeralPublicKey ephemeral key for this packet - * @param sharedSecret shared secret - * @param packet current packet (1 + all zeroes if this is the last packet) - * @param routingInfoFiller optional routing info filler, needed only when you're constructing the last packet + * @param sharedSecret shared secret + * @param packet current packet (1 + all zeroes if this is the last packet) + * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet * @return the next packet */ - private def makeNextPacket(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: ByteVector, sharedSecret: ByteVector32, packet: Packet, routingInfoFiller: ByteVector = ByteVector.empty): Packet = { - require(payload.length <= MaxPayloadLength-MacLength, s"packet payload cannot exceed ${MaxPayloadLength-MacLength} bytes") - require((payload.length+MacLength) % FrameSize == 0, "the payload and mac should use a discrete number of frames") - - val nextRoutingInfo = { - val routingInfo1 = payload ++ packet.hmac ++ packet.routingInfo.dropRight(payload.length + MacLength) - val routingInfo2 = routingInfo1 xor generateStream(generateKey("rho", sharedSecret), RoutingInfoLength) - routingInfo2.dropRight(routingInfoFiller.length) ++ routingInfoFiller + private def makeNextPacket(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: ByteVector, sharedSecret: ByteVector32, packet: Packet, onionPayloadFiller: ByteVector = ByteVector.empty): Packet = { + require(payload.length <= OnionPayloadLength - MacLength, s"packet payload cannot exceed ${OnionPayloadLength - MacLength} bytes") + + val nextOnionPayload = { + val onionPayload1 = payload ++ packet.hmac ++ packet.onionPayload.dropRight(payload.length + MacLength) + val onionPayload2 = onionPayload1 xor generateStream(generateKey("rho", sharedSecret), OnionPayloadLength) + onionPayload2.dropRight(onionPayloadFiller.length) ++ onionPayloadFiller } - val nextHmac = mac(generateKey("mu", sharedSecret), nextRoutingInfo ++ associatedData) - val nextPacket = Packet(Version, ephemeralPublicKey, nextHmac, nextRoutingInfo) + val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload ++ associatedData) + val nextPacket = Packet(Version, ephemeralPublicKey, nextHmac, nextOnionPayload) nextPacket } - /** * * @param packet onion packet @@ -277,7 +282,7 @@ object Sphinx extends Logging { case class ErrorPacket(originNode: PublicKey, failureMessage: FailureMessage) /** - * Builds an encrypted onion packet that contains payloads and routing information for all nodes in the list + * Builds an encrypted onion packet that contains payloads for all nodes in the list * * @param sessionKey session key * @param publicKeys node public keys (one per node) @@ -290,7 +295,7 @@ object Sphinx extends Logging { val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) - val lastPacket = makeNextPacket(payloads.last, associatedData, ephemeralPublicKeys.last.value, sharedsecrets.last, LAST_PACKET, filler) + val lastPacket = makeNextPacket(payloads.last, associatedData, ephemeralPublicKeys.last.value, sharedsecrets.last, EMPTY_PACKET, filler) @tailrec def loop(hoppayloads: Seq[ByteVector], ephkeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: Packet): Packet = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 32f8c3d0c8..985e1d5c19 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -27,7 +27,6 @@ import scala.util.Success /** * Created by fabrice on 10/01/17. */ - class SphinxSpec extends FunSuite { import Sphinx._ @@ -71,23 +70,23 @@ class SphinxSpec extends FunSuite { /* filler = 0xc6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac */ - test("generate single-frame filler") { + test("generate filler with fixed-size payloads") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceSingleFramePayloads.dropRight(1)) + val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceFixedSizePayloads.dropRight(1)) assert(filler == hex"c6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") } /* - filler = 0xec46532066fd5c8296a1b3ba75fe06c6f8d3c61b6d177512f0ae5a987030ea69a21af42cbc0989a13a1a0721fe7877a3fc7931c3136ad5b2c078600d8867c33e29ef61f91b4fc24bf0405b46efd70aa67c3dfbb63ef0979bdb1536eb103dc15ec63533560e081f45be5c7f3fccc0e50ca9ab4d8a93f9b444779993b1b14eeebdac9bfb0da1641c36a72dd23ccedbaa30f4745c83d6252b18cac443264854eb31904aa1bb140a88c2de41b333eba32efcacf7647316a136e32a893228b2e407456c26ef4b0b4bc24615b73988bbb967e3094d1d20ee19bcee838c1ff8e0cb268f86e22df2fa87ccf96227d0c5088bd6e8fa307f98634f0e96b91eacc86d88eb29a5eed0a0f633ff83ded2cee3ca35079cf0a16fb2303326d37804d20bcdaa1f9283a5e63c936207ddd1c25c712cb8a5f8fd050fb357db9e766b0d60812ced013a312ea325d1e3cd3e966bc8bd87ae3fc141ad89e6c7b61170191567bed291266f4e89225d5e6db93b9cf602674061df818bff34bc99772f96107ccc3839a9f879131299e5b56e3abe0bf55a30771807640ff11f1f9d07dcd6112f6e4c7ba3008ef4b070284524cad49cf9f9d636a7964318742158aa60fb3decd1f353c759beff1194c0fe00d3770e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac + filler = 0x72a32c7cf17217e3c6aa86985cf2ad07c9064676619b2219ad8b85e9dadafa929dc544e735b558940a19999baf779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3acb0dab321b834bb87aa5aa4c68e22218a540a4d94b921a53b4167f021c562d0e7da6d2b2b1783cc65596804720805c5bcd6c4ff10495fb7ed519b383258804a0e40173dfab4dedf52b30d816ad2dd732dd115eca4e7ae595ad3e5f9d3b6f645799bc1740a033cfc44aba80b74c9f3d823ee9844b6a31839bc0298f0253b19911441d6085fca875132b2cc9dcc4d0c23c29c6279ce4012200456ccd4866c07bcd803ba51435ac1b2623171688676f0a65bcd756bbd5e67e02c0d3eaa7287d70a3275260703e159665aaeddd8aa4aabbf3a90640b9d7b65320f4ea0aa8adf94720c9ebe */ - test("generate multi-frame filler") { + test("generate filler with variable-size payloads") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceMultiFramePayloads.dropRight(1)) - assert(filler == hex"ec46532066fd5c8296a1b3ba75fe06c6f8d3c61b6d177512f0ae5a987030ea69a21af42cbc0989a13a1a0721fe7877a3fc7931c3136ad5b2c078600d8867c33e29ef61f91b4fc24bf0405b46efd70aa67c3dfbb63ef0979bdb1536eb103dc15ec63533560e081f45be5c7f3fccc0e50ca9ab4d8a93f9b444779993b1b14eeebdac9bfb0da1641c36a72dd23ccedbaa30f4745c83d6252b18cac443264854eb31904aa1bb140a88c2de41b333eba32efcacf7647316a136e32a893228b2e407456c26ef4b0b4bc24615b73988bbb967e3094d1d20ee19bcee838c1ff8e0cb268f86e22df2fa87ccf96227d0c5088bd6e8fa307f98634f0e96b91eacc86d88eb29a5eed0a0f633ff83ded2cee3ca35079cf0a16fb2303326d37804d20bcdaa1f9283a5e63c936207ddd1c25c712cb8a5f8fd050fb357db9e766b0d60812ced013a312ea325d1e3cd3e966bc8bd87ae3fc141ad89e6c7b61170191567bed291266f4e89225d5e6db93b9cf602674061df818bff34bc99772f96107ccc3839a9f879131299e5b56e3abe0bf55a30771807640ff11f1f9d07dcd6112f6e4c7ba3008ef4b070284524cad49cf9f9d636a7964318742158aa60fb3decd1f353c759beff1194c0fe00d3770e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") + val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceVariableSizePayloads.dropRight(1)) + assert(filler == hex"72a32c7cf17217e3c6aa86985cf2ad07c9064676619b2219ad8b85e9dadafa929dc544e735b558940a19999baf779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3acb0dab321b834bb87aa5aa4c68e22218a540a4d94b921a53b4167f021c562d0e7da6d2b2b1783cc65596804720805c5bcd6c4ff10495fb7ed519b383258804a0e40173dfab4dedf52b30d816ad2dd732dd115eca4e7ae595ad3e5f9d3b6f645799bc1740a033cfc44aba80b74c9f3d823ee9844b6a31839bc0298f0253b19911441d6085fca875132b2cc9dcc4d0c23c29c6279ce4012200456ccd4866c07bcd803ba51435ac1b2623171688676f0a65bcd756bbd5e67e02c0d3eaa7287d70a3275260703e159665aaeddd8aa4aabbf3a90640b9d7b65320f4ea0aa8adf94720c9ebe") } - test("create single-frame packet (reference test vector)") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceSingleFramePayloads, associatedData) + test("create packet with fixed-size payloads (reference test vector)") { + val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceFixedSizePayloads, associatedData) assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) @@ -95,7 +94,7 @@ class SphinxSpec extends FunSuite { val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) - assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceSingleFramePayloads) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceFixedSizePayloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) @@ -107,57 +106,68 @@ class SphinxSpec extends FunSuite { assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } - test("create multi-frame packet (reference test vector)") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceMultiFramePayloads, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7188818bd40634aa515d2a9bc473fbcc3a3896391dbae9ed22b19f037fb2c004e4d10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793edb487b9ae0c8d87ab577dc51e030fc6086c7f5fc26ae7c5735a02c03a64224aee701faad79527c7a161ab08f8e865c873bf6b6ecb367819c43762521351d7964238add9bb94a74f5aba4011c6dd5c6c061e5602acb56969b38a66cc6a9a6f55a1ef8201831b403cc1e6588413b0aecd81b24ee0533a4955c677781485b08291c98001fc08067dc124facc49d211b93b727387b0b3ba423ece2aa6b6ed0ccf5503cbb06853918dc74342166696880645cd465b43f7e2b02676153fb278b85940189216e8bd70d7326363e878ada2dfae7f1b305058a4bffe75cbf5036989f4aaa30d8297cadc148bc603837fc7fe371ab7e466a3ec486d1d6e3e1dd82019bc01882d586514b4e5c9e7a5dd7754f48f7702e99c9ac1b84b811b944f6bb22c58f0bb7a538d7123f1c5b88b03a43f4924512cfc84555da326847824954af989e62f315afc855084a07d3400b04278d5a173bb3a95f7b0832c43c829f57e7469ba63a89cace2a7c15121108ffd40b0c804ab8f6fab44e110bee5e2f01ec9575adc53538582afd6ebe48d6aff8a0eac698d1a1f157e9999f3f3e424d46fb3ffefe420efeac1f7333b92b4eed93f8c32b2b436514375d7f84073f8606efdd5c4a452378a47f50a926675faf980ecfecba991c98351c5a36a27942972e26c5205ad2f5fbc1fe57496b2bcd046fffec92f960b1dc2e57f94408ba34785f9fd5af9c1f6f7368f09510f20e4eef76c144e33c8e312a9a5333fa2ec6615da4e3d79252cc1d0669ef1e8054a0238b92d05aacf00299604f830f6532e01da0518964567accf676e802096fd3adb1ea7af60094b3d82141b9cf1007b13a8f6565d691397fe6eda13fa3ee08bf0a990c3c145a440d7f726bfddbcdb8d065439e124a23f289a13d22320d55b1cddbc661f6587eb844598fa6bc2f2df6257956d142fea702e108bbbe1267d5eecb30e0e9bc33f7dc16853144d70c8d59c8b7c93c999e6b308c7b5092cd3f6bf4c496882639156b261f1f4039a40d0e7ac4717a2e13259a08126d17f48618c57b8005bcc98aa48c6bcd6519c1c37b0bc2a8f953745f7f0b0604c3b91b8905140e6b207517cbaebaac87489925a6d56e0eb5e47178e62e8d6614229ed2e3d831c7f278dfe89dac6ad3bcb54c6df85fc9d757f7168f0006f151a38e916c11abeb93ee3931c583e9afefc26910ae1b95bfe019855ba99a09bf1d84ec7be8783000c172802f63edba24daafda69448a3acf425208c8bd3ae944882e35c78c2610bd0536397bff7c6f3a582a5907bfe09e2b791a6e87380c2ca1d3ca449ab836c6769c1727f051ee02c967f891cb3ad7161c4942e9eb5dac560effd3d913afc2240b50077cb907064291e9792c9a3c0c0f881c5be0b10592263a87d418086e2fd7e050d502a423febd87a0488efb95b25c312b6e46638cd295c944bcf25f5b6bbf798af7fcfecaf8186f516aeb4b50a74439ebad8564ea680d568d948a5ee23dba9f68a867e033ee62eb5268f934ea197729d60e0ebf3c5997cdade0d0cb78cb37dc1555956c0821384fa6760409133fa45e127a8c54c09c99e0b684654530df5686c676708f5640d9d4f31f41dcd20c1a68bd7ffe98b2a7cc0f9341169b5bf94aa611c21a19e76d29a43cefd20119e3ccf04805ba3d95a432f497487b3d64bec0597b20eaf70ba26010691c84fbfb9585f4796ec5e8c4794baedbfca9e8d6f8ef17eb27758c96a6a6d9e62b47c7f0e06") + test("create packet with variable-size payloads (reference test vector)") { + val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceVariableSizePayloads, associatedData) + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e9e14350c2b76fc232b5e46d427d902fb8860a50479fe77889407ae7e04e1f5317872128e872a2c634bae6b7211fa0c825a9311120dc8a7e7b71453bf6699eeb64bc313a49e1458d06efee7f9b13d481f32fcc6c1aaa7a7a8f1aa5787143084f216612615db5cae80a14b79a92bd90b8ac1f23279d5d1568db1e1e7f7ffb6408874c9642da4b981e3bd64f2c59ef786868a11ffdf7690318d0a17c6542593e1a79971a39771a0f79f17103470d7636d1745da48dcaf0172ad03c5918da60c27b9b53e3509af9fe7a4273c4e52252a7489fb4f6e3ae609d9454d71bae519d07a7edf72ac2a9116dac2382c1bb6eee7c367e74667abf5998cd0d9802ea69ed85e608c7853c78df622c90bb8ed18579c4a27d7ff5e3ee6954d62a2f60b0f3d3f8db0f01ea89ac2a13afd6becacfbc22e907f52221789e1ddab1f654c7f1b3bfca924701a0d58c6ebba6a16f97455e180fac0f9ebae19ffbd8693e80a7b3a4c1bbbac0b0289d49220f6f0668cc0bf2fa736df181e85c122129e24bf273a6a1583a6b3963332b1abd341025e8b2ddaddd457a8486bcd54cc9891a31f3aa637bc4b5ad99e5f3bcdfd14d7028ad1d467b04211906a7e8502046599a40a971e0317fc9bdad9f3b5d265f60741b3692bd410e274d67eea83a0c3d3d0050fb9610473f190c1580911f4c5de514486651f569685a0547035796616d0c1a4843c62b9d87844549c6756b5bcaa16aabe6fa75191a6fe33971059b98849e2208ea5c7fcf198377eaaa4731b7c596c2665e41e75ec2b8bb0a484e3df6591aa59dd2754b7c4e50a540187b44e8405cc4f044638988f1cb3b4d5a137c539e6cd6a1be0b65bdaa68657565e8e6ab75a7290efa65fed0a9231933b93841141d6779fda4e18bacc6c0dc71fd59ea99d7245d97b095aa2d77526e27dd8966dbb48feeec1874cff0f4d812d8b78c8b5bea0a8398cbdad501022a43045944aa38a2cae34bb455a0a8e05c217ac547df8f5eed06c13cd4aba07e8750fe15ade0f82591c57360e5c95bd0a373806247f285c03b599c6900b9cb50c3b3cb9dae51145eef0807f28034c327b0aa515205064302b9930df5da66f2cc58e6ed9a6cd8687d8e95258c9fd16492ddd70faa4a940e55e3f592e54a1540ae0888e5a9b85089fab963a1b2779c4ac96d6bfc70b4c53b237622eae1d50d1979436bad6b045d4505ca6fad5fdc97db3e9dbf0591d7a2d01902816d733877d7f695764297170589cfc6afbefd63abd42ea90ffe0ce84c44e549d4abd8d3f56bf1e4ea718a1b08f0e1f0b5430d545b9a2aa621f55d071559f99b34bca131013310a5124e14848636d58cf425b680e849ceebdb9bdcdbac1785e0d7aefcecdf2dccdade0f645146493a2a9f8a0c5c2fb1660a3a63c7d5e1acaf9a4f4e01bd547a783be4cc5ac2a06c913f524ecb04d5abdffc00d033cb3b2ed9b563b8c0a9619fc432b43be0df418b52c4f688cd6e9575854cf4d4563ab3f8c31bb0ec442fa4b5bdb85ebb5aaaa837e677a0ddecf928c5ae17b9e7eaec38c10b1b7bade02503d21026e7cbe1dc85e8f0db90be873cd8c43830cea05d74d5d6db540ea632fa371d95abdcf6aad92c4cc26d88272e32686d37fadaf0ec8e90c8362b2adf0513b2a3bd1a1069be77774358b26407e66704155b1636e64661cd0017d1ca53cab3bd4264733750df7923b0863a56361e29224565135b5811dbb4d08a5d2868db75fbffa66b49fcde01903a2a5ca6470705ab625dc8b60ce8a53e767aea5969881b83e2bd25680df17ca4b2c2c3a9b5855c33e51ec9b9d8022ecc30d72806d2a336492c0c813b8167f19d8038967ccabdeca1e0186687ed5648c3169fc6d48af6184840f87a29850aba6f") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) - assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceMultiFramePayloads) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceVariableSizePayloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) - assert(packets(0).hmac == ByteVector32(hex"c9c4b58b8b161c64453a35906f1f559aa3dca4823a38697e38ffcb207411ac9f")) - assert(packets(1).hmac == ByteVector32(hex"115671e3866f025551ddab0c3a91437daf86a9169d0eed1c447705abed2cfe13")) - assert(packets(2).hmac == ByteVector32(hex"fa8ed50911736b2a4c13c832f9660993440bbc909de4c1c7d3b404820f79d21f")) - assert(packets(3).hmac == ByteVector32(hex"b8e37ddbe2ffdcf8b77c9567331b28b8e77ab5d4fe8caca1091809f36d01f575")) + assert(packets(0).hmac == ByteVector32(hex"63063aff9cb3b0fb179c977189253c67c1646966c66416b7ff807001a2f61975")) + assert(packets(1).hmac == ByteVector32(hex"ecddbb20ee5071ecf0c6846113cc6ae8dd166a71b44378290d660a5ece06db7e")) + assert(packets(2).hmac == ByteVector32(hex"2cadd35e288d582f0171bb076dbf8629a7cd3e6af9c5739cc4953605c2dee086")) + assert(packets(3).hmac == ByteVector32(hex"19013747701b476914c2831072301cab2ac69998f12ed2c24b73cc8a7af61681")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } - test("create multi-frame packet filling onion") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, multiFramePayloadsFull, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619c4f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2ce49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f590c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce4fcdbbfcca2995dccb71d6ccf759220bbbf52b19b0135308490c476816a316a3dbfcacbe96df37b4a7cb6b3b88dba165efe681b8fcb5c72febc91b133c6c1a15a5dd2b7ce8a23cf43225931b5967094f4f1c9c88eb8e864a5ac0f8276bc7036f2b36784b2ab4d0435336a8445b64cc7af1b703c8c94b1a83411d0234b1fd3fff0b0dc7992c9f5ab8fc8395d3cd70c5dd6ef9b2408d1b8608f6fff191c2437e8694296e3224b92e3b7a872877b31e5d2dd70ffaa9e8fd70ea552d85fb27289f91e1772794156c87b81bb2c9a470d364945fa1c96816dbc0f6440d3071a4bfc8596012ef384f20776d7384ef196db73b3aac192bf890666f0d5551a4551421558d87bf90218159b96bd0c4aa2acb6755e5db85e5fd41810434f3dd6680be8636190e249f3b97b1f65365922bbe2339bf4d815dc7cb0b588f8d0ef1d5017cff1501135d78d0996e5c122ce52cd6b49b4aba6222f4ba93c23643be48ddacf552221561720a8dc6b71ea07c65e869179c71ebb0ff002ed8a53ce05e0e6024addbbf3118f34319d75e4850d5e17fbce98ed8458b9e5ab996e00ec697d3752f5a1d6a3ad691d8015f78b10afb9b62bd92e0dbdfb2ed7783771d61b86d8bcb8e167e74919b0f61fd1c63ed6153abeb3d90b8bb6d5932d559dda555c9e0482cf3c953eb69f4c1a58e4dcd31361958666c2fdbe30bc281684c7421b1d23635c7e9176651c2d91db9a07f50dcaa914f1f50e153dafd00381bc868bfbdf84401ad5a22516a91653808d8942190441e600c60ee23358cf56f2e5007314df2f4700c24b27dae11cb45817459a566f582ab3d899d48fa61e8c76526c84994be0281bb3f20494b9bd8973e4169817250803d23a3fdd97f0a3c3806eb17177d4f07ec4c2d30fb5318b9feb9b055c8571fb7d0dd1782b9eb1b0155564b7fa21998c9c696b5979c81041818c2e774395141ef2283aea4353397d90dd37923aefcadda3a66785cdb335ace25d53a250ab57ff7cc6c43034d8afef7d1fdbb07ca85639586eb939c4c96ee38ca2c8dd5c14e03ddef13b25cdd2af2486f02f73de04571b402f6ead23cf3b27efaa0f3b88df280c77acc16ddd5359b06e0e2aef47b98f01a86df40b4974db1c36466ca7208d56e9088709dc499b930ec497f505060501391c1c396121efc0fa152ba6967b551fe13a0ae135a7bf8e347c52858d66bc1a07c379ec7a34654c3afe8443e30ea344c27af96a5d1ff1b94d9744fa1f273320f38c4a9bfffa81e05ae0dbbd2a232e57f4fca93fe588ebcd39bd334a4d26b7223dfeb10c0f8b7d42d2dfbe5264073752ab8e63f5fe65d627bfff286c4bf429cad13a17bc21412038ccddfdb956dfd1bef80d2adc04700bf50adb875f00c4e133e6c6bc88ae152bc2eb59450ce6f2f27522ba6cfa43847495019b0282f6ffdb3efbbe10a70f89ea311652670932218d70a9335163f25c7c45") + test("create packet with variable-size payloads filling the onion") { + val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, variableSizePayloadsFull, associatedData) + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866196ef84350c2a76fc232b5d46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6101810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bf74b2ce49922898e9353fa268086c00ae8b7f718405b72ad3829dbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf776b75ebb389bf84d0bfbf58590e510e034572a01e409c30939dc3c0b7d6a68a371019f5378bf6133fe0de00dc6f90d98ce1cbe3f165182ca37d6208da0f6bafd75ff41903ab352a1f47586eae3c6c8e437d4308766f71052b46ba2efbd87c0a781e8b3f456300fc7efbefc78ab515338666aed2070e674143c30b520b9cc1782ba8b46454db0d4ce72589cfc2eafb2db452ec98573ad08496483741de5376bfc7357fc6ea629e31236ba6ba7703014959129141a1719788ec83884f2e9151a680e2a96d2bcc67a8a2935aa11acee1f9d04812045b4ae5491220313756b5b9a0a6f867f2a95be1fab14870f04eeab694d9594620632b14ec4b424b495914f3dc587f75cd4582c113bb61e34a0fa7f79f97463be4e3c6fb99516889ed020acee419bb173d38e5ba18a00065e11fd733cf9ae46505dbb4ef70ef2f502601f4f6ee1fdb9d17435e15080e962f24760843f35bac1ac079b694ff7c347c1ed6a87f02b0758fbf00917652641cb68f584fd830b6c738e03b424ea0bc753d24306d7b691693d3286706fee7d57a939e28d77b3da47efc072436a3fd7f9c40515af8c4903764301e62b57153a5ca03ff5bb49c7dc8d3b2858100fb4aa5df7a94a271b73a76129445a3ea180d84d19029c003c164db926ed6983e5219028721a294f145e3fcc20915b8a2147efc896ee5d314e01874ea9e7bc1f386ba6b8f262942aa0193a537ffc91b1ccc9171a3c2016ecf387a3cd8b79df80a8e9412e707cb9c761a0809a84c606a779567f9f0edf685b38c98877e90d02aedd096ed841e50abf2114ce01efbff04788fb280f870eca20c7ec353d5c381903e7d08fc57695fd79c27d43e7bd603a876068d3f1c7f45af99003e5eec7e8d8c91e395320f1fc421ef3552ea033129429383304b760c8f93de342417c3223c2112a623c3514480cdfae8ec15a99abfca71b03a8396f19edc3d5000bcfb77b5544813476b1b521345f4da396db09e783870b97bc2034bd11611db30ed2514438b046f1eb7093eceddfb1e73880786cd7b540a3896eaadd0a0692e4b19439815b5f2ec855ec8ececce889442a64037e9564521fce926613b5d3074246c5a34a296ad1a18ef556d73fcd6c85ea3fdfb03b4e8e4bb0e35997fc35544d3c2a00e5e1f45dc925607d952c6a89721bd0b6f6aec03314d667166a5b8b18471403be7018b2479aaef6c7c6c554a50a98b717dff06d50be39fb36dc03e678e0a52fc615be46b223e3bee83fa0c7c47a1f29fb94f1e9eebf6c9ecf8fc79ae847df2effb60d07aba301fc536546ec4899eedb4fec9a9bed79e3a83c4b32757745778e977e485c67c0f12bbc82c0b3bb0f4df0bd13d046fed4446f54cd85bfce55ef781a80e5f63d289d08de001237928c2a4e0c8694d0c1e68cc23f2409f30009019085e831a928e7bc5b00a1f29d25482f7fd0b6dad30e6ef8edc68ddf7db404ea7d11540fc2cee74863d64af4c945457e04b7bea0a5fb8636edadb1e1d6f2630d61062b781c1821f46eddadf269ea1fada829547590081b16bc116e074cae0224a375f2d9ce16e836687c89cd285e3b40f1e59ce2caa3d1d8cf37ee4d5e3abe7ef0afd6ffeb4fd6905677b950894863c828ab8d93519566f69fa3c2129da763bf58d9c4d2837d4d9e13821258f7e7098b34218b846b5ee59284dc6791344738aac3d9a9014b764104cd606ef2c37a03fc7e") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) - assert(Seq(payload0, payload1, payload2, payload3, payload4) == multiFramePayloadsFull) + assert(Seq(payload0, payload1, payload2, payload3, payload4) == variableSizePayloadsFull) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) - assert(packets(0).hmac == ByteVector32(hex"d2481b1b6a9ab718e8d9103b4cff08fd8d63fce39f46ffbbc385504755d32e3b")) - assert(packets(1).hmac == ByteVector32(hex"92ee624aadb2a92e0e4645b1e7c63587f593c97580e6d865164b93e26a4ea4a1")) - assert(packets(2).hmac == ByteVector32(hex"3282b10831d5591893c491d8bb25a030707d7e6ed97df32e3dc0b8608f3e06bb")) - assert(packets(3).hmac == ByteVector32(hex"34fa139c3dcbb0cad1f207d9d240d774945384c1feac82eb064810c3cb098d3b")) + assert(packets(0).hmac == ByteVector32(hex"3ed8abed86a03f4619bffc2c5f0f792b44bd439e888e1d02b9ff7a44b1fa34e8")) + assert(packets(1).hmac == ByteVector32(hex"36f8d5a549a2ef20c3f47487af2594f5bbd4f79717f8e5f47662abd63a38d6a1")) + assert(packets(2).hmac == ByteVector32(hex"7b2a5997e7e615f67de503e71b73c8507bee506ce8054e1846950b436b6959ce")) + assert(packets(3).hmac == ByteVector32(hex"48675a9343e6514a8666f7acfaa623e5cab6dfbd3f0b5954d69e1cb5d27e0025")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } - test("create 20-frames 1-hop packet") { - val Sphinx.PacketAndSecrets(onion, _) = Sphinx.makePacket(sessionKey, publicKeys.take(1), maxFramesOneHopPayload, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866197cb3011280e52d8070f7a62f005cd4570558fba2feca39adba8b1d99c5cd39783303077c1dcf60f4775a52ec165ea6dbe2d908dfddc293c61ecb228a1d8493ea3990096ea6db60cbccd111b864c2c48248aaf5b55a021930ef7a4e99f1cec7a26842003cf6ffffeabe96690632c8dcbfa6deb511f51cf97ad9ba0f49b9b7c7d2a752a276156843a64b814b7ba0a6e2d2ae8ade755a62ed16a22c08970b1f0c1a33c1c78ee7030710a23abc9d6a0d9e6e87823289e05828cf0935101f998bebbeebf0b0d8ee15c1acc9ef954bba5e31bd22172eb0ac2078b231f7ea05dbee838804f526b509d544ce3a648e473b3b08164f3817a6dcee64e4d14102516cd628da7002f28f59316c721e5200b7d286f02d564d8388f1ac2d204b1aa2d5ddc7af9d1a2e034ea66cd1e6958a41a8961d863eb67b2f6a0470730f3adc318d7d110364847da00644dfdcc83e0ddaa5a772be1acc26ccb6a403740bc3f775370097a5e6e8e92f77b49d3f78df3ee5576377dbac408699a80f85ca22bcfc5f6b67de26f7d97171bd98a9f8bd0c3973803875d3b4fdc6c81a9d3e2bf9695a1090ef7739db5bbd98a732f03f854bb9e2c5657f780faba4e4fc2499263df4ea93e741f1b6c3fcb9d43618fd1e86e4f9e2b5c2978537d9dc7278682955a947f4a422b68636d7dff3c1a388a222a321d065ac498841d5c861cf86776c9526983f0d7c97713d2f41349db56457fcaca8ecad3be9d1a6fefd5a8fc666a9692895b17f690dbf1ce2c130a4af5fda03109d55a20e2d31164bca959fd5dc404666e32180335758671b24fc3c9cb66325fab88ee47126d57bf8fb3730f6c727ce9c02dc1f084d277d581b53565476a190f445d95b763fb42dda62371d97554b6fec20b965c949ed8e3894c0b5d05978071c4e212bdac53ba02d461af159bd95a6a7fdf15b06aa4d5249671181f454a519a3c16a9e014d245a971e9592d469fb54902c96f355862de0dc48536e40812262f108d4346ff46ab876d2c0cc8045db20efc32cb04ce2fee5cdf0f5b5ec9f0e05a137a400d69313bd414a7572712fa826d10241e035728e5e3fae120b9ff8c3ca507f93d3e8337b4f01e6ddf5d0e19832c0ade48eaf31b24c8ea85a561dfec6d0110bb5f7107308faaf0e0b5c7b68af795d41a93b050e6b0900a0a9e310cfe02612e81b6ef83ce06cd41e169db2bf2d9d94f769d84d1d346c65b94c2e7e9164e3885e1b319cd9aac301dd326a72edbcf2827683c88a9780b3778b26fd2b9c1294abd99ccaab4163f8c67fd5118aba7424521f67b8b95853a860946cb4b3be76880c22811f2b3578520fd80630ce4a80492156703143e08642e55c566683646a8ddd0825db489cc86041f2affb2e22c0962579314b8f1086f16940d75ff0b42e2e30ff2e3cee6912fa7a72851e9d8194ab80f7d0861c97b81d2c8298c733493c5720ece121a0c427c115d39e150fd832f0a7a0fbd578f7d29619d69006be0014d02e1817f25e92eee05da7bb8b452b5df264928725caf27ee1b2cff2e9fbbb434bcf9c6c8642772afe7ee03637bdc4e58948e99ba7512e1fa2269325361a0967626652d9f2624699e3b8fddcc0359c4b3848f29ecd2c64be96e0cdfb903e96e0b4f37c652c901be1dc5e3c22b1816e1e8686bb299bc3af891c6e34e7558f80c7e86ee73393a19621ae952308a0659d55ed360772199584238d3ff827c9489eb12942c6f5081de64aa030b54135488777517632d3508796a6bcda3c4e65f4320364d4b76971661e60d5379817456b938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3cc71285778b5a49c2c741b09b61c113eed6dd067d430d9c64f8918f0bc7ee2c0") + test("create packet with single variable-size payload filling the onion") { + val Sphinx.PacketAndSecrets(onion, _) = Sphinx.makePacket(sessionKey, publicKeys.take(1), variableSizeOneHopPayload, associatedData) + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661918004735c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2cc49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f580c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce42e93a4d7c803ead45fc47cf4396d284632314d789e73cf3f534126c63fe244069d9e8a7c4f98e7e530fc588e648ef4e641364981b5377542d5e7a4aaab6d35f6df7d3a9d7ca715213599ee02c4dbea4dc78860febe1d29259c64b59b3333ffdaebbaff4e7b31c27a3791f6bf848a58df7c69bb2b1852d2ad357b9919ffdae570b27dc709fba087273d3a4de9e6a6be66db647fb6a8d1a503b3f481befb96745abf5cc4a6bba0f780d5c7759b9e303a2a6b17eb05b6e660f4c474959db183e1cae060e1639227ee0bca03978a238dc4352ed764da7d4f3ed5337f6d0376dff72615beeeeaaeef79ab93e4bcbf18cd8424eb2b6ad7f33d2b4ffd5ea08372e6ed1d984152df17e04c6f73540988d7dd979e020424a163c271151a255966be7edef42167b8facca633649739bab97572b485658cde409e5d4a0f653f1a5911141634e3d2b6079b19347df66f9820755fd517092dae62fb278b0bafcc7ad682f7921b3a455e0c6369988779e26f0458b31bffd7e4e5bfb31944e80f100b2553c3b616e75be18328dc430f6618d55cd7d0962bb916d26ed4b117c46fa29e0a112c02c36020b34a96762db628fa3490828ec2079962ad816ef20ea0bca78fb2b7f7aedd4c47e375e64294d151ff03083730336dea64934003a27730cc1c7dec5049ddba8188123dd191aa71390d43a49fb792a3da7082efa6cced73f00eccea18145fbc84925349f7b552314ab8ed4c491e392aed3b1f03eb79474c294b42e2eba1528da26450aa592cba7ea22e965c54dff0fd6fdfd6b52b9a0f5f762e27fb0e6c3cd326a1ca1c5973de9be881439f702830affeb0c034c18ac8d5c2f135c964bf69de50d6e99bde88e90321ba843d9753c8f83666105d25fafb1a11ea22d62ef6f1fc34ca4e60c35d69773a104d9a44728c08c20b6314327301a2c400a71e1424c12628cf9f4a67990ade8a2203b0edb96c6082d4673b7309cd52c4b32b02951db2f66c6c72bd6c7eac2b50b83830c75cdfc3d6e9c2b592c45ed5fa5f6ec0da85710b7e1562aea363e28665835791dc574d9a70b2e5e2b9973ab590d45b94d244fc4256926c5a55b01cd0aca21fe5f9c907691fb026d0c56788b03ca3f08db0abb9f901098dde2ec4003568bc3ca27475ff86a7cb0aabd9e5136c5de064d16774584b252024109bb02004dba1fabf9e8277de097a0ab0dc8f6e26fcd4a28fb9d27cd4a2f6b13e276ed259a39e1c7e60f3c32c5cc4c4f96bd981edcb5e2c76a517cdc285aa2ca571d1e3d463ecd7614ae227df17af7445305bd7c661cf7dba658b0adcf36b0084b74a5fa408e272f703770ac5351334709112c5d4e4fe987e0c27b670412696f52b33245c229775da550729938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3da3bac7ec6b67f68f2838b6dc687c42dd3b4be7d8e44ded20177e8e3cc9c7014") val Success(Sphinx.ParsedPacket(payload, nextPacket, _)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) - assert(payload == maxFramesOneHopPayload.head) + assert(payload == variableSizeOneHopPayload.head) assert(nextPacket.hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } + test("create packet with invalid payload") { + // In this test vector, the payload length (encoded as a varint in the first bytes) isn't equal to the actual + // payload length. + val incorrectVarint = Seq( + hex"fd2a0101234567", + hex"000000000000000000000000000000000000000000000000000000000000000000" + ) + + assertThrows[IllegalArgumentException](Sphinx.makePacket(sessionKey, publicKeys.take(2), incorrectVarint, associatedData)) + } + test("last node replies with an error message") { - for (payloads <- Seq(referenceSingleFramePayloads, referenceMultiFramePayloads, multiFramePayloadsFull)) { + for (payloads <- Seq(referenceFixedSizePayloads, referenceVariableSizePayloads, variableSizePayloadsFull)) { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 // origin build the onion packet @@ -205,7 +215,7 @@ class SphinxSpec extends FunSuite { } test("intermediate node replies with an error message") { - for (payloads <- Seq(referenceSingleFramePayloads, referenceMultiFramePayloads, multiFramePayloadsFull)) { + for (payloads <- Seq(referenceFixedSizePayloads, referenceVariableSizePayloads, variableSizePayloadsFull)) { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 // origin build the onion packet @@ -253,9 +263,9 @@ object SphinxSpec { val sessionKey: PrivateKey = PrivateKey(hex"4141414141414141414141414141414141414141414141414141414141414141") - // This test vector uses only single-frame payloads. + // This test vector uses payloads with a fixed size. // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 - val referenceSingleFramePayloads = Seq( + val referenceFixedSizePayloads = Seq( hex"000000000000000000000000000000000000000000000000000000000000000000", hex"000101010101010101000000000000000100000001000000000000000000000000", hex"000202020202020202000000000000000200000002000000000000000000000000", @@ -263,33 +273,32 @@ object SphinxSpec { hex"000404040404040404000000000000000400000004000000000000000000000000" ) - // This test vector uses a multi-frame payload intertwined with single-frame payloads. - // The number of frames (-1) is encoded in the first 5 bits of the first byte. - // origin -> node #0 (1 frame) -> node #1 (1 frame) -> node #2 (N frames) -> node #3 (1 frame) -> node #4 (1 frame) - val referenceMultiFramePayloads = Seq( - hex"000000000000000000000000000000000000000000000000000000000000000000", + // TODO: add official test vector once available + + // This test vector uses variable-size payloads intertwined with fixed-size payloads. + // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + val referenceVariableSizePayloads = Seq( + hex"0c100000001000000000000000", hex"000101010101010101000000000000000100000001000000000000000000000000", - hex"20000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000000000000000000000000000000000000000000000000000000000000000000000000", - hex"000303030303030303000000000000000300000003000000000000000000000000", + hex"200101010101010101000000000000000100000001000000000000000000000000", + hex"fd000132000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000", hex"000404040404040404000000000000000400000004000000000000000000000000" ) - // This test vector uses multi-frame payloads. - // It fills all the frames available in an onion packet. - // The number of frames (-1) is encoded in the first 5 bits of the first byte. - // origin -> node #0 (5 frames) -> node #1 (5 frames) -> node #2 (4 frames) -> node #3 (3 frames) -> node #4 (3 frames) - val multiFramePayloadsFull = Seq( - hex"2100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - hex"2111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111", - hex"192222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222", - hex"11333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333", - hex"11444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444" + // This test vector uses variable-sized payloads and fills the whole onion packet. + // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 + val variableSizePayloadsFull = Seq( + hex"8b09000000000000000030000000000000000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000250000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000", + hex"fd2a0108000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000", + hex"620800000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hex"fc120000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000", + hex"fd58012200000000000000000000000000000000000000000022000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000" ) - // This test vector uses a payload containing a single hop filling all the available frames. - // origin -> recipient (20 frames) - val maxFramesOneHopPayload = Seq( - hex"9942424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242424242" + // This test vector uses a single variable-sized payload filling the whole onion payload. + // origin -> recipient + val variableSizeOneHopPayload = Seq( + hex"fdf1046500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) val associatedData = ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index 8eab3c0e7f..b55e30c85e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -42,7 +42,7 @@ class ChannelSelectionSpec extends FunSuite { val relayPayload = RelayPayload( add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, ByteVector.empty), payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), - nextPacket = Sphinx.LAST_PACKET // just a placeholder + nextPacket = Sphinx.EMPTY_PACKET // just a placeholder ) val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) @@ -75,7 +75,7 @@ class ChannelSelectionSpec extends FunSuite { val relayPayload = RelayPayload( add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, ByteVector.empty), payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), - nextPacket = Sphinx.LAST_PACKET // just a placeholder + nextPacket = Sphinx.EMPTY_PACKET // just a placeholder ) val (a, b) = (randomKey.publicKey, randomKey.publicKey) From b4cbed577756df2be60c24c0e6b37603271e07e6 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Fri, 7 Jun 2019 10:14:41 +0200 Subject: [PATCH 09/27] SphinxSpec: add official variable-length test vector --- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 985e1d5c19..21cd41d392 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -77,12 +77,12 @@ class SphinxSpec extends FunSuite { } /* - filler = 0x72a32c7cf17217e3c6aa86985cf2ad07c9064676619b2219ad8b85e9dadafa929dc544e735b558940a19999baf779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3acb0dab321b834bb87aa5aa4c68e22218a540a4d94b921a53b4167f021c562d0e7da6d2b2b1783cc65596804720805c5bcd6c4ff10495fb7ed519b383258804a0e40173dfab4dedf52b30d816ad2dd732dd115eca4e7ae595ad3e5f9d3b6f645799bc1740a033cfc44aba80b74c9f3d823ee9844b6a31839bc0298f0253b19911441d6085fca875132b2cc9dcc4d0c23c29c6279ce4012200456ccd4866c07bcd803ba51435ac1b2623171688676f0a65bcd756bbd5e67e02c0d3eaa7287d70a3275260703e159665aaeddd8aa4aabbf3a90640b9d7b65320f4ea0aa8adf94720c9ebe + filler = 0xb77d99c935d3f32469844f7e09340a91ded147557bdd0456c369f7e449587c0f5666faab58040146db49024db88553729bce12b860391c29c1779f022ae48a9cb314ca35d73fc91addc92632bcf7ba6fd9f38e6fd30fabcedbd5407b6648073c38331ee7ab0332f41f550c180e1601f8c25809ed75b3a1e78635a2ef1b828e92c9658e76e49f995d72cf9781eec0c838901d0bdde3ac21c13b4979ac9e738a1c4d0b9741d58e777ad1aed01263ad1390d36a18a6b92f4f799dcf75edbb43b7515e8d72cb4f827a9af0e7b9338d07b1a24e0305b5535f5b851b1144bad6238b9d9482b5ba6413f1aafac3cdde5067966ed8b78f7c1c5f916a05f874d5f17a2b7d0ae75d66a5f1bb6ff932570dc5a0cf3ce04eb5d26bc55c2057af1f8326e20a7d6f0ae644f09d00fac80de60f20aceee85be41a074d3e1dda017db79d0070b99f54736396f206ee3777abd4c00a4bb95c871750409261e3b01e59a3793a9c20159aae4988c68397a1443be6370fd9614e46108291e615691729faea58537209fa668a172d066d0efff9bc77c2bd34bd77870ad79effd80140990e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a */ test("generate filler with variable-size payloads") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceVariableSizePayloads.dropRight(1)) - assert(filler == hex"72a32c7cf17217e3c6aa86985cf2ad07c9064676619b2219ad8b85e9dadafa929dc544e735b558940a19999baf779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3acb0dab321b834bb87aa5aa4c68e22218a540a4d94b921a53b4167f021c562d0e7da6d2b2b1783cc65596804720805c5bcd6c4ff10495fb7ed519b383258804a0e40173dfab4dedf52b30d816ad2dd732dd115eca4e7ae595ad3e5f9d3b6f645799bc1740a033cfc44aba80b74c9f3d823ee9844b6a31839bc0298f0253b19911441d6085fca875132b2cc9dcc4d0c23c29c6279ce4012200456ccd4866c07bcd803ba51435ac1b2623171688676f0a65bcd756bbd5e67e02c0d3eaa7287d70a3275260703e159665aaeddd8aa4aabbf3a90640b9d7b65320f4ea0aa8adf94720c9ebe") + assert(filler == hex"b77d99c935d3f32469844f7e09340a91ded147557bdd0456c369f7e449587c0f5666faab58040146db49024db88553729bce12b860391c29c1779f022ae48a9cb314ca35d73fc91addc92632bcf7ba6fd9f38e6fd30fabcedbd5407b6648073c38331ee7ab0332f41f550c180e1601f8c25809ed75b3a1e78635a2ef1b828e92c9658e76e49f995d72cf9781eec0c838901d0bdde3ac21c13b4979ac9e738a1c4d0b9741d58e777ad1aed01263ad1390d36a18a6b92f4f799dcf75edbb43b7515e8d72cb4f827a9af0e7b9338d07b1a24e0305b5535f5b851b1144bad6238b9d9482b5ba6413f1aafac3cdde5067966ed8b78f7c1c5f916a05f874d5f17a2b7d0ae75d66a5f1bb6ff932570dc5a0cf3ce04eb5d26bc55c2057af1f8326e20a7d6f0ae644f09d00fac80de60f20aceee85be41a074d3e1dda017db79d0070b99f54736396f206ee3777abd4c00a4bb95c871750409261e3b01e59a3793a9c20159aae4988c68397a1443be6370fd9614e46108291e615691729faea58537209fa668a172d066d0efff9bc77c2bd34bd77870ad79effd80140990e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a") } test("create packet with fixed-size payloads (reference test vector)") { @@ -108,7 +108,7 @@ class SphinxSpec extends FunSuite { test("create packet with variable-size payloads (reference test vector)") { val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceVariableSizePayloads, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e9e14350c2b76fc232b5e46d427d902fb8860a50479fe77889407ae7e04e1f5317872128e872a2c634bae6b7211fa0c825a9311120dc8a7e7b71453bf6699eeb64bc313a49e1458d06efee7f9b13d481f32fcc6c1aaa7a7a8f1aa5787143084f216612615db5cae80a14b79a92bd90b8ac1f23279d5d1568db1e1e7f7ffb6408874c9642da4b981e3bd64f2c59ef786868a11ffdf7690318d0a17c6542593e1a79971a39771a0f79f17103470d7636d1745da48dcaf0172ad03c5918da60c27b9b53e3509af9fe7a4273c4e52252a7489fb4f6e3ae609d9454d71bae519d07a7edf72ac2a9116dac2382c1bb6eee7c367e74667abf5998cd0d9802ea69ed85e608c7853c78df622c90bb8ed18579c4a27d7ff5e3ee6954d62a2f60b0f3d3f8db0f01ea89ac2a13afd6becacfbc22e907f52221789e1ddab1f654c7f1b3bfca924701a0d58c6ebba6a16f97455e180fac0f9ebae19ffbd8693e80a7b3a4c1bbbac0b0289d49220f6f0668cc0bf2fa736df181e85c122129e24bf273a6a1583a6b3963332b1abd341025e8b2ddaddd457a8486bcd54cc9891a31f3aa637bc4b5ad99e5f3bcdfd14d7028ad1d467b04211906a7e8502046599a40a971e0317fc9bdad9f3b5d265f60741b3692bd410e274d67eea83a0c3d3d0050fb9610473f190c1580911f4c5de514486651f569685a0547035796616d0c1a4843c62b9d87844549c6756b5bcaa16aabe6fa75191a6fe33971059b98849e2208ea5c7fcf198377eaaa4731b7c596c2665e41e75ec2b8bb0a484e3df6591aa59dd2754b7c4e50a540187b44e8405cc4f044638988f1cb3b4d5a137c539e6cd6a1be0b65bdaa68657565e8e6ab75a7290efa65fed0a9231933b93841141d6779fda4e18bacc6c0dc71fd59ea99d7245d97b095aa2d77526e27dd8966dbb48feeec1874cff0f4d812d8b78c8b5bea0a8398cbdad501022a43045944aa38a2cae34bb455a0a8e05c217ac547df8f5eed06c13cd4aba07e8750fe15ade0f82591c57360e5c95bd0a373806247f285c03b599c6900b9cb50c3b3cb9dae51145eef0807f28034c327b0aa515205064302b9930df5da66f2cc58e6ed9a6cd8687d8e95258c9fd16492ddd70faa4a940e55e3f592e54a1540ae0888e5a9b85089fab963a1b2779c4ac96d6bfc70b4c53b237622eae1d50d1979436bad6b045d4505ca6fad5fdc97db3e9dbf0591d7a2d01902816d733877d7f695764297170589cfc6afbefd63abd42ea90ffe0ce84c44e549d4abd8d3f56bf1e4ea718a1b08f0e1f0b5430d545b9a2aa621f55d071559f99b34bca131013310a5124e14848636d58cf425b680e849ceebdb9bdcdbac1785e0d7aefcecdf2dccdade0f645146493a2a9f8a0c5c2fb1660a3a63c7d5e1acaf9a4f4e01bd547a783be4cc5ac2a06c913f524ecb04d5abdffc00d033cb3b2ed9b563b8c0a9619fc432b43be0df418b52c4f688cd6e9575854cf4d4563ab3f8c31bb0ec442fa4b5bdb85ebb5aaaa837e677a0ddecf928c5ae17b9e7eaec38c10b1b7bade02503d21026e7cbe1dc85e8f0db90be873cd8c43830cea05d74d5d6db540ea632fa371d95abdcf6aad92c4cc26d88272e32686d37fadaf0ec8e90c8362b2adf0513b2a3bd1a1069be77774358b26407e66704155b1636e64661cd0017d1ca53cab3bd4264733750df7923b0863a56361e29224565135b5811dbb4d08a5d2868db75fbffa66b49fcde01903a2a5ca6470705ab625dc8b60ce8a53e767aea5969881b83e2bd25680df17ca4b2c2c3a9b5855c33e51ec9b9d8022ecc30d72806d2a336492c0c813b8167f19d8038967ccabdeca1e0186687ed5648c3169fc6d48af6184840f87a29850aba6f") + assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7181924e4b6c645f2b6eecc821084ec9b16dfb9d2e7622d4c14db4fc5ecdfc07eac50f7d61ab590531cf08000178a333a347f8b4072e7d3ae44f4f309e150b49886d3f044cd6462be389c830784aba767682923c8683404aaf9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf050597c681185f336b1da63b0939aa2b7c50b21b5eb7b6ad66c81fab98a3cdf73f658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d7fca6a0e4ecc9f9de611a90da6e99c39551094c56e3196f282c5dffd9fc4b2fc12f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa337ef811d5345a9e16b60de1767b209645ba40bd1f9a5f75bc04feca9b27c5554be4fe83fac2cb83aa447a817bb85ae966c68b420063833fada375e2f515965e687a45699632902672c654d1d18d7bcbf55e8fa57f63f2da449f8e1e606e8722df081e5f193fc4179feb99ad22819afdeef211f7c54afdba92aeef0c00b7bc2b65a4813c01f907a8377585708f2d4c940a25328e585714c8ded0a9a4d7a6de1027c1cb7a0198cd3db68b58c0704dfd0cfbe624e9cd18cc0ae5d96697bb476708b9ee0403d211e64e0d5a7683a7a9a140c02f0ff1c6e67a302941b4052bdea8a63e70a3ad62c5b89c698f1fd3c7685cb49705096cad702d02d93bcb1c27a409f4c9bddec001205ca4a2740f19b50900be81c7e847f1a863deea8d35701f1355cad8db57b1d4eb2ab4e29587734785abfb46ddede71928213d7d089dfdeda052827f459f1688cc0935bd47e7bcec27427c8376dcce7e22699567c0d145f8a7db33f6758815f1f15f9f7a9760dec4f34ae095edda4c64e9735bdd029c4e32c2ee31ba47ec5e6bdb97813d52dbd15b4e0b7a2c7f790ae64104d99f38c127f0a093288fa34144adb16b8968d4fa7656fcec99de8503dd46d3b03620a71c7cd085364abd30dccf7fbda25a1cdc102600149c9af1c97aa0372cd2e1909f28ac5c686f432b310e79528c9b8b9e8f314c1e74621ce6308ad2278b81d460892e0d9dd38b7c76d58be6dfd10ae7583ee1e7ef5b3f6f78dc60af0950df1b00cc55b6d178ba2e476bea0eaeef49323b83f05804159e7aef4eed4cc60dd07be76f067dfd0bcfb0b806b69ba921336a20c43c832d0cab8fa3ddeb29e3bf07b0d98a112eb07802756235a49d44a8b82a950d84e95e01971f0e106ccb337f07384e21620e0ad39e16ed9edca123226cf55ac44f449eeb53e38a7f27d101806e4823e4efcc887414240ee6826c4a5cb1c6443ad36ebf905a435c1d9054e54173911b17b5b40f60b3d9fd5f12eac54ca1e20191f5f18544d5fd3d665e9bcef96fb44b76110aa64d9db4c86c9513cbdad546538e8aec521fbe83ceac5e74a15629f1ed0b870a1d0d1e5680b6d6100d1bdd9a2ba3be3772f71854cb12ae9b3afd87cda738dcd107fe56a15f1877450cd0e") val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) @@ -119,10 +119,10 @@ class SphinxSpec extends FunSuite { assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) - assert(packets(0).hmac == ByteVector32(hex"63063aff9cb3b0fb179c977189253c67c1646966c66416b7ff807001a2f61975")) - assert(packets(1).hmac == ByteVector32(hex"ecddbb20ee5071ecf0c6846113cc6ae8dd166a71b44378290d660a5ece06db7e")) - assert(packets(2).hmac == ByteVector32(hex"2cadd35e288d582f0171bb076dbf8629a7cd3e6af9c5739cc4953605c2dee086")) - assert(packets(3).hmac == ByteVector32(hex"19013747701b476914c2831072301cab2ac69998f12ed2c24b73cc8a7af61681")) + assert(packets(0).hmac == ByteVector32(hex"c0d77014e146e91e76fc667514aa5011f6b100b1f6f3509dc4d434010b2daf91")) + assert(packets(1).hmac == ByteVector32(hex"5c5ee40f24efa5e5332c1ffec3e6ba8c6e486c5b32baae839ee43a3935d650ca")) + assert(packets(2).hmac == ByteVector32(hex"4a630bdc56575e956627d7f191e731fabf110ef0044f8f64f4dcea79c2dcb995")) + assert(packets(3).hmac == ByteVector32(hex"a2a89cf333e198b68904ce59ddceb9f989ebfa1ad534fa74ee85e41ff303c3a0")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } @@ -273,15 +273,13 @@ object SphinxSpec { hex"000404040404040404000000000000000400000004000000000000000000000000" ) - // TODO: add official test vector once available - // This test vector uses variable-size payloads intertwined with fixed-size payloads. // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 val referenceVariableSizePayloads = Seq( - hex"0c100000001000000000000000", - hex"000101010101010101000000000000000100000001000000000000000000000000", - hex"200101010101010101000000000000000100000001000000000000000000000000", - hex"fd000132000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000320000000000000000000000000000000000000000000000000000000000000032000000000000000000000000000000000000000000000000000000000000003200000000000000000000000000000000000000000000000000000000000000", + hex"000000000000000000000000000000000000000000000000000000000000000000", + hex"140101010101010101000000000000000100000001", + hex"fd0001000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + hex"140303030303030303000000000000000300000003", hex"000404040404040404000000000000000400000004000000000000000000000000" ) From 9c42e5524d3ee2ac04a3a8fe1b8387ca0f2a100e Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Wed, 12 Jun 2019 09:33:30 +0200 Subject: [PATCH 10/27] features: rename variable-length onion flag --- eclair-core/src/main/scala/fr/acinq/eclair/Features.scala | 4 ++-- .../main/scala/fr/acinq/eclair/router/Announcements.scala | 2 +- .../src/test/scala/fr/acinq/eclair/FeaturesSpec.scala | 6 +++--- .../scala/fr/acinq/eclair/router/AnnouncementsSpec.scala | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index b2f7dc90e5..c45178be4c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -35,8 +35,8 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 - val OPTION_MULTI_FRAME_ONION_MANDATORY = 0 - val OPTION_MULTI_FRAME_ONION_OPTIONAL = 1 + val OPTION_VARIABLE_LENGTH_ONION_MANDATORY = 0 + val OPTION_VARIABLE_LENGTH_ONION_OPTIONAL = 1 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index e2f396902e..1dafa921a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -75,7 +75,7 @@ object Announcements { def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], timestamp: Long = Platform.currentTime.milliseconds.toSeconds): NodeAnnouncement = { require(alias.length <= 32) - val features = ByteVector.fromByte((1 << Features.OPTION_MULTI_FRAME_ONION_OPTIONAL).byteValue) + val features = ByteVector.fromByte((1 << Features.OPTION_VARIABLE_LENGTH_ONION_OPTIONAL).byteValue) val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, nodeAddresses, unknownFields = ByteVector.empty) val sig = Crypto.sign(witness, nodeSecret) NodeAnnouncement( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 79c2a09d75..a413c70f84 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -44,15 +44,15 @@ class FeaturesSpec extends FunSuite { } test("'multi_frame_onion' feature") { - assert(hasFeature(hex"01", Features.OPTION_MULTI_FRAME_ONION_MANDATORY)) - assert(hasFeature(hex"02", Features.OPTION_MULTI_FRAME_ONION_OPTIONAL)) + assert(hasFeature(hex"01", Features.OPTION_VARIABLE_LENGTH_ONION_MANDATORY)) + assert(hasFeature(hex"02", Features.OPTION_VARIABLE_LENGTH_ONION_OPTIONAL)) } test("features compatibility") { assert(areSupported(Protocol.writeUInt64(1l << INITIAL_ROUTING_SYNC_BIT_OPTIONAL, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1l << OPTION_DATA_LOSS_PROTECT_OPTIONAL, ByteOrder.BIG_ENDIAN))) - assert(areSupported(Protocol.writeUInt64(1l << OPTION_MULTI_FRAME_ONION_OPTIONAL, ByteOrder.BIG_ENDIAN))) + assert(areSupported(Protocol.writeUInt64(1l << OPTION_VARIABLE_LENGTH_ONION_OPTIONAL, ByteOrder.BIG_ENDIAN))) assert(!areSupported(hex"14")) assert(!areSupported(hex"0141")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index 34922fa82c..f31a692dd1 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -49,7 +49,7 @@ class AnnouncementsSpec extends FunSuite { test("create valid signed node announcement") { val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses) - assert(Features.hasFeature(ann.features, Features.OPTION_MULTI_FRAME_ONION_OPTIONAL)) + assert(Features.hasFeature(ann.features, Features.OPTION_VARIABLE_LENGTH_ONION_OPTIONAL)) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } From 9a434d538e8b02a9549c0ecc8a822f5798bf706e Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Tue, 25 Jun 2019 10:32:04 +0200 Subject: [PATCH 11/27] FeaturesSpec: update test name --- eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index a413c70f84..81cb1f4600 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -43,7 +43,7 @@ class FeaturesSpec extends FunSuite { assert(areSupported(features) && hasFeature(features, OPTION_DATA_LOSS_PROTECT_OPTIONAL) && hasFeature(features, INITIAL_ROUTING_SYNC_BIT_OPTIONAL)) } - test("'multi_frame_onion' feature") { + test("'variable_length_onion' feature") { assert(hasFeature(hex"01", Features.OPTION_VARIABLE_LENGTH_ONION_MANDATORY)) assert(hasFeature(hex"02", Features.OPTION_VARIABLE_LENGTH_ONION_OPTIONAL)) } From 367d6eb39941ad7ecd38c6afcd398a32543b41a4 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Fri, 28 Jun 2019 15:59:20 +0200 Subject: [PATCH 12/27] Sphinx: refactor onion objects. Grouped functions that belonged together under objects. Abstracted the OnionPacket in a sealed trait. This paves the way for onions of different sizes (trampoline, hornet) and with different stopping conditions. --- .../fr/acinq/eclair/channel/Channel.scala | 2 +- .../acinq/eclair/channel/ChannelTypes.scala | 4 +- .../fr/acinq/eclair/channel/Commitments.scala | 6 +- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 511 ++++++++++-------- .../eclair/payment/PaymentLifecycle.scala | 23 +- .../fr/acinq/eclair/payment/Relayer.scala | 1 + .../eclair/wire/LightningMessageCodecs.scala | 2 +- .../states/StateTestsHelperMethods.scala | 3 +- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 86 +-- .../eclair/payment/ChannelSelectionSpec.scala | 4 +- .../eclair/payment/HtlcGenerationSpec.scala | 42 +- .../eclair/payment/PaymentLifecycleSpec.scala | 21 +- .../fr/acinq/eclair/payment/RelayerSpec.scala | 4 +- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 6 +- .../wire/LightningMessageCodecsSpec.scala | 2 +- 15 files changed, 376 insertions(+), 341 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 3199abcbe4..8672f39d1f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -2164,7 +2164,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // let's now fail all pending htlc for which we are the final payee val htlcsToFail = commitments1.remoteCommit.spec.htlcs.collect { - case DirectedHtlc(OUT, add) if Sphinx.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket) + case DirectedHtlc(OUT, add) if Sphinx.PaymentPacket.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket) .map(_.nextPacket.isLastPacket).getOrElse(true) => add // we also fail htlcs which onion we can't decode (message won't be precise) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index 65ef68a6ea..f5c00118d9 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.channel import java.util.UUID import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.{PublicKey} +import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.transactions.CommitmentSpec @@ -107,7 +107,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.EMPTY_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.PaymentPacket.EMPTY_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 579f8bac9f..640adcf48a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -235,11 +235,11 @@ object Commitments { throw UnknownHtlcId(commitments.channelId, cmd.id) case Some(htlc) => // we need the shared secret to build the error packet - Sphinx.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).map(_.sharedSecret) match { + Sphinx.PaymentPacket.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).map(_.sharedSecret) match { case Success(sharedSecret) => val reason = cmd.reason match { - case Left(forwarded) => Sphinx.forwardErrorPacket(forwarded, sharedSecret) - case Right(failure) => Sphinx.createErrorPacket(sharedSecret, failure) + case Left(forwarded) => Sphinx.ErrorPacket.forwardPacket(forwarded, sharedSecret) + case Right(failure) => Sphinx.ErrorPacket.createPacket(sharedSecret, failure) } val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason) val commitments1 = addLocalProposal(commitments, fail) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 011be97ed9..e3fbecadfb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -36,28 +36,12 @@ import scala.util.{Failure, Success, Try} * see /~https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md */ object Sphinx extends Logging { - val Version = 0.toByte - // length of a public key val PubKeyLength = 33 - // length of a MAC + // We use hmac which returns 32-bytes message authentication codes. val MacLength = 32 - // The 1.0 BOLT spec used 20 fixed-size 65-bytes frames inside the onion payload. - // The first byte of the frame (called `realm`) was set to 0x00, followed by 32 bytes of hop data and a 32-bytes mac. - // The 1.1 BOLT spec changed that format to use variable-length per-hop length. - val LegacyFrameSize = 65 - - // length of the obfuscated onion payload - val OnionPayloadLength = 1300 - - // onion packet length - val PacketLength = 1 + PubKeyLength + MacLength + OnionPayloadLength - - // packet construction starts with an empty packet (all zeroes except for the version byte) - val EMPTY_PACKET = Packet(Version, ByteVector.fill(PubKeyLength)(0), ByteVector32.Zeroes, ByteVector.fill(OnionPayloadLength)(0)) - def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { val mac = new HMac(new SHA256Digest()) mac.init(new KeyParameter(key.toArray)) @@ -86,7 +70,7 @@ object Sphinx extends Logging { def blind(pub: PublicKey, blindingFactors: Seq[ByteVector32]): PublicKey = blindingFactors.foldLeft(pub)(blind) /** - * computes the ephemeral public keys and shared secrets for all nodes on the route. + * Compute the ephemeral public keys and shared secrets for all nodes on the route. * * @param sessionKey this node's session key * @param publicKeys public keys of each node on the route @@ -112,14 +96,18 @@ object Sphinx extends Logging { } /** - * Return the amount of bytes that should be read to extract the per-hop payload (data and mac). + * Return the number of bytes that should be read to extract the per-hop payload (data and mac). */ def currentHopLength(payload: ByteVector): Int = { payload.head match { - case 0 => LegacyFrameSize + case 0 => + // The 1.0 BOLT spec used 20 fixed-size 65-bytes frames inside the onion payload. + // The first byte of the frame (called `realm`) was set to 0x00, followed by 32 bytes of hop data and a 32-bytes mac. + // The 1.1 BOLT spec changed that format to use variable-length per-hop payloads. + 65 case _ => // For non-legacy packets the first bytes are a varint encoding the length of the payload data (not including mac). - // Since the onion packet is only 1300-bytes long, the varint will either be 1 or 3 bytes long. + // Since messages are always smaller than 65535 bytes, the varint will either be 1 or 3 bytes long. val dataLength = CommonCodecs.varlong.decode(BitVector(payload.take(3))).require.value.toInt val varintLength = dataLength match { case i if i < 253 => 1 @@ -129,62 +117,38 @@ object Sphinx extends Logging { } } - def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], payloads: Seq[ByteVector]): ByteVector = { - require(sharedSecrets.length == payloads.length, "the number of secrets should equal the number of payloads") - - (sharedSecrets zip payloads).foldLeft(ByteVector.empty)((padding, secretAndPayload) => { - val (secret, payload) = secretAndPayload - val payloadLength = currentHopLength(payload) - require(payloadLength == payload.length + MacLength, s"invalid payload: length isn't correctly encoded: $payload") - val key = generateKey(keyType, secret) - val padding1 = padding ++ ByteVector.fill(payloadLength)(0) - val stream = generateStream(key, OnionPayloadLength + payloadLength).takeRight(padding1.length) - padding1.xor(stream) - }) - } - + /** + * Our Sphinx onion packets have the following format: + * - version (1 byte) + * - ephemeral public key (33 bytes) + * - encrypted onion payload (variable size) + * - hmac of the whole packet (32 bytes) + */ case class Packet(version: Int, publicKey: ByteVector, hmac: ByteVector32, onionPayload: ByteVector) { require(publicKey.length == PubKeyLength, s"onion packet public key length should be $PubKeyLength") require(hmac.length == MacLength, s"onion packet hmac length should be $MacLength") - require(onionPayload.length == OnionPayloadLength, s"onion packet payload length should be $OnionPayloadLength") - - def isLastPacket: Boolean = hmac == ByteVector32.Zeroes - def serialize: ByteVector = Packet.write(this) - } - - object Packet { - def read(in: InputStream): Packet = { - val version = in.read - val publicKey = new Array[Byte](PubKeyLength) - in.read(publicKey) - val onionPayload = new Array[Byte](OnionPayloadLength) - in.read(onionPayload) - val hmac = new Array[Byte](MacLength) - in.read(hmac) - Packet(version, ByteVector.view(publicKey), ByteVector32(ByteVector.view(hmac)), ByteVector.view(onionPayload)) - } + val length = 1 + PubKeyLength + onionPayload.length.toInt + MacLength - def read(in: ByteVector): Packet = read(new ByteArrayInputStream(in.toArray)) + def isLastPacket: Boolean = hmac == ByteVector32.Zeroes - def write(packet: Packet, out: OutputStream): OutputStream = { - out.write(packet.version) - out.write(packet.publicKey.toArray) - out.write(packet.onionPayload.toArray) - out.write(packet.hmac.toArray) + def write(out: OutputStream): OutputStream = { + out.write(version) + out.write(publicKey.toArray) + out.write(onionPayload.toArray) + out.write(hmac.toArray) out } - def write(packet: Packet): ByteVector = { - val out = new ByteArrayOutputStream(PacketLength) - write(packet, out) + def serialize: ByteVector = { + val out = new ByteArrayOutputStream(length) + write(out) ByteVector.view(out.toByteArray) } - - def isLastPacket(packet: ByteVector): Boolean = Packet.read(packet).hmac == ByteVector32.Zeroes } /** + * Decrypting the received onion packet yields a ParsedPacket. * * @param payload payload for this node * @param nextPacket packet for the next node @@ -193,211 +157,284 @@ object Sphinx extends Logging { case class ParsedPacket(payload: ByteVector, nextPacket: Packet, sharedSecret: ByteVector32) /** + * A Packet with all the associated shared secrets. * - * @param privateKey this node's private key - * @param associatedData associated data - * @param rawPacket packet received by this node - * @return a ParsedPacket(payload, packet, shared secret) object where: - * - payload is the per-hop payload for this node - * - packet is the next packet, to be forwarded using the info that is given in payload (channel id for now) - * - shared secret is the secret we share with the node that sent the packet. We need it to propagate failure - * messages upstream. + * @param packet onion packet + * @param sharedSecrets shared secrets (one per node in the route). Known (and needed) only if you're creating the + * packet. Empty if you're just forwarding the packet to the next node */ - def parsePacket(privateKey: PrivateKey, associatedData: ByteVector, rawPacket: ByteVector): Try[ParsedPacket] = Try { - require(rawPacket.length == PacketLength, s"onion packet length is ${rawPacket.length}, it should be $PacketLength") - val packet = Packet.read(rawPacket) - val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey) - val mu = generateKey("mu", sharedSecret) - val check = mac(mu, packet.onionPayload ++ associatedData) - require(check == packet.hmac, "invalid header mac") - - val rho = generateKey("rho", sharedSecret) - // Since we don't know the length of the hop payload (we will learn it once we decode the first byte), - // we have to pessimistically generate a long cipher stream. - val stream = generateStream(rho, 2 * OnionPayloadLength) - val bin = (packet.onionPayload ++ ByteVector.fill(OnionPayloadLength)(0)) xor stream - - val payloadLength = currentHopLength(bin) - val payload = bin.take(payloadLength - MacLength) - - val hmac = ByteVector32(bin.slice(payloadLength - MacLength, payloadLength)) - val nextOnionPayload = bin.drop(payloadLength).take(OnionPayloadLength) - val nextPubKey = blind(PublicKey(packet.publicKey), computeBlindingFactor(PublicKey(packet.publicKey), sharedSecret)) - - ParsedPacket(payload, Packet(Version, nextPubKey.value, hmac, nextOnionPayload), sharedSecret) - } + case class PacketAndSecrets(packet: Packet, sharedSecrets: Seq[(ByteVector32, PublicKey)]) - @tailrec - private def extractSharedSecrets(packet: ByteVector, privateKey: PrivateKey, associatedData: ByteVector32, acc: Seq[ByteVector32] = Nil): Try[Seq[ByteVector32]] = { - parsePacket(privateKey, associatedData, packet) match { - case Success(ParsedPacket(_, nextPacket, sharedSecret)) if nextPacket.isLastPacket => Success(acc :+ sharedSecret) - case Success(ParsedPacket(_, nextPacket, sharedSecret)) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret) - case Failure(t) => Failure(t) - } - } + sealed trait OnionPacket { - /** - * Compute the next packet from the current packet and node parameters. - * Packets are constructed in reverse order: - * - you first build the last packet - * - then you call makeNextPacket(...) until you've built the final onion packet that will be sent to the first node - * in the route - * - * @param payload payload for this packet - * @param associatedData associated data - * @param ephemeralPublicKey ephemeral key for this packet - * @param sharedSecret shared secret - * @param packet current packet (1 + all zeroes if this is the last packet) - * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet - * @return the next packet - */ - private def makeNextPacket(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: ByteVector, sharedSecret: ByteVector32, packet: Packet, onionPayloadFiller: ByteVector = ByteVector.empty): Packet = { - require(payload.length <= OnionPayloadLength - MacLength, s"packet payload cannot exceed ${OnionPayloadLength - MacLength} bytes") + // Packet version. Note that since this value is outside of the onion encrypted payload, intermediate hops may or + // may not use this value when forwarding the packet to the next node. + val Version = 0.toByte - val nextOnionPayload = { - val onionPayload1 = payload ++ packet.hmac ++ packet.onionPayload.dropRight(payload.length + MacLength) - val onionPayload2 = onionPayload1 xor generateStream(generateKey("rho", sharedSecret), OnionPayloadLength) - onionPayload2.dropRight(onionPayloadFiller.length) ++ onionPayloadFiller - } + // Length of the obfuscated onion payload. + val PayloadLength: Int - val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload ++ associatedData) - val nextPacket = Packet(Version, ephemeralPublicKey, nextHmac, nextOnionPayload) - nextPacket - } + // Length of the whole packet. + def PacketLength = 1 + PubKeyLength + PayloadLength + MacLength - /** - * - * @param packet onion packet - * @param sharedSecrets shared secrets (one per node in the route). Known (and needed) only if you're creating the - * packet. Empty if you're just forwarding the packet to the next node - */ - case class PacketAndSecrets(packet: Packet, sharedSecrets: Seq[(ByteVector32, PublicKey)]) + // Packet construction starts with an empty packet (all zeroes except for the version byte). + def EMPTY_PACKET = Packet(Version, ByteVector.fill(PubKeyLength)(0), ByteVector32.Zeroes, ByteVector.fill(PayloadLength)(0)) - /** - * A properly decoded error from a node in the route - * - * @param originNode public key of the node that generated the failure. - * @param failureMessage friendly error message. - */ - case class ErrorPacket(originNode: PublicKey, failureMessage: FailureMessage) + def read(in: InputStream): Packet = { + val version = in.read + val publicKey = new Array[Byte](PubKeyLength) + in.read(publicKey) + val onionPayload = new Array[Byte](PayloadLength) + in.read(onionPayload) + val hmac = new Array[Byte](MacLength) + in.read(hmac) + Packet(version, ByteVector.view(publicKey), ByteVector32(ByteVector.view(hmac)), ByteVector.view(onionPayload)) + } - /** - * Builds an encrypted onion packet that contains payloads for all nodes in the list - * - * @param sessionKey session key - * @param publicKeys node public keys (one per node) - * @param payloads payloads (one per node) - * @param associatedData associated data - * @return an OnionPacket(onion packet, 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 error messages if needed - */ - def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { - val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) + def read(in: ByteVector): Packet = read(new ByteArrayInputStream(in.toArray)) - val lastPacket = makeNextPacket(payloads.last, associatedData, ephemeralPublicKeys.last.value, sharedsecrets.last, EMPTY_PACKET, filler) + /** + * Generate a deterministic filler to prevent intermediate nodes from knowing their position in the route. + * See /~https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#filler-generation + * + * @param keyType type of key used (depends on the onion we're building) + * @param sharedSecrets shared secrets for all the hops + * @param payloads payloads for all the hops + * @return filler bytes + */ + def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], payloads: Seq[ByteVector]): ByteVector = { + require(sharedSecrets.length == payloads.length, "the number of secrets should equal the number of payloads") + + (sharedSecrets zip payloads).foldLeft(ByteVector.empty)((padding, secretAndPayload) => { + val (secret, perHopPayload) = secretAndPayload + val perHopPayloadLength = currentHopLength(perHopPayload) + require(perHopPayloadLength == perHopPayload.length + MacLength, s"invalid payload: length isn't correctly encoded: $perHopPayload") + val key = generateKey(keyType, secret) + val padding1 = padding ++ ByteVector.fill(perHopPayloadLength)(0) + val stream = generateStream(key, PayloadLength + perHopPayloadLength).takeRight(padding1.length) + padding1.xor(stream) + }) + } + + /** + * Parse the incoming packet, decrypts the payload and builds the packet for the next node. + * + * @param privateKey this node's private key + * @param associatedData associated data + * @param rawPacket packet received by this node + * @return a ParsedPacket(payload, packet, shared secret) object where: + * - payload is the per-hop payload for this node + * - packet is the next packet, to be forwarded using the info that is given in payload (channel id for now) + * - shared secret is the secret we share with the node that sent the packet. We need it to propagate failure + * messages upstream. + */ + def parsePacket(privateKey: PrivateKey, associatedData: ByteVector, rawPacket: ByteVector): Try[ParsedPacket] = Try { + val packet = read(rawPacket) + val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey) + val mu = generateKey("mu", sharedSecret) + val check = mac(mu, packet.onionPayload ++ associatedData) + require(check == packet.hmac, "invalid header mac") + + val rho = generateKey("rho", sharedSecret) + // Since we don't know the length of the hop payload (we will learn it once we decode the first byte), + // we have to pessimistically generate a long cipher stream. + val stream = generateStream(rho, 2 * PayloadLength) + val bin = (packet.onionPayload ++ ByteVector.fill(PayloadLength)(0)) xor stream + + val perHopPayloadLength = currentHopLength(bin) + val perHopPayload = bin.take(perHopPayloadLength - MacLength) + + val hmac = ByteVector32(bin.slice(perHopPayloadLength - MacLength, perHopPayloadLength)) + val nextOnionPayload = bin.drop(perHopPayloadLength).take(PayloadLength) + val nextPubKey = blind(PublicKey(packet.publicKey), computeBlindingFactor(PublicKey(packet.publicKey), sharedSecret)) + + ParsedPacket(perHopPayload, Packet(Version, nextPubKey.value, hmac, nextOnionPayload), sharedSecret) + } @tailrec - def loop(hoppayloads: Seq[ByteVector], ephkeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: Packet): Packet = { - if (hoppayloads.isEmpty) packet else { - val nextPacket = makeNextPacket(hoppayloads.last, associatedData, ephkeys.last.value, sharedSecrets.last, packet) - loop(hoppayloads.dropRight(1), ephkeys.dropRight(1), sharedSecrets.dropRight(1), nextPacket) + private def extractSharedSecrets(packet: ByteVector, privateKey: PrivateKey, associatedData: ByteVector32, acc: Seq[ByteVector32] = Nil): Try[Seq[ByteVector32]] = { + parsePacket(privateKey, associatedData, packet) match { + case Success(ParsedPacket(_, nextPacket, sharedSecret)) if nextPacket.isLastPacket => Success(acc :+ sharedSecret) + case Success(ParsedPacket(_, nextPacket, sharedSecret)) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret) + case Failure(t) => Failure(t) } } - val packet = loop(payloads.dropRight(1), ephemeralPublicKeys.dropRight(1), sharedsecrets.dropRight(1), lastPacket) - PacketAndSecrets(packet, sharedsecrets.zip(publicKeys)) - } + /** + * Compute the next packet from the current packet and node parameters. + * Packets are constructed in reverse order: + * - you first build the last packet + * - then you call makeNextPacket(...) until you've built the final onion packet that will be sent to the first node + * in the route + * + * @param payload payload for this packet + * @param associatedData associated data + * @param ephemeralPublicKey ephemeral key for this packet + * @param sharedSecret shared secret + * @param packet current packet (1 + all zeroes if this is the last packet) + * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet + * @return the next packet + */ + private def makeNextPacket(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: ByteVector, sharedSecret: ByteVector32, packet: Packet, onionPayloadFiller: ByteVector = ByteVector.empty): Packet = { + require(payload.length <= PayloadLength - MacLength, s"packet payload cannot exceed ${PayloadLength - MacLength} bytes") + + val nextOnionPayload = { + val onionPayload1 = payload ++ packet.hmac ++ packet.onionPayload.dropRight(payload.length + MacLength) + val onionPayload2 = onionPayload1 xor generateStream(generateKey("rho", sharedSecret), PayloadLength) + onionPayload2.dropRight(onionPayloadFiller.length) ++ onionPayloadFiller + } - /* - error packet format: - +----------------+----------------------------------+-----------------+----------------------+-----+ - | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | - +----------------+----------------------------------+-----------------+----------------------+-----+ - with failure message length + pad length = 256 - */ - val MaxErrorPayloadLength = 256 - val ErrorPacketLength = MacLength + MaxErrorPayloadLength + 2 + 2 + val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload ++ associatedData) + val nextPacket = Packet(Version, ephemeralPublicKey, nextHmac, nextOnionPayload) + nextPacket + } - /** - * - * @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC - * was created or forwarded: see makePacket() and makeNextPacket() - * @param failure failure message - * @return an error packet that can be sent to the destination node - */ - def createErrorPacket(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { - val message: ByteVector = FailureMessageCodecs.failureMessageCodec.encode(failure).require.toByteVector - require(message.length <= MaxErrorPayloadLength, s"error message length is ${message.length}, it must be less than $MaxErrorPayloadLength") - val um = Sphinx.generateKey("um", sharedSecret) - val padlen = MaxErrorPayloadLength - message.length - val payload = Protocol.writeUInt16(message.length.toInt, ByteOrder.BIG_ENDIAN) ++ message ++ Protocol.writeUInt16(padlen.toInt, ByteOrder.BIG_ENDIAN) ++ ByteVector.fill(padlen.toInt)(0) - logger.debug(s"um key: $um") - logger.debug(s"error payload: ${payload.toHex}") - logger.debug(s"raw error packet: ${(Sphinx.mac(um, payload) ++ payload).toHex}") - forwardErrorPacket(Sphinx.mac(um, payload) ++ payload, sharedSecret) - } + /** + * Build an encrypted onion packet that contains payloads for all nodes in the list + * + * @param sessionKey session key + * @param publicKeys node public keys (one per node) + * @param payloads payloads (one per node) + * @param associatedData associated data + * @return an OnionPacket(onion packet, 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 error messages if needed + */ + def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { + val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) + val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) + + val lastPacket = makeNextPacket(payloads.last, associatedData, ephemeralPublicKeys.last.value, sharedsecrets.last, EMPTY_PACKET, filler) + + @tailrec + def loop(hopPayloads: Seq[ByteVector], ephKeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: Packet): Packet = { + if (hopPayloads.isEmpty) packet else { + val nextPacket = makeNextPacket(hopPayloads.last, associatedData, ephKeys.last.value, sharedSecrets.last, packet) + loop(hopPayloads.dropRight(1), ephKeys.dropRight(1), sharedSecrets.dropRight(1), nextPacket) + } + } - /** - * - * @param packet error packet - * @return the failure message that is embedded in the error packet - */ - private def extractFailureMessage(packet: ByteVector): FailureMessage = { - require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength") - val (mac, payload) = packet.splitAt(Sphinx.MacLength) - val len = Protocol.uint16(payload.toArray, ByteOrder.BIG_ENDIAN) - require((len >= 0) && (len <= MaxErrorPayloadLength), s"message length must be less than $MaxErrorPayloadLength") - FailureMessageCodecs.failureMessageCodec.decode(BitVector(payload.drop(2).take(len))).require.value - } + val packet = loop(payloads.dropRight(1), ephemeralPublicKeys.dropRight(1), sharedsecrets.dropRight(1), lastPacket) + PacketAndSecrets(packet, sharedsecrets.zip(publicKeys)) + } - /** - * - * @param packet error packet - * @param sharedSecret destination node's shared secret - * @return an obfuscated error packet that can be sent to the destination node - */ - def forwardErrorPacket(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = { - require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength") - val key = generateKey("ammag", sharedSecret) - val stream = generateStream(key, ErrorPacketLength) - logger.debug(s"ammag key: $key") - logger.debug(s"error stream: $stream") - packet xor stream } /** - * - * @param sharedSecret this node's shared secret - * @param packet error packet - * @return true if the packet's mac is valid, which means that it has been properly de-obfuscated + * A payment onion packet is used when offering an HTLC to a remote node. */ - private def checkMac(sharedSecret: ByteVector32, packet: ByteVector): Boolean = { - val (mac, payload) = packet.splitAt(Sphinx.MacLength) - val um = Sphinx.generateKey("um", sharedSecret) - ByteVector32(mac) == Sphinx.mac(um, payload) + object PaymentPacket extends OnionPacket { + + override val PayloadLength = 1300 + } /** - * Parse and de-obfuscate an error packet. Node shared secrets are applied until the packet's MAC becomes valid, - * which means that it was sent by the corresponding node. + * A properly decoded error from a node in the route. + * It has the following format: + * +----------------+----------------------------------+-----------------+----------------------+-----+ + * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | + * +----------------+----------------------------------+-----------------+----------------------+-----+ + * with failure message length + pad length = 256 * - * @param packet error packet - * @param sharedSecrets nodes shared secrets - * @return Success(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, Failure otherwise + * @param originNode public key of the node that generated the failure. + * @param failureMessage friendly error message. */ - def parseErrorPacket(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[ErrorPacket] = Try { - require(packet.length == ErrorPacketLength, s"invalid error packet length ${packet.length}, must be $ErrorPacketLength") + case class ErrorPacket(originNode: PublicKey, failureMessage: FailureMessage) - @tailrec - def loop(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): ErrorPacket = sharedSecrets match { - case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets") - case (secret, pubkey) :: tail => - val packet1 = forwardErrorPacket(packet, secret) - if (checkMac(secret, packet1)) ErrorPacket(pubkey, extractFailureMessage(packet1)) else loop(packet1, tail) + object ErrorPacket { + + val MaxPayloadLength = 256 + val PacketLength = MacLength + MaxPayloadLength + 2 + 2 + + /** + * Create an error packet that will be returned to the sender. + * Each intermediate hop will add a layer of encryption and forward to the previous hop. + * Note that malicious intermediate hops may drop the packet or alter it (which breaks the mac). + * + * @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC + * was created or forwarded: see makePacket() and makeNextPacket() + * @param failure failure message + * @return an error packet that can be sent to the destination node + */ + def createPacket(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { + val message: ByteVector = FailureMessageCodecs.failureMessageCodec.encode(failure).require.toByteVector + require(message.length <= MaxPayloadLength, s"error message length is ${message.length}, it must be less than $MaxPayloadLength") + val um = generateKey("um", sharedSecret) + val padLength = MaxPayloadLength - message.length + val payload = Protocol.writeUInt16(message.length.toInt, ByteOrder.BIG_ENDIAN) ++ message ++ Protocol.writeUInt16(padLength.toInt, ByteOrder.BIG_ENDIAN) ++ ByteVector.fill(padLength.toInt)(0) + logger.debug(s"um key: $um") + logger.debug(s"error payload: ${payload.toHex}") + logger.debug(s"raw error packet: ${(mac(um, payload) ++ payload).toHex}") + forwardPacket(mac(um, payload) ++ payload, sharedSecret) + } + + /** + * Extract the failure message from an error packet. + * + * @param packet error packet + * @return the failure message that is embedded in the error packet + */ + private def extractFailureMessage(packet: ByteVector): FailureMessage = { + require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") + val (_, payload) = packet.splitAt(MacLength) + val len = Protocol.uint16(payload.toArray, ByteOrder.BIG_ENDIAN) + require((len >= 0) && (len <= MaxPayloadLength), s"message length must be less than $MaxPayloadLength") + FailureMessageCodecs.failureMessageCodec.decode(BitVector(payload.drop(2).take(len))).require.value + } + + /** + * Forward an error packet to the previous hop. + * + * @param packet error packet + * @param sharedSecret destination node's shared secret + * @return an obfuscated error packet that can be sent to the destination node + */ + def forwardPacket(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = { + require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") + val key = generateKey("ammag", sharedSecret) + val stream = generateStream(key, PacketLength) + logger.debug(s"ammag key: $key") + logger.debug(s"error stream: $stream") + packet xor stream + } + + /** + * Check the mac of an error packet. + * Note that malicious nodes in the route may have altered the packet, thus breaking the mac. + * + * @param sharedSecret this node's shared secret + * @param packet error packet + * @return true if the packet's mac is valid, which means that it has been properly de-obfuscated + */ + private def checkMac(sharedSecret: ByteVector32, packet: ByteVector): Boolean = { + val (packetMac, payload) = packet.splitAt(MacLength) + val um = generateKey("um", sharedSecret) + ByteVector32(packetMac) == mac(um, payload) + } + + /** + * Parse and de-obfuscate an error packet. Node shared secrets are applied until the packet's MAC becomes valid, + * which means that it was sent by the corresponding node. + * + * @param packet error packet + * @param sharedSecrets nodes shared secrets + * @return Success(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, Failure otherwise + */ + def parsePacket(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[ErrorPacket] = Try { + require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") + + @tailrec + def loop(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): ErrorPacket = sharedSecrets match { + case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets") + case (secret, pubkey) :: tail => + val packet1 = forwardPacket(packet, secret) + if (checkMac(secret, packet1)) ErrorPacket(pubkey, extractFailureMessage(packet1)) else loop(packet1, tail) + } + + loop(packet, sharedSecrets) } - loop(packet, sharedSecrets) } + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index cf878ed627..9d633e9963 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -20,10 +20,9 @@ import java.util.UUID import akka.actor.{ActorRef, FSM, Props, Status} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, ByteVector64, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, CMD_ADD_HTLC, Channel, Register} -import fr.acinq.eclair.crypto.Sphinx.{ErrorPacket, Packet} import fr.acinq.eclair.crypto.{Sphinx, TransportHandler} import fr.acinq.eclair.db.{OutgoingPayment, OutgoingPaymentStatus} import fr.acinq.eclair.payment.PaymentLifecycle._ @@ -86,8 +85,8 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis stop(FSM.Normal) case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => - Sphinx.parseErrorPacket(fail.reason, sharedSecrets) match { - case Success(e@ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => + Sphinx.ErrorPacket.parsePacket(fail.reason, sharedSecrets) match { + case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) @@ -96,7 +95,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned val failure = res match { - case Success(e@ErrorPacket(nodeId, failureMessage)) => + case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)") RemoteFailure(hops, e) case Failure(t) => @@ -114,12 +113,12 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}") router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops)) - case Success(e@ErrorPacket(nodeId, failureMessage: Node)) => + case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") // let's try to route around this node router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) - case Success(e@ErrorPacket(nodeId, failureMessage: Update)) => + case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") if (Announcements.checkSig(failureMessage.update, nodeId)) { getChannelUpdateForNode(nodeId, hops) match { @@ -152,7 +151,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) - case Success(e@ErrorPacket(nodeId, failureMessage)) => + case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") // let's try again without the channel outgoing from nodeId val faultyChannel = hops.find(_.nodeId == nodeId).map(hop => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)) @@ -216,7 +215,7 @@ object PaymentLifecycle { case class PaymentSucceeded(id: UUID, amountMsat: Long, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees sealed trait PaymentFailure case class LocalFailure(t: Throwable) extends PaymentFailure - case class RemoteFailure(route: Seq[Hop], e: ErrorPacket) extends PaymentFailure + case class RemoteFailure(route: Seq[Hop], e: Sphinx.ErrorPacket) extends PaymentFailure case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure]) extends PaymentResult @@ -242,7 +241,7 @@ object PaymentLifecycle { case Attempt.Successful(bitVector) => bitVector.toByteVector case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause") } - Sphinx.makePacket(sessionKey, nodes, payloadsbin, associatedData) + Sphinx.PaymentPacket.makePacket(sessionKey, nodes, payloadsbin, associatedData) } /** @@ -267,7 +266,7 @@ object PaymentLifecycle { val nodes = hops.map(_.nextNodeId) // BOLT 2 requires that associatedData == paymentHash val onion = buildOnion(nodes, payloads, paymentHash) - CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, Packet.write(onion.packet), upstream = Left(id), commit = true) -> onion.sharedSecrets + CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion.packet.serialize, upstream = Left(id), commit = true) -> onion.sharedSecrets } /** @@ -312,6 +311,6 @@ object PaymentLifecycle { */ def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean = failures - .collectFirst { case RemoteFailure(_, ErrorPacket(origin, u: Update)) if origin == nodeId => u.update } + .collectFirst { case RemoteFailure(_, Sphinx.ErrorPacket(origin, u: Update)) if origin == nodeId => u.update } .isDefined } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 1a75ee2083..4143213b18 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -231,6 +231,7 @@ object Relayer { */ def tryParsePacket(add: UpdateAddHtlc, privateKey: PrivateKey): Try[NextPayload] = Sphinx + .PaymentPacket .parsePacket(privateKey, add.paymentHash, add.onionRoutingPacket) .flatMap { case Sphinx.ParsedPacket(payload, nextPacket, _) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 132576af7f..fe6cd28024 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -123,7 +123,7 @@ object LightningMessageCodecs { ("amountMsat" | uint64overflow) :: ("paymentHash" | bytes32) :: ("expiry" | uint32) :: - ("onionRoutingPacket" | bytes(Sphinx.PacketLength))).as[UpdateAddHtlc] + ("onionRoutingPacket" | bytes(Sphinx.PaymentPacket.PacketLength))).as[UpdateAddHtlc] val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = ( ("channelId" | bytes32) :: diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index 8ced14a635..b5d5aabba4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -18,7 +18,6 @@ package fr.acinq.eclair.channel.states import java.util.UUID -import akka.actor.Actor import akka.testkit.{TestFSMRef, TestKitBase, TestProbe} import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.TestConstants.{Alice, Bob} @@ -37,7 +36,7 @@ import scodec.bits.ByteVector */ trait StateTestsHelperMethods extends TestKitBase { - def defaultOnion: ByteVector = ByteVector.fill(Sphinx.PacketLength)(0) + def defaultOnion: ByteVector = ByteVector.fill(Sphinx.PaymentPacket.PacketLength)(0) case class SetupFixture(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 21cd41d392..e19a28349f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -72,7 +72,7 @@ class SphinxSpec extends FunSuite { */ test("generate filler with fixed-size payloads") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceFixedSizePayloads.dropRight(1)) + val filler = PaymentPacket.generateFiller("rho", sharedsecrets.dropRight(1), referenceFixedSizePayloads.dropRight(1)) assert(filler == hex"c6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") } @@ -81,19 +81,19 @@ class SphinxSpec extends FunSuite { */ test("generate filler with variable-size payloads") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) - val filler = generateFiller("rho", sharedsecrets.dropRight(1), referenceVariableSizePayloads.dropRight(1)) + val filler = PaymentPacket.generateFiller("rho", sharedsecrets.dropRight(1), referenceVariableSizePayloads.dropRight(1)) assert(filler == hex"b77d99c935d3f32469844f7e09340a91ded147557bdd0456c369f7e449587c0f5666faab58040146db49024db88553729bce12b860391c29c1779f022ae48a9cb314ca35d73fc91addc92632bcf7ba6fd9f38e6fd30fabcedbd5407b6648073c38331ee7ab0332f41f550c180e1601f8c25809ed75b3a1e78635a2ef1b828e92c9658e76e49f995d72cf9781eec0c838901d0bdde3ac21c13b4979ac9e738a1c4d0b9741d58e777ad1aed01263ad1390d36a18a6b92f4f799dcf75edbb43b7515e8d72cb4f827a9af0e7b9338d07b1a24e0305b5535f5b851b1144bad6238b9d9482b5ba6413f1aafac3cdde5067966ed8b78f7c1c5f916a05f874d5f17a2b7d0ae75d66a5f1bb6ff932570dc5a0cf3ce04eb5d26bc55c2057af1f8326e20a7d6f0ae644f09d00fac80de60f20aceee85be41a074d3e1dda017db79d0070b99f54736396f206ee3777abd4c00a4bb95c871750409261e3b01e59a3793a9c20159aae4988c68397a1443be6370fd9614e46108291e615691729faea58537209fa668a172d066d0efff9bc77c2bd34bd77870ad79effd80140990e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a") } test("create packet with fixed-size payloads (reference test vector)") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceFixedSizePayloads, associatedData) + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, referenceFixedSizePayloads, associatedData) assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf") - val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) - val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) - val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) - val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) - val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + val Success(ParsedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, onion.serialize) + val Success(ParsedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) + val Success(ParsedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) + val Success(ParsedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) + val Success(ParsedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceFixedSizePayloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) @@ -107,14 +107,14 @@ class SphinxSpec extends FunSuite { } test("create packet with variable-size payloads (reference test vector)") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, referenceVariableSizePayloads, associatedData) + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, referenceVariableSizePayloads, associatedData) assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7181924e4b6c645f2b6eecc821084ec9b16dfb9d2e7622d4c14db4fc5ecdfc07eac50f7d61ab590531cf08000178a333a347f8b4072e7d3ae44f4f309e150b49886d3f044cd6462be389c830784aba767682923c8683404aaf9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf050597c681185f336b1da63b0939aa2b7c50b21b5eb7b6ad66c81fab98a3cdf73f658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d7fca6a0e4ecc9f9de611a90da6e99c39551094c56e3196f282c5dffd9fc4b2fc12f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa337ef811d5345a9e16b60de1767b209645ba40bd1f9a5f75bc04feca9b27c5554be4fe83fac2cb83aa447a817bb85ae966c68b420063833fada375e2f515965e687a45699632902672c654d1d18d7bcbf55e8fa57f63f2da449f8e1e606e8722df081e5f193fc4179feb99ad22819afdeef211f7c54afdba92aeef0c00b7bc2b65a4813c01f907a8377585708f2d4c940a25328e585714c8ded0a9a4d7a6de1027c1cb7a0198cd3db68b58c0704dfd0cfbe624e9cd18cc0ae5d96697bb476708b9ee0403d211e64e0d5a7683a7a9a140c02f0ff1c6e67a302941b4052bdea8a63e70a3ad62c5b89c698f1fd3c7685cb49705096cad702d02d93bcb1c27a409f4c9bddec001205ca4a2740f19b50900be81c7e847f1a863deea8d35701f1355cad8db57b1d4eb2ab4e29587734785abfb46ddede71928213d7d089dfdeda052827f459f1688cc0935bd47e7bcec27427c8376dcce7e22699567c0d145f8a7db33f6758815f1f15f9f7a9760dec4f34ae095edda4c64e9735bdd029c4e32c2ee31ba47ec5e6bdb97813d52dbd15b4e0b7a2c7f790ae64104d99f38c127f0a093288fa34144adb16b8968d4fa7656fcec99de8503dd46d3b03620a71c7cd085364abd30dccf7fbda25a1cdc102600149c9af1c97aa0372cd2e1909f28ac5c686f432b310e79528c9b8b9e8f314c1e74621ce6308ad2278b81d460892e0d9dd38b7c76d58be6dfd10ae7583ee1e7ef5b3f6f78dc60af0950df1b00cc55b6d178ba2e476bea0eaeef49323b83f05804159e7aef4eed4cc60dd07be76f067dfd0bcfb0b806b69ba921336a20c43c832d0cab8fa3ddeb29e3bf07b0d98a112eb07802756235a49d44a8b82a950d84e95e01971f0e106ccb337f07384e21620e0ad39e16ed9edca123226cf55ac44f449eeb53e38a7f27d101806e4823e4efcc887414240ee6826c4a5cb1c6443ad36ebf905a435c1d9054e54173911b17b5b40f60b3d9fd5f12eac54ca1e20191f5f18544d5fd3d665e9bcef96fb44b76110aa64d9db4c86c9513cbdad546538e8aec521fbe83ceac5e74a15629f1ed0b870a1d0d1e5680b6d6100d1bdd9a2ba3be3772f71854cb12ae9b3afd87cda738dcd107fe56a15f1877450cd0e") - val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) - val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) - val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) - val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) - val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + val Success(ParsedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, onion.serialize) + val Success(ParsedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) + val Success(ParsedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) + val Success(ParsedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) + val Success(ParsedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceVariableSizePayloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) @@ -127,14 +127,14 @@ class SphinxSpec extends FunSuite { } test("create packet with variable-size payloads filling the onion") { - val Sphinx.PacketAndSecrets(onion, sharedSecrets) = Sphinx.makePacket(sessionKey, publicKeys, variableSizePayloadsFull, associatedData) + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, variableSizePayloadsFull, associatedData) assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866196ef84350c2a76fc232b5d46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6101810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bf74b2ce49922898e9353fa268086c00ae8b7f718405b72ad3829dbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf776b75ebb389bf84d0bfbf58590e510e034572a01e409c30939dc3c0b7d6a68a371019f5378bf6133fe0de00dc6f90d98ce1cbe3f165182ca37d6208da0f6bafd75ff41903ab352a1f47586eae3c6c8e437d4308766f71052b46ba2efbd87c0a781e8b3f456300fc7efbefc78ab515338666aed2070e674143c30b520b9cc1782ba8b46454db0d4ce72589cfc2eafb2db452ec98573ad08496483741de5376bfc7357fc6ea629e31236ba6ba7703014959129141a1719788ec83884f2e9151a680e2a96d2bcc67a8a2935aa11acee1f9d04812045b4ae5491220313756b5b9a0a6f867f2a95be1fab14870f04eeab694d9594620632b14ec4b424b495914f3dc587f75cd4582c113bb61e34a0fa7f79f97463be4e3c6fb99516889ed020acee419bb173d38e5ba18a00065e11fd733cf9ae46505dbb4ef70ef2f502601f4f6ee1fdb9d17435e15080e962f24760843f35bac1ac079b694ff7c347c1ed6a87f02b0758fbf00917652641cb68f584fd830b6c738e03b424ea0bc753d24306d7b691693d3286706fee7d57a939e28d77b3da47efc072436a3fd7f9c40515af8c4903764301e62b57153a5ca03ff5bb49c7dc8d3b2858100fb4aa5df7a94a271b73a76129445a3ea180d84d19029c003c164db926ed6983e5219028721a294f145e3fcc20915b8a2147efc896ee5d314e01874ea9e7bc1f386ba6b8f262942aa0193a537ffc91b1ccc9171a3c2016ecf387a3cd8b79df80a8e9412e707cb9c761a0809a84c606a779567f9f0edf685b38c98877e90d02aedd096ed841e50abf2114ce01efbff04788fb280f870eca20c7ec353d5c381903e7d08fc57695fd79c27d43e7bd603a876068d3f1c7f45af99003e5eec7e8d8c91e395320f1fc421ef3552ea033129429383304b760c8f93de342417c3223c2112a623c3514480cdfae8ec15a99abfca71b03a8396f19edc3d5000bcfb77b5544813476b1b521345f4da396db09e783870b97bc2034bd11611db30ed2514438b046f1eb7093eceddfb1e73880786cd7b540a3896eaadd0a0692e4b19439815b5f2ec855ec8ececce889442a64037e9564521fce926613b5d3074246c5a34a296ad1a18ef556d73fcd6c85ea3fdfb03b4e8e4bb0e35997fc35544d3c2a00e5e1f45dc925607d952c6a89721bd0b6f6aec03314d667166a5b8b18471403be7018b2479aaef6c7c6c554a50a98b717dff06d50be39fb36dc03e678e0a52fc615be46b223e3bee83fa0c7c47a1f29fb94f1e9eebf6c9ecf8fc79ae847df2effb60d07aba301fc536546ec4899eedb4fec9a9bed79e3a83c4b32757745778e977e485c67c0f12bbc82c0b3bb0f4df0bd13d046fed4446f54cd85bfce55ef781a80e5f63d289d08de001237928c2a4e0c8694d0c1e68cc23f2409f30009019085e831a928e7bc5b00a1f29d25482f7fd0b6dad30e6ef8edc68ddf7db404ea7d11540fc2cee74863d64af4c945457e04b7bea0a5fb8636edadb1e1d6f2630d61062b781c1821f46eddadf269ea1fada829547590081b16bc116e074cae0224a375f2d9ce16e836687c89cd285e3b40f1e59ce2caa3d1d8cf37ee4d5e3abe7ef0afd6ffeb4fd6905677b950894863c828ab8d93519566f69fa3c2129da763bf58d9c4d2837d4d9e13821258f7e7098b34218b846b5ee59284dc6791344738aac3d9a9014b764104cd606ef2c37a03fc7e") - val Success(Sphinx.ParsedPacket(payload0, nextPacket0, sharedSecret0)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) - val Success(Sphinx.ParsedPacket(payload1, nextPacket1, sharedSecret1)) = Sphinx.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) - val Success(Sphinx.ParsedPacket(payload2, nextPacket2, sharedSecret2)) = Sphinx.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) - val Success(Sphinx.ParsedPacket(payload3, nextPacket3, sharedSecret3)) = Sphinx.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) - val Success(Sphinx.ParsedPacket(payload4, nextPacket4, sharedSecret4)) = Sphinx.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + val Success(ParsedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, onion.serialize) + val Success(ParsedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) + val Success(ParsedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) + val Success(ParsedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) + val Success(ParsedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) assert(Seq(payload0, payload1, payload2, payload3, payload4) == variableSizePayloadsFull) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) @@ -147,10 +147,10 @@ class SphinxSpec extends FunSuite { } test("create packet with single variable-size payload filling the onion") { - val Sphinx.PacketAndSecrets(onion, _) = Sphinx.makePacket(sessionKey, publicKeys.take(1), variableSizeOneHopPayload, associatedData) + val PacketAndSecrets(onion, _) = PaymentPacket.makePacket(sessionKey, publicKeys.take(1), variableSizeOneHopPayload, associatedData) assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661918004735c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2cc49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f580c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce42e93a4d7c803ead45fc47cf4396d284632314d789e73cf3f534126c63fe244069d9e8a7c4f98e7e530fc588e648ef4e641364981b5377542d5e7a4aaab6d35f6df7d3a9d7ca715213599ee02c4dbea4dc78860febe1d29259c64b59b3333ffdaebbaff4e7b31c27a3791f6bf848a58df7c69bb2b1852d2ad357b9919ffdae570b27dc709fba087273d3a4de9e6a6be66db647fb6a8d1a503b3f481befb96745abf5cc4a6bba0f780d5c7759b9e303a2a6b17eb05b6e660f4c474959db183e1cae060e1639227ee0bca03978a238dc4352ed764da7d4f3ed5337f6d0376dff72615beeeeaaeef79ab93e4bcbf18cd8424eb2b6ad7f33d2b4ffd5ea08372e6ed1d984152df17e04c6f73540988d7dd979e020424a163c271151a255966be7edef42167b8facca633649739bab97572b485658cde409e5d4a0f653f1a5911141634e3d2b6079b19347df66f9820755fd517092dae62fb278b0bafcc7ad682f7921b3a455e0c6369988779e26f0458b31bffd7e4e5bfb31944e80f100b2553c3b616e75be18328dc430f6618d55cd7d0962bb916d26ed4b117c46fa29e0a112c02c36020b34a96762db628fa3490828ec2079962ad816ef20ea0bca78fb2b7f7aedd4c47e375e64294d151ff03083730336dea64934003a27730cc1c7dec5049ddba8188123dd191aa71390d43a49fb792a3da7082efa6cced73f00eccea18145fbc84925349f7b552314ab8ed4c491e392aed3b1f03eb79474c294b42e2eba1528da26450aa592cba7ea22e965c54dff0fd6fdfd6b52b9a0f5f762e27fb0e6c3cd326a1ca1c5973de9be881439f702830affeb0c034c18ac8d5c2f135c964bf69de50d6e99bde88e90321ba843d9753c8f83666105d25fafb1a11ea22d62ef6f1fc34ca4e60c35d69773a104d9a44728c08c20b6314327301a2c400a71e1424c12628cf9f4a67990ade8a2203b0edb96c6082d4673b7309cd52c4b32b02951db2f66c6c72bd6c7eac2b50b83830c75cdfc3d6e9c2b592c45ed5fa5f6ec0da85710b7e1562aea363e28665835791dc574d9a70b2e5e2b9973ab590d45b94d244fc4256926c5a55b01cd0aca21fe5f9c907691fb026d0c56788b03ca3f08db0abb9f901098dde2ec4003568bc3ca27475ff86a7cb0aabd9e5136c5de064d16774584b252024109bb02004dba1fabf9e8277de097a0ab0dc8f6e26fcd4a28fb9d27cd4a2f6b13e276ed259a39e1c7e60f3c32c5cc4c4f96bd981edcb5e2c76a517cdc285aa2ca571d1e3d463ecd7614ae227df17af7445305bd7c661cf7dba658b0adcf36b0084b74a5fa408e272f703770ac5351334709112c5d4e4fe987e0c27b670412696f52b33245c229775da550729938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3da3bac7ec6b67f68f2838b6dc687c42dd3b4be7d8e44ded20177e8e3cc9c7014") - val Success(Sphinx.ParsedPacket(payload, nextPacket, _)) = Sphinx.parsePacket(privKeys(0), associatedData, onion.serialize) + val Success(ParsedPacket(payload, nextPacket, _)) = PaymentPacket.parsePacket(privKeys(0), associatedData, onion.serialize) assert(payload == variableSizeOneHopPayload.head) assert(nextPacket.hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } @@ -163,7 +163,7 @@ class SphinxSpec extends FunSuite { hex"000000000000000000000000000000000000000000000000000000000000000000" ) - assertThrows[IllegalArgumentException](Sphinx.makePacket(sessionKey, publicKeys.take(2), incorrectVarint, associatedData)) + assertThrows[IllegalArgumentException](PaymentPacket.makePacket(sessionKey, publicKeys.take(2), incorrectVarint, associatedData)) } test("last node replies with an error message") { @@ -171,44 +171,44 @@ class SphinxSpec extends FunSuite { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 // origin build the onion packet - val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData) + val PacketAndSecrets(packet, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, payloads, associatedData) // each node parses and forwards the packet // node #0 - val Success(ParsedPacket(_, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) + val Success(ParsedPacket(_, packet1, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, packet.serialize) // node #1 - val Success(ParsedPacket(_, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) + val Success(ParsedPacket(_, packet2, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, packet1.serialize) // node #2 - val Success(ParsedPacket(_, packet3, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) + val Success(ParsedPacket(_, packet3, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, packet2.serialize) // node #3 - val Success(ParsedPacket(_, packet4, sharedSecret3)) = parsePacket(privKeys(3), associatedData, packet3.serialize) + val Success(ParsedPacket(_, packet4, sharedSecret3)) = PaymentPacket.parsePacket(privKeys(3), associatedData, packet3.serialize) // node #4 - val Success(ParsedPacket(_, packet5, sharedSecret4)) = parsePacket(privKeys(4), associatedData, packet4.serialize) + val Success(ParsedPacket(_, packet5, sharedSecret4)) = PaymentPacket.parsePacket(privKeys(4), associatedData, packet4.serialize) assert(packet5.isLastPacket) // node #4 want to reply with an error message - val error = createErrorPacket(sharedSecret4, TemporaryNodeFailure) + val error = ErrorPacket.createPacket(sharedSecret4, TemporaryNodeFailure) assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") // assert(error == hex"69b1e5a3e05a7b5478e6529cd1749fdd8c66da6f6db42078ff8497ac4e117e91a8cb9168b58f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c2") // error sent back to 3, 2, 1 and 0 - val error1 = forwardErrorPacket(error, sharedSecret3) + val error1 = ErrorPacket.forwardPacket(error, sharedSecret3) assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") // assert(error1 == hex"08cd44478211b8a4370ab1368b5ffe8c9c92fb830ff4ad6e3b0a316df9d24176a081bab161ea0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a93") - val error2 = forwardErrorPacket(error1, sharedSecret2) + val error2 = ErrorPacket.forwardPacket(error1, sharedSecret2) assert(error2 == hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") // assert(error2 == hex"6984b0ccd86f37995857363df13670acd064bfd1a540e521cad4d71c07b1bc3dff9ac25f41addfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd") - val error3 = forwardErrorPacket(error2, sharedSecret1) + val error3 = ErrorPacket.forwardPacket(error2, sharedSecret1) assert(error3 == hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") // assert(error3 == hex"669478a3ddf9ba4049df8fa51f73ac712b9c20380cda431696963a492713ebddb7dfadbb566c8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8") - val error4 = forwardErrorPacket(error3, sharedSecret0) + val error4 = ErrorPacket.forwardPacket(error3, sharedSecret0) assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") // assert(error4 == hex"500d8596f76d3045bfdbf99914b98519fe76ea130dc22338c473ab68d74378b13a06a19f891145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366") // origin parses error packet and can see that it comes from node #4 - val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error4, sharedSecrets) + val Success(ErrorPacket(pubkey, failure)) = ErrorPacket.parsePacket(error4, sharedSecrets) assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure) } @@ -219,25 +219,25 @@ class SphinxSpec extends FunSuite { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 // origin build the onion packet - val PacketAndSecrets(packet, sharedSecrets) = makePacket(sessionKey, publicKeys, payloads, associatedData) + val PacketAndSecrets(packet, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, payloads, associatedData) // each node parses and forwards the packet // node #0 - val Success(ParsedPacket(_, packet1, sharedSecret0)) = parsePacket(privKeys(0), associatedData, packet.serialize) + val Success(ParsedPacket(_, packet1, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, packet.serialize) // node #1 - val Success(ParsedPacket(_, packet2, sharedSecret1)) = parsePacket(privKeys(1), associatedData, packet1.serialize) + val Success(ParsedPacket(_, packet2, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, packet1.serialize) // node #2 - val Success(ParsedPacket(_, _, sharedSecret2)) = parsePacket(privKeys(2), associatedData, packet2.serialize) + val Success(ParsedPacket(_, _, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, packet2.serialize) // node #2 want to reply with an error message - val error = createErrorPacket(sharedSecret2, InvalidRealm) + val error = ErrorPacket.createPacket(sharedSecret2, InvalidRealm) // error sent back to 1 and 0 - val error1 = forwardErrorPacket(error, sharedSecret1) - val error2 = forwardErrorPacket(error1, sharedSecret0) + val error1 = ErrorPacket.forwardPacket(error, sharedSecret1) + val error2 = ErrorPacket.forwardPacket(error1, sharedSecret0) // origin parses error packet and can see that it comes from node #2 - val Success(ErrorPacket(pubkey, failure)) = parseErrorPacket(error2, sharedSecrets) + val Success(ErrorPacket(pubkey, failure)) = ErrorPacket.parsePacket(error2, sharedSecrets) assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index b55e30c85e..68353d1fe4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -42,7 +42,7 @@ class ChannelSelectionSpec extends FunSuite { val relayPayload = RelayPayload( add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, ByteVector.empty), payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), - nextPacket = Sphinx.EMPTY_PACKET // just a placeholder + nextPacket = Sphinx.PaymentPacket.EMPTY_PACKET // just a placeholder ) val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) @@ -75,7 +75,7 @@ class ChannelSelectionSpec extends FunSuite { val relayPayload = RelayPayload( add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, ByteVector.empty), payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), - nextPacket = Sphinx.EMPTY_PACKET // just a placeholder + nextPacket = Sphinx.PaymentPacket.EMPTY_PACKET // just a placeholder ) val (a, b) = (randomKey.publicKey, randomKey.publicKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index 6131b590d2..082bdb3159 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -68,30 +68,30 @@ class HtlcGenerationSpec extends FunSuite { val (_, _, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) val nodes = hops.map(_.nextNodeId) val PacketAndSecrets(packet_b, _) = buildOnion(nodes, payloads, paymentHash) - assert(packet_b.serialize.size === Sphinx.PacketLength) + assert(packet_b.serialize.size === Sphinx.PaymentPacket.PacketLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, packet_b.serialize) + val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.parsePacket(priv_b.privateKey, paymentHash, packet_b.serialize) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_c.serialize.size === Sphinx.PacketLength) + assert(packet_c.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) - val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize) + val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize) val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value - assert(packet_d.serialize.size === Sphinx.PacketLength) + assert(packet_d.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) - val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize) + val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize) val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value - assert(packet_e.serialize.size === Sphinx.PacketLength) + assert(packet_e.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) - val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize) + val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize) val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PacketLength) + assert(packet_random.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.outgoingCltvValue === finalExpiry) } @@ -103,30 +103,30 @@ class HtlcGenerationSpec extends FunSuite { assert(add.amountMsat > finalAmountMsat) assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash === paymentHash) - assert(add.onion.length === Sphinx.PacketLength) + assert(add.onion.length === Sphinx.PaymentPacket.PacketLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, add.onion) + val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.parsePacket(priv_b.privateKey, paymentHash, add.onion) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_c.serialize.size === Sphinx.PacketLength) + assert(packet_c.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) - val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize) + val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize) val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value - assert(packet_d.serialize.size === Sphinx.PacketLength) + assert(packet_d.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) - val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize) + val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize) val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value - assert(packet_e.serialize.size === Sphinx.PacketLength) + assert(packet_e.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) - val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize) + val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize) val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PacketLength) + assert(packet_random.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.outgoingCltvValue === finalExpiry) } @@ -137,12 +137,12 @@ class HtlcGenerationSpec extends FunSuite { assert(add.amountMsat === finalAmountMsat) assert(add.cltvExpiry === finalExpiry) assert(add.paymentHash === paymentHash) - assert(add.onion.size === Sphinx.PacketLength) + assert(add.onion.size === Sphinx.PaymentPacket.PacketLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_random, _)) = Sphinx.parsePacket(priv_b.privateKey, paymentHash, add.onion) + val Success(ParsedPacket(bin_b, packet_random, _)) = Sphinx.PaymentPacket.parsePacket(priv_b.privateKey, paymentHash, add.onion) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PacketLength) + assert(packet_random.serialize.size === Sphinx.PaymentPacket.PacketLength) assert(payload_b.amtToForward === finalAmountMsat) assert(payload_b.outgoingCltvValue === finalExpiry) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index deb2a51441..119a25c83f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -26,8 +26,7 @@ import fr.acinq.bitcoin.{Block, ByteVector32, MilliSatoshi, Satoshi, Transaction import fr.acinq.eclair.blockchain.{UtxoStatus, ValidateRequest, ValidateResult, WatchSpentBasic} import fr.acinq.eclair.channel.Register.ForwardShortId import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelUnavailable} -import fr.acinq.eclair.crypto.{KeyManager, Sphinx} -import fr.acinq.eclair.crypto.Sphinx.ErrorPacket +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.OutgoingPaymentStatus import fr.acinq.eclair.io.Peer.PeerRoutingMessage import fr.acinq.eclair.payment.PaymentLifecycle._ @@ -252,7 +251,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = TemporaryChannelFailure(channelUpdate_bc) relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.createPacket(sharedSecrets1.head._1, failure))) // payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c))) @@ -263,7 +262,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (Update)") { fixture => @@ -295,7 +294,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 42, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(5, channelUpdate_bc_modified) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.createPacket(sharedSecrets1.head._1, failure))) // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) @@ -312,7 +311,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 43, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure2 = IncorrectCltvExpiry(5, channelUpdate_bc_modified_2) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.createErrorPacket(sharedSecrets2.head._1, failure2))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.createPacket(sharedSecrets2.head._1, failure2))) // this time the payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c))) @@ -324,7 +323,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // this time the router can't find a route: game over - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: RemoteFailure(hops2, ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.ErrorPacket(b, failure)) :: RemoteFailure(hops2, Sphinx.ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } @@ -355,7 +354,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = PermanentChannelFailure relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.createErrorPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.createPacket(sharedSecrets1.head._1, failure))) // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) @@ -363,7 +362,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } @@ -452,8 +451,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { } test("filter errors properly") { _ => - val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil + val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil val filtered = PaymentLifecycle.transformForUser(failures) - assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) + assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index fd481b5ba2..c463b491a3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -230,7 +230,7 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, ByteVector.fill(Sphinx.PacketLength)(0)) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, ByteVector.fill(Sphinx.PaymentPacket.PacketLength)(0)) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) @@ -402,7 +402,7 @@ class RelayerSpec extends TestkitBaseClass { // we build a fake htlc for the downstream channel val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = ByteVector.empty) - val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.createErrorPacket(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) + val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.ErrorPacket.createPacket(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) sender.send(relayer, ForwardFail(fail_ba, origin, add_bc)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index c147cf1a1e..c000195254 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -131,7 +131,7 @@ class ChannelCodecsSpec extends FunSuite { amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PacketLength)) + onionRoutingPacket = randomBytes(Sphinx.PaymentPacket.PacketLength)) val htlc1 = DirectedHtlc(direction = IN, add = add) val htlc2 = DirectedHtlc(direction = OUT, add = add) assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require === htlc1) @@ -145,14 +145,14 @@ class ChannelCodecsSpec extends FunSuite { amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PacketLength)) + onionRoutingPacket = randomBytes(Sphinx.PaymentPacket.PacketLength)) val add2 = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PacketLength)) + onionRoutingPacket = randomBytes(Sphinx.PaymentPacket.PacketLength)) val htlc1 = DirectedHtlc(direction = IN, add = add1) val htlc2 = DirectedHtlc(direction = OUT, add = add2) val htlcs = Set(htlc1, htlc2) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index 5936eae80f..773d220faa 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -61,7 +61,7 @@ class LightningMessageCodecsSpec extends FunSuite { val update_fee = UpdateFee(randomBytes32, 2) val shutdown = Shutdown(randomBytes32, bin(47, 0)) val closing_signed = ClosingSigned(randomBytes32, 2, randomBytes64) - val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, 3, bin32(0), 4, bin(Sphinx.PacketLength, 0)) + val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, 3, bin32(0), 4, bin(Sphinx.PaymentPacket.PacketLength, 0)) val update_fulfill_htlc = UpdateFulfillHtlc(randomBytes32, 2, bin32(0)) val update_fail_htlc = UpdateFailHtlc(randomBytes32, 2, bin(154, 0)) val update_fail_malformed_htlc = UpdateFailMalformedHtlc(randomBytes32, 2, randomBytes32, 1111) From 97622d49ca2f8d2f5d4fd33fb2c20a0c575517c6 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 1 Jul 2019 14:19:14 +0200 Subject: [PATCH 13/27] Update varint codec name --- eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index e3fbecadfb..eca91c996e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -108,7 +108,7 @@ object Sphinx extends Logging { case _ => // For non-legacy packets the first bytes are a varint encoding the length of the payload data (not including mac). // Since messages are always smaller than 65535 bytes, the varint will either be 1 or 3 bytes long. - val dataLength = CommonCodecs.varlong.decode(BitVector(payload.take(3))).require.value.toInt + val dataLength = CommonCodecs.varintoverflow.decode(BitVector(payload.take(3))).require.value.toInt val varintLength = dataLength match { case i if i < 253 => 1 case _ => 3 From 22b5faa05e8b5725f53657757cb809cb8e4790c0 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Wed, 3 Jul 2019 16:04:14 +0200 Subject: [PATCH 14/27] Use scodec for onion packet. Refactor and rename sphinx onion packet functions. Comply with BAD_ONION spec sub-errors. --- .../fr/acinq/eclair/channel/Channel.scala | 6 +- .../acinq/eclair/channel/ChannelTypes.scala | 5 +- .../fr/acinq/eclair/channel/Commitments.scala | 14 +- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 316 ++++++++---------- .../fr/acinq/eclair/io/Switchboard.scala | 4 +- .../eclair/payment/PaymentLifecycle.scala | 6 +- .../fr/acinq/eclair/payment/Relayer.scala | 63 ++-- .../fr/acinq/eclair/wire/FailureMessage.scala | 25 +- .../eclair/wire/LightningMessageCodecs.scala | 10 +- .../eclair/wire/LightningMessageTypes.scala | 7 +- .../scala/fr/acinq/eclair/TestConstants.scala | 8 +- .../acinq/eclair/channel/ThroughputSpec.scala | 2 +- .../states/StateTestsHelperMethods.scala | 4 - .../channel/states/e/NormalStateSpec.scala | 103 +++--- .../channel/states/e/OfflineStateSpec.scala | 6 +- .../channel/states/f/ShutdownStateSpec.scala | 4 +- .../states/g/NegotiatingStateSpec.scala | 4 +- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 159 ++++++--- .../rustytests/SynchronizationPipe.scala | 4 +- .../fr/acinq/eclair/io/HtlcReaperSpec.scala | 9 +- .../eclair/payment/ChannelSelectionSpec.scala | 16 +- .../eclair/payment/HtlcGenerationSpec.scala | 44 +-- .../eclair/payment/PaymentHandlerSpec.scala | 16 +- .../eclair/payment/PaymentLifecycleSpec.scala | 10 +- .../fr/acinq/eclair/payment/RelayerSpec.scala | 20 +- .../transactions/CommitmentSpecSpec.scala | 11 +- .../eclair/transactions/TestVectorsSpec.scala | 11 +- .../transactions/TransactionsSpec.scala | 27 +- .../acinq/eclair/wire/ChannelCodecsSpec.scala | 19 +- .../wire/FailureMessageCodecsSpec.scala | 22 +- .../wire/LightningMessageCodecsSpec.scala | 25 +- 32 files changed, 533 insertions(+), 449 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 8672f39d1f..516130d518 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -2164,8 +2164,10 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // let's now fail all pending htlc for which we are the final payee val htlcsToFail = commitments1.remoteCommit.spec.htlcs.collect { - case DirectedHtlc(OUT, add) if Sphinx.PaymentPacket.parsePacket(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket) - .map(_.nextPacket.isLastPacket).getOrElse(true) => add // we also fail htlcs which onion we can't decode (message won't be precise) + case DirectedHtlc(OUT, add) if Sphinx.PaymentPacket.peel(nodeParams.privateKey, add.paymentHash, add.onionRoutingPacket).fold( + _ => true, // we also fail htlcs which onion we can't decode (message won't be precise) + p => p.isLastPacket + ) => add } log.debug(s"failing htlcs=${htlcsToFail.map(Commitments.msg2String(_)).mkString(",")}") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index f5c00118d9..cbd718fb5d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -21,10 +21,9 @@ import java.util.UUID import akka.actor.ActorRef import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx -import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OpenChannel, Shutdown, UpdateAddHtlc} +import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionPacket, OpenChannel, Shutdown, UpdateAddHtlc} import fr.acinq.eclair.{ShortChannelId, UInt64} import scodec.bits.ByteVector @@ -107,7 +106,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: ByteVector = Sphinx.PaymentPacket.EMPTY_PACKET.serialize, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: OnionPacket, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 640adcf48a..980eb6df25 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -26,8 +26,6 @@ import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, UInt64} -import scala.util.{Failure, Success} - // @formatter:off case class LocalChanges(proposed: List[UpdateMessage], signed: List[UpdateMessage], acked: List[UpdateMessage]) { def all: List[UpdateMessage] = proposed ++ signed ++ acked @@ -235,16 +233,16 @@ object Commitments { throw UnknownHtlcId(commitments.channelId, cmd.id) case Some(htlc) => // we need the shared secret to build the error packet - Sphinx.PaymentPacket.parsePacket(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket).map(_.sharedSecret) match { - case Success(sharedSecret) => + Sphinx.PaymentPacket.peel(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket) match { + case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => val reason = cmd.reason match { - case Left(forwarded) => Sphinx.ErrorPacket.forwardPacket(forwarded, sharedSecret) - case Right(failure) => Sphinx.ErrorPacket.createPacket(sharedSecret, failure) + case Left(forwarded) => Sphinx.ErrorPacket.wrap(forwarded, sharedSecret) + case Right(failure) => Sphinx.ErrorPacket.create(sharedSecret, failure) } val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason) val commitments1 = addLocalProposal(commitments, fail) (commitments1, fail) - case Failure(_) => throw new CannotExtractSharedSecret(commitments.channelId, htlc) + case Left(_) => throw CannotExtractSharedSecret(commitments.channelId, htlc) } case None => throw UnknownHtlcId(commitments.channelId, cmd.id) } @@ -263,7 +261,7 @@ object Commitments { } => // we have already sent a fail/fulfill for this htlc throw UnknownHtlcId(commitments.channelId, cmd.id) - case Some(htlc) => + case Some(_) => val fail = UpdateFailMalformedHtlc(commitments.channelId, cmd.id, cmd.onionHash, cmd.failureCode) val commitments1 = addLocalProposal(commitments, fail) (commitments1, fail) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index eca91c996e..c89b308b66 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -16,12 +16,12 @@ package fr.acinq.eclair.crypto -import java.io.{ByteArrayInputStream, ByteArrayOutputStream, InputStream, OutputStream} import java.nio.ByteOrder import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, Crypto, Protocol} -import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs, CommonCodecs} +import fr.acinq.eclair.wire +import fr.acinq.eclair.wire.{CommonCodecs, FailureMessage, FailureMessageCodecs} import grizzled.slf4j.Logging import org.spongycastle.crypto.digests.SHA256Digest import org.spongycastle.crypto.macs.HMac @@ -37,9 +37,7 @@ import scala.util.{Failure, Success, Try} */ object Sphinx extends Logging { - val PubKeyLength = 33 - - // We use hmac which returns 32-bytes message authentication codes. + // We use HMAC-SHA256 which returns 32-bytes message authentication codes. val MacLength = 32 def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { @@ -72,9 +70,9 @@ object Sphinx extends Logging { /** * Compute the ephemeral public keys and shared secrets for all nodes on the route. * - * @param sessionKey this node's session key - * @param publicKeys public keys of each node on the route - * @return a tuple (ephemeral public keys, shared secrets) + * @param sessionKey this node's session key. + * @param publicKeys public keys of each node on the route. + * @return a tuple (ephemeral public keys, shared secrets). */ def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey]): (Seq[PublicKey], Seq[ByteVector32]) = { val ephemeralPublicKey0 = blind(PublicKey(Crypto.curve.getG), sessionKey.value) @@ -84,7 +82,7 @@ object Sphinx extends Logging { } @tailrec - def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], ephemeralPublicKeys: Seq[PublicKey], blindingFactors: Seq[ByteVector32], sharedSecrets: Seq[ByteVector32]): (Seq[PublicKey], Seq[ByteVector32]) = { + private def computeEphemeralPublicKeysAndSharedSecrets(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], ephemeralPublicKeys: Seq[PublicKey], blindingFactors: Seq[ByteVector32], sharedSecrets: Seq[ByteVector32]): (Seq[PublicKey], Seq[ByteVector32]) = { if (publicKeys.isEmpty) (ephemeralPublicKeys, sharedSecrets) else { @@ -96,21 +94,21 @@ object Sphinx extends Logging { } /** - * Return the number of bytes that should be read to extract the per-hop payload (data and mac). + * Peek at the first bytes of the per-hop payload to extract its length. */ - def currentHopLength(payload: ByteVector): Int = { + def peekPayloadLength(payload: ByteVector): Int = { payload.head match { case 0 => - // The 1.0 BOLT spec used 20 fixed-size 65-bytes frames inside the onion payload. - // The first byte of the frame (called `realm`) was set to 0x00, followed by 32 bytes of hop data and a 32-bytes mac. - // The 1.1 BOLT spec changed that format to use variable-length per-hop payloads. + // 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. 65 case _ => - // For non-legacy packets the first bytes are a varint encoding the length of the payload data (not including mac). - // Since messages are always smaller than 65535 bytes, the varint will either be 1 or 3 bytes long. + // 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. val dataLength = CommonCodecs.varintoverflow.decode(BitVector(payload.take(3))).require.value.toInt val varintLength = dataLength match { - case i if i < 253 => 1 + case i if i < 0xfd => 1 case _ => 3 } varintLength + dataLength + MacLength @@ -118,96 +116,61 @@ object Sphinx extends Logging { } /** - * Our Sphinx onion packets have the following format: - * - version (1 byte) - * - ephemeral public key (33 bytes) - * - encrypted onion payload (variable size) - * - hmac of the whole packet (32 bytes) + * Decrypting an onion packet yields a payload for the current node and the encrypted packet for the next node. + * + * @param payload decrypted payload for this node. + * @param nextPacket packet for the next node. + * @param sharedSecret shared secret for the sending node, which we will need to return error messages. */ - case class Packet(version: Int, publicKey: ByteVector, hmac: ByteVector32, onionPayload: ByteVector) { - require(publicKey.length == PubKeyLength, s"onion packet public key length should be $PubKeyLength") - require(hmac.length == MacLength, s"onion packet hmac length should be $MacLength") - - val length = 1 + PubKeyLength + onionPayload.length.toInt + MacLength - - def isLastPacket: Boolean = hmac == ByteVector32.Zeroes - - def write(out: OutputStream): OutputStream = { - out.write(version) - out.write(publicKey.toArray) - out.write(onionPayload.toArray) - out.write(hmac.toArray) - out + case class DecryptedPacket(payload: ByteVector, nextPacket: wire.OnionPacket, sharedSecret: ByteVector32) { + + val isLastPacket: Boolean = payload.head match { + // In Bolt 1.0 the last hop is signaled via an empty hmac. + case 0 => nextPacket.hmac == ByteVector32.Zeroes + // In Bolt 1.1 the last hop can also be signaled via a dedicated TLV type with type=0x00. + case 0xfd => payload(3) == 0 || nextPacket.hmac == ByteVector32.Zeroes + case _ => payload(1) == 0 || nextPacket.hmac == ByteVector32.Zeroes } - def serialize: ByteVector = { - val out = new ByteArrayOutputStream(length) - write(out) - ByteVector.view(out.toByteArray) - } } /** - * Decrypting the received onion packet yields a ParsedPacket. - * - * @param payload payload for this node - * @param nextPacket packet for the next node - * @param sharedSecret shared secret for the sending node, which we will need to return error messages - */ - case class ParsedPacket(payload: ByteVector, nextPacket: Packet, sharedSecret: ByteVector32) - - /** - * A Packet with all the associated shared secrets. + * A encrypted onion packet with all the associated shared secrets. * - * @param packet onion packet + * @param packet encrypted onion packet. * @param sharedSecrets shared secrets (one per node in the route). Known (and needed) only if you're creating the - * packet. Empty if you're just forwarding the packet to the next node + * packet. Empty if you're just forwarding the packet to the next node. */ - case class PacketAndSecrets(packet: Packet, sharedSecrets: Seq[(ByteVector32, PublicKey)]) + case class PacketAndSecrets(packet: wire.OnionPacket, sharedSecrets: Seq[(ByteVector32, PublicKey)]) sealed trait OnionPacket { - // Packet version. Note that since this value is outside of the onion encrypted payload, intermediate hops may or - // may not use this value when forwarding the packet to the next node. - val Version = 0.toByte - - // Length of the obfuscated onion payload. - val PayloadLength: Int - - // Length of the whole packet. - def PacketLength = 1 + PubKeyLength + PayloadLength + MacLength - - // Packet construction starts with an empty packet (all zeroes except for the version byte). - def EMPTY_PACKET = Packet(Version, ByteVector.fill(PubKeyLength)(0), ByteVector32.Zeroes, ByteVector.fill(PayloadLength)(0)) - - def read(in: InputStream): Packet = { - val version = in.read - val publicKey = new Array[Byte](PubKeyLength) - in.read(publicKey) - val onionPayload = new Array[Byte](PayloadLength) - in.read(onionPayload) - val hmac = new Array[Byte](MacLength) - in.read(hmac) - Packet(version, ByteVector.view(publicKey), ByteVector32(ByteVector.view(hmac)), ByteVector.view(onionPayload)) - } + /** + * Supported packet version. Note that since this value is outside of the onion encrypted payload, intermediate + * nodes may or may not use this value when forwarding the packet to the next node. + */ + def Version = 0 - def read(in: ByteVector): Packet = read(new ByteArrayInputStream(in.toArray)) + /** + * Length of the encrypted onion payload. + */ + def PayloadLength: Int /** * Generate a deterministic filler to prevent intermediate nodes from knowing their position in the route. * See /~https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md#filler-generation * - * @param keyType type of key used (depends on the onion we're building) - * @param sharedSecrets shared secrets for all the hops - * @param payloads payloads for all the hops - * @return filler bytes + * @param keyType type of key used (depends on the onion we're building). + * @param sharedSecrets shared secrets for all the hops. + * @param payloads payloads for all the hops. + * @return filler bytes. */ def generateFiller(keyType: String, sharedSecrets: Seq[ByteVector32], payloads: Seq[ByteVector]): ByteVector = { require(sharedSecrets.length == payloads.length, "the number of secrets should equal the number of payloads") (sharedSecrets zip payloads).foldLeft(ByteVector.empty)((padding, secretAndPayload) => { val (secret, perHopPayload) = secretAndPayload - val perHopPayloadLength = currentHopLength(perHopPayload) + val perHopPayloadLength = peekPayloadLength(perHopPayload) require(perHopPayloadLength == perHopPayload.length + MacLength, s"invalid payload: length isn't correctly encoded: $perHopPayload") val key = generateKey(keyType, secret) val padding1 = padding ++ ByteVector.fill(perHopPayloadLength)(0) @@ -217,98 +180,104 @@ object Sphinx extends Logging { } /** - * Parse the incoming packet, decrypts the payload and builds the packet for the next node. + * Decrypt the incoming packet, extract the per-hop payload and build the packet for the next node. * - * @param privateKey this node's private key - * @param associatedData associated data - * @param rawPacket packet received by this node - * @return a ParsedPacket(payload, packet, shared secret) object where: - * - payload is the per-hop payload for this node - * - packet is the next packet, to be forwarded using the info that is given in payload (channel id for now) - * - shared secret is the secret we share with the node that sent the packet. We need it to propagate failure - * messages upstream. + * @param privateKey this node's private key. + * @param associatedData associated data. + * @param packet packet received by this node. + * @return a DecryptedPacket(payload, packet, shared secret) object where: + * - payload is the per-hop payload for this node. + * - packet is the next packet, to be forwarded using the info that is given in the payload. + * - shared secret is the secret we share with the node that sent the packet. We need it to propagate + * failure messages upstream. + * or a BadOnion error containing the hash of the invalid onion. */ - def parsePacket(privateKey: PrivateKey, associatedData: ByteVector, rawPacket: ByteVector): Try[ParsedPacket] = Try { - val packet = read(rawPacket) - val sharedSecret = computeSharedSecret(PublicKey(packet.publicKey), privateKey) - val mu = generateKey("mu", sharedSecret) - val check = mac(mu, packet.onionPayload ++ associatedData) - require(check == packet.hmac, "invalid header mac") - - val rho = generateKey("rho", sharedSecret) - // Since we don't know the length of the hop payload (we will learn it once we decode the first byte), - // we have to pessimistically generate a long cipher stream. - val stream = generateStream(rho, 2 * PayloadLength) - val bin = (packet.onionPayload ++ ByteVector.fill(PayloadLength)(0)) xor stream - - val perHopPayloadLength = currentHopLength(bin) - val perHopPayload = bin.take(perHopPayloadLength - MacLength) - - val hmac = ByteVector32(bin.slice(perHopPayloadLength - MacLength, perHopPayloadLength)) - val nextOnionPayload = bin.drop(perHopPayloadLength).take(PayloadLength) - val nextPubKey = blind(PublicKey(packet.publicKey), computeBlindingFactor(PublicKey(packet.publicKey), sharedSecret)) - - ParsedPacket(perHopPayload, Packet(Version, nextPubKey.value, hmac, nextOnionPayload), sharedSecret) - } - - @tailrec - private def extractSharedSecrets(packet: ByteVector, privateKey: PrivateKey, associatedData: ByteVector32, acc: Seq[ByteVector32] = Nil): Try[Seq[ByteVector32]] = { - parsePacket(privateKey, associatedData, packet) match { - case Success(ParsedPacket(_, nextPacket, sharedSecret)) if nextPacket.isLastPacket => Success(acc :+ sharedSecret) - case Success(ParsedPacket(_, nextPacket, sharedSecret)) => extractSharedSecrets(nextPacket.serialize, privateKey, associatedData, acc :+ sharedSecret) - case Failure(t) => Failure(t) + def peel(privateKey: PrivateKey, associatedData: ByteVector, packet: wire.OnionPacket): Either[wire.BadOnion, DecryptedPacket] = packet.version match { + case 0 => Try(PublicKey(packet.publicKey, checkValid = true)) match { + case Success(packetEphKey) => + val sharedSecret = computeSharedSecret(packetEphKey, privateKey) + val mu = generateKey("mu", sharedSecret) + val check = mac(mu, packet.payload ++ associatedData) + if (check == packet.hmac) { + val rho = generateKey("rho", sharedSecret) + // Since we don't know the length of the per-hop payload (we will learn it once we decode the first bytes), + // we have to pessimistically generate a long cipher stream. + val stream = generateStream(rho, 2 * PayloadLength) + val bin = (packet.payload ++ ByteVector.fill(PayloadLength)(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(PayloadLength) + val nextPubKey = blind(packetEphKey, computeBlindingFactor(packetEphKey, sharedSecret)) + + Right(DecryptedPacket(perHopPayload, wire.OnionPacket(Version, nextPubKey.value, nextOnionPayload, hmac), sharedSecret)) + } else { + Left(wire.InvalidOnionHmac(hash(packet))) + } + case Failure(_) => Left(wire.InvalidOnionKey(hash(packet))) } + case _ => Left(wire.InvalidOnionVersion(hash(packet))) } /** - * Compute the next packet from the current packet and node parameters. + * Wrap the given packet in an additional layer of onion encryption, adding an encrypted payload for a specific + * node. + * * Packets are constructed in reverse order: - * - you first build the last packet - * - then you call makeNextPacket(...) until you've built the final onion packet that will be sent to the first node - * in the route + * - you first create the packet for the final recipient + * - then you call wrap(...) until you've built the final onion packet that will be sent to the first node in the + * route * - * @param payload payload for this packet - * @param associatedData associated data - * @param ephemeralPublicKey ephemeral key for this packet - * @param sharedSecret shared secret - * @param packet current packet (1 + all zeroes if this is the last packet) - * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet - * @return the next packet + * @param payload per-hop payload for the target node. + * @param associatedData associated data. + * @param ephemeralPublicKey ephemeral key shared with the target node. + * @param sharedSecret shared secret with this hop. + * @param packet current packet (None if the packet hasn't been initialized). + * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet. + * @return the next packet. */ - private def makeNextPacket(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: ByteVector, sharedSecret: ByteVector32, packet: Packet, onionPayloadFiller: ByteVector = ByteVector.empty): Packet = { + def wrap(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Option[wire.OnionPacket], onionPayloadFiller: ByteVector = ByteVector.empty): wire.OnionPacket = { require(payload.length <= PayloadLength - MacLength, s"packet payload cannot exceed ${PayloadLength - MacLength} bytes") + val (currentMac, currentPayload): (ByteVector32, ByteVector) = packet match { + // Packet construction starts with an empty mac and payload. + case None => (ByteVector32.Zeroes, ByteVector.fill(PayloadLength)(0)) + case Some(p) => (p.hmac, p.payload) + } + val nextOnionPayload = { - val onionPayload1 = payload ++ packet.hmac ++ packet.onionPayload.dropRight(payload.length + MacLength) + val onionPayload1 = payload ++ currentMac ++ currentPayload.dropRight(payload.length + MacLength) val onionPayload2 = onionPayload1 xor generateStream(generateKey("rho", sharedSecret), PayloadLength) onionPayload2.dropRight(onionPayloadFiller.length) ++ onionPayloadFiller } val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload ++ associatedData) - val nextPacket = Packet(Version, ephemeralPublicKey, nextHmac, nextOnionPayload) + val nextPacket = wire.OnionPacket(Version, ephemeralPublicKey.value, nextOnionPayload, nextHmac) nextPacket } /** - * Build an encrypted onion packet that contains payloads for all nodes in the list + * Create an encrypted onion packet that contains payloads for all nodes in the list. * - * @param sessionKey session key - * @param publicKeys node public keys (one per node) - * @param payloads payloads (one per node) - * @param associatedData associated data - * @return an OnionPacket(onion packet, 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 error messages if needed + * @param sessionKey session key. + * @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 error messages if needed. */ - def makePacket(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { + def create(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = generateFiller("rho", sharedsecrets.dropRight(1), payloads.dropRight(1)) - val lastPacket = makeNextPacket(payloads.last, associatedData, ephemeralPublicKeys.last.value, sharedsecrets.last, EMPTY_PACKET, filler) + val lastPacket = wrap(payloads.last, associatedData, ephemeralPublicKeys.last, sharedsecrets.last, None, filler) @tailrec - def loop(hopPayloads: Seq[ByteVector], ephKeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: Packet): Packet = { + def loop(hopPayloads: Seq[ByteVector], ephKeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: wire.OnionPacket): wire.OnionPacket = { if (hopPayloads.isEmpty) packet else { - val nextPacket = makeNextPacket(hopPayloads.last, associatedData, ephKeys.last.value, sharedSecrets.last, packet) + val nextPacket = wrap(hopPayloads.last, associatedData, ephKeys.last, sharedSecrets.last, Some(packet)) loop(hopPayloads.dropRight(1), ephKeys.dropRight(1), sharedSecrets.dropRight(1), nextPacket) } } @@ -317,6 +286,12 @@ object Sphinx extends Logging { PacketAndSecrets(packet, sharedsecrets.zip(publicKeys)) } + /** + * When an invalid onion is received, its hash should be included in the failure message. + */ + def hash(onion: wire.OnionPacket): ByteVector32 = + Crypto.sha256(wire.LightningMessageCodecs.onionPacketCodec(onion.payload.length.toInt).encode(onion).require.toByteVector) + } /** @@ -328,6 +303,9 @@ object Sphinx extends Logging { } + // TODO: + // * Use scodec for error package (and clean-up existing stuff / verify spec conformance) + /** * A properly decoded error from a node in the route. * It has the following format: @@ -352,11 +330,11 @@ object Sphinx extends Logging { * Note that malicious intermediate hops may drop the packet or alter it (which breaks the mac). * * @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC - * was created or forwarded: see makePacket() and makeNextPacket() - * @param failure failure message - * @return an error packet that can be sent to the destination node + * was created or forwarded: see makePacket() and makeNextPacket(). + * @param failure failure message. + * @return an error packet that can be sent to the destination node. */ - def createPacket(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { + def create(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { val message: ByteVector = FailureMessageCodecs.failureMessageCodec.encode(failure).require.toByteVector require(message.length <= MaxPayloadLength, s"error message length is ${message.length}, it must be less than $MaxPayloadLength") val um = generateKey("um", sharedSecret) @@ -365,14 +343,14 @@ object Sphinx extends Logging { logger.debug(s"um key: $um") logger.debug(s"error payload: ${payload.toHex}") logger.debug(s"raw error packet: ${(mac(um, payload) ++ payload).toHex}") - forwardPacket(mac(um, payload) ++ payload, sharedSecret) + wrap(mac(um, payload) ++ payload, sharedSecret) } /** * Extract the failure message from an error packet. * - * @param packet error packet - * @return the failure message that is embedded in the error packet + * @param packet error packet. + * @return the failure message that is embedded in the error packet. */ private def extractFailureMessage(packet: ByteVector): FailureMessage = { require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") @@ -383,13 +361,13 @@ object Sphinx extends Logging { } /** - * Forward an error packet to the previous hop. + * Wrap the given packet in an additional layer of onion encryption for the previous hop. * - * @param packet error packet - * @param sharedSecret destination node's shared secret - * @return an obfuscated error packet that can be sent to the destination node + * @param packet error packet. + * @param sharedSecret destination node's shared secret. + * @return an encrypted error packet that can be sent to the destination node. */ - def forwardPacket(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = { + def wrap(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = { require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") val key = generateKey("ammag", sharedSecret) val stream = generateStream(key, PacketLength) @@ -402,9 +380,9 @@ object Sphinx extends Logging { * Check the mac of an error packet. * Note that malicious nodes in the route may have altered the packet, thus breaking the mac. * - * @param sharedSecret this node's shared secret - * @param packet error packet - * @return true if the packet's mac is valid, which means that it has been properly de-obfuscated + * @param sharedSecret this node's shared secret. + * @param packet error packet. + * @return true if the packet's mac is valid, which means that it has been properly decrypted. */ private def checkMac(sharedSecret: ByteVector32, packet: ByteVector): Boolean = { val (packetMac, payload) = packet.splitAt(MacLength) @@ -413,21 +391,23 @@ object Sphinx extends Logging { } /** - * Parse and de-obfuscate an error packet. Node shared secrets are applied until the packet's MAC becomes valid, - * which means that it was sent by the corresponding node. + * Decrypt an error packet. Node shared secrets are applied until the packet's MAC becomes valid, which means that + * it was sent by the corresponding node. + * Note that malicious nodes in the route may have altered the packet, triggering a decryption failure. * - * @param packet error packet - * @param sharedSecrets nodes shared secrets - * @return Success(secret, failure message) if the origin of the packet could be identified and the packet de-obfuscated, Failure otherwise + * @param packet error packet. + * @param sharedSecrets nodes shared secrets. + * @return Success(secret, failure message) if the origin of the packet could be identified and the packet + * decrypted, Failure otherwise. */ - def parsePacket(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[ErrorPacket] = Try { + def decrypt(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[ErrorPacket] = Try { require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") @tailrec def loop(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): ErrorPacket = sharedSecrets match { case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets") case (secret, pubkey) :: tail => - val packet1 = forwardPacket(packet, secret) + val packet1 = wrap(packet, secret) if (checkMac(secret, packet1)) ErrorPacket(pubkey, extractFailureMessage(packet1)) else loop(packet1, tail) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 5ba608af54..54f1f10938 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -174,8 +174,8 @@ object Switchboard extends Logging { .flatMap(_.commitments.remoteCommit.spec.htlcs) .filter(_.direction == OUT) .map(_.add) - .map(Relayer.tryParsePacket(_, privateKey)) - .collect { case Success(RelayPayload(add, _, _)) => add } // we only consider htlcs that are relayed, not the ones for which we are the final node + .map(Relayer.tryDecryptPacket(_, privateKey)) + .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. val relayed_out = channels diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 9d633e9963..9613d335bf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -85,7 +85,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis stop(FSM.Normal) case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => - Sphinx.ErrorPacket.parsePacket(fail.reason, sharedSecrets) match { + Sphinx.ErrorPacket.decrypt(fail.reason, sharedSecrets) match { case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") @@ -241,7 +241,7 @@ object PaymentLifecycle { case Attempt.Successful(bitVector) => bitVector.toByteVector case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause") } - Sphinx.PaymentPacket.makePacket(sessionKey, nodes, payloadsbin, associatedData) + Sphinx.PaymentPacket.create(sessionKey, nodes, payloadsbin, associatedData) } /** @@ -266,7 +266,7 @@ object PaymentLifecycle { val nodes = hops.map(_.nextNodeId) // BOLT 2 requires that associatedData == paymentHash val onion = buildOnion(nodes, payloads, paymentHash) - CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion.packet.serialize, upstream = Left(id), commit = true) -> onion.sharedSecrets + CMD_ADD_HTLC(firstAmountMsat, paymentHash, firstExpiry, onion.packet, upstream = Left(id), commit = true) -> onion.sharedSecrets } /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 4143213b18..0f639bdc94 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -21,7 +21,7 @@ import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props, Status} import akka.event.LoggingAdapter import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.OutgoingPaymentStatus @@ -33,7 +33,6 @@ import scodec.bits.BitVector import scodec.{Attempt, DecodeResult} import scala.collection.mutable -import scala.util.{Failure, Success, Try} // @formatter:off @@ -74,13 +73,13 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR def main(channelUpdates: Map[ShortChannelId, OutgoingChannel], node2channels: mutable.HashMap[PublicKey, mutable.Set[ShortChannelId]] with mutable.MultiMap[PublicKey, ShortChannelId]): Receive = { case GetUsableBalances => sender ! channelUpdates.values - .filter(o => Announcements.isEnabled(o.channelUpdate.channelFlags)) - .map(o => UsableBalances( - remoteNodeId = o.nextNodeId, - shortChannelId = o.channelUpdate.shortChannelId, - canSendMsat = o.commitments.availableBalanceForSendMsat, - canReceiveMsat = o.commitments.availableBalanceForReceiveMsat, - isPublic = o.commitments.announceChannel)) + .filter(o => Announcements.isEnabled(o.channelUpdate.channelFlags)) + .map(o => UsableBalances( + remoteNodeId = o.nextNodeId, + shortChannelId = o.channelUpdate.shortChannelId, + canSendMsat = o.commitments.availableBalanceForSendMsat, + canReceiveMsat = o.commitments.availableBalanceForReceiveMsat, + isPublic = o.commitments.announceChannel)) case LocalChannelUpdate(_, channelId, shortChannelId, remoteNodeId, _, channelUpdate, commitments) => log.debug(s"updating local channel info for channelId=$channelId shortChannelId=$shortChannelId remoteNodeId=$remoteNodeId channelUpdate={} commitments={}", channelUpdate, commitments) @@ -100,8 +99,8 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardAdd(add, previousFailures) => log.debug(s"received forwarding request for htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId}") - tryParsePacket(add, nodeParams.privateKey) match { - case Success(p: FinalPayload) => + tryDecryptPacket(add, nodeParams.privateKey) match { + case Right(p: FinalPayload) => handleFinal(p) match { case Left(cmdFail) => log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} reason=${cmdFail.reason}") @@ -110,7 +109,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR log.debug(s"forwarding htlc #${add.id} paymentHash=${add.paymentHash} to payment-handler") paymentHandler forward addHtlc } - case Success(r: RelayPayload) => + case Right(r: RelayPayload) => handleRelay(r, channelUpdates, node2channels, previousFailures, nodeParams.chainHash) match { case RelayFailure(cmdFail) => log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} to shortChannelId=${r.payload.shortChannelId} reason=${cmdFail.reason}") @@ -119,9 +118,9 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR log.info(s"forwarding htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} to shortChannelId=$selectedShortChannelId") register ! Register.ForwardShortId(selectedShortChannelId, cmdAdd) } - case Failure(t) => - log.warning(s"couldn't parse onion: reason=${t.getMessage}") - val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, Crypto.sha256(add.onionRoutingPacket), failureCode = FailureMessageCodecs.BADONION, commit = true) + case Left(badOnion) => + log.warning(s"couldn't parse onion: reason=${badOnion.message}") + val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.failureCode, commit = true) log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} reason=malformed onionHash=${cmdFail.onionHash} failureCode=${cmdFail.failureCode}") commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) } @@ -215,35 +214,33 @@ object Relayer { // @formatter:off sealed trait NextPayload case class FinalPayload(add: UpdateAddHtlc, payload: PerHopPayload) extends NextPayload - case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: Sphinx.Packet) extends NextPayload { + case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: OnionPacket) extends NextPayload { val relayFeeMsat: Long = add.amountMsat - payload.amtToForward val expiryDelta: Long = add.cltvExpiry - payload.outgoingCltvValue } // @formatter:on /** - * Parse and decode the onion of a received htlc, and find out if the payment is to be relayed, + * Decrypt the onion of a received htlc, and find out if the payment is to be relayed, * or if our node is the last one in the route * * @param add incoming htlc * @param privateKey this node's private key * @return the payload for the next hop */ - def tryParsePacket(add: UpdateAddHtlc, privateKey: PrivateKey): Try[NextPayload] = - Sphinx - .PaymentPacket - .parsePacket(privateKey, add.paymentHash, add.onionRoutingPacket) - .flatMap { - case Sphinx.ParsedPacket(payload, nextPacket, _) => - LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(payload)) match { - case Attempt.Successful(DecodeResult(perHopPayload, _)) if nextPacket.isLastPacket => - Success(FinalPayload(add, perHopPayload)) - case Attempt.Successful(DecodeResult(perHopPayload, _)) => - Success(RelayPayload(add, perHopPayload, nextPacket)) - case Attempt.Failure(cause) => - Failure(new RuntimeException(cause.messageWithContext)) - } - } + def tryDecryptPacket(add: UpdateAddHtlc, privateKey: PrivateKey): Either[BadOnion, NextPayload] = + Sphinx.PaymentPacket.peel(privateKey, add.paymentHash, add.onionRoutingPacket) match { + case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => + LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(payload)) match { + case Attempt.Successful(DecodeResult(perHopPayload, _)) if p.isLastPacket => + Right(FinalPayload(add, perHopPayload)) + case Attempt.Successful(DecodeResult(perHopPayload, _)) => + Right(RelayPayload(add, perHopPayload, nextPacket)) + case Attempt.Failure(_) => + Left(InvalidOnionUnknown(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) + } + case Left(badOnion) => Left(badOnion) + } /** * Handle an incoming htlc when we are the last node @@ -371,7 +368,7 @@ object Relayer { case Some(channelUpdate) if relayPayload.relayFeeMsat < nodeFee(channelUpdate.feeBaseMsat, channelUpdate.feeProportionalMillionths, payload.amtToForward) => RelayFailure(CMD_FAIL_HTLC(add.id, Right(FeeInsufficient(add.amountMsat, channelUpdate)), commit = true)) case Some(channelUpdate) => - RelaySuccess(channelUpdate.shortChannelId, CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket.serialize, upstream = Right(add), commit = true, previousFailures = previousFailures)) + RelaySuccess(channelUpdate.shortChannelId, CMD_ADD_HTLC(payload.amtToForward, add.paymentHash, payload.outgoingCltvValue, nextPacket, upstream = Right(add), commit = true, previousFailures = previousFailures)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index fc1233283b..9e6bd18604 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -29,7 +29,8 @@ import scodec.Attempt // @formatter:off sealed trait FailureMessage { def message: String } -sealed trait BadOnion extends FailureMessage { def onionHash: ByteVector32 } +sealed trait FailureCode { def failureCode: Int } +sealed trait BadOnion extends FailureMessage with FailureCode { def onionHash: ByteVector32 } sealed trait Perm extends FailureMessage sealed trait Node extends FailureMessage sealed trait Update extends FailureMessage { def update: ChannelUpdate } @@ -38,9 +39,22 @@ case object InvalidRealm extends Perm { def message = "realm was not understood case object TemporaryNodeFailure extends Node { def message = "general temporary failure of the processing node" } case object PermanentNodeFailure extends Perm with Node { def message = "general permanent failure of the processing node" } case object RequiredNodeFeatureMissing extends Perm with Node { def message = "processing node requires features that are missing from this onion" } -case class InvalidOnionVersion(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion version was not understood by the processing node" } -case class InvalidOnionHmac(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion HMAC was incorrect when it reached the processing node" } -case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { def message = "ephemeral key was unparsable by the processing node" } +case class InvalidOnionVersion(onionHash: ByteVector32) extends BadOnion with Perm { + def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 4 + def message = "onion version was not understood by the processing node" +} +case class InvalidOnionHmac(onionHash: ByteVector32) extends BadOnion with Perm { + def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5 + def message = "onion HMAC was incorrect when it reached the processing node" +} +case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { + def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 6 + def message = "ephemeral key was unparsable by the processing node" +} +case class InvalidOnionUnknown(onionHash: ByteVector32) extends BadOnion with Perm { + def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM + def message = "onion per-hop payload unknown error" +} case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel ${update.shortChannelId} is currently unavailable" } case object PermanentChannelFailure extends Perm { def message = "channel is permanently unavailable" } case object RequiredChannelFeatureMissing extends Perm { def message = "channel requires features not present in the onion" } @@ -66,7 +80,7 @@ object FailureMessageCodecs { val channelUpdateCodecWithType = LightningMessageCodecs.lightningMessageCodec.narrow[ChannelUpdate](f => Attempt.successful(f.asInstanceOf[ChannelUpdate]), g => g) - // NB: for historical reasons some implementations were including/ommitting the message type (258 for ChannelUpdate) + // NB: for historical reasons some implementations were including/omitting the message type (258 for ChannelUpdate) // this codec supports both versions for decoding, and will encode with the message type val channelUpdateWithLengthCodec = variableSizeBytes(uint16, choice(channelUpdateCodecWithType, channelUpdateCodec)) @@ -75,6 +89,7 @@ object FailureMessageCodecs { .typecase(NODE | 2, provide(TemporaryNodeFailure)) .typecase(PERM | 2, provide(PermanentNodeFailure)) .typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing)) + .typecase(BADONION | PERM, sha256.as[InvalidOnionUnknown]) .typecase(BADONION | PERM | 4, sha256.as[InvalidOnionVersion]) .typecase(BADONION | PERM | 5, sha256.as[InvalidOnionHmac]) .typecase(BADONION | PERM | 6, sha256.as[InvalidOnionKey]) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index fe6cd28024..59c7be428c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -36,6 +36,14 @@ import scodec.Codec */ object LightningMessageCodecs { + def onionPacketCodec(payloadLength: Int): Codec[OnionPacket] = ( + ("version" | uint8) :: + ("publicKey" | bytes(33)) :: + ("onionPayload" | bytes(payloadLength)) :: + ("hmac" | bytes32)).as[OnionPacket] + + val paymentOnionPacketCodec: Codec[OnionPacket] = onionPacketCodec(Sphinx.PaymentPacket.PayloadLength) + val initCodec: Codec[Init] = ( ("globalFeatures" | varsizebinarydata) :: ("localFeatures" | varsizebinarydata)).as[Init] @@ -123,7 +131,7 @@ object LightningMessageCodecs { ("amountMsat" | uint64overflow) :: ("paymentHash" | bytes32) :: ("expiry" | uint32) :: - ("onionRoutingPacket" | bytes(Sphinx.PaymentPacket.PacketLength))).as[UpdateAddHtlc] + ("onionRoutingPacket" | paymentOnionPacketCodec)).as[UpdateAddHtlc] val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = ( ("channelId" | bytes32) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 0e4aef640a..5df611f1c7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -123,7 +123,7 @@ case class UpdateAddHtlc(channelId: ByteVector32, amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, - onionRoutingPacket: ByteVector) extends HtlcMessage with UpdateMessage with HasChannelId + onionRoutingPacket: OnionPacket) extends HtlcMessage with UpdateMessage with HasChannelId case class UpdateFulfillHtlc(channelId: ByteVector32, id: Long, @@ -225,6 +225,11 @@ case class ChannelUpdate(signature: ByteVector64, require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags") } +case class OnionPacket(version: Int, + publicKey: ByteVector, + payload: ByteVector, + hmac: ByteVector32) + case class PerHopPayload(shortChannelId: ShortChannelId, amtToForward: Long, outgoingCltvValue: Long) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 79770b5833..5fcc228451 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -23,7 +23,6 @@ import fr.acinq.bitcoin.{Block, ByteVector32, Script} import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.crypto.LocalKeyManager import fr.acinq.eclair.db._ -import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.router.RouterConf import fr.acinq.eclair.wire.{Color, NodeAddress} @@ -34,15 +33,16 @@ import scala.concurrent.duration._ * Created by PM on 26/04/2016. */ object TestConstants { + val fundingSatoshis = 1000000L val pushMsat = 200000000L val feeratePerKw = 10000L + val emptyOnionPacket = wire.OnionPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes) def sqliteInMemory() = DriverManager.getConnection("jdbc:sqlite::memory:") def inMemoryDb(connection: Connection = sqliteInMemory()): Databases = Databases.databaseByConnections(connection, connection, connection) - object Alice { val seed = ByteVector32(ByteVector.fill(32)(1)) val keyManager = new LocalKeyManager(seed, Block.RegtestGenesisBlock.hash) @@ -69,7 +69,7 @@ object TestConstants { feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, - db = inMemoryDb(sqliteInMemory), + db = inMemoryDb(sqliteInMemory()), revocationTimeout = 20 seconds, pingInterval = 30 seconds, pingTimeout = 10 seconds, @@ -135,7 +135,7 @@ object TestConstants { feeProportionalMillionth = 10, reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, - db = inMemoryDb(sqliteInMemory), + db = inMemoryDb(sqliteInMemory()), revocationTimeout = 20 seconds, pingInterval = 30 seconds, pingTimeout = 10 seconds, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala index 18aff515ef..9ff1329b8e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/ThroughputSpec.scala @@ -51,7 +51,7 @@ class ThroughputSpec extends FunSuite { case ('add, tgt: ActorRef) => val r = randomBytes32 val h = Crypto.sha256(r) - tgt ! CMD_ADD_HTLC(1, h, 1, upstream = Left(UUID.randomUUID())) + tgt ! CMD_ADD_HTLC(1, h, 1, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) context.become(run(h2r + (h -> r))) case ('sig, tgt: ActorRef) => tgt ! CMD_SIGN diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala index b5d5aabba4..f2cf9e0940 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/StateTestsHelperMethods.scala @@ -24,20 +24,16 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.PaymentLifecycle import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire._ import fr.acinq.eclair.{Globals, NodeParams, TestConstants, randomBytes32} -import scodec.bits.ByteVector /** * Created by PM on 23/08/2016. */ trait StateTestsHelperMethods extends TestKitBase { - def defaultOnion: ByteVector = ByteVector.fill(Sphinx.PaymentPacket.PacketLength)(0) - case class SetupFixture(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala index b37eb9418b..7ce27fffb6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala @@ -27,6 +27,7 @@ import fr.acinq.eclair.TestConstants.{Alice, Bob} import fr.acinq.eclair.UInt64.Conversions._ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw +import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh, Reconnected, RevocationTimeout} import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods @@ -66,7 +67,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val add = CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -84,7 +85,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val h = randomBytes32 for (i <- 0 until 10) { - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] assert(htlc.id == i && htlc.paymentHash == h) @@ -96,8 +97,8 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val sender = TestProbe() val h = randomBytes32 - val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = ByteVector.fill(1254)(0)) - val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, upstream = Right(originHtlc)) + val originHtlc = UpdateAddHtlc(channelId = randomBytes32, id = 5656, amountMsat = 50000000, cltvExpiry = 400144, paymentHash = h, onionRoutingPacket = TestConstants.emptyOnionPacket) + val cmd = CMD_ADD_HTLC(originHtlc.amountMsat - 10000, h, originHtlc.cltvExpiry - 7, TestConstants.emptyOnionPacket, upstream = Right(originHtlc)) sender.send(alice, cmd) sender.expectMsg("ok") val htlc = alice2bob.expectMsgType[UpdateAddHtlc] @@ -116,7 +117,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val currentBlockCount = Globals.blockCount.get val expiryTooSmall = currentBlockCount + 3 - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooSmall, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, randomBytes32, expiryTooSmall, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ExpiryTooSmall(channelId(alice), currentBlockCount + Channel.MIN_CLTV_EXPIRY, expiryTooSmall, currentBlockCount) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -129,7 +130,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] val currentBlockCount = Globals.blockCount.get val expiryTooBig = currentBlockCount + Channel.MAX_CLTV_EXPIRY + 1 - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = expiryTooBig, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, randomBytes32, expiryTooBig, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ExpiryTooBig(channelId(alice), maximum = currentBlockCount + Channel.MAX_CLTV_EXPIRY, actual = expiryTooBig, blockCount = currentBlockCount) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -140,7 +141,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(50, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(50, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = HtlcValueTooSmall(channelId(alice), 1000, 50) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -151,7 +152,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(Int.MaxValue, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = Int.MaxValue, missingSatoshis = 1376443, reserveSatoshis = 20000, feesSatoshis = 8960) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -162,16 +163,16 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(500000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(200000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(200000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(67600000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(67600000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 1000000, missingSatoshis = 1000, reserveSatoshis = 20000, feesSatoshis = 12400) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -182,13 +183,13 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] - val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = InsufficientFunds(channelId(alice), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -199,7 +200,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(151000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(bob, add) val error = HtlcValueTooHighInFlight(channelId(bob), maximum = 150000000, actual = 151000000) sender.expectMsg(Failure(AddHtlcFailed(channelId(bob), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -212,11 +213,11 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - sender.send(alice, CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] } - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = TooManyAcceptedHtlcs(channelId(alice), maximum = 30) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -227,7 +228,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add1 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -235,7 +236,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.expectMsg("ok") alice2bob.expectMsgType[CommitSig] // this is over channel-capacity - val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add2 = CMD_ADD_HTLC(TestConstants.fundingSatoshis * 2 / 3 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add2) val error = InsufficientFunds(channelId(alice), add2.amountMsat, 564012, 20000, 10680) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add2.paymentHash, error, Local(add2.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add2)))) @@ -249,10 +250,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { sender.send(alice, CMD_CLOSE(None)) sender.expectMsg("ok") alice2bob.expectMsgType[Shutdown] - awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && !alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isDefined) + awaitCond(alice.stateData.asInstanceOf[DATA_NORMAL].localShutdown.isDefined && alice.stateData.asInstanceOf[DATA_NORMAL].remoteShutdown.isEmpty) // actual test starts here - val add = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = NoMoreHtlcsClosingInProgress(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), Some(initialState.channelUpdate), Some(add)))) @@ -264,14 +265,14 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] // let's make alice send an htlc - val add1 = CMD_ADD_HTLC(500000000, randomBytes32, cltvExpiry = 400144, upstream = Left(UUID.randomUUID())) + val add1 = CMD_ADD_HTLC(500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add1) sender.expectMsg("ok") // at the same time bob initiates a closing sender.send(bob, CMD_CLOSE(None)) sender.expectMsg("ok") // this command will be received by alice right after having received the shutdown - val add2 = CMD_ADD_HTLC(100000000, randomBytes32, cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) + val add2 = CMD_ADD_HTLC(100000000, randomBytes32, 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) // messages cross alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -285,7 +286,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc") { f => import f._ val initialData = bob.stateData.asInstanceOf[DATA_NORMAL] - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000, randomBytes32, 400144, defaultOnion) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150000, randomBytes32, 400144, TestConstants.emptyOnionPacket) bob ! htlc awaitCond(bob.stateData == initialData.copy(commitments = initialData.commitments.copy(remoteChanges = initialData.commitments.remoteChanges.copy(proposed = initialData.commitments.remoteChanges.proposed :+ htlc), remoteNextHtlcId = 1))) // bob won't forward the add before it is cross-signed @@ -295,7 +296,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (unexpected id)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000, randomBytes32, 400144, defaultOnion) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 42, 150000, randomBytes32, 400144, TestConstants.emptyOnionPacket) bob ! htlc.copy(id = 0) bob ! htlc.copy(id = 1) bob ! htlc.copy(id = 2) @@ -312,7 +313,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (value too small)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150, randomBytes32, cltvExpiry = 400144, defaultOnion) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, 150, randomBytes32, cltvExpiry = 400144, TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === HtlcValueTooSmall(channelId(bob), minimum = 1000, actual = 150).getMessage) @@ -327,7 +328,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Long.MaxValue, randomBytes32, 400144, defaultOnion) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Long.MaxValue, randomBytes32, 400144, TestConstants.emptyOnionPacket) alice2bob.forward(bob, htlc) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = Long.MaxValue, missingSatoshis = 9223372036083735L, reserveSatoshis = 20000, feesSatoshis = 8960).getMessage) @@ -342,10 +343,10 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 1/2)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 400000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 200000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 167600000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 3, 10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = 10000000, missingSatoshis = 11720, reserveSatoshis = 20000, feesSatoshis = 14120).getMessage) awaitCond(bob.stateName == CLOSING) @@ -359,9 +360,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (insufficient funds w/ pending htlcs 2/2)") { f => import f._ val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000, randomBytes32, 400144, defaultOnion)) - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 0, 300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 1, 300000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 2, 500000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === InsufficientFunds(channelId(bob), amountMsat = 500000000, missingSatoshis = 332400, reserveSatoshis = 20000, feesSatoshis = 12400).getMessage) awaitCond(bob.stateName == CLOSING) @@ -375,7 +376,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv UpdateAddHtlc (over max inflight htlc value)") { f => import f._ val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(alice, UpdateAddHtlc(ByteVector32.Zeroes, 0, 151000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) === HtlcValueTooHighInFlight(channelId(alice), maximum = 150000000, actual = 151000000).getMessage) awaitCond(alice.stateName == CLOSING) @@ -391,9 +392,9 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val tx = bob.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx // Bob accepts a maximum of 30 htlcs for (i <- 0 until 30) { - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, i, 1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) } - alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000, randomBytes32, 400144, defaultOnion)) + alice2bob.forward(bob, UpdateAddHtlc(ByteVector32.Zeroes, 30, 1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket)) val error = bob2alice.expectMsgType[Error] assert(new String(error.data.toArray) === TooManyAcceptedHtlcs(channelId(bob), maximum = 30).getMessage) awaitCond(bob.stateName == CLOSING) @@ -418,7 +419,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (two identical htlcs in each direction)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] @@ -465,19 +466,19 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { assert(a2b_2 > aliceMinOffer && a2b_2 > bobMinReceive) assert(b2a_1 > aliceMinReceive && b2a_1 > bobMinOffer) assert(b2a_2 < aliceMinReceive && b2a_2 > bobMinOffer) - sender.send(alice, CMD_ADD_HTLC(a2b_1 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(a2b_1 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(a2b_2 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(bob, CMD_ADD_HTLC(b2a_1 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) - sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(bob, CMD_ADD_HTLC(b2a_2 * 1000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") bob2alice.expectMsgType[UpdateAddHtlc] bob2alice.forward(alice) @@ -497,7 +498,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_SIGN (htlcs with same pubkeyScript but different amounts)") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) val epsilons = List(3, 1, 5, 7, 6) // unordered on purpose val htlcCount = epsilons.size for (i <- epsilons) { @@ -694,12 +695,12 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val r = randomBytes32 val h = Crypto.sha256(r) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc1 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) - sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(50000000, h, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) sender.expectMsg("ok") val htlc2 = alice2bob.expectMsgType[UpdateAddHtlc] alice2bob.forward(bob) @@ -965,7 +966,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { val sender = TestProbe() val (_, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice) crossSign(alice, bob, alice2bob, bob2alice) - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) sender.expectMsg("ok") val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] bob2alice.forward(alice) @@ -1177,7 +1178,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test begins val initialState = bob.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) sender.expectMsg("ok") val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] awaitCond(bob.stateData == initialState.copy( @@ -1241,7 +1242,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { crossSign(alice, bob, alice2bob, bob2alice) // Bob fails the HTLC because he cannot parse it val initialState = alice.stateData.asInstanceOf[DATA_NORMAL] - sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Crypto.sha256(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) + sender.send(bob, CMD_FAIL_MALFORMED_HTLC(htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), FailureMessageCodecs.BADONION)) sender.expectMsg("ok") val fail = bob2alice.expectMsgType[UpdateFailMalformedHtlc] bob2alice.forward(alice) @@ -1269,7 +1270,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test begins val tx = alice.stateData.asInstanceOf[DATA_NORMAL].commitments.localCommit.publishableTxs.commitTx.tx - val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Crypto.sha256(htlc.onionRoutingPacket), 42) + val fail = UpdateFailMalformedHtlc(ByteVector32.Zeroes, htlc.id, Sphinx.PaymentPacket.hash(htlc.onionRoutingPacket), 42) sender.send(alice, fail) val error = alice2bob.expectMsgType[Error] assert(new String(error.data.toArray) === InvalidFailureCode(ByteVector32.Zeroes).getMessage) @@ -1901,7 +1902,7 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // alice = 800 000 // bob = 200 000 - val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(10000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) sender.expectMsg("ok") alice2bob.expectMsgType[UpdateAddHtlc] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala index e3a2c2dfcc..011f228cdf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/OfflineStateSpec.scala @@ -61,7 +61,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, ByteVector32.Zeroes, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(1000000, ByteVector32.Zeroes, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob) @@ -138,7 +138,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { import f._ val sender = TestProbe() - sender.send(alice, CMD_ADD_HTLC(1000000, randomBytes32, 400144, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(1000000, randomBytes32, 400144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val ab_add_0 = alice2bob.expectMsgType[UpdateAddHtlc] // add ->b alice2bob.forward(bob, ab_add_0) @@ -382,7 +382,7 @@ class OfflineStateSpec extends TestkitBaseClass with StateTestsHelperMethods { channelUpdateListener.expectNoMsg(300 millis) // we attempt to send a payment - sender.send(alice, CMD_ADD_HTLC(4200, randomBytes32, 123456, upstream = Left(UUID.randomUUID()))) + sender.send(alice, CMD_ADD_HTLC(4200, randomBytes32, 123456, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID()))) val failure = sender.expectMsgType[Status.Failure] val AddHtlcFailed(_, _, ChannelUnavailable(_), _, _, _) = failure.cause diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala index dd73f294f4..8946196271 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala @@ -20,7 +20,7 @@ import java.util.UUID import akka.actor.Status.Failure import akka.testkit.TestProbe -import fr.acinq.bitcoin.Crypto.{PrivateKey} +import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, ByteVector64, Crypto, Satoshi, ScriptFlags, Transaction} import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.fee.FeeratesPerKw @@ -103,7 +103,7 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods { test("recv CMD_ADD_HTLC") { f => import f._ val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, r1, cltvExpiry = 300000, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala index 6ae3d67399..85a1b45fcc 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/g/NegotiatingStateSpec.scala @@ -30,7 +30,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.states.StateTestsHelperMethods import fr.acinq.eclair.payment.Local import fr.acinq.eclair.wire.{ClosingSigned, Error, Shutdown} -import fr.acinq.eclair.{Globals, TestkitBaseClass} +import fr.acinq.eclair.{Globals, TestConstants, TestkitBaseClass} import org.scalatest.{Outcome, Tag} import scodec.bits.ByteVector @@ -72,7 +72,7 @@ class NegotiatingStateSpec extends TestkitBaseClass with StateTestsHelperMethods import f._ alice2bob.expectMsgType[ClosingSigned] val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala index ec19d818e7..e9054c14a5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/h/ClosingStateSpec.scala @@ -278,7 +278,7 @@ class ClosingStateSpec extends TestkitBaseClass with StateTestsHelperMethods { // actual test starts here val sender = TestProbe() - val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, upstream = Left(UUID.randomUUID())) + val add = CMD_ADD_HTLC(500000000, ByteVector32(ByteVector.fill(32)(1)), cltvExpiry = 300000, onion = TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) sender.send(alice, add) val error = ChannelUnavailable(channelId(alice)) sender.expectMsg(Failure(AddHtlcFailed(channelId(alice), add.paymentHash, error, Local(add.upstream.left.get, Some(sender.ref)), None, Some(add)))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index e19a28349f..bea8bc4ec6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} +import fr.acinq.eclair.wire import fr.acinq.eclair.wire._ import org.scalatest.FunSuite import scodec.bits._ @@ -53,7 +54,7 @@ class SphinxSpec extends FunSuite { hop_blinding_factor[4] = 0xc96e00dddaf57e7edcd4fb5954be5b65b09f17cb6d20651b4e90315be5779205 hop_ephemeral_pubkey[4] = 0x03a214ebd875aab6ddfd77f22c5e7311d7f77f17a169e599f157bbcdae8bf071f4 */ - test("generate ephemeral keys and secrets") { + test("generate ephemeral keys and secrets (reference test vector)") { val (ephkeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) assert(ephkeys(0) == PublicKey(hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619")) assert(sharedsecrets(0) == ByteVector32(hex"53eb63ea8a3fec3b3cd433b85cd62a4b145e1dda09391b348c4e1cd36a03ea66")) @@ -70,7 +71,7 @@ class SphinxSpec extends FunSuite { /* filler = 0xc6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac */ - test("generate filler with fixed-size payloads") { + test("generate filler with fixed-size payloads (reference test vector)") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = PaymentPacket.generateFiller("rho", sharedsecrets.dropRight(1), referenceFixedSizePayloads.dropRight(1)) assert(filler == hex"c6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") @@ -85,15 +86,66 @@ class SphinxSpec extends FunSuite { assert(filler == hex"b77d99c935d3f32469844f7e09340a91ded147557bdd0456c369f7e449587c0f5666faab58040146db49024db88553729bce12b860391c29c1779f022ae48a9cb314ca35d73fc91addc92632bcf7ba6fd9f38e6fd30fabcedbd5407b6648073c38331ee7ab0332f41f550c180e1601f8c25809ed75b3a1e78635a2ef1b828e92c9658e76e49f995d72cf9781eec0c838901d0bdde3ac21c13b4979ac9e738a1c4d0b9741d58e777ad1aed01263ad1390d36a18a6b92f4f799dcf75edbb43b7515e8d72cb4f827a9af0e7b9338d07b1a24e0305b5535f5b851b1144bad6238b9d9482b5ba6413f1aafac3cdde5067966ed8b78f7c1c5f916a05f874d5f17a2b7d0ae75d66a5f1bb6ff932570dc5a0cf3ce04eb5d26bc55c2057af1f8326e20a7d6f0ae644f09d00fac80de60f20aceee85be41a074d3e1dda017db79d0070b99f54736396f206ee3777abd4c00a4bb95c871750409261e3b01e59a3793a9c20159aae4988c68397a1443be6370fd9614e46108291e615691729faea58537209fa668a172d066d0efff9bc77c2bd34bd77870ad79effd80140990e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a") } + test("peek at per-hop payload length") { + val testCases = Map( + 34 -> hex"01", + 41 -> hex"08", + 65 -> hex"00", + 285 -> hex"fc", + 288 -> hex"fdfd00", + 65570 -> hex"fdffff" + ) + + for ((expected, payload) <- testCases) { + assert(peekPayloadLength(payload) === expected) + } + } + + test("is last packet") { + val testCases = Seq( + // Bolt 1.0 payloads use the next packet's hmac to signal termination. + (true, DecryptedPacket(hex"00", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), + (false, DecryptedPacket(hex"00", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + // Bolt 1.1 payloads may use either the next packet's hmac or a tlv type to signal termination. + (true, DecryptedPacket(hex"0101", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), + (false, DecryptedPacket(hex"0101", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + (true, DecryptedPacket(hex"0100", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + (false, DecryptedPacket(hex"0101", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)) + ) + + for ((expected, packet) <- testCases) { + assert(packet.isLastPacket === expected) + } + } + + test("bad onion") { + val badOnions = Seq[wire.OnionPacket]( + wire.OnionPacket(1, ByteVector.fill(33)(0), ByteVector.fill(65)(1), ByteVector32.Zeroes), + wire.OnionPacket(0, ByteVector.fill(33)(0), ByteVector.fill(65)(1), ByteVector32.Zeroes), + wire.OnionPacket(0, publicKeys.head.value, ByteVector.fill(42)(1), ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")) + ) + + val expected = Seq[BadOnion]( + InvalidOnionVersion(ByteVector32(hex"2f89b15c6cb0bb256d7a71b66de0d50cd3dd806f77d1cc1a3b0d86a0becd28ce")), + InvalidOnionKey(ByteVector32(hex"d2602c65fc331d6ae728331ae50e602f35929312ca7a951dc5ce250031b6b999")), + InvalidOnionHmac(ByteVector32(hex"3c01a86e6bc51b44a2718745fbbbc71a5c5dde5f46a489da17046c9d097bb303")) + ) + + for ((packet, expected) <- badOnions zip expected) { + val Left(onionErr) = PaymentPacket.peel(privKeys.head, associatedData, packet) + assert(onionErr === expected) + } + } + test("create packet with fixed-size payloads (reference test vector)") { - val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, referenceFixedSizePayloads, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf") - - val Success(ParsedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, onion.serialize) - val Success(ParsedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) - val Success(ParsedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) - val Success(ParsedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) - val Success(ParsedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, referenceFixedSizePayloads, associatedData) + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf") + + val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) + val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) + val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, nextPacket1) + val Right(DecryptedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.peel(privKeys(3), associatedData, nextPacket2) + val Right(DecryptedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.peel(privKeys(4), associatedData, nextPacket3) assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceFixedSizePayloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) @@ -107,14 +159,14 @@ class SphinxSpec extends FunSuite { } test("create packet with variable-size payloads (reference test vector)") { - val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, referenceVariableSizePayloads, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7181924e4b6c645f2b6eecc821084ec9b16dfb9d2e7622d4c14db4fc5ecdfc07eac50f7d61ab590531cf08000178a333a347f8b4072e7d3ae44f4f309e150b49886d3f044cd6462be389c830784aba767682923c8683404aaf9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf050597c681185f336b1da63b0939aa2b7c50b21b5eb7b6ad66c81fab98a3cdf73f658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d7fca6a0e4ecc9f9de611a90da6e99c39551094c56e3196f282c5dffd9fc4b2fc12f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa337ef811d5345a9e16b60de1767b209645ba40bd1f9a5f75bc04feca9b27c5554be4fe83fac2cb83aa447a817bb85ae966c68b420063833fada375e2f515965e687a45699632902672c654d1d18d7bcbf55e8fa57f63f2da449f8e1e606e8722df081e5f193fc4179feb99ad22819afdeef211f7c54afdba92aeef0c00b7bc2b65a4813c01f907a8377585708f2d4c940a25328e585714c8ded0a9a4d7a6de1027c1cb7a0198cd3db68b58c0704dfd0cfbe624e9cd18cc0ae5d96697bb476708b9ee0403d211e64e0d5a7683a7a9a140c02f0ff1c6e67a302941b4052bdea8a63e70a3ad62c5b89c698f1fd3c7685cb49705096cad702d02d93bcb1c27a409f4c9bddec001205ca4a2740f19b50900be81c7e847f1a863deea8d35701f1355cad8db57b1d4eb2ab4e29587734785abfb46ddede71928213d7d089dfdeda052827f459f1688cc0935bd47e7bcec27427c8376dcce7e22699567c0d145f8a7db33f6758815f1f15f9f7a9760dec4f34ae095edda4c64e9735bdd029c4e32c2ee31ba47ec5e6bdb97813d52dbd15b4e0b7a2c7f790ae64104d99f38c127f0a093288fa34144adb16b8968d4fa7656fcec99de8503dd46d3b03620a71c7cd085364abd30dccf7fbda25a1cdc102600149c9af1c97aa0372cd2e1909f28ac5c686f432b310e79528c9b8b9e8f314c1e74621ce6308ad2278b81d460892e0d9dd38b7c76d58be6dfd10ae7583ee1e7ef5b3f6f78dc60af0950df1b00cc55b6d178ba2e476bea0eaeef49323b83f05804159e7aef4eed4cc60dd07be76f067dfd0bcfb0b806b69ba921336a20c43c832d0cab8fa3ddeb29e3bf07b0d98a112eb07802756235a49d44a8b82a950d84e95e01971f0e106ccb337f07384e21620e0ad39e16ed9edca123226cf55ac44f449eeb53e38a7f27d101806e4823e4efcc887414240ee6826c4a5cb1c6443ad36ebf905a435c1d9054e54173911b17b5b40f60b3d9fd5f12eac54ca1e20191f5f18544d5fd3d665e9bcef96fb44b76110aa64d9db4c86c9513cbdad546538e8aec521fbe83ceac5e74a15629f1ed0b870a1d0d1e5680b6d6100d1bdd9a2ba3be3772f71854cb12ae9b3afd87cda738dcd107fe56a15f1877450cd0e") - - val Success(ParsedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, onion.serialize) - val Success(ParsedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) - val Success(ParsedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) - val Success(ParsedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) - val Success(ParsedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, referenceVariableSizePayloads, associatedData) + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7181924e4b6c645f2b6eecc821084ec9b16dfb9d2e7622d4c14db4fc5ecdfc07eac50f7d61ab590531cf08000178a333a347f8b4072e7d3ae44f4f309e150b49886d3f044cd6462be389c830784aba767682923c8683404aaf9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf050597c681185f336b1da63b0939aa2b7c50b21b5eb7b6ad66c81fab98a3cdf73f658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d7fca6a0e4ecc9f9de611a90da6e99c39551094c56e3196f282c5dffd9fc4b2fc12f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa337ef811d5345a9e16b60de1767b209645ba40bd1f9a5f75bc04feca9b27c5554be4fe83fac2cb83aa447a817bb85ae966c68b420063833fada375e2f515965e687a45699632902672c654d1d18d7bcbf55e8fa57f63f2da449f8e1e606e8722df081e5f193fc4179feb99ad22819afdeef211f7c54afdba92aeef0c00b7bc2b65a4813c01f907a8377585708f2d4c940a25328e585714c8ded0a9a4d7a6de1027c1cb7a0198cd3db68b58c0704dfd0cfbe624e9cd18cc0ae5d96697bb476708b9ee0403d211e64e0d5a7683a7a9a140c02f0ff1c6e67a302941b4052bdea8a63e70a3ad62c5b89c698f1fd3c7685cb49705096cad702d02d93bcb1c27a409f4c9bddec001205ca4a2740f19b50900be81c7e847f1a863deea8d35701f1355cad8db57b1d4eb2ab4e29587734785abfb46ddede71928213d7d089dfdeda052827f459f1688cc0935bd47e7bcec27427c8376dcce7e22699567c0d145f8a7db33f6758815f1f15f9f7a9760dec4f34ae095edda4c64e9735bdd029c4e32c2ee31ba47ec5e6bdb97813d52dbd15b4e0b7a2c7f790ae64104d99f38c127f0a093288fa34144adb16b8968d4fa7656fcec99de8503dd46d3b03620a71c7cd085364abd30dccf7fbda25a1cdc102600149c9af1c97aa0372cd2e1909f28ac5c686f432b310e79528c9b8b9e8f314c1e74621ce6308ad2278b81d460892e0d9dd38b7c76d58be6dfd10ae7583ee1e7ef5b3f6f78dc60af0950df1b00cc55b6d178ba2e476bea0eaeef49323b83f05804159e7aef4eed4cc60dd07be76f067dfd0bcfb0b806b69ba921336a20c43c832d0cab8fa3ddeb29e3bf07b0d98a112eb07802756235a49d44a8b82a950d84e95e01971f0e106ccb337f07384e21620e0ad39e16ed9edca123226cf55ac44f449eeb53e38a7f27d101806e4823e4efcc887414240ee6826c4a5cb1c6443ad36ebf905a435c1d9054e54173911b17b5b40f60b3d9fd5f12eac54ca1e20191f5f18544d5fd3d665e9bcef96fb44b76110aa64d9db4c86c9513cbdad546538e8aec521fbe83ceac5e74a15629f1ed0b870a1d0d1e5680b6d6100d1bdd9a2ba3be3772f71854cb12ae9b3afd87cda738dcd107fe56a15f1877450cd0e") + + val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) + val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) + val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, nextPacket1) + val Right(DecryptedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.peel(privKeys(3), associatedData, nextPacket2) + val Right(DecryptedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.peel(privKeys(4), associatedData, nextPacket3) assert(Seq(payload0, payload1, payload2, payload3, payload4) == referenceVariableSizePayloads) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) @@ -127,14 +179,14 @@ class SphinxSpec extends FunSuite { } test("create packet with variable-size payloads filling the onion") { - val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, variableSizePayloadsFull, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866196ef84350c2a76fc232b5d46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6101810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bf74b2ce49922898e9353fa268086c00ae8b7f718405b72ad3829dbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf776b75ebb389bf84d0bfbf58590e510e034572a01e409c30939dc3c0b7d6a68a371019f5378bf6133fe0de00dc6f90d98ce1cbe3f165182ca37d6208da0f6bafd75ff41903ab352a1f47586eae3c6c8e437d4308766f71052b46ba2efbd87c0a781e8b3f456300fc7efbefc78ab515338666aed2070e674143c30b520b9cc1782ba8b46454db0d4ce72589cfc2eafb2db452ec98573ad08496483741de5376bfc7357fc6ea629e31236ba6ba7703014959129141a1719788ec83884f2e9151a680e2a96d2bcc67a8a2935aa11acee1f9d04812045b4ae5491220313756b5b9a0a6f867f2a95be1fab14870f04eeab694d9594620632b14ec4b424b495914f3dc587f75cd4582c113bb61e34a0fa7f79f97463be4e3c6fb99516889ed020acee419bb173d38e5ba18a00065e11fd733cf9ae46505dbb4ef70ef2f502601f4f6ee1fdb9d17435e15080e962f24760843f35bac1ac079b694ff7c347c1ed6a87f02b0758fbf00917652641cb68f584fd830b6c738e03b424ea0bc753d24306d7b691693d3286706fee7d57a939e28d77b3da47efc072436a3fd7f9c40515af8c4903764301e62b57153a5ca03ff5bb49c7dc8d3b2858100fb4aa5df7a94a271b73a76129445a3ea180d84d19029c003c164db926ed6983e5219028721a294f145e3fcc20915b8a2147efc896ee5d314e01874ea9e7bc1f386ba6b8f262942aa0193a537ffc91b1ccc9171a3c2016ecf387a3cd8b79df80a8e9412e707cb9c761a0809a84c606a779567f9f0edf685b38c98877e90d02aedd096ed841e50abf2114ce01efbff04788fb280f870eca20c7ec353d5c381903e7d08fc57695fd79c27d43e7bd603a876068d3f1c7f45af99003e5eec7e8d8c91e395320f1fc421ef3552ea033129429383304b760c8f93de342417c3223c2112a623c3514480cdfae8ec15a99abfca71b03a8396f19edc3d5000bcfb77b5544813476b1b521345f4da396db09e783870b97bc2034bd11611db30ed2514438b046f1eb7093eceddfb1e73880786cd7b540a3896eaadd0a0692e4b19439815b5f2ec855ec8ececce889442a64037e9564521fce926613b5d3074246c5a34a296ad1a18ef556d73fcd6c85ea3fdfb03b4e8e4bb0e35997fc35544d3c2a00e5e1f45dc925607d952c6a89721bd0b6f6aec03314d667166a5b8b18471403be7018b2479aaef6c7c6c554a50a98b717dff06d50be39fb36dc03e678e0a52fc615be46b223e3bee83fa0c7c47a1f29fb94f1e9eebf6c9ecf8fc79ae847df2effb60d07aba301fc536546ec4899eedb4fec9a9bed79e3a83c4b32757745778e977e485c67c0f12bbc82c0b3bb0f4df0bd13d046fed4446f54cd85bfce55ef781a80e5f63d289d08de001237928c2a4e0c8694d0c1e68cc23f2409f30009019085e831a928e7bc5b00a1f29d25482f7fd0b6dad30e6ef8edc68ddf7db404ea7d11540fc2cee74863d64af4c945457e04b7bea0a5fb8636edadb1e1d6f2630d61062b781c1821f46eddadf269ea1fada829547590081b16bc116e074cae0224a375f2d9ce16e836687c89cd285e3b40f1e59ce2caa3d1d8cf37ee4d5e3abe7ef0afd6ffeb4fd6905677b950894863c828ab8d93519566f69fa3c2129da763bf58d9c4d2837d4d9e13821258f7e7098b34218b846b5ee59284dc6791344738aac3d9a9014b764104cd606ef2c37a03fc7e") - - val Success(ParsedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, onion.serialize) - val Success(ParsedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, nextPacket0.serialize) - val Success(ParsedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, nextPacket1.serialize) - val Success(ParsedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.parsePacket(privKeys(3), associatedData, nextPacket2.serialize) - val Success(ParsedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.parsePacket(privKeys(4), associatedData, nextPacket3.serialize) + val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, variableSizePayloadsFull, associatedData) + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866196ef84350c2a76fc232b5d46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6101810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bf74b2ce49922898e9353fa268086c00ae8b7f718405b72ad3829dbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf776b75ebb389bf84d0bfbf58590e510e034572a01e409c30939dc3c0b7d6a68a371019f5378bf6133fe0de00dc6f90d98ce1cbe3f165182ca37d6208da0f6bafd75ff41903ab352a1f47586eae3c6c8e437d4308766f71052b46ba2efbd87c0a781e8b3f456300fc7efbefc78ab515338666aed2070e674143c30b520b9cc1782ba8b46454db0d4ce72589cfc2eafb2db452ec98573ad08496483741de5376bfc7357fc6ea629e31236ba6ba7703014959129141a1719788ec83884f2e9151a680e2a96d2bcc67a8a2935aa11acee1f9d04812045b4ae5491220313756b5b9a0a6f867f2a95be1fab14870f04eeab694d9594620632b14ec4b424b495914f3dc587f75cd4582c113bb61e34a0fa7f79f97463be4e3c6fb99516889ed020acee419bb173d38e5ba18a00065e11fd733cf9ae46505dbb4ef70ef2f502601f4f6ee1fdb9d17435e15080e962f24760843f35bac1ac079b694ff7c347c1ed6a87f02b0758fbf00917652641cb68f584fd830b6c738e03b424ea0bc753d24306d7b691693d3286706fee7d57a939e28d77b3da47efc072436a3fd7f9c40515af8c4903764301e62b57153a5ca03ff5bb49c7dc8d3b2858100fb4aa5df7a94a271b73a76129445a3ea180d84d19029c003c164db926ed6983e5219028721a294f145e3fcc20915b8a2147efc896ee5d314e01874ea9e7bc1f386ba6b8f262942aa0193a537ffc91b1ccc9171a3c2016ecf387a3cd8b79df80a8e9412e707cb9c761a0809a84c606a779567f9f0edf685b38c98877e90d02aedd096ed841e50abf2114ce01efbff04788fb280f870eca20c7ec353d5c381903e7d08fc57695fd79c27d43e7bd603a876068d3f1c7f45af99003e5eec7e8d8c91e395320f1fc421ef3552ea033129429383304b760c8f93de342417c3223c2112a623c3514480cdfae8ec15a99abfca71b03a8396f19edc3d5000bcfb77b5544813476b1b521345f4da396db09e783870b97bc2034bd11611db30ed2514438b046f1eb7093eceddfb1e73880786cd7b540a3896eaadd0a0692e4b19439815b5f2ec855ec8ececce889442a64037e9564521fce926613b5d3074246c5a34a296ad1a18ef556d73fcd6c85ea3fdfb03b4e8e4bb0e35997fc35544d3c2a00e5e1f45dc925607d952c6a89721bd0b6f6aec03314d667166a5b8b18471403be7018b2479aaef6c7c6c554a50a98b717dff06d50be39fb36dc03e678e0a52fc615be46b223e3bee83fa0c7c47a1f29fb94f1e9eebf6c9ecf8fc79ae847df2effb60d07aba301fc536546ec4899eedb4fec9a9bed79e3a83c4b32757745778e977e485c67c0f12bbc82c0b3bb0f4df0bd13d046fed4446f54cd85bfce55ef781a80e5f63d289d08de001237928c2a4e0c8694d0c1e68cc23f2409f30009019085e831a928e7bc5b00a1f29d25482f7fd0b6dad30e6ef8edc68ddf7db404ea7d11540fc2cee74863d64af4c945457e04b7bea0a5fb8636edadb1e1d6f2630d61062b781c1821f46eddadf269ea1fada829547590081b16bc116e074cae0224a375f2d9ce16e836687c89cd285e3b40f1e59ce2caa3d1d8cf37ee4d5e3abe7ef0afd6ffeb4fd6905677b950894863c828ab8d93519566f69fa3c2129da763bf58d9c4d2837d4d9e13821258f7e7098b34218b846b5ee59284dc6791344738aac3d9a9014b764104cd606ef2c37a03fc7e") + + val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) + val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) + val Right(DecryptedPacket(payload2, nextPacket2, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, nextPacket1) + val Right(DecryptedPacket(payload3, nextPacket3, sharedSecret3)) = PaymentPacket.peel(privKeys(3), associatedData, nextPacket2) + val Right(DecryptedPacket(payload4, nextPacket4, sharedSecret4)) = PaymentPacket.peel(privKeys(4), associatedData, nextPacket3) assert(Seq(payload0, payload1, payload2, payload3, payload4) == variableSizePayloadsFull) assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) @@ -147,10 +199,10 @@ class SphinxSpec extends FunSuite { } test("create packet with single variable-size payload filling the onion") { - val PacketAndSecrets(onion, _) = PaymentPacket.makePacket(sessionKey, publicKeys.take(1), variableSizeOneHopPayload, associatedData) - assert(onion.serialize == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661918004735c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2cc49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f580c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce42e93a4d7c803ead45fc47cf4396d284632314d789e73cf3f534126c63fe244069d9e8a7c4f98e7e530fc588e648ef4e641364981b5377542d5e7a4aaab6d35f6df7d3a9d7ca715213599ee02c4dbea4dc78860febe1d29259c64b59b3333ffdaebbaff4e7b31c27a3791f6bf848a58df7c69bb2b1852d2ad357b9919ffdae570b27dc709fba087273d3a4de9e6a6be66db647fb6a8d1a503b3f481befb96745abf5cc4a6bba0f780d5c7759b9e303a2a6b17eb05b6e660f4c474959db183e1cae060e1639227ee0bca03978a238dc4352ed764da7d4f3ed5337f6d0376dff72615beeeeaaeef79ab93e4bcbf18cd8424eb2b6ad7f33d2b4ffd5ea08372e6ed1d984152df17e04c6f73540988d7dd979e020424a163c271151a255966be7edef42167b8facca633649739bab97572b485658cde409e5d4a0f653f1a5911141634e3d2b6079b19347df66f9820755fd517092dae62fb278b0bafcc7ad682f7921b3a455e0c6369988779e26f0458b31bffd7e4e5bfb31944e80f100b2553c3b616e75be18328dc430f6618d55cd7d0962bb916d26ed4b117c46fa29e0a112c02c36020b34a96762db628fa3490828ec2079962ad816ef20ea0bca78fb2b7f7aedd4c47e375e64294d151ff03083730336dea64934003a27730cc1c7dec5049ddba8188123dd191aa71390d43a49fb792a3da7082efa6cced73f00eccea18145fbc84925349f7b552314ab8ed4c491e392aed3b1f03eb79474c294b42e2eba1528da26450aa592cba7ea22e965c54dff0fd6fdfd6b52b9a0f5f762e27fb0e6c3cd326a1ca1c5973de9be881439f702830affeb0c034c18ac8d5c2f135c964bf69de50d6e99bde88e90321ba843d9753c8f83666105d25fafb1a11ea22d62ef6f1fc34ca4e60c35d69773a104d9a44728c08c20b6314327301a2c400a71e1424c12628cf9f4a67990ade8a2203b0edb96c6082d4673b7309cd52c4b32b02951db2f66c6c72bd6c7eac2b50b83830c75cdfc3d6e9c2b592c45ed5fa5f6ec0da85710b7e1562aea363e28665835791dc574d9a70b2e5e2b9973ab590d45b94d244fc4256926c5a55b01cd0aca21fe5f9c907691fb026d0c56788b03ca3f08db0abb9f901098dde2ec4003568bc3ca27475ff86a7cb0aabd9e5136c5de064d16774584b252024109bb02004dba1fabf9e8277de097a0ab0dc8f6e26fcd4a28fb9d27cd4a2f6b13e276ed259a39e1c7e60f3c32c5cc4c4f96bd981edcb5e2c76a517cdc285aa2ca571d1e3d463ecd7614ae227df17af7445305bd7c661cf7dba658b0adcf36b0084b74a5fa408e272f703770ac5351334709112c5d4e4fe987e0c27b670412696f52b33245c229775da550729938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3da3bac7ec6b67f68f2838b6dc687c42dd3b4be7d8e44ded20177e8e3cc9c7014") + val PacketAndSecrets(onion, _) = PaymentPacket.create(sessionKey, publicKeys.take(1), variableSizeOneHopPayload, associatedData) + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661918004735c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2cc49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f580c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce42e93a4d7c803ead45fc47cf4396d284632314d789e73cf3f534126c63fe244069d9e8a7c4f98e7e530fc588e648ef4e641364981b5377542d5e7a4aaab6d35f6df7d3a9d7ca715213599ee02c4dbea4dc78860febe1d29259c64b59b3333ffdaebbaff4e7b31c27a3791f6bf848a58df7c69bb2b1852d2ad357b9919ffdae570b27dc709fba087273d3a4de9e6a6be66db647fb6a8d1a503b3f481befb96745abf5cc4a6bba0f780d5c7759b9e303a2a6b17eb05b6e660f4c474959db183e1cae060e1639227ee0bca03978a238dc4352ed764da7d4f3ed5337f6d0376dff72615beeeeaaeef79ab93e4bcbf18cd8424eb2b6ad7f33d2b4ffd5ea08372e6ed1d984152df17e04c6f73540988d7dd979e020424a163c271151a255966be7edef42167b8facca633649739bab97572b485658cde409e5d4a0f653f1a5911141634e3d2b6079b19347df66f9820755fd517092dae62fb278b0bafcc7ad682f7921b3a455e0c6369988779e26f0458b31bffd7e4e5bfb31944e80f100b2553c3b616e75be18328dc430f6618d55cd7d0962bb916d26ed4b117c46fa29e0a112c02c36020b34a96762db628fa3490828ec2079962ad816ef20ea0bca78fb2b7f7aedd4c47e375e64294d151ff03083730336dea64934003a27730cc1c7dec5049ddba8188123dd191aa71390d43a49fb792a3da7082efa6cced73f00eccea18145fbc84925349f7b552314ab8ed4c491e392aed3b1f03eb79474c294b42e2eba1528da26450aa592cba7ea22e965c54dff0fd6fdfd6b52b9a0f5f762e27fb0e6c3cd326a1ca1c5973de9be881439f702830affeb0c034c18ac8d5c2f135c964bf69de50d6e99bde88e90321ba843d9753c8f83666105d25fafb1a11ea22d62ef6f1fc34ca4e60c35d69773a104d9a44728c08c20b6314327301a2c400a71e1424c12628cf9f4a67990ade8a2203b0edb96c6082d4673b7309cd52c4b32b02951db2f66c6c72bd6c7eac2b50b83830c75cdfc3d6e9c2b592c45ed5fa5f6ec0da85710b7e1562aea363e28665835791dc574d9a70b2e5e2b9973ab590d45b94d244fc4256926c5a55b01cd0aca21fe5f9c907691fb026d0c56788b03ca3f08db0abb9f901098dde2ec4003568bc3ca27475ff86a7cb0aabd9e5136c5de064d16774584b252024109bb02004dba1fabf9e8277de097a0ab0dc8f6e26fcd4a28fb9d27cd4a2f6b13e276ed259a39e1c7e60f3c32c5cc4c4f96bd981edcb5e2c76a517cdc285aa2ca571d1e3d463ecd7614ae227df17af7445305bd7c661cf7dba658b0adcf36b0084b74a5fa408e272f703770ac5351334709112c5d4e4fe987e0c27b670412696f52b33245c229775da550729938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3da3bac7ec6b67f68f2838b6dc687c42dd3b4be7d8e44ded20177e8e3cc9c7014") - val Success(ParsedPacket(payload, nextPacket, _)) = PaymentPacket.parsePacket(privKeys(0), associatedData, onion.serialize) + val Right(DecryptedPacket(payload, nextPacket, _)) = PaymentPacket.peel(privKeys(0), associatedData, onion) assert(payload == variableSizeOneHopPayload.head) assert(nextPacket.hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } @@ -163,7 +215,7 @@ class SphinxSpec extends FunSuite { hex"000000000000000000000000000000000000000000000000000000000000000000" ) - assertThrows[IllegalArgumentException](PaymentPacket.makePacket(sessionKey, publicKeys.take(2), incorrectVarint, associatedData)) + assertThrows[IllegalArgumentException](PaymentPacket.create(sessionKey, publicKeys.take(2), incorrectVarint, associatedData)) } test("last node replies with an error message") { @@ -171,44 +223,44 @@ class SphinxSpec extends FunSuite { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 // origin build the onion packet - val PacketAndSecrets(packet, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, payloads, associatedData) + val PacketAndSecrets(packet, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, payloads, associatedData) // each node parses and forwards the packet // node #0 - val Success(ParsedPacket(_, packet1, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, packet.serialize) + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, packet) // node #1 - val Success(ParsedPacket(_, packet2, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, packet1.serialize) + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, packet1) // node #2 - val Success(ParsedPacket(_, packet3, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, packet2.serialize) + val Right(DecryptedPacket(_, packet3, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, packet2) // node #3 - val Success(ParsedPacket(_, packet4, sharedSecret3)) = PaymentPacket.parsePacket(privKeys(3), associatedData, packet3.serialize) + val Right(DecryptedPacket(_, packet4, sharedSecret3)) = PaymentPacket.peel(privKeys(3), associatedData, packet3) // node #4 - val Success(ParsedPacket(_, packet5, sharedSecret4)) = PaymentPacket.parsePacket(privKeys(4), associatedData, packet4.serialize) - assert(packet5.isLastPacket) + val Right(lastPacket@DecryptedPacket(_, _, sharedSecret4)) = PaymentPacket.peel(privKeys(4), associatedData, packet4) + assert(lastPacket.isLastPacket) // node #4 want to reply with an error message - val error = ErrorPacket.createPacket(sharedSecret4, TemporaryNodeFailure) + val error = ErrorPacket.create(sharedSecret4, TemporaryNodeFailure) assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") // assert(error == hex"69b1e5a3e05a7b5478e6529cd1749fdd8c66da6f6db42078ff8497ac4e117e91a8cb9168b58f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c2") // error sent back to 3, 2, 1 and 0 - val error1 = ErrorPacket.forwardPacket(error, sharedSecret3) + val error1 = ErrorPacket.wrap(error, sharedSecret3) assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") // assert(error1 == hex"08cd44478211b8a4370ab1368b5ffe8c9c92fb830ff4ad6e3b0a316df9d24176a081bab161ea0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a93") - val error2 = ErrorPacket.forwardPacket(error1, sharedSecret2) + val error2 = ErrorPacket.wrap(error1, sharedSecret2) assert(error2 == hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") // assert(error2 == hex"6984b0ccd86f37995857363df13670acd064bfd1a540e521cad4d71c07b1bc3dff9ac25f41addfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd") - val error3 = ErrorPacket.forwardPacket(error2, sharedSecret1) + val error3 = ErrorPacket.wrap(error2, sharedSecret1) assert(error3 == hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") // assert(error3 == hex"669478a3ddf9ba4049df8fa51f73ac712b9c20380cda431696963a492713ebddb7dfadbb566c8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8") - val error4 = ErrorPacket.forwardPacket(error3, sharedSecret0) + val error4 = ErrorPacket.wrap(error3, sharedSecret0) assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") // assert(error4 == hex"500d8596f76d3045bfdbf99914b98519fe76ea130dc22338c473ab68d74378b13a06a19f891145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366") // origin parses error packet and can see that it comes from node #4 - val Success(ErrorPacket(pubkey, failure)) = ErrorPacket.parsePacket(error4, sharedSecrets) + val Success(ErrorPacket(pubkey, failure)) = ErrorPacket.decrypt(error4, sharedSecrets) assert(pubkey == publicKeys(4)) assert(failure == TemporaryNodeFailure) } @@ -219,25 +271,25 @@ class SphinxSpec extends FunSuite { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 // origin build the onion packet - val PacketAndSecrets(packet, sharedSecrets) = PaymentPacket.makePacket(sessionKey, publicKeys, payloads, associatedData) + val PacketAndSecrets(packet, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, payloads, associatedData) // each node parses and forwards the packet // node #0 - val Success(ParsedPacket(_, packet1, sharedSecret0)) = PaymentPacket.parsePacket(privKeys(0), associatedData, packet.serialize) + val Right(DecryptedPacket(_, packet1, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, packet) // node #1 - val Success(ParsedPacket(_, packet2, sharedSecret1)) = PaymentPacket.parsePacket(privKeys(1), associatedData, packet1.serialize) + val Right(DecryptedPacket(_, packet2, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, packet1) // node #2 - val Success(ParsedPacket(_, _, sharedSecret2)) = PaymentPacket.parsePacket(privKeys(2), associatedData, packet2.serialize) + val Right(DecryptedPacket(_, _, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, packet2) // node #2 want to reply with an error message - val error = ErrorPacket.createPacket(sharedSecret2, InvalidRealm) + val error = ErrorPacket.create(sharedSecret2, InvalidRealm) // error sent back to 1 and 0 - val error1 = ErrorPacket.forwardPacket(error, sharedSecret1) - val error2 = ErrorPacket.forwardPacket(error1, sharedSecret0) + val error1 = ErrorPacket.wrap(error, sharedSecret1) + val error2 = ErrorPacket.wrap(error1, sharedSecret0) // origin parses error packet and can see that it comes from node #2 - val Success(ErrorPacket(pubkey, failure)) = ErrorPacket.parsePacket(error2, sharedSecrets) + val Success(ErrorPacket(pubkey, failure)) = ErrorPacket.decrypt(error2, sharedSecrets) assert(pubkey == publicKeys(2)) assert(failure == InvalidRealm) } @@ -245,6 +297,11 @@ class SphinxSpec extends FunSuite { } object SphinxSpec { + + import fr.acinq.eclair.wire.LightningMessageCodecs.paymentOnionPacketCodec + + def serializePaymentOnion(onion: OnionPacket): ByteVector = paymentOnionPacketCodec.encode(onion).require.toByteVector + val privKeys = Seq( PrivateKey(hex"4141414141414141414141414141414141414141414141414141414141414141"), PrivateKey(hex"4242424242424242424242424242424242424242424242424242424242424242"), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala index 1f1f86f200..2c72623576 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/SynchronizationPipe.scala @@ -22,7 +22,7 @@ import java.util.concurrent.CountDownLatch import akka.actor.{Actor, ActorLogging, ActorRef, Stash} import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.TestUtils +import fr.acinq.eclair.{TestConstants, TestUtils} import fr.acinq.eclair.channel._ import fr.acinq.eclair.transactions.{IN, OUT} @@ -57,7 +57,7 @@ class SynchronizationPipe(latch: CountDownLatch) extends Actor with ActorLogging script match { case offer(x, amount, rhash) :: rest => - resolve(x) ! CMD_ADD_HTLC(amount.toInt, ByteVector32.fromValidHex(rhash), 144, upstream = Left(UUID.randomUUID())) + resolve(x) ! CMD_ADD_HTLC(amount.toInt, ByteVector32.fromValidHex(rhash), 144, TestConstants.emptyOnionPacket, upstream = Left(UUID.randomUUID())) exec(rest, a, b) case fulfill(x, id, r) :: rest => resolve(x) ! CMD_FULFILL_HTLC(id.toInt, ByteVector32.fromValidHex(r)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala index c1197b1214..4378f5cf1d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/HtlcReaperSpec.scala @@ -19,10 +19,9 @@ package fr.acinq.eclair.io import akka.actor.{ActorSystem, Props} import akka.testkit.{TestKit, TestProbe} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.wire.{ChannelCodecsSpec, TemporaryNodeFailure, UpdateAddHtlc} import org.scalatest.FunSuiteLike -import scodec.bits.ByteVector import scala.concurrent.duration._ @@ -37,11 +36,11 @@ class HtlcReaperSpec extends TestKit(ActorSystem("test")) with FunSuiteLike { val data = ChannelCodecsSpec.normal // assuming that data has incoming htlcs 0 and 1, we don't care about the amount/payment_hash/onion fields - val add0 = UpdateAddHtlc(data.channelId, 0, 20000, randomBytes32, 100, ByteVector.empty) - val add1 = UpdateAddHtlc(data.channelId, 1, 30000, randomBytes32, 100, ByteVector.empty) + val add0 = UpdateAddHtlc(data.channelId, 0, 20000, randomBytes32, 100, TestConstants.emptyOnionPacket) + val add1 = UpdateAddHtlc(data.channelId, 1, 30000, randomBytes32, 100, TestConstants.emptyOnionPacket) // unrelated htlc - val add99 = UpdateAddHtlc(randomBytes32, 0, 12345678, randomBytes32, 100, ByteVector.empty) + val add99 = UpdateAddHtlc(randomBytes32, 0, 12345678, randomBytes32, 100, TestConstants.emptyOnionPacket) val brokenHtlcs = Seq(add0, add1, add99) val brokenHtlcKiller = system.actorOf(Props[HtlcReaper], name = "htlc-reaper") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala index 68353d1fe4..ccc65efe0b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/ChannelSelectionSpec.scala @@ -19,14 +19,12 @@ package fr.acinq.eclair.payment import fr.acinq.bitcoin.{Block, ByteVector32} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.channel.{CMD_ADD_HTLC, CMD_FAIL_HTLC} -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.Relayer.{OutgoingChannel, RelayFailure, RelayPayload, RelaySuccess} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ -import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomKey} +import fr.acinq.eclair.{ShortChannelId, TestConstants, randomBytes32, randomKey} import fr.acinq.eclair.payment.HtlcGenerationSpec.makeCommitments import org.scalatest.FunSuite -import scodec.bits.ByteVector import scala.collection.mutable @@ -40,9 +38,9 @@ class ChannelSelectionSpec extends FunSuite { test("convert to CMD_FAIL_HTLC/CMD_ADD_HTLC") { val relayPayload = RelayPayload( - add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, ByteVector.empty), + add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, TestConstants.emptyOnionPacket), payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), - nextPacket = Sphinx.PaymentPacket.EMPTY_PACKET // just a placeholder + nextPacket = TestConstants.emptyOnionPacket // just a placeholder ) val channelUpdate = dummyUpdate(ShortChannelId(12345), 10, 100, 1000, 100, 10000000, true) @@ -50,7 +48,7 @@ class ChannelSelectionSpec extends FunSuite { implicit val log = akka.event.NoLogging // nominal case - assert(Relayer.relayOrFail(relayPayload, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true))) + assert(Relayer.relayOrFail(relayPayload, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload.payload.amtToForward, relayPayload.add.paymentHash, relayPayload.payload.outgoingCltvValue, relayPayload.nextPacket, upstream = Right(relayPayload.add), commit = true))) // no channel_update assert(Relayer.relayOrFail(relayPayload, channelUpdate_opt = None) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(UnknownNextPeer), commit = true))) // channel disabled @@ -67,15 +65,15 @@ class ChannelSelectionSpec extends FunSuite { assert(Relayer.relayOrFail(relayPayload_insufficientfee, Some(channelUpdate)) === RelayFailure(CMD_FAIL_HTLC(relayPayload.add.id, Right(FeeInsufficient(relayPayload_insufficientfee.add.amountMsat, channelUpdate)), commit = true))) // note that a generous fee is ok! val relayPayload_highfee = relayPayload.copy(payload = relayPayload.payload.copy(amtToForward = 900000)) - assert(Relayer.relayOrFail(relayPayload_highfee, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket.serialize, upstream = Right(relayPayload.add), commit = true))) + assert(Relayer.relayOrFail(relayPayload_highfee, Some(channelUpdate)) === RelaySuccess(ShortChannelId(12345), CMD_ADD_HTLC(relayPayload_highfee.payload.amtToForward, relayPayload_highfee.add.paymentHash, relayPayload_highfee.payload.outgoingCltvValue, relayPayload_highfee.nextPacket, upstream = Right(relayPayload.add), commit = true))) } test("channel selection") { val relayPayload = RelayPayload( - add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, ByteVector.empty), + add = UpdateAddHtlc(randomBytes32, 42, 1000000, randomBytes32, 70, TestConstants.emptyOnionPacket), payload = PerHopPayload(ShortChannelId(12345), amtToForward = 998900, outgoingCltvValue = 60), - nextPacket = Sphinx.PaymentPacket.EMPTY_PACKET // just a placeholder + nextPacket = TestConstants.emptyOnionPacket // just a placeholder ) val (a, b) = (randomKey.publicKey, randomKey.publicKey) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index 082bdb3159..e7e35b22ad 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -22,7 +22,7 @@ import fr.acinq.bitcoin.DeterministicWallet.ExtendedPrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, DeterministicWallet} import fr.acinq.eclair.channel.{Channel, ChannelVersion, Commitments} import fr.acinq.eclair.crypto.Sphinx -import fr.acinq.eclair.crypto.Sphinx.{PacketAndSecrets, ParsedPacket} +import fr.acinq.eclair.crypto.Sphinx.{DecryptedPacket, PacketAndSecrets} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Hop import fr.acinq.eclair.wire.{ChannelUpdate, LightningMessageCodecs, PerHopPayload} @@ -68,30 +68,30 @@ class HtlcGenerationSpec extends FunSuite { val (_, _, payloads) = buildPayloads(finalAmountMsat, finalExpiry, hops.drop(1)) val nodes = hops.map(_.nextNodeId) val PacketAndSecrets(packet_b, _) = buildOnion(nodes, payloads, paymentHash) - assert(packet_b.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_b.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.parsePacket(priv_b.privateKey, paymentHash, packet_b.serialize) + val Right(DecryptedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, packet_b) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_c.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_c.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) - val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize) + val Right(DecryptedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.peel(priv_c.privateKey, paymentHash, packet_c) val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value - assert(packet_d.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_d.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) - val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize) + val Right(DecryptedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.peel(priv_d.privateKey, paymentHash, packet_d) val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value - assert(packet_e.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_e.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) - val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize) + val Right(DecryptedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_e.privateKey, paymentHash, packet_e) val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.outgoingCltvValue === finalExpiry) } @@ -103,30 +103,30 @@ class HtlcGenerationSpec extends FunSuite { assert(add.amountMsat > finalAmountMsat) assert(add.cltvExpiry === finalExpiry + channelUpdate_de.cltvExpiryDelta + channelUpdate_cd.cltvExpiryDelta + channelUpdate_bc.cltvExpiryDelta) assert(add.paymentHash === paymentHash) - assert(add.onion.length === Sphinx.PaymentPacket.PacketLength) + assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.parsePacket(priv_b.privateKey, paymentHash, add.onion) + val Right(DecryptedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, add.onion) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_c.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_c.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) - val Success(ParsedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.parsePacket(priv_c.privateKey, paymentHash, packet_c.serialize) + val Right(DecryptedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.peel(priv_c.privateKey, paymentHash, packet_c) val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value - assert(packet_d.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_d.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) - val Success(ParsedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.parsePacket(priv_d.privateKey, paymentHash, packet_d.serialize) + val Right(DecryptedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.peel(priv_d.privateKey, paymentHash, packet_d) val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value - assert(packet_e.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_e.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) - val Success(ParsedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.parsePacket(priv_e.privateKey, paymentHash, packet_e.serialize) + val Right(DecryptedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_e.privateKey, paymentHash, packet_e) val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.outgoingCltvValue === finalExpiry) } @@ -137,12 +137,12 @@ class HtlcGenerationSpec extends FunSuite { assert(add.amountMsat === finalAmountMsat) assert(add.cltvExpiry === finalExpiry) assert(add.paymentHash === paymentHash) - assert(add.onion.size === Sphinx.PaymentPacket.PacketLength) + assert(add.onion.payload.length === Sphinx.PaymentPacket.PayloadLength) // let's peel the onion - val Success(ParsedPacket(bin_b, packet_random, _)) = Sphinx.PaymentPacket.parsePacket(priv_b.privateKey, paymentHash, add.onion) + val Right(DecryptedPacket(bin_b, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, add.onion) val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value - assert(packet_random.serialize.size === Sphinx.PaymentPacket.PacketLength) + assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === finalAmountMsat) assert(payload_b.outgoingCltvValue === finalExpiry) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala index 9edf987b85..3915d24895 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentHandlerSpec.scala @@ -17,15 +17,15 @@ package fr.acinq.eclair.payment import akka.actor.Status.Failure -import akka.actor.{ActorSystem, Status} +import akka.actor.ActorSystem import akka.testkit.{TestActorRef, TestKit, TestProbe} -import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi, Satoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair.TestConstants.Alice import fr.acinq.eclair.channel.{CMD_FAIL_HTLC, CMD_FULFILL_HTLC} -import fr.acinq.eclair.payment.PaymentLifecycle.{ReceivePayment} +import fr.acinq.eclair.payment.PaymentLifecycle.ReceivePayment import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.wire.{FinalExpiryTooSoon, UpdateAddHtlc} -import fr.acinq.eclair.{Globals, ShortChannelId, randomKey} +import fr.acinq.eclair.{Globals, ShortChannelId, TestConstants, randomKey} import org.scalatest.FunSuiteLike import scodec.bits.ByteVector @@ -54,7 +54,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike assert(nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).isDefined) assert(!nodeParams.db.payments.getPendingPaymentRequestAndPreimage(pr.paymentHash).get._2.isExpired) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] @@ -68,7 +68,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FULFILL_HTLC] val paymentRelayed = eventListener.expectMsgType[PaymentReceived] @@ -81,7 +81,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike val pr = sender.expectMsgType[PaymentRequest] assert(nodeParams.db.payments.getIncomingPayment(pr.paymentHash).isEmpty) - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, cltvExpiry = Globals.blockCount.get() + 3, TestConstants.emptyOnionPacket) sender.send(handler, add) assert(sender.expectMsgType[CMD_FAIL_HTLC].reason == Right(FinalExpiryTooSoon)) eventListener.expectNoMsg(300 milliseconds) @@ -164,7 +164,7 @@ class PaymentHandlerSpec extends TestKit(ActorSystem("test")) with FunSuiteLike sender.send(handler, ReceivePayment(Some(amountMsat), "some desc", expirySeconds_opt = Some(0))) val pr = sender.expectMsgType[PaymentRequest] - val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, ByteVector.empty) + val add = UpdateAddHtlc(ByteVector32(ByteVector.fill(32)(1)), 0, amountMsat.amount, pr.paymentHash, expiry, TestConstants.emptyOnionPacket) sender.send(handler, add) sender.expectMsgType[CMD_FAIL_HTLC] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index 119a25c83f..b80a4e63ac 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -219,7 +219,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val WaitingForComplete(_, _, cmd1, Nil, _, _, _, hops) = paymentFSM.stateData relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, defaultPaymentHash, FailureMessageCodecs.BADONION)) + sender.send(paymentFSM, UpdateFailMalformedHtlc(ByteVector32.Zeroes, 0, randomBytes32, FailureMessageCodecs.BADONION)) // then the payment lifecycle will ask for a new route excluding the channel routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set(ChannelDesc(channelId_ab, a, b)))) @@ -251,7 +251,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = TemporaryChannelFailure(channelUpdate_bc) relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.createPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c))) @@ -294,7 +294,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 42, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(5, channelUpdate_bc_modified) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.createPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) @@ -311,7 +311,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 43, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure2 = IncorrectCltvExpiry(5, channelUpdate_bc_modified_2) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.createPacket(sharedSecrets2.head._1, failure2))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.create(sharedSecrets2.head._1, failure2))) // this time the payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c))) @@ -354,7 +354,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = PermanentChannelFailure relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.createPacket(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index c463b491a3..e2a1191d00 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -20,7 +20,7 @@ import java.util.UUID import akka.actor.{ActorRef, Status} import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, Crypto, MilliSatoshi} +import fr.acinq.bitcoin.{ByteVector32, MilliSatoshi} import fr.acinq.eclair.channel._ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.payment.PaymentLifecycle.buildCommand @@ -48,7 +48,6 @@ class RelayerSpec extends TestkitBaseClass { val register = TestProbe() val paymentHandler = TestProbe() // we are node B in the route A -> B -> C -> .... - //val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams.copy(nodeKey = priv_b), register.ref, paymentHandler.ref)) val relayer = system.actorOf(Relayer.props(TestConstants.Bob.nodeParams, register.ref, paymentHandler.ref)) withFixture(test.toNoArgTest(FixtureParam(relayer, register, paymentHandler))) } @@ -192,10 +191,10 @@ class RelayerSpec extends TestkitBaseClass { val (cmd1, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, randomBytes32, hops) val add_ab1 = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd1.amountMsat, cmd1.paymentHash, cmd1.cltvExpiry, cmd1.onion) - sender.send(relayer, ForwardAdd(add_ab)) + sender.send(relayer, ForwardAdd(add_ab1)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_HTLC]].message - assert(fail.id === add_ab.id) + assert(fail.id === add_ab1.id) assert(fail.reason === Right(UnknownNextPeer)) register.expectNoMsg(100 millis) @@ -229,15 +228,16 @@ class RelayerSpec extends TestkitBaseClass { // we use this to build a valid onion val (cmd, _) = buildCommand(UUID.randomUUID(), finalAmountMsat, finalExpiry, paymentHash, hops) - // and then manually build an htlc - val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, ByteVector.fill(Sphinx.PaymentPacket.PacketLength)(0)) + // and then manually build an htlc with an invalid onion (hmac) + val add_ab = UpdateAddHtlc(channelId = channelId_ab, id = 123456, cmd.amountMsat, cmd.paymentHash, cmd.cltvExpiry, cmd.onion.copy(hmac = cmd.onion.hmac.reverse)) relayer ! LocalChannelUpdate(null, channelId_bc, channelUpdate_bc.shortChannelId, c, None, channelUpdate_bc, makeCommitments(channelId_bc)) sender.send(relayer, ForwardAdd(add_ab)) val fail = register.expectMsgType[Register.Forward[CMD_FAIL_MALFORMED_HTLC]].message assert(fail.id === add_ab.id) - assert(fail.onionHash == Crypto.sha256(add_ab.onionRoutingPacket)) + assert(fail.onionHash == Sphinx.PaymentPacket.hash(add_ab.onionRoutingPacket)) + assert(fail.failureCode === (FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5)) register.expectNoMsg(100 millis) paymentHandler.expectNoMsg(100 millis) @@ -383,7 +383,7 @@ class RelayerSpec extends TestkitBaseClass { system.eventStream.subscribe(eventListener.ref, classOf[PaymentEvent]) // we build a fake htlc for the downstream channel - val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = ByteVector.empty) + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) val fulfill_ba = UpdateFulfillHtlc(channelId = channelId_bc, id = 42, paymentPreimage = ByteVector32.Zeroes) val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) sender.send(relayer, ForwardFulfill(fulfill_ba, origin, add_bc)) @@ -401,8 +401,8 @@ class RelayerSpec extends TestkitBaseClass { val sender = TestProbe() // we build a fake htlc for the downstream channel - val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = ByteVector.empty) - val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.ErrorPacket.createPacket(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) + val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) + val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.ErrorPacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) sender.send(relayer, ForwardFail(fail_ba, origin, add_bc)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala index ec31ac1936..1b95b50856 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/CommitmentSpecSpec.scala @@ -17,10 +17,9 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.{ByteVector32, Crypto} -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.wire.{UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc} import org.scalatest.FunSuite -import scodec.bits.ByteVector class CommitmentSpecSpec extends FunSuite { @@ -29,11 +28,11 @@ class CommitmentSpecSpec extends FunSuite { val R = randomBytes32 val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, ByteVector.empty) + val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, add1 :: Nil, Nil) assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(OUT, add1)), toLocalMsat = 3000 * 1000)) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, ByteVector.empty) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, add2 :: Nil, Nil) assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(OUT, add1), DirectedHtlc(OUT, add2)), toLocalMsat = 2000 * 1000)) @@ -51,11 +50,11 @@ class CommitmentSpecSpec extends FunSuite { val R = randomBytes32 val H = Crypto.sha256(R) - val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, ByteVector.empty) + val add1 = UpdateAddHtlc(ByteVector32.Zeroes, 1, 2000 * 1000, H, 400, TestConstants.emptyOnionPacket) val spec1 = CommitmentSpec.reduce(spec, Nil, add1 :: Nil) assert(spec1 === spec.copy(htlcs = Set(DirectedHtlc(IN, add1)), toRemoteMsat = 3000 * 1000)) - val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, ByteVector.empty) + val add2 = UpdateAddHtlc(ByteVector32.Zeroes, 2, 1000 * 1000, H, 400, TestConstants.emptyOnionPacket) val spec2 = CommitmentSpec.reduce(spec1, Nil, add2 :: Nil) assert(spec2 === spec1.copy(htlcs = Set(DirectedHtlc(IN, add1), DirectedHtlc(IN, add2)), toRemoteMsat = 2000 * 1000)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala index f32d4f046a..013d70b8fb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TestVectorsSpec.scala @@ -18,6 +18,7 @@ package fr.acinq.eclair.transactions import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin._ +import fr.acinq.eclair.TestConstants import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.transactions.Transactions.{HtlcSuccessTx, HtlcTimeoutTx, TransactionWithInputInfo} @@ -153,11 +154,11 @@ class TestVectorsSpec extends FunSuite with Logging { ) val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, ByteVector.empty)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, ByteVector.empty)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, ByteVector.empty)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, ByteVector.empty)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, ByteVector.empty)) + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) ) val htlcScripts = htlcs.map(htlc => htlc.direction match { case OUT => Scripts.htlcOffered(Local.payment_privkey.publicKey, Remote.payment_privkey.publicKey, Local.revocation_pubkey, Crypto.ripemd160(htlc.add.paymentHash)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala index 12ca3f87a0..f3819c8bc8 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala @@ -22,13 +22,12 @@ import fr.acinq.bitcoin.Crypto.{PrivateKey, ripemd160, sha256} import fr.acinq.bitcoin.Script.{pay2wpkh, pay2wsh, write} import fr.acinq.bitcoin._ import fr.acinq.eclair.channel.Helpers.Funding -import fr.acinq.eclair.randomBytes32 +import fr.acinq.eclair.{TestConstants, randomBytes32} import fr.acinq.eclair.transactions.Scripts.{htlcOffered, htlcReceived, toLocalDelayed} import fr.acinq.eclair.transactions.Transactions.{addSigs, _} import fr.acinq.eclair.wire.UpdateAddHtlc import grizzled.slf4j.Logging import org.scalatest.FunSuite -import scodec.bits.ByteVector import scala.io.Source import scala.util.{Failure, Random, Success, Try} @@ -64,10 +63,10 @@ class TransactionsSpec extends FunSuite with Logging { test("compute fees") { // see BOLT #3 specs val htlcs = Set( - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(5000000).amount, ByteVector32.Zeroes, 552, ByteVector.empty)), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, ByteVector32.Zeroes, 553, ByteVector.empty)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(7000000).amount, ByteVector32.Zeroes, 550, ByteVector.empty)), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(800000).amount, ByteVector32.Zeroes, 551, ByteVector.empty)) + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(5000000).amount, ByteVector32.Zeroes, 552, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, ByteVector32.Zeroes, 553, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(7000000).amount, ByteVector32.Zeroes, 550, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(800000).amount, ByteVector32.Zeroes, 551, TestConstants.emptyOnionPacket)) ) val spec = CommitmentSpec(htlcs, feeratePerKw = 5000, toLocalMsat = 0, toRemoteMsat = 0) val fee = Transactions.commitTxFee(Satoshi(546), spec) @@ -126,7 +125,7 @@ class TransactionsSpec extends FunSuite with Logging { // HtlcPenaltyTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, ByteVector.empty) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val redeemScript = htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry) val pubKeyScript = write(pay2wsh(redeemScript)) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) @@ -141,7 +140,7 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcSuccessTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, ByteVector.empty) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val pubKeyScript = write(pay2wsh(htlcOffered(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash)))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) @@ -155,7 +154,7 @@ class TransactionsSpec extends FunSuite with Logging { // ClaimHtlcTimeoutTx // first we create a fake commitTx tx, containing only the output that will be spent by the ClaimHtlcSuccessTx val paymentPreimage = randomBytes32 - val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, ByteVector.empty) + val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, Satoshi(20000).amount * 1000, sha256(paymentPreimage), cltvExpiry = 400144, TestConstants.emptyOnionPacket) val pubKeyScript = write(pay2wsh(htlcReceived(localHtlcPriv.publicKey, remoteHtlcPriv.publicKey, localRevocationPriv.publicKey, ripemd160(htlc.paymentHash), htlc.cltvExpiry))) val commitTx = Transaction(version = 0, txIn = Nil, txOut = TxOut(Satoshi(htlc.amountMsat / 1000), pubKeyScript) :: Nil, lockTime = 0) val claimClaimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx, outputsAlreadyUsed = Set.empty, localDustLimit, remoteHtlcPriv.publicKey, localHtlcPriv.publicKey, localRevocationPriv.publicKey, finalPubKeyScript, htlc, feeratePerKw) @@ -184,14 +183,14 @@ class TransactionsSpec extends FunSuite with Logging { // htlc1 and htlc2 are regular IN/OUT htlcs val paymentPreimage1 = randomBytes32 - val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, millibtc2satoshi(MilliBtc(100)).amount * 1000, sha256(paymentPreimage1), 300, ByteVector.empty) + val htlc1 = UpdateAddHtlc(ByteVector32.Zeroes, 0, millibtc2satoshi(MilliBtc(100)).amount * 1000, sha256(paymentPreimage1), 300, TestConstants.emptyOnionPacket) val paymentPreimage2 = randomBytes32 - val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, millibtc2satoshi(MilliBtc(200)).amount * 1000, sha256(paymentPreimage2), 300, ByteVector.empty) + val htlc2 = UpdateAddHtlc(ByteVector32.Zeroes, 1, millibtc2satoshi(MilliBtc(200)).amount * 1000, sha256(paymentPreimage2), 300, TestConstants.emptyOnionPacket) // htlc3 and htlc4 are dust htlcs IN/OUT htlcs, with an amount large enough to be included in the commit tx, but too small to be claimed at 2nd stage val paymentPreimage3 = randomBytes32 - val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, htlcTimeoutWeight)).amount * 1000, sha256(paymentPreimage3), 300, ByteVector.empty) + val htlc3 = UpdateAddHtlc(ByteVector32.Zeroes, 2, (localDustLimit + weight2fee(feeratePerKw, htlcTimeoutWeight)).amount * 1000, sha256(paymentPreimage3), 300, TestConstants.emptyOnionPacket) val paymentPreimage4 = randomBytes32 - val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).amount * 1000, sha256(paymentPreimage4), 300, ByteVector.empty) + val htlc4 = UpdateAddHtlc(ByteVector32.Zeroes, 3, (localDustLimit + weight2fee(feeratePerKw, htlcSuccessWeight)).amount * 1000, sha256(paymentPreimage4), 300, TestConstants.emptyOnionPacket) val spec = CommitmentSpec( htlcs = Set( DirectedHtlc(OUT, htlc1), @@ -321,7 +320,7 @@ class TransactionsSpec extends FunSuite with Logging { } def htlc(direction: Direction, amount: Satoshi): DirectedHtlc = - DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.amount * 1000, ByteVector32.Zeroes, 144, ByteVector.empty)) + DirectedHtlc(direction, UpdateAddHtlc(ByteVector32.Zeroes, 0, amount.amount * 1000, ByteVector32.Zeroes, 144, TestConstants.emptyOnionPacket)) test("BOLT 2 fee tests") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala index c000195254..726d463251 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/ChannelCodecsSpec.scala @@ -26,13 +26,14 @@ import fr.acinq.eclair._ import fr.acinq.eclair.api.JsonSupport import fr.acinq.eclair.channel.Helpers.Funding import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain, Sphinx} +import fr.acinq.eclair.crypto.{LocalKeyManager, ShaChain} import fr.acinq.eclair.payment.{Local, Relayed} import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.CommitTx import fr.acinq.eclair.transactions._ import fr.acinq.eclair.wire.ChannelCodecs._ import org.json4s.jackson.Serialization +import fr.acinq.eclair.{TestConstants, UInt64, randomBytes, randomBytes32, randomKey} import org.scalatest.FunSuite import scodec.bits._ import scodec.{Attempt, DecodeResult} @@ -131,7 +132,7 @@ class ChannelCodecsSpec extends FunSuite { amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PaymentPacket.PacketLength)) + onionRoutingPacket = TestConstants.emptyOnionPacket) val htlc1 = DirectedHtlc(direction = IN, add = add) val htlc2 = DirectedHtlc(direction = OUT, add = add) assert(htlcCodec.decodeValue(htlcCodec.encode(htlc1).require).require === htlc1) @@ -145,14 +146,14 @@ class ChannelCodecsSpec extends FunSuite { amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PaymentPacket.PacketLength)) + onionRoutingPacket = TestConstants.emptyOnionPacket) val add2 = UpdateAddHtlc( channelId = randomBytes32, id = Random.nextInt(Int.MaxValue), amountMsat = Random.nextInt(Int.MaxValue), cltvExpiry = Random.nextInt(Int.MaxValue), paymentHash = randomBytes32, - onionRoutingPacket = randomBytes(Sphinx.PaymentPacket.PacketLength)) + onionRoutingPacket = TestConstants.emptyOnionPacket) val htlc1 = DirectedHtlc(direction = IN, add = add1) val htlc2 = DirectedHtlc(direction = OUT, add = add2) val htlcs = Set(htlc1, htlc2) @@ -363,11 +364,11 @@ object ChannelCodecsSpec { ) val htlcs = Seq( - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, ByteVector.fill(Sphinx.PacketLength)(0))), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, ByteVector.fill(Sphinx.PacketLength)(0))), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, ByteVector.fill(Sphinx.PacketLength)(0))), - DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, ByteVector.fill(Sphinx.PacketLength)(0))), - DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, ByteVector.fill(Sphinx.PacketLength)(0))) + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 0, MilliSatoshi(1000000).amount, Crypto.sha256(paymentPreimages(0)), 500, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 1, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(1)), 501, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 30, MilliSatoshi(2000000).amount, Crypto.sha256(paymentPreimages(2)), 502, TestConstants.emptyOnionPacket)), + DirectedHtlc(OUT, UpdateAddHtlc(ByteVector32.Zeroes, 31, MilliSatoshi(3000000).amount, Crypto.sha256(paymentPreimages(3)), 503, TestConstants.emptyOnionPacket)), + DirectedHtlc(IN, UpdateAddHtlc(ByteVector32.Zeroes, 2, MilliSatoshi(4000000).amount, Crypto.sha256(paymentPreimages(4)), 504, TestConstants.emptyOnionPacket)) ) val fundingTx = Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index f48af32c47..8b28a3dd97 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -20,6 +20,7 @@ import fr.acinq.bitcoin.{Block, ByteVector64} import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomBytes64} import org.scalatest.FunSuite import scodec.bits._ +import scodec.codecs.uint16 /** * Created by PM on 31/05/2016. @@ -42,13 +43,13 @@ class FailureMessageCodecsSpec extends FunSuite { test("encode/decode all channel messages") { val msgs: List[FailureMessage] = InvalidRealm :: TemporaryNodeFailure :: PermanentNodeFailure :: RequiredNodeFeatureMissing :: - InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: + InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnionUnknown(randomBytes32) :: TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer :: AmountBelowMinimum(123456, channelUpdate) :: FeeInsufficient(546463, channelUpdate) :: IncorrectCltvExpiry(1211, channelUpdate) :: ExpiryTooSoon(channelUpdate) :: IncorrectOrUnknownPaymentDetails(123456L) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil msgs.foreach { - case msg => { + msg => { val encoded = FailureMessageCodecs.failureMessageCodec.encode(msg).require val decoded = FailureMessageCodecs.failureMessageCodec.decode(encoded).require assert(msg === decoded.value) @@ -56,6 +57,23 @@ class FailureMessageCodecsSpec extends FunSuite { } } + test("bad onion failure code") { + val msgs = Seq( + InvalidOnionVersion(randomBytes32), + InvalidOnionHmac(randomBytes32), + InvalidOnionKey(randomBytes32), + InvalidOnionUnknown(randomBytes32) + ) + + msgs.foreach { + msg => { + val encoded = FailureMessageCodecs.failureMessageCodec.encode(msg).require.toByteVector + val failureCode = uint16.decode(encoded.take(2).toBitVector).require.value + assert(failureCode === msg.failureCode) + } + } + } + test("support encoding of channel_update with/without type in failure messages") { val tmp_channel_failure_notype = hex"10070080cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" val tmp_channel_failure_withtype = hex"100700820102cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index 773d220faa..96aaaf7a78 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -25,7 +25,7 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.LightningMessageCodecs._ import org.scalatest.FunSuite -import scodec.bits.{BitVector, ByteVector, HexStringSyntax} +import scodec.bits.{ByteVector, HexStringSyntax} /** * Created by PM on 31/05/2016. @@ -61,7 +61,7 @@ class LightningMessageCodecsSpec extends FunSuite { val update_fee = UpdateFee(randomBytes32, 2) val shutdown = Shutdown(randomBytes32, bin(47, 0)) val closing_signed = ClosingSigned(randomBytes32, 2, randomBytes64) - val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, 3, bin32(0), 4, bin(Sphinx.PaymentPacket.PacketLength, 0)) + val update_add_htlc = UpdateAddHtlc(randomBytes32, 2, 3, bin32(0), 4, TestConstants.emptyOnionPacket) val update_fulfill_htlc = UpdateFulfillHtlc(randomBytes32, 2, bin32(0)) val update_fail_htlc = UpdateFailHtlc(randomBytes32, 2, bin(154, 0)) val update_fail_malformed_htlc = UpdateFailMalformedHtlc(randomBytes32, 2, randomBytes32, 1111) @@ -93,17 +93,28 @@ class LightningMessageCodecsSpec extends FunSuite { } } + test("encode/decode onion packet") { + val bin = hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf" + val expected = OnionPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172", ByteVector32(hex"65f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf")) + + val decoded = paymentOnionPacketCodec.decode(bin.toBitVector).require.value + assert(decoded === expected) + + val encoded = paymentOnionPacketCodec.encode(decoded).require + assert(encoded.toByteVector === bin) + } + test("encode/decode per-hop payload") { val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) - val bin = LightningMessageCodecs.perHopPayloadCodec.encode(payload).require + val bin = perHopPayloadCodec.encode(payload).require assert(bin.toByteVector.size === 33) - val payload1 = LightningMessageCodecs.perHopPayloadCodec.decode(bin).require.value + val payload1 = perHopPayloadCodec.decode(bin).require.value assert(payload === payload1) // realm (the first byte) should be 0 val bin1 = bin.toByteVector.update(0, 1) intercept[IllegalArgumentException] { - val payload2 = LightningMessageCodecs.perHopPayloadCodec.decode(bin1.toBitVector).require.value + val payload2 = perHopPayloadCodec.decode(bin1.toBitVector).require.value assert(payload2 === payload1) } } @@ -111,11 +122,11 @@ class LightningMessageCodecsSpec extends FunSuite { test("decode channel_update with htlc_maximum_msat") { // this was generated by c-lightning val bin = hex"010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00" - val update = LightningMessageCodecs.lightningMessageCodec.decode(BitVector(bin.toArray)).require.value.asInstanceOf[ChannelUpdate] + val update = lightningMessageCodec.decode(bin.toBitVector).require.value.asInstanceOf[ChannelUpdate] assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129, 1, 1, 6, 1, 1, 10, Some(980000000L))) val nodeId = PublicKey(hex"03370c9bac836e557eb4f017fe8f9cc047f44db39c1c4e410ff0f7be142b817ae4") assert(Announcements.checkSig(update, nodeId)) - val bin2 = ByteVector(LightningMessageCodecs.lightningMessageCodec.encode(update).require.toByteArray) + val bin2 = ByteVector(lightningMessageCodec.encode(update).require.toByteArray) assert(bin === bin2) } From 0f28e8a4868a0673163e0c65a344f38a03a0566b Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Thu, 4 Jul 2019 13:51:12 +0200 Subject: [PATCH 15/27] Use scodec for onion failure packet. Add codec that prepends a valid mac. Refactor to harmonize methods with onion packet. --- .../fr/acinq/eclair/channel/Commitments.scala | 4 +- .../scala/fr/acinq/eclair/crypto/Mac.scala | 59 ++++++++++ .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 103 +++++------------- .../fr/acinq/eclair/payment/Autoprobe.scala | 4 +- .../eclair/payment/PaymentLifecycle.scala | 16 +-- .../fr/acinq/eclair/wire/CommonCodecs.scala | 20 ++++ .../fr/acinq/eclair/wire/FailureMessage.scala | 24 +++- .../fr/acinq/eclair/crypto/MacSpec.scala | 62 +++++++++++ .../fr/acinq/eclair/crypto/SphinxSpec.scala | 96 +++++++++++----- .../eclair/integration/IntegrationSpec.scala | 12 +- .../eclair/payment/PaymentLifecycleSpec.scala | 18 +-- .../fr/acinq/eclair/payment/RelayerSpec.scala | 2 +- .../acinq/eclair/wire/CommonCodecsSpec.scala | 38 +++++-- .../wire/FailureMessageCodecsSpec.scala | 57 ++++++++-- .../wire/LightningMessageCodecsSpec.scala | 1 - 15 files changed, 363 insertions(+), 153 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala index 980eb6df25..ab5f158dd7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala @@ -236,8 +236,8 @@ object Commitments { Sphinx.PaymentPacket.peel(nodeSecret, htlc.paymentHash, htlc.onionRoutingPacket) match { case Right(Sphinx.DecryptedPacket(_, _, sharedSecret)) => val reason = cmd.reason match { - case Left(forwarded) => Sphinx.ErrorPacket.wrap(forwarded, sharedSecret) - case Right(failure) => Sphinx.ErrorPacket.create(sharedSecret, failure) + case Left(forwarded) => Sphinx.FailurePacket.wrap(forwarded, sharedSecret) + case Right(failure) => Sphinx.FailurePacket.create(sharedSecret, failure) } val fail = UpdateFailHtlc(commitments.channelId, cmd.id, reason) val commitments1 = addLocalProposal(commitments, fail) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala new file mode 100644 index 0000000000..9df7ad5f96 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala @@ -0,0 +1,59 @@ +/* + * 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.crypto + +import fr.acinq.bitcoin.ByteVector32 +import org.spongycastle.crypto.digests.SHA256Digest +import org.spongycastle.crypto.macs.HMac +import org.spongycastle.crypto.params.KeyParameter +import scodec.bits.ByteVector + +/** + * Created by t-bast on 04/07/19. + */ + +/** + * Create and verify message authentication codes. + */ +trait Mac { + + def mac(message: ByteVector): ByteVector32 + + def verify(mac: ByteVector32, message: ByteVector): Boolean + +} + +case class Hmac256(key: ByteVector) extends Mac { + + override def mac(message: ByteVector): ByteVector32 = Mac.hmac256(key, message) + + override def verify(mac: ByteVector32, message: ByteVector): Boolean = this.mac(message) === mac + +} + +object Mac { + + def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { + val mac = new HMac(new SHA256Digest()) + mac.init(new KeyParameter(key.toArray)) + mac.update(message.toArray, 0, message.length.toInt) + val output = new Array[Byte](32) + mac.doFinal(output, 0) + ByteVector32(ByteVector.view(output)) + } + +} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index c89b308b66..1c5fc0f26d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -16,16 +16,12 @@ package fr.acinq.eclair.crypto -import java.nio.ByteOrder - import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, Crypto, Protocol} +import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.wire import fr.acinq.eclair.wire.{CommonCodecs, FailureMessage, FailureMessageCodecs} import grizzled.slf4j.Logging -import org.spongycastle.crypto.digests.SHA256Digest -import org.spongycastle.crypto.macs.HMac -import org.spongycastle.crypto.params.KeyParameter +import scodec.Attempt import scodec.bits.{BitVector, ByteVector} import scala.annotation.tailrec @@ -40,18 +36,9 @@ object Sphinx extends Logging { // We use HMAC-SHA256 which returns 32-bytes message authentication codes. val MacLength = 32 - def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { - val mac = new HMac(new SHA256Digest()) - mac.init(new KeyParameter(key.toArray)) - mac.update(message.toArray, 0, message.length.toInt) - val output = new Array[Byte](32) - mac.doFinal(output, 0) - ByteVector32(ByteVector.view(output)) - } - - def mac(key: ByteVector, message: ByteVector): ByteVector32 = hmac256(key, message) + def mac(key: ByteVector, message: ByteVector): ByteVector32 = Mac.hmac256(key, message) - def generateKey(keyType: ByteVector, secret: ByteVector32): ByteVector32 = hmac256(keyType, secret) + def generateKey(keyType: ByteVector, secret: ByteVector32): ByteVector32 = Mac.hmac256(keyType, secret) def generateKey(keyType: String, secret: ByteVector32): ByteVector32 = generateKey(ByteVector.view(keyType.getBytes("UTF-8")), secret) @@ -120,7 +107,7 @@ object Sphinx extends Logging { * * @param payload decrypted payload for this node. * @param nextPacket packet for the next node. - * @param sharedSecret shared secret for the sending node, which we will need to return error messages. + * @param sharedSecret shared secret for the sending node, which we will need to return failure messages. */ case class DecryptedPacket(payload: ByteVector, nextPacket: wire.OnionPacket, sharedSecret: ByteVector32) { @@ -266,7 +253,7 @@ object Sphinx extends Logging { * @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 error messages if needed. + * the shared secrets (one per node) can be used to parse returned failure messages if needed. */ def create(sessionKey: PrivateKey, publicKeys: Seq[PublicKey], payloads: Seq[ByteVector], associatedData: ByteVector32): PacketAndSecrets = { val (ephemeralPublicKeys, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) @@ -303,69 +290,43 @@ object Sphinx extends Logging { } - // TODO: - // * Use scodec for error package (and clean-up existing stuff / verify spec conformance) - /** - * A properly decoded error from a node in the route. - * It has the following format: - * +----------------+----------------------------------+-----------------+----------------------+-----+ - * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | - * +----------------+----------------------------------+-----------------+----------------------+-----+ - * with failure message length + pad length = 256 + * A properly decrypted failure from a node in the route. * * @param originNode public key of the node that generated the failure. - * @param failureMessage friendly error message. + * @param failureMessage friendly failure message. */ - case class ErrorPacket(originNode: PublicKey, failureMessage: FailureMessage) + case class DecryptedFailurePacket(originNode: PublicKey, failureMessage: FailureMessage) - object ErrorPacket { + object FailurePacket { val MaxPayloadLength = 256 val PacketLength = MacLength + MaxPayloadLength + 2 + 2 /** - * Create an error packet that will be returned to the sender. + * Create a failure packet that will be returned to the sender. * Each intermediate hop will add a layer of encryption and forward to the previous hop. * Note that malicious intermediate hops may drop the packet or alter it (which breaks the mac). * * @param sharedSecret destination node's shared secret that was computed when the original onion for the HTLC - * was created or forwarded: see makePacket() and makeNextPacket(). + * was created or forwarded: see OnionPacket.create() and OnionPacket.wrap(). * @param failure failure message. - * @return an error packet that can be sent to the destination node. + * @return a failure packet that can be sent to the destination node. */ def create(sharedSecret: ByteVector32, failure: FailureMessage): ByteVector = { - val message: ByteVector = FailureMessageCodecs.failureMessageCodec.encode(failure).require.toByteVector - require(message.length <= MaxPayloadLength, s"error message length is ${message.length}, it must be less than $MaxPayloadLength") val um = generateKey("um", sharedSecret) - val padLength = MaxPayloadLength - message.length - val payload = Protocol.writeUInt16(message.length.toInt, ByteOrder.BIG_ENDIAN) ++ message ++ Protocol.writeUInt16(padLength.toInt, ByteOrder.BIG_ENDIAN) ++ ByteVector.fill(padLength.toInt)(0) + val packet = FailureMessageCodecs.failureOnionCodec(Hmac256(um)).encode(failure).require.toByteVector logger.debug(s"um key: $um") - logger.debug(s"error payload: ${payload.toHex}") - logger.debug(s"raw error packet: ${(mac(um, payload) ++ payload).toHex}") - wrap(mac(um, payload) ++ payload, sharedSecret) - } - - /** - * Extract the failure message from an error packet. - * - * @param packet error packet. - * @return the failure message that is embedded in the error packet. - */ - private def extractFailureMessage(packet: ByteVector): FailureMessage = { - require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") - val (_, payload) = packet.splitAt(MacLength) - val len = Protocol.uint16(payload.toArray, ByteOrder.BIG_ENDIAN) - require((len >= 0) && (len <= MaxPayloadLength), s"message length must be less than $MaxPayloadLength") - FailureMessageCodecs.failureMessageCodec.decode(BitVector(payload.drop(2).take(len))).require.value + logger.debug(s"raw error packet: ${packet.toHex}") + wrap(packet, sharedSecret) } /** * Wrap the given packet in an additional layer of onion encryption for the previous hop. * - * @param packet error packet. + * @param packet failure packet. * @param sharedSecret destination node's shared secret. - * @return an encrypted error packet that can be sent to the destination node. + * @return an encrypted failure packet that can be sent to the destination node. */ def wrap(packet: ByteVector, sharedSecret: ByteVector32): ByteVector = { require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") @@ -377,38 +338,28 @@ object Sphinx extends Logging { } /** - * Check the mac of an error packet. - * Note that malicious nodes in the route may have altered the packet, thus breaking the mac. - * - * @param sharedSecret this node's shared secret. - * @param packet error packet. - * @return true if the packet's mac is valid, which means that it has been properly decrypted. - */ - private def checkMac(sharedSecret: ByteVector32, packet: ByteVector): Boolean = { - val (packetMac, payload) = packet.splitAt(MacLength) - val um = generateKey("um", sharedSecret) - ByteVector32(packetMac) == mac(um, payload) - } - - /** - * Decrypt an error packet. Node shared secrets are applied until the packet's MAC becomes valid, which means that + * Decrypt a failure packet. Node shared secrets are applied until the packet's MAC becomes valid, which means that * it was sent by the corresponding node. * Note that malicious nodes in the route may have altered the packet, triggering a decryption failure. * - * @param packet error packet. + * @param packet failure packet. * @param sharedSecrets nodes shared secrets. * @return Success(secret, failure message) if the origin of the packet could be identified and the packet * decrypted, Failure otherwise. */ - def decrypt(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[ErrorPacket] = Try { + def decrypt(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): Try[DecryptedFailurePacket] = Try { require(packet.length == PacketLength, s"invalid error packet length ${packet.length}, must be $PacketLength") @tailrec - def loop(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): ErrorPacket = sharedSecrets match { + def loop(packet: ByteVector, sharedSecrets: Seq[(ByteVector32, PublicKey)]): DecryptedFailurePacket = sharedSecrets match { case Nil => throw new RuntimeException(s"couldn't parse error packet=$packet with sharedSecrets=$sharedSecrets") case (secret, pubkey) :: tail => val packet1 = wrap(packet, secret) - if (checkMac(secret, packet1)) ErrorPacket(pubkey, extractFailureMessage(packet1)) else loop(packet1, tail) + val um = generateKey("um", secret) + FailureMessageCodecs.failureOnionCodec(Hmac256(um)).decode(packet1.toBitVector) match { + case Attempt.Successful(value) => DecryptedFailurePacket(pubkey, value.value) + case _ => loop(packet1, tail) + } } loop(packet, sharedSecrets) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala index 918d02f065..94e33deb94 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Autoprobe.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.payment import akka.actor.{Actor, ActorLogging, ActorRef, Props} import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.crypto.Sphinx.ErrorPacket +import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentResult, RemoteFailure, SendPayment} import fr.acinq.eclair.router.{Announcements, Data} import fr.acinq.eclair.wire.{IncorrectOrUnknownPaymentDetails} @@ -62,7 +62,7 @@ class Autoprobe(nodeParams: NodeParams, router: ActorRef, paymentInitiator: Acto case paymentResult: PaymentResult => paymentResult match { - case PaymentFailed(_, _, _ :+ RemoteFailure(_, ErrorPacket(targetNodeId, IncorrectOrUnknownPaymentDetails(_)))) => + case PaymentFailed(_, _, _ :+ RemoteFailure(_, DecryptedFailurePacket(targetNodeId, IncorrectOrUnknownPaymentDetails(_)))) => log.info(s"payment probe successful to node=$targetNodeId") case _ => log.info(s"payment probe failed with paymentResult=$paymentResult") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index 9613d335bf..b4d9c507d2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -85,8 +85,8 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis stop(FSM.Normal) case Event(fail: UpdateFailHtlc, WaitingForComplete(s, c, _, failures, sharedSecrets, ignoreNodes, ignoreChannels, hops)) => - Sphinx.ErrorPacket.decrypt(fail.reason, sharedSecrets) match { - case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => + Sphinx.FailurePacket.decrypt(fail.reason, sharedSecrets) match { + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) if nodeId == c.targetNodeId => // if destination node returns an error, we fail the payment immediately log.warning(s"received an error message from target nodeId=$nodeId, failing the payment (failure=$failureMessage)") reply(s, PaymentFailed(id, c.paymentHash, failures = failures :+ RemoteFailure(hops, e))) @@ -95,7 +95,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis case res if failures.size + 1 >= c.maxAttempts => // otherwise we never try more than maxAttempts, no matter the kind of error returned val failure = res match { - case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId (failure=$failureMessage)") RemoteFailure(hops, e) case Failure(t) => @@ -113,12 +113,12 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis log.warning(s"blacklisting intermediate nodes=${blacklist.mkString(",")}") router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes ++ blacklist, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ UnreadableRemoteFailure(hops)) - case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage: Node)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Node)) => log.info(s"received 'Node' type error message from nodeId=$nodeId, trying to route around it (failure=$failureMessage)") // let's try to route around this node router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) - case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage: Update)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage: Update)) => log.info(s"received 'Update' type error message from nodeId=$nodeId, retrying payment (failure=$failureMessage)") if (Announcements.checkSig(failureMessage.update, nodeId)) { getChannelUpdateForNode(nodeId, hops) match { @@ -151,7 +151,7 @@ class PaymentLifecycle(nodeParams: NodeParams, id: UUID, router: ActorRef, regis router ! RouteRequest(nodeParams.nodeId, c.targetNodeId, c.amountMsat, c.assistedRoutes, ignoreNodes + nodeId, ignoreChannels, c.routeParams) } goto(WAITING_FOR_ROUTE) using WaitingForRoute(s, c, failures :+ RemoteFailure(hops, e)) - case Success(e@Sphinx.ErrorPacket(nodeId, failureMessage)) => + case Success(e@Sphinx.DecryptedFailurePacket(nodeId, failureMessage)) => log.info(s"received an error message from nodeId=$nodeId, trying to use a different channel (failure=$failureMessage)") // let's try again without the channel outgoing from nodeId val faultyChannel = hops.find(_.nodeId == nodeId).map(hop => ChannelDesc(hop.lastUpdate.shortChannelId, hop.nodeId, hop.nextNodeId)) @@ -215,7 +215,7 @@ object PaymentLifecycle { case class PaymentSucceeded(id: UUID, amountMsat: Long, paymentHash: ByteVector32, paymentPreimage: ByteVector32, route: Seq[Hop]) extends PaymentResult // note: the amount includes fees sealed trait PaymentFailure case class LocalFailure(t: Throwable) extends PaymentFailure - case class RemoteFailure(route: Seq[Hop], e: Sphinx.ErrorPacket) extends PaymentFailure + case class RemoteFailure(route: Seq[Hop], e: Sphinx.DecryptedFailurePacket) extends PaymentFailure case class UnreadableRemoteFailure(route: Seq[Hop]) extends PaymentFailure case class PaymentFailed(id: UUID, paymentHash: ByteVector32, failures: Seq[PaymentFailure]) extends PaymentResult @@ -311,6 +311,6 @@ object PaymentLifecycle { */ def hasAlreadyFailedOnce(nodeId: PublicKey, failures: Seq[PaymentFailure]): Boolean = failures - .collectFirst { case RemoteFailure(_, Sphinx.ErrorPacket(origin, u: Update)) if origin == nodeId => u.update } + .collectFirst { case RemoteFailure(_, Sphinx.DecryptedFailurePacket(origin, u: Update)) if origin == nodeId => u.update } .isDefined } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala index b0e81166a5..18ced0e084 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala @@ -20,6 +20,7 @@ import java.net.{Inet4Address, Inet6Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64} +import fr.acinq.eclair.crypto.Mac import fr.acinq.eclair.{ShortChannelId, UInt64} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} @@ -125,4 +126,23 @@ object CommonCodecs { def zeropaddedstring(size: Int): Codec[String] = fixedSizeBytes(32, utf8).xmap(s => s.takeWhile(_ != '\u0000'), s => s) + /** + * When encoding, prepend a valid mac to the output of the given codec. + * When decoding, verify that a valid mac is prepended. + */ + def prependmac[A](codec: Codec[A], mac: Mac) = Codec[A]( + (a: A) => codec.encode(a) match { + case Attempt.Successful(bits) => Attempt.Successful(mac.mac(bits.toByteVector).toBitVector ++ bits) + case Attempt.Failure(err) => Attempt.Failure(err) + }, + (bits: BitVector) => ("mac" | bytes32).decode(bits) match { + case Attempt.Successful(DecodeResult(msgMac, remainder)) => + if (mac.verify(msgMac, remainder.toByteVector)) + codec.decode(remainder) + else + Attempt.Failure(scodec.Err("invalid mac")) + case Attempt.Failure(err) => Attempt.Failure(err) + } + ) + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index 9e6bd18604..05371187d4 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -17,10 +17,12 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.crypto.Mac import fr.acinq.eclair.wire.CommonCodecs.{sha256, uint64overflow} -import fr.acinq.eclair.wire.LightningMessageCodecs.channelUpdateCodec +import fr.acinq.eclair.wire.LightningMessageCodecs.{channelUpdateCodec, lightningMessageCodec} +import scodec.bits.ByteVector import scodec.codecs._ -import scodec.Attempt +import scodec.{Attempt, Codec} /** * see /~https://github.com/lightningnetwork/lightning-rfc/blob/master/04-onion-routing.md @@ -78,7 +80,7 @@ object FailureMessageCodecs { val NODE = 0x2000 val UPDATE = 0x1000 - val channelUpdateCodecWithType = LightningMessageCodecs.lightningMessageCodec.narrow[ChannelUpdate](f => Attempt.successful(f.asInstanceOf[ChannelUpdate]), g => g) + val channelUpdateCodecWithType = lightningMessageCodec.narrow[ChannelUpdate](f => Attempt.successful(f.asInstanceOf[ChannelUpdate]), g => g) // NB: for historical reasons some implementations were including/omitting the message type (258 for ChannelUpdate) // this codec supports both versions for decoding, and will encode with the message type @@ -108,4 +110,20 @@ object FailureMessageCodecs { .typecase(18, ("expiry" | uint32).as[FinalIncorrectCltvExpiry]) .typecase(19, ("amountMsat" | uint64overflow).as[FinalIncorrectHtlcAmount]) .typecase(21, provide(ExpiryTooFar)) + + /** + * An onion-encrypted failure from an intermediate node: + * +----------------+----------------------------------+-----------------+----------------------+-----+ + * | HMAC(32 bytes) | failure message length (2 bytes) | failure message | pad length (2 bytes) | pad | + * +----------------+----------------------------------+-----------------+----------------------+-----+ + * with failure message length + pad length = 256 + */ + def failureOnionCodec(mac: Mac): Codec[FailureMessage] = CommonCodecs.prependmac( + paddedFixedSizeBytesDependent( + 260, + "failureMessage" | variableSizeBytes(uint16, FailureMessageCodecs.failureMessageCodec), + nBits => { + val nBytes = (nBits / 8).toInt + variableSizeBytes(uint16, bytes(nBytes - 2)).unit(ByteVector.empty) + }).as[FailureMessage], mac) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala new file mode 100644 index 0000000000..47a8f077aa --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/MacSpec.scala @@ -0,0 +1,62 @@ +/* + * 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.crypto + +import fr.acinq.bitcoin.ByteVector32 +import org.scalatest.FunSuite +import scodec.bits.HexStringSyntax + +/** + * Created by t-bast on 04/07/19. + */ + +class MacSpec extends FunSuite { + + test("HMAC-256 mac/verify") { + val keys = Seq( + hex"0000000000000000000000000000000000000000000000000000000000000000", + hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + hex"24653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c7f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007" + ) + val messages = Seq( + hex"2a", + hex"451", + hex"eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + hex"fd0001000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff" + ) + + for (key <- keys) { + val instance = Hmac256(key) + for (message <- messages) { + assert(instance.verify(instance.mac(message), message)) + } + } + } + + test("HMAC-256 invalid macs") { + val instance = Hmac256(ByteVector32.Zeroes) + val testCases = Seq( + (hex"0000000000000000000000000000000000000000000000000000000000000000", hex"2a"), + (hex"4aa79e2da0cb5beae9b5dad4006909cb402e4201e191733bc2b5279629e4ed80", hex"fd0001000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f30313233") + ).map(testCase => (ByteVector32(testCase._1), testCase._2)) + + for ((mac, message) <- testCases) { + assert(!instance.verify(mac, message)) + } + } + +} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index bea8bc4ec6..187f3491cb 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -218,7 +218,51 @@ class SphinxSpec extends FunSuite { assertThrows[IllegalArgumentException](PaymentPacket.create(sessionKey, publicKeys.take(2), incorrectVarint, associatedData)) } - test("last node replies with an error message") { + test("decrypt failure message") { + val sharedSecrets = Seq( + hex"0101010101010101010101010101010101010101010101010101010101010101", + hex"0202020202020202020202020202020202020202020202020202020202020202", + hex"0303030303030303030303030303030303030303030303030303030303030303" + ).map(ByteVector32(_)) + + val expected = DecryptedFailurePacket(publicKeys.head, InvalidOnionKey(ByteVector32.One)) + + val packet1 = FailurePacket.create(sharedSecrets.head, expected.failureMessage) + assert(packet1.length === FailurePacket.PacketLength) + + val Success(decrypted1) = FailurePacket.decrypt(packet1, Seq(0).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted1 === expected) + + val packet2 = FailurePacket.wrap(packet1, sharedSecrets(1)) + assert(packet2.length === FailurePacket.PacketLength) + + val Success(decrypted2) = FailurePacket.decrypt(packet2, Seq(1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted2 === expected) + + val packet3 = FailurePacket.wrap(packet2, sharedSecrets(2)) + assert(packet3.length === FailurePacket.PacketLength) + + val Success(decrypted3) = FailurePacket.decrypt(packet3, Seq(2, 1, 0).map(i => (sharedSecrets(i), publicKeys(i)))) + assert(decrypted3 === expected) + } + + test("decrypt invalid failure message") { + val sharedSecrets = Seq( + hex"0101010101010101010101010101010101010101010101010101010101010101", + hex"0202020202020202020202020202020202020202020202020202020202020202", + hex"0303030303030303030303030303030303030303030303030303030303030303" + ).map(ByteVector32(_)) + + val packet = FailurePacket.wrap( + FailurePacket.wrap( + FailurePacket.create(sharedSecrets.head, InvalidOnionUnknown(ByteVector32.Zeroes)), + sharedSecrets(1)), + sharedSecrets(2)) + + assert(FailurePacket.decrypt(packet, Seq(0, 2, 1).map(i => (sharedSecrets(i), publicKeys(i)))).isFailure) + } + + test("last node replies with a failure message (reference test vector)") { for (payloads <- Seq(referenceFixedSizePayloads, referenceVariableSizePayloads, variableSizePayloadsFull)) { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 @@ -239,34 +283,29 @@ class SphinxSpec extends FunSuite { assert(lastPacket.isLastPacket) // node #4 want to reply with an error message - val error = ErrorPacket.create(sharedSecret4, TemporaryNodeFailure) - assert(error == hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") - // assert(error == hex"69b1e5a3e05a7b5478e6529cd1749fdd8c66da6f6db42078ff8497ac4e117e91a8cb9168b58f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c2") + val error = FailurePacket.create(sharedSecret4, TemporaryNodeFailure) + assert(error === hex"a5e6bd0c74cb347f10cce367f949098f2457d14c046fd8a22cb96efb30b0fdcda8cb9168b50f2fd45edd73c1b0c8b33002df376801ff58aaa94000bf8a86f92620f343baef38a580102395ae3abf9128d1047a0736ff9b83d456740ebbb4aeb3aa9737f18fb4afb4aa074fb26c4d702f42968888550a3bded8c05247e045b866baef0499f079fdaeef6538f31d44deafffdfd3afa2fb4ca9082b8f1c465371a9894dd8c243fb4847e004f5256b3e90e2edde4c9fb3082ddfe4d1e734cacd96ef0706bf63c9984e22dc98851bcccd1c3494351feb458c9c6af41c0044bea3c47552b1d992ae542b17a2d0bba1a096c78d169034ecb55b6e3a7263c26017f033031228833c1daefc0dedb8cf7c3e37c9c37ebfe42f3225c326e8bcfd338804c145b16e34e4") // error sent back to 3, 2, 1 and 0 - val error1 = ErrorPacket.wrap(error, sharedSecret3) - assert(error1 == hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") - // assert(error1 == hex"08cd44478211b8a4370ab1368b5ffe8c9c92fb830ff4ad6e3b0a316df9d24176a081bab161ea0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a93") + val error1 = FailurePacket.wrap(error, sharedSecret3) + assert(error1 === hex"c49a1ce81680f78f5f2000cda36268de34a3f0a0662f55b4e837c83a8773c22aa081bab1616a0011585323930fa5b9fae0c85770a2279ff59ec427ad1bbff9001c0cd1497004bd2a0f68b50704cf6d6a4bf3c8b6a0833399a24b3456961ba00736785112594f65b6b2d44d9f5ea4e49b5e1ec2af978cbe31c67114440ac51a62081df0ed46d4a3df295da0b0fe25c0115019f03f15ec86fabb4c852f83449e812f141a9395b3f70b766ebbd4ec2fae2b6955bd8f32684c15abfe8fd3a6261e52650e8807a92158d9f1463261a925e4bfba44bd20b166d532f0017185c3a6ac7957adefe45559e3072c8dc35abeba835a8cb01a71a15c736911126f27d46a36168ca5ef7dccd4e2886212602b181463e0dd30185c96348f9743a02aca8ec27c0b90dca270") - val error2 = ErrorPacket.wrap(error1, sharedSecret2) - assert(error2 == hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") - // assert(error2 == hex"6984b0ccd86f37995857363df13670acd064bfd1a540e521cad4d71c07b1bc3dff9ac25f41addfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd") + val error2 = FailurePacket.wrap(error1, sharedSecret2) + assert(error2 === hex"a5d3e8634cfe78b2307d87c6d90be6fe7855b4f2cc9b1dfb19e92e4b79103f61ff9ac25f412ddfb7466e74f81b3e545563cdd8f5524dae873de61d7bdfccd496af2584930d2b566b4f8d3881f8c043df92224f38cf094cfc09d92655989531524593ec6d6caec1863bdfaa79229b5020acc034cd6deeea1021c50586947b9b8e6faa83b81fbfa6133c0af5d6b07c017f7158fa94f0d206baf12dda6b68f785b773b360fd0497e16cc402d779c8d48d0fa6315536ef0660f3f4e1865f5b38ea49c7da4fd959de4e83ff3ab686f059a45c65ba2af4a6a79166aa0f496bf04d06987b6d2ea205bdb0d347718b9aeff5b61dfff344993a275b79717cd815b6ad4c0beb568c4ac9c36ff1c315ec1119a1993c4b61e6eaa0375e0aaf738ac691abd3263bf937e3") - val error3 = ErrorPacket.wrap(error2, sharedSecret1) - assert(error3 == hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") - // assert(error3 == hex"669478a3ddf9ba4049df8fa51f73ac712b9c20380cda431696963a492713ebddb7dfadbb566c8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8") + val error3 = FailurePacket.wrap(error2, sharedSecret1) + assert(error3 === hex"aac3200c4968f56b21f53e5e374e3a2383ad2b1b6501bbcc45abc31e59b26881b7dfadbb56ec8dae8857add94e6702fb4c3a4de22e2e669e1ed926b04447fc73034bb730f4932acd62727b75348a648a1128744657ca6a4e713b9b646c3ca66cac02cdab44dd3439890ef3aaf61708714f7375349b8da541b2548d452d84de7084bb95b3ac2345201d624d31f4d52078aa0fa05a88b4e20202bd2b86ac5b52919ea305a8949de95e935eed0319cf3cf19ebea61d76ba92532497fcdc9411d06bcd4275094d0a4a3c5d3a945e43305a5a9256e333e1f64dbca5fcd4e03a39b9012d197506e06f29339dfee3331995b21615337ae060233d39befea925cc262873e0530408e6990f1cbd233a150ef7b004ff6166c70c68d9f8c853c1abca640b8660db2921") - val error4 = ErrorPacket.wrap(error3, sharedSecret0) - assert(error4 == hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") - // assert(error4 == hex"500d8596f76d3045bfdbf99914b98519fe76ea130dc22338c473ab68d74378b13a06a19f891145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366") + val error4 = FailurePacket.wrap(error3, sharedSecret0) + assert(error4 === hex"9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d") // origin parses error packet and can see that it comes from node #4 - val Success(ErrorPacket(pubkey, failure)) = ErrorPacket.decrypt(error4, sharedSecrets) - assert(pubkey == publicKeys(4)) - assert(failure == TemporaryNodeFailure) + val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error4, sharedSecrets) + assert(pubkey === publicKeys(4)) + assert(failure === TemporaryNodeFailure) } } - test("intermediate node replies with an error message") { + test("intermediate node replies with a failure message (reference test vector)") { for (payloads <- Seq(referenceFixedSizePayloads, referenceVariableSizePayloads, variableSizePayloadsFull)) { // route: origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 @@ -282,25 +321,26 @@ class SphinxSpec extends FunSuite { val Right(DecryptedPacket(_, _, sharedSecret2)) = PaymentPacket.peel(privKeys(2), associatedData, packet2) // node #2 want to reply with an error message - val error = ErrorPacket.create(sharedSecret2, InvalidRealm) + val error = FailurePacket.create(sharedSecret2, InvalidRealm) // error sent back to 1 and 0 - val error1 = ErrorPacket.wrap(error, sharedSecret1) - val error2 = ErrorPacket.wrap(error1, sharedSecret0) + val error1 = FailurePacket.wrap(error, sharedSecret1) + val error2 = FailurePacket.wrap(error1, sharedSecret0) // origin parses error packet and can see that it comes from node #2 - val Success(ErrorPacket(pubkey, failure)) = ErrorPacket.decrypt(error2, sharedSecrets) - assert(pubkey == publicKeys(2)) - assert(failure == InvalidRealm) + val Success(DecryptedFailurePacket(pubkey, failure)) = FailurePacket.decrypt(error2, sharedSecrets) + assert(pubkey === publicKeys(2)) + assert(failure === InvalidRealm) } } } object SphinxSpec { - import fr.acinq.eclair.wire.LightningMessageCodecs.paymentOnionPacketCodec + import fr.acinq.eclair.wire.LightningMessageCodecs - def serializePaymentOnion(onion: OnionPacket): ByteVector = paymentOnionPacketCodec.encode(onion).require.toByteVector + def serializePaymentOnion(onion: OnionPacket): ByteVector = + LightningMessageCodecs.paymentOnionPacketCodec.encode(onion).require.toByteVector val privKeys = Seq( PrivateKey(hex"4141414141414141414141414141414141414141414141414141414141414141"), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala index 53b1fc33aa..8b2c2e79da 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/IntegrationSpec.scala @@ -31,7 +31,7 @@ import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed} import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.channel.Register.{Forward, ForwardShortId} import fr.acinq.eclair.channel._ -import fr.acinq.eclair.crypto.Sphinx.ErrorPacket +import fr.acinq.eclair.crypto.Sphinx.DecryptedFailurePacket import fr.acinq.eclair.io.Peer.{Disconnect, PeerRoutingMessage} import fr.acinq.eclair.io.{NodeURI, Peer} import fr.acinq.eclair.payment.PaymentLifecycle.{State => _, _} @@ -343,7 +343,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) } test("send an HTLC A->D with a lower amount than requested") { @@ -363,7 +363,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(100000000L))) } test("send an HTLC A->D with too much overpayment") { @@ -383,7 +383,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(paymentId == failed.id) assert(failed.paymentHash === pr.paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000L))) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("D").nodeParams.nodeId, IncorrectOrUnknownPaymentDetails(600000000L))) } test("send an HTLC A->D with a reasonable overpayment") { @@ -651,7 +651,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions sender.send(bitcoincli, BitcoinReq("generate", 150)) sender.expectMsgType[JValue](10 seconds) @@ -717,7 +717,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService assert(failed.id == paymentId) assert(failed.paymentHash === paymentHash) assert(failed.failures.size === 1) - assert(failed.failures.head.asInstanceOf[RemoteFailure].e === ErrorPacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) + assert(failed.failures.head.asInstanceOf[RemoteFailure].e === DecryptedFailurePacket(nodes("C").nodeParams.nodeId, PermanentChannelFailure)) // we then generate enough blocks to confirm all delayed transactions sender.send(bitcoincli, BitcoinReq("generate", 145)) sender.expectMsgType[JValue](10 seconds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala index b80a4e63ac..79192309bf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/PaymentLifecycleSpec.scala @@ -251,7 +251,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = TemporaryChannelFailure(channelUpdate_bc) relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.create(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c))) @@ -262,7 +262,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.expectMsg(RouteRequest(a, d, defaultAmountMsat, assistedRoutes = Nil, ignoreNodes = Set.empty, ignoreChannels = Set.empty)) routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) } test("payment failed (Update)") { fixture => @@ -294,7 +294,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 42, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure = IncorrectCltvExpiry(5, channelUpdate_bc_modified) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.create(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle forwards the embedded channelUpdate to the router routerForwarder.expectMsg(channelUpdate_bc_modified) @@ -311,7 +311,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val channelUpdate_bc_modified_2 = makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv_b, c, channelId_bc, cltvExpiryDelta = 43, htlcMinimumMsat = channelUpdate_bc.htlcMinimumMsat, feeBaseMsat = channelUpdate_bc.feeBaseMsat, feeProportionalMillionths = channelUpdate_bc.feeProportionalMillionths, htlcMaximumMsat = channelUpdate_bc.htlcMaximumMsat.get) val failure2 = IncorrectCltvExpiry(5, channelUpdate_bc_modified_2) // and node replies with a failure containing a new channel update - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.create(sharedSecrets2.head._1, failure2))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets2.head._1, failure2))) // this time the payment lifecycle will ask the router to temporarily exclude this channel from its route calculations routerForwarder.expectMsg(ExcludeChannel(ChannelDesc(channelUpdate_bc.shortChannelId, b, c))) @@ -323,7 +323,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // this time the router can't find a route: game over - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.ErrorPacket(b, failure)) :: RemoteFailure(hops2, Sphinx.ErrorPacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: RemoteFailure(hops2, Sphinx.DecryptedFailurePacket(b, failure2)) :: LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } @@ -354,7 +354,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { val failure = PermanentChannelFailure relayer.expectMsg(ForwardShortId(channelId_ab, cmd1)) - sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.ErrorPacket.create(sharedSecrets1.head._1, failure))) + sender.send(paymentFSM, UpdateFailHtlc(ByteVector32.Zeroes, 0, Sphinx.FailurePacket.create(sharedSecrets1.head._1, failure))) // payment lifecycle forwards the embedded channelUpdate to the router awaitCond(paymentFSM.stateName == WAITING_FOR_ROUTE) @@ -362,7 +362,7 @@ class PaymentLifecycleSpec extends BaseRouterSpec { routerForwarder.forward(router) // we allow 2 tries, so we send a 2nd request to the router, which won't find another route - sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.ErrorPacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) + sender.expectMsg(PaymentFailed(id, request.paymentHash, RemoteFailure(hops, Sphinx.DecryptedFailurePacket(b, failure)) :: LocalFailure(RouteNotFound) :: Nil)) awaitCond(paymentDb.getOutgoingPayment(id).exists(_.status == OutgoingPaymentStatus.FAILED)) } @@ -451,8 +451,8 @@ class PaymentLifecycleSpec extends BaseRouterSpec { } test("filter errors properly") { _ => - val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil + val failures = LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(AddHtlcFailed(ByteVector32.Zeroes, ByteVector32.Zeroes, ChannelUnavailable(ByteVector32.Zeroes), Local(UUID.randomUUID(), None), None, None)) :: LocalFailure(RouteNotFound) :: Nil val filtered = PaymentLifecycle.transformForUser(failures) - assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.ErrorPacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) + assert(filtered == LocalFailure(RouteNotFound) :: RemoteFailure(Hop(a, b, channelUpdate_ab) :: Nil, Sphinx.DecryptedFailurePacket(a, TemporaryNodeFailure)) :: LocalFailure(ChannelUnavailable(ByteVector32.Zeroes)) :: Nil) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala index e2a1191d00..c6f6a27d41 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/RelayerSpec.scala @@ -402,7 +402,7 @@ class RelayerSpec extends TestkitBaseClass { // we build a fake htlc for the downstream channel val add_bc = UpdateAddHtlc(channelId = channelId_bc, id = 72, amountMsat = 10000000L, paymentHash = ByteVector32.Zeroes, cltvExpiry = 4200, onionRoutingPacket = TestConstants.emptyOnionPacket) - val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.ErrorPacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) + val fail_ba = UpdateFailHtlc(channelId = channelId_bc, id = 42, reason = Sphinx.FailurePacket.create(ByteVector32(ByteVector.fill(32)(1)), TemporaryChannelFailure(channelUpdate_cd))) val origin = Relayed(channelId_ab, 150, 11000000L, 10000000L) sender.send(relayer, ForwardFail(fail_ba, origin, add_bc)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala index 221efba9dd..b50695b6a0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala @@ -19,7 +19,9 @@ package fr.acinq.eclair.wire import java.net.{Inet4Address, Inet6Address, InetAddress} import com.google.common.net.InetAddresses +import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PrivateKey +import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.wire.CommonCodecs._ import fr.acinq.eclair.{UInt64, randomBytes32} import org.scalatest.FunSuite @@ -47,6 +49,17 @@ class CommonCodecsSpec extends FunSuite { } } + test("encode/decode UInt64") { + Seq( + UInt64(hex"ffffffffffffffff"), + UInt64(hex"fffffffffffffffe"), + UInt64(hex"efffffffffffffff"), + UInt64(hex"effffffffffffffe") + ).map(value => { + assert(uint64.decode(uint64.encode(value).require).require.value === value) + }) + } + test("encode/decode with varint codec") { val expected = Map( UInt64(0L) -> hex"00", @@ -222,16 +235,21 @@ class CommonCodecsSpec extends FunSuite { } } - test("encode/decode UInt64") { - val codec = uint64 - Seq( - UInt64(hex"ffffffffffffffff"), - UInt64(hex"fffffffffffffffe"), - UInt64(hex"efffffffffffffff"), - UInt64(hex"effffffffffffffe") - ).map(value => { - assert(codec.decode(codec.encode(value).require).require.value === value) - }) + test("encode/decode with prependmac codec") { + val mac = Hmac256(ByteVector32.Zeroes) + val testCases = Seq( + (uint64, UInt64(561), hex"d5b500b8843e19a34d8ab54740db76a7ea597e4ff2ada3827420f87c7e60b7c6 0000000000000231"), + (varint, UInt64(65535), hex"71e17e5b97deb6916f7ad97a53650769d4e4f0b1e580ff35ca332200d61e765c fdffff") + ) + + for ((codec, expected, bin) <- testCases) { + val macCodec = prependmac(codec, mac) + val decoded = macCodec.decode(bin.toBitVector).require.value + assert(decoded === expected) + + val encoded = macCodec.encode(expected).require.toByteVector + assert(encoded === bin) + } } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index 8b28a3dd97..7eab4db0f0 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -16,8 +16,10 @@ package fr.acinq.eclair.wire -import fr.acinq.bitcoin.{Block, ByteVector64} +import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} +import fr.acinq.eclair.crypto.Hmac256 import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomBytes64} +import fr.acinq.eclair.wire.FailureMessageCodecs._ import org.scalatest.FunSuite import scodec.bits._ import scodec.codecs.uint16 @@ -50,8 +52,8 @@ class FailureMessageCodecsSpec extends FunSuite { msgs.foreach { msg => { - val encoded = FailureMessageCodecs.failureMessageCodec.encode(msg).require - val decoded = FailureMessageCodecs.failureMessageCodec.decode(encoded).require + val encoded = failureMessageCodec.encode(msg).require + val decoded = failureMessageCodec.decode(encoded).require assert(msg === decoded.value) } } @@ -67,23 +69,64 @@ class FailureMessageCodecsSpec extends FunSuite { msgs.foreach { msg => { - val encoded = FailureMessageCodecs.failureMessageCodec.encode(msg).require.toByteVector + val encoded = failureMessageCodec.encode(msg).require.toByteVector val failureCode = uint16.decode(encoded.take(2).toBitVector).require.value assert(failureCode === msg.failureCode) } } } + test("encode/decode failure onion") { + val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) + val testCases = Seq( + hex"41a824e2d630111669fa3e52b600a518f369691909b4e89205dc624ee17ed2c1 0022 c006 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00de 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + hex"ba6e122b2941619e2106e8437bf525356ffc8439ac3b2245f68546e298a08cc6 000a 400f 000000000000002a 00f6 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) zip Seq( + InvalidOnionKey(ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")), + IncorrectOrUnknownPaymentDetails(42) + ) + + for ((bin, expected) <- testCases) { + val decoded = codec.decode(bin.toBitVector).require.value + assert(decoded === expected) + + val encoded = codec.encode(expected).require.toByteVector + assert(encoded === bin) + } + } + + test("decode invalid failure onion packet") { + val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) + val testCases = Seq( + // Invalid failure message. + hex"fd2f3eb163dacfa7fe2ec1a7dc73c33438e7ca97c561475cf0dc96dc15a75039 0020 c005 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00e0 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // Invalid mac. + hex"0000000000000000000000000000000000000000000000000000000000000000 0022 c006 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00de 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // Padding too small. + hex"7bfb2aa46218240684f623322ae48af431d06986c82e210bb0cee83c7ddb2ba8 0002 4001 0002 0000", + // Padding length doesn't match actual padding. + hex"8c92256e45bbe765130d952e6c043cf594ab25224701f5477fce0e50ee88fa21 0002 4001 0002 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // Padding too big. + hex"6f9e2c0e44b3692dac37523c6ff054cc9b26ecab1a78ed6906a46848bffc2bd5 0002 4001 00ff 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + // Padding length doesn't match actual padding. + hex"3898307b7c01781628ff6f854a4a78524541e4afde9b44046bdb84093f082d9d 0002 4001 00ff 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + ) + + for (testCase <- testCases) { + assert(codec.decode(testCase.toBitVector).isFailure) + } + } + test("support encoding of channel_update with/without type in failure messages") { val tmp_channel_failure_notype = hex"10070080cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" val tmp_channel_failure_withtype = hex"100700820102cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f45782196fe28c0ab6f1b372c1a6a246ae63f74f931e8365e15a089c68d619000000000008260500041300005b91b52f0003000e00000000000003e80000000100000001" val ref = TemporaryChannelFailure(ChannelUpdate(ByteVector64(hex"cc3e80149073ed487c76e48e9622bf980f78267b8a34a3f61921f2d8fce6063b08e74f34a073a13f2097337e4915bb4c001f3b5c4d81e9524ed575e1f4578219"), Block.LivenetGenesisBlock.hash, ShortChannelId(0x826050004130000L), 1536275759, 0, 3, 14, 1000, 1, 1, None)) - val u = FailureMessageCodecs.failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value + val u = failureMessageCodec.decode(tmp_channel_failure_notype.toBitVector).require.value assert(u === ref) - val bin = ByteVector(FailureMessageCodecs.failureMessageCodec.encode(u).require.toByteArray) + val bin = ByteVector(failureMessageCodec.encode(u).require.toByteArray) assert(bin === tmp_channel_failure_withtype) - val u2 = FailureMessageCodecs.failureMessageCodec.decode(bin.toBitVector).require.value + val u2 = failureMessageCodec.decode(bin.toBitVector).require.value assert(u2 === ref) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index 96aaaf7a78..2631ca6618 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -21,7 +21,6 @@ import java.net.{Inet4Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64} import fr.acinq.eclair._ -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire.LightningMessageCodecs._ import org.scalatest.FunSuite From 50228474e1bf8d9aef81656599d680564518495d Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Fri, 5 Jul 2019 17:20:03 +0200 Subject: [PATCH 16/27] Address PR comments: - Rename Mac to Mac32 - Move onion types and codecs to their own files - Small refactorings --- .../scala/fr/acinq/eclair/crypto/Mac.scala | 8 +-- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 19 +++---- .../fr/acinq/eclair/io/Switchboard.scala | 2 +- .../eclair/payment/PaymentLifecycle.scala | 2 +- .../fr/acinq/eclair/payment/Relayer.scala | 12 ++-- .../fr/acinq/eclair/wire/CommonCodecs.scala | 16 ++---- .../fr/acinq/eclair/wire/FailureMessage.scala | 10 ++-- .../eclair/wire/LightningMessageCodecs.scala | 29 +--------- .../eclair/wire/LightningMessageTypes.scala | 9 --- .../scala/fr/acinq/eclair/wire/Onion.scala | 56 ++++++++++++++++++ .../fr/acinq/eclair/crypto/SphinxSpec.scala | 4 +- .../eclair/payment/HtlcGenerationSpec.scala | 22 ++++--- .../acinq/eclair/wire/CommonCodecsSpec.scala | 7 +-- .../wire/FailureMessageCodecsSpec.scala | 4 +- .../wire/LightningMessageCodecsSpec.scala | 30 +--------- .../acinq/eclair/wire/OnionCodecsSpec.scala | 57 +++++++++++++++++++ 16 files changed, 165 insertions(+), 122 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala index 9df7ad5f96..494cd10201 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Mac.scala @@ -29,7 +29,7 @@ import scodec.bits.ByteVector /** * Create and verify message authentication codes. */ -trait Mac { +trait Mac32 { def mac(message: ByteVector): ByteVector32 @@ -37,15 +37,15 @@ trait Mac { } -case class Hmac256(key: ByteVector) extends Mac { +case class Hmac256(key: ByteVector) extends Mac32 { - override def mac(message: ByteVector): ByteVector32 = Mac.hmac256(key, message) + override def mac(message: ByteVector): ByteVector32 = Mac32.hmac256(key, message) override def verify(mac: ByteVector32, message: ByteVector): Boolean = this.mac(message) === mac } -object Mac { +object Mac32 { def hmac256(key: ByteVector, message: ByteVector): ByteVector32 = { val mac = new HMac(new SHA256Digest()) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 1c5fc0f26d..c8169219ca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -22,7 +22,7 @@ import fr.acinq.eclair.wire import fr.acinq.eclair.wire.{CommonCodecs, FailureMessage, FailureMessageCodecs} import grizzled.slf4j.Logging import scodec.Attempt -import scodec.bits.{BitVector, ByteVector} +import scodec.bits.ByteVector import scala.annotation.tailrec import scala.util.{Failure, Success, Try} @@ -36,9 +36,9 @@ object Sphinx extends Logging { // We use HMAC-SHA256 which returns 32-bytes message authentication codes. val MacLength = 32 - def mac(key: ByteVector, message: ByteVector): ByteVector32 = Mac.hmac256(key, message) + def mac(key: ByteVector, message: ByteVector): ByteVector32 = Mac32.hmac256(key, message) - def generateKey(keyType: ByteVector, secret: ByteVector32): ByteVector32 = Mac.hmac256(keyType, secret) + def generateKey(keyType: ByteVector, secret: ByteVector32): ByteVector32 = Mac32.hmac256(keyType, secret) def generateKey(keyType: String, secret: ByteVector32): ByteVector32 = generateKey(ByteVector.view(keyType.getBytes("UTF-8")), secret) @@ -93,12 +93,11 @@ object Sphinx extends Logging { // 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. - val dataLength = CommonCodecs.varintoverflow.decode(BitVector(payload.take(3))).require.value.toInt - val varintLength = dataLength match { - case i if i < 0xfd => 1 - case _ => 3 - } - varintLength + dataLength + MacLength + val lengthPrefix = payload.take(3) + val decodedLength = CommonCodecs.varintoverflow.decode(lengthPrefix.bits).require + val dataLength = decodedLength.value + val prefixLength = lengthPrefix.length - decodedLength.remainder.toByteVector.length + (prefixLength + dataLength + MacLength).toInt } } @@ -277,7 +276,7 @@ object Sphinx extends Logging { * When an invalid onion is received, its hash should be included in the failure message. */ def hash(onion: wire.OnionPacket): ByteVector32 = - Crypto.sha256(wire.LightningMessageCodecs.onionPacketCodec(onion.payload.length.toInt).encode(onion).require.toByteVector) + Crypto.sha256(wire.OnionCodecs.onionPacketCodec(onion.payload.length.toInt).encode(onion).require.toByteVector) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 54f1f10938..7d29142d2e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -174,7 +174,7 @@ object Switchboard extends Logging { .flatMap(_.commitments.remoteCommit.spec.htlcs) .filter(_.direction == OUT) .map(_.add) - .map(Relayer.tryDecryptPacket(_, privateKey)) + .map(Relayer.decryptPacket(_, privateKey)) .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. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala index b4d9c507d2..907a68a25c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentLifecycle.scala @@ -236,7 +236,7 @@ object PaymentLifecycle { require(nodes.size == payloads.size) val sessionKey = randomKey val payloadsbin: Seq[ByteVector] = payloads - .map(LightningMessageCodecs.perHopPayloadCodec.encode) + .map(OnionCodecs.perHopPayloadCodec.encode) .map { case Attempt.Successful(bitVector) => bitVector.toByteVector case Attempt.Failure(cause) => throw new RuntimeException(s"serialization error: $cause") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 0f639bdc94..a7a33ff44f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -29,7 +29,6 @@ import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentSucceeded import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ import fr.acinq.eclair.{NodeParams, ShortChannelId, nodeFee} -import scodec.bits.BitVector import scodec.{Attempt, DecodeResult} import scala.collection.mutable @@ -99,7 +98,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR case ForwardAdd(add, previousFailures) => log.debug(s"received forwarding request for htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId}") - tryDecryptPacket(add, nodeParams.privateKey) match { + decryptPacket(add, nodeParams.privateKey) match { case Right(p: FinalPayload) => handleFinal(p) match { case Left(cmdFail) => @@ -226,18 +225,19 @@ object Relayer { * * @param add incoming htlc * @param privateKey this node's private key - * @return the payload for the next hop + * @return the payload for the next hop or an error. */ - def tryDecryptPacket(add: UpdateAddHtlc, privateKey: PrivateKey): Either[BadOnion, NextPayload] = + def decryptPacket(add: UpdateAddHtlc, privateKey: PrivateKey): Either[BadOnion, NextPayload] = Sphinx.PaymentPacket.peel(privateKey, add.paymentHash, add.onionRoutingPacket) match { case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => - LightningMessageCodecs.perHopPayloadCodec.decode(BitVector(payload)) match { + OnionCodecs.perHopPayloadCodec.decode(payload.bits) match { case Attempt.Successful(DecodeResult(perHopPayload, _)) if p.isLastPacket => Right(FinalPayload(add, perHopPayload)) case Attempt.Successful(DecodeResult(perHopPayload, _)) => Right(RelayPayload(add, perHopPayload, nextPacket)) case Attempt.Failure(_) => - Left(InvalidOnionUnknown(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) + // Onion is correctly encrypted but the content of the per-hop payload couldn't be parsed. + Left(InvalidOnion(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) } case Left(badOnion) => Left(badOnion) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala index 18ced0e084..e56a3645fd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/CommonCodecs.scala @@ -20,7 +20,7 @@ import java.net.{Inet4Address, Inet6Address, InetAddress} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64} -import fr.acinq.eclair.crypto.Mac +import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.{ShortChannelId, UInt64} import org.apache.commons.codec.binary.Base32 import scodec.bits.{BitVector, ByteVector} @@ -130,17 +130,11 @@ object CommonCodecs { * When encoding, prepend a valid mac to the output of the given codec. * When decoding, verify that a valid mac is prepended. */ - def prependmac[A](codec: Codec[A], mac: Mac) = Codec[A]( - (a: A) => codec.encode(a) match { - case Attempt.Successful(bits) => Attempt.Successful(mac.mac(bits.toByteVector).toBitVector ++ bits) - case Attempt.Failure(err) => Attempt.Failure(err) - }, + def prependmac[A](codec: Codec[A], mac: Mac32) = Codec[A]( + (a: A) => codec.encode(a).map(bits => mac.mac(bits.toByteVector).bits ++ bits), (bits: BitVector) => ("mac" | bytes32).decode(bits) match { - case Attempt.Successful(DecodeResult(msgMac, remainder)) => - if (mac.verify(msgMac, remainder.toByteVector)) - codec.decode(remainder) - else - Attempt.Failure(scodec.Err("invalid mac")) + case Attempt.Successful(DecodeResult(msgMac, remainder)) if mac.verify(msgMac, remainder.toByteVector) => codec.decode(remainder) + case Attempt.Successful(_) => Attempt.Failure(scodec.Err("invalid mac")) case Attempt.Failure(err) => Attempt.Failure(err) } ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index 05371187d4..e2fa2d41a1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -17,7 +17,7 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.eclair.crypto.Mac +import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.wire.CommonCodecs.{sha256, uint64overflow} import fr.acinq.eclair.wire.LightningMessageCodecs.{channelUpdateCodec, lightningMessageCodec} import scodec.bits.ByteVector @@ -53,9 +53,9 @@ case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 6 def message = "ephemeral key was unparsable by the processing node" } -case class InvalidOnionUnknown(onionHash: ByteVector32) extends BadOnion with Perm { +case class InvalidOnion(onionHash: ByteVector32) extends BadOnion with Perm { def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM - def message = "onion per-hop payload unknown error" + def message = "onion per-hop payload could not be parsed" } case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel ${update.shortChannelId} is currently unavailable" } case object PermanentChannelFailure extends Perm { def message = "channel is permanently unavailable" } @@ -91,7 +91,7 @@ object FailureMessageCodecs { .typecase(NODE | 2, provide(TemporaryNodeFailure)) .typecase(PERM | 2, provide(PermanentNodeFailure)) .typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing)) - .typecase(BADONION | PERM, sha256.as[InvalidOnionUnknown]) + .typecase(BADONION | PERM, sha256.as[InvalidOnion]) .typecase(BADONION | PERM | 4, sha256.as[InvalidOnionVersion]) .typecase(BADONION | PERM | 5, sha256.as[InvalidOnionHmac]) .typecase(BADONION | PERM | 6, sha256.as[InvalidOnionKey]) @@ -118,7 +118,7 @@ object FailureMessageCodecs { * +----------------+----------------------------------+-----------------+----------------------+-----+ * with failure message length + pad length = 256 */ - def failureOnionCodec(mac: Mac): Codec[FailureMessage] = CommonCodecs.prependmac( + def failureOnionCodec(mac: Mac32): Codec[FailureMessage] = CommonCodecs.prependmac( paddedFixedSizeBytesDependent( 260, "failureMessage" | variableSizeBytes(uint16, FailureMessageCodecs.failureMessageCodec), diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 59c7be428c..8bb602003a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -16,19 +16,9 @@ package fr.acinq.eclair.wire -import java.net.{Inet4Address, Inet6Address, InetAddress} - -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.{ByteVector32, ByteVector64} -import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.wire import fr.acinq.eclair.wire.CommonCodecs._ -import scodec.bits.ByteVector import scodec.codecs._ -import scodec.{Attempt, Codec, Err} - -import scala.util.{Failure, Success, Try} - import scodec.Codec /** @@ -36,14 +26,6 @@ import scodec.Codec */ object LightningMessageCodecs { - def onionPacketCodec(payloadLength: Int): Codec[OnionPacket] = ( - ("version" | uint8) :: - ("publicKey" | bytes(33)) :: - ("onionPayload" | bytes(payloadLength)) :: - ("hmac" | bytes32)).as[OnionPacket] - - val paymentOnionPacketCodec: Codec[OnionPacket] = onionPacketCodec(Sphinx.PaymentPacket.PayloadLength) - val initCodec: Codec[Init] = ( ("globalFeatures" | varsizebinarydata) :: ("localFeatures" | varsizebinarydata)).as[Init] @@ -131,7 +113,7 @@ object LightningMessageCodecs { ("amountMsat" | uint64overflow) :: ("paymentHash" | bytes32) :: ("expiry" | uint32) :: - ("onionRoutingPacket" | paymentOnionPacketCodec)).as[UpdateAddHtlc] + ("onionRoutingPacket" | OnionCodecs.paymentOnionPacketCodec)).as[UpdateAddHtlc] val updateFulfillHtlcCodec: Codec[UpdateFulfillHtlc] = ( ("channelId" | bytes32) :: @@ -199,7 +181,7 @@ object LightningMessageCodecs { val nodeAnnouncementCodec: Codec[NodeAnnouncement] = ( ("signature" | bytes64) :: nodeAnnouncementWitnessCodec).as[NodeAnnouncement] - + val channelUpdateWitnessCodec = ("chainHash" | bytes32) :: ("shortChannelId" | shortchannelid) :: @@ -278,11 +260,4 @@ object LightningMessageCodecs { .typecase(264, replyChannelRangeCodec) .typecase(265, gossipTimestampFilterCodec) - val perHopPayloadCodec: Codec[PerHopPayload] = ( - ("realm" | constant(ByteVector.fromByte(0))) :: - ("short_channel_id" | shortchannelid) :: - ("amt_to_forward" | uint64overflow) :: - ("outgoing_cltv_value" | uint32) :: - ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload] - } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 5df611f1c7..0e40500044 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -225,15 +225,6 @@ case class ChannelUpdate(signature: ByteVector64, require(((messageFlags & 1) != 0) == htlcMaximumMsat.isDefined, "htlcMaximumMsat is not consistent with messageFlags") } -case class OnionPacket(version: Int, - publicKey: ByteVector, - payload: ByteVector, - hmac: ByteVector32) - -case class PerHopPayload(shortChannelId: ShortChannelId, - amtToForward: Long, - outgoingCltvValue: Long) - case class QueryShortChannelIds(chainHash: ByteVector32, data: ByteVector) extends RoutingMessage with HasChainHash diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala new file mode 100644 index 0000000000..a2b2869b7d --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala @@ -0,0 +1,56 @@ +/* + * 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.wire + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.crypto.Sphinx +import scodec.Codec +import scodec.bits.ByteVector +import scodec.codecs._ + +/** + * Created by t-bast on 05/07/2019. + */ + +case class OnionPacket(version: Int, + publicKey: ByteVector, + payload: ByteVector, + hmac: ByteVector32) + +case class PerHopPayload(shortChannelId: ShortChannelId, + amtToForward: Long, + outgoingCltvValue: Long) + +object OnionCodecs { + + def onionPacketCodec(payloadLength: Int): Codec[OnionPacket] = ( + ("version" | uint8) :: + ("publicKey" | bytes(33)) :: + ("onionPayload" | bytes(payloadLength)) :: + ("hmac" | CommonCodecs.bytes32)).as[OnionPacket] + + val paymentOnionPacketCodec: Codec[OnionPacket] = onionPacketCodec(Sphinx.PaymentPacket.PayloadLength) + + val perHopPayloadCodec: Codec[PerHopPayload] = ( + ("realm" | constant(ByteVector.fromByte(0))) :: + ("short_channel_id" | CommonCodecs.shortchannelid) :: + ("amt_to_forward" | CommonCodecs.uint64overflow) :: + ("outgoing_cltv_value" | uint32) :: + ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload] + +} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 187f3491cb..dd503dbf88 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -255,7 +255,7 @@ class SphinxSpec extends FunSuite { val packet = FailurePacket.wrap( FailurePacket.wrap( - FailurePacket.create(sharedSecrets.head, InvalidOnionUnknown(ByteVector32.Zeroes)), + FailurePacket.create(sharedSecrets.head, InvalidOnion(ByteVector32.Zeroes)), sharedSecrets(1)), sharedSecrets(2)) @@ -340,7 +340,7 @@ object SphinxSpec { import fr.acinq.eclair.wire.LightningMessageCodecs def serializePaymentOnion(onion: OnionPacket): ByteVector = - LightningMessageCodecs.paymentOnionPacketCodec.encode(onion).require.toByteVector + OnionCodecs.paymentOnionPacketCodec.encode(onion).require.toByteVector val privKeys = Seq( PrivateKey(hex"4141414141414141414141414141414141414141414141414141414141414141"), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala index e7e35b22ad..9114a8e418 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/HtlcGenerationSpec.scala @@ -25,13 +25,11 @@ import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.crypto.Sphinx.{DecryptedPacket, PacketAndSecrets} import fr.acinq.eclair.payment.PaymentLifecycle._ import fr.acinq.eclair.router.Hop -import fr.acinq.eclair.wire.{ChannelUpdate, LightningMessageCodecs, PerHopPayload} +import fr.acinq.eclair.wire.{ChannelUpdate, OnionCodecs, PerHopPayload} import fr.acinq.eclair.{ShortChannelId, TestConstants, nodeFee, randomBytes32} import org.scalatest.FunSuite import scodec.bits.ByteVector -import scala.util.Success - /** * Created by PM on 31/05/2016. */ @@ -72,25 +70,25 @@ class HtlcGenerationSpec extends FunSuite { // let's peel the onion val Right(DecryptedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, packet_b) - val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value + val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value assert(packet_c.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) val Right(DecryptedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.peel(priv_c.privateKey, paymentHash, packet_c) - val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value + val payload_c = OnionCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value assert(packet_d.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) val Right(DecryptedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.peel(priv_d.privateKey, paymentHash, packet_d) - val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value + val payload_d = OnionCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value assert(packet_e.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) val Right(DecryptedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_e.privateKey, paymentHash, packet_e) - val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value + val payload_e = OnionCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.outgoingCltvValue === finalExpiry) @@ -107,25 +105,25 @@ class HtlcGenerationSpec extends FunSuite { // let's peel the onion val Right(DecryptedPacket(bin_b, packet_c, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, add.onion) - val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value + val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value assert(packet_c.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === amount_bc) assert(payload_b.outgoingCltvValue === expiry_bc) val Right(DecryptedPacket(bin_c, packet_d, _)) = Sphinx.PaymentPacket.peel(priv_c.privateKey, paymentHash, packet_c) - val payload_c = LightningMessageCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value + val payload_c = OnionCodecs.perHopPayloadCodec.decode(bin_c.toBitVector).require.value assert(packet_d.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_c.amtToForward === amount_cd) assert(payload_c.outgoingCltvValue === expiry_cd) val Right(DecryptedPacket(bin_d, packet_e, _)) = Sphinx.PaymentPacket.peel(priv_d.privateKey, paymentHash, packet_d) - val payload_d = LightningMessageCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value + val payload_d = OnionCodecs.perHopPayloadCodec.decode(bin_d.toBitVector).require.value assert(packet_e.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_d.amtToForward === amount_de) assert(payload_d.outgoingCltvValue === expiry_de) val Right(DecryptedPacket(bin_e, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_e.privateKey, paymentHash, packet_e) - val payload_e = LightningMessageCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value + val payload_e = OnionCodecs.perHopPayloadCodec.decode(bin_e.toBitVector).require.value assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_e.amtToForward === finalAmountMsat) assert(payload_e.outgoingCltvValue === finalExpiry) @@ -141,7 +139,7 @@ class HtlcGenerationSpec extends FunSuite { // let's peel the onion val Right(DecryptedPacket(bin_b, packet_random, _)) = Sphinx.PaymentPacket.peel(priv_b.privateKey, paymentHash, add.onion) - val payload_b = LightningMessageCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value + val payload_b = OnionCodecs.perHopPayloadCodec.decode(bin_b.toBitVector).require.value assert(packet_random.payload.length === Sphinx.PaymentPacket.PayloadLength) assert(payload_b.amtToForward === finalAmountMsat) assert(payload_b.outgoingCltvValue === finalExpiry) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala index b50695b6a0..6d46c1d051 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/CommonCodecsSpec.scala @@ -50,14 +50,13 @@ class CommonCodecsSpec extends FunSuite { } test("encode/decode UInt64") { - Seq( + val refs = Seq( UInt64(hex"ffffffffffffffff"), UInt64(hex"fffffffffffffffe"), UInt64(hex"efffffffffffffff"), UInt64(hex"effffffffffffffe") - ).map(value => { - assert(uint64.decode(uint64.encode(value).require).require.value === value) - }) + ) + assert(refs.forall(value => uint64.decode(uint64.encode(value).require).require.value === value)) } test("encode/decode with varint codec") { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index 7eab4db0f0..01110d4e22 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -45,7 +45,7 @@ class FailureMessageCodecsSpec extends FunSuite { test("encode/decode all channel messages") { val msgs: List[FailureMessage] = InvalidRealm :: TemporaryNodeFailure :: PermanentNodeFailure :: RequiredNodeFeatureMissing :: - InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnionUnknown(randomBytes32) :: + InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnion(randomBytes32) :: TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer :: AmountBelowMinimum(123456, channelUpdate) :: FeeInsufficient(546463, channelUpdate) :: IncorrectCltvExpiry(1211, channelUpdate) :: ExpiryTooSoon(channelUpdate) :: IncorrectOrUnknownPaymentDetails(123456L) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil @@ -64,7 +64,7 @@ class FailureMessageCodecsSpec extends FunSuite { InvalidOnionVersion(randomBytes32), InvalidOnionHmac(randomBytes32), InvalidOnionKey(randomBytes32), - InvalidOnionUnknown(randomBytes32) + InvalidOnion(randomBytes32) ) msgs.foreach { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala index 2631ca6618..ff2c26b5a2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/LightningMessageCodecsSpec.scala @@ -44,7 +44,7 @@ class LightningMessageCodecsSpec extends FunSuite { test("encode/decode live node_announcements") { val ann = hex"a58338c9660d135fd7d087eb62afd24a33562c54507a9334e79f0dc4f17d407e6d7c61f0e2f3d0d38599502f61704cf1ae93608df027014ade7ff592f27ce2690001025acdf50702d2eabbbacc7c25bbd73b39e65d28237705f7bde76f557e94fb41cb18a9ec00841122116c6e302e646563656e7465722e776f726c64000000000000000000000000000000130200000000000000000000ffffae8a0b082607" - val bin = ann.toBitVector + val bin = ann.bits val node = nodeAnnouncementCodec.decode(bin).require.value val bin2 = nodeAnnouncementCodec.encode(node).require @@ -92,36 +92,10 @@ class LightningMessageCodecsSpec extends FunSuite { } } - test("encode/decode onion packet") { - val bin = hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf" - val expected = OnionPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172", ByteVector32(hex"65f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf")) - - val decoded = paymentOnionPacketCodec.decode(bin.toBitVector).require.value - assert(decoded === expected) - - val encoded = paymentOnionPacketCodec.encode(decoded).require - assert(encoded.toByteVector === bin) - } - - test("encode/decode per-hop payload") { - val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) - val bin = perHopPayloadCodec.encode(payload).require - assert(bin.toByteVector.size === 33) - val payload1 = perHopPayloadCodec.decode(bin).require.value - assert(payload === payload1) - - // realm (the first byte) should be 0 - val bin1 = bin.toByteVector.update(0, 1) - intercept[IllegalArgumentException] { - val payload2 = perHopPayloadCodec.decode(bin1.toBitVector).require.value - assert(payload2 === payload1) - } - } - test("decode channel_update with htlc_maximum_msat") { // this was generated by c-lightning val bin = hex"010258fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf1792306226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f0005a100000200005bc75919010100060000000000000001000000010000000a000000003a699d00" - val update = lightningMessageCodec.decode(bin.toBitVector).require.value.asInstanceOf[ChannelUpdate] + val update = lightningMessageCodec.decode(bin.bits).require.value.asInstanceOf[ChannelUpdate] assert(update === ChannelUpdate(ByteVector64(hex"58fff7d0e987e2cdd560e3bb5a046b4efe7b26c969c2f51da1dceec7bcb8ae1b634790503d5290c1a6c51d681cf8f4211d27ed33a257dcc1102862571bf17923"), ByteVector32(hex"06226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f"), ShortChannelId(0x5a10000020000L), 1539791129, 1, 1, 6, 1, 1, 10, Some(980000000L))) val nodeId = PublicKey(hex"03370c9bac836e557eb4f017fe8f9cc047f44db39c1c4e410ff0f7be142b817ae4") assert(Announcements.checkSig(update, nodeId)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala new file mode 100644 index 0000000000..bbef3cae9d --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala @@ -0,0 +1,57 @@ +/* + * 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.wire + +import fr.acinq.bitcoin.ByteVector32 +import fr.acinq.eclair.ShortChannelId +import fr.acinq.eclair.wire.OnionCodecs._ +import org.scalatest.FunSuite +import scodec.bits.HexStringSyntax + +/** + * Created by t-bast on 05/07/2019. + */ + +class OnionCodecsSpec extends FunSuite { + + test("encode/decode onion packet") { + val bin = hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf" + val expected = OnionPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172", ByteVector32(hex"65f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf")) + + val decoded = paymentOnionPacketCodec.decode(bin.bits).require.value + assert(decoded === expected) + + val encoded = paymentOnionPacketCodec.encode(decoded).require + assert(encoded.toByteVector === bin) + } + + test("encode/decode per-hop payload") { + val payload = PerHopPayload(shortChannelId = ShortChannelId(42), amtToForward = 142000, outgoingCltvValue = 500000) + val bin = perHopPayloadCodec.encode(payload).require + assert(bin.toByteVector.size === 33) + val payload1 = perHopPayloadCodec.decode(bin).require.value + assert(payload === payload1) + + // realm (the first byte) should be 0 + val bin1 = bin.toByteVector.update(0, 1) + intercept[IllegalArgumentException] { + val payload2 = perHopPayloadCodec.decode(bin1.bits).require.value + assert(payload2 === payload1) + } + } + +} From b39769e412517920d7c3d709b1b8e4258b881f1a Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 8 Jul 2019 09:54:50 +0200 Subject: [PATCH 17/27] Create a decode-only codec to extract onion payload length --- .../acinq/eclair/channel/ChannelTypes.scala | 4 +-- .../scala/fr/acinq/eclair/crypto/Sphinx.scala | 24 +++++++---------- .../fr/acinq/eclair/payment/Relayer.scala | 2 +- .../eclair/wire/LightningMessageTypes.scala | 2 +- .../scala/fr/acinq/eclair/wire/Onion.scala | 27 ++++++++++++------- .../scala/fr/acinq/eclair/TestConstants.scala | 2 +- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 22 +++++++-------- .../acinq/eclair/wire/OnionCodecsSpec.scala | 19 ++++++++++++- 8 files changed, 62 insertions(+), 40 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala index cbd718fb5d..d4993e2ade 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/ChannelTypes.scala @@ -23,7 +23,7 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, DeterministicWallet, OutPoint, Satoshi, Transaction} import fr.acinq.eclair.transactions.CommitmentSpec import fr.acinq.eclair.transactions.Transactions.CommitTx -import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionPacket, OpenChannel, Shutdown, UpdateAddHtlc} +import fr.acinq.eclair.wire.{AcceptChannel, ChannelAnnouncement, ChannelReestablish, ChannelUpdate, ClosingSigned, FailureMessage, FundingCreated, FundingLocked, FundingSigned, Init, OnionRoutingPacket, OpenChannel, Shutdown, UpdateAddHtlc} import fr.acinq.eclair.{ShortChannelId, UInt64} import scodec.bits.ByteVector @@ -106,7 +106,7 @@ case class BITCOIN_PARENT_TX_CONFIRMED(childTx: Transaction) extends BitcoinEven */ sealed trait Command -final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: OnionPacket, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command +final case class CMD_ADD_HTLC(amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, onion: OnionRoutingPacket, upstream: Either[UUID, UpdateAddHtlc], commit: Boolean = false, previousFailures: Seq[AddHtlcFailed] = Seq.empty) extends Command final case class CMD_FULFILL_HTLC(id: Long, r: ByteVector32, commit: Boolean = false) extends Command final case class CMD_FAIL_HTLC(id: Long, reason: Either[ByteVector, FailureMessage], commit: Boolean = false) extends Command final case class CMD_FAIL_MALFORMED_HTLC(id: Long, onionHash: ByteVector32, failureCode: Int, commit: Boolean = false) extends Command diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index c8169219ca..203f2809cb 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -19,7 +19,7 @@ package fr.acinq.eclair.crypto import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, Crypto} import fr.acinq.eclair.wire -import fr.acinq.eclair.wire.{CommonCodecs, FailureMessage, FailureMessageCodecs} +import fr.acinq.eclair.wire.{FailureMessage, FailureMessageCodecs, OnionCodecs} import grizzled.slf4j.Logging import scodec.Attempt import scodec.bits.ByteVector @@ -93,11 +93,7 @@ object Sphinx extends Logging { // 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. - val lengthPrefix = payload.take(3) - val decodedLength = CommonCodecs.varintoverflow.decode(lengthPrefix.bits).require - val dataLength = decodedLength.value - val prefixLength = lengthPrefix.length - decodedLength.remainder.toByteVector.length - (prefixLength + dataLength + MacLength).toInt + MacLength + OnionCodecs.payloadLengthDecoder.decode(payload.take(3).bits).require.value.toInt } } @@ -108,7 +104,7 @@ object Sphinx extends Logging { * @param nextPacket packet for the next node. * @param sharedSecret shared secret for the sending node, which we will need to return failure messages. */ - case class DecryptedPacket(payload: ByteVector, nextPacket: wire.OnionPacket, sharedSecret: ByteVector32) { + case class DecryptedPacket(payload: ByteVector, nextPacket: wire.OnionRoutingPacket, sharedSecret: ByteVector32) { val isLastPacket: Boolean = payload.head match { // In Bolt 1.0 the last hop is signaled via an empty hmac. @@ -127,7 +123,7 @@ object Sphinx extends Logging { * @param sharedSecrets shared secrets (one per node in the route). Known (and needed) only if you're creating the * packet. Empty if you're just forwarding the packet to the next node. */ - case class PacketAndSecrets(packet: wire.OnionPacket, sharedSecrets: Seq[(ByteVector32, PublicKey)]) + case class PacketAndSecrets(packet: wire.OnionRoutingPacket, sharedSecrets: Seq[(ByteVector32, PublicKey)]) sealed trait OnionPacket { @@ -178,7 +174,7 @@ object Sphinx extends Logging { * failure messages upstream. * or a BadOnion error containing the hash of the invalid onion. */ - def peel(privateKey: PrivateKey, associatedData: ByteVector, packet: wire.OnionPacket): Either[wire.BadOnion, DecryptedPacket] = packet.version match { + def peel(privateKey: PrivateKey, associatedData: ByteVector, packet: wire.OnionRoutingPacket): Either[wire.BadOnion, DecryptedPacket] = packet.version match { case 0 => Try(PublicKey(packet.publicKey, checkValid = true)) match { case Success(packetEphKey) => val sharedSecret = computeSharedSecret(packetEphKey, privateKey) @@ -198,7 +194,7 @@ object Sphinx extends Logging { val nextOnionPayload = bin.drop(perHopPayloadLength).take(PayloadLength) val nextPubKey = blind(packetEphKey, computeBlindingFactor(packetEphKey, sharedSecret)) - Right(DecryptedPacket(perHopPayload, wire.OnionPacket(Version, nextPubKey.value, nextOnionPayload, hmac), sharedSecret)) + Right(DecryptedPacket(perHopPayload, wire.OnionRoutingPacket(Version, nextPubKey.value, nextOnionPayload, hmac), sharedSecret)) } else { Left(wire.InvalidOnionHmac(hash(packet))) } @@ -224,7 +220,7 @@ object Sphinx extends Logging { * @param onionPayloadFiller optional onion payload filler, needed only when you're constructing the last packet. * @return the next packet. */ - def wrap(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Option[wire.OnionPacket], onionPayloadFiller: ByteVector = ByteVector.empty): wire.OnionPacket = { + def wrap(payload: ByteVector, associatedData: ByteVector32, ephemeralPublicKey: PublicKey, sharedSecret: ByteVector32, packet: Option[wire.OnionRoutingPacket], onionPayloadFiller: ByteVector = ByteVector.empty): wire.OnionRoutingPacket = { require(payload.length <= PayloadLength - MacLength, s"packet payload cannot exceed ${PayloadLength - MacLength} bytes") val (currentMac, currentPayload): (ByteVector32, ByteVector) = packet match { @@ -240,7 +236,7 @@ object Sphinx extends Logging { } val nextHmac = mac(generateKey("mu", sharedSecret), nextOnionPayload ++ associatedData) - val nextPacket = wire.OnionPacket(Version, ephemeralPublicKey.value, nextOnionPayload, nextHmac) + val nextPacket = wire.OnionRoutingPacket(Version, ephemeralPublicKey.value, nextOnionPayload, nextHmac) nextPacket } @@ -261,7 +257,7 @@ object Sphinx extends Logging { val lastPacket = wrap(payloads.last, associatedData, ephemeralPublicKeys.last, sharedsecrets.last, None, filler) @tailrec - def loop(hopPayloads: Seq[ByteVector], ephKeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: wire.OnionPacket): wire.OnionPacket = { + def loop(hopPayloads: Seq[ByteVector], ephKeys: Seq[PublicKey], sharedSecrets: Seq[ByteVector32], packet: wire.OnionRoutingPacket): wire.OnionRoutingPacket = { if (hopPayloads.isEmpty) packet else { val nextPacket = wrap(hopPayloads.last, associatedData, ephKeys.last, sharedSecrets.last, Some(packet)) loop(hopPayloads.dropRight(1), ephKeys.dropRight(1), sharedSecrets.dropRight(1), nextPacket) @@ -275,7 +271,7 @@ object Sphinx extends Logging { /** * When an invalid onion is received, its hash should be included in the failure message. */ - def hash(onion: wire.OnionPacket): ByteVector32 = + def hash(onion: wire.OnionRoutingPacket): ByteVector32 = Crypto.sha256(wire.OnionCodecs.onionPacketCodec(onion.payload.length.toInt).encode(onion).require.toByteVector) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index a7a33ff44f..3c8e7c1a31 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -213,7 +213,7 @@ object Relayer { // @formatter:off sealed trait NextPayload case class FinalPayload(add: UpdateAddHtlc, payload: PerHopPayload) extends NextPayload - case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: OnionPacket) extends NextPayload { + case class RelayPayload(add: UpdateAddHtlc, payload: PerHopPayload, nextPacket: OnionRoutingPacket) extends NextPayload { val relayFeeMsat: Long = add.amountMsat - payload.amtToForward val expiryDelta: Long = add.cltvExpiry - payload.outgoingCltvValue } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 0e40500044..25c1cfb032 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -123,7 +123,7 @@ case class UpdateAddHtlc(channelId: ByteVector32, amountMsat: Long, paymentHash: ByteVector32, cltvExpiry: Long, - onionRoutingPacket: OnionPacket) extends HtlcMessage with UpdateMessage with HasChannelId + onionRoutingPacket: OnionRoutingPacket) extends HtlcMessage with UpdateMessage with HasChannelId case class UpdateFulfillHtlc(channelId: ByteVector32, id: Long, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala index a2b2869b7d..cea34feb01 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala @@ -19,18 +19,18 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.crypto.Sphinx -import scodec.Codec -import scodec.bits.ByteVector +import scodec.{Codec, DecodeResult, Decoder} +import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ /** * Created by t-bast on 05/07/2019. */ -case class OnionPacket(version: Int, - publicKey: ByteVector, - payload: ByteVector, - hmac: ByteVector32) +case class OnionRoutingPacket(version: Int, + publicKey: ByteVector, + payload: ByteVector, + hmac: ByteVector32) case class PerHopPayload(shortChannelId: ShortChannelId, amtToForward: Long, @@ -38,13 +38,13 @@ case class PerHopPayload(shortChannelId: ShortChannelId, object OnionCodecs { - def onionPacketCodec(payloadLength: Int): Codec[OnionPacket] = ( + def onionPacketCodec(payloadLength: Int): Codec[OnionRoutingPacket] = ( ("version" | uint8) :: ("publicKey" | bytes(33)) :: ("onionPayload" | bytes(payloadLength)) :: - ("hmac" | CommonCodecs.bytes32)).as[OnionPacket] + ("hmac" | CommonCodecs.bytes32)).as[OnionRoutingPacket] - val paymentOnionPacketCodec: Codec[OnionPacket] = onionPacketCodec(Sphinx.PaymentPacket.PayloadLength) + val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = onionPacketCodec(Sphinx.PaymentPacket.PayloadLength) val perHopPayloadCodec: Codec[PerHopPayload] = ( ("realm" | constant(ByteVector.fromByte(0))) :: @@ -53,4 +53,13 @@ object OnionCodecs { ("outgoing_cltv_value" | uint32) :: ("unused_with_v0_version_on_header" | ignore(8 * 12))).as[PerHopPayload] + /** + * The 1.1 BOLT spec changed the onion 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). + * That varint is considered to be part of the payload, so the payload length includes the number of bytes used by + * the varint prefix. + */ + val payloadLengthDecoder = Decoder[Long]((bits: BitVector) => + CommonCodecs.varintoverflow.decode(bits).map(d => DecodeResult(d.value + (bits.length - d.remainder.length) / 8, d.remainder))) + } \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index 5fcc228451..13460d4298 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -37,7 +37,7 @@ object TestConstants { val fundingSatoshis = 1000000L val pushMsat = 200000000L val feeratePerKw = 10000L - val emptyOnionPacket = wire.OnionPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes) + val emptyOnionPacket = wire.OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(1300)(0), ByteVector32.Zeroes) def sqliteInMemory() = DriverManager.getConnection("jdbc:sqlite::memory:") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index dd503dbf88..d2ad0307bf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -104,13 +104,13 @@ class SphinxSpec extends FunSuite { test("is last packet") { val testCases = Seq( // Bolt 1.0 payloads use the next packet's hmac to signal termination. - (true, DecryptedPacket(hex"00", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), - (false, DecryptedPacket(hex"00", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + (true, DecryptedPacket(hex"00", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), + (false, DecryptedPacket(hex"00", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), // Bolt 1.1 payloads may use either the next packet's hmac or a tlv type to signal termination. - (true, DecryptedPacket(hex"0101", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), - (false, DecryptedPacket(hex"0101", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), - (true, DecryptedPacket(hex"0100", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), - (false, DecryptedPacket(hex"0101", OnionPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)) + (true, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), + (false, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + (true, DecryptedPacket(hex"0100", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + (false, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)) ) for ((expected, packet) <- testCases) { @@ -119,10 +119,10 @@ class SphinxSpec extends FunSuite { } test("bad onion") { - val badOnions = Seq[wire.OnionPacket]( - wire.OnionPacket(1, ByteVector.fill(33)(0), ByteVector.fill(65)(1), ByteVector32.Zeroes), - wire.OnionPacket(0, ByteVector.fill(33)(0), ByteVector.fill(65)(1), ByteVector32.Zeroes), - wire.OnionPacket(0, publicKeys.head.value, ByteVector.fill(42)(1), ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")) + val badOnions = Seq[wire.OnionRoutingPacket]( + wire.OnionRoutingPacket(1, ByteVector.fill(33)(0), ByteVector.fill(65)(1), ByteVector32.Zeroes), + wire.OnionRoutingPacket(0, ByteVector.fill(33)(0), ByteVector.fill(65)(1), ByteVector32.Zeroes), + wire.OnionRoutingPacket(0, publicKeys.head.value, ByteVector.fill(42)(1), ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")) ) val expected = Seq[BadOnion]( @@ -339,7 +339,7 @@ object SphinxSpec { import fr.acinq.eclair.wire.LightningMessageCodecs - def serializePaymentOnion(onion: OnionPacket): ByteVector = + def serializePaymentOnion(onion: OnionRoutingPacket): ByteVector = OnionCodecs.paymentOnionPacketCodec.encode(onion).require.toByteVector val privKeys = Seq( diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala index bbef3cae9d..1ab76696c5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala @@ -30,7 +30,7 @@ class OnionCodecsSpec extends FunSuite { test("encode/decode onion packet") { val bin = hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf" - val expected = OnionPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172", ByteVector32(hex"65f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf")) + val expected = OnionRoutingPacket(0, hex"02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", hex"e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f9172", ByteVector32(hex"65f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf")) val decoded = paymentOnionPacketCodec.decode(bin.bits).require.value assert(decoded === expected) @@ -54,4 +54,21 @@ class OnionCodecsSpec extends FunSuite { } } + test("decode payload length") { + val testCases = Seq( + (1, hex"00"), + (43, hex"2a 0000"), + (253, hex"fc 0000"), + (256, hex"fdfd00 000000"), + (260, hex"fd0101 00"), + (65538, hex"fdffff 00"), + (65541, hex"fe00000100 00"), + (4294967305L, hex"ff0000000001000000 00") + ) + + for ((payloadLength, bin) <- testCases) { + assert(payloadLengthDecoder.decode(bin.bits).require.value === payloadLength) + } + } + } From 52d702393f828d7d9d8e817c2f44bcaded395886 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 8 Jul 2019 11:00:57 +0200 Subject: [PATCH 18/27] Clean-up a few nits: * Remove redundant comments * Rename InvalidOnion error --- .../main/scala/fr/acinq/eclair/payment/Relayer.scala | 2 +- .../scala/fr/acinq/eclair/wire/FailureMessage.scala | 4 ++-- .../test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala | 10 +--------- .../acinq/eclair/wire/FailureMessageCodecsSpec.scala | 4 ++-- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 3c8e7c1a31..35039fb792 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -237,7 +237,7 @@ object Relayer { Right(RelayPayload(add, perHopPayload, nextPacket)) case Attempt.Failure(_) => // Onion is correctly encrypted but the content of the per-hop payload couldn't be parsed. - Left(InvalidOnion(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) + Left(InvalidOnionPayload(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) } case Left(badOnion) => Left(badOnion) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index e2fa2d41a1..3d738ad137 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -53,7 +53,7 @@ case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 6 def message = "ephemeral key was unparsable by the processing node" } -case class InvalidOnion(onionHash: ByteVector32) extends BadOnion with Perm { +case class InvalidOnionPayload(onionHash: ByteVector32) extends BadOnion with Perm { def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM def message = "onion per-hop payload could not be parsed" } @@ -91,7 +91,7 @@ object FailureMessageCodecs { .typecase(NODE | 2, provide(TemporaryNodeFailure)) .typecase(PERM | 2, provide(PermanentNodeFailure)) .typecase(PERM | NODE | 3, provide(RequiredNodeFeatureMissing)) - .typecase(BADONION | PERM, sha256.as[InvalidOnion]) + .typecase(BADONION | PERM, sha256.as[InvalidOnionPayload]) .typecase(BADONION | PERM | 4, sha256.as[InvalidOnionVersion]) .typecase(BADONION | PERM | 5, sha256.as[InvalidOnionHmac]) .typecase(BADONION | PERM | 6, sha256.as[InvalidOnionKey]) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index d2ad0307bf..1d97602c61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -68,18 +68,12 @@ class SphinxSpec extends FunSuite { assert(sharedsecrets(4) == ByteVector32(hex"b5756b9b542727dbafc6765a49488b023a725d631af688fc031217e90770c328")) } - /* - filler = 0xc6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac - */ test("generate filler with fixed-size payloads (reference test vector)") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = PaymentPacket.generateFiller("rho", sharedsecrets.dropRight(1), referenceFixedSizePayloads.dropRight(1)) assert(filler == hex"c6b008cf6414ed6e4c42c291eb505e9f22f5fe7d0ecdd15a833f4d016ac974d33adc6ea3293e20859e87ebfb937ba406abd025d14af692b12e9c9c2adbe307a679779259676211c071e614fdb386d1ff02db223a5b2fae03df68d321c7b29f7c7240edd3fa1b7cb6903f89dc01abf41b2eb0b49b6b8d73bb0774b58204c0d0e96d3cce45ad75406be0bc009e327b3e712a4bd178609c00b41da2daf8a4b0e1319f07a492ab4efb056f0f599f75e6dc7e0d10ce1cf59088ab6e873de377343880f7a24f0e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a851391377d3406a35a9af3ac") } - /* - filler = 0xb77d99c935d3f32469844f7e09340a91ded147557bdd0456c369f7e449587c0f5666faab58040146db49024db88553729bce12b860391c29c1779f022ae48a9cb314ca35d73fc91addc92632bcf7ba6fd9f38e6fd30fabcedbd5407b6648073c38331ee7ab0332f41f550c180e1601f8c25809ed75b3a1e78635a2ef1b828e92c9658e76e49f995d72cf9781eec0c838901d0bdde3ac21c13b4979ac9e738a1c4d0b9741d58e777ad1aed01263ad1390d36a18a6b92f4f799dcf75edbb43b7515e8d72cb4f827a9af0e7b9338d07b1a24e0305b5535f5b851b1144bad6238b9d9482b5ba6413f1aafac3cdde5067966ed8b78f7c1c5f916a05f874d5f17a2b7d0ae75d66a5f1bb6ff932570dc5a0cf3ce04eb5d26bc55c2057af1f8326e20a7d6f0ae644f09d00fac80de60f20aceee85be41a074d3e1dda017db79d0070b99f54736396f206ee3777abd4c00a4bb95c871750409261e3b01e59a3793a9c20159aae4988c68397a1443be6370fd9614e46108291e615691729faea58537209fa668a172d066d0efff9bc77c2bd34bd77870ad79effd80140990e36731a0b72092f8d5bc8cd346762e93b2bf203d00264e4bc136fc142de8f7b69154deb05854ea88e2d7506222c95ba1aab065c8a - */ test("generate filler with variable-size payloads") { val (_, sharedsecrets) = computeEphemeralPublicKeysAndSharedSecrets(sessionKey, publicKeys) val filler = PaymentPacket.generateFiller("rho", sharedsecrets.dropRight(1), referenceVariableSizePayloads.dropRight(1)) @@ -255,7 +249,7 @@ class SphinxSpec extends FunSuite { val packet = FailurePacket.wrap( FailurePacket.wrap( - FailurePacket.create(sharedSecrets.head, InvalidOnion(ByteVector32.Zeroes)), + FailurePacket.create(sharedSecrets.head, InvalidOnionPayload(ByteVector32.Zeroes)), sharedSecrets(1)), sharedSecrets(2)) @@ -337,8 +331,6 @@ class SphinxSpec extends FunSuite { object SphinxSpec { - import fr.acinq.eclair.wire.LightningMessageCodecs - def serializePaymentOnion(onion: OnionRoutingPacket): ByteVector = OnionCodecs.paymentOnionPacketCodec.encode(onion).require.toByteVector diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index 01110d4e22..ca2425996e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -45,7 +45,7 @@ class FailureMessageCodecsSpec extends FunSuite { test("encode/decode all channel messages") { val msgs: List[FailureMessage] = InvalidRealm :: TemporaryNodeFailure :: PermanentNodeFailure :: RequiredNodeFeatureMissing :: - InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnion(randomBytes32) :: + InvalidOnionVersion(randomBytes32) :: InvalidOnionHmac(randomBytes32) :: InvalidOnionKey(randomBytes32) :: InvalidOnionPayload(randomBytes32) :: TemporaryChannelFailure(channelUpdate) :: PermanentChannelFailure :: RequiredChannelFeatureMissing :: UnknownNextPeer :: AmountBelowMinimum(123456, channelUpdate) :: FeeInsufficient(546463, channelUpdate) :: IncorrectCltvExpiry(1211, channelUpdate) :: ExpiryTooSoon(channelUpdate) :: IncorrectOrUnknownPaymentDetails(123456L) :: IncorrectPaymentAmount :: FinalExpiryTooSoon :: FinalIncorrectCltvExpiry(1234) :: ChannelDisabled(0, 1, channelUpdate) :: ExpiryTooFar :: Nil @@ -64,7 +64,7 @@ class FailureMessageCodecsSpec extends FunSuite { InvalidOnionVersion(randomBytes32), InvalidOnionHmac(randomBytes32), InvalidOnionKey(randomBytes32), - InvalidOnion(randomBytes32) + InvalidOnionPayload(randomBytes32) ) msgs.foreach { From 5946b6a55fefa636ada17ea64e3ca99b8dba9f91 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 8 Jul 2019 11:40:52 +0200 Subject: [PATCH 19/27] Add function to extract failure code from failure message --- .../fr/acinq/eclair/payment/Relayer.scala | 2 +- .../fr/acinq/eclair/wire/FailureMessage.scala | 29 +++++++------------ .../wire/FailureMessageCodecsSpec.scala | 19 +++++------- 3 files changed, 19 insertions(+), 31 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 35039fb792..7800797d04 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -119,7 +119,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR } case Left(badOnion) => log.warning(s"couldn't parse onion: reason=${badOnion.message}") - val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, badOnion.failureCode, commit = true) + val cmdFail = CMD_FAIL_MALFORMED_HTLC(add.id, badOnion.onionHash, FailureMessageCodecs.failureCode(badOnion), commit = true) log.info(s"rejecting htlc #${add.id} paymentHash=${add.paymentHash} from channelId=${add.channelId} reason=malformed onionHash=${cmdFail.onionHash} failureCode=${cmdFail.failureCode}") commandBuffer ! CommandBuffer.CommandSend(add.channelId, add.id, cmdFail) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index 3d738ad137..aca3d9210f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -31,8 +31,7 @@ import scodec.{Attempt, Codec} // @formatter:off sealed trait FailureMessage { def message: String } -sealed trait FailureCode { def failureCode: Int } -sealed trait BadOnion extends FailureMessage with FailureCode { def onionHash: ByteVector32 } +sealed trait BadOnion extends FailureMessage { def onionHash: ByteVector32 } sealed trait Perm extends FailureMessage sealed trait Node extends FailureMessage sealed trait Update extends FailureMessage { def update: ChannelUpdate } @@ -41,22 +40,10 @@ case object InvalidRealm extends Perm { def message = "realm was not understood case object TemporaryNodeFailure extends Node { def message = "general temporary failure of the processing node" } case object PermanentNodeFailure extends Perm with Node { def message = "general permanent failure of the processing node" } case object RequiredNodeFeatureMissing extends Perm with Node { def message = "processing node requires features that are missing from this onion" } -case class InvalidOnionVersion(onionHash: ByteVector32) extends BadOnion with Perm { - def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 4 - def message = "onion version was not understood by the processing node" -} -case class InvalidOnionHmac(onionHash: ByteVector32) extends BadOnion with Perm { - def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 5 - def message = "onion HMAC was incorrect when it reached the processing node" -} -case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { - def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM | 6 - def message = "ephemeral key was unparsable by the processing node" -} -case class InvalidOnionPayload(onionHash: ByteVector32) extends BadOnion with Perm { - def failureCode = FailureMessageCodecs.BADONION | FailureMessageCodecs.PERM - def message = "onion per-hop payload could not be parsed" -} +case class InvalidOnionVersion(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion version was not understood by the processing node" } +case class InvalidOnionHmac(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion HMAC was incorrect when it reached the processing node" } +case class InvalidOnionKey(onionHash: ByteVector32) extends BadOnion with Perm { def message = "ephemeral key was unparsable by the processing node" } +case class InvalidOnionPayload(onionHash: ByteVector32) extends BadOnion with Perm { def message = "onion per-hop payload could not be parsed" } case class TemporaryChannelFailure(update: ChannelUpdate) extends Update { def message = s"channel ${update.shortChannelId} is currently unavailable" } case object PermanentChannelFailure extends Perm { def message = "channel is permanently unavailable" } case object RequiredChannelFeatureMissing extends Perm { def message = "channel requires features not present in the onion" } @@ -111,6 +98,12 @@ object FailureMessageCodecs { .typecase(19, ("amountMsat" | uint64overflow).as[FinalIncorrectHtlcAmount]) .typecase(21, provide(ExpiryTooFar)) + /** + * Return the failure code for a given failure message. This method actually encodes the failure message, which is a + * bit clunky and not particularly efficient. It shouldn't be used on the application's hot path. + */ + def failureCode(failure: FailureMessage): Int = failureMessageCodec.encode(failure).flatMap(uint16.decode).require.value + /** * An onion-encrypted failure from an intermediate node: * +----------------+----------------------------------+-----------------+----------------------+-----+ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index ca2425996e..4cb1fee2b2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -22,7 +22,6 @@ import fr.acinq.eclair.{ShortChannelId, randomBytes32, randomBytes64} import fr.acinq.eclair.wire.FailureMessageCodecs._ import org.scalatest.FunSuite import scodec.bits._ -import scodec.codecs.uint16 /** * Created by PM on 31/05/2016. @@ -60,19 +59,15 @@ class FailureMessageCodecsSpec extends FunSuite { } test("bad onion failure code") { - val msgs = Seq( - InvalidOnionVersion(randomBytes32), - InvalidOnionHmac(randomBytes32), - InvalidOnionKey(randomBytes32), - InvalidOnionPayload(randomBytes32) + val msgs = Map( + (BADONION | PERM | 4) -> InvalidOnionVersion(randomBytes32), + (BADONION | PERM | 5) -> InvalidOnionHmac(randomBytes32), + (BADONION | PERM | 6) -> InvalidOnionKey(randomBytes32), + (BADONION | PERM) -> InvalidOnionPayload(randomBytes32) ) - msgs.foreach { - msg => { - val encoded = failureMessageCodec.encode(msg).require.toByteVector - val failureCode = uint16.decode(encoded.take(2).toBitVector).require.value - assert(failureCode === msg.failureCode) - } + for ((code, message) <- msgs) { + assert(failureCode(message) === code) } } From 4c6a05aa418a92b0330c2350075db3a04bba22c5 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 8 Jul 2019 13:48:21 +0200 Subject: [PATCH 20/27] Clean-up failureOnionCodec --- .../scala/fr/acinq/eclair/wire/FailureMessage.scala | 6 ++---- .../acinq/eclair/wire/FailureMessageCodecsSpec.scala | 11 ++++------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index aca3d9210f..55d21eafd7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -115,8 +115,6 @@ object FailureMessageCodecs { paddedFixedSizeBytesDependent( 260, "failureMessage" | variableSizeBytes(uint16, FailureMessageCodecs.failureMessageCodec), - nBits => { - val nBytes = (nBits / 8).toInt - variableSizeBytes(uint16, bytes(nBytes - 2)).unit(ByteVector.empty) - }).as[FailureMessage], mac) + nBits => "padding" | variableSizeBytes(uint16, ignore(nBits - 2 * 8)) // two bytes are used to encode the padding length + ).as[FailureMessage], mac) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala index 4cb1fee2b2..3b3ecd7a61 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/FailureMessageCodecsSpec.scala @@ -73,15 +73,12 @@ class FailureMessageCodecsSpec extends FunSuite { test("encode/decode failure onion") { val codec = failureOnionCodec(Hmac256(ByteVector32.Zeroes)) - val testCases = Seq( - hex"41a824e2d630111669fa3e52b600a518f369691909b4e89205dc624ee17ed2c1 0022 c006 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00de 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", - hex"ba6e122b2941619e2106e8437bf525356ffc8439ac3b2245f68546e298a08cc6 000a 400f 000000000000002a 00f6 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - ) zip Seq( - InvalidOnionKey(ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")), - IncorrectOrUnknownPaymentDetails(42) + val testCases = Map( + InvalidOnionKey(ByteVector32(hex"2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a")) -> hex"41a824e2d630111669fa3e52b600a518f369691909b4e89205dc624ee17ed2c1 0022 c006 2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a2a 00de 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + IncorrectOrUnknownPaymentDetails(42) -> hex"ba6e122b2941619e2106e8437bf525356ffc8439ac3b2245f68546e298a08cc6 000a 400f 000000000000002a 00f6 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) - for ((bin, expected) <- testCases) { + for ((expected, bin) <- testCases) { val decoded = codec.decode(bin.toBitVector).require.value assert(decoded === expected) From 0c13aea6e4effdd038b74126b0f9441da66b192d Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 8 Jul 2019 14:50:39 +0200 Subject: [PATCH 21/27] Rename to OnionRouting everywhere. It's clearer like that. --- .../src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala | 6 +++--- eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 203f2809cb..9e4de4323d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -125,7 +125,7 @@ object Sphinx extends Logging { */ case class PacketAndSecrets(packet: wire.OnionRoutingPacket, sharedSecrets: Seq[(ByteVector32, PublicKey)]) - sealed trait OnionPacket { + sealed trait OnionRoutingPacket { /** * Supported packet version. Note that since this value is outside of the onion encrypted payload, intermediate @@ -272,14 +272,14 @@ object Sphinx extends Logging { * When an invalid onion is received, its hash should be included in the failure message. */ def hash(onion: wire.OnionRoutingPacket): ByteVector32 = - Crypto.sha256(wire.OnionCodecs.onionPacketCodec(onion.payload.length.toInt).encode(onion).require.toByteVector) + Crypto.sha256(wire.OnionCodecs.onionRoutingPacketCodec(onion.payload.length.toInt).encode(onion).require.toByteVector) } /** * A payment onion packet is used when offering an HTLC to a remote node. */ - object PaymentPacket extends OnionPacket { + object PaymentPacket extends OnionRoutingPacket { override val PayloadLength = 1300 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala index cea34feb01..1c81c21c8c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala @@ -38,13 +38,13 @@ case class PerHopPayload(shortChannelId: ShortChannelId, object OnionCodecs { - def onionPacketCodec(payloadLength: Int): Codec[OnionRoutingPacket] = ( + def onionRoutingPacketCodec(payloadLength: Int): Codec[OnionRoutingPacket] = ( ("version" | uint8) :: ("publicKey" | bytes(33)) :: ("onionPayload" | bytes(payloadLength)) :: ("hmac" | CommonCodecs.bytes32)).as[OnionRoutingPacket] - val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = onionPacketCodec(Sphinx.PaymentPacket.PayloadLength) + val paymentOnionPacketCodec: Codec[OnionRoutingPacket] = onionRoutingPacketCodec(Sphinx.PaymentPacket.PayloadLength) val perHopPayloadCodec: Codec[PerHopPayload] = ( ("realm" | constant(ByteVector.fromByte(0))) :: From 9506dd7b618d728104c5e7c5ef1552d313f3f0e1 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 8 Jul 2019 16:36:14 +0200 Subject: [PATCH 22/27] Add error log if onion payload decoder has remaining bytes --- .../scala/fr/acinq/eclair/payment/Relayer.scala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index 7800797d04..fae3c4aee2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -29,6 +29,7 @@ import fr.acinq.eclair.payment.PaymentLifecycle.{PaymentFailed, PaymentSucceeded import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.wire._ import fr.acinq.eclair.{NodeParams, ShortChannelId, nodeFee} +import grizzled.slf4j.Logging import scodec.{Attempt, DecodeResult} import scala.collection.mutable @@ -205,7 +206,7 @@ class Relayer(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorR } -object Relayer { +object Relayer extends Logging { def props(nodeParams: NodeParams, register: ActorRef, paymentHandler: ActorRef) = Props(classOf[Relayer], nodeParams, register, paymentHandler) case class OutgoingChannel(nextNodeId: PublicKey, channelUpdate: ChannelUpdate, commitments: Commitments) @@ -231,10 +232,13 @@ object Relayer { Sphinx.PaymentPacket.peel(privateKey, add.paymentHash, add.onionRoutingPacket) match { case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => OnionCodecs.perHopPayloadCodec.decode(payload.bits) match { - case Attempt.Successful(DecodeResult(perHopPayload, _)) if p.isLastPacket => - Right(FinalPayload(add, perHopPayload)) - case Attempt.Successful(DecodeResult(perHopPayload, _)) => - Right(RelayPayload(add, perHopPayload, nextPacket)) + case Attempt.Successful(DecodeResult(perHopPayload, remainder)) => + if (remainder.nonEmpty) + logger.warn(s"${remainder.length} bits remaining after per-hop payload decoding: there might be an issue with the onion codec") + if (p.isLastPacket) + Right(FinalPayload(add, perHopPayload)) + else + Right(RelayPayload(add, perHopPayload, nextPacket)) case Attempt.Failure(_) => // Onion is correctly encrypted but the content of the per-hop payload couldn't be parsed. Left(InvalidOnionPayload(Sphinx.PaymentPacket.hash(add.onionRoutingPacket))) From 9fa1dc395b074f191bc774f8b995d7911a773dca Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Tue, 9 Jul 2019 13:33:27 +0200 Subject: [PATCH 23/27] Update onion feature bit. --- eclair-core/src/main/scala/fr/acinq/eclair/Features.scala | 4 ++-- .../main/scala/fr/acinq/eclair/router/Announcements.scala | 4 +--- .../src/test/scala/fr/acinq/eclair/FeaturesSpec.scala | 6 +++--- .../scala/fr/acinq/eclair/router/AnnouncementsSpec.scala | 2 +- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala index c45178be4c..41f8e358ff 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Features.scala @@ -35,8 +35,8 @@ object Features { val CHANNEL_RANGE_QUERIES_BIT_MANDATORY = 6 val CHANNEL_RANGE_QUERIES_BIT_OPTIONAL = 7 - val OPTION_VARIABLE_LENGTH_ONION_MANDATORY = 0 - val OPTION_VARIABLE_LENGTH_ONION_OPTIONAL = 1 + val VARIABLE_LENGTH_ONION_MANDATORY = 8 + val VARIABLE_LENGTH_ONION_OPTIONAL = 9 def hasFeature(features: BitSet, bit: Int): Boolean = features.get(bit) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala index 1dafa921a1..1e76f48b7b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Announcements.scala @@ -75,7 +75,7 @@ object Announcements { def makeNodeAnnouncement(nodeSecret: PrivateKey, alias: String, color: Color, nodeAddresses: List[NodeAddress], timestamp: Long = Platform.currentTime.milliseconds.toSeconds): NodeAnnouncement = { require(alias.length <= 32) - val features = ByteVector.fromByte((1 << Features.OPTION_VARIABLE_LENGTH_ONION_OPTIONAL).byteValue) + val features = BitVector.fromLong(1 << Features.VARIABLE_LENGTH_ONION_OPTIONAL).bytes val witness = nodeAnnouncementWitnessEncode(timestamp, nodeSecret.publicKey, color, alias, features, nodeAddresses, unknownFields = ByteVector.empty) val sig = Crypto.sign(witness, nodeSecret) NodeAnnouncement( @@ -119,8 +119,6 @@ object Announcements { /** * This method compares channel updates, ignoring fields that don't matter, like signature or timestamp * - * @param u1 - * @param u2 * @return true if channel updates are "equal" */ def areSame(u1: ChannelUpdate, u2: ChannelUpdate): Boolean = diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala index 81cb1f4600..102e50e532 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/FeaturesSpec.scala @@ -44,15 +44,15 @@ class FeaturesSpec extends FunSuite { } test("'variable_length_onion' feature") { - assert(hasFeature(hex"01", Features.OPTION_VARIABLE_LENGTH_ONION_MANDATORY)) - assert(hasFeature(hex"02", Features.OPTION_VARIABLE_LENGTH_ONION_OPTIONAL)) + assert(hasFeature(hex"0100", Features.VARIABLE_LENGTH_ONION_MANDATORY)) + assert(hasFeature(hex"0200", Features.VARIABLE_LENGTH_ONION_OPTIONAL)) } test("features compatibility") { assert(areSupported(Protocol.writeUInt64(1l << INITIAL_ROUTING_SYNC_BIT_OPTIONAL, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1L << OPTION_DATA_LOSS_PROTECT_MANDATORY, ByteOrder.BIG_ENDIAN))) assert(areSupported(Protocol.writeUInt64(1l << OPTION_DATA_LOSS_PROTECT_OPTIONAL, ByteOrder.BIG_ENDIAN))) - assert(areSupported(Protocol.writeUInt64(1l << OPTION_VARIABLE_LENGTH_ONION_OPTIONAL, ByteOrder.BIG_ENDIAN))) + assert(areSupported(Protocol.writeUInt64(1l << VARIABLE_LENGTH_ONION_OPTIONAL, ByteOrder.BIG_ENDIAN))) assert(!areSupported(hex"14")) assert(!areSupported(hex"0141")) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala index f31a692dd1..d64bfab29f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/router/AnnouncementsSpec.scala @@ -49,7 +49,7 @@ class AnnouncementsSpec extends FunSuite { test("create valid signed node announcement") { val ann = makeNodeAnnouncement(Alice.nodeParams.privateKey, Alice.nodeParams.alias, Alice.nodeParams.color, Alice.nodeParams.publicAddresses) - assert(Features.hasFeature(ann.features, Features.OPTION_VARIABLE_LENGTH_ONION_OPTIONAL)) + assert(Features.hasFeature(ann.features, Features.VARIABLE_LENGTH_ONION_OPTIONAL)) assert(checkSig(ann)) assert(checkSig(ann.copy(timestamp = 153)) === false) } From 5a1c3b2ae0f352db7765b87ac35727cffda58f07 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Thu, 11 Jul 2019 17:37:18 +0200 Subject: [PATCH 24/27] Clean-up: sort imports --- .../src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala | 1 - .../scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala | 2 +- .../main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala | 2 +- eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala index 55d21eafd7..a7cee716dd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/FailureMessage.scala @@ -20,7 +20,6 @@ import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.crypto.Mac32 import fr.acinq.eclair.wire.CommonCodecs.{sha256, uint64overflow} import fr.acinq.eclair.wire.LightningMessageCodecs.{channelUpdateCodec, lightningMessageCodec} -import scodec.bits.ByteVector import scodec.codecs._ import scodec.{Attempt, Codec} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala index 8bb602003a..b7e3891c43 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageCodecs.scala @@ -18,8 +18,8 @@ package fr.acinq.eclair.wire import fr.acinq.eclair.wire import fr.acinq.eclair.wire.CommonCodecs._ -import scodec.codecs._ import scodec.Codec +import scodec.codecs._ /** * Created by PM on 15/11/2016. diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala index 25c1cfb032..89be06884a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/LightningMessageTypes.scala @@ -20,8 +20,8 @@ import java.net.{Inet4Address, Inet6Address, InetAddress, InetSocketAddress} import java.nio.charset.StandardCharsets import com.google.common.base.Charsets +import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, ByteVector64} -import fr.acinq.bitcoin.Crypto.{PublicKey, PrivateKey} import fr.acinq.eclair.{ShortChannelId, UInt64} import scodec.bits.ByteVector diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala index 1c81c21c8c..32d942f547 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/Onion.scala @@ -19,9 +19,9 @@ package fr.acinq.eclair.wire import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.ShortChannelId import fr.acinq.eclair.crypto.Sphinx -import scodec.{Codec, DecodeResult, Decoder} import scodec.bits.{BitVector, ByteVector} import scodec.codecs._ +import scodec.{Codec, DecodeResult, Decoder} /** * Created by t-bast on 05/07/2019. From b3232e471e328fc0a70f9ca3f85443e7aa6f2ec8 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Mon, 15 Jul 2019 13:23:29 +0200 Subject: [PATCH 25/27] Update test vectors to use big-endian varints --- .../fr/acinq/eclair/crypto/SphinxSpec.scala | 28 +++++++++---------- .../acinq/eclair/wire/OnionCodecsSpec.scala | 6 ++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 1d97602c61..9a0070a1d6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -86,7 +86,7 @@ class SphinxSpec extends FunSuite { 41 -> hex"08", 65 -> hex"00", 285 -> hex"fc", - 288 -> hex"fdfd00", + 288 -> hex"fd00fd", 65570 -> hex"fdffff" ) @@ -154,7 +154,7 @@ class SphinxSpec extends FunSuite { test("create packet with variable-size payloads (reference test vector)") { val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, referenceVariableSizePayloads, associatedData) - assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7181924e4b6c645f2b6eecc821084ec9b16dfb9d2e7622d4c14db4fc5ecdfc07eac50f7d61ab590531cf08000178a333a347f8b4072e7d3ae44f4f309e150b49886d3f044cd6462be389c830784aba767682923c8683404aaf9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf050597c681185f336b1da63b0939aa2b7c50b21b5eb7b6ad66c81fab98a3cdf73f658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d7fca6a0e4ecc9f9de611a90da6e99c39551094c56e3196f282c5dffd9fc4b2fc12f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa337ef811d5345a9e16b60de1767b209645ba40bd1f9a5f75bc04feca9b27c5554be4fe83fac2cb83aa447a817bb85ae966c68b420063833fada375e2f515965e687a45699632902672c654d1d18d7bcbf55e8fa57f63f2da449f8e1e606e8722df081e5f193fc4179feb99ad22819afdeef211f7c54afdba92aeef0c00b7bc2b65a4813c01f907a8377585708f2d4c940a25328e585714c8ded0a9a4d7a6de1027c1cb7a0198cd3db68b58c0704dfd0cfbe624e9cd18cc0ae5d96697bb476708b9ee0403d211e64e0d5a7683a7a9a140c02f0ff1c6e67a302941b4052bdea8a63e70a3ad62c5b89c698f1fd3c7685cb49705096cad702d02d93bcb1c27a409f4c9bddec001205ca4a2740f19b50900be81c7e847f1a863deea8d35701f1355cad8db57b1d4eb2ab4e29587734785abfb46ddede71928213d7d089dfdeda052827f459f1688cc0935bd47e7bcec27427c8376dcce7e22699567c0d145f8a7db33f6758815f1f15f9f7a9760dec4f34ae095edda4c64e9735bdd029c4e32c2ee31ba47ec5e6bdb97813d52dbd15b4e0b7a2c7f790ae64104d99f38c127f0a093288fa34144adb16b8968d4fa7656fcec99de8503dd46d3b03620a71c7cd085364abd30dccf7fbda25a1cdc102600149c9af1c97aa0372cd2e1909f28ac5c686f432b310e79528c9b8b9e8f314c1e74621ce6308ad2278b81d460892e0d9dd38b7c76d58be6dfd10ae7583ee1e7ef5b3f6f78dc60af0950df1b00cc55b6d178ba2e476bea0eaeef49323b83f05804159e7aef4eed4cc60dd07be76f067dfd0bcfb0b806b69ba921336a20c43c832d0cab8fa3ddeb29e3bf07b0d98a112eb07802756235a49d44a8b82a950d84e95e01971f0e106ccb337f07384e21620e0ad39e16ed9edca123226cf55ac44f449eeb53e38a7f27d101806e4823e4efcc887414240ee6826c4a5cb1c6443ad36ebf905a435c1d9054e54173911b17b5b40f60b3d9fd5f12eac54ca1e20191f5f18544d5fd3d665e9bcef96fb44b76110aa64d9db4c86c9513cbdad546538e8aec521fbe83ceac5e74a15629f1ed0b870a1d0d1e5680b6d6100d1bdd9a2ba3be3772f71854cb12ae9b3afd87cda738dcd107fe56a15f1877450cd0e") + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71a060daf367132b378b3a3883c0e2c0e026b8900b2b5cdbc784e1a3bb913f88a9c50f7d61ab590531cf08000178a333a347f8b4072ed056f820f77774345e183a342ec4729f3d84accf515e88adddb85ecc08daba68404bae9a8e8d7178977d7094a1ae549f89338c0777551f874159eb42d3a59fb9285ad4e24883f27de23942ec966611e99bee1cee503455be9e8e642cef6cef7b9864130f692283f8a973d47a8f1c1726b6e59969385975c766e35737c8d76388b64f748ee7943ffb0e2ee45c57a1abc40762ae598723d21bd184e2b338f68ebff47219357bd19cd7e01e2337b806ef4d717888e129e59cd3dc31e6201ccb2fd6d7499836f37a993262468bcb3a4dcd03a22818aca49c6b7b9b8e9e870045631d8e039b066ff86e0d1b7291f71cefa7264c70404a8e538b566c17ccc5feab231401e6c08a01bd5edfc1aa8e3e533b96e82d1f91118d508924b923531929aea889fcdf050597c681185f336b1da63b0939aa2b7c50b21b5eb7b6ad66c81fab98a3cdf73f658149e7e9ced4edde5d38c9b8f92e16f6b4ab13d7fca6a0e4ecc9f9de611a90da6e99c39551094c56e3196f282c5dffd9fc4b2fc12f3bca8e6fe47eb45fbdd3be21a8a8d200797eae3c9a0497132f92410d804977408494dff49dd3d8bce248e0b74fd9e6f0f7102c25ddfa02bd9ad9f746abbfa337ef811d5345a9e16b60de1767b209645ba40bd1f9a5f75bc04feca9b27c5554be4fe83fac2cb83aa447a817bb85ae966c68b420063833fada375e2f515965e687a45699632902672c654d1d18d7bcbf55e8fa57f63f2da449f8e1e606e8722df081e5f193fc4179feb99ad22819afdeef211f7c54afdba92aeef0c00b7bc2b65a4813c01f907a8377585708f2d4c940a25328e585714c8ded0a9a4d7a6de1027c1cb7a0198cd3db68b58c0704dfd0cfbe624e9cd18cc0ae5d96697bb476708b9ee0403d211e64e0d5a7683a7a9a140c02f0ff1c6e67a302941b4052bdea8a63e70a3ad62c5b89c698f1fd3c7685cb49705096cad702d02d93bcb1c27a409f4c9bddec001205ca4a2740f19b50900be81c7e847f1a863deea8d35701f1355cad8db57b1d4eb2ab4e29587734785abfb46ddede71928213d7d089dfdeda052827f459f1688cc0935bd47e7bcec27427c8376dcce7e22699567c0d145f8a7db33f6758815f1f15f9f7a9760dec4f34ae095edda4c64e9735bdd029c4e32c2ee31ba47ec5e6bdb97813d52dbd15b4e0b7a2c7f790ae64104d99f38c127f0a093288fa34144adb16b8968d4fa7656fcec99de8503dd46d3b03620a71c7cd085364abd30dccf7fbda25a1cdc102600149c9af1c97aa0372cd2e1909f28ac5c686f432b310e79528c9b8b9e8f314c1e74621ce6308ad2278b81d460892e0d9dd38b7c76d58be6dfd10ae7583ee1e7ef5b3f6f78dc60af0950df1b00cc55b6d178ba2e476bea0eaeef49323b83f05804159e7aef4eed4cc60dd07be76f067dfd0bcfb0b806b69ba921336a20c43c832d0cab8fa3ddeb29e3bf07b0d98a112eb07802756235a49d44a8b82a950d84e95e01971f0e106ccb337f07384e21620e0ad39e16ed9edca123226cf55ac44f449eeb53e38a7f27d101806e4823e4efcc887414240ee6826c4a5cb1c6443ad36ebf905a435c1d9054e54173911b17b5b40f60b3d9fd5f12eac54ca1e20191f5f18544d5fd3d665e9bcef96fb44b76110aa64d9db4c86c9513cbdad546538e8aec521fbe83ceac5e74a15629f1ed0b870a1d0d1e5680b6d6100d1bd3f3b9043bd35b8919c4088f1949b8be89e4701eb870f8ed64fafa446c78df3ea") val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) @@ -165,8 +165,8 @@ class SphinxSpec extends FunSuite { assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) - assert(packets(0).hmac == ByteVector32(hex"c0d77014e146e91e76fc667514aa5011f6b100b1f6f3509dc4d434010b2daf91")) - assert(packets(1).hmac == ByteVector32(hex"5c5ee40f24efa5e5332c1ffec3e6ba8c6e486c5b32baae839ee43a3935d650ca")) + assert(packets(0).hmac == ByteVector32(hex"e125e4acea319d02932a96d7dc065940bdf20d94ab8d5f9b0d816be457ee20d2")) + assert(packets(1).hmac == ByteVector32(hex"f132f8609ca84fc4667dada7d22684c515e7231dabd45e64f92a1277af306c21")) assert(packets(2).hmac == ByteVector32(hex"4a630bdc56575e956627d7f191e731fabf110ef0044f8f64f4dcea79c2dcb995")) assert(packets(3).hmac == ByteVector32(hex"a2a89cf333e198b68904ce59ddceb9f989ebfa1ad534fa74ee85e41ff303c3a0")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) @@ -174,7 +174,7 @@ class SphinxSpec extends FunSuite { test("create packet with variable-size payloads filling the onion") { val PacketAndSecrets(onion, sharedSecrets) = PaymentPacket.create(sessionKey, publicKeys, variableSizePayloadsFull, associatedData) - assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866196ef84350c2a76fc232b5d46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6101810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bf74b2ce49922898e9353fa268086c00ae8b7f718405b72ad3829dbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf776b75ebb389bf84d0bfbf58590e510e034572a01e409c30939dc3c0b7d6a68a371019f5378bf6133fe0de00dc6f90d98ce1cbe3f165182ca37d6208da0f6bafd75ff41903ab352a1f47586eae3c6c8e437d4308766f71052b46ba2efbd87c0a781e8b3f456300fc7efbefc78ab515338666aed2070e674143c30b520b9cc1782ba8b46454db0d4ce72589cfc2eafb2db452ec98573ad08496483741de5376bfc7357fc6ea629e31236ba6ba7703014959129141a1719788ec83884f2e9151a680e2a96d2bcc67a8a2935aa11acee1f9d04812045b4ae5491220313756b5b9a0a6f867f2a95be1fab14870f04eeab694d9594620632b14ec4b424b495914f3dc587f75cd4582c113bb61e34a0fa7f79f97463be4e3c6fb99516889ed020acee419bb173d38e5ba18a00065e11fd733cf9ae46505dbb4ef70ef2f502601f4f6ee1fdb9d17435e15080e962f24760843f35bac1ac079b694ff7c347c1ed6a87f02b0758fbf00917652641cb68f584fd830b6c738e03b424ea0bc753d24306d7b691693d3286706fee7d57a939e28d77b3da47efc072436a3fd7f9c40515af8c4903764301e62b57153a5ca03ff5bb49c7dc8d3b2858100fb4aa5df7a94a271b73a76129445a3ea180d84d19029c003c164db926ed6983e5219028721a294f145e3fcc20915b8a2147efc896ee5d314e01874ea9e7bc1f386ba6b8f262942aa0193a537ffc91b1ccc9171a3c2016ecf387a3cd8b79df80a8e9412e707cb9c761a0809a84c606a779567f9f0edf685b38c98877e90d02aedd096ed841e50abf2114ce01efbff04788fb280f870eca20c7ec353d5c381903e7d08fc57695fd79c27d43e7bd603a876068d3f1c7f45af99003e5eec7e8d8c91e395320f1fc421ef3552ea033129429383304b760c8f93de342417c3223c2112a623c3514480cdfae8ec15a99abfca71b03a8396f19edc3d5000bcfb77b5544813476b1b521345f4da396db09e783870b97bc2034bd11611db30ed2514438b046f1eb7093eceddfb1e73880786cd7b540a3896eaadd0a0692e4b19439815b5f2ec855ec8ececce889442a64037e9564521fce926613b5d3074246c5a34a296ad1a18ef556d73fcd6c85ea3fdfb03b4e8e4bb0e35997fc35544d3c2a00e5e1f45dc925607d952c6a89721bd0b6f6aec03314d667166a5b8b18471403be7018b2479aaef6c7c6c554a50a98b717dff06d50be39fb36dc03e678e0a52fc615be46b223e3bee83fa0c7c47a1f29fb94f1e9eebf6c9ecf8fc79ae847df2effb60d07aba301fc536546ec4899eedb4fec9a9bed79e3a83c4b32757745778e977e485c67c0f12bbc82c0b3bb0f4df0bd13d046fed4446f54cd85bfce55ef781a80e5f63d289d08de001237928c2a4e0c8694d0c1e68cc23f2409f30009019085e831a928e7bc5b00a1f29d25482f7fd0b6dad30e6ef8edc68ddf7db404ea7d11540fc2cee74863d64af4c945457e04b7bea0a5fb8636edadb1e1d6f2630d61062b781c1821f46eddadf269ea1fada829547590081b16bc116e074cae0224a375f2d9ce16e836687c89cd285e3b40f1e59ce2caa3d1d8cf37ee4d5e3abe7ef0afd6ffeb4fd6905677b950894863c828ab8d93519566f69fa3c2129da763bf58d9c4d2837d4d9e13821258f7e7098b34218b846b5ee59284dc6791344738aac3d9a9014b764104cd606ef2c37a03fc7e") + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f2836866196ef84350c2a76fc232b5d46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6101810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bf74b2ce49922898e9353fa268086c00ae8b7f718405b72ad3829dbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf776b75ebb389bf84d0bfbf58590e510e034572a01e409c309396778760423a8d8754c52e9a01a8f0e271cba5068bab5ee5bd0b5cd98276b0e04d60ba6a0f6bafd75ff41903ab352a1f47586eae3c6c8e437d4308766f71052b46ba2efbd87c0a781e8b3f456300fc7efbefc78ab515338666aed2070e674143c30b520b9cc1782ba8b46454db0d4ce72589cfc2eafb2db452ec98573ad08496483741de5376bfc7357fc6ea629e31236ba6ba7703014959129141a1719788ec83884f2e9151a680e2a96d2bcc67a8a2935aa11acee1f9d04812045b4ae5491220313756b5b9a0a6f867f2a95be1fab14870f04eeab694d9594620632b14ec4b424b495914f3dc587f75cd4582c113bb61e34a0fa7f79f97463be4e3c6fb99516889ed020acee419bb173d38e5ba18a00065e11fd733cf9ae46505dbb4ef70ef2f502601f4f6ee1fdb9d17435e15080e962f24760843f35bac1ac079b694ff7c347c1ed6a87f02b0758fbf00917764716c68ed7d6e6c0e75ccdb6dc7fa59554784b3ad906127ea77a6cdd814662ee7d57a939e28d77b3da47efc072436a3fd7f9c40515af8c4903764301e62b57153a5ca03ff5bb49c7dc8d3b2858100fb4aa5df7a94a271b73a76129445a3ea180d84d19029c003c164db926ed6983e5219028721a294f145e3fcc20915b8a2147efc8b5d508339f64970feee3e2da9b9c9348c1a0a4df7527d0ae3f8ae507a5beb5c73c2016ecf387a3cd8b79df80a8e9412e707cb9c761a0809a84c606a779567f9f0edf685b38c98877e90d02aedd096ed841e50abf2114ce01efbff04788fb280f870eca20c7ec353d5c381903e7d08fc57695fd79c27d43e7bd603a876068d3f1c7f45af99003e5eec7e8d8c91e395320f1fc421ef3552ea033129429383304b760c8f93de342417c3223c2112a623c3514480cdfae8ec15a99abfca71b03a8396f19edc3d5000bcfb77b5544813476b1b521345f4da396db09e783870b97bc2034bd11611db30ed2514438b046f1eb7093eceddfb1e73880786cd7b540a3896eaadd0a0692e4b19439815b5f2ec855ec8ececce889442a64037e956452a3f7b86cb3780b3e316c8dde464bc74a60a85b613f849eb0b29daf81892877bd4be9ba5997fc35544d3c2a00e5e1f45dc925607d952c6a89721bd0b6f6aec03314d667166a5b8b18471403be7018b2479aaef6c7c6c554a50a98b717dff06d50be39fb36dc03e678e0a52fc615be46b223e3bee83fa0c7c47a1f29fb94f1e9eebf6c9ecf8fc79ae847df2effb60d07aba301fc536546ec4899eedb4fec9a9bed79e3a83c4b32757745778e977e485c67c0f12bbc82c0b3bb0f4df0bd13d046fed4446f54cd85bfce55ef781a80e5f63d289d08de001237928c2a4e0c8694d0c1e68cc23f2409f30009019085e831a928e7bc5b00a1f29d25482f7fd0b6dad30e6ef8edc68ddf7db404ea7d11540fc2cee74863d64af4c945457e04b7bea0a5fb8636edadb1e1d6f2630d61062b781c1821f46eddadf269ea1fada829547590081b16bc116e074cae0224a375f2d9ce16e836687c89cd285e3b40f1e59ce2caa3d1d8cf37ee4d5e3abe7ef0afd6ffeb4fd6905677b950894863c828ab8d93519566f69fa3c2129da763bf58d9c4d2837d4d9e13821258f7e7098b34f695a589bd9eb568ba51ee3014b2d3ba1d4cf9ebaed0231ed57ecea7bd918216") val Right(DecryptedPacket(payload0, nextPacket0, sharedSecret0)) = PaymentPacket.peel(privKeys(0), associatedData, onion) val Right(DecryptedPacket(payload1, nextPacket1, sharedSecret1)) = PaymentPacket.peel(privKeys(1), associatedData, nextPacket0) @@ -185,16 +185,16 @@ class SphinxSpec extends FunSuite { assert(Seq(sharedSecret0, sharedSecret1, sharedSecret2, sharedSecret3, sharedSecret4) == sharedSecrets.map(_._1)) val packets = Seq(nextPacket0, nextPacket1, nextPacket2, nextPacket3, nextPacket4) - assert(packets(0).hmac == ByteVector32(hex"3ed8abed86a03f4619bffc2c5f0f792b44bd439e888e1d02b9ff7a44b1fa34e8")) - assert(packets(1).hmac == ByteVector32(hex"36f8d5a549a2ef20c3f47487af2594f5bbd4f79717f8e5f47662abd63a38d6a1")) - assert(packets(2).hmac == ByteVector32(hex"7b2a5997e7e615f67de503e71b73c8507bee506ce8054e1846950b436b6959ce")) - assert(packets(3).hmac == ByteVector32(hex"48675a9343e6514a8666f7acfaa623e5cab6dfbd3f0b5954d69e1cb5d27e0025")) + assert(packets(0).hmac == ByteVector32(hex"859cd694cf604442547246f4fae144f255e71e30cb366b9775f488cac713f0db")) + assert(packets(1).hmac == ByteVector32(hex"259982a8af80bd3b8018443997fa5f74c48b488fff62e531be54b887d53fe0ac")) + assert(packets(2).hmac == ByteVector32(hex"58110c95368305b73ae15d22b884fda0482c60993d3ba4e506e37ff5021efb13")) + assert(packets(3).hmac == ByteVector32(hex"f45e7099e32b8973f54cbfd1f6c48e7e0b90718ad7b00a88e1e98cebeb6d3916")) assert(packets(4).hmac == ByteVector32(hex"0000000000000000000000000000000000000000000000000000000000000000")) } test("create packet with single variable-size payload filling the onion") { val PacketAndSecrets(onion, _) = PaymentPacket.create(sessionKey, publicKeys.take(1), variableSizeOneHopPayload, associatedData) - assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661918004735c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2cc49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f580c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce42e93a4d7c803ead45fc47cf4396d284632314d789e73cf3f534126c63fe244069d9e8a7c4f98e7e530fc588e648ef4e641364981b5377542d5e7a4aaab6d35f6df7d3a9d7ca715213599ee02c4dbea4dc78860febe1d29259c64b59b3333ffdaebbaff4e7b31c27a3791f6bf848a58df7c69bb2b1852d2ad357b9919ffdae570b27dc709fba087273d3a4de9e6a6be66db647fb6a8d1a503b3f481befb96745abf5cc4a6bba0f780d5c7759b9e303a2a6b17eb05b6e660f4c474959db183e1cae060e1639227ee0bca03978a238dc4352ed764da7d4f3ed5337f6d0376dff72615beeeeaaeef79ab93e4bcbf18cd8424eb2b6ad7f33d2b4ffd5ea08372e6ed1d984152df17e04c6f73540988d7dd979e020424a163c271151a255966be7edef42167b8facca633649739bab97572b485658cde409e5d4a0f653f1a5911141634e3d2b6079b19347df66f9820755fd517092dae62fb278b0bafcc7ad682f7921b3a455e0c6369988779e26f0458b31bffd7e4e5bfb31944e80f100b2553c3b616e75be18328dc430f6618d55cd7d0962bb916d26ed4b117c46fa29e0a112c02c36020b34a96762db628fa3490828ec2079962ad816ef20ea0bca78fb2b7f7aedd4c47e375e64294d151ff03083730336dea64934003a27730cc1c7dec5049ddba8188123dd191aa71390d43a49fb792a3da7082efa6cced73f00eccea18145fbc84925349f7b552314ab8ed4c491e392aed3b1f03eb79474c294b42e2eba1528da26450aa592cba7ea22e965c54dff0fd6fdfd6b52b9a0f5f762e27fb0e6c3cd326a1ca1c5973de9be881439f702830affeb0c034c18ac8d5c2f135c964bf69de50d6e99bde88e90321ba843d9753c8f83666105d25fafb1a11ea22d62ef6f1fc34ca4e60c35d69773a104d9a44728c08c20b6314327301a2c400a71e1424c12628cf9f4a67990ade8a2203b0edb96c6082d4673b7309cd52c4b32b02951db2f66c6c72bd6c7eac2b50b83830c75cdfc3d6e9c2b592c45ed5fa5f6ec0da85710b7e1562aea363e28665835791dc574d9a70b2e5e2b9973ab590d45b94d244fc4256926c5a55b01cd0aca21fe5f9c907691fb026d0c56788b03ca3f08db0abb9f901098dde2ec4003568bc3ca27475ff86a7cb0aabd9e5136c5de064d16774584b252024109bb02004dba1fabf9e8277de097a0ab0dc8f6e26fcd4a28fb9d27cd4a2f6b13e276ed259a39e1c7e60f3c32c5cc4c4f96bd981edcb5e2c76a517cdc285aa2ca571d1e3d463ecd7614ae227df17af7445305bd7c661cf7dba658b0adcf36b0084b74a5fa408e272f703770ac5351334709112c5d4e4fe987e0c27b670412696f52b33245c229775da550729938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3da3bac7ec6b67f68f2838b6dc687c42dd3b4be7d8e44ded20177e8e3cc9c7014") + assert(serializePaymentOnion(onion) == hex"0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661918f5b235c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a7141453e5f8d22b6351810ae541ce499a09b4a9d9f80d1845c8960c85fc6d1a87bd24b2cc49922898e9353fa268086c00ae8b7f718405b72ad380cdbb38c85e02a00427eb4bdbda8fcd42b44708a9efde49cf753b75ebb389bf84d0bfbf58590e510e034572a01e409c30939e2e4a090ecc89c371820af54e06e4ad5495d4e58718385cca5414552e078fedf284fdc2cc5c070cba21a6a8d4b77525ddbc9a9fca9b2f29aac5783ee8badd709f81c73ff60556cf2ee623af073b5a84799acc1ca46b764f74b97068c7826cc0579794a540d7a55e49eac26a6930340132e946a983240b0cd1b732e305c1042f580c4b26f140fc1cab3ee6f620958e0979f85eddf586c410ce42e93a4d7c803ead45fc47cf4396d284632314d789e73cf3f534126c63fe244069d9e8a7c4f98e7e530fc588e648ef4e641364981b5377542d5e7a4aaab6d35f6df7d3a9d7ca715213599ee02c4dbea4dc78860febe1d29259c64b59b3333ffdaebbaff4e7b31c27a3791f6bf848a58df7c69bb2b1852d2ad357b9919ffdae570b27dc709fba087273d3a4de9e6a6be66db647fb6a8d1a503b3f481befb96745abf5cc4a6bba0f780d5c7759b9e303a2a6b17eb05b6e660f4c474959db183e1cae060e1639227ee0bca03978a238dc4352ed764da7d4f3ed5337f6d0376dff72615beeeeaaeef79ab93e4bcbf18cd8424eb2b6ad7f33d2b4ffd5ea08372e6ed1d984152df17e04c6f73540988d7dd979e020424a163c271151a255966be7edef42167b8facca633649739bab97572b485658cde409e5d4a0f653f1a5911141634e3d2b6079b19347df66f9820755fd517092dae62fb278b0bafcc7ad682f7921b3a455e0c6369988779e26f0458b31bffd7e4e5bfb31944e80f100b2553c3b616e75be18328dc430f6618d55cd7d0962bb916d26ed4b117c46fa29e0a112c02c36020b34a96762db628fa3490828ec2079962ad816ef20ea0bca78fb2b7f7aedd4c47e375e64294d151ff03083730336dea64934003a27730cc1c7dec5049ddba8188123dd191aa71390d43a49fb792a3da7082efa6cced73f00eccea18145fbc84925349f7b552314ab8ed4c491e392aed3b1f03eb79474c294b42e2eba1528da26450aa592cba7ea22e965c54dff0fd6fdfd6b52b9a0f5f762e27fb0e6c3cd326a1ca1c5973de9be881439f702830affeb0c034c18ac8d5c2f135c964bf69de50d6e99bde88e90321ba843d9753c8f83666105d25fafb1a11ea22d62ef6f1fc34ca4e60c35d69773a104d9a44728c08c20b6314327301a2c400a71e1424c12628cf9f4a67990ade8a2203b0edb96c6082d4673b7309cd52c4b32b02951db2f66c6c72bd6c7eac2b50b83830c75cdfc3d6e9c2b592c45ed5fa5f6ec0da85710b7e1562aea363e28665835791dc574d9a70b2e5e2b9973ab590d45b94d244fc4256926c5a55b01cd0aca21fe5f9c907691fb026d0c56788b03ca3f08db0abb9f901098dde2ec4003568bc3ca27475ff86a7cb0aabd9e5136c5de064d16774584b252024109bb02004dba1fabf9e8277de097a0ab0dc8f6e26fcd4a28fb9d27cd4a2f6b13e276ed259a39e1c7e60f3c32c5cc4c4f96bd981edcb5e2c76a517cdc285aa2ca571d1e3d463ecd7614ae227df17af7445305bd7c661cf7dba658b0adcf36b0084b74a5fa408e272f703770ac5351334709112c5d4e4fe987e0c27b670412696f52b33245c229775da550729938268ee4e7a282e4a60b25dbb28ea8877a5069f819e5d1d31d9140bbc627ff3df267d22e5f0e151db066577845d71b7cd4484089f3f59194963c8f02bd7a637") val Right(DecryptedPacket(payload, nextPacket, _)) = PaymentPacket.peel(privKeys(0), associatedData, onion) assert(payload == variableSizeOneHopPayload.head) @@ -367,7 +367,7 @@ object SphinxSpec { val referenceVariableSizePayloads = Seq( hex"000000000000000000000000000000000000000000000000000000000000000000", hex"140101010101010101000000000000000100000001", - hex"fd0001000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + hex"fd0100000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", hex"140303030303030303000000000000000300000003", hex"000404040404040404000000000000000400000004000000000000000000000000" ) @@ -376,16 +376,16 @@ object SphinxSpec { // origin -> node #0 -> node #1 -> node #2 -> node #3 -> node #4 val variableSizePayloadsFull = Seq( hex"8b09000000000000000030000000000000000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000250000000000000000000000000000000000000000000000002500000000000000000000000000000000000000000000000025000000000000000000000000000000000000000000000000", - hex"fd2a0108000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000", + hex"fd012a08000000000000009000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000200000000000000000000000000000000000000020000000000000000000000000000000000000002000000000000000000000000000000000000000", hex"620800000000000000900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", hex"fc120000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000240000000000000000000000000000000000000000000000", - hex"fd58012200000000000000000000000000000000000000000022000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000" + hex"fd01582200000000000000000000000000000000000000000022000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000300000000000000000000000000000000000000000000000000000000000" ) // This test vector uses a single variable-sized payload filling the whole onion payload. // origin -> recipient val variableSizeOneHopPayload = Seq( - hex"fdf1046500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + hex"fd04f16500000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" ) val associatedData = ByteVector32(hex"4242424242424242424242424242424242424242424242424242424242424242") diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala index 1ab76696c5..763a9ce42a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/wire/OnionCodecsSpec.scala @@ -59,11 +59,11 @@ class OnionCodecsSpec extends FunSuite { (1, hex"00"), (43, hex"2a 0000"), (253, hex"fc 0000"), - (256, hex"fdfd00 000000"), + (256, hex"fd00fd 000000"), (260, hex"fd0101 00"), (65538, hex"fdffff 00"), - (65541, hex"fe00000100 00"), - (4294967305L, hex"ff0000000001000000 00") + (65541, hex"fe00010000 00"), + (4294967305L, hex"ff0000000100000000 00") ) for ((payloadLength, bin) <- testCases) { From 358b20aef5137afb9276767b616d830e2fc9d698 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Tue, 23 Jul 2019 10:15:01 +0200 Subject: [PATCH 26/27] Sphinx: remove the tlv termination signal. We only use an empty next HMAC to signal termination. --- .../src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala | 8 +------- .../test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala | 4 ++-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 9e4de4323d..0cf7ba9aef 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -106,13 +106,7 @@ object Sphinx extends Logging { */ case class DecryptedPacket(payload: ByteVector, nextPacket: wire.OnionRoutingPacket, sharedSecret: ByteVector32) { - val isLastPacket: Boolean = payload.head match { - // In Bolt 1.0 the last hop is signaled via an empty hmac. - case 0 => nextPacket.hmac == ByteVector32.Zeroes - // In Bolt 1.1 the last hop can also be signaled via a dedicated TLV type with type=0x00. - case 0xfd => payload(3) == 0 || nextPacket.hmac == ByteVector32.Zeroes - case _ => payload(1) == 0 || nextPacket.hmac == ByteVector32.Zeroes - } + val isLastPacket: Boolean = nextPacket.hmac == ByteVector32.Zeroes } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala index 9a0070a1d6..59a622b48b 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/SphinxSpec.scala @@ -100,10 +100,10 @@ class SphinxSpec extends FunSuite { // Bolt 1.0 payloads use the next packet's hmac to signal termination. (true, DecryptedPacket(hex"00", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), (false, DecryptedPacket(hex"00", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), - // Bolt 1.1 payloads may use either the next packet's hmac or a tlv type to signal termination. + // Bolt 1.1 payloads currently also use the next packet's hmac to signal termination. (true, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.Zeroes), ByteVector32.One)), (false, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), - (true, DecryptedPacket(hex"0100", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), + (false, DecryptedPacket(hex"0100", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)), (false, DecryptedPacket(hex"0101", OnionRoutingPacket(0, publicKeys.head.value, ByteVector.empty, ByteVector32.One), ByteVector32.One)) ) From 873bb413308bd314cc46b7d247c5701a98b88137 Mon Sep 17 00:00:00 2001 From: Bastien Teinturier Date: Tue, 23 Jul 2019 11:09:35 +0200 Subject: [PATCH 27/27] Formatting and small fixes --- .../src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala | 2 +- .../src/main/scala/fr/acinq/eclair/payment/Relayer.scala | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala index 0cf7ba9aef..7142924b7a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Sphinx.scala @@ -93,7 +93,7 @@ object Sphinx extends Logging { // 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. - MacLength + OnionCodecs.payloadLengthDecoder.decode(payload.take(3).bits).require.value.toInt + MacLength + OnionCodecs.payloadLengthDecoder.decode(payload.bits).require.value.toInt } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala index fae3c4aee2..d5d6b9f17e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/Relayer.scala @@ -233,12 +233,14 @@ object Relayer extends Logging { case Right(p@Sphinx.DecryptedPacket(payload, nextPacket, _)) => OnionCodecs.perHopPayloadCodec.decode(payload.bits) match { case Attempt.Successful(DecodeResult(perHopPayload, remainder)) => - if (remainder.nonEmpty) + if (remainder.nonEmpty) { logger.warn(s"${remainder.length} bits remaining after per-hop payload decoding: there might be an issue with the onion codec") - if (p.isLastPacket) + } + if (p.isLastPacket) { Right(FinalPayload(add, perHopPayload)) - else + } else { Right(RelayPayload(add, perHopPayload, nextPacket)) + } case Attempt.Failure(_) => // Onion is correctly encrypted but the content of the per-hop payload couldn't be parsed. Left(InvalidOnionPayload(Sphinx.PaymentPacket.hash(add.onionRoutingPacket)))