From 950c55ab0538f05f60210f817d2421541598c7f1 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 12 Apr 2019 15:39:31 +0200 Subject: [PATCH 01/19] Channel: add helper method to check if a channel is closed --- .../fr/acinq/eclair/channel/Channel.scala | 28 +++++----------- .../fr/acinq/eclair/channel/Helpers.scala | 33 +++++++++++++++++++ 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 6191550165..0e610aa77e 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -1258,34 +1258,24 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId .onchainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) .filter(add => Closing.isSentByLocal(add.id, d.commitments.originChannels)) // we only care about htlcs for which we were the original sender here .foreach(add => context.system.eventStream.publish(PaymentSettlingOnChain(amount = MilliSatoshi(add.amountMsat), add.paymentHash))) - // then let's see if any of the possible close scenarii can be considered done - val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed - val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) - val remoteCommitDone = remoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) - val nextRemoteCommitDone = nextRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) - val futureRemoteCommitDone = futureRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) - val revokedCommitDone = revokedCommitPublished1.map(Closing.isRevokedCommitDone(_)).exists(_ == true) // we only need one revoked commit done + // finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state) val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) // we also send events related to fee Closing.networkFeePaid(tx, d1) map { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) } - val closeType_opt = if (mutualCloseDone) { - Some("mutual") - } else if (localCommitDone) { - Some("local") - } else if (remoteCommitDone || nextRemoteCommitDone) { - Some("remote") - } else if (futureRemoteCommitDone) { - Some("recovery") - } else if (revokedCommitDone) { - Some("revoked") + + // can we close this channel ? + val closeType_opt = if (d1.mutualClosePublished.exists(_.txid == tx.txid)) { + // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed) + Some(Closing.MutualClose) } else { - None + Closing.closingType(d1) } + closeType_opt match { case Some(closeType) => log.info(s"channel closed (type=$closeType)") - context.system.eventStream.publish(ChannelClosed(self, d.channelId, closeType, d.commitments)) + context.system.eventStream.publish(ChannelClosed(self, d.channelId, closeType.toString, d.commitments)) goto(CLOSED) using store(d1) case None => stay using store(d1) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index bd2c267a93..4e6bcbf9f3 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -337,6 +337,15 @@ object Helpers { object Closing { + // @formatter:off + sealed trait ClosingType + case object MutualClose extends ClosingType + case object LocalClose extends ClosingType + case object RemoteClose extends ClosingType + case object RecoveryClose extends ClosingType + case object RevokedClose extends ClosingType + // @formatter:on + /** * Indicates whether local has anything at stake in this channel * @@ -350,6 +359,30 @@ object Helpers { data.commitments.remoteCommit.spec.toRemoteMsat == 0 && data.commitments.remoteNextCommitInfo.isRight + /** + * Check if a channel is closed (i.e. its closing tx has been confirmed) + * @param data channel state data + * @return the channel closing type, if any + */ + def closingType(data: HasCommitments): Option[ClosingType] = data match { + case closing: DATA_CLOSING if closing.localCommitPublished.map(Closing.isLocalCommitDone(_)).getOrElse(false) => + Some(LocalClose) + + case closing: DATA_CLOSING if closing.remoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => + Some(RemoteClose) + + case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => + Some(RemoteClose) + + case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => + Some(RecoveryClose) + + case closing: DATA_CLOSING if closing.revokedCommitPublished.map(Closing.isRevokedCommitDone(_)).exists(_ == true) => + Some(RevokedClose) + + case _ => None + } + // used only to compute tx weights and estimate fees lazy val dummyPublicKey = PrivateKey(ByteVector32(ByteVector.fill(32)(1)), true).publicKey From bab7ee148afec9ed707ec1aca490fe5e1bf2c06d Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 12 Apr 2019 15:51:51 +0200 Subject: [PATCH 02/19] Remove closed channels when application starts If the app is stopped just after a channel has transition fronm CLOSING to CLOSED, when the application starts again if will be restored as CLOSING. This commit checks channel data and remove closed channels instead of restoring them. --- .../src/main/scala/fr/acinq/eclair/io/Switchboard.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index c2b6cf0b18..2084e1ea50 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -22,6 +22,7 @@ import akka.actor.{Actor, ActorLogging, ActorRef, OneForOneStrategy, Props, Stat import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.eclair.NodeParams import fr.acinq.eclair.blockchain.EclairWallet +import fr.acinq.eclair.channel.Helpers.Closing import fr.acinq.eclair.channel.{HasCommitments, _} import fr.acinq.eclair.payment.Relayer.RelayPayload import fr.acinq.eclair.payment.{Relayed, Relayer} @@ -44,7 +45,11 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto // we load peers and channels from database { - val channels = nodeParams.db.channels.listLocalChannels() + val (channels, closedChannels) = nodeParams.db.channels.listLocalChannels().partition(c => Closing.closingType(c).isEmpty) + closedChannels.foreach(c => { + log.info(s"closing channel ${c.channelId}") + nodeParams.db.channels.removeChannel(c.channelId) + }) val peers = nodeParams.db.peers.listPeers() checkBrokenHtlcsLink(channels, nodeParams.privateKey) match { From ee66e8a312dc0813b9ee67a751d88d2b42b601e2 Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 12 Apr 2019 16:57:36 +0200 Subject: [PATCH 03/19] Channels Database: tag closed channels but don't delete them Instead we add a new `closed` column that we check when we restore channels. --- .../eclair/db/sqlite/SqliteChannelsDb.scala | 17 +++++++++++------ .../fr/acinq/eclair/db/sqlite/SqliteUtils.scala | 6 ++++++ 2 files changed, 17 insertions(+), 6 deletions(-) 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 fdc10418fe..00df0f3c07 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 @@ -31,12 +31,17 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { import SqliteUtils._ val DB_NAME = "channels" - val CURRENT_VERSION = 1 + val CURRENT_VERSION = 2 using(sqlite.createStatement()) { statement => - require(getVersion(statement, DB_NAME, CURRENT_VERSION) == CURRENT_VERSION) // there is only one version currently deployed + getVersion(statement, DB_NAME, CURRENT_VERSION) match { + case 1 => + statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN closed BOOLEAN NOT NULL DEFAULT 0") + updateVersion(statement, DB_NAME, CURRENT_VERSION) + case CURRENT_VERSION => () + } statement.execute("PRAGMA foreign_keys = ON") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, closed BOOLEAN NOT NULL DEFAULT 0)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") } @@ -47,7 +52,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { update.setBytes(1, data) update.setBytes(2, state.channelId.toArray) if (update.executeUpdate() == 0) { - using(sqlite.prepareStatement("INSERT INTO local_channels VALUES (?, ?)")) { statement => + using(sqlite.prepareStatement("INSERT INTO local_channels VALUES (?, ?, 0)")) { statement => statement.setBytes(1, state.channelId.toArray) statement.setBytes(2, data) statement.executeUpdate() @@ -67,7 +72,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { statement.executeUpdate() } - using(sqlite.prepareStatement("DELETE FROM local_channels WHERE channel_id=?")) { statement => + using(sqlite.prepareStatement("UPDATE local_channels SET closed=1 WHERE channel_id=?")) { statement => statement.setBytes(1, channelId.toArray) statement.executeUpdate() } @@ -75,7 +80,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { override def listLocalChannels(): Seq[HasCommitments] = { using(sqlite.createStatement) { statement => - val rs = statement.executeQuery("SELECT data FROM local_channels") + val rs = statement.executeQuery("SELECT data FROM local_channels WHERE closed=0") codecSequence(rs, stateDataCodec) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 8d8e602e60..5547f0de07 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -60,6 +60,12 @@ object SqliteUtils { res.getInt("version") } + def updateVersion(statement: Statement, db_name: String, version: Int): Int = { + statement.executeUpdate(s"UPDATE versions SET version=$version WHERE db_name='$db_name'") + // if there was a previous version installed, this will return a different value from current version + val res = statement.executeQuery(s"SELECT version FROM versions WHERE db_name='$db_name'") + res.getInt("version") + } /** * This helper assumes that there is a "data" column available, decodable with the provided codec * From 83d6cc35342c59224f595f84eff517a3126748b1 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 16 Apr 2019 10:53:36 +0200 Subject: [PATCH 04/19] Update eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala Co-Authored-By: sstone --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 4e6bcbf9f3..ac96b16518 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -362,7 +362,7 @@ object Helpers { /** * Check if a channel is closed (i.e. its closing tx has been confirmed) * @param data channel state data - * @return the channel closing type, if any + * @return the channel closing type, if applicable */ def closingType(data: HasCommitments): Option[ClosingType] = data match { case closing: DATA_CLOSING if closing.localCommitPublished.map(Closing.isLocalCommitDone(_)).getOrElse(false) => From fec1ba6c7c1241a6955a38b22678792bb349a404 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 16 Apr 2019 10:53:45 +0200 Subject: [PATCH 05/19] Update eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala Co-Authored-By: sstone --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index ac96b16518..56b9124042 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -365,7 +365,7 @@ object Helpers { * @return the channel closing type, if applicable */ def closingType(data: HasCommitments): Option[ClosingType] = data match { - case closing: DATA_CLOSING if closing.localCommitPublished.map(Closing.isLocalCommitDone(_)).getOrElse(false) => + case closing: DATA_CLOSING if closing.localCommitPublished.exists(Closing.isLocalCommitDone(_)) => Some(LocalClose) case closing: DATA_CLOSING if closing.remoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => From 9c230e4aeb5ee444fff88ce73c6a4b0580c08009 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 16 Apr 2019 10:53:55 +0200 Subject: [PATCH 06/19] Update eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala Co-Authored-By: sstone --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 56b9124042..0238a84d14 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -364,7 +364,7 @@ object Helpers { * @param data channel state data * @return the channel closing type, if applicable */ - def closingType(data: HasCommitments): Option[ClosingType] = data match { + def isClosed(data: HasCommitments): Option[ClosingType] = data match { case closing: DATA_CLOSING if closing.localCommitPublished.exists(Closing.isLocalCommitDone(_)) => Some(LocalClose) From 68153fd4c806c67ea253a4edc81b0b1ef02ffd91 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 16 Apr 2019 10:54:05 +0200 Subject: [PATCH 07/19] Update eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala Co-Authored-By: sstone --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 0238a84d14..970dbbebf5 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -377,7 +377,7 @@ object Helpers { case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => Some(RecoveryClose) - case closing: DATA_CLOSING if closing.revokedCommitPublished.map(Closing.isRevokedCommitDone(_)).exists(_ == true) => + case closing: DATA_CLOSING if closing.revokedCommitPublished.exists(Closing.isRevokedCommitDone(_)) => Some(RevokedClose) case _ => None From 444a83cafbd6904fa9e2460a0688490f63001523 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 16 Apr 2019 10:54:58 +0200 Subject: [PATCH 08/19] Update eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala Co-Authored-By: sstone --- .../main/scala/fr/acinq/eclair/db/sqlite/SqliteChannelsDb.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 00df0f3c07..8f296c71c2 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 @@ -41,7 +41,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { case CURRENT_VERSION => () } statement.execute("PRAGMA foreign_keys = ON") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, closed BOOLEAN NOT NULL DEFAULT 0)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0)") statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") } From e482f8d0b0edcf6a2ab56974b8a17fe9be05b013 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 16 Apr 2019 10:55:25 +0200 Subject: [PATCH 09/19] Update eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala Co-Authored-By: sstone --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 970dbbebf5..a4ed6ae35c 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -374,7 +374,7 @@ object Helpers { case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => Some(RemoteClose) - case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => + case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => Some(RecoveryClose) case closing: DATA_CLOSING if closing.revokedCommitPublished.exists(Closing.isRevokedCommitDone(_)) => From 7a7d8bdd0a7a0dc08e9dcada8493156c2e5c7e0c Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 16 Apr 2019 10:55:32 +0200 Subject: [PATCH 10/19] Update eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala Co-Authored-By: sstone --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index a4ed6ae35c..37684dd515 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -371,7 +371,7 @@ object Helpers { case closing: DATA_CLOSING if closing.remoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => Some(RemoteClose) - case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => + case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => Some(RemoteClose) case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => From 338c32435499af38bf6a67bc8efb85cd39e33a07 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Tue, 16 Apr 2019 10:55:53 +0200 Subject: [PATCH 11/19] Update eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala Co-Authored-By: sstone --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 37684dd515..457d2963f7 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -368,7 +368,7 @@ object Helpers { case closing: DATA_CLOSING if closing.localCommitPublished.exists(Closing.isLocalCommitDone(_)) => Some(LocalClose) - case closing: DATA_CLOSING if closing.remoteCommitPublished.map(Closing.isRemoteCommitDone(_)).getOrElse(false) => + case closing: DATA_CLOSING if closing.remoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => Some(RemoteClose) case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => From 3b9857e62a62a6eb7e23ec221077df7d46e33587 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 16 Apr 2019 13:31:21 +0200 Subject: [PATCH 12/19] Make helper functions handle all possible closing types --- .../src/main/scala/fr/acinq/eclair/channel/Channel.scala | 7 +------ .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 6 +++++- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 0e610aa77e..9b68c8ad39 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -1265,12 +1265,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId Closing.networkFeePaid(tx, d1) map { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) } // can we close this channel ? - val closeType_opt = if (d1.mutualClosePublished.exists(_.txid == tx.txid)) { - // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed) - Some(Closing.MutualClose) - } else { - Closing.closingType(d1) - } + val closeType_opt = Closing.isClosed(d1, Some(tx)) closeType_opt match { case Some(closeType) => diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 457d2963f7..6d4bc93e85 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -362,9 +362,13 @@ object Helpers { /** * Check if a channel is closed (i.e. its closing tx has been confirmed) * @param data channel state data + * @param closingTx closing transaction, needed in case of mutual close * @return the channel closing type, if applicable */ - def isClosed(data: HasCommitments): Option[ClosingType] = data match { + def isClosed(data: HasCommitments, closingTx: Option[Transaction]): Option[ClosingType] = data match { + case closing: DATA_CLOSING if closingTx.isDefined && closing.mutualClosePublished.exists(_.txid == closingTx.get.txid) => + Some(MutualClose) + case closing: DATA_CLOSING if closing.localCommitPublished.exists(Closing.isLocalCommitDone(_)) => Some(LocalClose) From 8fa0bf9359367291107aa5236f5fbea80ff47a8e Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 16 Apr 2019 13:31:56 +0200 Subject: [PATCH 13/19] ChannelsDb: rename `closed` field to `is_closed` --- .../eclair/db/sqlite/SqliteChannelsDb.scala | 8 ++++---- .../fr/acinq/eclair/db/sqlite/SqliteUtils.scala | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 9 deletions(-) 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 8f296c71c2..1e15541d68 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 @@ -36,8 +36,8 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { using(sqlite.createStatement()) { statement => getVersion(statement, DB_NAME, CURRENT_VERSION) match { case 1 => - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN closed BOOLEAN NOT NULL DEFAULT 0") - updateVersion(statement, DB_NAME, CURRENT_VERSION) + statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN is_closed BOOLEAN NOT NULL DEFAULT 0") + setVersion(statement, DB_NAME, CURRENT_VERSION) case CURRENT_VERSION => () } statement.execute("PRAGMA foreign_keys = ON") @@ -72,7 +72,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { statement.executeUpdate() } - using(sqlite.prepareStatement("UPDATE local_channels SET closed=1 WHERE channel_id=?")) { statement => + using(sqlite.prepareStatement("UPDATE local_channels SET is_closed=1 WHERE channel_id=?")) { statement => statement.setBytes(1, channelId.toArray) statement.executeUpdate() } @@ -80,7 +80,7 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { override def listLocalChannels(): Seq[HasCommitments] = { using(sqlite.createStatement) { statement => - val rs = statement.executeQuery("SELECT data FROM local_channels WHERE closed=0") + val rs = statement.executeQuery("SELECT data FROM local_channels WHERE is_closed=0") codecSequence(rs, stateDataCodec) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala index 5547f0de07..a0e20e3fad 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/db/sqlite/SqliteUtils.scala @@ -60,12 +60,19 @@ object SqliteUtils { res.getInt("version") } - def updateVersion(statement: Statement, db_name: String, version: Int): Int = { - statement.executeUpdate(s"UPDATE versions SET version=$version WHERE db_name='$db_name'") - // if there was a previous version installed, this will return a different value from current version - val res = statement.executeQuery(s"SELECT version FROM versions WHERE db_name='$db_name'") - res.getInt("version") + /** + * Updates the version for a particular logical database, it will overwrite the previous version. + * @param statement + * @param db_name + * @param newVersion + * @return + */ + def setVersion(statement: Statement, db_name: String, newVersion: Int) = { + statement.executeUpdate("CREATE TABLE IF NOT EXISTS versions (db_name TEXT NOT NULL PRIMARY KEY, version INTEGER NOT NULL)") + // overwrite the existing version + statement.executeUpdate(s"UPDATE versions SET version=$newVersion WHERE db_name='$db_name'") } + /** * This helper assumes that there is a "data" column available, decodable with the provided codec * From f0a762056e6b8ff5fa364bbdcff20f99185869b5 Mon Sep 17 00:00:00 2001 From: sstone Date: Tue, 16 Apr 2019 13:32:42 +0200 Subject: [PATCH 14/19] Document how we check and remove closed channels on startup --- .../src/main/scala/fr/acinq/eclair/io/Switchboard.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala index 2084e1ea50..81ef73831b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/io/Switchboard.scala @@ -45,7 +45,10 @@ class Switchboard(nodeParams: NodeParams, authenticator: ActorRef, watcher: Acto // we load peers and channels from database { - val (channels, closedChannels) = nodeParams.db.channels.listLocalChannels().partition(c => Closing.closingType(c).isEmpty) + // Check if channels that are still in CLOSING state have actually been closed. This can happen when the app is stopped + // just after a channel state has transitioned to CLOSED and before it has effectively been removed. + // Closed channels will be removed, other channels will be restored. + val (channels, closedChannels) = nodeParams.db.channels.listLocalChannels().partition(c => Closing.isClosed(c, None).isEmpty) closedChannels.foreach(c => { log.info(s"closing channel ${c.channelId}") nodeParams.db.channels.removeChannel(c.channelId) From 0ad8d9fb079dcb7eb720c9d585fc1a97b11a75f5 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Padiou Date: Fri, 19 Apr 2019 10:43:25 +0200 Subject: [PATCH 15/19] Minor: comment --- .../src/main/scala/fr/acinq/eclair/channel/Channel.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 781af82d53..459da416af 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -1270,7 +1270,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId // we also send events related to fee Closing.networkFeePaid(tx, d1) map { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) } - // can we close this channel ? + // can we consider this channel closed? val closeType_opt = Closing.isClosed(d1, Some(tx)) closeType_opt match { From 6867e03c9c57368e4641feffd4e6fb64ed28f3d6 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 19 Apr 2019 10:44:36 +0200 Subject: [PATCH 16/19] removed unnecessary blank lines --- .../src/main/scala/fr/acinq/eclair/channel/Helpers.scala | 6 ------ 1 file changed, 6 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index c40b78d415..3a33760a4f 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -368,22 +368,16 @@ object Helpers { def isClosed(data: HasCommitments, closingTx: Option[Transaction]): Option[ClosingType] = data match { case closing: DATA_CLOSING if closingTx.isDefined && closing.mutualClosePublished.exists(_.txid == closingTx.get.txid) => Some(MutualClose) - case closing: DATA_CLOSING if closing.localCommitPublished.exists(Closing.isLocalCommitDone(_)) => Some(LocalClose) - case closing: DATA_CLOSING if closing.remoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => Some(RemoteClose) - case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => Some(RemoteClose) - case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => Some(RecoveryClose) - case closing: DATA_CLOSING if closing.revokedCommitPublished.exists(Closing.isRevokedCommitDone(_)) => Some(RevokedClose) - case _ => None } From c9b1d1f82073f6433c08eba03b1cbb533908adf9 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 19 Apr 2019 10:56:14 +0200 Subject: [PATCH 17/19] removed unused code --- .../scala/fr/acinq/eclair/channel/Channel.scala | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 459da416af..c9c016b3d8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -1258,21 +1258,13 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId .onchainOutgoingHtlcs(d.commitments.localCommit, d.commitments.remoteCommit, d.commitments.remoteNextCommitInfo.left.toOption.map(_.nextRemoteCommit), tx) .map(add => (add, d.commitments.originChannels.get(add.id).collect { case Local(id, _) => id })) // we resolve the payment id if this was a local payment .collect { case (add, Some(id)) => context.system.eventStream.publish(PaymentSettlingOnChain(id, amount = MilliSatoshi(add.amountMsat), add.paymentHash)) } - // then let's see if any of the possible close scenarii can be considered done - val mutualCloseDone = d.mutualClosePublished.exists(_.txid == tx.txid) // this case is trivial, in a mutual close scenario we only need to make sure that one of the closing txes is confirmed - val localCommitDone = localCommitPublished1.map(Closing.isLocalCommitDone(_)).getOrElse(false) - val remoteCommitDone = remoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) - val nextRemoteCommitDone = nextRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) - val futureRemoteCommitDone = futureRemoteCommitPublished1.map(Closing.isRemoteCommitDone(_)).getOrElse(false) - val revokedCommitDone = revokedCommitPublished1.map(Closing.isRevokedCommitDone(_)).exists(_ == true) // we only need one revoked commit done - // finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state) + // we update the channel data val d1 = d.copy(localCommitPublished = localCommitPublished1, remoteCommitPublished = remoteCommitPublished1, nextRemoteCommitPublished = nextRemoteCommitPublished1, futureRemoteCommitPublished = futureRemoteCommitPublished1, revokedCommitPublished = revokedCommitPublished1) - // we also send events related to fee + // and we also send events related to fee Closing.networkFeePaid(tx, d1) map { case (fee, desc) => feePaid(fee, tx, desc, d.channelId) } - - // can we consider this channel closed? + // then let's see if any of the possible close scenarii can be considered done val closeType_opt = Closing.isClosed(d1, Some(tx)) - + // finally, if one of the unilateral closes is done, we move to CLOSED state, otherwise we stay (note that we don't store the state) closeType_opt match { case Some(closeType) => log.info(s"channel closed (type=$closeType)") From bd3c2188cbb73676f1c3e9329b313552533194a6 Mon Sep 17 00:00:00 2001 From: pm47 Date: Fri, 19 Apr 2019 11:03:04 +0200 Subject: [PATCH 18/19] refactored `isClosed` --- .../fr/acinq/eclair/channel/Helpers.scala | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala index 3a33760a4f..af4642b898 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala @@ -360,23 +360,25 @@ object Helpers { data.commitments.remoteNextCommitInfo.isRight /** - * Check if a channel is closed (i.e. its closing tx has been confirmed) + * Checks if a channel is closed (i.e. its closing tx has been confirmed) + * * @param data channel state data - * @param closingTx closing transaction, needed in case of mutual close + * @param additionalConfirmedTx_opt additional confirmed transaction; we need this for the mutual close scenario + * because we don't store the closing tx in the channel state * @return the channel closing type, if applicable */ - def isClosed(data: HasCommitments, closingTx: Option[Transaction]): Option[ClosingType] = data match { - case closing: DATA_CLOSING if closingTx.isDefined && closing.mutualClosePublished.exists(_.txid == closingTx.get.txid) => + def isClosed(data: HasCommitments, additionalConfirmedTx_opt: Option[Transaction]): Option[ClosingType] = data match { + case closing: DATA_CLOSING if additionalConfirmedTx_opt.exists(closing.mutualClosePublished.contains) => Some(MutualClose) - case closing: DATA_CLOSING if closing.localCommitPublished.exists(Closing.isLocalCommitDone(_)) => + case closing: DATA_CLOSING if closing.localCommitPublished.exists(Closing.isLocalCommitDone) => Some(LocalClose) - case closing: DATA_CLOSING if closing.remoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => + case closing: DATA_CLOSING if closing.remoteCommitPublished.exists(Closing.isRemoteCommitDone) => Some(RemoteClose) - case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => + case closing: DATA_CLOSING if closing.nextRemoteCommitPublished.exists(Closing.isRemoteCommitDone) => Some(RemoteClose) - case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.exists(Closing.isRemoteCommitDone(_)) => + case closing: DATA_CLOSING if closing.futureRemoteCommitPublished.exists(Closing.isRemoteCommitDone) => Some(RecoveryClose) - case closing: DATA_CLOSING if closing.revokedCommitPublished.exists(Closing.isRevokedCommitDone(_)) => + case closing: DATA_CLOSING if closing.revokedCommitPublished.exists(Closing.isRevokedCommitDone) => Some(RevokedClose) case _ => None } From da916136269ba003eed7a1f3bfddbe077469bc5c Mon Sep 17 00:00:00 2001 From: sstone Date: Fri, 19 Apr 2019 16:32:22 +0200 Subject: [PATCH 19/19] Channels db: clean-up migration code --- .../eclair/db/sqlite/SqliteChannelsDb.scala | 24 +++++++++----- .../eclair/db/SqliteChannelsDbSpec.scala | 32 ++++++++++++++++++- 2 files changed, 47 insertions(+), 9 deletions(-) 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 1e15541d68..5ffd26c51d 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 @@ -16,16 +16,17 @@ package fr.acinq.eclair.db.sqlite -import java.sql.Connection +import java.sql.{Connection, Statement} import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.channel.HasCommitments import fr.acinq.eclair.db.ChannelsDb import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec +import grizzled.slf4j.Logging import scala.collection.immutable.Queue -class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { +class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb with Logging { import SqliteUtils.ExtendedResultSet._ import SqliteUtils._ @@ -33,17 +34,24 @@ class SqliteChannelsDb(sqlite: Connection) extends ChannelsDb { val DB_NAME = "channels" val CURRENT_VERSION = 2 + private def migration12(statement: Statement) = { + statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN is_closed BOOLEAN NOT NULL DEFAULT 0") + } + using(sqlite.createStatement()) { statement => getVersion(statement, DB_NAME, CURRENT_VERSION) match { case 1 => - statement.executeUpdate("ALTER TABLE local_channels ADD COLUMN is_closed BOOLEAN NOT NULL DEFAULT 0") + logger.warn(s"migrating db $DB_NAME, found version=1 current=$CURRENT_VERSION") + migration12(statement) setVersion(statement, DB_NAME, CURRENT_VERSION) - case CURRENT_VERSION => () + case CURRENT_VERSION => + statement.execute("PRAGMA foreign_keys = ON") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") + + case unknownVersion => throw new RuntimeException(s"Unknown version of DB $DB_NAME found, version=$unknownVersion") } - statement.execute("PRAGMA foreign_keys = ON") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL, is_closed BOOLEAN NOT NULL DEFAULT 0)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") - statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") } override def addOrUpdateChannel(state: HasCommitments): Unit = { diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala index 76731dfcd0..64b62472e5 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/db/SqliteChannelsDbSpec.scala @@ -18,7 +18,9 @@ package fr.acinq.eclair.db import fr.acinq.bitcoin.ByteVector32 import fr.acinq.eclair.TestConstants +import fr.acinq.eclair.db.sqlite.SqliteUtils.{getVersion, using} import fr.acinq.eclair.db.sqlite.{SqliteChannelsDb, SqlitePendingRelayDb} +import fr.acinq.eclair.wire.ChannelCodecs.stateDataCodec import org.scalatest.FunSuite import org.sqlite.SQLiteException import scodec.bits.ByteVector @@ -63,4 +65,32 @@ class SqliteChannelsDbSpec extends FunSuite { assert(db.listHtlcInfos(channel.channelId, commitNumber).toList == Nil) } -} + test("migrate channel database v1 -> v2") { + val sqlite = TestConstants.sqliteInMemory() + + // create a v1 channels database + using(sqlite.createStatement()) { statement => + getVersion(statement, "channels", 1) + statement.execute("PRAGMA foreign_keys = ON") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS local_channels (channel_id BLOB NOT NULL PRIMARY KEY, data BLOB NOT NULL)") + statement.executeUpdate("CREATE TABLE IF NOT EXISTS htlc_infos (channel_id BLOB NOT NULL, commitment_number BLOB NOT NULL, payment_hash BLOB NOT NULL, cltv_expiry INTEGER NOT NULL, FOREIGN KEY(channel_id) REFERENCES local_channels(channel_id))") + statement.executeUpdate("CREATE INDEX IF NOT EXISTS htlc_infos_idx ON htlc_infos(channel_id, commitment_number)") + } + + // insert 1 row + val channel = ChannelStateSpec.normal + val data = stateDataCodec.encode(channel).require.toByteArray + using(sqlite.prepareStatement("INSERT INTO local_channels VALUES (?, ?)")) { statement => + statement.setBytes(1, channel.channelId.toArray) + statement.setBytes(2, data) + statement.executeUpdate() + } + + // check that db migration works + val db = new SqliteChannelsDb(sqlite) + using(sqlite.createStatement()) { statement => + assert(getVersion(statement, "channels", 1) == 2) // version changed from 1 -> 2 + } + assert(db.listLocalChannels() === List(channel)) + } +} \ No newline at end of file