Skip to content

Commit 15ddc17

Browse files
Add trampoline info to auditDB (#1767)
* Add trampoline info to auditDB Add a new table containing the recipient and amount sent to the recipient in case of trampoline relaying. When using trampoline, the recipient may not be the next node on the path.
1 parent d0e79fa commit 15ddc17

File tree

7 files changed

+298
-29
lines changed

7 files changed

+298
-29
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/db/DbEventHandler.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class DbEventHandler(nodeParams: NodeParams) extends Actor with ActorLogging {
6969
.withTag(PaymentTags.Relay, PaymentTags.RelayType(e))
7070
.record((e.amountIn - e.amountOut).truncateToSatoshi.toLong)
7171
e match {
72-
case TrampolinePaymentRelayed(_, incoming, outgoing, _) =>
72+
case TrampolinePaymentRelayed(_, incoming, outgoing, _, _, _) =>
7373
PaymentMetrics.PaymentParts.withTag(PaymentTags.Direction, PaymentTags.Directions.Received).record(incoming.length)
7474
PaymentMetrics.PaymentParts.withTag(PaymentTags.Direction, PaymentTags.Directions.Sent).record(outgoing.length)
7575
incoming.foreach(p => channelsDb.updateChannelMeta(p.channelId, ChannelEvent.EventType.PaymentReceived))

eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scala

+38-3
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,11 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
2525
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
2626
import fr.acinq.eclair.db._
2727
import fr.acinq.eclair.payment._
28+
import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey
2829
import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong}
2930
import grizzled.slf4j.Logging
3031

32+
import java.sql.Statement
3133
import java.util.UUID
3234
import javax.sql.DataSource
3335
import scala.collection.immutable.Queue
@@ -38,18 +40,28 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
3840
import ExtendedResultSet._
3941

4042
val DB_NAME = "audit"
41-
val CURRENT_VERSION = 4
43+
val CURRENT_VERSION = 5
4244

4345
case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: Long)
4446

4547
inTransaction { pg =>
4648
using(pg.createStatement()) { statement =>
49+
def migration45(statement: Statement): Int = {
50+
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed_trampoline (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, next_node_id TEXT NOT NULL, timestamp BIGINT NOT NULL)")
51+
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_trampoline_timestamp_idx ON relayed_trampoline(timestamp)")
52+
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_trampoline_payment_hash_idx ON relayed_trampoline(payment_hash)")
53+
}
4754

4855
getVersion(statement, DB_NAME, CURRENT_VERSION) match {
56+
case 4 =>
57+
logger.warn(s"migrating db $DB_NAME, found version=4 current=$CURRENT_VERSION")
58+
migration45(statement)
59+
setVersion(statement, DB_NAME, CURRENT_VERSION)
4960
case CURRENT_VERSION =>
5061
statement.executeUpdate("CREATE TABLE IF NOT EXISTS sent (amount_msat BIGINT NOT NULL, fees_msat BIGINT NOT NULL, recipient_amount_msat BIGINT NOT NULL, payment_id TEXT NOT NULL, parent_payment_id TEXT NOT NULL, payment_hash TEXT NOT NULL, payment_preimage TEXT NOT NULL, recipient_node_id TEXT NOT NULL, to_channel_id TEXT NOT NULL, timestamp BIGINT NOT NULL)")
5162
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat BIGINT NOT NULL, payment_hash TEXT NOT NULL, from_channel_id TEXT NOT NULL, timestamp BIGINT NOT NULL)")
5263
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, channel_id TEXT NOT NULL, direction TEXT NOT NULL, relay_type TEXT NOT NULL, timestamp BIGINT NOT NULL)")
64+
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed_trampoline (payment_hash TEXT NOT NULL, amount_msat BIGINT NOT NULL, next_node_id TEXT NOT NULL, timestamp BIGINT NOT NULL)")
5365
statement.executeUpdate("CREATE TABLE IF NOT EXISTS 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 BIGINT NOT NULL)")
5466
statement.executeUpdate("CREATE TABLE IF NOT EXISTS 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 BIGINT NOT NULL)")
5567
statement.executeUpdate("CREATE TABLE IF NOT EXISTS 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 BIGINT NOT NULL)")
@@ -58,6 +70,8 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
5870
statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)")
5971
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)")
6072
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_payment_hash_idx ON relayed(payment_hash)")
73+
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_trampoline_timestamp_idx ON relayed_trampoline(timestamp)")
74+
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_trampoline_payment_hash_idx ON relayed_trampoline(payment_hash)")
6175
statement.executeUpdate("CREATE INDEX IF NOT EXISTS network_fees_timestamp_idx ON network_fees(timestamp)")
6276
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_events_timestamp_idx ON channel_events(timestamp)")
6377
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_errors_timestamp_idx ON channel_errors(timestamp)")
@@ -124,7 +138,14 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
124138
case ChannelPaymentRelayed(amountIn, amountOut, _, fromChannelId, toChannelId, ts) =>
125139
// non-trampoline relayed payments have one input and one output
126140
Seq(RelayedPart(fromChannelId, amountIn, "IN", "channel", ts), RelayedPart(toChannelId, amountOut, "OUT", "channel", ts))
127-
case TrampolinePaymentRelayed(_, incoming, outgoing, ts) =>
141+
case TrampolinePaymentRelayed(_, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount, ts) =>
142+
using(pg.prepareStatement("INSERT INTO relayed_trampoline VALUES (?, ?, ?, ?)")) { statement =>
143+
statement.setString(1, e.paymentHash.toHex)
144+
statement.setLong(2, nextTrampolineAmount.toLong)
145+
statement.setString(3, nextTrampolineNodeId.value.toHex)
146+
statement.setLong(4, e.timestamp)
147+
statement.executeUpdate()
148+
}
128149
// trampoline relayed payments do MPP aggregation and may have M inputs and N outputs
129150
incoming.map(i => RelayedPart(i.channelId, i.amount, "IN", "trampoline", ts)) ++ outgoing.map(o => RelayedPart(o.channelId, o.amount, "OUT", "trampoline", ts))
130151
}
@@ -231,6 +252,18 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
231252

232253
override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] =
233254
inTransaction { pg =>
255+
var trampolineByHash = Map.empty[ByteVector32, (MilliSatoshi, PublicKey)]
256+
using(pg.prepareStatement("SELECT * FROM relayed_trampoline WHERE timestamp >= ? AND timestamp < ?")) { statement =>
257+
statement.setLong(1, from)
258+
statement.setLong(2, to)
259+
val rs = statement.executeQuery()
260+
while (rs.next()) {
261+
val paymentHash = rs.getByteVector32FromHex("payment_hash")
262+
val amount = MilliSatoshi(rs.getLong("amount_msat"))
263+
val nodeId = PublicKey(rs.getByteVectorFromHex("next_node_id"))
264+
trampolineByHash += (paymentHash -> (amount, nodeId))
265+
}
266+
}
234267
using(pg.prepareStatement("SELECT * FROM relayed WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement =>
235268
statement.setLong(1, from)
236269
statement.setLong(2, to)
@@ -256,7 +289,9 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
256289
case Some(RelayedPart(_, _, _, "channel", timestamp)) => incoming.zip(outgoing).map {
257290
case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, timestamp)
258291
}
259-
case Some(RelayedPart(_, _, _, "trampoline", timestamp)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, timestamp) :: Nil
292+
case Some(RelayedPart(_, _, _, "trampoline", timestamp)) =>
293+
val (nextTrampolineAmount, nextTrampolineNodeId) = trampolineByHash.getOrElse(paymentHash, (0 msat, PlaceHolderPubKey))
294+
TrampolinePaymentRelayed(paymentHash, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount, timestamp) :: Nil
260295
case _ => Nil
261296
}
262297
}.toSeq.sortBy(_.timestamp)

eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteAuditDb.scala

+45-5
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import fr.acinq.eclair.db.Monitoring.Metrics.withMetrics
2525
import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
2626
import fr.acinq.eclair.db._
2727
import fr.acinq.eclair.payment._
28+
import fr.acinq.eclair.transactions.Transactions.PlaceHolderPubKey
2829
import fr.acinq.eclair.{MilliSatoshi, MilliSatoshiLong}
2930
import grizzled.slf4j.Logging
3031

@@ -38,7 +39,7 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
3839
import ExtendedResultSet._
3940

4041
val DB_NAME = "audit"
41-
val CURRENT_VERSION = 4
42+
val CURRENT_VERSION = 5
4243

4344
case class RelayedPart(channelId: ByteVector32, amount: MilliSatoshi, direction: String, relayType: String, timestamp: Long)
4445

@@ -75,26 +76,40 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
7576
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_payment_hash_idx ON relayed(payment_hash)")
7677
}
7778

79+
def migration45(statement: Statement): Int = {
80+
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed_trampoline (payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, next_node_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
81+
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_trampoline_timestamp_idx ON relayed_trampoline(timestamp)")
82+
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_trampoline_payment_hash_idx ON relayed_trampoline(payment_hash)")
83+
}
84+
7885
getVersion(statement, DB_NAME, CURRENT_VERSION) match {
7986
case 1 => // previous version let's migrate
8087
logger.warn(s"migrating db $DB_NAME, found version=1 current=$CURRENT_VERSION")
8188
migration12(statement)
8289
migration23(statement)
8390
migration34(statement)
91+
migration45(statement)
8492
setVersion(statement, DB_NAME, CURRENT_VERSION)
8593
case 2 =>
8694
logger.warn(s"migrating db $DB_NAME, found version=2 current=$CURRENT_VERSION")
8795
migration23(statement)
8896
migration34(statement)
97+
migration45(statement)
8998
setVersion(statement, DB_NAME, CURRENT_VERSION)
9099
case 3 =>
91100
logger.warn(s"migrating db $DB_NAME, found version=3 current=$CURRENT_VERSION")
92101
migration34(statement)
102+
migration45(statement)
103+
setVersion(statement, DB_NAME, CURRENT_VERSION)
104+
case 4 =>
105+
logger.warn(s"migrating db $DB_NAME, found version=4 current=$CURRENT_VERSION")
106+
migration45(statement)
93107
setVersion(statement, DB_NAME, CURRENT_VERSION)
94108
case CURRENT_VERSION =>
95109
statement.executeUpdate("CREATE TABLE IF NOT EXISTS 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)")
96110
statement.executeUpdate("CREATE TABLE IF NOT EXISTS received (amount_msat INTEGER NOT NULL, payment_hash BLOB NOT NULL, from_channel_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
97111
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed (payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, channel_id BLOB NOT NULL, direction TEXT NOT NULL, relay_type TEXT NOT NULL, timestamp INTEGER NOT NULL)")
112+
statement.executeUpdate("CREATE TABLE IF NOT EXISTS relayed_trampoline (payment_hash BLOB NOT NULL, amount_msat INTEGER NOT NULL, next_node_id BLOB NOT NULL, timestamp INTEGER NOT NULL)")
98113
statement.executeUpdate("CREATE TABLE IF NOT EXISTS 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)")
99114
statement.executeUpdate("CREATE TABLE IF NOT EXISTS 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)")
100115
statement.executeUpdate("CREATE TABLE IF NOT EXISTS 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)")
@@ -103,6 +118,8 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
103118
statement.executeUpdate("CREATE INDEX IF NOT EXISTS received_timestamp_idx ON received(timestamp)")
104119
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_timestamp_idx ON relayed(timestamp)")
105120
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_payment_hash_idx ON relayed(payment_hash)")
121+
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_trampoline_timestamp_idx ON relayed_trampoline(timestamp)")
122+
statement.executeUpdate("CREATE INDEX IF NOT EXISTS relayed_trampoline_payment_hash_idx ON relayed_trampoline(payment_hash)")
106123
statement.executeUpdate("CREATE INDEX IF NOT EXISTS network_fees_timestamp_idx ON network_fees(timestamp)")
107124
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_events_timestamp_idx ON channel_events(timestamp)")
108125
statement.executeUpdate("CREATE INDEX IF NOT EXISTS channel_errors_timestamp_idx ON channel_errors(timestamp)")
@@ -160,9 +177,17 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
160177
case ChannelPaymentRelayed(amountIn, amountOut, _, fromChannelId, toChannelId, ts) =>
161178
// non-trampoline relayed payments have one input and one output
162179
Seq(RelayedPart(fromChannelId, amountIn, "IN", "channel", ts), RelayedPart(toChannelId, amountOut, "OUT", "channel", ts))
163-
case TrampolinePaymentRelayed(_, incoming, outgoing, ts) =>
180+
case TrampolinePaymentRelayed(_, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount, ts) =>
181+
using(sqlite.prepareStatement("INSERT INTO relayed_trampoline VALUES (?, ?, ?, ?)")) { statement =>
182+
statement.setBytes(1, e.paymentHash.toArray)
183+
statement.setLong(2, nextTrampolineAmount.toLong)
184+
statement.setBytes(3, nextTrampolineNodeId.value.toArray)
185+
statement.setLong(4, e.timestamp)
186+
statement.executeUpdate()
187+
}
164188
// trampoline relayed payments do MPP aggregation and may have M inputs and N outputs
165-
incoming.map(i => RelayedPart(i.channelId, i.amount, "IN", "trampoline", ts)) ++ outgoing.map(o => RelayedPart(o.channelId, o.amount, "OUT", "trampoline", ts))
189+
incoming.map(i => RelayedPart(i.channelId, i.amount, "IN", "trampoline", ts)) ++
190+
outgoing.map(o => RelayedPart(o.channelId, o.amount, "OUT", "trampoline", ts))
166191
}
167192
for (p <- payments) {
168193
using(sqlite.prepareStatement("INSERT INTO relayed VALUES (?, ?, ?, ?, ?, ?)")) { statement =>
@@ -256,7 +281,19 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
256281
receivedByHash.values.toSeq.sortBy(_.timestamp)
257282
}
258283

259-
override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] =
284+
override def listRelayed(from: Long, to: Long): Seq[PaymentRelayed] = {
285+
var trampolineByHash = Map.empty[ByteVector32, (MilliSatoshi, PublicKey)]
286+
using(sqlite.prepareStatement("SELECT * FROM relayed_trampoline WHERE timestamp >= ? AND timestamp < ?")) { statement =>
287+
statement.setLong(1, from)
288+
statement.setLong(2, to)
289+
val rs = statement.executeQuery()
290+
while (rs.next()) {
291+
val paymentHash = rs.getByteVector32("payment_hash")
292+
val amount = MilliSatoshi(rs.getLong("amount_msat"))
293+
val nodeId = PublicKey(rs.getByteVector("next_node_id"))
294+
trampolineByHash += (paymentHash -> (amount, nodeId))
295+
}
296+
}
260297
using(sqlite.prepareStatement("SELECT * FROM relayed WHERE timestamp >= ? AND timestamp < ?")) { statement =>
261298
statement.setLong(1, from)
262299
statement.setLong(2, to)
@@ -282,11 +319,14 @@ class SqliteAuditDb(sqlite: Connection) extends AuditDb with Logging {
282319
case Some(RelayedPart(_, _, _, "channel", timestamp)) => incoming.zip(outgoing).map {
283320
case (in, out) => ChannelPaymentRelayed(in.amount, out.amount, paymentHash, in.channelId, out.channelId, timestamp)
284321
}
285-
case Some(RelayedPart(_, _, _, "trampoline", timestamp)) => TrampolinePaymentRelayed(paymentHash, incoming, outgoing, timestamp) :: Nil
322+
case Some(RelayedPart(_, _, _, "trampoline", timestamp)) =>
323+
val (nextTrampolineAmount, nextTrampolineNodeId) = trampolineByHash.getOrElse(paymentHash, (0 msat, PlaceHolderPubKey))
324+
TrampolinePaymentRelayed(paymentHash, incoming, outgoing, nextTrampolineNodeId, nextTrampolineAmount, timestamp) :: Nil
286325
case _ => Nil
287326
}
288327
}.toSeq.sortBy(_.timestamp)
289328
}
329+
}
290330

291331
override def listNetworkFees(from: Long, to: Long): Seq[NetworkFee] =
292332
using(sqlite.prepareStatement("SELECT * FROM network_fees WHERE timestamp >= ? AND timestamp < ? ORDER BY timestamp")) { statement =>

eclair-core/src/main/scala/fr/acinq/eclair/payment/PaymentEvents.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ sealed trait PaymentRelayed extends PaymentEvent {
8585

8686
case class ChannelPaymentRelayed(amountIn: MilliSatoshi, amountOut: MilliSatoshi, paymentHash: ByteVector32, fromChannelId: ByteVector32, toChannelId: ByteVector32, timestamp: Long = System.currentTimeMillis) extends PaymentRelayed
8787

88-
case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, timestamp: Long = System.currentTimeMillis) extends PaymentRelayed {
88+
case class TrampolinePaymentRelayed(paymentHash: ByteVector32, incoming: PaymentRelayed.Incoming, outgoing: PaymentRelayed.Outgoing, nextTrampolineNodeId: PublicKey, nextTrampolineAmount: MilliSatoshi, timestamp: Long = System.currentTimeMillis) extends PaymentRelayed {
8989
override val amountIn: MilliSatoshi = incoming.map(_.amount).sum
9090
override val amountOut: MilliSatoshi = outgoing.map(_.amount).sum
9191
}

eclair-core/src/main/scala/fr/acinq/eclair/payment/relay/NodeRelay.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ class NodeRelay private(nodeParams: NodeParams,
349349
}
350350
val incoming = upstream.adds.map(add => PaymentRelayed.Part(add.amountMsat, add.channelId))
351351
val outgoing = paymentSent.parts.map(part => PaymentRelayed.Part(part.amountWithFees, part.toChannelId))
352-
context.system.eventStream ! EventStream.Publish(TrampolinePaymentRelayed(paymentHash, incoming, outgoing))
352+
context.system.eventStream ! EventStream.Publish(TrampolinePaymentRelayed(paymentHash, incoming, outgoing, paymentSent.recipientNodeId, paymentSent.recipientAmount))
353353
}
354354

355355
}

0 commit comments

Comments
 (0)