Skip to content

Commit 9bf78e3

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 9bf78e3

File tree

5 files changed

+29
-334
lines changed

5 files changed

+29
-334
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: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,29 @@ 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
*/
87103
def checkChannelsDbVersion(statement: Statement, db_name: String, minimum: Int): Unit = {
88104
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.")
105+
case Some(v) if v < minimum => 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 channel data, and afterwards you'll be able to update to the latest version.")
90106
case _ => ()
91107
}
92108
}

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

Lines changed: 2 additions & 86 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")
@@ -148,9 +66,7 @@ class PgChannelsDb(implicit ds: DataSource, lock: PgLock) extends ChannelsDb wit
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)")
15068
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)
69+
case Some(v) if v < 12 => throw new RuntimeException("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 channel data, and afterwards you'll be able to update to the latest version.")
15470
case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do
15571
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
15672
}

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

Lines changed: 2 additions & 89 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 =>
@@ -147,9 +62,7 @@ class SqliteChannelsDb(val sqlite: Connection) extends ChannelsDb with Logging {
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)")
14964
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)
65+
case Some(v) if v < 8 => throw new RuntimeException("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 channel data, and afterwards you'll be able to update to the latest version.")
15366
case Some(CURRENT_VERSION) => () // table is up-to-date, nothing to do
15467
case Some(unknownVersion) => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion")
15568
}

0 commit comments

Comments
 (0)