From 6afe28d147015137611426b3d086f58d10db0707 Mon Sep 17 00:00:00 2001 From: Fabrice Drouin Date: Wed, 17 Apr 2019 19:10:14 +0200 Subject: [PATCH] Electrum: do not persist transaction locks (#953) Locks held on utxos that are used in unpublished funding transactions should not be persisted. If the app is stopped before the funding transaction has been published the channel is forgotten and so should be locks on its funding tx utxos. --- .../electrum/db/sqlite/SqliteWalletDb.scala | 2 +- .../db/sqlite/SqliteWalletDbSpec.scala | 97 ++++++++++++------- 2 files changed, 63 insertions(+), 36 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala index a9928cfbe..6534bee37 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala @@ -211,7 +211,7 @@ object SqliteWalletDb { ("history" | historyCodec) :: ("proofs" | proofsCodec) :: ("pendingTransactions" | listOfN(uint16, txCodec)) :: - ("locks" | setCodec(txCodec))).as[PersistentData] + ("locks" | provide(Set.empty[Transaction]))).as[PersistentData] def serialize(data: PersistentData): Array[Byte] = persistentDataCodec.encode(data).require.toByteArray diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala index d0468e863..b6241e9da 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala @@ -17,11 +17,16 @@ package fr.acinq.eclair.blockchain.electrum.db.sqlite import fr.acinq.bitcoin.{Block, BlockHeader, OutPoint, Satoshi, Transaction, TxIn, TxOut} -import fr.acinq.eclair.TestConstants +import fr.acinq.eclair.{TestConstants, randomBytes, randomBytes32} import fr.acinq.eclair.blockchain.electrum.ElectrumClient import fr.acinq.eclair.blockchain.electrum.ElectrumClient.GetMerkleResponse import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.PersistentData +import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb.version +import fr.acinq.eclair.wire.ChannelCodecs.txCodec import org.scalatest.FunSuite +import scodec.Codec +import scodec.bits.BitVector +import scodec.codecs.{constant, listOfN, provide, uint16} import scala.util.Random @@ -34,6 +39,36 @@ class SqliteWalletDbSpec extends FunSuite { if (acc.size == n) acc else makeHeaders(n, acc :+ makeChildHeader(acc.last)) } + def randomTransaction = Transaction(version = 2, + txIn = TxIn(OutPoint(randomBytes32, random.nextInt(100)), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, + txOut = TxOut(Satoshi(random.nextInt(10000000)), randomBytes(20)) :: Nil, + 0L + ) + + def randomHeight = if (random.nextBoolean()) random.nextInt(500000) else -1 + + def randomHistoryItem = ElectrumClient.TransactionHistoryItem(randomHeight, randomBytes32) + + def randomHistoryItems = (0 to random.nextInt(100)).map(_ => randomHistoryItem).toList + + def randomProof = GetMerkleResponse(randomBytes32, ((0 until 10).map(_ => randomBytes32)).toList, random.nextInt(100000), 0) + + def randomPersistentData = { + val transactions = for (i <- 0 until random.nextInt(100)) yield randomTransaction + + PersistentData( + accountKeysCount = 10, + changeKeysCount = 10, + status = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> random.nextInt(100000).toHexString).toMap, + transactions = transactions.map(tx => tx.hash -> tx).toMap, + heights = transactions.map(tx => tx.hash -> randomHeight).toMap, + history = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> randomHistoryItems).toMap, + proofs = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> randomProof).toMap, + pendingTransactions = transactions.toList, + locks = (for (i <- 0 until random.nextInt(10)) yield randomTransaction).toSet + ) + } + test("add/get/list headers") { val db = new SqliteWalletDb(TestConstants.sqliteInMemory()) val headers = makeHeaders(100) @@ -59,46 +94,38 @@ class SqliteWalletDbSpec extends FunSuite { test("serialize persistent data") { val db = new SqliteWalletDb(TestConstants.sqliteInMemory()) - - import fr.acinq.eclair.{randomBytes, randomBytes32} - - def randomTransaction = Transaction(version = 2, - txIn = TxIn(OutPoint(randomBytes32, random.nextInt(100)), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(Satoshi(random.nextInt(10000000)), randomBytes(20)) :: Nil, - 0L - ) - - def randomHeight = if (random.nextBoolean()) random.nextInt(500000) else -1 - - def randomHistoryItem = ElectrumClient.TransactionHistoryItem(randomHeight, randomBytes32) - - def randomHistoryItems = (0 to random.nextInt(100)).map(_ => randomHistoryItem).toList - - def randomProof = GetMerkleResponse(randomBytes32, ((0 until 10).map(_ => randomBytes32)).toList, random.nextInt(100000), 0) - - def randomPersistentData = { - val transactions = for (i <- 0 until random.nextInt(100)) yield randomTransaction - - PersistentData( - accountKeysCount = 10, - changeKeysCount = 10, - status = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> random.nextInt(100000).toHexString).toMap, - transactions = transactions.map(tx => tx.hash -> tx).toMap, - heights = transactions.map(tx => tx.hash -> randomHeight).toMap, - history = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> randomHistoryItems).toMap, - proofs = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> randomProof).toMap, - pendingTransactions = transactions.toList, - locks = (for (i <- 0 until random.nextInt(10)) yield randomTransaction).toSet - ) - } - assert(db.readPersistentData() == None) for (i <- 0 until 50) { val data = randomPersistentData db.persist(data) val Some(check) = db.readPersistentData() - assert(check === data) + assert(check === data.copy(locks = Set.empty[Transaction])) + } + } + + test("read old persistent data") { + import scodec.codecs._ + import SqliteWalletDb._ + import fr.acinq.eclair.wire.ChannelCodecs._ + + val oldPersistentDataCodec: Codec[PersistentData] = ( + ("version" | constant(BitVector.fromInt(version))) :: + ("accountKeysCount" | int32) :: + ("changeKeysCount" | int32) :: + ("status" | statusCodec) :: + ("transactions" | transactionsCodec) :: + ("heights" | heightsCodec) :: + ("history" | historyCodec) :: + ("proofs" | proofsCodec) :: + ("pendingTransactions" | listOfN(uint16, txCodec)) :: + ("locks" | setCodec(txCodec))).as[PersistentData] + + for (i <- 0 until 50) { + val data = randomPersistentData + val encoded = oldPersistentDataCodec.encode(data).require + val decoded = persistentDataCodec.decode(encoded).require.value + assert(decoded === data.copy(locks = Set.empty[Transaction])) } } }