diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala b/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala index 9a3b7ef8e..9ba71086f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/DBCompatChecker.scala @@ -39,7 +39,7 @@ object DBCompatChecker extends Logging { * @param nodeParams */ def checkNetworkDBCompatibility(nodeParams: NodeParams): Unit = - Try(nodeParams.db.network.listChannels(), nodeParams.db.network.listNodes()) match { + Try(nodeParams.db.network.listChannels(nodeParams.chainHash), nodeParams.db.network.listNodes()) match { case Success(_) => {} case Failure(_) => throw IncompatibleNetworkDBException } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala index 28e496d7f..33edcb623 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/ChaCha20Poly1305.scala @@ -22,8 +22,8 @@ import fr.acinq.bitcoin.{ByteVector32, Protocol} import fr.acinq.eclair.crypto.ChaCha20Poly1305.{DecryptionError, EncryptionError, InvalidCounter} import grizzled.slf4j.Logger import grizzled.slf4j.Logging -import org.spongycastle.crypto.engines.ChaCha7539Engine -import org.spongycastle.crypto.params.{KeyParameter, ParametersWithIV} +import org.bouncycastle.crypto.engines.ChaCha7539Engine +import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV} import scodec.bits.ByteVector /** @@ -39,7 +39,7 @@ object Poly1305 { */ def mac(key: ByteVector, datas: ByteVector*): ByteVector = { val out = new Array[Byte](16) - val poly = new org.spongycastle.crypto.macs.Poly1305() + val poly = new org.bouncycastle.crypto.macs.Poly1305() poly.init(new KeyParameter(key.toArray)) datas.foreach(data => poly.update(data.toArray, 0, data.length.toInt)) poly.doFinal(out, 0) 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 494cd1020..62e50100f 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 @@ -17,9 +17,9 @@ 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 org.bouncycastle.crypto.digests.SHA256Digest +import org.bouncycastle.crypto.macs.HMac +import org.bouncycastle.crypto.params.KeyParameter import scodec.bits.ByteVector /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala index ceed60483..3502a1a37 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/crypto/Noise.scala @@ -23,9 +23,9 @@ import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Crypto, Protocol} import fr.acinq.eclair.randomBytes import grizzled.slf4j.Logging -import org.spongycastle.crypto.digests.SHA256Digest -import org.spongycastle.crypto.macs.HMac -import org.spongycastle.crypto.params.KeyParameter +import org.bouncycastle.crypto.digests.SHA256Digest +import org.bouncycastle.crypto.macs.HMac +import org.bouncycastle.crypto.params.KeyParameter import scodec.bits.ByteVector /** diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala index eca888c0e..d1d34871b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/NetworkDb.scala @@ -40,13 +40,13 @@ trait NetworkDb extends Closeable { def addChannel(c: ChannelAnnouncement, txid: ByteVector32, capacity: Satoshi): Unit - def updateChannel(u: ChannelUpdate): Unit + def updateChannel(chainHash: ByteVector32, u: ChannelUpdate): Unit def removeChannel(shortChannelId: ShortChannelId) = removeChannels(Set(shortChannelId)): Unit def removeChannels(shortChannelIds: Iterable[ShortChannelId]): Unit - def listChannels(): SortedMap[ShortChannelId, PublicChannel] + def listChannels(chainHash: ByteVector32): SortedMap[ShortChannelId, PublicChannel] def addToPruned(shortChannelIds: Iterable[ShortChannelId]): Unit diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala index fd5a64dc2..03b558534 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteNetworkDb.scala @@ -59,8 +59,11 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { ("bitcoinSignature2" | provide(null.asInstanceOf[ByteVector64])) :: channelAnnouncementWitnessCodec).as[ChannelAnnouncement] - val channelUpdateWitnessCodec = - ("chainHash" | provide(null.asInstanceOf[ByteVector32])) :: + /** + * we override the chain hash because we don't write it but we need it at runtime to compute channel queries checksums + */ + def channelUpdateWitnessCodec(chainHash: ByteVector32) = + ("chainHash" | provide(chainHash)) :: ("shortChannelId" | shortchannelid) :: ("timestamp" | uint32) :: (("messageFlags" | byte) >>:~ { messageFlags => @@ -73,9 +76,9 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { ("unknownFields" | bytes) }) - val channelUpdateCodec: Codec[ChannelUpdate] = ( + def channelUpdateCodec(chainHash: ByteVector32): Codec[ChannelUpdate] = ( ("signature" | provide(null.asInstanceOf[ByteVector64])) :: - channelUpdateWitnessCodec).as[ChannelUpdate] + channelUpdateWitnessCodec(chainHash)).as[ChannelUpdate] using(sqlite.createStatement(), inTransaction = true) { statement => getVersion(statement, DB_NAME, CURRENT_VERSION) match { @@ -145,16 +148,16 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { } } - override def updateChannel(u: ChannelUpdate): Unit = withMetrics("network/update-channel") { + override def updateChannel(chainHash: ByteVector32, u: ChannelUpdate): Unit = withMetrics("network/update-channel") { val column = if (u.isNode1) "channel_update_1" else "channel_update_2" using(sqlite.prepareStatement(s"UPDATE channels SET $column=? WHERE short_channel_id=?")) { statement => - statement.setBytes(1, channelUpdateCodec.encode(u).require.toByteArray) + statement.setBytes(1, channelUpdateCodec(chainHash).encode(u).require.toByteArray) statement.setLong(2, u.shortChannelId.toLong) statement.executeUpdate() } } - override def listChannels(): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels") { + override def listChannels(chainHash: ByteVector32): SortedMap[ShortChannelId, PublicChannel] = withMetrics("network/list-channels") { using(sqlite.createStatement()) { statement => val rs = statement.executeQuery("SELECT channel_announcement, txid, capacity_sat, channel_update_1, channel_update_2 FROM channels") var m = SortedMap.empty[ShortChannelId, PublicChannel] @@ -162,8 +165,8 @@ class SqliteNetworkDb(sqlite: Connection) extends NetworkDb with Logging { val ann = channelAnnouncementCodec.decode(rs.getBitVectorOpt("channel_announcement").get).require.value val txId = ByteVector32.fromValidHex(rs.getString("txid")) val capacity = rs.getLong("capacity_sat") - val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec.decode(_).require.value) - val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec.decode(_).require.value) + val channel_update_1_opt = rs.getBitVectorOpt("channel_update_1").map(channelUpdateCodec(chainHash).decode(_).require.value) + val channel_update_2_opt = rs.getBitVectorOpt("channel_update_2").map(channelUpdateCodec(chainHash).decode(_).require.value) m = m + (ann.shortChannelId -> PublicChannel(ann, txId, Satoshi(capacity), channel_update_1_opt, channel_update_2_opt, None)) } m diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala index be7ee0b25..df0cef09b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala @@ -16,15 +16,16 @@ package fr.acinq.eclair.payment -import java.util.UUID - import fr.acinq.bitcoin.ByteVector32 import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.crypto.Sphinx +import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.router.Router.{ChannelDesc, ChannelHop, Hop, Ignore} -import fr.acinq.eclair.wire.Node +import fr.acinq.eclair.wire.{ChannelUpdate, Node} +import fr.acinq.eclair.{MilliSatoshi, ShortChannelId} + +import java.util.UUID /** * Created by PM on 01/02/2017. @@ -206,4 +207,23 @@ object PaymentFailure { failures.foldLeft(ignore) { case (current, failure) => updateIgnored(failure, current) } } + /** Update the invoice routing hints based on more recent channel updates received. */ + def updateRoutingHints(failures: Seq[PaymentFailure], routingHints: Seq[Seq[ExtraHop]]): Seq[Seq[ExtraHop]] = { + // We're only interested in the last channel update received per channel. + val updates = failures.foldLeft(Map.empty[ShortChannelId, ChannelUpdate]) { + case (current, failure) => failure match { + case RemoteFailure(_, Sphinx.DecryptedFailurePacket(_, f: Update)) => current.updated(f.update.shortChannelId, f.update) + case _ => current + } + } + routingHints.map(_.map(extraHop => updates.get(extraHop.shortChannelId) match { + case Some(u) => extraHop.copy( + cltvExpiryDelta = u.cltvExpiryDelta, + feeBase = u.feeBaseMsat, + feeProportionalMillionths = u.feeProportionalMillionths + ) + case None => extraHop + })) + } + } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala index 664871d9e..012f4cf31 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/send/MultiPartPaymentLifecycle.scala @@ -129,7 +129,8 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, gotoAbortedOrStop(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures, d.pending.keySet - pf.id)) } else { val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore) - stay using d.copy(pending = d.pending - pf.id, ignore = ignore1, failures = d.failures ++ pf.failures) + val assistedRoutes1 = PaymentFailure.updateRoutingHints(pf.failures, d.request.assistedRoutes) + stay using d.copy(pending = d.pending - pf.id, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(assistedRoutes = assistedRoutes1)) } // The recipient released the preimage without receiving the full payment amount. @@ -150,11 +151,12 @@ class MultiPartPaymentLifecycle(nodeParams: NodeParams, cfg: SendPaymentConfig, gotoAbortedOrStop(PaymentAborted(d.sender, d.request, d.failures ++ pf.failures :+ failure, d.pending.keySet - pf.id)) } else { val ignore1 = PaymentFailure.updateIgnored(pf.failures, d.ignore) + val assistedRoutes1 = PaymentFailure.updateRoutingHints(pf.failures, d.request.assistedRoutes) val stillPending = d.pending - pf.id val (toSend, maxFee) = remainingToSend(nodeParams, d.request, stillPending.values) log.info("child payment failed, retry sending {} with maximum fee {}", toSend, maxFee) val routeParams = d.request.getRouteParams(nodeParams, randomize = true) // we randomize route selection when we retry - val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures) + val d1 = d.copy(pending = stillPending, ignore = ignore1, failures = d.failures ++ pf.failures, request = d.request.copy(assistedRoutes = assistedRoutes1)) router ! createRouteRequest(nodeParams, toSend, maxFee, routeParams, d1, cfg) goto(WAIT_FOR_ROUTES) using d1 } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index a001dd46b..4aeddc72c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -71,7 +71,7 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ { log.info("loading network announcements from db...") // On Android, we discard the node announcements - val channels = db.listChannels() + val channels = db.listChannels(nodeParams.chainHash) log.info("loaded from db: channels={}", channels.size) val initChannels = channels // this will be used to calculate routes diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 86c5f3ed6..8f9a34a1e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -317,7 +317,7 @@ object Validation { Metrics.channelUpdateRefreshed(u, pc.getChannelUpdateSameSideAs(u).get, publicChannel) sendDecision(origins, GossipDecision.Accepted(u)) ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.updateChannel(u) + db.updateChannel(u.chainHash, u) // update the graph val pc1 = pc.applyChannelUpdate(update) val graph1 = if (Announcements.isEnabled(u.channelFlags)) { @@ -330,7 +330,7 @@ object Validation { log.debug("added channel_update for shortChannelId={} public={} flags={} {}", u.shortChannelId, publicChannel, u.channelFlags, u) sendDecision(origins, GossipDecision.Accepted(u)) ctx.system.eventStream.publish(ChannelUpdatesReceived(u :: Nil)) - db.updateChannel(u) + db.updateChannel(u.chainHash, u) // we also need to update the graph val pc1 = pc.applyChannelUpdate(update) val graph1 = d.graph.addEdge(desc, u, pc1.capacity, pc1.getBalanceSameSideAs(u)) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala index 8d55cb505..150e8a693 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -231,7 +231,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I } val mockNetworkDb = mock[NetworkDb](withSettings.lenient()) // on Android listNodes() and removeNode() are not used - mockNetworkDb.listChannels() returns channels + mockNetworkDb.listChannels(Block.RegtestGenesisBlock.hash) returns channels mockNetworkDb.listNodes() returns Seq.empty Mockito.doNothing().when(mockNetworkDb).removeNode(kit.nodeParams.nodeId) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseSpec.scala index 359ba623b..56abc6d35 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/crypto/NoiseSpec.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.crypto import fr.acinq.eclair.crypto.Noise._ import org.scalatest.funsuite.AnyFunSuite -import org.spongycastle.crypto.ec.CustomNamedCurves +import org.bouncycastle.crypto.ec.CustomNamedCurves import scodec.bits._ diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala index 005b49634..f2c45e208 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteNetworkDbSpec.scala @@ -35,6 +35,7 @@ class SqliteNetworkDbSpec extends AnyFunSuite { import TestConstants.forAllDbs val shortChannelIds = (42 to (5000 + 42)).map(i => ShortChannelId(i)) + val chainHash = Block.RegtestGenesisBlock.hash test("init sqlite 2 times in a row") { forAllDbs { dbs => @@ -112,17 +113,17 @@ class SqliteNetworkDbSpec extends AnyFunSuite { forAllDbs { dbs => val db = dbs.network() val sig = ByteVector64.Zeroes - val c = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) + val c = Announcements.makeChannelAnnouncement(chainHash, ShortChannelId(42), randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) val c_shrunk = shrink(c) val txid = ByteVector32.fromValidHex("0001" * 16) db.addChannel(c, txid, Satoshi(42)) - assert(db.listChannels() === SortedMap(c.shortChannelId -> PublicChannel(c_shrunk, txid, Satoshi(42), None, None, None))) + assert(db.listChannels(chainHash) === SortedMap(c.shortChannelId -> PublicChannel(c_shrunk, txid, Satoshi(42), None, None, None))) } } def shrink(c: ChannelAnnouncement) = c.copy(bitcoinKey1 = null, bitcoinKey2 = null, bitcoinSignature1 = null, bitcoinSignature2 = null, nodeSignature1 = null, nodeSignature2 = null, chainHash = null, features = null) - def shrink(c: ChannelUpdate) = c.copy(signature = null, chainHash = null) + def shrink(c: ChannelUpdate) = c.copy(signature = null) def simpleTest(dbs: TestDatabases) = { val db = dbs.network() @@ -140,9 +141,9 @@ class SqliteNetworkDbSpec extends AnyFunSuite { val b = generatePubkeyHigherThan(a) val c = generatePubkeyHigherThan(b) - val channel_1 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(42), a.publicKey, b.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) - val channel_2 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(43), a.publicKey, c.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) - val channel_3 = Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, ShortChannelId(44), b.publicKey, c.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) + val channel_1 = Announcements.makeChannelAnnouncement(chainHash, ShortChannelId(42), a.publicKey, b.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) + val channel_2 = Announcements.makeChannelAnnouncement(chainHash, ShortChannelId(43), a.publicKey, c.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) + val channel_3 = Announcements.makeChannelAnnouncement(chainHash, ShortChannelId(44), b.publicKey, c.publicKey, randomKey.publicKey, randomKey.publicKey, sig, sig, sig, sig) val channel_1_shrunk = shrink(channel_1) val channel_2_shrunk = shrink(channel_2) @@ -153,39 +154,39 @@ class SqliteNetworkDbSpec extends AnyFunSuite { val txid_3 = randomBytes32 val capacity = 10000 sat - assert(db.listChannels().toSet === Set.empty) + assert(db.listChannels(chainHash).toSet === Set.empty) db.addChannel(channel_1, txid_1, capacity) db.addChannel(channel_1, txid_1, capacity) // duplicate is ignored - assert(db.listChannels().size === 1) + assert(db.listChannels(chainHash).size === 1) db.addChannel(channel_2, txid_2, capacity) db.addChannel(channel_3, txid_3, capacity) - assert(db.listChannels() === SortedMap( + assert(db.listChannels(chainHash) === SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1_shrunk, txid_1, capacity, None, None, None), channel_2.shortChannelId -> PublicChannel(channel_2_shrunk, txid_2, capacity, None, None, None), channel_3.shortChannelId -> PublicChannel(channel_3_shrunk, txid_3, capacity, None, None, None))) db.removeChannel(channel_2.shortChannelId) - assert(db.listChannels() === SortedMap( + assert(db.listChannels(chainHash) === SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1_shrunk, txid_1, capacity, None, None, None), channel_3.shortChannelId -> PublicChannel(channel_3_shrunk, txid_3, capacity, None, None, None))) - val channel_update_1 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, a, b.publicKey, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) - val channel_update_2 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, a.publicKey, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) - val channel_update_3 = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, b, c.publicKey, ShortChannelId(44), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) + val channel_update_1 = Announcements.makeChannelUpdate(chainHash, a, b.publicKey, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) + val channel_update_2 = Announcements.makeChannelUpdate(chainHash, b, a.publicKey, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) + val channel_update_3 = Announcements.makeChannelUpdate(chainHash, b, c.publicKey, ShortChannelId(44), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) val channel_update_1_shrunk = shrink(channel_update_1) val channel_update_2_shrunk = shrink(channel_update_2) val channel_update_3_shrunk = shrink(channel_update_3) - db.updateChannel(channel_update_1) - db.updateChannel(channel_update_1) // duplicate is ignored - db.updateChannel(channel_update_2) - db.updateChannel(channel_update_3) - assert(db.listChannels() === SortedMap( + db.updateChannel(chainHash, channel_update_1) + db.updateChannel(chainHash, channel_update_1) // duplicate is ignored + db.updateChannel(chainHash, channel_update_2) + db.updateChannel(chainHash, channel_update_3) + assert(db.listChannels(chainHash) === SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1_shrunk, txid_1, capacity, Some(channel_update_1_shrunk), Some(channel_update_2_shrunk), None), channel_3.shortChannelId -> PublicChannel(channel_3_shrunk, txid_3, capacity, Some(channel_update_3_shrunk), None, None))) db.removeChannel(channel_3.shortChannelId) - assert(db.listChannels() === SortedMap( + assert(db.listChannels(chainHash) === SortedMap( channel_1.shortChannelId -> PublicChannel(channel_1_shrunk, txid_1, capacity, Some(channel_update_1_shrunk), Some(channel_update_2_shrunk), None))) } @@ -247,17 +248,17 @@ class SqliteNetworkDbSpec extends AnyFunSuite { val pub = priv.publicKey val capacity = 10000 sat - val channels = shortChannelIds.map(id => Announcements.makeChannelAnnouncement(Block.RegtestGenesisBlock.hash, id, pub, pub, pub, pub, sig, sig, sig, sig)) - val template = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, priv, pub, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) + val channels = shortChannelIds.map(id => Announcements.makeChannelAnnouncement(chainHash, id, pub, pub, pub, pub, sig, sig, sig, sig)) + val template = Announcements.makeChannelUpdate(chainHash, priv, pub, ShortChannelId(42), CltvExpiryDelta(5), 7000000 msat, 50000 msat, 100, 500000000L msat, true) val updates = shortChannelIds.map(id => template.copy(shortChannelId = id)) val txid = randomBytes32 channels.foreach(ca => db.addChannel(ca, txid, capacity)) - updates.foreach(u => db.updateChannel(u)) - assert(db.listChannels().keySet === channels.map(_.shortChannelId).toSet) + updates.foreach(u => db.updateChannel(chainHash, u)) + assert(db.listChannels(chainHash).keySet === channels.map(_.shortChannelId).toSet) val toDelete = channels.map(_.shortChannelId).drop(500).take(2500) db.removeChannels(toDelete) - assert(db.listChannels().keySet === (channels.map(_.shortChannelId).toSet -- toDelete)) + assert(db.listChannels(chainHash).keySet === (channels.map(_.shortChannelId).toSet -- toDelete)) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala index bc8e93507..7a6f924df 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/payment/MultiPartPaymentLifecycleSpec.scala @@ -16,8 +16,6 @@ package fr.acinq.eclair.payment -import java.util.UUID - import akka.actor.{ActorRef, Status} import akka.testkit.{TestFSMRef, TestProbe} import fr.acinq.bitcoin.{Block, Crypto} @@ -25,6 +23,7 @@ import fr.acinq.eclair._ import fr.acinq.eclair.channel.{AddHtlcFailed, ChannelFlags, ChannelUnavailable, Upstream} import fr.acinq.eclair.crypto.Sphinx import fr.acinq.eclair.db.{FailureSummary, FailureType, OutgoingPaymentStatus} +import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle._ import fr.acinq.eclair.payment.send.PaymentError.RetryExhausted @@ -37,6 +36,7 @@ import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike import scodec.bits.{ByteVector, HexStringSyntax} +import java.util.UUID import scala.concurrent.duration._ /** @@ -239,6 +239,64 @@ class MultiPartPaymentLifecycleSpec extends TestKitBaseClass with FixtureAnyFunS assert(result.amountWithFees === 1000200.msat) } + test("retry with updated routing hints") { f => + import f._ + + // The B -> E channel is private and provided in the invoice routing hints. + val routingHint = ExtraHop(b, hop_be.lastUpdate.shortChannelId, hop_be.lastUpdate.feeBaseMsat, hop_be.lastUpdate.feeProportionalMillionths, hop_be.lastUpdate.cltvExpiryDelta) + val payment = SendMultiPartPayment(randomBytes32, e, finalAmount, expiry, 3, routeParams = Some(routeParams), assistedRoutes = List(List(routingHint))) + sender.send(payFsm, payment) + assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === routingHint) + val route = Route(finalAmount, hop_ab_1 :: hop_be :: Nil) + router.send(payFsm, RouteResponse(Seq(route))) + childPayFsm.expectMsgType[SendPaymentToRoute] + childPayFsm.expectNoMsg(100 millis) + + // B changed his fees and expiry after the invoice was issued. + val channelUpdate = hop_be.lastUpdate.copy(feeBaseMsat = 250 msat, feeProportionalMillionths = 150, cltvExpiryDelta = CltvExpiryDelta(24)) + val childId = payFsm.stateData.asInstanceOf[PaymentProgress].pending.keys.head + childPayFsm.send(payFsm, PaymentFailed(childId, paymentHash, Seq(RemoteFailure(route.hops, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(finalAmount, channelUpdate)))))) + // We update the routing hints accordingly before requesting a new route. + assert(router.expectMsgType[RouteRequest].assistedRoutes.head.head === ExtraHop(b, channelUpdate.shortChannelId, 250 msat, 150, CltvExpiryDelta(24))) + } + + test("update routing hints") { _ => + val routingHints = Seq( + Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 0 msat, 100, CltvExpiryDelta(24))), + Seq(ExtraHop(a, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144))) + ) + + def makeChannelUpdate(shortChannelId: ShortChannelId, feeBase: MilliSatoshi, feeProportional: Long, cltvExpiryDelta: CltvExpiryDelta): ChannelUpdate = { + defaultChannelUpdate.copy(shortChannelId = shortChannelId, feeBaseMsat = feeBase, feeProportionalMillionths = feeProportional, cltvExpiryDelta = cltvExpiryDelta) + } + + { + val failures = Seq( + LocalFailure(Nil, ChannelUnavailable(randomBytes32)), + RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))))), + UnreadableRemoteFailure(Nil) + ) + val routingHints1 = Seq( + Seq(ExtraHop(a, ShortChannelId(1), 10 msat, 0, CltvExpiryDelta(12)), ExtraHop(b, ShortChannelId(2), 15 msat, 150, CltvExpiryDelta(48))), + Seq(ExtraHop(a, ShortChannelId(3), 1 msat, 10, CltvExpiryDelta(144))) + ) + assert(routingHints1 === PaymentFailure.updateRoutingHints(failures, routingHints)) + } + { + val failures = Seq( + RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(1), 20 msat, 20, CltvExpiryDelta(20))))), + RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(b, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21))))), + RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))))), + RemoteFailure(Nil, Sphinx.DecryptedFailurePacket(a, FeeInsufficient(100 msat, makeChannelUpdate(ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23))))) + ) + val routingHints1 = Seq( + Seq(ExtraHop(a, ShortChannelId(1), 23 msat, 23, CltvExpiryDelta(23)), ExtraHop(b, ShortChannelId(2), 21 msat, 21, CltvExpiryDelta(21))), + Seq(ExtraHop(a, ShortChannelId(3), 22 msat, 22, CltvExpiryDelta(22))) + ) + assert(routingHints1 === PaymentFailure.updateRoutingHints(failures, routingHints)) + } + } + test("abort after too many failed attempts") { f => import f._ diff --git a/pom.xml b/pom.xml index b207a5998..30a0f1431 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 2.3.14 10.0.11 1.3.9 - 0.17 + 0.18 24.0-android 2.1.0