From a2c6e0b628b790e484f089e585b3d33acceb093e Mon Sep 17 00:00:00 2001 From: Thomas HUET <thomas.huet@acinq.fr> Date: Wed, 8 Jan 2025 11:21:55 +0100 Subject: [PATCH] Remove quantity --- .../main/scala/fr/acinq/eclair/Eclair.scala | 6 +-- .../fr/acinq/eclair/db/DualDatabases.scala | 16 ++------ .../scala/fr/acinq/eclair/db/OffersDb.scala | 9 +---- .../fr/acinq/eclair/db/pg/PgOffersDb.scala | 11 ++---- .../eclair/db/sqlite/SqliteOffersDb.scala | 6 +-- .../eclair/payment/offer/DefaultHandler.scala | 37 +++++++------------ .../eclair/payment/offer/OfferCreator.scala | 26 ++++++------- 7 files changed, 36 insertions(+), 75 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala index 8a2af919ab..2cfd257bd7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -122,7 +122,7 @@ trait Eclair { def receive(description: Either[String, ByteVector32], amount_opt: Option[MilliSatoshi], expire_opt: Option[Long], fallbackAddress_opt: Option[String], paymentPreimage_opt: Option[ByteVector32], privateChannelIds_opt: Option[List[ByteVector32]])(implicit timeout: Timeout): Future[Bolt11Invoice] - def createOffer(description_opt: Option[String], amount_opt: Option[MilliSatoshi], expiry_opt: Option[TimestampSecond], issuer_opt: Option[String], availableQuantity_opt: Option[Long], firstNodeId_opt: Option[PublicKey], hideNodeId: Boolean)(implicit timeout: Timeout): Future[Offer] + def createOffer(description_opt: Option[String], amount_opt: Option[MilliSatoshi], expiry_opt: Option[TimestampSecond], issuer_opt: Option[String], firstNodeId_opt: Option[PublicKey], hideNodeId: Boolean)(implicit timeout: Timeout): Future[Offer] def disableOffer(offer: Offer)(implicit timeout: Timeout): Unit @@ -378,9 +378,9 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } } - override def createOffer(description_opt: Option[String], amount_opt: Option[MilliSatoshi], expiry_opt: Option[TimestampSecond], issuer_opt: Option[String], availableQuantity_opt: Option[Long], firstNodeId_opt: Option[PublicKey], hideNodeId: Boolean)(implicit timeout: Timeout): Future[Offer] = { + override def createOffer(description_opt: Option[String], amount_opt: Option[MilliSatoshi], expiry_opt: Option[TimestampSecond], issuer_opt: Option[String], firstNodeId_opt: Option[PublicKey], hideNodeId: Boolean)(implicit timeout: Timeout): Future[Offer] = { val offerCreator = appKit.system.spawnAnonymous(OfferCreator(appKit.nodeParams, appKit.router, appKit.offerManager, appKit.defaultOfferHandler)) - offerCreator.ask[Either[String, Offer]](replyTo => OfferCreator.Create(replyTo, description_opt, amount_opt, expiry_opt, issuer_opt, availableQuantity_opt, firstNodeId_opt, hideNodeId)) + offerCreator.ask[Either[String, Offer]](replyTo => OfferCreator.Create(replyTo, description_opt, amount_opt, expiry_opt, issuer_opt, firstNodeId_opt, hideNodeId)) .flatMap { case Left(errorMessage) => Future.failed(new Exception(errorMessage)) case Right(offer) => Future.successful(offer) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala index fdbf1ffaf9..bed6d5627c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DualDatabases.scala @@ -405,9 +405,9 @@ case class DualOffersDb(primary: OffersDb, secondary: OffersDb) extends OffersDb private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-offers").build())) - override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], quantityAvailable: Long): Unit = { - runAsync(secondary.addOffer(offer, pathId_opt, quantityAvailable)) - primary.addOffer(offer, pathId_opt, quantityAvailable) + override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32]): Unit = { + runAsync(secondary.addOffer(offer, pathId_opt)) + primary.addOffer(offer, pathId_opt) } override def disableOffer(offer: OfferTypes.Offer): Unit = { @@ -415,16 +415,6 @@ case class DualOffersDb(primary: OffersDb, secondary: OffersDb) extends OffersDb primary.disableOffer(offer) } - override def getAvailableQuantity(offer: OfferTypes.Offer): Long = { - runAsync(secondary.getAvailableQuantity(offer)) - primary.getAvailableQuantity(offer) - } - - override def setAvailableQuantity(offer: OfferTypes.Offer, quantityAvailable: Long): Unit = { - runAsync(secondary.setAvailableQuantity(offer, quantityAvailable)) - primary.setAvailableQuantity(offer, quantityAvailable) - } - override def listOffers(onlyActive: Boolean): Seq[OfferData] = { runAsync(secondary.listOffers(onlyActive)) primary.listOffers(onlyActive) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala index de908303a3..7ba147a190 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/OffersDb.scala @@ -19,17 +19,12 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.scalacompat.ByteVector32 import fr.acinq.eclair.wire.protocol.OfferTypes.Offer -case class OfferData(offer: Offer, pathId_opt: Option[ByteVector32], quantityAvailable: Long) +case class OfferData(offer: Offer, pathId_opt: Option[ByteVector32]) trait OffersDb { - def addOffer(offer: Offer, pathId_opt: Option[ByteVector32], quantityAvailable: Long): Unit + def addOffer(offer: Offer, pathId_opt: Option[ByteVector32]): Unit def disableOffer(offer: Offer): Unit - def getAvailableQuantity(offer: Offer): Long - - // Must only be called from the `DefaultHandler` actor to prevent data races. - def setAvailableQuantity(offer: Offer, quantityAvailable: Long): Unit - def listOffers(onlyActive: Boolean): Seq[OfferData] } \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgOffersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgOffersDb.scala index c2844e533b..baea2aa974 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgOffersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgOffersDb.scala @@ -45,7 +45,7 @@ class PgOffersDb(implicit ds: DataSource, lock: PgLock) extends OffersDb with Lo case None => statement.executeUpdate("CREATE SCHEMA offers") - statement.executeUpdate("CREATE TABLE offers.managed (offer_id TEXT NOT NULL PRIMARY KEY, offer TEXT NOT NULL, path_id TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL, quantity_available BIGINT NOT NULL)") + statement.executeUpdate("CREATE TABLE offers.managed (offer_id TEXT NOT NULL PRIMARY KEY, offer TEXT NOT NULL, path_id TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL, is_active BOOLEAN NOT NULL)") statement.executeUpdate("CREATE INDEX offer_is_active_idx ON offers.managed(is_active)") case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do @@ -55,16 +55,15 @@ class PgOffersDb(implicit ds: DataSource, lock: PgLock) extends OffersDb with Lo } } - override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], quantityAvailable: Long): Unit = withMetrics("offers/add", DbBackends.Postgres){ + override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32]): Unit = withMetrics("offers/add", DbBackends.Postgres){ withLock { pg => - using(pg.prepareStatement("INSERT INTO offers.managed (offer_id, offer, path_id, created_at, is_active, quantity_available) VALUES (?, ?, ?, NOW, TRUE, ?)")) { statement => + using(pg.prepareStatement("INSERT INTO offers.managed (offer_id, offer, path_id, created_at, is_active) VALUES (?, ?, ?, NOW, TRUE)")) { statement => statement.setString(1, offer.offerId.toHex) statement.setString(2, offer.toString) pathId_opt match { case Some(pathId) => statement.setString(3, pathId.toHex) case None => statement.setNull(3, java.sql.Types.VARCHAR) } - statement.setLong(4, quantityAvailable) statement.executeUpdate() } @@ -73,9 +72,5 @@ class PgOffersDb(implicit ds: DataSource, lock: PgLock) extends OffersDb with Lo override def disableOffer(offer: OfferTypes.Offer): Unit = ??? - override def getAvailableQuantity(offer: OfferTypes.Offer): Long = ??? - - override def setAvailableQuantity(offer: OfferTypes.Offer, quantityAvailable: Long): Unit = ??? - override def listOffers(onlyActive: Boolean): Seq[OfferData] = ??? } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteOffersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteOffersDb.scala index b2c9bd3fa5..9803f65647 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteOffersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteOffersDb.scala @@ -25,13 +25,9 @@ import java.sql.Connection class SqliteOffersDb(val sqlite: Connection) extends OffersDb with Logging { - override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32], quantityAvailable: Long): Unit = ??? + override def addOffer(offer: OfferTypes.Offer, pathId_opt: Option[ByteVector32]): Unit = ??? override def disableOffer(offer: OfferTypes.Offer): Unit = ??? - override def getAvailableQuantity(offer: OfferTypes.Offer): Long = ??? - - override def setAvailableQuantity(offer: OfferTypes.Offer, quantityAvailable: Long): Unit = ??? - override def listOffers(onlyActive: Boolean): Seq[OfferData] = ??? } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/DefaultHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/DefaultHandler.scala index 6019679558..7e069c13d3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/DefaultHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/DefaultHandler.scala @@ -34,32 +34,21 @@ object DefaultHandler { Behaviors.setup(context => Behaviors.receiveMessage { case OfferManager.HandleInvoiceRequest(replyTo, invoiceRequest) => - if (nodeParams.db.offers.getAvailableQuantity(invoiceRequest.offer) >= invoiceRequest.quantity) { - val amount = invoiceRequest.amount.getOrElse(10_000_000.msat) - invoiceRequest.offer.contactInfos.head match { - case OfferTypes.RecipientNodeId(nodeId) => - val route = ReceivingRoute(Seq(nodeId), nodeParams.channelConf.maxExpiryDelta) - replyTo ! OfferManager.InvoiceRequestActor.ApproveRequest(amount, Seq(route), None) - case OfferTypes.BlindedPath(BlindedRoute(firstNodeId: EncodedNodeId.WithPublicKey, _, _)) => - val routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams - router ! BlindedRouteRequest(context.spawnAnonymous(waitForRoute(nodeParams, replyTo, amount)), firstNodeId.publicKey, nodeParams.nodeId, amount, routeParams, pathsToFind = 2) - case OfferTypes.BlindedPath(BlindedRoute(_: EncodedNodeId.ShortChannelIdDir, _, _)) => - context.log.error("unexpected managed offer with compact first node id") - replyTo ! OfferManager.InvoiceRequestActor.RejectRequest("internal error") - } - } else { - replyTo ! OfferManager.InvoiceRequestActor.RejectRequest("quantity unavailable for this offer") + val amount = invoiceRequest.amount.getOrElse(10_000_000.msat) + invoiceRequest.offer.contactInfos.head match { + case OfferTypes.RecipientNodeId(nodeId) => + val route = ReceivingRoute(Seq(nodeId), nodeParams.channelConf.maxExpiryDelta) + replyTo ! OfferManager.InvoiceRequestActor.ApproveRequest(amount, Seq(route), None) + case OfferTypes.BlindedPath(BlindedRoute(firstNodeId: EncodedNodeId.WithPublicKey, _, _)) => + val routeParams = nodeParams.routerConf.pathFindingExperimentConf.getRandomConf().getDefaultRouteParams + router ! BlindedRouteRequest(context.spawnAnonymous(waitForRoute(nodeParams, replyTo, amount)), firstNodeId.publicKey, nodeParams.nodeId, amount, routeParams, pathsToFind = 2) + case OfferTypes.BlindedPath(BlindedRoute(_: EncodedNodeId.ShortChannelIdDir, _, _)) => + context.log.error("unexpected managed offer with compact first node id") + replyTo ! OfferManager.InvoiceRequestActor.RejectRequest("internal error") } Behaviors.same - case OfferManager.HandlePayment(replyTo, offer, invoiceData) => - val availableQuantity = nodeParams.db.offers.getAvailableQuantity(offer) - if (availableQuantity >= invoiceData.quantity) { - // Only this actor reads and writes the available quantity so there is no data race. - nodeParams.db.offers.setAvailableQuantity(offer, availableQuantity - invoiceData.quantity) - replyTo ! OfferManager.PaymentActor.AcceptPayment() - } else { - replyTo ! OfferManager.PaymentActor.RejectPayment("quantity unavailable for this offer") - } + case OfferManager.HandlePayment(replyTo, _, _) => + replyTo ! OfferManager.PaymentActor.AcceptPayment() Behaviors.same } ) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferCreator.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferCreator.scala index 1db56aad74..763cc11157 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferCreator.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/offer/OfferCreator.scala @@ -36,7 +36,6 @@ object OfferCreator { amount_opt: Option[MilliSatoshi], expiry_opt: Option[TimestampSecond], issuer_opt: Option[String], - availableQuantity_opt: Option[Long], firstNodeId_opt: Option[PublicKey], hideNodeId: Boolean) extends Command @@ -46,8 +45,8 @@ object OfferCreator { def apply(nodeParams: NodeParams, router: ActorRef, offerManager: typed.ActorRef[OfferManager.Command], defaultOfferHandler: typed.ActorRef[OfferManager.HandlerCommand]): Behavior[Command] = Behaviors.receivePartial { - case (context, Create(replyTo, description_opt, amount_opt, expiry_opt, issuer_opt, availableQuantity_opt, firstNodeId, hideNodeId)) => - new OfferCreator(context, replyTo, nodeParams, router, offerManager, defaultOfferHandler).init(description_opt, amount_opt, expiry_opt, issuer_opt, availableQuantity_opt, firstNodeId, hideNodeId) + case (context, Create(replyTo, description_opt, amount_opt, expiry_opt, issuer_opt, firstNodeId, hideNodeId)) => + new OfferCreator(context, replyTo, nodeParams, router, offerManager, defaultOfferHandler).init(description_opt, amount_opt, expiry_opt, issuer_opt, firstNodeId, hideNodeId) } } @@ -63,7 +62,6 @@ private class OfferCreator(context: ActorContext[OfferCreator.Command], amount_opt: Option[MilliSatoshi], expiry_opt: Option[TimestampSecond], issuer_opt: Option[String], - availableQuantity_opt: Option[Long], firstNodeId_opt: Option[PublicKey], hideNodeId: Boolean): Behavior[Command] = { if (amount_opt.nonEmpty && description_opt.isEmpty) { @@ -76,47 +74,45 @@ private class OfferCreator(context: ActorContext[OfferCreator.Command], description_opt.map(OfferDescription), expiry_opt.map(OfferAbsoluteExpiry), issuer_opt.map(OfferIssuer), - availableQuantity_opt.map(_ => OfferQuantityMax(0)), ).flatten - val quantityAvailable = availableQuantity_opt.getOrElse(Long.MaxValue) firstNodeId_opt match { case Some(firstNodeId) => router ! Router.MessageRouteRequest(context.messageAdapter(RouteResponseWrapper(_)), firstNodeId, nodeParams.nodeId, Set.empty) - waitForRoute(tlvs, quantityAvailable) + waitForRoute(tlvs) case None if hideNodeId => router ! Router.GetCentralNode(context.messageAdapter(FirstNodeWrapper(_))) - waitForFirstNode(tlvs, quantityAvailable) + waitForFirstNode(tlvs) case None => val offer = Offer(TlvStream(tlvs + OfferNodeId(nodeParams.nodeId))) - registerOffer(offer, None, quantityAvailable) + registerOffer(offer, None) } } } - private def waitForFirstNode(tlvs: Set[OfferTlv], quantityAvailable: Long): Behavior[Command] = { + private def waitForFirstNode(tlvs: Set[OfferTlv]): Behavior[Command] = { Behaviors.receiveMessagePartial { case FirstNodeWrapper(firstNodeId) => router ! Router.MessageRouteRequest(context.messageAdapter(RouteResponseWrapper(_)), firstNodeId, nodeParams.nodeId, Set.empty) - waitForRoute(tlvs, quantityAvailable) + waitForRoute(tlvs) } } - private def waitForRoute(tlvs: Set[OfferTlv], quantityAvailable: Long): Behavior[Command] = { + private def waitForRoute(tlvs: Set[OfferTlv]): Behavior[Command] = { Behaviors.receiveMessagePartial { case RouteResponseWrapper(Router.MessageRoute(intermediateNodes, _)) => val pathId = randomBytes32() val paths = Seq(OnionMessages.buildRoute(randomKey(), intermediateNodes.map(IntermediateNode(_)), Recipient(nodeParams.nodeId, Some(pathId))).route) val offer = Offer(TlvStream(tlvs + OfferPaths(paths))) - registerOffer(offer, Some(pathId), quantityAvailable) + registerOffer(offer, Some(pathId)) case RouteResponseWrapper(Router.MessageRouteNotFound(_)) => replyTo ! Left("No route found") Behaviors.stopped } } - private def registerOffer(offer: Offer, pathId_opt: Option[ByteVector32], quantityAvailable: Long): Behavior[Command] = { - nodeParams.db.offers.addOffer(offer, pathId_opt, quantityAvailable) + private def registerOffer(offer: Offer, pathId_opt: Option[ByteVector32]): Behavior[Command] = { + nodeParams.db.offers.addOffer(offer, pathId_opt) offerManager ! OfferManager.RegisterOffer(offer, None, pathId_opt, defaultOfferHandler) replyTo ! Right(offer) Behaviors.stopped