Skip to content

Commit 87e22b2

Browse files
committed
Require closed channels migration before starting
We require closed channels to be migrated to the closed channels table introduced in #3170 before starting `eclair`. This ensures that we will not lose channel data when removing support for non-anchor channels in the next release. Node operators will have to: - run the v0.13.0 release to migrate their channel data to v5 - run the v0.13.1 release to migrate their closed channels Afterwards, they'll be able to update to the (future) v0.14.x release once all of their pre-anchor channels have been closed.
1 parent a01e682 commit 87e22b2

File tree

5 files changed

+31
-337
lines changed

5 files changed

+31
-337
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ object Databases extends Logging {
8080
def apply(auditJdbc: Connection, networkJdbc: Connection, eclairJdbc: Connection, jdbcUrlFile_opt: Option[File]): SqliteDatabases = {
8181
jdbcUrlFile_opt.foreach(checkIfDatabaseUrlIsUnchanged("sqlite", _))
8282
// We check whether the node operator needs to run an intermediate eclair version first.
83-
using(eclairJdbc.createStatement(), inTransaction = true) { statement => checkChannelsDbVersion(statement, SqliteChannelsDb.DB_NAME, minimum = 7) }
83+
using(eclairJdbc.createStatement(), inTransaction = true) { statement => checkChannelsDbVersion(statement, SqliteChannelsDb.DB_NAME, minimum = 8) }
8484
SqliteDatabases(
8585
network = new SqliteNetworkDb(networkJdbc),
8686
liquidity = new SqliteLiquidityDb(eclairJdbc),
@@ -157,7 +157,7 @@ object Databases extends Logging {
157157

158158
// We check whether the node operator needs to run an intermediate eclair version first.
159159
PgUtils.inTransaction { connection =>
160-
using(connection.createStatement()) { statement => checkChannelsDbVersion(statement, PgChannelsDb.DB_NAME, minimum = 11) }
160+
using(connection.createStatement()) { statement => checkChannelsDbVersion(statement, PgChannelsDb.DB_NAME, minimum = 12) }
161161
}
162162

163163
val databases = PostgresDatabases(

eclair-core/src/main/scala/fr/acinq/eclair/db/jdbc/JdbcUtils.scala

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,32 @@ trait JdbcUtils {
8080
}
8181

8282
/**
83-
* We removed legacy channels codecs after the v0.13.0 eclair release, and migrated channels in that release.
84-
* It is thus not possible to directly upgrade from an eclair version earlier than v0.13.0.
85-
* We warn node operators that they must first run the v0.13.0 release to migrate their channel data.
83+
* We made some changes in the v0.13.0 and v0.13.1 releases to allow deprecating legacy channel data and channel types.
84+
* It is thus not possible to directly upgrade from an eclair version earlier than v0.13.x without going through some
85+
* data migration code.
86+
*
87+
* In the v0.13.0 release, we:
88+
* - introduced channel codecs v5
89+
* - migrated all channels to this codec version
90+
* - incremented the channels DB version to 7 (sqlite) and 11 (postgres)
91+
*
92+
* In the v0.13.1 release, we:
93+
* - refused to start if the channels DB version wasn't 7 (sqlite) or 11 (postgres), to force node operators to
94+
* run the v0.13.0 release first to migrate their channels to channel codecs v5
95+
* - removed support for older channel codecs
96+
* - moved closed channels to a dedicated DB table, that doesn't have a dependency on legacy channel types, to
97+
* allow deprecating channel types that aren't used anymore
98+
* - incremented the channels DB version to 8 (sqlite) and 12 (postgres)
99+
*
100+
* We warn node operators that they must first run the v0.13.x releases to migrate their channel data and prevent
101+
* eclair from starting.
86102
*/
87-
def checkChannelsDbVersion(statement: Statement, db_name: String, minimum: Int): Unit = {
103+
def checkChannelsDbVersion(statement: Statement, db_name: String, isSqlite: Boolean): Unit = {
104+
val eclair130 = if (isSqlite) 7 else 11
105+
val eclair131 = if (isSqlite) 8 else 12
88106
getVersion(statement, db_name) match {
89-
case Some(v) if v < minimum => throw new IllegalArgumentException("You are updating from a version of eclair older than v0.13.0: please update to the v0.13.0 release first to migrate your channel data, and afterwards you'll be able to update to the latest version.")
107+
case Some(v) if v < eclair130 => throw new IllegalArgumentException("You are updating from a version of eclair older than v0.13.0: please update to the v0.13.0 release first to migrate your channel data, then to the v0.13.1 release to migrate your closed channels, and afterwards you'll be able to update to the latest version.")
108+
case Some(v) if v < eclair131 => throw new IllegalArgumentException("You are updating from a version of eclair older than v0.13.1: please update to the v0.13.1 release first to migrate your closed channels, and afterwards you'll be able to update to the latest version.")
90109
case _ => ()
91110
}
92111
}

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

Lines changed: 1 addition & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, Paginated}
3030
import grizzled.slf4j.Logging
3131
import scodec.bits.BitVector
3232

33-
import java.sql.{Connection, Statement, Timestamp}
33+
import java.sql.{Connection, Timestamp}
3434
import java.time.Instant
3535
import javax.sql.DataSource
3636

@@ -49,88 +49,6 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
4949

5050
inTransaction { pg =>
5151
using(pg.createStatement()) { statement =>
52-
/**
53-
* Before version 12, closed channels were directly kept in the local_channels table with an is_closed flag set to true.
54-
* We move them to a dedicated table, where we keep minimal channel information.
55-
*/
56-
def migration1112(statement: Statement): Unit = {
57-
// We start by dropping for foreign key constraint on htlc_infos, otherwise we won't be able to move recently
58-
// closed channels to a different table.
59-
statement.executeQuery("SELECT conname FROM pg_catalog.pg_constraint WHERE contype = 'f'").map(rs => rs.getString("conname")).headOption match {
60-
case Some(foreignKeyConstraint) => statement.executeUpdate(s"ALTER TABLE local.htlc_infos DROP CONSTRAINT $foreignKeyConstraint")
61-
case None => logger.warn("couldn't find foreign key constraint for htlc_infos table: DB migration may fail")
62-
}
63-
// We can now move closed channels to a dedicated table.
64-
statement.executeUpdate("CREATE TABLE local.channels_closed (channel_id TEXT NOT NULL PRIMARY KEY, remote_node_id TEXT NOT NULL, funding_txid TEXT NOT NULL, funding_output_index BIGINT NOT NULL, funding_tx_index BIGINT NOT NULL, funding_key_path TEXT NOT NULL, channel_features TEXT NOT NULL, is_channel_opener BOOLEAN NOT NULL, commitment_format TEXT NOT NULL, announced BOOLEAN NOT NULL, capacity_satoshis BIGINT NOT NULL, closing_txid TEXT NOT NULL, closing_type TEXT NOT NULL, closing_script TEXT NOT NULL, local_balance_msat BIGINT NOT NULL, remote_balance_msat BIGINT NOT NULL, closing_amount_satoshis BIGINT NOT NULL, created_at TIMESTAMP WITH TIME ZONE NOT NULL, closed_at TIMESTAMP WITH TIME ZONE NOT NULL)")
65-
statement.executeUpdate("CREATE INDEX channels_closed_remote_node_id_idx ON local.channels_closed(remote_node_id)")
66-
// We migrate closed channels from the local_channels table to the new channels_closed table, whenever possible.
67-
val insertStatement = pg.prepareStatement("INSERT INTO local.channels_closed VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
68-
val batchSize = 50
69-
using(pg.prepareStatement("SELECT channel_id, data, is_closed, created_timestamp, closed_timestamp FROM local.channels WHERE is_closed=TRUE")) { queryStatement =>
70-
val rs = queryStatement.executeQuery()
71-
var inserted = 0
72-
var batchCount = 0
73-
while (rs.next()) {
74-
val channelId = rs.getByteVector32FromHex("channel_id")
75-
val data_opt = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value match {
76-
case d: DATA_NEGOTIATING_SIMPLE =>
77-
// We didn't store which closing transaction actually confirmed, so we select the most likely one.
78-
// The simple_close feature wasn't widely supported before this migration, so this shouldn't affect a lot of channels.
79-
val closingTx = d.publishedClosingTxs.lastOption.getOrElse(d.proposedClosingTxs.last.preferred_opt.get)
80-
Some(DATA_CLOSED(d, closingTx))
81-
case d: DATA_CLOSING =>
82-
Helpers.Closing.isClosingTypeAlreadyKnown(d) match {
83-
case Some(closingType) => Some(DATA_CLOSED(d, closingType))
84-
// If the closing type cannot be inferred from the stored data, it must be a mutual close.
85-
// In that case, we didn't store which closing transaction actually confirmed, so we select the most likely one.
86-
case None if d.mutualClosePublished.nonEmpty => Some(DATA_CLOSED(d, Helpers.Closing.MutualClose(d.mutualClosePublished.last)))
87-
case None =>
88-
logger.warn(s"cannot move channel_id=$channelId to the channels_closed table, unknown closing_type")
89-
None
90-
}
91-
case d =>
92-
logger.warn(s"cannot move channel_id=$channelId to the channels_closed table (state=${d.getClass.getSimpleName})")
93-
None
94-
}
95-
data_opt match {
96-
case Some(data) =>
97-
insertStatement.setString(1, channelId.toHex)
98-
insertStatement.setString(2, data.remoteNodeId.toHex)
99-
insertStatement.setString(3, data.fundingTxId.value.toHex)
100-
insertStatement.setLong(4, data.fundingOutputIndex)
101-
insertStatement.setLong(5, data.fundingTxIndex)
102-
insertStatement.setString(6, data.fundingKeyPath)
103-
insertStatement.setString(7, data.channelFeatures)
104-
insertStatement.setBoolean(8, data.isChannelOpener)
105-
insertStatement.setString(9, data.commitmentFormat)
106-
insertStatement.setBoolean(10, data.announced)
107-
insertStatement.setLong(11, data.capacity.toLong)
108-
insertStatement.setString(12, data.closingTxId.value.toHex)
109-
insertStatement.setString(13, data.closingType)
110-
insertStatement.setString(14, data.closingScript.toHex)
111-
insertStatement.setLong(15, data.localBalance.toLong)
112-
insertStatement.setLong(16, data.remoteBalance.toLong)
113-
insertStatement.setLong(17, data.closingAmount.toLong)
114-
insertStatement.setTimestamp(18, rs.getTimestampNullable("created_timestamp").getOrElse(Timestamp.from(Instant.ofEpochMilli(0))))
115-
insertStatement.setTimestamp(19, rs.getTimestampNullable("closed_timestamp").getOrElse(Timestamp.from(Instant.ofEpochMilli(0))))
116-
insertStatement.addBatch()
117-
batchCount = batchCount + 1
118-
if (batchCount % batchSize == 0) {
119-
inserted = inserted + insertStatement.executeBatch().sum
120-
batchCount = 0
121-
}
122-
case None => ()
123-
}
124-
}
125-
inserted = inserted + insertStatement.executeBatch().sum
126-
logger.info(s"moved $inserted channels to the channels_closed table")
127-
}
128-
// We can now clean-up the active channels table.
129-
statement.executeUpdate("DELETE FROM local.channels WHERE is_closed=TRUE")
130-
statement.executeUpdate("ALTER TABLE local.channels DROP COLUMN is_closed")
131-
statement.executeUpdate("ALTER TABLE local.channels DROP COLUMN closed_timestamp")
132-
}
133-
13452
getVersion(statement, DB_NAME) match {
13553
case None =>
13654
statement.executeUpdate("CREATE SCHEMA IF NOT EXISTS local")
@@ -147,10 +65,6 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
14765
statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON local.htlc_infos(channel_id)")
14866
statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON local.htlc_infos(commitment_number)")
14967
statement.executeUpdate("CREATE INDEX channels_closed_remote_node_id_idx ON local.channels_closed(remote_node_id)")
150-
case Some(v) if v < 11 => throw new RuntimeException("You are updating from a version of eclair older than v0.13.0: please update to the v0.13.0 release first to migrate your channel data, and afterwards you'll be able to update to the latest version.")
151-
case Some(v@11) =>
152-
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
153-
if (v < 12) migration1112(statement)
15468
case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do
15569
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
15670
}

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

Lines changed: 1 addition & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,8 @@ import fr.acinq.eclair.db.Monitoring.Tags.DbBackends
2626
import fr.acinq.eclair.wire.internal.channel.ChannelCodecs.channelDataCodec
2727
import fr.acinq.eclair.{CltvExpiry, MilliSatoshi, Paginated, TimestampMilli}
2828
import grizzled.slf4j.Logging
29-
import scodec.bits.BitVector
3029

31-
import java.sql.{Connection, Statement}
30+
import java.sql.Connection
3231

3332
object SqliteChannelsDb {
3433
val DB_NAME = "channels"
@@ -50,90 +49,6 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
5049
statement.execute("PRAGMA foreign_keys = ON")
5150
}
5251

53-
/**
54-
* Before version 8, closed channels were directly kept in the local_channels table with an is_closed flag set to true.
55-
* We move them to a dedicated table, where we keep minimal channel information.
56-
*/
57-
def migration78(statement: Statement): Unit = {
58-
// We start by dropping for foreign key constraint on htlc_infos, otherwise we won't be able to move recently
59-
// closed channels to a different table. The only option for that in sqlite is to re-create the table.
60-
statement.executeUpdate("ALTER TABLE htlc_infos RENAME TO htlc_infos_old")
61-
statement.executeUpdate("CREATE TABLE htlc_infos (channel_id BLOB NOT NULL, commitment_number INTEGER NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL)")
62-
statement.executeUpdate("INSERT INTO htlc_infos(channel_id, commitment_number, payment_hash, cltv_expiry) SELECT channel_id, commitment_number, payment_hash, cltv_expiry FROM htlc_infos_old")
63-
statement.executeUpdate("DROP TABLE htlc_infos_old")
64-
statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON htlc_infos(channel_id)")
65-
statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON htlc_infos(commitment_number)")
66-
// We can now move closed channels to a dedicated table.
67-
statement.executeUpdate("CREATE TABLE local_channels_closed (channel_id TEXT NOT NULL PRIMARY KEY, remote_node_id TEXT NOT NULL, funding_txid TEXT NOT NULL, funding_output_index INTEGER NOT NULL, funding_tx_index INTEGER NOT NULL, funding_key_path TEXT NOT NULL, channel_features TEXT NOT NULL, is_channel_opener BOOLEAN NOT NULL, commitment_format TEXT NOT NULL, announced BOOLEAN NOT NULL, capacity_satoshis INTEGER NOT NULL, closing_txid TEXT NOT NULL, closing_type TEXT NOT NULL, closing_script TEXT NOT NULL, local_balance_msat INTEGER NOT NULL, remote_balance_msat INTEGER NOT NULL, closing_amount_satoshis INTEGER NOT NULL, created_at INTEGER NOT NULL, closed_at INTEGER NOT NULL)")
68-
statement.executeUpdate("CREATE INDEX local_channels_closed_remote_node_id_idx ON local_channels_closed(remote_node_id)")
69-
// We migrate closed channels from the local_channels table to the new local_channels_closed table, whenever possible.
70-
val insertStatement = sqlite.prepareStatement("INSERT INTO local_channels_closed VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
71-
val batchSize = 50
72-
using(sqlite.prepareStatement("SELECT channel_id, data, is_closed, created_timestamp, closed_timestamp FROM local_channels WHERE is_closed=1")) { queryStatement =>
73-
val rs = queryStatement.executeQuery()
74-
var inserted = 0
75-
var batchCount = 0
76-
while (rs.next()) {
77-
val channelId = rs.getByteVector32("channel_id")
78-
val data_opt = channelDataCodec.decode(BitVector(rs.getBytes("data"))).require.value match {
79-
case d: DATA_NEGOTIATING_SIMPLE =>
80-
// We didn't store which closing transaction actually confirmed, so we select the most likely one.
81-
// The simple_close feature wasn't widely supported before this migration, so this shouldn't affect a lot of channels.
82-
val closingTx = d.publishedClosingTxs.lastOption.getOrElse(d.proposedClosingTxs.last.preferred_opt.get)
83-
Some(DATA_CLOSED(d, closingTx))
84-
case d: DATA_CLOSING =>
85-
Helpers.Closing.isClosingTypeAlreadyKnown(d) match {
86-
case Some(closingType) => Some(DATA_CLOSED(d, closingType))
87-
// If the closing type cannot be inferred from the stored data, it must be a mutual close.
88-
// In that case, we didn't store which closing transaction actually confirmed, so we select the most likely one.
89-
case None if d.mutualClosePublished.nonEmpty => Some(DATA_CLOSED(d, Helpers.Closing.MutualClose(d.mutualClosePublished.last)))
90-
case None =>
91-
logger.warn(s"cannot move channel_id=$channelId to the local_channels_closed table, unknown closing_type")
92-
None
93-
}
94-
case d =>
95-
logger.warn(s"cannot move channel_id=$channelId to the local_channels_closed table (state=${d.getClass.getSimpleName})")
96-
None
97-
}
98-
data_opt match {
99-
case Some(data) =>
100-
insertStatement.setString(1, channelId.toHex)
101-
insertStatement.setString(2, data.remoteNodeId.toHex)
102-
insertStatement.setString(3, data.fundingTxId.value.toHex)
103-
insertStatement.setLong(4, data.fundingOutputIndex)
104-
insertStatement.setLong(5, data.fundingTxIndex)
105-
insertStatement.setString(6, data.fundingKeyPath)
106-
insertStatement.setString(7, data.channelFeatures)
107-
insertStatement.setBoolean(8, data.isChannelOpener)
108-
insertStatement.setString(9, data.commitmentFormat)
109-
insertStatement.setBoolean(10, data.announced)
110-
insertStatement.setLong(11, data.capacity.toLong)
111-
insertStatement.setString(12, data.closingTxId.value.toHex)
112-
insertStatement.setString(13, data.closingType)
113-
insertStatement.setString(14, data.closingScript.toHex)
114-
insertStatement.setLong(15, data.localBalance.toLong)
115-
insertStatement.setLong(16, data.remoteBalance.toLong)
116-
insertStatement.setLong(17, data.closingAmount.toLong)
117-
insertStatement.setLong(18, rs.getLongNullable("created_timestamp").getOrElse(0))
118-
insertStatement.setLong(19, rs.getLongNullable("closed_timestamp").getOrElse(0))
119-
insertStatement.addBatch()
120-
batchCount = batchCount + 1
121-
if (batchCount % batchSize == 0) {
122-
inserted = inserted + insertStatement.executeBatch().sum
123-
batchCount = 0
124-
}
125-
case None => ()
126-
}
127-
}
128-
inserted = inserted + insertStatement.executeBatch().sum
129-
logger.info(s"moved $inserted channels to the local_channels_closed table")
130-
}
131-
// We can now clean-up the active channels table.
132-
statement.executeUpdate("DELETE FROM local_channels WHERE is_closed=1")
133-
statement.executeUpdate("ALTER TABLE local_channels DROP COLUMN is_closed")
134-
statement.executeUpdate("ALTER TABLE local_channels DROP COLUMN closed_timestamp")
135-
}
136-
13752
using(sqlite.createStatement(), inTransaction = true) { statement =>
13853
getVersion(statement, DB_NAME) match {
13954
case None =>
@@ -146,10 +61,6 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
14661
statement.executeUpdate("CREATE INDEX htlc_infos_channel_id_idx ON htlc_infos(channel_id)")
14762
statement.executeUpdate("CREATE INDEX htlc_infos_commitment_number_idx ON htlc_infos(commitment_number)")
14863
statement.executeUpdate("CREATE INDEX local_channels_closed_remote_node_id_idx ON local_channels_closed(remote_node_id)")
149-
case Some(v) if v < 7 => throw new RuntimeException("You are updating from a version of eclair older than v0.13.0: please update to the v0.13.0 release first to migrate your channel data, and afterwards you'll be able to update to the latest version.")
150-
case Some(v@7) =>
151-
logger.warn(s"migrating db $DB_NAME, found version=$v current=$CURRENT_VERSION")
152-
if (v < 8) migration78(statement)
15364
case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do
15465
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
15566
}

0 commit comments

Comments
 (0)