What changed, and why it matters
This commit fixes a flaky automated test and, as a side effect, changes several PostgreSQL database queries to exclude the upper time bound instead of including it. The change is described by the author as a correctness fix for pagination, not a security fix. There is no direct evidence of an exploitable vulnerability.
No immediate security action required. Review whether any downstream callers of the changed PgAuditDb methods depend on the previously inclusive upper bound, as the semantic change could affect pagination or reporting results at interval boundaries.
Security signals we found
Change in database query boundary semantics (inclusive to half-open interval)
Author-described race condition in test, not production code
No input sanitization, authentication, or authorization changes
No vendor disclosure of security relevance
Evidence from the diff
The commit replaces SQL BETWEEN ? AND ? with >= ? AND < ? across PgAuditDb list methods. BETWEEN is inclusive on both bounds; the new form excludes the upper bound. The commit message states this is needed to allow pagination and to fix a race condition in a test where a record’s timestamp could equal now. The test change subtracts 1 ms from a timestamp to avoid the race. No SQL injection, privilege escalation, or cryptographic weakness is introduced or fixed.
Changed components
eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgAuditDb.scalaeclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scalaInspect captured patch +11 / −9
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 83172bd..aefead9 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
@@ -442,7 +442,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
override def listPublished(remoteNodeId: PublicKey, from: TimestampMilli, to: TimestampMilli): Seq[PublishedTransaction] = withMetrics("audit/list-published-by-node-id", DbBackends.Postgres) {
inTransaction { pg =>
- using(pg.prepareStatement("SELECT * FROM audit.transactions_published WHERE node_id = ? AND timestamp BETWEEN ? AND ?")) { statement =>
+ using(pg.prepareStatement("SELECT * FROM audit.transactions_published WHERE node_id = ? AND timestamp >= ? AND timestamp < ?")) { statement =>
statement.setString(1, remoteNodeId.toHex)
statement.setTimestamp(2, from.toSqlTimestamp)
statement.setTimestamp(3, to.toSqlTimestamp)
@@ -481,7 +481,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
override def listConfirmed(remoteNodeId: PublicKey, from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[ConfirmedTransaction] = withMetrics("audit/list-confirmed-by-node-id", DbBackends.Postgres) {
inTransaction { pg =>
- using(pg.prepareStatement("SELECT * FROM audit.transactions_confirmed INNER JOIN audit.transactions_published ON audit.transactions_published.tx_id = audit.transactions_confirmed.tx_id WHERE audit.transactions_confirmed.node_id = ? AND audit.transactions_confirmed.timestamp BETWEEN ? and ? ORDER BY audit.transactions_confirmed.timestamp")) { statement =>
+ using(pg.prepareStatement("SELECT * FROM audit.transactions_confirmed INNER JOIN audit.transactions_published ON audit.transactions_published.tx_id = audit.transactions_confirmed.tx_id WHERE audit.transactions_confirmed.node_id = ? AND audit.transactions_confirmed.timestamp >= ? AND audit.transactions_confirmed.timestamp < ? ORDER BY audit.transactions_confirmed.timestamp")) { statement =>
statement.setString(1, remoteNodeId.toHex)
statement.setTimestamp(2, from.toSqlTimestamp)
statement.setTimestamp(3, to.toSqlTimestamp)
@@ -501,7 +501,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
override def listConfirmed(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated]): Seq[ConfirmedTransaction] = withMetrics("audit/list-confirmed", DbBackends.Postgres) {
inTransaction { pg =>
- using(pg.prepareStatement("SELECT * FROM audit.transactions_confirmed INNER JOIN audit.transactions_published ON audit.transactions_published.tx_id = audit.transactions_confirmed.tx_id WHERE audit.transactions_confirmed.timestamp BETWEEN ? and ? ORDER BY audit.transactions_confirmed.timestamp")) { statement =>
+ using(pg.prepareStatement("SELECT * FROM audit.transactions_confirmed INNER JOIN audit.transactions_published ON audit.transactions_published.tx_id = audit.transactions_confirmed.tx_id WHERE audit.transactions_confirmed.timestamp >= ? AND audit.transactions_confirmed.timestamp < ? ORDER BY audit.transactions_confirmed.timestamp")) { statement =>
statement.setTimestamp(1, from.toSqlTimestamp)
statement.setTimestamp(2, to.toSqlTimestamp)
Paginated.paginate(statement.executeQuery().map { rs =>
@@ -520,7 +520,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
override def listChannelEvents(channelId: ByteVector32, from: TimestampMilli, to: TimestampMilli): Seq[ChannelEvent] = withMetrics("audit/list-channel-events-by-channel-id", DbBackends.Postgres) {
inTransaction { pg =>
- using(pg.prepareStatement("SELECT * FROM audit.channel_events WHERE channel_id = ? AND timestamp BETWEEN ? AND ?")) { statement =>
+ using(pg.prepareStatement("SELECT * FROM audit.channel_events WHERE channel_id = ? AND timestamp >= ? AND timestamp < ?")) { statement =>
statement.setString(1, channelId.toHex)
statement.setTimestamp(2, from.toSqlTimestamp)
statement.setTimestamp(3, to.toSqlTimestamp)
@@ -543,7 +543,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
override def listChannelEvents(remoteNodeId: PublicKey, from: TimestampMilli, to: TimestampMilli): Seq[ChannelEvent] = withMetrics("audit/list-channel-events-by-node-id", DbBackends.Postgres) {
inTransaction { pg =>
- using(pg.prepareStatement("SELECT * FROM audit.channel_events WHERE node_id = ? AND timestamp BETWEEN ? AND ?")) { statement =>
+ using(pg.prepareStatement("SELECT * FROM audit.channel_events WHERE node_id = ? AND timestamp >= ? AND timestamp < ?")) { statement =>
statement.setString(1, remoteNodeId.toHex)
statement.setTimestamp(2, from.toSqlTimestamp)
statement.setTimestamp(3, to.toSqlTimestamp)
@@ -566,7 +566,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
override def listSent(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated] = None): Seq[PaymentSent] =
inTransaction { pg =>
- using(pg.prepareStatement("SELECT * FROM audit.sent WHERE settled_at BETWEEN ? AND ?")) { statement =>
+ using(pg.prepareStatement("SELECT * FROM audit.sent WHERE settled_at >= ? AND settled_at < ?")) { statement =>
statement.setTimestamp(1, from.toSqlTimestamp)
statement.setTimestamp(2, to.toSqlTimestamp)
Paginated.paginate(statement.executeQuery()
@@ -601,7 +601,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
override def listReceived(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated] = None): Seq[PaymentReceived] =
inTransaction { pg =>
- using(pg.prepareStatement("SELECT * FROM audit.received WHERE received_at BETWEEN ? AND ?")) { statement =>
+ using(pg.prepareStatement("SELECT * FROM audit.received WHERE received_at >= ? AND received_at < ?")) { statement =>
statement.setTimestamp(1, from.toSqlTimestamp)
statement.setTimestamp(2, to.toSqlTimestamp)
Paginated.paginate(statement.executeQuery()
@@ -623,7 +623,7 @@ class PgAuditDb(implicit ds: DataSource) extends AuditDb with Logging {
override def listRelayed(from: TimestampMilli, to: TimestampMilli, paginated_opt: Option[Paginated] = None): Seq[PaymentRelayed] =
inTransaction { pg =>
- val relayedByHash = using(pg.prepareStatement("SELECT * FROM audit.relayed WHERE timestamp BETWEEN ? and ?")) { statement =>
+ val relayedByHash = using(pg.prepareStatement("SELECT * FROM audit.relayed WHERE timestamp >= ? AND timestamp < ?")) { statement =>
statement.setTimestamp(1, from.toSqlTimestamp)
statement.setTimestamp(2, to.toSqlTimestamp)
statement.executeQuery().foldLeft(Map.empty[ByteVector32, Seq[RelayedPart]]) { (relayedByHash, rs) =>
diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala
index 4d0afda..c2b7a95 100644
--- a/eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala
+++ b/eclair-core/src/test/scala/fr/acinq/eclair/profit/PeerStatsTrackerSpec.scala
@@ -479,7 +479,9 @@ class PeerStatsTrackerSpec extends ScalaTestWithActorTestKit(ConfigFactory.load(
}
// Add another confirmed transaction, verify the cumulative total.
- val now2 = TimestampMilli.now()
+ // Note that we subtract 1 millisecond, otherwise the test may be flaky if it runs too fast and processes the
+ // message at exactly the same millisecond (because we read from the DB events < now).
+ val now2 = TimestampMilli.now() - 1.millis
val dummyTx2 = Transaction(2, Nil, Seq(TxOut(30_000 sat, Script.pay2wpkh(dummyPubKey))), 0)
val txPublished2 = TransactionPublished(channelId1, remoteNodeId1, dummyTx2, 150 sat, 0 sat, "splice", None, now2)
db.add(txPublished2)
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.