What changed, and why it matters
This commit is a planned database migration for the Eclair Lightning node software. When users upgrade, it automatically re-encodes all stored payment-channel data to a newer internal format (v5). The change also warns users that older 'non-anchor' channels will no longer be supported in the next release and should be closed. There is no direct security vulnerability in the patch itself; it is a cleanup step to let the developers remove legacy code later.
Treat as a routine upgrade/migration commit. Operators should back up channel databases before upgrading, monitor startup logs for the non-anchor channel warning, and close legacy channels as instructed. No security patch or incident response is indicated by the diff.
Security signals we found
Database migration re-serializes all channel blobs using current codec
Deprecation of non-anchor output channels with user-facing close instructions
No input validation, cryptographic, or network changes observed
Evidence from the diff
The patch bumps the channel database version (Pg 10→11, SQLite 6→7) and adds migration routines that decode every row in the local channels table and re-encode it with the current channelDataCodec. This forces on-disk data to the v5 codec so that legacy codec support can be dropped in a future release. A runtime warning is added in Channel.scala when a restored channel uses DefaultCommitmentFormat (legacy) instead of anchor outputs. Release notes document the migration and deprecation.
Changed components
eclair-core PostgreSQL channels database migration (PgChannelsDb.scala)eclair-core SQLite channels database migration (SqliteChannelsDb.scala)Channel state machine restore path (Channel.scala)Release notes (eclair-vnext.md)Inspect captured patch +80 / −5
diff --git a/docs/release-notes/eclair-vnext.md b/docs/release-notes/eclair-vnext.md
index 8d5f76b..34204c3 100644
--- a/docs/release-notes/eclair-vnext.md
+++ b/docs/release-notes/eclair-vnext.md
@@ -13,6 +13,30 @@ When using anchor outputs, allows propagating our local commitment transaction t
This removes the need for increasing the commitment feerate based on mempool conditions, which ensures that channels won't be force-closed anymore when nodes disagree on the current feerate.
+### Deprecation warning for non-anchor channels
+
+This is the last release where `eclair` will support non-anchor channels.
+Starting with the next release, those channels will be deprecated and `eclair` will refuse to start.
+Please make sure you close your existing non-anchor channels whenever convenient.
+
+You can list those channels using the following command:
+
+```sh
+$ eclair-cli channels | jq '.[] | { channelId: .data.commitments.channelParams.channelId, commitmentFormat: .data.commitments.active[].commitmentFormat }' | jq 'select(.["commitmentFormat"] == "legacy")'
+```
+
+If your peer is online, you can then cooperatively close those channels using the following command:
+
+```sh
+$ eclair-cli close --channelId=<channel_id_from_previous_step> --preferredFeerateSatByte=<feerate_satoshis_per_byte>
+```
+
+If your peer isn't online, you may want to force-close those channels to recover your funds:
+
+```sh
+$ eclair-cli forceclose --channelId=<channel_id_from_previous_step>
+```
+
### Attribution data
Eclair now supports attributable failures which allow nodes to prove they are not the source of the failure.
@@ -96,6 +120,14 @@ eclair.channel.min-depth-blocks = 8
Note however that we require `min-depth` to be at least 6 blocks, since the BOLTs require this before announcing channels.
See #3044 for more details.
+#### Database migration of channel data
+
+When updating your node, eclair will automatically migrate all of your channel data to the latest (internal) encoding.
+Depending on the number of open channels, this may be a bit slow: don't worry if this initial start-up is taking more time than usual.
+This will only happen the first time you restart your node.
+
+This is an important step towards removing legacy code from our codebase, which we will do before the next release.
+
## Verifying signatures
You will need `gpg` and our release signing key E04E48E72C205463. Note that you can get it:
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
index 5760bbb..7a4e1dd 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala
@@ -317,6 +317,11 @@ class Channel(val nodeParams: NodeParams, val channelKeys: ChannelKeys, val wall
case Event(INPUT_RESTORED(data), _) =>
log.debug("restoring channel")
+ data match {
+ case data: ChannelDataWithCommitments if data.commitments.active.exists(_.commitmentFormat == DefaultCommitmentFormat) =>
+ log.warning("channel is not using anchor outputs: please close it and re-open an anchor output channel before updating to the next version of eclair")
+ case _ => ()
+ }
context.system.eventStream.publish(ChannelRestored(self, data.channelId, peer, remoteNodeId, data))
txPublisher ! SetChannelId(remoteNodeId, data.channelId)
// We watch all unconfirmed funding txs, whatever our state is.
diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala
index 599c46d..103f382 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/pg/PgChannelsDb.scala
@@ -36,7 +36,7 @@ import javax.sql.DataSource
object PgChannelsDb {
val DB_NAME = "channels"
- val CURRENT_VERSION = 10
+ val CURRENT_VERSION = 11
}
class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb with Logging {
@@ -118,7 +118,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
def migration89(statement: Statement): Unit = {
statement.executeUpdate("CREATE TABLE local.htlc_infos_to_remove (channel_id TEXT NOT NULL PRIMARY KEY, before_commitment_number BIGINT NOT NULL)")
}
-
+
def migration910(statement: Statement): Unit = {
// We're changing our composite index to two distinct indices to improve performance.
statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON local.htlc_infos(channel_id)")
@@ -126,6 +126,23 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
statement.executeUpdate("DROP INDEX IF EXISTS local.htlc_infos_idx")
}
+ def migration1011(statement: Statement): Unit = {
+ migrateTable(pg, pg,
+ "local.channels",
+ "UPDATE local.channels SET data=?, json=?::JSONB WHERE channel_id=?",
+ (rs, statement) => {
+ // This forces a re-serialization of the channel data with latest codecs, because we want to remove support
+ // for codecs older than v5 in the next release.
+ val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value
+ val data = channelDataCodec.encode(state).require.toByteArray
+ val json = serialization.write(state)
+ statement.setBytes(1, data)
+ statement.setString(2, json)
+ statement.setString(3, state.channelId.toHex)
+ }
+ )(logger)
+ }
+
getVersion(statement, DB_NAME) match {
case None =>
statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local")
@@ -140,7 +157,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
// This is more efficient because we're writing a lot to this table but only reading when a channel is force-closed.
statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON local.htlc_infos(channel_id)")
statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON local.htlc_infos(commitment_number)")
- case Some(v@(2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)) =>
+ case Some(v@(2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10)) =>
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
if (v < 3) {
migration23(statement)
@@ -166,6 +183,9 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
if (v < 10) {
migration910(statement)
}
+ if (v < 11) {
+ migration1011(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/SqliteChannelsDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala
index f9c54cf..4a27b40 100644
--- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala
+++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala
@@ -32,7 +32,7 @@ import java.sql.{Connection, Statement}
object SqliteChannelsDb {
val DB_NAME = "channels"
- val CURRENT_VERSION = 6
+ val CURRENT_VERSION = 7
}
class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
@@ -90,6 +90,21 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
statement.executeUpdate("DROP INDEX IF EXISTS htlc_infos_idx")
}
+ def migration67(): Unit = {
+ migrateTable(sqlite, sqlite,
+ "local_channels",
+ "UPDATE local_channels SET data=? WHERE channel_id=?",
+ (rs, statement) => {
+ // This forces a re-serialization of the channel data with latest codecs, because we want to remove support
+ // for codecs older than v5 in the next release.
+ val state = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value
+ val data = channelDataCodec.encode(state).require.toByteArray
+ statement.setBytes(1, data)
+ statement.setBytes(2, state.channelId.toArray)
+ }
+ )(logger)
+ }
+
getVersion(statement, DB_NAME) match {
case None =>
statement.executeUpdate("CREATE TABLE local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0, created_timestamp INTEGER, last_payment_sent_timestamp INTEGER, last_payment_received_timestamp INTEGER, last_connected_timestamp INTEGER, closed_timestamp INTEGER)")
@@ -99,7 +114,7 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
// This is more efficient because we're writing a lot to this table but only reading when a channel is force-closed.
statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON htlc_infos(channel_id)")
statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON htlc_infos(commitment_number)")
- case Some(v@(1 | 2 | 3 | 4 | 5)) =>
+ case Some(v@(1 | 2 | 3 | 4 | 5 | 6)) =>
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
if (v < 2) {
migration12(statement)
@@ -116,6 +131,9 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
if (v < 6) {
migration56()
}
+ if (v < 7) {
+ migration67()
+ }
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")
}
Why this scored 29/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.