From 721333129bb0b8d986527f77b2d46e87a60f6351 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Mon, 19 Jul 2021 11:51:18 +0200 Subject: [PATCH 1/8] Set relay fees per node and save them to database - Fees are set per node instead of per channel (setting different fees for different channels to the same node is most probably an error) - Fees are saved to a database so that we can keep a trace of historic fees and new channels with a known node use the fee that we set and not the default fee. --- .../main/scala/fr/acinq/eclair/Eclair.scala | 18 ++-- .../fr/acinq/eclair/channel/Channel.scala | 43 +++++----- .../acinq/eclair/channel/ChannelTypes.scala | 7 +- .../scala/fr/acinq/eclair/db/Databases.scala | 5 ++ .../fr/acinq/eclair/db/DualDatabases.scala | 22 +++++ .../fr/acinq/eclair/db/RelayFeesDb.scala | 30 +++++++ .../fr/acinq/eclair/db/pg/PgRelayFeesDb.scala | 83 +++++++++++++++++++ .../eclair/db/sqlite/SqliteRelayFeesDb.scala | 70 ++++++++++++++++ .../main/scala/fr/acinq/eclair/io/Peer.scala | 4 +- .../acinq/eclair/payment/relay/Relayer.scala | 11 ++- .../channel/version0/ChannelCodecs0.scala | 5 +- .../channel/version1/ChannelCodecs1.scala | 4 +- .../channel/version2/ChannelCodecs2.scala | 4 +- .../channel/version3/ChannelCodecs3.scala | 4 +- .../fr/acinq/eclair/EclairImplSpec.scala | 42 +++++++++- .../scala/fr/acinq/eclair/TestDatabases.scala | 1 + .../fr/acinq/eclair/channel/FuzzySpec.scala | 2 +- .../states/StateTestsHelperMethods.scala | 2 +- .../a/WaitForAcceptChannelStateSpec.scala | 2 +- .../a/WaitForOpenChannelStateSpec.scala | 2 +- ...itForFundingCreatedInternalStateSpec.scala | 2 +- .../b/WaitForFundingCreatedStateSpec.scala | 2 +- .../b/WaitForFundingSignedStateSpec.scala | 2 +- .../c/WaitForFundingConfirmedStateSpec.scala | 2 +- .../c/WaitForFundingLockedStateSpec.scala | 11 +-- .../channel/states/h/ClosingStateSpec.scala | 2 +- .../fr/acinq/eclair/db/RelayFeesDbSpec.scala | 62 ++++++++++++++ .../eclair/integration/IntegrationSpec.scala | 1 - .../interop/rustytests/RustyTestsSpec.scala | 2 +- .../scala/fr/acinq/eclair/io/PeerSpec.scala | 30 ++----- .../controllers/OpenChannelController.scala | 2 +- .../api/directives/ExtraDirectives.scala | 6 ++ .../fr/acinq/eclair/api/handlers/Fees.scala | 4 +- 33 files changed, 393 insertions(+), 96 deletions(-) create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala create mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala create mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala 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 23ac0541e0..840163cb68 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -96,7 +96,7 @@ trait Eclair { def forceClose(channels: List[ApiTypes.ChannelIdentifier])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_FORCECLOSE]]]] - def updateRelayFee(channels: List[ApiTypes.ChannelIdentifier], feeBase: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] + def updateRelayFee(nodes: List[PublicKey], feeBase: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] def channelsInfo(toRemoteNode_opt: Option[PublicKey])(implicit timeout: Timeout): Future[Iterable[RES_GETINFO]] @@ -177,7 +177,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { (appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String] } - override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], initialRelayFees_opt: Option[(MilliSatoshi, Int)], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { + override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], relayFees_opt: Option[(MilliSatoshi, Int)], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { + relayFees_opt match { + case Some((feeBase, feeProportionalMillionths)) => updateRelayFee(List(nodeId), feeBase, feeProportionalMillionths) + case None => () + } // we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds)) (appKit.switchboard ? Peer.OpenChannel( @@ -185,7 +189,6 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { fundingSatoshis = fundingAmount, pushMsat = pushAmount_opt.getOrElse(0 msat), fundingTxFeeratePerKw_opt = fundingFeeratePerByte_opt.map(FeeratePerKw(_)), - initialRelayFees_opt = initialRelayFees_opt, channelFlags = flags_opt.map(_.toByte), timeout_opt = Some(openTimeout))).mapTo[ChannelOpenResponse] } @@ -198,8 +201,13 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { sendToChannels[CommandResponse[CMD_FORCECLOSE]](channels, CMD_FORCECLOSE(ActorRef.noSender)) } - override def updateRelayFee(channels: List[ApiTypes.ChannelIdentifier], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] = { - sendToChannels[CommandResponse[CMD_UPDATE_RELAY_FEE]](channels, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths)) + override def updateRelayFee(nodes: List[PublicKey], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] = { + for (node_id <- nodes) { + appKit.nodeParams.db.relayFees.addOrUpdateFees(node_id, feeBaseMsat, feeProportionalMillionths) + } + allChannels() + .map(channels => channels.filter(c => nodes.contains(c.a) || nodes.contains(c.b)).map(c => Right(c.shortChannelId))) + .flatMap(channels => sendToChannels[CommandResponse[CMD_UPDATE_RELAY_FEE]](channels.toList, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths))) } override def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]] = for { 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 d895b3d052..e697efa6e9 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 @@ -195,7 +195,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId startWith(WAIT_FOR_INIT_INTERNAL, Nothing) when(WAIT_FOR_INIT_INTERNAL)(handleExceptions { - case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, _, localParams, remote, _, channelFlags, channelConfig, channelFeatures), Nothing) => + case Event(initFunder@INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, remote, _, channelFlags, channelConfig, channelFeatures), Nothing) => context.system.eventStream.publish(ChannelCreated(self, peer, remoteNodeId, isFunder = true, temporaryChannelId, initialFeeratePerKw, Some(fundingTxFeeratePerKw))) activeConnection = remote txPublisher ! SetChannelId(remoteNodeId, temporaryChannelId) @@ -285,8 +285,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.channelUpdate.shortChannelId, None)) // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down - // NB: we don't update the routing fees, because we don't want to overwrite manual changes made with CMD_UPDATE_RELAY_FEE - // Since CMD_UPDATE_RELAY_FEE is handled even when being offline, that's the preferred solution to update routing fees + val defaultFees = nodeParams.relayParams.defaultFees(data.commitments.announceChannel) + val (feeBase, feeProportionalMillionth) = nodeParams.db.relayFees.getFees(remoteNodeId).getOrElse((defaultFees.feeBase, defaultFees.feeProportionalMillionth.toLong)) val candidateChannelUpdate = Announcements.makeChannelUpdate( nodeParams.chainHash, nodeParams.privateKey, @@ -294,8 +294,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId normal.channelUpdate.shortChannelId, nodeParams.expiryDelta, normal.commitments.remoteParams.htlcMinimum, - normal.channelUpdate.feeBaseMsat, - normal.channelUpdate.feeProportionalMillionths, + feeBase, + feeProportionalMillionth, normal.commitments.capacity.toMilliSatoshi, enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) val channelUpdate1 = if (Announcements.areSame(candidateChannelUpdate, normal.channelUpdate)) { @@ -381,7 +381,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId initFeatures = remoteInit.features, shutdownScript = remoteShutdownScript) log.debug("remote params: {}", remoteParams) - goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, None, open.firstPerCommitmentPoint, open.channelFlags, channelConfig, channelFeatures, accept) sending accept + goto(WAIT_FOR_FUNDING_CREATED) using DATA_WAIT_FOR_FUNDING_CREATED(open.temporaryChannelId, localParams, remoteParams, open.fundingSatoshis, open.pushMsat, open.feeratePerKw, open.firstPerCommitmentPoint, open.channelFlags, channelConfig, channelFeatures, accept) sending accept } case Event(c: CloseCommand, d) => handleFastClose(c, d.channelId) @@ -392,7 +392,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_ACCEPT_CHANNEL)(handleExceptions { - case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, initialRelayFees_opt, localParams, _, remoteInit, _, channelConfig, channelFeatures), open)) => + case Event(accept: AcceptChannel, d@DATA_WAIT_FOR_ACCEPT_CHANNEL(INPUT_INIT_FUNDER(temporaryChannelId, fundingSatoshis, pushMsat, initialFeeratePerKw, fundingTxFeeratePerKw, localParams, _, remoteInit, _, channelConfig, channelFeatures), open)) => log.info(s"received AcceptChannel=$accept") Helpers.validateParamsFunder(nodeParams, channelFeatures, open, accept) match { case Left(t) => handleLocalError(t, d, Some(accept)) @@ -416,7 +416,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val localFundingPubkey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val fundingPubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(localFundingPubkey.publicKey, remoteParams.fundingPubKey))) wallet.makeFundingTx(fundingPubkeyScript, fundingSatoshis, fundingTxFeeratePerKw).pipeTo(self) - goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, initialRelayFees_opt, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) + goto(WAIT_FOR_FUNDING_INTERNAL) using DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingSatoshis, pushMsat, initialFeeratePerKw, accept.firstPerCommitmentPoint, channelConfig, channelFeatures, open) } case Event(c: CloseCommand, d: DATA_WAIT_FOR_ACCEPT_CHANNEL) => @@ -437,7 +437,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_FUNDING_INTERNAL)(handleExceptions { - case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, initialRelayFees_opt, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => + case Event(MakeFundingTxResponse(fundingTx, fundingTxOutputIndex, fundingTxFee), d@DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelConfig, channelFeatures, open)) => // let's create the first commitment tx that spends the yet uncommitted funding tx Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTx.hash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { case Left(ex) => handleLocalError(ex, d, None) @@ -456,7 +456,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId txPublisher ! SetChannelId(remoteNodeId, channelId) context.system.eventStream.publish(ChannelIdAssigned(self, remoteNodeId, temporaryChannelId, channelId)) // NB: we don't send a ChannelSignatureSent for the first commit - goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, initialRelayFees_opt, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, channelConfig, channelFeatures, fundingCreated) sending fundingCreated + goto(WAIT_FOR_FUNDING_SIGNED) using DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, RemoteCommit(0, remoteSpec, remoteCommitTx.tx.txid, remoteFirstPerCommitmentPoint), open.channelFlags, channelConfig, channelFeatures, fundingCreated) sending fundingCreated } case Event(Status.Failure(t), d: DATA_WAIT_FOR_FUNDING_INTERNAL) => @@ -482,7 +482,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_FUNDING_CREATED)(handleExceptions { - case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, initialRelayFees_opt, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => + case Event(FundingCreated(_, fundingTxHash, fundingTxOutputIndex, remoteSig), d@DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, _)) => // they fund the channel with their funding tx, so the money is theirs (but we are paid pushMsat) Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, temporaryChannelId, localParams, remoteParams, fundingAmount, pushMsat, initialFeeratePerKw, fundingTxHash, fundingTxOutputIndex, remoteFirstPerCommitmentPoint) match { case Left(ex) => handleLocalError(ex, d, None) @@ -518,7 +518,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId watchFundingTx(commitments) val fundingMinDepth = Helpers.minDepthForFunding(nodeParams, fundingAmount) blockchain ! WatchFundingConfirmed(self, commitInput.outPoint.txid, fundingMinDepth) - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, initialRelayFees_opt, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned } } @@ -532,7 +532,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_FUNDING_SIGNED)(handleExceptions { - case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, initialRelayFees_opt, localSpec, localCommitTx, remoteCommit, channelFlags, channelConfig, channelFeatures, fundingCreated)) => + case Event(msg@FundingSigned(_, remoteSig), d@DATA_WAIT_FOR_FUNDING_SIGNED(channelId, localParams, remoteParams, fundingTx, fundingTxFee, localSpec, localCommitTx, remoteCommit, channelFlags, channelConfig, channelFeatures, fundingCreated)) => // we make sure that their sig checks out and that our first commit tx is spendable val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath) val localSigOfLocalTx = keyManager.sign(localCommitTx, fundingPubKey, TxOwner.Local, channelFeatures.commitmentFormat) @@ -576,7 +576,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId } } - goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), initialRelayFees_opt, now, None, Left(fundingCreated)) storing() calling publishFundingTx() + goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, Some(fundingTx), now, None, Left(fundingCreated)) storing() calling publishFundingTx() } case Event(c: CloseCommand, d: DATA_WAIT_FOR_FUNDING_SIGNED) => @@ -609,7 +609,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.info(s"received their FundingLocked, deferring message") stay() using d.copy(deferred = Some(msg)) // no need to store, they will re-send if we get disconnected - case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, initialRelayFees_opt, _, deferred, _)) => + case Event(WatchFundingConfirmedTriggered(blockHeight, txIndex, fundingTx), d@DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, _, _, deferred, _)) => Try(Transaction.correctlySpends(commitments.fullySignedLocalCommitTx(keyManager).tx, Seq(fundingTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)) match { case Success(_) => log.info(s"channelId=${commitments.channelId} was confirmed at blockHeight=$blockHeight txIndex=$txIndex") @@ -622,7 +622,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // as soon as it reaches NORMAL state, and before it is announced on the network // (this id might be updated when the funding tx gets deeply buried, if there was a reorg in the meantime) val shortChannelId = ShortChannelId(blockHeight, txIndex, commitments.commitInput.outPoint.index.toInt) - goto(WAIT_FOR_FUNDING_LOCKED) using DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, fundingLocked, initialRelayFees_opt) storing() sending fundingLocked + goto(WAIT_FOR_FUNDING_LOCKED) using DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, fundingLocked) storing() sending fundingLocked case Failure(t) => log.error(t, s"rejecting channel with invalid funding tx: ${fundingTx.bin}") goto(CLOSED) @@ -658,18 +658,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId }) when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { - case Event(FundingLocked(_, nextPerCommitmentPoint), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _, initialRelayFees_opt)) => + case Event(FundingLocked(_, nextPerCommitmentPoint), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _)) => // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) blockchain ! WatchFundingDeeplyBuried(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val defaultFees = - if (commitments.announceChannel) { - (nodeParams.relayParams.publicChannelFees.feeBase, nodeParams.relayParams.publicChannelFees.feeProportionalMillionth) - } else { - (nodeParams.relayParams.privateChannelFees.feeBase, nodeParams.relayParams.privateChannelFees.feeProportionalMillionth) - } - val (feeBase, feeProportionalMillionths) = initialRelayFees_opt.getOrElse(defaultFees) + val defaultFees = nodeParams.relayParams.defaultFees(commitments.announceChannel) + val (feeBase, feeProportionalMillionths) = nodeParams.db.relayFees.getFees(remoteNodeId).getOrElse((defaultFees.feeBase, defaultFees.feeProportionalMillionth.toLong)) val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, feeBase, feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) 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 979479e5d0..2b3e5facde 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 @@ -82,7 +82,6 @@ case class INPUT_INIT_FUNDER(temporaryChannelId: ByteVector32, pushAmount: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, fundingTxFeeratePerKw: FeeratePerKw, - initialRelayFees_opt: Option[(MilliSatoshi, Int)], localParams: LocalParams, remote: ActorRef, remoteInit: Init, @@ -380,7 +379,6 @@ final case class DATA_WAIT_FOR_FUNDING_INTERNAL(temporaryChannelId: ByteVector32 fundingAmount: Satoshi, pushAmount: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, - initialRelayFees_opt: Option[(MilliSatoshi, Int)], remoteFirstPerCommitmentPoint: PublicKey, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, @@ -393,7 +391,6 @@ final case class DATA_WAIT_FOR_FUNDING_CREATED(temporaryChannelId: ByteVector32, fundingAmount: Satoshi, pushAmount: MilliSatoshi, initialFeeratePerKw: FeeratePerKw, - initialRelayFees_opt: Option[(MilliSatoshi, Int)], remoteFirstPerCommitmentPoint: PublicKey, channelFlags: Byte, channelConfig: ChannelConfig, @@ -406,7 +403,6 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, remoteParams: RemoteParams, fundingTx: Transaction, fundingTxFee: Satoshi, - initialRelayFees_opt: Option[(MilliSatoshi, Int)], localSpec: CommitmentSpec, localCommitTx: CommitTx, remoteCommit: RemoteCommit, @@ -416,11 +412,10 @@ final case class DATA_WAIT_FOR_FUNDING_SIGNED(channelId: ByteVector32, lastSent: FundingCreated) extends Data final case class DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments: Commitments, fundingTx: Option[Transaction], - initialRelayFees_opt: Option[(MilliSatoshi, Int)], waitingSinceBlock: Long, // how long have we been waiting for the funding tx to confirm deferred: Option[FundingLocked], lastSent: Either[FundingCreated, FundingSigned]) extends Data with HasCommitments -final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked, initialRelayFees_opt: Option[(MilliSatoshi, Int)]) extends Data with HasCommitments +final case class DATA_WAIT_FOR_FUNDING_LOCKED(commitments: Commitments, shortChannelId: ShortChannelId, lastSent: FundingLocked) extends Data with HasCommitments final case class DATA_NORMAL(commitments: Commitments, shortChannelId: ShortChannelId, buried: Boolean, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala index 600357180a..7b45b0ac7c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala @@ -39,6 +39,7 @@ trait Databases { def peers: PeersDb def payments: PaymentsDb def pendingCommands: PendingCommandsDb + def relayFees: RelayFeesDb //@formatter:on } @@ -60,6 +61,7 @@ object Databases extends Logging { peers: SqlitePeersDb, payments: SqlitePaymentsDb, pendingCommands: SqlitePendingCommandsDb, + relayFees: SqliteRelayFeesDb, private val backupConnection: Connection) extends Databases with FileBackup { override def backup(backupFile: File): Unit = SqliteUtils.using(backupConnection.createStatement()) { statement => { @@ -76,6 +78,7 @@ object Databases extends Logging { peers = new SqlitePeersDb(eclairJdbc), payments = new SqlitePaymentsDb(eclairJdbc), pendingCommands = new SqlitePendingCommandsDb(eclairJdbc), + relayFees = new SqliteRelayFeesDb(eclairJdbc), backupConnection = eclairJdbc ) } @@ -86,6 +89,7 @@ object Databases extends Logging { peers: PgPeersDb, payments: PgPaymentsDb, pendingCommands: PgPendingCommandsDb, + relayFees: PgRelayFeesDb, dataSource: HikariDataSource, lock: PgLock) extends Databases with ExclusiveLock { override def obtainExclusiveLock(): Unit = lock.obtainExclusiveLock(dataSource) @@ -121,6 +125,7 @@ object Databases extends Logging { peers = new PgPeersDb, payments = new PgPaymentsDb, pendingCommands = new PgPendingCommandsDb, + relayFees = new PgRelayFeesDb, dataSource = ds, lock = lock) 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 e0182dc099..cea9c9db89 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 @@ -41,6 +41,8 @@ case class DualDatabases(sqlite: SqliteDatabases, postgres: PostgresDatabases) e override val pendingCommands: PendingCommandsDb = DualPendingCommandsDb(sqlite.pendingCommands, postgres.pendingCommands) + override val relayFees: RelayFeesDb = DualRelayFeesDb(sqlite.relayFees, postgres.relayFees) + override def backup(backupFile: File): Unit = sqlite.backup(backupFile) } @@ -371,6 +373,26 @@ case class DualPendingCommandsDb(sqlite: SqlitePendingCommandsDb, postgres: PgPe sqlite.listSettlementCommands() } + override def close(): Unit = { + runAsync(postgres.close()) + sqlite.close() + } +} + +case class DualRelayFeesDb(sqlite: SqliteRelayFeesDb, postgres: PgRelayFeesDb) extends RelayFeesDb { + + private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-relay-fees").build())) + + override def addOrUpdateFees(nodeId: Crypto.PublicKey, feeBase: MilliSatoshi, feeProportionalMillionths: Long): Unit = { + runAsync(postgres.addOrUpdateFees(nodeId, feeBase, feeProportionalMillionths)) + sqlite.addOrUpdateFees(nodeId, feeBase, feeProportionalMillionths) + } + + override def getFees(nodeId: Crypto.PublicKey): Option[(MilliSatoshi, Long)] = { + runAsync(postgres.getFees(nodeId)) + sqlite.getFees(nodeId) + } + override def close(): Unit = { runAsync(postgres.close()) sqlite.close() diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala new file mode 100644 index 0000000000..0fa0acb0c1 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2021 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.db + +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.MilliSatoshi + +import java.io.Closeable + +trait RelayFeesDb extends Closeable { + + def addOrUpdateFees(nodeId: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionths: Long): Unit + + def getFees(nodeId: PublicKey): Option[(MilliSatoshi, Long)] + +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala new file mode 100644 index 0000000000..a41102b193 --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala @@ -0,0 +1,83 @@ +/* + * Copyright 2021 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.db.pg + +import fr.acinq.bitcoin.Crypto +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics +import fr.acinq.eclair.db.Monitoring.Tags.DbBackends +import fr.acinq.eclair.db.RelayFeesDb +import fr.acinq.eclair.db.pg.PgUtils.PgLock +import grizzled.slf4j.Logging + +import java.sql.Timestamp +import java.time.Instant +import javax.sql.DataSource + +class PgRelayFeesDb(implicit ds: DataSource, lock: PgLock) extends RelayFeesDb with Logging { + + import PgUtils.ExtendedResultSet._ + import PgUtils._ + import lock._ + + val DB_NAME = "relay_fees" + val CURRENT_VERSION = 1 + + inTransaction { pg => + using(pg.createStatement()) { statement => + getVersion(statement, DB_NAME) match { + case None => + statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local") + statement.executeUpdate("CREATE TABLE local.relay_fees (node_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE INDEX relay_fees_node_id_idx ON local.relay_fees(node_id)") + statement.executeUpdate("CREATE INDEX relay_fees_timestamp_idx ON local.relay_fees(timestamp)") + case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do + case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") + } + setVersion(statement, DB_NAME, CURRENT_VERSION) + } + } + + override def addOrUpdateFees(nodeId: Crypto.PublicKey, feeBase: MilliSatoshi, feeProportionalMillionths: Long): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Postgres) { + withLock { pg => + using(pg.prepareStatement( + "INSERT INTO local.relay_fees VALUES (?, ?, ?, ?)")) { statement => + statement.setString(1, nodeId.value.toHex) + statement.setLong(2, feeBase.toLong) + statement.setLong(3, feeProportionalMillionths) + statement.setTimestamp(4, Timestamp.from(Instant.now())) + statement.executeUpdate() + } + } + } + + override def getFees(nodeId: PublicKey): Option[(MilliSatoshi, Long)] = withMetrics("relay_fees/get", DbBackends.Postgres) { + withLock { pg => + using(pg.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM local.relay_fees WHERE node_id=? ORDER BY timestamp DESC LIMIT 1")) { statement => + statement.setString(1, nodeId.value.toHex) + statement.executeQuery() + .headOption + .map(rs => + (MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) + ) + } + } + } + + override def close(): Unit = () +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala new file mode 100644 index 0000000000..2842df871e --- /dev/null +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala @@ -0,0 +1,70 @@ +/* + * Copyright 2021 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.db.sqlite + +import fr.acinq.bitcoin.Crypto +import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics +import fr.acinq.eclair.db.Monitoring.Tags.DbBackends +import fr.acinq.eclair.db.RelayFeesDb +import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, setVersion, using} + +import java.sql.Connection + +class SqliteRelayFeesDb(sqlite: Connection) extends RelayFeesDb { + + import SqliteUtils.ExtendedResultSet._ + + val DB_NAME = "relay_fees" + val CURRENT_VERSION = 1 + + using(sqlite.createStatement(), inTransaction = true) { statement => + getVersion(statement, DB_NAME) match { + case None => + statement.executeUpdate("CREATE TABLE relay_fees (node_id BLOB NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE INDEX relay_fees_node_id_idx ON relay_fees(node_id)") + statement.executeUpdate("CREATE INDEX relay_fees_timestamp_idx ON relay_fees(timestamp)") + case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do + case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") + } + setVersion(statement, DB_NAME, CURRENT_VERSION) + } + + override def addOrUpdateFees(nodeId: Crypto.PublicKey, feeBase: MilliSatoshi, feeProportionalMillionths: Long): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Sqlite) { + using(sqlite.prepareStatement("INSERT INTO relay_fees VALUES (?, ?, ?, ?)")) { statement => + statement.setBytes(1, nodeId.value.toArray) + statement.setLong(2, feeBase.toLong) + statement.setLong(3, feeProportionalMillionths) + statement.setLong(4, System.currentTimeMillis) + statement.executeUpdate() + } + } + + override def getFees(nodeId: PublicKey): Option[(MilliSatoshi, Long)] = withMetrics("relay_fees/get", DbBackends.Sqlite) { + using(sqlite.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM relay_fees WHERE node_id=? ORDER BY timestamp DESC LIMIT 1")) { statement => + statement.setBytes(1, nodeId.value.toArray) + statement.executeQuery() + .headOption + .map(rs => + (MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) + ) + } + } + + override def close(): Unit = sqlite.close() +} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala index 2d3a92f312..0d9d62367f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Peer.scala @@ -144,7 +144,7 @@ class Peer(val nodeParams: NodeParams, remoteNodeId: PublicKey, wallet: EclairWa val channelFeeratePerKw = nodeParams.onChainFeeConf.getCommitmentFeerate(remoteNodeId, channelFeatures, c.fundingSatoshis, None) val fundingTxFeeratePerKw = c.fundingTxFeeratePerKw_opt.getOrElse(nodeParams.onChainFeeConf.feeEstimator.getFeeratePerKw(target = nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) log.info(s"requesting a new channel with fundingSatoshis=${c.fundingSatoshis}, pushMsat=${c.pushMsat} and fundingFeeratePerByte=${c.fundingTxFeeratePerKw_opt} temporaryChannelId=$temporaryChannelId localParams=$localParams") - channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, c.initialRelayFees_opt, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), channelConfig, channelFeatures) + channel ! INPUT_INIT_FUNDER(temporaryChannelId, c.fundingSatoshis, c.pushMsat, channelFeeratePerKw, fundingTxFeeratePerKw, localParams, d.peerConnection, d.remoteInit, c.channelFlags.getOrElse(nodeParams.channelFlags), channelConfig, channelFeatures) stay() using d.copy(channels = d.channels + (TemporaryChannelId(temporaryChannelId) -> channel)) } @@ -409,7 +409,7 @@ object Peer { } case class Disconnect(nodeId: PublicKey) extends PossiblyHarmful - case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, fundingTxFeeratePerKw_opt: Option[FeeratePerKw], initialRelayFees_opt: Option[(MilliSatoshi, Int)], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful { + case class OpenChannel(remoteNodeId: PublicKey, fundingSatoshis: Satoshi, pushMsat: MilliSatoshi, fundingTxFeeratePerKw_opt: Option[FeeratePerKw], channelFlags: Option[Byte], timeout_opt: Option[Timeout]) extends PossiblyHarmful { require(pushMsat <= fundingSatoshis, s"pushMsat must be less or equal to fundingSatoshis") require(fundingSatoshis >= 0.sat, s"fundingSatoshis must be positive") require(pushMsat >= 0.msat, s"pushMsat must be positive") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index 140d09bb9a..4e19a844bd 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -113,9 +113,18 @@ object Relayer extends Logging { // @formatter:off case class RelayFees(feeBase: MilliSatoshi, feeProportionalMillionth: Int) + case class RelayParams(publicChannelFees: RelayFees, privateChannelFees: RelayFees, - minTrampolineFees: RelayFees) + minTrampolineFees: RelayFees) { + def defaultFees(announceChannel: Boolean): RelayFees = { + if (announceChannel) { + publicChannelFees + } else { + privateChannelFees + } + } + } case class RelayForward(add: UpdateAddHtlc) case class UsableBalance(remoteNodeId: PublicKey, shortChannelId: ShortChannelId, canSend: MilliSatoshi, canReceive: MilliSatoshi, isPublic: Boolean) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala index b4c22df3c6..e623df31ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version0/ChannelCodecs0.scala @@ -286,7 +286,6 @@ private[channel] object ChannelCodecs0 { val DATA_WAIT_FOR_FUNDING_CONFIRMED_COMPAT_01_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | provide[Option[Transaction]](None)) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)])) :: ("waitingSince" | provide(System.currentTimeMillis.milliseconds.toSeconds)) :: ("deferred" | optional(bool, fundingLockedCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly @@ -294,7 +293,6 @@ private[channel] object ChannelCodecs0 { val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool, txCodec)) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)])) :: ("waitingSince" | int64) :: ("deferred" | optional(bool, fundingLockedCodec)) :: ("lastSent" | either(bool, fundingCreatedCodec, fundingSignedCodec))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED].decodeOnly @@ -302,8 +300,7 @@ private[channel] object ChannelCodecs0 { val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | shortchannelid) :: - ("lastSent" | fundingLockedCodec) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)]))).as[DATA_WAIT_FOR_FUNDING_LOCKED].decodeOnly + ("lastSent" | fundingLockedCodec)).as[DATA_WAIT_FOR_FUNDING_LOCKED].decodeOnly // this is a decode-only codec compatible with versions 9afb26e and below val DATA_NORMAL_COMPAT_03_Codec: Codec[DATA_NORMAL] = ( diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala index fd145291e9..d8e4293c92 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version1/ChannelCodecs1.scala @@ -232,7 +232,6 @@ private[channel] object ChannelCodecs1 { val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)])) :: ("waitingSince" | int64) :: ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] @@ -240,8 +239,7 @@ private[channel] object ChannelCodecs1 { val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec)) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)]))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala index a73cf05661..0fabb04cca 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version2/ChannelCodecs2.scala @@ -267,7 +267,6 @@ private[channel] object ChannelCodecs2 { val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)])) :: ("waitingSince" | int64) :: ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] @@ -275,8 +274,7 @@ private[channel] object ChannelCodecs2 { val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec)) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)]))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala index c9b6d3ef99..dc213c2cc2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/wire/internal/channel/version3/ChannelCodecs3.scala @@ -287,7 +287,6 @@ private[channel] object ChannelCodecs3 { val DATA_WAIT_FOR_FUNDING_CONFIRMED_Codec: Codec[DATA_WAIT_FOR_FUNDING_CONFIRMED] = ( ("commitments" | commitmentsCodec) :: ("fundingTx" | optional(bool8, txCodec)) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)])) :: ("waitingSince" | int64) :: ("deferred" | optional(bool8, lengthDelimited(fundingLockedCodec))) :: ("lastSent" | either(bool8, lengthDelimited(fundingCreatedCodec), lengthDelimited(fundingSignedCodec)))).as[DATA_WAIT_FOR_FUNDING_CONFIRMED] @@ -295,8 +294,7 @@ private[channel] object ChannelCodecs3 { val DATA_WAIT_FOR_FUNDING_LOCKED_Codec: Codec[DATA_WAIT_FOR_FUNDING_LOCKED] = ( ("commitments" | commitmentsCodec) :: ("shortChannelId" | shortchannelid) :: - ("lastSent" | lengthDelimited(fundingLockedCodec)) :: - ("initialRelayFees" | provide(Option.empty[(MilliSatoshi, Int)]))).as[DATA_WAIT_FOR_FUNDING_LOCKED] + ("lastSent" | lengthDelimited(fundingLockedCodec))).as[DATA_WAIT_FOR_FUNDING_LOCKED] val DATA_NORMAL_Codec: Codec[DATA_NORMAL] = ( ("commitments" | commitmentsCodec) :: 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 947f1a499a..f57263cd18 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -90,12 +90,12 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") // standard conversion - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), initialRelayFees_opt = None, flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), relayFees_opt = None, flags_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] assert(open.fundingTxFeeratePerKw_opt === Some(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), initialRelayFees_opt = None, flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), relayFees_opt = None, flags_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] assert(open1.fundingTxFeeratePerKw_opt === Some(FeeratePerKw.MinimumFeeratePerKw)) } @@ -484,4 +484,42 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I assert(verifiedMessage.publicKey !== kit.nodeParams.nodeId) } + test("update relay fees in database") { f => + import f._ + + val relayFeesDb = mock[RelayFeesDb] + + val databases = mock[Databases] + databases.relayFees returns relayFeesDb + + val kitWithMockDb = kit.copy(nodeParams = kit.nodeParams.copy(db = databases)) + val eclair = new EclairImpl(kitWithMockDb) + + val a = randomKey().publicKey + val b = randomKey().publicKey + + eclair.updateRelayFee(List(a, b), 999 msat, 1234) + + relayFeesDb.addOrUpdateFees(a, 999 msat, 1234).wasCalled(once) + relayFeesDb.addOrUpdateFees(b, 999 msat, 1234).wasCalled(once) + } + + test("opening a channel with non default relay fees updates relay fees for the node") { f => + import f._ + + val relayFeesDb = mock[RelayFeesDb] + + val databases = mock[Databases] + databases.relayFees returns relayFeesDb + + val kitWithMockDb = kit.copy(nodeParams = kit.nodeParams.copy(db = databases)) + val eclair = new EclairImpl(kitWithMockDb) + + val a = randomKey().publicKey + + eclair.open(a, 10000000L sat, None, None, Some(888 msat, 2345), None, None) + + relayFeesDb.addOrUpdateFees(a, 888 msat, 2345).wasCalled(once) + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index 17c047460d..a8edd94855 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -30,6 +30,7 @@ sealed trait TestDatabases extends Databases { override def peers: PeersDb = db.peers override def payments: PaymentsDb = db.payments override def pendingCommands: PendingCommandsDb = db.pendingCommands + override def relayFees: RelayFeesDb = db.relayFees def close(): Unit // @formatter:on } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala index 562abfc074..d334ed4b40 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/FuzzySpec.scala @@ -79,7 +79,7 @@ class FuzzySpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateT registerA ! alice registerB ! bob // no announcements - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelConfig.standard, ChannelFeatures()) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Alice.channelParams, pipe, bobInit, channelFlags = 0x00.toByte, ChannelConfig.standard, ChannelFeatures()) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, ChannelConfig.standard, ChannelFeatures()) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] 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 5c6e1b9d99..a12ee7e88d 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 @@ -162,7 +162,7 @@ trait StateTestsHelperMethods extends TestKitBase { val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, aliceChannelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, initialFeeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, channelFlags, channelConfig, aliceChannelFeatures) assert(alice2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) assert(bob2blockchain.expectMsgType[TxPublisher.SetChannelId].channelId === ByteVector32.Zeroes) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala index 04d0ec3031..59783dff53 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForAcceptChannelStateSpec.scala @@ -65,7 +65,7 @@ class WaitForAcceptChannelStateSpec extends TestKitBaseClass with FixtureAnyFunS val bobInit = Init(bobParams.initFeatures) within(30 seconds) { val fundingAmount = if (test.tags.contains(StateTestsTags.Wumbo)) Btc(5).toSatoshi else TestConstants.fundingSatoshis - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingAmount, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala index 1e3e0eac78..8bb4413bc3 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/a/WaitForOpenChannelStateSpec.scala @@ -53,7 +53,7 @@ class WaitForOpenChannelStateSpec extends TestKitBaseClass with FixtureAnyFunSui val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) awaitCond(bob.stateName == WAIT_FOR_OPEN_CHANNEL) withFixture(test.toNoArgTest(FixtureParam(alice, bob, alice2bob, bob2alice, bob2blockchain))) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala index a979951684..1c842f2776 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedInternalStateSpec.scala @@ -50,7 +50,7 @@ class WaitForFundingCreatedInternalStateSpec extends TestKitBaseClass with Fixtu val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) alice2bob.expectMsgType[OpenChannel] alice2bob.forward(bob) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala index 5bd4fd0616..aa6c93e914 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingCreatedStateSpec.scala @@ -63,7 +63,7 @@ class WaitForFundingCreatedStateSpec extends TestKitBaseClass with FixtureAnyFun val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala index d57c6e9427..724ead8267 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/b/WaitForFundingSignedStateSpec.scala @@ -61,7 +61,7 @@ class WaitForFundingSignedStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, fundingSatoshis, pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala index cad9e03c17..5c5c5d899e 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingConfirmedStateSpec.scala @@ -50,7 +50,7 @@ class WaitForFundingConfirmedStateSpec extends TestKitBaseClass with FixtureAnyF val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 48bcddf0d2..602359c7af 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -23,7 +23,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.StateTestsBase import fr.acinq.eclair.wire.protocol._ -import fr.acinq.eclair.{MilliSatoshiLong, TestConstants, TestKitBaseClass} +import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, TestConstants, TestKitBaseClass} import org.scalatest.Outcome import org.scalatest.funsuite.FixtureAnyFunSuiteLike @@ -35,7 +35,7 @@ import scala.concurrent.duration._ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTestsBase { - val initialRelayFees = (1000 msat, 100) + val relayFees: (MilliSatoshi, Long) = (999 msat, 1234) case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, router: TestProbe) @@ -47,7 +47,8 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, Some(initialRelayFees), aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) + alice.underlyingActor.nodeParams.db.relayFees.addOrUpdateFees(bobParams.nodeId, relayFees._1, relayFees._2) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] @@ -84,8 +85,8 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice) awaitCond(alice.stateName == NORMAL) val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - assert(initialChannelUpdate.feeBaseMsat === initialRelayFees._1) - assert(initialChannelUpdate.feeProportionalMillionths === initialRelayFees._2) + assert(initialChannelUpdate.feeBaseMsat === relayFees._1) + assert(initialChannelUpdate.feeProportionalMillionths === relayFees._2) bob2alice.expectNoMessage(200 millis) } 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 570c510654..6fe54a7572 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 @@ -67,7 +67,7 @@ class ClosingStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with val (aliceParams, aliceChannelFeatures, bobParams, bobChannelFeatures) = computeFeatures(setup, test.tags) val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, None, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) alice2blockchain.expectMsgType[SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) bob2blockchain.expectMsgType[SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala new file mode 100644 index 0000000000..046626c726 --- /dev/null +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala @@ -0,0 +1,62 @@ +/* + * Copyright 2021 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.db + +import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} +import fr.acinq.eclair.db.pg.PgRelayFeesDb +import fr.acinq.eclair.db.sqlite.SqliteRelayFeesDb +import fr.acinq.eclair._ +import org.scalatest.funsuite.AnyFunSuite + +class RelayFeesDbSpec extends AnyFunSuite { + + import fr.acinq.eclair.TestDatabases.forAllDbs + + test("init database two times in a row") { + forAllDbs { + case sqlite: TestSqliteDatabases => + new SqliteRelayFeesDb(sqlite.connection) + new SqliteRelayFeesDb(sqlite.connection) + case pg: TestPgDatabases => + new PgRelayFeesDb()(pg.datasource, pg.lock) + new PgRelayFeesDb()(pg.datasource, pg.lock) + } + } + + test("add and update relay fees") { + forAllDbs { dbs => + val db = dbs.relayFees + + val a = randomKey().publicKey + val b = randomKey().publicKey + + assert(db.getFees(a) === None) + assert(db.getFees(b) === None) + db.addOrUpdateFees(a, 1 msat, 123) + assert(db.getFees(a) === Some(1 msat, 123)) + assert(db.getFees(b) === None) + Thread.sleep(2) + db.addOrUpdateFees(a, 2 msat, 456) + assert(db.getFees(a) === Some(2 msat, 456)) + assert(db.getFees(b) === None) + db.addOrUpdateFees(b, 3 msat, 789) + assert(db.getFees(a) === Some(2 msat, 456)) + assert(db.getFees(b) === Some(3 msat, 789)) + } + } + +} 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 3c17eae336..3d459e1b76 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 @@ -153,7 +153,6 @@ abstract class IntegrationSpec extends TestKitBaseClass with BitcoindService wit fundingSatoshis = fundingSatoshis, pushMsat = pushMsat, fundingTxFeeratePerKw_opt = None, - initialRelayFees_opt = None, channelFlags = None, timeout_opt = None)) sender.expectMsgType[ChannelOpenResponse.ChannelOpened](10 seconds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala index ed0ab17a2a..fa376a3f49 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/interop/rustytests/RustyTestsSpec.scala @@ -74,7 +74,7 @@ class RustyTestsSpec extends TestKitBaseClass with Matchers with FixtureAnyFunSu val bobInit = Init(Bob.channelParams.initFeatures) // alice and bob will both have 1 000 000 sat feeEstimator.setFeerate(FeeratesPerKw.single(FeeratePerKw(10000 sat))) - alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), None, Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, channelConfig, channelFeatures) + alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, 2000000 sat, 1000000000 msat, feeEstimator.getFeeratePerKw(target = 2), feeEstimator.getFeeratePerKw(target = 6), Alice.channelParams, pipe, bobInit, ChannelFlags.Empty, channelConfig, channelFeatures) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, Bob.channelParams, pipe, aliceInit, channelConfig, channelFeatures) bob2blockchain.expectMsgType[TxPublisher.SetChannelId] diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala index 766d2369f8..9f8db16c2f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/io/PeerSpec.scala @@ -270,7 +270,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection) assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None)) assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big, you must enable large channels support in 'eclair.features' to use funding above ${Channel.MAX_FUNDING} (see eclair.conf)") } @@ -284,7 +284,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection) // Bob doesn't support wumbo, Alice does assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None)) assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big, the remote peer doesn't support wumbo") } @@ -298,28 +298,11 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(Wumbo -> Optional))) // Bob supports wumbo assert(peer.stateData.channels.isEmpty) - probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, fundingAmountBig, 0 msat, None, None, None)) assert(probe.expectMsgType[Failure].cause.getMessage == s"fundingSatoshis=$fundingAmountBig is too big for the current settings, increase 'eclair.max-funding-satoshis' (see eclair.conf)") } - test("use correct fee rates when spawning a channel") { f => - import f._ - - val probe = TestProbe() - connect(remoteNodeId, peer, peerConnection) - assert(peer.stateData.channels.isEmpty) - - val relayFees = Some(100 msat, 1000) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 12300 sat, 0 msat, None, relayFees, None, None)) - val init = channel.expectMsgType[INPUT_INIT_FUNDER] - assert(init.channelConfig === ChannelConfig.standard) - assert(init.channelFeatures === ChannelFeatures()) - assert(init.fundingAmount === 12300.sat) - assert(init.initialRelayFees_opt === relayFees) - awaitCond(peer.stateData.channels.nonEmpty) - } - test("use correct on-chain fee rates when spawning a channel (anchor outputs)", Tag("anchor_outputs")) { f => import f._ @@ -330,11 +313,10 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle // We ensure the current network feerate is higher than the default anchor output feerate. val feeEstimator = nodeParams.onChainFeeConf.feeEstimator.asInstanceOf[TestFeeEstimator] feeEstimator.setFeerate(FeeratesPerKw.single(TestConstants.anchorOutputsFeeratePerKw * 2)) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 0 msat, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] assert(init.channelFeatures === ChannelFeatures(StaticRemoteKey, AnchorOutputs)) assert(init.fundingAmount === 15000.sat) - assert(init.initialRelayFees_opt === None) assert(init.initialFeeratePerKw === TestConstants.anchorOutputsFeeratePerKw) assert(init.fundingTxFeeratePerKw === feeEstimator.getFeeratePerKw(nodeParams.onChainFeeConf.feeTargets.fundingBlockTarget)) } @@ -344,7 +326,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle val probe = TestProbe() connect(remoteNodeId, peer, peerConnection, remoteInit = protocol.Init(Features(StaticRemoteKey -> Mandatory))) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 24000 sat, 0 msat, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] assert(init.channelFeatures === ChannelFeatures(StaticRemoteKey)) assert(init.localParams.walletStaticPaymentBasepoint.isDefined) @@ -363,7 +345,7 @@ class PeerSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with Paralle } val peer = TestFSMRef(new Peer(TestConstants.Alice.nodeParams, remoteNodeId, new TestWallet, channelFactory)) connect(remoteNodeId, peer, peerConnection) - probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 100 msat, None, None, None, None)) + probe.send(peer, Peer.OpenChannel(remoteNodeId, 15000 sat, 100 msat, None, None, None)) val init = channel.expectMsgType[INPUT_INIT_FUNDER] assert(init.fundingAmount === 15000.sat) assert(init.pushAmount === 100.msat) diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/OpenChannelController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/OpenChannelController.scala index 962edd4b28..5f1be9d129 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/OpenChannelController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/OpenChannelController.scala @@ -116,7 +116,7 @@ class OpenChannelController(val handlers: Handlers, val stage: Stage) extends Lo feerateError.setText("Fee rate must be greater than 0") case (Success(capacitySat), Success(pushMsat), Success(feeratePerByte_opt)) => val channelFlags = if (publicChannel.isSelected) ChannelFlags.AnnounceChannel else ChannelFlags.Empty - handlers.open(nodeUri, Some(Peer.OpenChannel(nodeUri.nodeId, capacitySat, MilliSatoshi(pushMsat), feeratePerByte_opt.map(f => FeeratePerKw(FeeratePerByte(f.sat))), None, Some(channelFlags), Some(30 seconds)))) + handlers.open(nodeUri, Some(Peer.OpenChannel(nodeUri.nodeId, capacitySat, MilliSatoshi(pushMsat), feeratePerByte_opt.map(f => FeeratePerKw(FeeratePerByte(f.sat))), Some(channelFlags), Some(30 seconds)))) stage.close() case (Failure(t), _, _) => logger.error(s"could not parse capacity with cause=${t.getLocalizedMessage}") diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala index ad08015a12..bea2befe6c 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala @@ -71,6 +71,12 @@ trait ExtraDirectives extends Directives { provide((channelId ++ channelIds ++ shortChannelId ++ shortChannelIds).distinct) } + def withNodesIdentifier: Directive1[List[PublicKey]] = formFields(nodeIdFormParam.?, nodeIdsFormParam.?).tflatMap { + case (Some(nodeId), None) => provide(List(nodeId)) + case (None, Some(nodeIds)) => provide(nodeIds) + case _ => reject(MalformedFormFieldRejection("nodeId(s)", "Must specify nodeId or nodeIds (but not both)")) + } + def withRoute: Directive1[Either[Seq[ShortChannelId], Seq[PublicKey]]] = formFields(shortChannelIdsFormParam.?, nodeIdsFormParam.?).tflatMap { case (None, None) => reject(MalformedFormFieldRejection("nodeIds/shortChannelIds", "Must specify either nodeIds or shortChannelIds (but not both)")) case (Some(shortChannelIds), _) => provide(Left(shortChannelIds)) diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala index 6953b7fb38..40de4c5dbb 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Fees.scala @@ -34,9 +34,9 @@ trait Fees { } val updateRelayFee: Route = postRequest("updaterelayfee") { implicit t => - withChannelsIdentifier { channels => + withNodesIdentifier { nodes => formFields("feeBaseMsat".as[MilliSatoshi], "feeProportionalMillionths".as[Long]) { (feeBase, feeProportional) => - complete(eclairApi.updateRelayFee(channels, feeBase, feeProportional)) + complete(eclairApi.updateRelayFee(nodes, feeBase, feeProportional)) } } } From fa926d18b1d9301647679b04f851b94f10e44ff1 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Mon, 9 Aug 2021 17:46:16 +0200 Subject: [PATCH 2/8] Use RelayFees instead of tuple --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 4 ++-- .../main/scala/fr/acinq/eclair/channel/Channel.scala | 10 +++++----- .../scala/fr/acinq/eclair/db/DualDatabases.scala | 9 +++++---- .../main/scala/fr/acinq/eclair/db/RelayFeesDb.scala | 6 +++--- .../scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala | 11 ++++++----- .../acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala | 11 ++++++----- .../fr/acinq/eclair/payment/relay/NodeRelay.scala | 4 ++-- .../fr/acinq/eclair/payment/relay/Relayer.scala | 2 +- .../test/scala/fr/acinq/eclair/EclairImplSpec.scala | 7 ++++--- .../test/scala/fr/acinq/eclair/TestConstants.scala | 12 ++++++------ .../states/c/WaitForFundingLockedStateSpec.scala | 9 +++++---- .../eclair/channel/states/e/NormalStateSpec.scala | 2 +- .../eclair/channel/states/f/ShutdownStateSpec.scala | 2 +- .../scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala | 7 ++++--- .../eclair/integration/PaymentIntegrationSpec.scala | 2 +- .../fr/acinq/eclair/router/AnnouncementsSpec.scala | 2 +- 16 files changed, 53 insertions(+), 47 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 840163cb68..b8f378cc16 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -37,7 +37,7 @@ import fr.acinq.eclair.io.Peer.{GetPeerInfo, PeerInfo} import fr.acinq.eclair.io.{NodeURI, Peer, PeerConnection} import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment -import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, UsableBalance} +import fr.acinq.eclair.payment.relay.Relayer.{GetOutgoingChannels, OutgoingChannels, RelayFees, UsableBalance} import fr.acinq.eclair.payment.send.MultiPartPaymentLifecycle.PreimageReceived import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPayment, SendPaymentToRoute, SendPaymentToRouteResponse, SendSpontaneousPayment} import fr.acinq.eclair.router.Router._ @@ -203,7 +203,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { override def updateRelayFee(nodes: List[PublicKey], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] = { for (node_id <- nodes) { - appKit.nodeParams.db.relayFees.addOrUpdateFees(node_id, feeBaseMsat, feeProportionalMillionths) + appKit.nodeParams.db.relayFees.addOrUpdateFees(node_id, RelayFees(feeBaseMsat, feeProportionalMillionths)) } allChannels() .map(channels => channels.filter(c => nodes.contains(c.a) || nodes.contains(c.b)).map(c => Right(c.shortChannelId))) 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 e697efa6e9..8fe737b206 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 @@ -286,7 +286,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down val defaultFees = nodeParams.relayParams.defaultFees(data.commitments.announceChannel) - val (feeBase, feeProportionalMillionth) = nodeParams.db.relayFees.getFees(remoteNodeId).getOrElse((defaultFees.feeBase, defaultFees.feeProportionalMillionth.toLong)) + val fees = nodeParams.db.relayFees.getFees(remoteNodeId).getOrElse(defaultFees) val candidateChannelUpdate = Announcements.makeChannelUpdate( nodeParams.chainHash, nodeParams.privateKey, @@ -294,8 +294,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId normal.channelUpdate.shortChannelId, nodeParams.expiryDelta, normal.commitments.remoteParams.htlcMinimum, - feeBase, - feeProportionalMillionth, + fees.feeBase, + fees.feeProportionalMillionths, normal.commitments.capacity.toMilliSatoshi, enable = Announcements.isEnabled(normal.channelUpdate.channelFlags)) val channelUpdate1 = if (Announcements.areSame(candidateChannelUpdate, normal.channelUpdate)) { @@ -664,8 +664,8 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val defaultFees = nodeParams.relayParams.defaultFees(commitments.announceChannel) - val (feeBase, feeProportionalMillionths) = nodeParams.db.relayFees.getFees(remoteNodeId).getOrElse((defaultFees.feeBase, defaultFees.feeProportionalMillionth.toLong)) - val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, feeBase, feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + val fees = nodeParams.db.relayFees.getFees(remoteNodeId).getOrElse(defaultFees) + val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None) storing() 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 cea9c9db89..09225141ac 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 @@ -10,6 +10,7 @@ import fr.acinq.eclair.db.pg._ import fr.acinq.eclair.db.sqlite._ import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Router import fr.acinq.eclair.wire.protocol.{ChannelAnnouncement, ChannelUpdate, NodeAddress, NodeAnnouncement} import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, ShortChannelId} @@ -383,12 +384,12 @@ case class DualRelayFeesDb(sqlite: SqliteRelayFeesDb, postgres: PgRelayFeesDb) e private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-relay-fees").build())) - override def addOrUpdateFees(nodeId: Crypto.PublicKey, feeBase: MilliSatoshi, feeProportionalMillionths: Long): Unit = { - runAsync(postgres.addOrUpdateFees(nodeId, feeBase, feeProportionalMillionths)) - sqlite.addOrUpdateFees(nodeId, feeBase, feeProportionalMillionths) + override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = { + runAsync(postgres.addOrUpdateFees(nodeId, fees)) + sqlite.addOrUpdateFees(nodeId, fees) } - override def getFees(nodeId: Crypto.PublicKey): Option[(MilliSatoshi, Long)] = { + override def getFees(nodeId: Crypto.PublicKey): Option[RelayFees] = { runAsync(postgres.getFees(nodeId)) sqlite.getFees(nodeId) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala index 0fa0acb0c1..023add9f89 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala @@ -17,14 +17,14 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.MilliSatoshi +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import java.io.Closeable trait RelayFeesDb extends Closeable { - def addOrUpdateFees(nodeId: PublicKey, feeBase: MilliSatoshi, feeProportionalMillionths: Long): Unit + def addOrUpdateFees(nodeId: PublicKey, fees: RelayFees): Unit - def getFees(nodeId: PublicKey): Option[(MilliSatoshi, Long)] + def getFees(nodeId: PublicKey): Option[RelayFees] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala index a41102b193..85ecb63836 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala @@ -23,6 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.RelayFeesDb import fr.acinq.eclair.db.pg.PgUtils.PgLock +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import grizzled.slf4j.Logging import java.sql.Timestamp @@ -53,27 +54,27 @@ class PgRelayFeesDb(implicit ds: DataSource, lock: PgLock) extends RelayFeesDb w } } - override def addOrUpdateFees(nodeId: Crypto.PublicKey, feeBase: MilliSatoshi, feeProportionalMillionths: Long): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Postgres) { + override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement( "INSERT INTO local.relay_fees VALUES (?, ?, ?, ?)")) { statement => statement.setString(1, nodeId.value.toHex) - statement.setLong(2, feeBase.toLong) - statement.setLong(3, feeProportionalMillionths) + statement.setLong(2, fees.feeBase.toLong) + statement.setLong(3, fees.feeProportionalMillionths) statement.setTimestamp(4, Timestamp.from(Instant.now())) statement.executeUpdate() } } } - override def getFees(nodeId: PublicKey): Option[(MilliSatoshi, Long)] = withMetrics("relay_fees/get", DbBackends.Postgres) { + override def getFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("relay_fees/get", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM local.relay_fees WHERE node_id=? ORDER BY timestamp DESC LIMIT 1")) { statement => statement.setString(1, nodeId.value.toHex) statement.executeQuery() .headOption .map(rs => - (MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) + RelayFees(MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala index 2842df871e..6e5e53265e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala @@ -23,6 +23,7 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.RelayFeesDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, setVersion, using} +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import java.sql.Connection @@ -45,23 +46,23 @@ class SqliteRelayFeesDb(sqlite: Connection) extends RelayFeesDb { setVersion(statement, DB_NAME, CURRENT_VERSION) } - override def addOrUpdateFees(nodeId: Crypto.PublicKey, feeBase: MilliSatoshi, feeProportionalMillionths: Long): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Sqlite) { + override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Sqlite) { using(sqlite.prepareStatement("INSERT INTO relay_fees VALUES (?, ?, ?, ?)")) { statement => statement.setBytes(1, nodeId.value.toArray) - statement.setLong(2, feeBase.toLong) - statement.setLong(3, feeProportionalMillionths) + statement.setLong(2, fees.feeBase.toLong) + statement.setLong(3, fees.feeProportionalMillionths) statement.setLong(4, System.currentTimeMillis) statement.executeUpdate() } } - override def getFees(nodeId: PublicKey): Option[(MilliSatoshi, Long)] = withMetrics("relay_fees/get", DbBackends.Sqlite) { + override def getFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("relay_fees/get", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM relay_fees WHERE node_id=? ORDER BY timestamp DESC LIMIT 1")) { statement => statement.setBytes(1, nodeId.value.toArray) statement.executeQuery() .headOption .map(rs => - (MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) + RelayFees(MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) ) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala index 8b1f3a0897..0d297bd1db 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala @@ -102,7 +102,7 @@ object NodeRelay { } def validateRelay(nodeParams: NodeParams, upstream: Upstream.Trampoline, payloadOut: Onion.NodeRelayPayload): Option[FailureMessage] = { - val fee = nodeFee(nodeParams.relayParams.minTrampolineFees.feeBase, nodeParams.relayParams.minTrampolineFees.feeProportionalMillionth, payloadOut.amountToForward) + val fee = nodeFee(nodeParams.relayParams.minTrampolineFees.feeBase, nodeParams.relayParams.minTrampolineFees.feeProportionalMillionths, payloadOut.amountToForward) if (upstream.amountIn - payloadOut.amountToForward < fee) { Some(TrampolineFeeInsufficient) } else if (upstream.expiryIn - payloadOut.outgoingCltv < nodeParams.expiryDelta) { @@ -136,7 +136,7 @@ object NodeRelay { */ def translateError(nodeParams: NodeParams, failures: Seq[PaymentFailure], upstream: Upstream.Trampoline, nextPayload: Onion.NodeRelayPayload): Option[FailureMessage] = { val routeNotFound = failures.collectFirst { case f@LocalFailure(_, RouteNotFound) => f }.nonEmpty - val routingFeeHigh = upstream.amountIn - nextPayload.amountToForward >= nodeFee(nodeParams.relayParams.minTrampolineFees.feeBase, nodeParams.relayParams.minTrampolineFees.feeProportionalMillionth, nextPayload.amountToForward) * 5 + val routingFeeHigh = upstream.amountIn - nextPayload.amountToForward >= nodeFee(nodeParams.relayParams.minTrampolineFees.feeBase, nodeParams.relayParams.minTrampolineFees.feeProportionalMillionths, nextPayload.amountToForward) * 5 failures match { case Nil => None case LocalFailure(_, BalanceTooLow) :: Nil if routingFeeHigh => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala index 4e19a844bd..2227ea075d 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/Relayer.scala @@ -112,7 +112,7 @@ object Relayer extends Logging { Props(new Relayer(nodeParams, router, register, paymentHandler, initialized)) // @formatter:off - case class RelayFees(feeBase: MilliSatoshi, feeProportionalMillionth: Int) + case class RelayFees(feeBase: MilliSatoshi, feeProportionalMillionths: Long) case class RelayParams(publicChannelFees: RelayFees, privateChannelFees: RelayFees, 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 f57263cd18..8aa61d5890 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -32,6 +32,7 @@ import fr.acinq.eclair.payment.PaymentRequest import fr.acinq.eclair.payment.PaymentRequest.ExtraHop import fr.acinq.eclair.payment.receive.MultiPartHandler.ReceivePayment import fr.acinq.eclair.payment.receive.PaymentHandler +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.payment.send.PaymentInitiator.{SendPayment, SendPaymentToRoute, SendSpontaneousPayment} import fr.acinq.eclair.router.RouteCalculationSpec.makeUpdateShort import fr.acinq.eclair.router.Router.{GetNetworkStats, GetNetworkStatsResponse, PredefinedNodeRoute, PublicChannel} @@ -500,8 +501,8 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.updateRelayFee(List(a, b), 999 msat, 1234) - relayFeesDb.addOrUpdateFees(a, 999 msat, 1234).wasCalled(once) - relayFeesDb.addOrUpdateFees(b, 999 msat, 1234).wasCalled(once) + relayFeesDb.addOrUpdateFees(a, RelayFees(999 msat, 1234)).wasCalled(once) + relayFeesDb.addOrUpdateFees(b, RelayFees(999 msat, 1234)).wasCalled(once) } test("opening a channel with non default relay fees updates relay fees for the node") { f => @@ -519,7 +520,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.open(a, 10000000L sat, None, None, Some(888 msat, 2345), None, None) - relayFeesDb.addOrUpdateFees(a, 888 msat, 2345).wasCalled(once) + relayFeesDb.addOrUpdateFees(a, RelayFees(888 msat, 2345)).wasCalled(once) } } 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 8ea160cf93..8f4ab2f6cf 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -121,13 +121,13 @@ object TestConstants { relayParams = RelayParams( publicChannelFees = RelayFees( feeBase = 546000 msat, - feeProportionalMillionth = 10), + feeProportionalMillionths = 10), privateChannelFees = RelayFees( feeBase = 547000 msat, - feeProportionalMillionth = 20), + feeProportionalMillionths = 20), minTrampolineFees = RelayFees( feeBase = 548000 msat, - feeProportionalMillionth = 30)), + feeProportionalMillionths = 30)), reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, db = TestDatabases.inMemoryDb(), @@ -236,13 +236,13 @@ object TestConstants { relayParams = RelayParams( publicChannelFees = RelayFees( feeBase = 546000 msat, - feeProportionalMillionth = 10), + feeProportionalMillionths = 10), privateChannelFees = RelayFees( feeBase = 547000 msat, - feeProportionalMillionth = 20), + feeProportionalMillionths = 20), minTrampolineFees = RelayFees( feeBase = 548000 msat, - feeProportionalMillionth = 30)), + feeProportionalMillionths = 30)), reserveToFundingRatio = 0.01, // note: not used (overridden below) maxReserveToFundingRatio = 0.05, db = TestDatabases.inMemoryDb(), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 602359c7af..f1c94e6571 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -22,6 +22,7 @@ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.channel._ import fr.acinq.eclair.channel.publish.TxPublisher import fr.acinq.eclair.channel.states.StateTestsBase +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol._ import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong, TestConstants, TestKitBaseClass} import org.scalatest.Outcome @@ -35,7 +36,7 @@ import scala.concurrent.duration._ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with StateTestsBase { - val relayFees: (MilliSatoshi, Long) = (999 msat, 1234) + val relayFees: RelayFees = RelayFees(999 msat, 1234) case class FixtureParam(alice: TestFSMRef[State, Data, Channel], bob: TestFSMRef[State, Data, Channel], alice2bob: TestProbe, bob2alice: TestProbe, alice2blockchain: TestProbe, router: TestProbe) @@ -47,7 +48,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice.underlyingActor.nodeParams.db.relayFees.addOrUpdateFees(bobParams.nodeId, relayFees._1, relayFees._2) + alice.underlyingActor.nodeParams.db.relayFees.addOrUpdateFees(bobParams.nodeId, relayFees) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) @@ -85,8 +86,8 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS bob2alice.forward(alice) awaitCond(alice.stateName == NORMAL) val initialChannelUpdate = alice.stateData.asInstanceOf[DATA_NORMAL].channelUpdate - assert(initialChannelUpdate.feeBaseMsat === relayFees._1) - assert(initialChannelUpdate.feeProportionalMillionths === relayFees._2) + assert(initialChannelUpdate.feeBaseMsat === relayFees.feeBase) + assert(initialChannelUpdate.feeProportionalMillionths === relayFees.feeProportionalMillionths) bob2alice.expectNoMessage(200 millis) } 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 28aede062c..f5ef01c986 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 @@ -1781,7 +1781,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with import f._ val sender = TestProbe() val newFeeBaseMsat = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeBase * 2 - val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionth * 2 + val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths * 2 sender.send(alice, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, newFeeBaseMsat, newFeeProportionalMillionth)) sender.expectMsgType[RES_SUCCESS[CMD_UPDATE_RELAY_FEE]] 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 20eb484760..ed417d8bfc 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 @@ -597,7 +597,7 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit import f._ val sender = TestProbe() val newFeeBaseMsat = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeBase * 2 - val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionth * 2 + val newFeeProportionalMillionth = TestConstants.Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths * 2 alice ! CMD_UPDATE_RELAY_FEE(sender.ref, newFeeBaseMsat, newFeeProportionalMillionth) sender.expectMsgType[RES_FAILURE[CMD_UPDATE_RELAY_FEE, _]] relayerA.expectNoMessage(1 seconds) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala index 046626c726..8372d6f3ea 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala @@ -20,6 +20,7 @@ import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} import fr.acinq.eclair.db.pg.PgRelayFeesDb import fr.acinq.eclair.db.sqlite.SqliteRelayFeesDb import fr.acinq.eclair._ +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import org.scalatest.funsuite.AnyFunSuite class RelayFeesDbSpec extends AnyFunSuite { @@ -46,14 +47,14 @@ class RelayFeesDbSpec extends AnyFunSuite { assert(db.getFees(a) === None) assert(db.getFees(b) === None) - db.addOrUpdateFees(a, 1 msat, 123) + db.addOrUpdateFees(a, RelayFees(1 msat, 123)) assert(db.getFees(a) === Some(1 msat, 123)) assert(db.getFees(b) === None) Thread.sleep(2) - db.addOrUpdateFees(a, 2 msat, 456) + db.addOrUpdateFees(a, RelayFees(2 msat, 456)) assert(db.getFees(a) === Some(2 msat, 456)) assert(db.getFees(b) === None) - db.addOrUpdateFees(b, 3 msat, 789) + db.addOrUpdateFees(b, RelayFees(3 msat, 789)) assert(db.getFees(a) === Some(2 msat, 456)) assert(db.getFees(b) === Some(3 msat, 789)) } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala index f15ae1c02a..35244d533f 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/integration/PaymentIntegrationSpec.scala @@ -177,7 +177,7 @@ class PaymentIntegrationSpec extends IntegrationSpec { nodes("B").register ! Register.ForwardShortId(sender.ref, shortIdBC, CMD_GETINFO(ActorRef.noSender)) val commitmentBC = sender.expectMsgType[RES_GETINFO].data.asInstanceOf[DATA_NORMAL].commitments // we then forge a new channel_update for B-C... - val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.expiryDelta + 1, nodes("C").nodeParams.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionth, 500000000 msat) + val channelUpdateBC = Announcements.makeChannelUpdate(Block.RegtestGenesisBlock.hash, nodes("B").nodeParams.privateKey, nodes("C").nodeParams.nodeId, shortIdBC, nodes("B").nodeParams.expiryDelta + 1, nodes("C").nodeParams.htlcMinimum, nodes("B").nodeParams.relayParams.publicChannelFees.feeBase, nodes("B").nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) // ...and notify B's relayer nodes("B").system.eventStream.publish(LocalChannelUpdate(system.deadLetters, commitmentBC.channelId, shortIdBC, commitmentBC.remoteParams.nodeId, None, channelUpdateBC, commitmentBC)) // we retrieve a payment hash from D 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 b6721e0e42..d3fb97ecd7 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 @@ -80,7 +80,7 @@ class AnnouncementsSpec extends AnyFunSuite { } test("create valid signed channel update announcement") { - val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey().publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDelta, Alice.nodeParams.htlcMinimum, Alice.nodeParams.relayParams.publicChannelFees.feeBase, Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionth, 500000000 msat) + val ann = makeChannelUpdate(Block.RegtestGenesisBlock.hash, Alice.nodeParams.privateKey, randomKey().publicKey, ShortChannelId(45561L), Alice.nodeParams.expiryDelta, Alice.nodeParams.htlcMinimum, Alice.nodeParams.relayParams.publicChannelFees.feeBase, Alice.nodeParams.relayParams.publicChannelFees.feeProportionalMillionths, 500000000 msat) assert(checkSig(ann, Alice.nodeParams.nodeId)) assert(checkSig(ann, randomKey().publicKey) === false) } From be0ef4dc75a3d23ae5e5fa9fb0e81f902f9facb4 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 10 Aug 2021 11:24:51 +0200 Subject: [PATCH 3/8] Put RelayFees in PeersDb --- .../main/scala/fr/acinq/eclair/Eclair.scala | 2 +- .../fr/acinq/eclair/channel/Channel.scala | 4 +- .../scala/fr/acinq/eclair/db/Databases.scala | 5 -- .../fr/acinq/eclair/db/DualDatabases.scala | 33 +++----- .../scala/fr/acinq/eclair/db/PeersDb.scala | 5 ++ .../fr/acinq/eclair/db/RelayFeesDb.scala | 30 ------- .../fr/acinq/eclair/db/pg/PgPeersDb.scala | 46 +++++++++- .../fr/acinq/eclair/db/pg/PgRelayFeesDb.scala | 84 ------------------- .../eclair/db/sqlite/SqlitePeersDb.scala | 46 +++++++++- .../eclair/db/sqlite/SqliteRelayFeesDb.scala | 71 ---------------- .../fr/acinq/eclair/EclairImplSpec.scala | 14 ++-- .../scala/fr/acinq/eclair/TestDatabases.scala | 1 - .../c/WaitForFundingLockedStateSpec.scala | 2 +- .../fr/acinq/eclair/db/PeersDbSpec.scala | 25 +++++- .../fr/acinq/eclair/db/RelayFeesDbSpec.scala | 63 -------------- 15 files changed, 137 insertions(+), 294 deletions(-) delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala 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 b8f378cc16..a5e4fc37e3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -203,7 +203,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { override def updateRelayFee(nodes: List[PublicKey], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] = { for (node_id <- nodes) { - appKit.nodeParams.db.relayFees.addOrUpdateFees(node_id, RelayFees(feeBaseMsat, feeProportionalMillionths)) + appKit.nodeParams.db.peers.addOrUpdateFees(node_id, RelayFees(feeBaseMsat, feeProportionalMillionths)) } allChannels() .map(channels => channels.filter(c => nodes.contains(c.a) || nodes.contains(c.b)).map(c => Right(c.shortChannelId))) 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 8fe737b206..4f7f68fcc6 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 @@ -286,7 +286,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down val defaultFees = nodeParams.relayParams.defaultFees(data.commitments.announceChannel) - val fees = nodeParams.db.relayFees.getFees(remoteNodeId).getOrElse(defaultFees) + val fees = nodeParams.db.peers.getFees(remoteNodeId).getOrElse(defaultFees) val candidateChannelUpdate = Announcements.makeChannelUpdate( nodeParams.chainHash, nodeParams.privateKey, @@ -664,7 +664,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val defaultFees = nodeParams.relayParams.defaultFees(commitments.announceChannel) - val fees = nodeParams.db.relayFees.getFees(remoteNodeId).getOrElse(defaultFees) + val fees = nodeParams.db.peers.getFees(remoteNodeId).getOrElse(defaultFees) val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala index 7b45b0ac7c..600357180a 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/Databases.scala @@ -39,7 +39,6 @@ trait Databases { def peers: PeersDb def payments: PaymentsDb def pendingCommands: PendingCommandsDb - def relayFees: RelayFeesDb //@formatter:on } @@ -61,7 +60,6 @@ object Databases extends Logging { peers: SqlitePeersDb, payments: SqlitePaymentsDb, pendingCommands: SqlitePendingCommandsDb, - relayFees: SqliteRelayFeesDb, private val backupConnection: Connection) extends Databases with FileBackup { override def backup(backupFile: File): Unit = SqliteUtils.using(backupConnection.createStatement()) { statement => { @@ -78,7 +76,6 @@ object Databases extends Logging { peers = new SqlitePeersDb(eclairJdbc), payments = new SqlitePaymentsDb(eclairJdbc), pendingCommands = new SqlitePendingCommandsDb(eclairJdbc), - relayFees = new SqliteRelayFeesDb(eclairJdbc), backupConnection = eclairJdbc ) } @@ -89,7 +86,6 @@ object Databases extends Logging { peers: PgPeersDb, payments: PgPaymentsDb, pendingCommands: PgPendingCommandsDb, - relayFees: PgRelayFeesDb, dataSource: HikariDataSource, lock: PgLock) extends Databases with ExclusiveLock { override def obtainExclusiveLock(): Unit = lock.obtainExclusiveLock(dataSource) @@ -125,7 +121,6 @@ object Databases extends Logging { peers = new PgPeersDb, payments = new PgPaymentsDb, pendingCommands = new PgPendingCommandsDb, - relayFees = new PgRelayFeesDb, dataSource = ds, lock = lock) 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 09225141ac..e662c8765c 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 @@ -8,7 +8,6 @@ import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.DualDatabases.runAsync import fr.acinq.eclair.db.pg._ import fr.acinq.eclair.db.sqlite._ -import fr.acinq.eclair.io.Peer import fr.acinq.eclair.payment._ import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Router @@ -42,8 +41,6 @@ case class DualDatabases(sqlite: SqliteDatabases, postgres: PostgresDatabases) e override val pendingCommands: PendingCommandsDb = DualPendingCommandsDb(sqlite.pendingCommands, postgres.pendingCommands) - override val relayFees: RelayFeesDb = DualRelayFeesDb(sqlite.relayFees, postgres.relayFees) - override def backup(backupFile: File): Unit = sqlite.backup(backupFile) } @@ -259,6 +256,16 @@ case class DualPeersDb(sqlite: SqlitePeersDb, postgres: PgPeersDb) extends Peers sqlite.listPeers() } + override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = { + runAsync(postgres.addOrUpdateFees(nodeId, fees)) + sqlite.addOrUpdateFees(nodeId, fees) + } + + override def getFees(nodeId: Crypto.PublicKey): Option[RelayFees] = { + runAsync(postgres.getFees(nodeId)) + sqlite.getFees(nodeId) + } + override def close(): Unit = { runAsync(postgres.close()) sqlite.close() @@ -379,23 +386,3 @@ case class DualPendingCommandsDb(sqlite: SqlitePendingCommandsDb, postgres: PgPe sqlite.close() } } - -case class DualRelayFeesDb(sqlite: SqliteRelayFeesDb, postgres: PgRelayFeesDb) extends RelayFeesDb { - - private implicit val ec: ExecutionContext = ExecutionContext.fromExecutor(Executors.newSingleThreadExecutor(new ThreadFactoryBuilder().setNameFormat("db-relay-fees").build())) - - override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = { - runAsync(postgres.addOrUpdateFees(nodeId, fees)) - sqlite.addOrUpdateFees(nodeId, fees) - } - - override def getFees(nodeId: Crypto.PublicKey): Option[RelayFees] = { - runAsync(postgres.getFees(nodeId)) - sqlite.getFees(nodeId) - } - - override def close(): Unit = { - runAsync(postgres.close()) - sqlite.close() - } -} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala index 4d71e0b3d6..a754becc71 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala @@ -17,6 +17,7 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol.NodeAddress import java.io.Closeable @@ -31,4 +32,8 @@ trait PeersDb extends Closeable { def listPeers(): Map[PublicKey, NodeAddress] + def addOrUpdateFees(nodeId: PublicKey, fees: RelayFees): Unit + + def getFees(nodeId: PublicKey): Option[RelayFees] + } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala deleted file mode 100644 index 023add9f89..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/RelayFeesDb.scala +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright 2021 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.db - -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.payment.relay.Relayer.RelayFees - -import java.io.Closeable - -trait RelayFeesDb extends Closeable { - - def addOrUpdateFees(nodeId: PublicKey, fees: RelayFees): Unit - - def getFees(nodeId: PublicKey): Option[RelayFees] - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala index 035c443758..2026be9163 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala @@ -18,15 +18,18 @@ package fr.acinq.eclair.db.pg import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PeersDb import fr.acinq.eclair.db.pg.PgUtils.PgLock +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol._ import grizzled.slf4j.Logging import scodec.bits.BitVector -import java.sql.Statement +import java.sql.{Statement, Timestamp} +import java.time.Instant import javax.sql.DataSource class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logging { @@ -36,7 +39,7 @@ class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logg import lock._ val DB_NAME = "peers" - val CURRENT_VERSION = 2 + val CURRENT_VERSION = 3 inTransaction { pg => @@ -45,14 +48,23 @@ class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logg statement.executeUpdate("ALTER TABLE peers SET SCHEMA local") } + def migration23(statement: Statement): Unit = { + statement.executeUpdate("CREATE TABLE local.relay_fees (node_id TEXT NOT NULL PRIMARY KEY, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL)") + } + using(pg.createStatement()) { statement => getVersion(statement, DB_NAME) match { case None => statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local") statement.executeUpdate("CREATE TABLE local.peers (node_id TEXT NOT NULL PRIMARY KEY, data BYTEA NOT NULL)") + statement.executeUpdate("CREATE TABLE local.relay_fees (node_id TEXT NOT NULL PRIMARY KEY, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL)") case Some(v@1) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") migration12(statement) + migration23(statement) + case Some(v@2) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration23(statement) case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -111,5 +123,35 @@ class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logg } } + override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Postgres) { + withLock { pg => + using(pg.prepareStatement( + """ + | INSERT INTO local.relay_fees (node_id, fee_base_msat, fee_proportional_millionths) + | VALUES (?, ?, ?) + | ON CONFLICT (node_id) + | DO UPDATE SET fee_base_msat = EXCLUDED.fee_base_msat, fee_proportional_millionths = EXCLUDED.fee_proportional_millionths + | """.stripMargin)) { statement => + statement.setString(1, nodeId.value.toHex) + statement.setLong(2, fees.feeBase.toLong) + statement.setLong(3, fees.feeProportionalMillionths) + statement.executeUpdate() + } + } + } + + override def getFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("relay_fees/get", DbBackends.Postgres) { + withLock { pg => + using(pg.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM local.relay_fees WHERE node_id=?")) { statement => + statement.setString(1, nodeId.value.toHex) + statement.executeQuery() + .headOption + .map(rs => + RelayFees(MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) + ) + } + } + } + override def close(): Unit = () } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala deleted file mode 100644 index 85ecb63836..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgRelayFeesDb.scala +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright 2021 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.db.pg - -import fr.acinq.bitcoin.Crypto -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.MilliSatoshi -import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics -import fr.acinq.eclair.db.Monitoring.Tags.DbBackends -import fr.acinq.eclair.db.RelayFeesDb -import fr.acinq.eclair.db.pg.PgUtils.PgLock -import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import grizzled.slf4j.Logging - -import java.sql.Timestamp -import java.time.Instant -import javax.sql.DataSource - -class PgRelayFeesDb(implicit ds: DataSource, lock: PgLock) extends RelayFeesDb with Logging { - - import PgUtils.ExtendedResultSet._ - import PgUtils._ - import lock._ - - val DB_NAME = "relay_fees" - val CURRENT_VERSION = 1 - - inTransaction { pg => - using(pg.createStatement()) { statement => - getVersion(statement, DB_NAME) match { - case None => - statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local") - statement.executeUpdate("CREATE TABLE local.relay_fees (node_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") - statement.executeUpdate("CREATE INDEX relay_fees_node_id_idx ON local.relay_fees(node_id)") - statement.executeUpdate("CREATE INDEX relay_fees_timestamp_idx ON local.relay_fees(timestamp)") - case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do - case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") - } - setVersion(statement, DB_NAME, CURRENT_VERSION) - } - } - - override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Postgres) { - withLock { pg => - using(pg.prepareStatement( - "INSERT INTO local.relay_fees VALUES (?, ?, ?, ?)")) { statement => - statement.setString(1, nodeId.value.toHex) - statement.setLong(2, fees.feeBase.toLong) - statement.setLong(3, fees.feeProportionalMillionths) - statement.setTimestamp(4, Timestamp.from(Instant.now())) - statement.executeUpdate() - } - } - } - - override def getFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("relay_fees/get", DbBackends.Postgres) { - withLock { pg => - using(pg.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM local.relay_fees WHERE node_id=? ORDER BY timestamp DESC LIMIT 1")) { statement => - statement.setString(1, nodeId.value.toHex) - statement.executeQuery() - .headOption - .map(rs => - RelayFees(MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) - ) - } - } - } - - override def close(): Unit = () -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index 9125b1daee..0377765c69 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -18,26 +18,38 @@ package fr.acinq.eclair.db.sqlite import fr.acinq.bitcoin.Crypto import fr.acinq.bitcoin.Crypto.PublicKey +import fr.acinq.eclair.MilliSatoshi import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db.PeersDb import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, setVersion, using} +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.wire.protocol._ +import grizzled.slf4j.Logging import scodec.bits.BitVector -import java.sql.Connection +import java.sql.{Connection, Statement} -class SqlitePeersDb(sqlite: Connection) extends PeersDb { +class SqlitePeersDb(sqlite: Connection) extends PeersDb with Logging { import SqliteUtils.ExtendedResultSet._ val DB_NAME = "peers" - val CURRENT_VERSION = 1 + val CURRENT_VERSION = 2 using(sqlite.createStatement(), inTransaction = true) { statement => + + def migration12(statement: Statement): Unit = { + statement.executeUpdate("CREATE TABLE relay_fees (node_id BLOB NOT NULL PRIMARY KEY, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL)") + } + getVersion(statement, DB_NAME) match { case None => statement.executeUpdate("CREATE TABLE peers (node_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") + statement.executeUpdate("CREATE TABLE relay_fees (node_id BLOB NOT NULL PRIMARY KEY, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL)") + case Some(v@1) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration12(statement) case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -87,6 +99,34 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb { } } + override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Sqlite) { + using(sqlite.prepareStatement("UPDATE relay_fees SET fee_base_msat=?, fee_proportional_millionths=? WHERE node_id=?")) { update => + update.setLong(1, fees.feeBase.toLong) + update.setLong(2, fees.feeProportionalMillionths) + update.setBytes(3, nodeId.value.toArray) + if (update.executeUpdate() == 0) { + using(sqlite.prepareStatement("INSERT INTO relay_fees VALUES (?, ?, ?)")) { statement => + statement.setBytes(1, nodeId.value.toArray) + statement.setLong(2, fees.feeBase.toLong) + statement.setLong(3, fees.feeProportionalMillionths) + statement.executeUpdate() + } + } + } + } + + override def getFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("relay_fees/get", DbBackends.Sqlite) { + using(sqlite.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM relay_fees WHERE node_id=?")) { statement => + statement.setBytes(1, nodeId.value.toArray) + statement.executeQuery() + .headOption + .map(rs => + RelayFees(MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) + ) + } + } + + // used by mobile apps override def close(): Unit = sqlite.close() } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala deleted file mode 100644 index 6e5e53265e..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteRelayFeesDb.scala +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2021 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.db.sqlite - -import fr.acinq.bitcoin.Crypto -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.eclair.MilliSatoshi -import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics -import fr.acinq.eclair.db.Monitoring.Tags.DbBackends -import fr.acinq.eclair.db.RelayFeesDb -import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, setVersion, using} -import fr.acinq.eclair.payment.relay.Relayer.RelayFees - -import java.sql.Connection - -class SqliteRelayFeesDb(sqlite: Connection) extends RelayFeesDb { - - import SqliteUtils.ExtendedResultSet._ - - val DB_NAME = "relay_fees" - val CURRENT_VERSION = 1 - - using(sqlite.createStatement(), inTransaction = true) { statement => - getVersion(statement, DB_NAME) match { - case None => - statement.executeUpdate("CREATE TABLE relay_fees (node_id BLOB NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE INDEX relay_fees_node_id_idx ON relay_fees(node_id)") - statement.executeUpdate("CREATE INDEX relay_fees_timestamp_idx ON relay_fees(timestamp)") - case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do - case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") - } - setVersion(statement, DB_NAME, CURRENT_VERSION) - } - - override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Sqlite) { - using(sqlite.prepareStatement("INSERT INTO relay_fees VALUES (?, ?, ?, ?)")) { statement => - statement.setBytes(1, nodeId.value.toArray) - statement.setLong(2, fees.feeBase.toLong) - statement.setLong(3, fees.feeProportionalMillionths) - statement.setLong(4, System.currentTimeMillis) - statement.executeUpdate() - } - } - - override def getFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("relay_fees/get", DbBackends.Sqlite) { - using(sqlite.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM relay_fees WHERE node_id=? ORDER BY timestamp DESC LIMIT 1")) { statement => - statement.setBytes(1, nodeId.value.toArray) - statement.executeQuery() - .headOption - .map(rs => - RelayFees(MilliSatoshi(rs.getLong("fee_base_msat")), rs.getLong("fee_proportional_millionths")) - ) - } - } - - override def close(): Unit = sqlite.close() -} 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 8aa61d5890..2b019ff3c6 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -488,10 +488,10 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I test("update relay fees in database") { f => import f._ - val relayFeesDb = mock[RelayFeesDb] + val peersDb = mock[PeersDb] val databases = mock[Databases] - databases.relayFees returns relayFeesDb + databases.peers returns peersDb val kitWithMockDb = kit.copy(nodeParams = kit.nodeParams.copy(db = databases)) val eclair = new EclairImpl(kitWithMockDb) @@ -501,17 +501,17 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.updateRelayFee(List(a, b), 999 msat, 1234) - relayFeesDb.addOrUpdateFees(a, RelayFees(999 msat, 1234)).wasCalled(once) - relayFeesDb.addOrUpdateFees(b, RelayFees(999 msat, 1234)).wasCalled(once) + peersDb.addOrUpdateFees(a, RelayFees(999 msat, 1234)).wasCalled(once) + peersDb.addOrUpdateFees(b, RelayFees(999 msat, 1234)).wasCalled(once) } test("opening a channel with non default relay fees updates relay fees for the node") { f => import f._ - val relayFeesDb = mock[RelayFeesDb] + val peersDb = mock[PeersDb] val databases = mock[Databases] - databases.relayFees returns relayFeesDb + databases.peers returns peersDb val kitWithMockDb = kit.copy(nodeParams = kit.nodeParams.copy(db = databases)) val eclair = new EclairImpl(kitWithMockDb) @@ -520,7 +520,7 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.open(a, 10000000L sat, None, None, Some(888 msat, 2345), None, None) - relayFeesDb.addOrUpdateFees(a, RelayFees(888 msat, 2345)).wasCalled(once) + peersDb.addOrUpdateFees(a, RelayFees(888 msat, 2345)).wasCalled(once) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala index a8edd94855..17c047460d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestDatabases.scala @@ -30,7 +30,6 @@ sealed trait TestDatabases extends Databases { override def peers: PeersDb = db.peers override def payments: PaymentsDb = db.payments override def pendingCommands: PendingCommandsDb = db.pendingCommands - override def relayFees: RelayFeesDb = db.relayFees def close(): Unit // @formatter:on } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index f1c94e6571..50e4af2823 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -48,7 +48,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice.underlyingActor.nodeParams.db.relayFees.addOrUpdateFees(bobParams.nodeId, relayFees) + alice.underlyingActor.nodeParams.db.peers.addOrUpdateFees(bobParams.nodeId, relayFees) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala index afa16c3c79..3f373c10d4 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala @@ -20,7 +20,8 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} import fr.acinq.eclair.db.pg.PgPeersDb import fr.acinq.eclair.db.sqlite.SqlitePeersDb -import fr.acinq.eclair.randomKey +import fr.acinq.eclair.payment.relay.Relayer.RelayFees +import fr.acinq.eclair._ import fr.acinq.eclair.wire.protocol.{NodeAddress, Tor2, Tor3} import org.scalatest.funsuite.AnyFunSuite @@ -85,4 +86,26 @@ class PeersDbSpec extends AnyFunSuite { } } + test("add and update relay fees") { + forAllDbs { dbs => + val db = dbs.peers + + val a = randomKey().publicKey + val b = randomKey().publicKey + + assert(db.getFees(a) === None) + assert(db.getFees(b) === None) + db.addOrUpdateFees(a, RelayFees(1 msat, 123)) + assert(db.getFees(a) === Some(RelayFees(1 msat, 123))) + assert(db.getFees(b) === None) + Thread.sleep(2) + db.addOrUpdateFees(a, RelayFees(2 msat, 456)) + assert(db.getFees(a) === Some(RelayFees(2 msat, 456))) + assert(db.getFees(b) === None) + db.addOrUpdateFees(b, RelayFees(3 msat, 789)) + assert(db.getFees(a) === Some(RelayFees(2 msat, 456))) + assert(db.getFees(b) === Some(RelayFees(3 msat, 789))) + } + } + } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala deleted file mode 100644 index 8372d6f3ea..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/RelayFeesDbSpec.scala +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright 2021 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.db - -import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases} -import fr.acinq.eclair.db.pg.PgRelayFeesDb -import fr.acinq.eclair.db.sqlite.SqliteRelayFeesDb -import fr.acinq.eclair._ -import fr.acinq.eclair.payment.relay.Relayer.RelayFees -import org.scalatest.funsuite.AnyFunSuite - -class RelayFeesDbSpec extends AnyFunSuite { - - import fr.acinq.eclair.TestDatabases.forAllDbs - - test("init database two times in a row") { - forAllDbs { - case sqlite: TestSqliteDatabases => - new SqliteRelayFeesDb(sqlite.connection) - new SqliteRelayFeesDb(sqlite.connection) - case pg: TestPgDatabases => - new PgRelayFeesDb()(pg.datasource, pg.lock) - new PgRelayFeesDb()(pg.datasource, pg.lock) - } - } - - test("add and update relay fees") { - forAllDbs { dbs => - val db = dbs.relayFees - - val a = randomKey().publicKey - val b = randomKey().publicKey - - assert(db.getFees(a) === None) - assert(db.getFees(b) === None) - db.addOrUpdateFees(a, RelayFees(1 msat, 123)) - assert(db.getFees(a) === Some(1 msat, 123)) - assert(db.getFees(b) === None) - Thread.sleep(2) - db.addOrUpdateFees(a, RelayFees(2 msat, 456)) - assert(db.getFees(a) === Some(2 msat, 456)) - assert(db.getFees(b) === None) - db.addOrUpdateFees(b, RelayFees(3 msat, 789)) - assert(db.getFees(a) === Some(2 msat, 456)) - assert(db.getFees(b) === Some(3 msat, 789)) - } - } - -} From 42d7dc5a9d7e8d8f1efeaaed04448c1260db0b7c Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 10 Aug 2021 15:50:00 +0200 Subject: [PATCH 4/8] Use all local channels --- .../src/main/scala/fr/acinq/eclair/Eclair.scala | 8 ++++---- .../main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala | 12 ++++++------ .../main/scala/fr/acinq/eclair/router/Router.scala | 4 ++-- .../test/scala/fr/acinq/eclair/db/PeersDbSpec.scala | 1 - 4 files changed, 12 insertions(+), 13 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 a5e4fc37e3..20365cf3ec 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -202,11 +202,11 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { } override def updateRelayFee(nodes: List[PublicKey], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] = { - for (node_id <- nodes) { - appKit.nodeParams.db.peers.addOrUpdateFees(node_id, RelayFees(feeBaseMsat, feeProportionalMillionths)) + for (nodeId <- nodes) { + appKit.nodeParams.db.peers.addOrUpdateFees(nodeId, RelayFees(feeBaseMsat, feeProportionalMillionths)) } - allChannels() - .map(channels => channels.filter(c => nodes.contains(c.a) || nodes.contains(c.b)).map(c => Right(c.shortChannelId))) + (appKit.router ? Router.GetLocalChannels).mapTo[Iterable[LocalChannel]] + .map(channels => channels.filter(c => nodes.contains(c.remoteNodeId)).map(c => Right(c.shortChannelId))) .flatMap(channels => sendToChannels[CommandResponse[CMD_UPDATE_RELAY_FEE]](channels.toList, CMD_UPDATE_RELAY_FEE(ActorRef.noSender, feeBaseMsat, feeProportionalMillionths))) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala index 2026be9163..643c952d57 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala @@ -126,12 +126,12 @@ class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logg override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement( - """ - | INSERT INTO local.relay_fees (node_id, fee_base_msat, fee_proportional_millionths) - | VALUES (?, ?, ?) - | ON CONFLICT (node_id) - | DO UPDATE SET fee_base_msat = EXCLUDED.fee_base_msat, fee_proportional_millionths = EXCLUDED.fee_proportional_millionths - | """.stripMargin)) { statement => + """ + INSERT INTO local.relay_fees (node_id, fee_base_msat, fee_proportional_millionths) + VALUES (?, ?, ?) + ON CONFLICT (node_id) + DO UPDATE SET fee_base_msat = EXCLUDED.fee_base_msat, fee_proportional_millionths = EXCLUDED.fee_proportional_millionths + """)) { statement => statement.setString(1, nodeId.value.toHex) statement.setLong(2, fees.feeBase.toLong) statement.setLong(3, fees.feeProportionalMillionths) 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 262b233e50..89c8fbaec6 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 @@ -195,7 +195,7 @@ class Router(val nodeParams: NodeParams, watcher: typed.ActorRef[ZmqWatcher.Comm case Event(GetLocalChannels, d) => val scids = d.graph.getIncomingEdgesOf(nodeParams.nodeId).map(_.desc.shortChannelId) - val localChannels = scids.flatMap(scid => d.channels.get(scid).orElse(d.privateChannels.get(scid))).map(c => LocalChannel(nodeParams.nodeId, c)) + val localChannels = scids.flatMap(scid => d.channels.get(scid).orElse(d.privateChannels.get(scid)).map(c => LocalChannel(nodeParams.nodeId, scid, c))) sender() ! localChannels stay() @@ -364,7 +364,7 @@ object Router { case Right(rcu) => updateChannelUpdateSameSideAs(rcu.channelUpdate) } } - case class LocalChannel(localNodeId: PublicKey, channel: ChannelDetails) { + case class LocalChannel(localNodeId: PublicKey, shortChannelId: ShortChannelId, channel: ChannelDetails) { val isPrivate: Boolean = channel match { case _: PrivateChannel => true case _ => false diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala index 3f373c10d4..eeeb59b9f2 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala @@ -98,7 +98,6 @@ class PeersDbSpec extends AnyFunSuite { db.addOrUpdateFees(a, RelayFees(1 msat, 123)) assert(db.getFees(a) === Some(RelayFees(1 msat, 123))) assert(db.getFees(b) === None) - Thread.sleep(2) db.addOrUpdateFees(a, RelayFees(2 msat, 456)) assert(db.getFees(a) === Some(RelayFees(2 msat, 456))) assert(db.getFees(b) === None) From 9b93677844720ec3714fe18dcee2847c00abac4c Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Tue, 10 Aug 2021 17:46:14 +0200 Subject: [PATCH 5/8] Fix review comments --- .../main/scala/fr/acinq/eclair/Eclair.scala | 10 ++----- .../fr/acinq/eclair/channel/Channel.scala | 8 ++---- .../fr/acinq/eclair/channel/Helpers.scala | 6 ++++ .../fr/acinq/eclair/db/DualDatabases.scala | 12 ++++---- .../scala/fr/acinq/eclair/db/PeersDb.scala | 4 +-- .../fr/acinq/eclair/db/pg/PgPeersDb.scala | 4 +-- .../eclair/db/sqlite/SqlitePeersDb.scala | 4 +-- .../fr/acinq/eclair/EclairImplSpec.scala | 26 +++-------------- .../c/WaitForFundingLockedStateSpec.scala | 2 +- .../fr/acinq/eclair/db/PeersDbSpec.scala | 22 +++++++-------- .../acinq/eclair/api/handlers/Channel.scala | 22 ++++----------- .../fr/acinq/eclair/api/ApiServiceSpec.scala | 28 ++----------------- 12 files changed, 49 insertions(+), 99 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 20365cf3ec..12fbcaa6a2 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Eclair.scala @@ -90,7 +90,7 @@ trait Eclair { def disconnect(nodeId: PublicKey)(implicit timeout: Timeout): Future[String] - def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], initialRelayFees_opt: Option[(MilliSatoshi, Int)], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] + def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] def close(channels: List[ApiTypes.ChannelIdentifier], scriptPubKey_opt: Option[ByteVector])(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_CLOSE]]]] @@ -177,11 +177,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { (appKit.switchboard ? Peer.Disconnect(nodeId)).mapTo[String] } - override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], relayFees_opt: Option[(MilliSatoshi, Int)], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { - relayFees_opt match { - case Some((feeBase, feeProportionalMillionths)) => updateRelayFee(List(nodeId), feeBase, feeProportionalMillionths) - case None => () - } + override def open(nodeId: PublicKey, fundingAmount: Satoshi, pushAmount_opt: Option[MilliSatoshi], fundingFeeratePerByte_opt: Option[FeeratePerByte], flags_opt: Option[Int], openTimeout_opt: Option[Timeout])(implicit timeout: Timeout): Future[ChannelOpenResponse] = { // we want the open timeout to expire *before* the default ask timeout, otherwise user won't get a generic response val openTimeout = openTimeout_opt.getOrElse(Timeout(10 seconds)) (appKit.switchboard ? Peer.OpenChannel( @@ -203,7 +199,7 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging { override def updateRelayFee(nodes: List[PublicKey], feeBaseMsat: MilliSatoshi, feeProportionalMillionths: Long)(implicit timeout: Timeout): Future[Map[ApiTypes.ChannelIdentifier, Either[Throwable, CommandResponse[CMD_UPDATE_RELAY_FEE]]]] = { for (nodeId <- nodes) { - appKit.nodeParams.db.peers.addOrUpdateFees(nodeId, RelayFees(feeBaseMsat, feeProportionalMillionths)) + appKit.nodeParams.db.peers.addOrUpdateRelayFees(nodeId, RelayFees(feeBaseMsat, feeProportionalMillionths)) } (appKit.router ? Router.GetLocalChannels).mapTo[Iterable[LocalChannel]] .map(channels => channels.filter(c => nodes.contains(c.remoteNodeId)).map(c => Right(c.shortChannelId))) 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 4f7f68fcc6..2c924f242c 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 @@ -29,7 +29,7 @@ import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._ import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient -import fr.acinq.eclair.channel.Helpers.{Closing, Funding} +import fr.acinq.eclair.channel.Helpers.{Closing, Funding, getRelayFees} import fr.acinq.eclair.channel.Monitoring.Metrics.ProcessMessage import fr.acinq.eclair.channel.Monitoring.{Metrics, Tags} import fr.acinq.eclair.channel.publish.TxPublisher @@ -285,8 +285,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId context.system.eventStream.publish(ShortChannelIdAssigned(self, normal.channelId, normal.channelUpdate.shortChannelId, None)) // we rebuild a new channel_update with values from the configuration because they may have changed while eclair was down - val defaultFees = nodeParams.relayParams.defaultFees(data.commitments.announceChannel) - val fees = nodeParams.db.peers.getFees(remoteNodeId).getOrElse(defaultFees) + val fees = getRelayFees(nodeParams, remoteNodeId, data.commitments) val candidateChannelUpdate = Announcements.makeChannelUpdate( nodeParams.chainHash, nodeParams.privateKey, @@ -663,8 +662,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId blockchain ! WatchFundingDeeplyBuried(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF) context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced - val defaultFees = nodeParams.relayParams.defaultFees(commitments.announceChannel) - val fees = nodeParams.db.peers.getFees(remoteNodeId).getOrElse(defaultFees) + val fees = getRelayFees(nodeParams, remoteNodeId, commitments) val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 9d50a17ec9..9ae1bea80e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -27,6 +27,7 @@ import fr.acinq.eclair.channel.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL import fr.acinq.eclair.crypto.Generators import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager import fr.acinq.eclair.db.ChannelsDb +import fr.acinq.eclair.payment.relay.Relayer.RelayFees import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.DirectedHtlc._ import fr.acinq.eclair.transactions.Scripts._ @@ -250,6 +251,11 @@ object Helpers { Await.result(wallet.getReceivePubkey(), 40 seconds) } + def getRelayFees(nodeParams: NodeParams, remoteNodeId: PublicKey, commitments: Commitments): RelayFees = { + val defaultFees = nodeParams.relayParams.defaultFees(commitments.announceChannel) + nodeParams.db.peers.getRelayFees(remoteNodeId).getOrElse(defaultFees) + } + object Funding { def makeFundingInputInfo(fundingTxId: ByteVector32, fundingTxOutputIndex: Int, fundingSatoshis: Satoshi, fundingPubkey1: PublicKey, fundingPubkey2: PublicKey): InputInfo = { 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 e662c8765c..fc28346a3d 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 @@ -256,14 +256,14 @@ case class DualPeersDb(sqlite: SqlitePeersDb, postgres: PgPeersDb) extends Peers sqlite.listPeers() } - override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = { - runAsync(postgres.addOrUpdateFees(nodeId, fees)) - sqlite.addOrUpdateFees(nodeId, fees) + override def addOrUpdateRelayFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = { + runAsync(postgres.addOrUpdateRelayFees(nodeId, fees)) + sqlite.addOrUpdateRelayFees(nodeId, fees) } - override def getFees(nodeId: Crypto.PublicKey): Option[RelayFees] = { - runAsync(postgres.getFees(nodeId)) - sqlite.getFees(nodeId) + override def getRelayFees(nodeId: Crypto.PublicKey): Option[RelayFees] = { + runAsync(postgres.getRelayFees(nodeId)) + sqlite.getRelayFees(nodeId) } override def close(): Unit = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala index a754becc71..ab979881fa 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/PeersDb.scala @@ -32,8 +32,8 @@ trait PeersDb extends Closeable { def listPeers(): Map[PublicKey, NodeAddress] - def addOrUpdateFees(nodeId: PublicKey, fees: RelayFees): Unit + def addOrUpdateRelayFees(nodeId: PublicKey, fees: RelayFees): Unit - def getFees(nodeId: PublicKey): Option[RelayFees] + def getRelayFees(nodeId: PublicKey): Option[RelayFees] } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala index 643c952d57..036565ba56 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgPeersDb.scala @@ -123,7 +123,7 @@ class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logg } } - override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Postgres) { + override def addOrUpdateRelayFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("peers/add-or-update-relay-fees", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement( """ @@ -140,7 +140,7 @@ class PgPeersDb(implicit ds: DataSource, lock: PgLock) extends PeersDb with Logg } } - override def getFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("relay_fees/get", DbBackends.Postgres) { + override def getRelayFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("peers/get-relay-fees", DbBackends.Postgres) { withLock { pg => using(pg.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM local.relay_fees WHERE node_id=?")) { statement => statement.setString(1, nodeId.value.toHex) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala index 0377765c69..bf6e6fbfbf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqlitePeersDb.scala @@ -99,7 +99,7 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb with Logging { } } - override def addOrUpdateFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("relay_fees/add-or-update", DbBackends.Sqlite) { + override def addOrUpdateRelayFees(nodeId: Crypto.PublicKey, fees: RelayFees): Unit = withMetrics("peers/add-or-update-relay-fees", DbBackends.Sqlite) { using(sqlite.prepareStatement("UPDATE relay_fees SET fee_base_msat=?, fee_proportional_millionths=? WHERE node_id=?")) { update => update.setLong(1, fees.feeBase.toLong) update.setLong(2, fees.feeProportionalMillionths) @@ -115,7 +115,7 @@ class SqlitePeersDb(sqlite: Connection) extends PeersDb with Logging { } } - override def getFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("relay_fees/get", DbBackends.Sqlite) { + override def getRelayFees(nodeId: PublicKey): Option[RelayFees] = withMetrics("peers/get-relay-fees", DbBackends.Sqlite) { using(sqlite.prepareStatement("SELECT fee_base_msat, fee_proportional_millionths FROM relay_fees WHERE node_id=?")) { statement => statement.setBytes(1, nodeId.value.toArray) statement.executeQuery() 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 2b019ff3c6..6b1b9aa35a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/EclairImplSpec.scala @@ -91,12 +91,12 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I val nodeId = PublicKey(hex"030bb6a5e0c6b203c7e2180fb78c7ba4bdce46126761d8201b91ddac089cdecc87") // standard conversion - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), relayFees_opt = None, flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(5 sat)), flags_opt = None, openTimeout_opt = None) val open = switchboard.expectMsgType[OpenChannel] assert(open.fundingTxFeeratePerKw_opt === Some(FeeratePerKw(1250 sat))) // check that minimum fee rate of 253 sat/bw is used - eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), relayFees_opt = None, flags_opt = None, openTimeout_opt = None) + eclair.open(nodeId, fundingAmount = 10000000L sat, pushAmount_opt = None, fundingFeeratePerByte_opt = Some(FeeratePerByte(1 sat)), flags_opt = None, openTimeout_opt = None) val open1 = switchboard.expectMsgType[OpenChannel] assert(open1.fundingTxFeeratePerKw_opt === Some(FeeratePerKw.MinimumFeeratePerKw)) } @@ -501,26 +501,8 @@ class EclairImplSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with I eclair.updateRelayFee(List(a, b), 999 msat, 1234) - peersDb.addOrUpdateFees(a, RelayFees(999 msat, 1234)).wasCalled(once) - peersDb.addOrUpdateFees(b, RelayFees(999 msat, 1234)).wasCalled(once) - } - - test("opening a channel with non default relay fees updates relay fees for the node") { f => - import f._ - - val peersDb = mock[PeersDb] - - val databases = mock[Databases] - databases.peers returns peersDb - - val kitWithMockDb = kit.copy(nodeParams = kit.nodeParams.copy(db = databases)) - val eclair = new EclairImpl(kitWithMockDb) - - val a = randomKey().publicKey - - eclair.open(a, 10000000L sat, None, None, Some(888 msat, 2345), None, None) - - peersDb.addOrUpdateFees(a, RelayFees(888 msat, 2345)).wasCalled(once) + peersDb.addOrUpdateRelayFees(a, RelayFees(999 msat, 1234)).wasCalled(once) + peersDb.addOrUpdateRelayFees(b, RelayFees(999 msat, 1234)).wasCalled(once) } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala index 50e4af2823..8e50ed492a 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/channel/states/c/WaitForFundingLockedStateSpec.scala @@ -48,7 +48,7 @@ class WaitForFundingLockedStateSpec extends TestKitBaseClass with FixtureAnyFunS val aliceInit = Init(aliceParams.initFeatures) val bobInit = Init(bobParams.initFeatures) within(30 seconds) { - alice.underlyingActor.nodeParams.db.peers.addOrUpdateFees(bobParams.nodeId, relayFees) + alice.underlyingActor.nodeParams.db.peers.addOrUpdateRelayFees(bobParams.nodeId, relayFees) alice ! INPUT_INIT_FUNDER(ByteVector32.Zeroes, TestConstants.fundingSatoshis, TestConstants.pushMsat, TestConstants.feeratePerKw, TestConstants.feeratePerKw, aliceParams, alice2bob.ref, bobInit, ChannelFlags.Empty, channelConfig, aliceChannelFeatures) alice2blockchain.expectMsgType[TxPublisher.SetChannelId] bob ! INPUT_INIT_FUNDEE(ByteVector32.Zeroes, bobParams, bob2alice.ref, aliceInit, channelConfig, bobChannelFeatures) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala index eeeb59b9f2..b1865e3431 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/PeersDbSpec.scala @@ -93,17 +93,17 @@ class PeersDbSpec extends AnyFunSuite { val a = randomKey().publicKey val b = randomKey().publicKey - assert(db.getFees(a) === None) - assert(db.getFees(b) === None) - db.addOrUpdateFees(a, RelayFees(1 msat, 123)) - assert(db.getFees(a) === Some(RelayFees(1 msat, 123))) - assert(db.getFees(b) === None) - db.addOrUpdateFees(a, RelayFees(2 msat, 456)) - assert(db.getFees(a) === Some(RelayFees(2 msat, 456))) - assert(db.getFees(b) === None) - db.addOrUpdateFees(b, RelayFees(3 msat, 789)) - assert(db.getFees(a) === Some(RelayFees(2 msat, 456))) - assert(db.getFees(b) === Some(RelayFees(3 msat, 789))) + assert(db.getRelayFees(a) === None) + assert(db.getRelayFees(b) === None) + db.addOrUpdateRelayFees(a, RelayFees(1 msat, 123)) + assert(db.getRelayFees(a) === Some(RelayFees(1 msat, 123))) + assert(db.getRelayFees(b) === None) + db.addOrUpdateRelayFees(a, RelayFees(2 msat, 456)) + assert(db.getRelayFees(a) === Some(RelayFees(2 msat, 456))) + assert(db.getRelayFees(b) === None) + db.addOrUpdateRelayFees(b, RelayFees(3 msat, 789)) + assert(db.getRelayFees(a) === Some(RelayFees(2 msat, 456))) + assert(db.getRelayFees(b) === Some(RelayFees(3 msat, 789))) } } diff --git a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala index cacaf4b9f0..222c4c0171 100644 --- a/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala +++ b/eclair-node/src/main/scala/fr/acinq/eclair/api/handlers/Channel.scala @@ -16,7 +16,7 @@ package fr.acinq.eclair.api.handlers -import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route} +import akka.http.scaladsl.server.Route import akka.util.Timeout import fr.acinq.bitcoin.Satoshi import fr.acinq.eclair.MilliSatoshi @@ -32,21 +32,11 @@ trait Channel { import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization} val open: Route = postRequest("open") { implicit t => - formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "fundingFeerateSatByte".as[FeeratePerByte].?, "feeBaseMsat".as[MilliSatoshi].?, - "feeProportionalMillionths".as[Int].?, "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) { - (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, feeBase, feeProportional, channelFlags, openTimeout_opt) => - if (feeBase.nonEmpty && feeProportional.isEmpty || feeBase.isEmpty && feeProportional.nonEmpty) { - reject(MalformedFormFieldRejection("feeBaseMsat/feeProportionalMillionths", - "All relay fees parameters (feeBaseMsat/feeProportionalMillionths) must be specified to override node defaults" - )) - } else { - val initialRelayFees = (feeBase, feeProportional) match { - case (Some(feeBase), Some(feeProportional)) => Some(feeBase, feeProportional) - case _ => None - } - complete { - eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, initialRelayFees, channelFlags, openTimeout_opt) - } + formFields(nodeIdFormParam, "fundingSatoshis".as[Satoshi], "pushMsat".as[MilliSatoshi].?, "fundingFeerateSatByte".as[FeeratePerByte].?, + "channelFlags".as[Int].?, "openTimeoutSeconds".as[Timeout].?) { + (nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt) => + complete { + eclairApi.open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags, openTimeout_opt) } } } diff --git a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala index 106252f3c0..7f52aca9bf 100644 --- a/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala +++ b/eclair-node/src/test/scala/fr/acinq/eclair/api/ApiServiceSpec.scala @@ -250,10 +250,10 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM val channelId = ByteVector32(hex"56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e") val eclair = mock[Eclair] - eclair.open(any, any, any, any, any, any, any)(any[Timeout]) returns Future.successful(ChannelOpened(channelId)) + eclair.open(any, any, any, any, any, any)(any[Timeout]) returns Future.successful(ChannelOpened(channelId)) val mockService = new MockService(eclair) - Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "100000").toEntity) ~> + Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "100002").toEntity) ~> addCredentials(BasicHttpCredentials("", mockApi().password)) ~> addHeader("Content-Type", "application/json") ~> Route.seal(mockService.open) ~> @@ -261,29 +261,7 @@ class ApiServiceSpec extends AnyFunSuite with ScalatestRouteTest with IdiomaticM assert(handled) assert(status == OK) assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") - eclair.open(nodeId, 100000 sat, None, None, None, None, None)(any[Timeout]).wasCalled(once) - } - - Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "50000", "feeBaseMsat" -> "100", "feeProportionalMillionths" -> "10").toEntity) ~> - addCredentials(BasicHttpCredentials("", mockApi().password)) ~> - addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.open) ~> - check { - assert(handled) - assert(status == OK) - assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") - eclair.open(nodeId, 50000 sat, None, None, Some(100 msat, 10), None, None)(any[Timeout]).wasCalled(once) - } - - Post("/open", FormData("nodeId" -> nodeId.toString(), "fundingSatoshis" -> "25000", "feeBaseMsat" -> "250", "feeProportionalMillionths" -> "10").toEntity) ~> - addCredentials(BasicHttpCredentials("", mockApi().password)) ~> - addHeader("Content-Type", "application/json") ~> - Route.seal(mockService.route) ~> - check { - assert(handled) - assert(status == OK) - assert(entityAs[String] == "\"created channel 56d7d6eda04d80138270c49709f1eadb5ab4939e5061309ccdacdb98ce637d0e\"") - eclair.open(nodeId, 25000 sat, None, None, Some(250 msat, 10), None, None)(any[Timeout]).wasCalled(once) + eclair.open(nodeId, 100002 sat, None, None, None, None)(any[Timeout]).wasCalled(once) } } From fb074471baf8923921d9cfc0ca805ccfbb3254b1 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 11 Aug 2021 15:07:19 +0200 Subject: [PATCH 6/8] Add channel updates to auditDb --- .../fr/acinq/eclair/channel/Channel.scala | 4 +++ .../scala/fr/acinq/eclair/db/AuditDb.scala | 3 ++ .../fr/acinq/eclair/db/DualDatabases.scala | 5 +++ .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 33 +++++++++++++++++- .../eclair/db/sqlite/SqliteAuditDb.scala | 25 +++++++++++++- .../fr/acinq/eclair/db/AuditDbSpec.scala | 34 +++++++++---------- 6 files changed, 85 insertions(+), 19 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 2c924f242c..ca1fc581b9 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 @@ -302,6 +302,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId normal.channelUpdate } else { log.info("refreshing channel_update due to configuration changes old={} new={}", normal.channelUpdate, candidateChannelUpdate) + nodeParams.db.audit.addChannelUpdate(data.channelId, candidateChannelUpdate) candidateChannelUpdate } // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network @@ -664,6 +665,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val fees = getRelayFees(nodeParams, remoteNodeId, commitments) val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + nodeParams.db.audit.addChannelUpdate(d.channelId, initialChannelUpdate) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None) storing() @@ -1015,6 +1017,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => log.info("updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) + nodeParams.db.audit.addChannelUpdate(d.channelId, channelUpdate) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) // we use GOTO instead of stay() because we want to fire transitions @@ -1542,6 +1545,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) + nodeParams.db.audit.addChannelUpdate(d.channelId, channelUpdate) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala index 180d9d67d3..ce5c046971 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala @@ -22,6 +22,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} +import fr.acinq.eclair.wire.protocol.ChannelUpdate import java.io.Closeable @@ -39,6 +40,8 @@ trait AuditDb extends Closeable { def add(channelErrorOccurred: ChannelErrorOccurred): Unit + def addChannelUpdate(channelId: ByteVector32, channelUpdate: ChannelUpdate): Unit + def listSent(from: Long, to: Long): Seq[PaymentSent] def listReceived(from: Long, to: Long): Seq[PaymentReceived] 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 fc28346a3d..8657b79e61 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 @@ -161,6 +161,11 @@ case class DualAuditDb(sqlite: SqliteAuditDb, postgres: PgAuditDb) extends Audit sqlite.add(channelErrorOccurred) } + override def addChannelUpdate(channelId: ByteVector32, channelUpdate: ChannelUpdate): Unit = { + runAsync(postgres.addChannelUpdate(channelId, channelUpdate)) + sqlite.addChannelUpdate(channelId, channelUpdate) + } + override def listSent(from: Long, to: Long): Seq[PaymentSent] = { runAsync(postgres.listSent(from, to)) sqlite.listSent(from, to) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index 0a7c29a4ce..e3251b7ddf 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey +import fr.acinq.eclair.wire.protocol.ChannelUpdate import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong} import grizzled.slf4j.Logging @@ -40,7 +41,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { import ExtendedResultSet._ val DB_NAME = "audit" - val CURRENT_VERSION = 7 + val CURRENT_VERSION = 8 case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: Long) @@ -73,6 +74,12 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("ALTER TABLE channel_errors SET SCHEMA audit") } + def migration78(statement: Statement): Unit = { + statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE INDEX channel_updates_id_idx ON audit.channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON audit.channel_updates(timestamp)") + } + getVersion(statement, DB_NAME) match { case None => statement.executeUpdate("CREATE SCHEMA audit") @@ -83,6 +90,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE TABLE audit.relayed_trampoline (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, next_node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.network_fees (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, tx_id TEXT NOT NULL, fee_sat BIGINT NOT NULL, tx_type TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.channel_events (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, capacity_sat BIGINT NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.channel_errors (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal BOOLEAN NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE INDEX sent_timestamp_idx ON audit.sent(timestamp)") @@ -94,18 +102,26 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX network_fees_timestamp_idx ON audit.network_fees(timestamp)") statement.executeUpdate("CREATE INDEX channel_events_timestamp_idx ON audit.channel_events(timestamp)") statement.executeUpdate("CREATE INDEX channel_errors_timestamp_idx ON audit.channel_errors(timestamp)") + statement.executeUpdate("CREATE INDEX channel_updates_id_idx ON audit.channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON audit.channel_updates(timestamp)") case Some(v@4) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") migration45(statement) migration56(statement) migration67(statement) + migration78(statement) case Some(v@5) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") migration56(statement) migration67(statement) + migration78(statement) case Some(v@6) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") migration67(statement) + migration78(statement) + case Some(v@7) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration78(statement) case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -227,6 +243,21 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } } + override def addChannelUpdate(channelId: ByteVector32, u : ChannelUpdate): Unit = withMetrics("audit/add-channel-update", DbBackends.Postgres) { + inTransaction { pg => + using(pg.prepareStatement("INSERT INTO audit.channel_updates VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setString(1, channelId.toHex) + statement.setLong(2, u.feeBaseMsat.toLong) + statement.setLong(3, u.feeProportionalMillionths) + statement.setLong(4, u.cltvExpiryDelta.toInt) + statement.setLong(5, u.htlcMinimumMsat.toLong) + statement.setLong(6, u.htlcMaximumMsat.map(_.toLong).getOrElse(-1)) + statement.setTimestamp(7, Timestamp.from(Instant.now())) + statement.executeUpdate() + } + } + } + override def listSent(from: Long, to: Long): Seq[PaymentSent] = inTransaction { pg => using(pg.prepareStatement("SELECT * FROM audit.sent WHERE timestamp BETWEEN ? AND ?")) { statement => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index 52593cad35..f69d69373e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey +import fr.acinq.eclair.wire.protocol.ChannelUpdate import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong} import grizzled.slf4j.Logging @@ -38,7 +39,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { import ExtendedResultSet._ val DB_NAME = "audit" - val CURRENT_VERSION = 5 + val CURRENT_VERSION = 6 case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: Long) @@ -81,6 +82,12 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX relayed_trampoline_payment_hash_idx ON relayed_trampoline(payment_hash)") } + def migration56(statement: Statement): Unit = { + statement.executeUpdate("CREATE TABLE channel_updates (channel_id BLOB NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, cltv_expiry_delta INTEGER NOT NULL, htlc_minimum_msat INTEGER NOT NULL, htlc_maximum_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE INDEX channel_updates_id_idx ON channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON channel_updates(timestamp)") + } + getVersion(statement, DB_NAME) match { case None => statement.executeUpdate("CREATE TABLE sent (amount_msat INTEGER NOT NULL, fees_msat INTEGER NOT NULL, recipient_amount_msat INTEGER NOT NULL, payment_id TEXT NOT NULL, parent_payment_id TEXT NOT NULL, payment_hash BLOB NOT NULL, payment_preimage BLOB NOT NULL, recipient_node_id BLOB NOT NULL, to_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)") @@ -90,6 +97,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate("CREATE TABLE network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event TEXT NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE channel_errors (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE channel_updates (channel_id BLOB NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, cltv_expiry_delta INTEGER NOT NULL, htlc_minimum_msat INTEGER NOT NULL, htlc_maximum_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE INDEX sent_timestamp_idx ON sent(timestamp)") statement.executeUpdate("CREATE INDEX received_timestamp_idx ON received(timestamp)") @@ -100,6 +108,8 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX network_fees_timestamp_idx ON network_fees(timestamp)") statement.executeUpdate("CREATE INDEX channel_events_timestamp_idx ON channel_events(timestamp)") statement.executeUpdate("CREATE INDEX channel_errors_timestamp_idx ON channel_errors(timestamp)") + statement.executeUpdate("CREATE INDEX channel_updates_id_idx ON channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON channel_updates(timestamp)") case Some(v@1) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") migration12(statement) @@ -227,6 +237,19 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { } } + override def addChannelUpdate(channelId: ByteVector32, u: ChannelUpdate): Unit = withMetrics("audit/add-channel-update", DbBackends.Sqlite) { + using(sqlite.prepareStatement("INSERT INTO channel_updates VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setBytes(1, channelId.toArray) + statement.setLong(2, u.feeBaseMsat.toLong) + statement.setLong(3, u.feeProportionalMillionths) + statement.setLong(4, u.cltvExpiryDelta.toInt) + statement.setLong(5, u.htlcMinimumMsat.toLong) + statement.setLong(6, u.htlcMaximumMsat.map(_.toLong).getOrElse(-1)) + statement.setLong(7, System.currentTimeMillis) + statement.executeUpdate() + } + } + override def listSent(from: Long, to: Long): Seq[PaymentSent] = using(sqlite.prepareStatement("SELECT * FROM sent WHERE timestamp >= ? AND timestamp < ?")) { statement => statement.setLong(1, from) diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index 31fb7bb4ec..71a986baf5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -183,7 +183,7 @@ class AuditDbSpec extends AnyFunSuite { } } - test("migrate sqlite audit database v1 -> v5") { + test("migrate sqlite audit database v1 -> v6") { val dbs = TestSqliteDatabases() @@ -228,7 +228,7 @@ class AuditDbSpec extends AnyFunSuite { } }, dbName = "audit", - targetVersion = 5, + targetVersion = 6, postCheck = connection => { // existing rows in the 'sent' table will use id=00000000-0000-0000-0000-000000000000 as default assert(dbs.audit.listSent(0, (System.currentTimeMillis.milliseconds + 1.minute).toMillis) === Seq(ps.copy(id = ZERO_UUID, parts = Seq(ps.parts.head.copy(id = ZERO_UUID))))) @@ -236,7 +236,7 @@ class AuditDbSpec extends AnyFunSuite { val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => - assert(getVersion(statement, "audit").contains(5)) + assert(getVersion(statement, "audit").contains(6)) } postMigrationDb.add(ps1) @@ -250,7 +250,7 @@ class AuditDbSpec extends AnyFunSuite { ) } - test("migrate sqlite audit database v2 -> v5") { + test("migrate sqlite audit database v2 -> v6") { val dbs = TestSqliteDatabases() val e1 = ChannelErrorOccurred(null, randomBytes32(), randomKey().publicKey, null, LocalError(new RuntimeException("oops")), isFatal = true) @@ -279,24 +279,24 @@ class AuditDbSpec extends AnyFunSuite { } }, dbName = "audit", - targetVersion = 5, + targetVersion = 6, postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => - assert(getVersion(statement, "audit").contains(5)) + assert(getVersion(statement, "audit").contains(6)) } migratedDb.add(e1) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => - assert(getVersion(statement, "audit").contains(5)) + assert(getVersion(statement, "audit").contains(6)) } postMigrationDb.add(e2) } ) } - test("migrate sqlite audit database v3 -> v5") { + test("migrate sqlite audit database v3 -> v6") { val dbs = TestSqliteDatabases() @@ -357,11 +357,11 @@ class AuditDbSpec extends AnyFunSuite { } }, dbName = "audit", - targetVersion = 5, + targetVersion = 6, postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => - assert(getVersion(statement, "audit").contains(5)) + assert(getVersion(statement, "audit").contains(6)) } assert(migratedDb.listSent(50, 150).toSet === Set( ps1.copy(id = pp1.id, recipientAmount = pp1.amount, parts = pp1 :: Nil), @@ -371,7 +371,7 @@ class AuditDbSpec extends AnyFunSuite { val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => - assert(getVersion(statement, "audit").contains(5)) + assert(getVersion(statement, "audit").contains(6)) } val ps2 = PaymentSent(UUID.randomUUID(), randomBytes32(), randomBytes32(), 1100 msat, randomKey().publicKey, Seq( PaymentSent.PartialPayment(UUID.randomUUID(), 500 msat, 10 msat, randomBytes32(), None, 160), @@ -386,7 +386,7 @@ class AuditDbSpec extends AnyFunSuite { ) } - test("migrate audit database v4 -> v5/v7") { + test("migrate audit database v4 -> v6/v8") { val relayed1 = ChannelPaymentRelayed(600 msat, 500 msat, randomBytes32(), randomBytes32(), randomBytes32(), 105) val relayed2 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(300 msat, randomBytes32()), PaymentRelayed.Part(350 msat, randomBytes32())), Seq(PaymentRelayed.Part(600 msat, randomBytes32())), PlaceHolderPubKey, 0 msat, 110) @@ -458,7 +458,7 @@ class AuditDbSpec extends AnyFunSuite { } }, dbName = "audit", - targetVersion = 7, + targetVersion = 8, postCheck = connection => { val migratedDb = dbs.audit @@ -466,7 +466,7 @@ class AuditDbSpec extends AnyFunSuite { val postMigrationDb = new PgAuditDb()(dbs.datasource) using(connection.createStatement()) { statement => - assert(getVersion(statement, "audit").contains(7)) + assert(getVersion(statement, "audit").contains(8)) } val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150) postMigrationDb.add(relayed3) @@ -539,17 +539,17 @@ class AuditDbSpec extends AnyFunSuite { } }, dbName = "audit", - targetVersion = 5, + targetVersion = 6, postCheck = connection => { val migratedDb = dbs.audit using(connection.createStatement()) { statement => - assert(getVersion(statement, "audit").contains(5)) + assert(getVersion(statement, "audit").contains(6)) } assert(migratedDb.listRelayed(100, 120) === Seq(relayed1, relayed2)) val postMigrationDb = new SqliteAuditDb(connection) using(connection.createStatement()) { statement => - assert(getVersion(statement, "audit").contains(5)) + assert(getVersion(statement, "audit").contains(6)) } val relayed3 = TrampolinePaymentRelayed(randomBytes32(), Seq(PaymentRelayed.Part(450 msat, randomBytes32()), PaymentRelayed.Part(500 msat, randomBytes32())), Seq(PaymentRelayed.Part(800 msat, randomBytes32())), randomKey().publicKey, 700 msat, 150) postMigrationDb.add(relayed3) From a592e004ca9a3a34a8c5c4661dab3d481f27f22a Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 11 Aug 2021 17:18:57 +0200 Subject: [PATCH 7/8] Call to auditDb from DbEventHandler and refactor auditDb --- .../fr/acinq/eclair/channel/Channel.scala | 4 -- .../scala/fr/acinq/eclair/db/AuditDb.scala | 3 +- .../fr/acinq/eclair/db/DbEventHandler.scala | 7 ++ .../fr/acinq/eclair/db/DualDatabases.scala | 6 +- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 62 +++++++++--------- .../eclair/db/sqlite/SqliteAuditDb.scala | 65 ++++++++++--------- .../fr/acinq/eclair/db/AuditDbSpec.scala | 13 +++- 7 files changed, 86 insertions(+), 74 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 ca1fc581b9..2c924f242c 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 @@ -302,7 +302,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId normal.channelUpdate } else { log.info("refreshing channel_update due to configuration changes old={} new={}", normal.channelUpdate, candidateChannelUpdate) - nodeParams.db.audit.addChannelUpdate(data.channelId, candidateChannelUpdate) candidateChannelUpdate } // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network @@ -665,7 +664,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val fees = getRelayFees(nodeParams, remoteNodeId, commitments) val initialChannelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, shortChannelId, nodeParams.expiryDelta, d.commitments.remoteParams.htlcMinimum, fees.feeBase, fees.feeProportionalMillionths, commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) - nodeParams.db.audit.addChannelUpdate(d.channelId, initialChannelUpdate) // we need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh)) goto(NORMAL) using DATA_NORMAL(commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)), shortChannelId, buried = false, None, initialChannelUpdate, None, None) storing() @@ -1017,7 +1015,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => log.info("updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = Helpers.aboveReserve(d.commitments)) - nodeParams.db.audit.addChannelUpdate(d.channelId, channelUpdate) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) // we use GOTO instead of stay() because we want to fire transitions @@ -1545,7 +1542,6 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(c: CMD_UPDATE_RELAY_FEE, d: DATA_NORMAL) => log.info(s"updating relay fees: prevFeeBaseMsat={} nextFeeBaseMsat={} prevFeeProportionalMillionths={} nextFeeProportionalMillionths={}", d.channelUpdate.feeBaseMsat, c.feeBase, d.channelUpdate.feeProportionalMillionths, c.feeProportionalMillionths) val channelUpdate = Announcements.makeChannelUpdate(nodeParams.chainHash, nodeParams.privateKey, remoteNodeId, d.shortChannelId, d.channelUpdate.cltvExpiryDelta, d.channelUpdate.htlcMinimumMsat, c.feeBase, c.feeProportionalMillionths, d.commitments.capacity.toMilliSatoshi, enable = false) - nodeParams.db.audit.addChannelUpdate(d.channelId, channelUpdate) val replyTo = if (c.replyTo == ActorRef.noSender) sender() else c.replyTo replyTo ! RES_SUCCESS(c, d.channelId) // we're in OFFLINE state, we don't broadcast the new update right away, we will do that when next time we go to NORMAL state diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala index ce5c046971..35c9192988 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/AuditDb.scala @@ -22,7 +22,6 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.payment.{PaymentReceived, PaymentRelayed, PaymentSent} -import fr.acinq.eclair.wire.protocol.ChannelUpdate import java.io.Closeable @@ -40,7 +39,7 @@ trait AuditDb extends Closeable { def add(channelErrorOccurred: ChannelErrorOccurred): Unit - def addChannelUpdate(channelId: ByteVector32, channelUpdate: ChannelUpdate): Unit + def addChannelUpdate(localChannelUpdate: LocalChannelUpdate): Unit def listSent(from: Long, to: Long): Seq[PaymentSent] diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala index f9349ef833..be1179f812 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala @@ -26,6 +26,7 @@ import fr.acinq.eclair.channel._ import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.payment.Monitoring.{Metrics => PaymentMetrics, Tags => PaymentTags} import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.Announcements /** * This actor sits at the interface between our event stream and the database. @@ -40,6 +41,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with ActorLogging { context.system.eventStream.subscribe(self, classOf[ChannelErrorOccurred]) context.system.eventStream.subscribe(self, classOf[ChannelStateChanged]) context.system.eventStream.subscribe(self, classOf[ChannelClosed]) + context.system.eventStream.subscribe(self, classOf[LocalChannelUpdate]) override def receive: Receive = { @@ -116,6 +118,11 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with ActorLogging { auditDb.add(ChannelEvent(e.channelId, e.commitments.remoteParams.nodeId, e.commitments.commitInput.txOut.amount, e.commitments.localParams.isFunder, !e.commitments.announceChannel, event)) channelsDb.updateChannelMeta(e.channelId, event) + case u: LocalChannelUpdate => + if (Announcements.isEnabled(u.channelUpdate.channelFlags)) { + auditDb.addChannelUpdate(u) + } + } override def unhandled(message: Any): Unit = log.warning(s"unhandled msg=$message") 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 8657b79e61..ea87173ec2 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 @@ -161,9 +161,9 @@ case class DualAuditDb(sqlite: SqliteAuditDb, postgres: PgAuditDb) extends Audit sqlite.add(channelErrorOccurred) } - override def addChannelUpdate(channelId: ByteVector32, channelUpdate: ChannelUpdate): Unit = { - runAsync(postgres.addChannelUpdate(channelId, channelUpdate)) - sqlite.addChannelUpdate(channelId, channelUpdate) + override def addChannelUpdate(localChannelUpdate: LocalChannelUpdate): Unit = { + runAsync(postgres.addChannelUpdate(localChannelUpdate)) + sqlite.addChannelUpdate(localChannelUpdate) } override def listSent(from: Long, to: Long): Seq[PaymentSent] = { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index e3251b7ddf..bd75594ebc 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.db.pg import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong} -import fr.acinq.eclair.channel.{ChannelErrorOccurred, LocalError, NetworkFeePaid, RemoteError} +import fr.acinq.eclair.channel.{ChannelErrorOccurred, LocalChannelUpdate, LocalError, NetworkFeePaid, RemoteError} import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics @@ -26,7 +26,6 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey -import fr.acinq.eclair.wire.protocol.ChannelUpdate import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong} import grizzled.slf4j.Logging @@ -75,8 +74,9 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } def migration78(statement: Statement): Unit = { - statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") - statement.executeUpdate("CREATE INDEX channel_updates_id_idx ON audit.channel_updates(channel_id)") + statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE INDEX channel_updates_cid_idx ON audit.channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_nid_idx ON audit.channel_updates(node_id)") statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON audit.channel_updates(timestamp)") } @@ -90,7 +90,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE TABLE audit.relayed_trampoline (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, next_node_id TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.network_fees (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, tx_id TEXT NOT NULL, fee_sat BIGINT NOT NULL, tx_type TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.channel_events (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, capacity_sat BIGINT NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event TEXT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") - statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") + statement.executeUpdate("CREATE TABLE audit.channel_updates (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, fee_base_msat BIGINT NOT NULL, fee_proportional_millionths BIGINT NOT NULL, cltv_expiry_delta BIGINT NOT NULL, htlc_minimum_msat BIGINT NOT NULL, htlc_maximum_msat BIGINT NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE TABLE audit.channel_errors (channel_id TEXT NOT NULL, node_id TEXT NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal BOOLEAN NOT NULL, timestamp TIMESTAMP WITH TIME ZONE NOT NULL)") statement.executeUpdate("CREATE INDEX sent_timestamp_idx ON audit.sent(timestamp)") @@ -102,26 +102,23 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX network_fees_timestamp_idx ON audit.network_fees(timestamp)") statement.executeUpdate("CREATE INDEX channel_events_timestamp_idx ON audit.channel_events(timestamp)") statement.executeUpdate("CREATE INDEX channel_errors_timestamp_idx ON audit.channel_errors(timestamp)") - statement.executeUpdate("CREATE INDEX channel_updates_id_idx ON audit.channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_cid_idx ON audit.channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_nid_idx ON audit.channel_updates(node_id)") statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON audit.channel_updates(timestamp)") - case Some(v@4) => + case Some(v@(4 | 5 | 6 | 7)) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration45(statement) - migration56(statement) - migration67(statement) - migration78(statement) - case Some(v@5) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration56(statement) - migration67(statement) - migration78(statement) - case Some(v@6) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration67(statement) - migration78(statement) - case Some(v@7) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration78(statement) + if (v < 5) { + migration45(statement) + } + if (v < 6) { + migration56(statement) + } + if (v < 7) { + migration67(statement) + } + if (v < 8) { + migration78(statement) + } case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -243,16 +240,17 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { } } - override def addChannelUpdate(channelId: ByteVector32, u : ChannelUpdate): Unit = withMetrics("audit/add-channel-update", DbBackends.Postgres) { + override def addChannelUpdate(u: LocalChannelUpdate): Unit = withMetrics("audit/add-channel-update", DbBackends.Postgres) { inTransaction { pg => - using(pg.prepareStatement("INSERT INTO audit.channel_updates VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => - statement.setString(1, channelId.toHex) - statement.setLong(2, u.feeBaseMsat.toLong) - statement.setLong(3, u.feeProportionalMillionths) - statement.setLong(4, u.cltvExpiryDelta.toInt) - statement.setLong(5, u.htlcMinimumMsat.toLong) - statement.setLong(6, u.htlcMaximumMsat.map(_.toLong).getOrElse(-1)) - statement.setTimestamp(7, Timestamp.from(Instant.now())) + using(pg.prepareStatement("INSERT INTO audit.channel_updates VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setString(1, u.channelId.toHex) + statement.setString(2, u.remoteNodeId.value.toHex) + statement.setLong(3, u.channelUpdate.feeBaseMsat.toLong) + statement.setLong(4, u.channelUpdate.feeProportionalMillionths) + statement.setLong(5, u.channelUpdate.cltvExpiryDelta.toInt) + statement.setLong(6, u.channelUpdate.htlcMinimumMsat.toLong) + statement.setLong(7, u.channelUpdate.htlcMaximumMsat.map(_.toLong).getOrElse(-1)) + statement.setTimestamp(8, Timestamp.from(Instant.now())) statement.executeUpdate() } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index f69d69373e..ab30ea33da 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -18,7 +18,7 @@ package fr.acinq.eclair.db.sqlite import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{ByteVector32, Satoshi, SatoshiLong} -import fr.acinq.eclair.channel.{ChannelErrorOccurred, LocalError, NetworkFeePaid, RemoteError} +import fr.acinq.eclair.channel.{ChannelErrorOccurred, LocalChannelUpdate, LocalError, NetworkFeePaid, RemoteError} import fr.acinq.eclair.db.AuditDb.{NetworkFee, Stats} import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics @@ -26,7 +26,6 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends import fr.acinq.eclair.db._ import fr.acinq.eclair.payment._ import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey -import fr.acinq.eclair.wire.protocol.ChannelUpdate import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong} import grizzled.slf4j.Logging @@ -83,8 +82,9 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { } def migration56(statement: Statement): Unit = { - statement.executeUpdate("CREATE TABLE channel_updates (channel_id BLOB NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, cltv_expiry_delta INTEGER NOT NULL, htlc_minimum_msat INTEGER NOT NULL, htlc_maximum_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE INDEX channel_updates_id_idx ON channel_updates(channel_id)") + statement.executeUpdate("CREATE TABLE channel_updates (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, cltv_expiry_delta INTEGER NOT NULL, htlc_minimum_msat INTEGER NOT NULL, htlc_maximum_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE INDEX channel_updates_cid_idx ON channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_nid_idx ON channel_updates(node_id)") statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON channel_updates(timestamp)") } @@ -97,7 +97,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate("CREATE TABLE network_fees (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, tx_id BLOB NOT NULL, fee_sat INTEGER NOT NULL, tx_type TEXT NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE channel_events (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, capacity_sat INTEGER NOT NULL, is_funder BOOLEAN NOT NULL, is_private BOOLEAN NOT NULL, event TEXT NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE TABLE channel_errors (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, error_name TEXT NOT NULL, error_message TEXT NOT NULL, is_fatal INTEGER NOT NULL, timestamp INTEGER NOT NULL)") - statement.executeUpdate("CREATE TABLE channel_updates (channel_id BLOB NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, cltv_expiry_delta INTEGER NOT NULL, htlc_minimum_msat INTEGER NOT NULL, htlc_maximum_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") + statement.executeUpdate("CREATE TABLE channel_updates (channel_id BLOB NOT NULL, node_id BLOB NOT NULL, fee_base_msat INTEGER NOT NULL, fee_proportional_millionths INTEGER NOT NULL, cltv_expiry_delta INTEGER NOT NULL, htlc_minimum_msat INTEGER NOT NULL, htlc_maximum_msat INTEGER NOT NULL, timestamp INTEGER NOT NULL)") statement.executeUpdate("CREATE INDEX sent_timestamp_idx ON sent(timestamp)") statement.executeUpdate("CREATE INDEX received_timestamp_idx ON received(timestamp)") @@ -108,26 +108,26 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX network_fees_timestamp_idx ON network_fees(timestamp)") statement.executeUpdate("CREATE INDEX channel_events_timestamp_idx ON channel_events(timestamp)") statement.executeUpdate("CREATE INDEX channel_errors_timestamp_idx ON channel_errors(timestamp)") - statement.executeUpdate("CREATE INDEX channel_updates_id_idx ON channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_cid_idx ON channel_updates(channel_id)") + statement.executeUpdate("CREATE INDEX channel_updates_nid_idx ON channel_updates(node_id)") statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON channel_updates(timestamp)") - case Some(v@1) => + case Some(v@(1 | 2 | 3 | 4 | 5)) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration12(statement) - migration23(statement) - migration34(statement) - migration45(statement) - case Some(v@2) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration23(statement) - migration34(statement) - migration45(statement) - case Some(v@3) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration34(statement) - migration45(statement) - case Some(v@4) => - logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - migration45(statement) + if (v < 2) { + migration12(statement) + } + if (v < 3) { + migration23(statement) + } + if (v < 4) { + migration34(statement) + } + if (v < 5) { + migration45(statement) + } + if (v < 6) { + migration56(statement) + } case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } @@ -237,15 +237,16 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { } } - override def addChannelUpdate(channelId: ByteVector32, u: ChannelUpdate): Unit = withMetrics("audit/add-channel-update", DbBackends.Sqlite) { - using(sqlite.prepareStatement("INSERT INTO channel_updates VALUES (?, ?, ?, ?, ?, ?, ?)")) { statement => - statement.setBytes(1, channelId.toArray) - statement.setLong(2, u.feeBaseMsat.toLong) - statement.setLong(3, u.feeProportionalMillionths) - statement.setLong(4, u.cltvExpiryDelta.toInt) - statement.setLong(5, u.htlcMinimumMsat.toLong) - statement.setLong(6, u.htlcMaximumMsat.map(_.toLong).getOrElse(-1)) - statement.setLong(7, System.currentTimeMillis) + override def addChannelUpdate(u: LocalChannelUpdate): Unit = withMetrics("audit/add-channel-update", DbBackends.Sqlite) { + using(sqlite.prepareStatement("INSERT INTO channel_updates VALUES (?, ?, ?, ?, ?, ?, ?, ?)")) { statement => + statement.setBytes(1, u.channelId.toArray) + statement.setBytes(2, u.remoteNodeId.value.toArray) + statement.setLong(3, u.channelUpdate.feeBaseMsat.toLong) + statement.setLong(4, u.channelUpdate.feeProportionalMillionths) + statement.setLong(5, u.channelUpdate.cltvExpiryDelta.toInt) + statement.setLong(6, u.channelUpdate.htlcMinimumMsat.toLong) + statement.setLong(7, u.channelUpdate.htlcMaximumMsat.map(_.toLong).getOrElse(-1)) + statement.setLong(8, System.currentTimeMillis) statement.executeUpdate() } } diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala index 71a986baf5..cd2d6934c7 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/AuditDbSpec.scala @@ -21,7 +21,7 @@ import fr.acinq.bitcoin.{ByteVector32, SatoshiLong, Transaction} import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck} import fr.acinq.eclair._ import fr.acinq.eclair.channel.Helpers.Closing.MutualClose -import fr.acinq.eclair.channel.{ChannelErrorOccurred, LocalError, NetworkFeePaid, RemoteError} +import fr.acinq.eclair.channel.{ChannelErrorOccurred, LocalChannelUpdate, LocalError, NetworkFeePaid, RemoteError} import fr.acinq.eclair.db.AuditDb.Stats import fr.acinq.eclair.db.DbEventHandler.ChannelEvent import fr.acinq.eclair.db.jdbc.JdbcUtils.using @@ -29,6 +29,7 @@ import fr.acinq.eclair.db.pg.PgAuditDb import fr.acinq.eclair.db.pg.PgUtils.{getVersion, setVersion} import fr.acinq.eclair.db.sqlite.SqliteAuditDb import fr.acinq.eclair.payment._ +import fr.acinq.eclair.router.Announcements import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey import fr.acinq.eclair.wire.protocol.Error import org.scalatest.Tag @@ -603,4 +604,14 @@ class AuditDbSpec extends AnyFunSuite { } } + test("add channel update") { + forAllDbs { dbs => + val channelId = randomBytes32() + val scid = ShortChannelId(123) + val remoteNodeId = randomKey().publicKey + val u = Announcements.makeChannelUpdate(randomBytes32(), randomKey(), remoteNodeId, scid, CltvExpiryDelta(56), 2000 msat, 1000 msat, 999, 1000000000 msat) + dbs.audit.addChannelUpdate(LocalChannelUpdate(null, channelId, scid, remoteNodeId, None, u, null)) + } + } + } From 44da4e2e4619d6f9ff64da762f05fc5f8b9aea44 Mon Sep 17 00:00:00 2001 From: Thomas HUET Date: Wed, 11 Aug 2021 17:48:37 +0200 Subject: [PATCH 8/8] No refactor yet --- .../fr/acinq/eclair/db/pg/PgAuditDb.scala | 30 ++++++++------ .../eclair/db/sqlite/SqliteAuditDb.scala | 40 +++++++++++-------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala index bd75594ebc..ef809d0eed 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala @@ -105,20 +105,24 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX channel_updates_cid_idx ON audit.channel_updates(channel_id)") statement.executeUpdate("CREATE INDEX channel_updates_nid_idx ON audit.channel_updates(node_id)") statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON audit.channel_updates(timestamp)") - case Some(v@(4 | 5 | 6 | 7)) => + case Some(v@4) => logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - if (v < 5) { - migration45(statement) - } - if (v < 6) { - migration56(statement) - } - if (v < 7) { - migration67(statement) - } - if (v < 8) { - migration78(statement) - } + migration45(statement) + migration56(statement) + migration67(statement) + migration78(statement) + case Some(v@5) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration56(statement) + migration67(statement) + migration78(statement) + case Some(v@6) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration67(statement) + migration78(statement) + case Some(v@7) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration78(statement) case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala index ab30ea33da..66481e8502 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala @@ -111,23 +111,31 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging { statement.executeUpdate("CREATE INDEX channel_updates_cid_idx ON channel_updates(channel_id)") statement.executeUpdate("CREATE INDEX channel_updates_nid_idx ON channel_updates(node_id)") statement.executeUpdate("CREATE INDEX channel_updates_timestamp_idx ON channel_updates(timestamp)") - case Some(v@(1 | 2 | 3 | 4 | 5)) => + case Some(v@1)=> logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") - if (v < 2) { - migration12(statement) - } - if (v < 3) { - migration23(statement) - } - if (v < 4) { - migration34(statement) - } - if (v < 5) { - migration45(statement) - } - if (v < 6) { - migration56(statement) - } + migration12(statement) + migration23(statement) + migration34(statement) + migration45(statement) + migration56(statement) + case Some(v@2) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration23(statement) + migration34(statement) + migration45(statement) + migration56(statement) + case Some(v@3) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration34(statement) + migration45(statement) + migration56(statement) + case Some(v@4) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration45(statement) + migration56(statement) + case Some(v@5) => + logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION") + migration56(statement) case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") }