1
0
mirror of https://github.com/ACINQ/eclair.git synced 2025-01-18 13:23:46 +01:00

Add closedchannels RPC (#2642)

This RPC allows to access the historic channel data without
relying on third party services like LN explorers.

Note that when the `remoteNodeId` filter is not provided, this
query may be expensive on nodes with a lot of closed channels.
This commit is contained in:
rorp 2023-06-02 02:38:58 -07:00 committed by GitHub
parent 46d1c73889
commit 37eb1420dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 78 additions and 8 deletions

View File

@ -21,7 +21,7 @@ _eclair-cli()
*)
# works fine, but is too slow at the moment.
# allopts=$($eclaircli help 2>&1 | awk '$1 ~ /^"/ { sub(/,/, ""); print $1}' | sed 's/[":]//g')
allopts="getinfo connect open cpfpbumpfees close forceclose updaterelayfee peers channels channel allnodes allchannels allupdates findroute findroutetonode findroutebetweennodes parseinvoice payinvoice sendtonode getsentinfo createinvoice getinvoice listinvoices listpendinginvoices listreceivedpayments getreceivedinfo audit networkfees channelstats"
allopts="getinfo connect open cpfpbumpfees close forceclose updaterelayfee peers channels channel closedchannels allnodes allchannels allupdates findroute findroutetonode findroutebetweennodes parseinvoice payinvoice sendtonode getsentinfo createinvoice getinvoice listinvoices listpendinginvoices listreceivedpayments getreceivedinfo audit networkfees channelstats"
if ! [[ " $allopts " =~ " $prev " ]]; then # prevent double arguments
if [[ -z "$cur" || "$cur" =~ ^[a-z] ]]; then

View File

@ -48,6 +48,7 @@ Node operators that use Postgres as database backend and make SQL queries on cha
- `channel-opened` websocket event was updated to contain the final `channel_id` and be published when a channel is ready to process payments (#2567)
- `getsentinfo` can now be used with `--offer` to list payments sent to a specific offer.
- `listreceivedpayments` lists payments received by your node (#2607)
- `closedchannels` lists closed channels. It accepts `--count` and `--skip` parameters to limit the number of retrieved items as well (#2642)
- `cpfpbumpfees` can be used to unblock chains of unconfirmed transactions by creating a child transaction that pays a high fee (#1783)
### Miscellaneous improvements and bug fixes

View File

@ -45,6 +45,7 @@ and COMMAND is one of the available commands:
- forceclose
- channel
- channels
- closedchannels
- allchannels
- allupdates
- channelstats

View File

@ -104,6 +104,8 @@ trait Eclair {
def channelInfo(channel: ApiTypes.ChannelIdentifier)(implicit timeout: Timeout): Future[CommandResponse[CMD_GET_CHANNEL_INFO]]
def closedChannels(nodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]]
def peers()(implicit timeout: Timeout): Future[Iterable[PeerInfo]]
def node(nodeId: PublicKey)(implicit timeout: Timeout): Future[Option[Router.PublicNode]]
@ -288,6 +290,14 @@ class EclairImpl(appKit: Kit) extends Eclair with Logging {
sendToChannel[CMD_GET_CHANNEL_INFO, CommandResponse[CMD_GET_CHANNEL_INFO]](channel, CMD_GET_CHANNEL_INFO(ActorRef.noSender))
}
override def closedChannels(nodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated])(implicit timeout: Timeout): Future[Iterable[RES_GET_CHANNEL_INFO]] = {
Future {
appKit.nodeParams.db.channels.listClosedChannels(nodeId_opt, paginated_opt).map { data =>
RES_GET_CHANNEL_INFO(nodeId = data.remoteNodeId, channelId = data.channelId, state = CLOSED, data = data)
}
}
}
override def allChannels()(implicit timeout: Timeout): Future[Iterable[ChannelDesc]] = {
(appKit.router ? Router.GetChannels).mapTo[Iterable[ChannelAnnouncement]].map(_.map(c => ChannelDesc(c.shortChannelId, c.nodeId1, c.nodeId2)))
}

View File

@ -17,7 +17,8 @@
package fr.acinq.eclair.db
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.CltvExpiry
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.{CltvExpiry, Paginated, TimestampSecond}
import fr.acinq.eclair.channel.PersistentChannelData
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent
@ -33,6 +34,8 @@ trait ChannelsDb {
def listLocalChannels(): Seq[PersistentChannelData]
def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData]
def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit
def listHtlcInfos(channelId: ByteVector32, commitmentNumber: Long): Seq[(ByteVector32, CltvExpiry)]

View File

@ -1,6 +1,7 @@
package fr.acinq.eclair.db
import com.google.common.util.concurrent.ThreadFactoryBuilder
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.bitcoin.scalacompat.{ByteVector32, Crypto, Satoshi}
import fr.acinq.eclair.channel._
import fr.acinq.eclair.db.Databases.{FileBackup, PostgresDatabases, SqliteDatabases}
@ -10,7 +11,7 @@ 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, Paginated, RealShortChannelId, ShortChannelId, TimestampMilli}
import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, Paginated, RealShortChannelId, ShortChannelId, TimestampMilli, TimestampSecond}
import grizzled.slf4j.Logging
import java.io.File
@ -229,6 +230,11 @@ case class DualChannelsDb(primary: ChannelsDb, secondary: ChannelsDb) extends Ch
primary.listLocalChannels()
}
override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = {
runAsync(secondary.listClosedChannels(remoteNodeId_opt, paginated_opt))
primary.listClosedChannels(remoteNodeId_opt, paginated_opt)
}
override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = {
runAsync(secondary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry))
primary.addHtlcInfo(channelId, commitmentNumber, paymentHash, cltvExpiry)

View File

@ -18,7 +18,7 @@ package fr.acinq.eclair.db.pg
import com.zaxxer.hikari.util.IsolationLevel
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.eclair.CltvExpiry
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.channel.PersistentChannelData
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent
@ -26,6 +26,7 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
import fr.acinq.eclair.db.pg.PgUtils.PgLock
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec
import fr.acinq.eclair.{CltvExpiry, Paginated}
import grizzled.slf4j.Logging
import scodec.bits.BitVector
@ -246,6 +247,19 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
}
}
override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = withMetrics("channels/list-closed-channels", DbBackends.Postgres) {
val sql = remoteNodeId_opt match {
case None => "SELECT data FROM local.channels WHERE is_closed=TRUE ORDER BY closed_timestamp DESC"
case Some(remoteNodeId) => s"SELECT data FROM local.channels WHERE is_closed=TRUE AND remote_node_id = '${remoteNodeId.toHex}' ORDER BY closed_timestamp DESC"
}
withLock { pg =>
using(pg.prepareStatement(limited(sql, paginated_opt))) { statement =>
statement.executeQuery()
.mapCodec(channelDataCodec).toSeq
}
}
}
override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = withMetrics("channels/add-htlc-info", DbBackends.Postgres) {
withLock { pg =>
using(pg.prepareStatement("INSERT INTO local.htlc_infos VALUES (?, ?, ?, ?)")) { statement =>

View File

@ -17,13 +17,14 @@
package fr.acinq.eclair.db.sqlite
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.channel.PersistentChannelData
import fr.acinq.eclair.db.ChannelsDb
import fr.acinq.eclair.db.DbEventHandler.ChannelEvent
import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec
import fr.acinq.eclair.{CltvExpiry, TimestampMilli}
import fr.acinq.eclair.{CltvExpiry, Paginated, TimestampMilli, TimestampSecond}
import grizzled.slf4j.Logging
import scodec.bits.BitVector
@ -170,6 +171,27 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
}
}
override def listClosedChannels(remoteNodeId_opt: Option[PublicKey], paginated_opt: Option[Paginated]): Seq[PersistentChannelData] = withMetrics("channels/list-closed-channels", DbBackends.Sqlite) {
val sql = "SELECT data FROM local_channels WHERE is_closed=1 ORDER BY closed_timestamp DESC"
remoteNodeId_opt match {
case None =>
using(sqlite.prepareStatement(limited(sql, paginated_opt))) { statement =>
statement.executeQuery().mapCodec(channelDataCodec).toSeq
}
case Some(nodeId) =>
using(sqlite.prepareStatement(sql)) { statement =>
val filtered = statement.executeQuery()
.mapCodec(channelDataCodec).filter(_.remoteNodeId == nodeId)
val limited = paginated_opt match {
case None => filtered
case Some(p) => filtered.slice(p.skip, p.skip + p.count)
}
limited.toSeq
}
}
}
override def addHtlcInfo(channelId: ByteVector32, commitmentNumber: Long, paymentHash: ByteVector32, cltvExpiry: CltvExpiry): Unit = withMetrics("channels/add-htlc-info", DbBackends.Sqlite) {
using(sqlite.prepareStatement("INSERT INTO htlc_infos VALUES (?, ?, ?, ?)")) { statement =>
statement.setBytes(1, channelId.toArray)

View File

@ -18,6 +18,7 @@ package fr.acinq.eclair.db
import com.softwaremill.quicklens._
import fr.acinq.bitcoin.scalacompat.ByteVector32
import fr.acinq.bitcoin.scalacompat.Crypto.PrivateKey
import fr.acinq.bitcoin.scalacompat.Crypto.PublicKey
import fr.acinq.eclair.TestDatabases.{TestPgDatabases, TestSqliteDatabases, migrationCheck}
import fr.acinq.eclair.channel.RealScidStatus
@ -30,7 +31,7 @@ import fr.acinq.eclair.db.sqlite.SqliteChannelsDb
import fr.acinq.eclair.db.sqlite.SqliteUtils.ExtendedResultSet._
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec
import fr.acinq.eclair.wire.internal.channel.ChannelCodecsSpec
import fr.acinq.eclair.{CltvExpiry, RealShortChannelId, TestDatabases, randomBytes32, randomKey}
import fr.acinq.eclair.{CltvExpiry, RealShortChannelId, TestDatabases, TimestampSecond, randomBytes32, randomKey}
import org.scalatest.funsuite.AnyFunSuite
import scodec.bits.ByteVector
@ -90,9 +91,13 @@ class ChannelsDbSpec extends AnyFunSuite {
assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList.toSet == Set((paymentHash1, cltvExpiry1), (paymentHash2, cltvExpiry2)))
assert(db.listHtlcInfos(channel1.channelId, 43).toList == Nil)
assert(db.listClosedChannels(None, None).isEmpty)
db.removeChannel(channel1.channelId)
assert(db.getChannel(channel1.channelId).isEmpty)
assert(db.listLocalChannels() == List(channel2b))
assert(db.listClosedChannels(None, None) == List(channel1))
assert(db.listClosedChannels(Some(channel1.remoteNodeId), None) == List(channel1))
assert(db.listClosedChannels(Some(PrivateKey(randomBytes32()).publicKey), None) == Nil)
assert(db.listHtlcInfos(channel1.channelId, commitNumber).toList == Nil)
db.removeChannel(channel2b.channelId)
assert(db.getChannel(channel2b.channelId).isEmpty)

View File

@ -19,7 +19,7 @@ package fr.acinq.eclair.api.handlers
import akka.http.scaladsl.server.{MalformedFormFieldRejection, Route}
import akka.util.Timeout
import fr.acinq.bitcoin.scalacompat.{Satoshi, Script}
import fr.acinq.eclair.MilliSatoshi
import fr.acinq.eclair.{MilliSatoshi, Paginated}
import fr.acinq.eclair.api.Service
import fr.acinq.eclair.api.directives.EclairDirectives
import fr.acinq.eclair.api.serde.FormParamExtractors._
@ -127,6 +127,14 @@ trait Channel {
}
}
val closedChannels: Route = postRequest("closedchannels") { implicit t =>
withPaginated { paginated_opt =>
formFields(nodeIdFormParam.?) { toRemoteNodeId_opt =>
complete(eclairApi.closedChannels(toRemoteNodeId_opt, paginated_opt.orElse(Some(Paginated(count = 10, skip = 0)))))
}
}
}
val allChannels: Route = postRequest("allchannels") { implicit t =>
complete(eclairApi.allChannels())
}
@ -147,6 +155,6 @@ trait Channel {
complete(eclairApi.channelBalances())
}
val channelRoutes: Route = open ~ rbfOpen ~ spliceIn ~ spliceOut ~ close ~ forceClose ~ channel ~ channels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances
val channelRoutes: Route = open ~ rbfOpen ~ spliceIn ~ spliceOut ~ close ~ forceClose ~ channel ~ channels ~ closedChannels ~ allChannels ~ allUpdates ~ channelStats ~ channelBalances
}