1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-22 14:22:39 +01:00

Electrum fixes and improvements (#873)

* Electrum: don't ask for merkle proofs for unconfirmed txs

* Electrum: clear status when we get disconnected and still have pending history transactions

When we get disconnected and have script hashes for which we still have pending connections,
clear the script hash status. When we reconnect we will ask for its history again, this way we
won't miss anything. Since we rotate keys it should not result in heavy traffic (script hashes have
few history items).

* Electrum: represent and persist block heights as 32 bits signed ints

Int.MaxValue is about 40,000 years of block which should be enough, and it will fix the encoding
problem users on testnet when there's a reorg and one of their txs has a height of -1.

Side-note: changing the wallet codec can be done easily: if we cannot read and decode persisted data
we just start with an empty wallet and retrieve all wallet data from electrum servers, and once it's
ready it will be encoded with the new codec and saved.

* Electrum persistence: include a version number

It provides a clean way, when upgrading the app, of choosing whether to keep the same version and start from the
last persisted wallet (if the persistence format has not been changed or is compatible with the old one), or to
change the version and force starting from an empty wallet and downloading all wallet items from Electrum servers.

* ElectrumClient: remove useless buffer
This commit is contained in:
Fabrice Drouin 2019-02-19 11:06:36 +01:00 committed by Pierre-Marie Padiou
parent aeb4360464
commit c0af665990
5 changed files with 56 additions and 21 deletions

View file

@ -282,12 +282,12 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
val (height, header) = parseBlockHeader(json.result)
log.debug("connected to server={}, tip={} height={}", serverAddress, header.hash, height)
statusListeners.map(_ ! ElectrumReady(height, header, serverAddress))
context become connected(ctx, height, header, "", Map())
context become connected(ctx, height, header, Map())
case AddStatusListener(actor) => statusListeners += actor
}
def connected(ctx: ChannelHandlerContext, height: Int, tip: BlockHeader, buffer: String, requests: Map[String, (Request, ActorRef)]): Receive = {
def connected(ctx: ChannelHandlerContext, height: Int, tip: BlockHeader, requests: Map[String, (Request, ActorRef)]): Receive = {
case AddStatusListener(actor) =>
statusListeners += actor
actor ! ElectrumReady(height, tip, serverAddress)
@ -308,7 +308,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
context watch actor
case _ => ()
}
context become connected(ctx, height, tip, buffer, requests + (curReqId -> (request, sender())))
context become connected(ctx, height, tip, requests + (curReqId -> (request, sender())))
case Right(json: JsonRPCResponse) =>
requests.get(json.id) match {
@ -319,7 +319,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
case None =>
log.warning("server={} could not find requestor for reqId=${} response={}", serverAddress, json.id, json)
}
context become connected(ctx, height, tip, buffer, requests - json.id)
context become connected(ctx, height, tip, requests - json.id)
case Left(response: HeaderSubscriptionResponse) => headerSubscriptions.map(_ ! response)
@ -329,7 +329,7 @@ class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL)(implicit val ec
case HeaderSubscriptionResponse(height, newtip) =>
log.info("server={} new tip={}", serverAddress, newtip)
context become connected(ctx, height, newtip, buffer, requests)
context become connected(ctx, height, newtip, requests)
}
}

View file

@ -127,8 +127,13 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
pendingTransactionRequests = Set(),
pendingTransactions = persisted.pendingTransactions,
lastReadyMessage = None)
case _ =>
log.info("starting with a default wallet")
case Success(None) =>
log.info(s"wallet db is empty, starting with a default wallet")
val firstAccountKeys = (0 until params.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector
val firstChangeKeys = (0 until params.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector
Data(params, blockchain1, firstAccountKeys, firstChangeKeys)
case Failure(exception) =>
log.info(s"cannot read wallet db ($exception), starting with a default wallet")
val firstAccountKeys = (0 until params.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector
val firstChangeKeys = (0 until params.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector
Data(params, blockchain1, firstAccountKeys, firstChangeKeys)
@ -331,9 +336,11 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
case (Some(previousHeight), height) if previousHeight != height =>
// there was a reorg
context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations, data.computeTimestamp(txid, params.walletDb)))
downloadHeadersIfMissing(height.toInt)
client ! GetMerkle(txid, height.toInt)
case (Some(previousHeight), height) if previousHeight == height && data.proofs.get(txid).isEmpty =>
if (height > 0) {
downloadHeadersIfMissing(height.toInt)
client ! GetMerkle(txid, height.toInt)
}
case (Some(previousHeight), height) if previousHeight == height && height > 0 && data.proofs.get(txid).isEmpty =>
downloadHeadersIfMissing(height.toInt)
client ! GetMerkle(txid, height.toInt)
case (Some(previousHeight), height) if previousHeight == height =>
@ -440,7 +447,10 @@ class ElectrumWallet(seed: BinaryData, client: ActorRef, params: ElectrumWallet.
case Event(ElectrumClient.ElectrumDisconnected, data) =>
log.info(s"wallet got disconnected")
// remove status for each script hash for which we have pending requests
// this will make us query script hash history for these script hashes again when we reconnect
goto(DISCONNECTED) using data.copy(
status = data.status -- data.pendingHistoryRequests,
pendingHistoryRequests = Set(),
pendingTransactionRequests = Set(),
pendingHeadersRequests = Set(),
@ -650,8 +660,7 @@ object ElectrumWallet {
* @param blockchain blockchain
* @param accountKeys account keys
* @param changeKeys change keys
* @param status script hash -> status; "" means that the script hash has not been used
* yet
* @param status script hash -> status; "" means that the script hash has not been used yet
* @param transactions wallet transactions
* @param heights transactions heights
* @param history script hash -> history
@ -665,7 +674,7 @@ object ElectrumWallet {
changeKeys: Vector[ExtendedPrivateKey],
status: Map[BinaryData, String],
transactions: Map[BinaryData, Transaction],
heights: Map[BinaryData, Long],
heights: Map[BinaryData, Int],
history: Map[BinaryData, List[ElectrumClient.TransactionHistoryItem]],
proofs: Map[BinaryData, GetMerkleResponse],
locks: Set[Transaction],
@ -1000,7 +1009,7 @@ object ElectrumWallet {
}
history + (scriptHash -> entry)
}
this.copy(locks = this.locks - tx, transactions = this.transactions + (tx.txid -> tx), heights = this.heights + (tx.txid -> 0L), history = history1)
this.copy(locks = this.locks - tx, transactions = this.transactions + (tx.txid -> tx), heights = this.heights + (tx.txid -> 0), history = history1)
}
/**
@ -1038,7 +1047,7 @@ object ElectrumWallet {
changeKeysCount: Int,
status: Map[BinaryData, String],
transactions: Map[BinaryData, Transaction],
heights: Map[BinaryData, Long],
heights: Map[BinaryData, Int],
history: Map[BinaryData, List[ElectrumClient.TransactionHistoryItem]],
proofs: Map[BinaryData, GetMerkleResponse],
pendingTransactions: List[Transaction],

View file

@ -160,10 +160,10 @@ object SqliteWalletDb {
(wire: BitVector) => statusListCodec.decode(wire).map(_.map(_.toMap))
)
val heightsListCodec: Codec[List[(BinaryData, Long)]] = listOfN(uint16, binarydata(32) ~ uint32)
val heightsListCodec: Codec[List[(BinaryData, Int)]] = listOfN(uint16, binarydata(32) ~ int32)
val heightsCodec: Codec[Map[BinaryData, Long]] = Codec[Map[BinaryData, Long]](
(map: Map[BinaryData, Long]) => heightsListCodec.encode(map.toList),
val heightsCodec: Codec[Map[BinaryData, Int]] = Codec[Map[BinaryData, Int]](
(map: Map[BinaryData, Int]) => heightsListCodec.encode(map.toList),
(wire: BitVector) => heightsListCodec.decode(wire).map(_.map(_.toMap))
)
@ -194,9 +194,17 @@ object SqliteWalletDb {
(wire: BitVector) => proofsListCodec.decode(wire).map(_.map(_.toMap))
)
/**
* change this value
* -if the new codec is incompatible with the old one
* - OR if you want to force a full sync from Electrum servers
*/
val version = 0x0000
val persistentDataCodec: Codec[PersistentData] = (
("accountKeysCount" | int32) ::
("version" | constant(BitVector.fromInt(version))) ::
("accountKeysCount" | int32) ::
("changeKeysCount" | int32) ::
("status" | statusCodec) ::
("transactions" | transactionsCodec) ::
("heights" | heightsCodec) ::

View file

@ -229,4 +229,20 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
}
wallet ! HeaderSubscriptionResponse(wallet.stateData.blockchain.tip.height + 1, bad)
}
test("clear status when we have pending history requests") {
while (client.msgAvailable) {
client.receiveOne(100 milliseconds)
}
// tell wallet that there is something for our first account key
val scriptHash = ElectrumWallet.computeScriptHashFromPublicKey(wallet.stateData.accountKeys(0).publicKey)
wallet ! ScriptHashSubscriptionResponse(scriptHash, "010101")
client.expectMsg(GetScriptHashHistory(scriptHash))
assert(wallet.stateData.status(scriptHash) == "010101")
// disconnect wallet
wallet ! ElectrumDisconnected
awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED)
assert(wallet.stateData.status.get(scriptHash).isEmpty)
}
}

View file

@ -75,7 +75,9 @@ class SqliteWalletDbSpec extends FunSuite {
0L
)
def randomHistoryItem = ElectrumClient.TransactionHistoryItem(random.nextInt(1000000), randomBytes(32))
def randomHeight = if (random.nextBoolean()) random.nextInt(500000) else -1
def randomHistoryItem = ElectrumClient.TransactionHistoryItem(randomHeight, randomBytes(32))
def randomHistoryItems = (0 to random.nextInt(100)).map(_ => randomHistoryItem).toList
@ -89,7 +91,7 @@ class SqliteWalletDbSpec extends FunSuite {
changeKeysCount = 10,
status = (for (i <- 0 until random.nextInt(100)) yield randomBytes(32) -> random.nextInt(100000).toHexString).toMap,
transactions = transactions.map(tx => tx.hash -> tx).toMap,
heights = transactions.map(tx => tx.hash -> random.nextInt(500000).toLong).toMap,
heights = transactions.map(tx => tx.hash -> randomHeight).toMap,
history = (for (i <- 0 until random.nextInt(100)) yield randomBytes(32) -> randomHistoryItems).toMap,
proofs = (for (i <- 0 until random.nextInt(100)) yield randomBytes(32) -> randomProof).toMap,
pendingTransactions = transactions.toList,