From ac92fcd324e7f70d2205287833aa1d391474d5ca Mon Sep 17 00:00:00 2001 From: sstone Date: Wed, 17 Apr 2019 18:06:14 +0200 Subject: [PATCH] Electrum: do not persist transaction locks Locks hold 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 lokcs 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 a9928cfbe7..6534bee373 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 d0468e8632..b6241e9dac 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])) } } }