mirror of
https://github.com/ACINQ/eclair.git
synced 2025-02-24 06:47:46 +01:00
Electrum: consistently retrieve wallet transactions (#1010)
* Electrum: update mainnet and testnet servers list * Electrum: request missing history transactions on reconnect Upon connection/reconnection, ask for transactions that are included in our history but which we don't have and don't have a pending request for. * Electrum: add disconnect/reconnect tests Simulate disconnections and check that wallet eventually gets it history and transactions.
This commit is contained in:
parent
92e2d21eab
commit
648f57276a
6 changed files with 303 additions and 86 deletions
|
@ -16,11 +16,6 @@
|
|||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"MEADS.hopto.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"VPS.hsmiths.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
|
@ -69,23 +64,12 @@
|
|||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"btc.smsys.me": {
|
||||
"pruning": "-",
|
||||
"s": "995",
|
||||
"version": "1.4"
|
||||
},
|
||||
"btc.xskyx.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"cashyes.zapto.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"currentlane.lovebitco.in": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
|
@ -121,11 +105,6 @@
|
|||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"elec.luggs.co": {
|
||||
"pruning": "-",
|
||||
"s": "443",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum-server.ninja": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
|
@ -289,17 +268,12 @@
|
|||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"kirsche.emzy.de": {
|
||||
"electrum.emzy.de": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"luggscoqbymhvnkp.onion": {
|
||||
"pruning": "-",
|
||||
"t": "80",
|
||||
"version": "1.4"
|
||||
},
|
||||
"ndnd.selfhost.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
|
@ -396,5 +370,60 @@
|
|||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"bitcoin.dragon.zone": {
|
||||
"pruning": "-",
|
||||
"s": "50004",
|
||||
"t": "50003",
|
||||
"version": "1.4"
|
||||
},
|
||||
"ecdsa.net" : {
|
||||
"pruning": "-",
|
||||
"s": "110",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"btc.usebsv.com": {
|
||||
"pruning": "-",
|
||||
"s": "50006",
|
||||
"version": "1.4"
|
||||
},
|
||||
"e2.keff.org": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum.hodlister.co": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum3.hodlister.co": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrum5.hodlister.co": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"electrumx.electricnewyear.net": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"version": "1.4"
|
||||
},
|
||||
"fortress.qtornado.com": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"green-gold.westeurope.cloudapp.azure.com": {
|
||||
"pruning": "-",
|
||||
"s": "56002",
|
||||
"t": "56001",
|
||||
"version": "1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,31 +1,38 @@
|
|||
{
|
||||
"electrumx.kekku.li": {
|
||||
"pruning": "-",
|
||||
"s": "51002",
|
||||
"version": "1.2"
|
||||
},
|
||||
"hsmithsxurybd7uh.onion": {
|
||||
"pruning": "-",
|
||||
"s": "53012",
|
||||
"t": "53011",
|
||||
"version": "1.2"
|
||||
"version": "1.4"
|
||||
},
|
||||
"testnet.hsmiths.com": {
|
||||
"pruning": "-",
|
||||
"s": "53012",
|
||||
"t": "53011",
|
||||
"version": "1.2"
|
||||
"version": "1.4"
|
||||
},
|
||||
"testnet.qtornado.com": {
|
||||
"pruning": "-",
|
||||
"s": "51002",
|
||||
"t": "51001",
|
||||
"version": "1.2"
|
||||
"version": "1.4"
|
||||
},
|
||||
"testnet1.bauerj.eu": {
|
||||
"pruning": "-",
|
||||
"s": "50002",
|
||||
"t": "50001",
|
||||
"version": "1.2"
|
||||
"version": "1.4"
|
||||
},
|
||||
"tn.not.fyi": {
|
||||
"pruning": "-",
|
||||
"s": "55002",
|
||||
"t": "55001",
|
||||
"version": "1.4"
|
||||
},
|
||||
"bitcoin.cluelessperson.com": {
|
||||
"pruning": "-",
|
||||
"s": "51002",
|
||||
"t": "51001",
|
||||
"version": "1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -336,7 +336,7 @@ object Blockchain extends Logging {
|
|||
*/
|
||||
def getDifficulty(blockchain: Blockchain, height: Int, headerDb: HeaderDb): Option[Long] = {
|
||||
blockchain.chainHash match {
|
||||
case Block.LivenetGenesisBlock.hash | Block.RegtestGenesisBlock.hash =>
|
||||
case Block.LivenetGenesisBlock.hash =>
|
||||
(height % RETARGETING_PERIOD) match {
|
||||
case 0 =>
|
||||
for {
|
||||
|
|
|
@ -26,7 +26,7 @@ import org.json4s.jackson.JsonMethods
|
|||
/**
|
||||
*
|
||||
* @param hash block hash
|
||||
* @param target difficulty target for the next block
|
||||
* @param nextBits difficulty target for the next block
|
||||
*/
|
||||
case class CheckPoint(hash: ByteVector32, nextBits: Long)
|
||||
|
||||
|
|
|
@ -167,7 +167,6 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet.
|
|||
data.accountKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self))
|
||||
data.changeKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self))
|
||||
advertiseTransactions(data)
|
||||
// tell everyone we're ready
|
||||
goto(RUNNING) using persistAndNotify(data)
|
||||
} else {
|
||||
client ! ElectrumClient.GetHeaders(data.blockchain.tip.height + 1, RETARGETING_PERIOD)
|
||||
|
@ -237,7 +236,9 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet.
|
|||
}
|
||||
|
||||
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if data.status.get(scriptHash) == Some(status) =>
|
||||
stay using persistAndNotify(data) // we already have it
|
||||
val missing = data.missingTransactions(scriptHash)
|
||||
missing.foreach(txid => client ! GetTransaction(txid))
|
||||
stay using persistAndNotify(data.copy(pendingHistoryRequests = data.pendingTransactionRequests ++ missing))
|
||||
|
||||
case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if !data.accountKeyMap.contains(scriptHash) && !data.changeKeyMap.contains(scriptHash) =>
|
||||
log.warning(s"received status=$status for scriptHash=$scriptHash which does not match any of our keys")
|
||||
|
@ -379,10 +380,17 @@ class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet.
|
|||
case None =>
|
||||
// missing parents
|
||||
log.info(s"couldn't connect txid=${tx.txid}")
|
||||
val data1 = data.copy(pendingTransactions = data.pendingTransactions :+ tx)
|
||||
val data1 = data.copy(pendingTransactionRequests = data.pendingTransactionRequests - tx.txid, pendingTransactions = data.pendingTransactions :+ tx)
|
||||
stay using persistAndNotify(data1)
|
||||
}
|
||||
|
||||
case Event(ServerError(GetTransaction(txid), error), data) if data.pendingTransactionRequests.contains(txid) =>
|
||||
// server tells us that txid belongs to our wallet history, but cannot provide tx ?
|
||||
log.error(s"server cannot find history tx $txid: $error")
|
||||
sender ! PoisonPill
|
||||
goto(DISCONNECTED) using data
|
||||
|
||||
|
||||
case Event(response@GetMerkleResponse(txid, _, height, _), data) =>
|
||||
data.blockchain.getHeader(height).orElse(params.walletDb.getHeader(height)) match {
|
||||
case Some(header) if header.hashMerkleRoot == response.root =>
|
||||
|
@ -730,6 +738,16 @@ object ElectrumWallet {
|
|||
WalletReady(confirmed, unconfirmed, blockchain.tip.height, blockchain.tip.header.time)
|
||||
}
|
||||
|
||||
/**
|
||||
* @scriptHash script hash
|
||||
* @return the ids of transactions that belong to our wallet history for this script hash but that we don't have
|
||||
* and have no pending requests for.
|
||||
*/
|
||||
def missingTransactions(scriptHash: ByteVector32): Set[ByteVector32] = {
|
||||
val txids = history.getOrElse(scriptHash, List()).map(_.tx_hash).filterNot(txhash => transactions.contains(txhash)).toSet
|
||||
txids -- pendingTransactionRequests
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return the current receive key. In most cases it will be a key that has not
|
||||
|
|
|
@ -22,7 +22,10 @@ import java.sql.DriverManager
|
|||
import akka.actor.{ActorRef, ActorSystem, Terminated}
|
||||
import akka.testkit
|
||||
import akka.testkit.{TestActor, TestFSMRef, TestKit, TestProbe}
|
||||
import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32, MnemonicCode, Satoshi, Transaction, TxOut}
|
||||
import fr.acinq.bitcoin.Crypto.PublicKey
|
||||
import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey
|
||||
import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32, Crypto, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut}
|
||||
import fr.acinq.eclair.blockchain.bitcoind.rpc.Error
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumClient._
|
||||
import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._
|
||||
import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb
|
||||
|
@ -34,6 +37,8 @@ import scala.concurrent.duration._
|
|||
|
||||
|
||||
class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) with FunSuiteLike {
|
||||
|
||||
import ElectrumWalletSimulatedClientSpec._
|
||||
val sender = TestProbe()
|
||||
|
||||
val entropy = ByteVector32(ByteVector.fill(32)(1))
|
||||
|
@ -45,7 +50,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
|
|||
|
||||
val genesis = Block.RegtestGenesisBlock.header
|
||||
// initial headers that we will sync when we connect to our mock server
|
||||
val headers = makeHeaders(genesis, 2016 + 2000)
|
||||
var headers = makeHeaders(genesis, 2016 + 2000)
|
||||
|
||||
val client = TestProbe()
|
||||
client.ignoreMsg {
|
||||
|
@ -65,27 +70,22 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
|
|||
}
|
||||
})
|
||||
|
||||
|
||||
val wallet = TestFSMRef(new ElectrumWallet(seed, client.ref, WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = Satoshi(5000))))
|
||||
val walletParameters = WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = Satoshi(5000))
|
||||
val wallet = TestFSMRef(new ElectrumWallet(seed, client.ref, walletParameters))
|
||||
|
||||
// wallet sends a receive address notification as soon as it is created
|
||||
listener.expectMsgType[NewWalletReceiveAddress]
|
||||
|
||||
def makeHeader(previousHeader: BlockHeader, timestamp: Long): BlockHeader = {
|
||||
var template = previousHeader.copy(hashPreviousBlock = previousHeader.hash, time = timestamp, nonce = 0)
|
||||
while (!BlockHeader.checkProofOfWork(template)) {
|
||||
template = template.copy(nonce = template.nonce + 1)
|
||||
def reconnect: WalletReady = {
|
||||
sender.send(wallet, ElectrumClient.ElectrumReady(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header, InetSocketAddress.createUnresolved("0.0.0.0", 9735)))
|
||||
awaitCond(wallet.stateName == ElectrumWallet.WAITING_FOR_TIP)
|
||||
while (listener.msgAvailable) {
|
||||
listener.receiveOne(100 milliseconds)
|
||||
}
|
||||
template
|
||||
}
|
||||
|
||||
def makeHeader(previousHeader: BlockHeader): BlockHeader = makeHeader(previousHeader, previousHeader.time + 1)
|
||||
|
||||
def makeHeaders(previousHeader: BlockHeader, count: Int): Vector[BlockHeader] = {
|
||||
@tailrec
|
||||
def loop(acc: Vector[BlockHeader]): Vector[BlockHeader] = if (acc.length == count) acc else loop(acc :+ makeHeader(acc.last))
|
||||
|
||||
loop(Vector(makeHeader(previousHeader)))
|
||||
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header))
|
||||
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
|
||||
val ready = listener.expectMsgType[WalletReady]
|
||||
ready
|
||||
}
|
||||
|
||||
test("wait until wallet is ready") {
|
||||
|
@ -102,7 +102,9 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
|
|||
|
||||
test("tell wallet is ready when a new block comes in, even if nothing else has changed") {
|
||||
val last = wallet.stateData.blockchain.tip
|
||||
assert(last.header == headers.last)
|
||||
val header = makeHeader(last.header)
|
||||
headers = headers :+ header
|
||||
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height + 1, header))
|
||||
assert(listener.expectMsgType[WalletReady].timestamp == header.time)
|
||||
val NewWalletReceiveAddress(address) = listener.expectMsgType[NewWalletReceiveAddress]
|
||||
|
@ -116,6 +118,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
|
|||
|
||||
// reconnect wallet
|
||||
val last = wallet.stateData.blockchain.tip
|
||||
assert(last.header == headers.last)
|
||||
sender.send(wallet, ElectrumClient.ElectrumReady(2016, headers(2015), InetSocketAddress.createUnresolved("0.0.0.0", 9735)))
|
||||
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height, last.header))
|
||||
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
|
||||
|
@ -128,7 +131,9 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
|
|||
test("don't send the same ready message more then once") {
|
||||
// listener should be notified
|
||||
val last = wallet.stateData.blockchain.tip
|
||||
assert(last.header == headers.last)
|
||||
val header = makeHeader(last.header)
|
||||
headers = headers :+ header
|
||||
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height + 1, header))
|
||||
assert(listener.expectMsgType[WalletReady].timestamp == header.time)
|
||||
listener.expectMsgType[NewWalletReceiveAddress]
|
||||
|
@ -158,10 +163,7 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
|
|||
watcher.expectTerminated(probe.ref)
|
||||
awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED)
|
||||
|
||||
sender.send(wallet, ElectrumClient.ElectrumReady(last.height, last.header, InetSocketAddress.createUnresolved("0.0.0.0", 9735)))
|
||||
awaitCond(wallet.stateName == ElectrumWallet.WAITING_FOR_TIP)
|
||||
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height, last.header))
|
||||
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
|
||||
reconnect
|
||||
}
|
||||
|
||||
|
||||
|
@ -206,31 +208,10 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
|
|||
watcher.expectTerminated(probe.ref)
|
||||
awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED)
|
||||
|
||||
sender.send(wallet, ElectrumClient.ElectrumReady(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header, InetSocketAddress.createUnresolved("0.0.0.0", 9735)))
|
||||
awaitCond(wallet.stateName == ElectrumWallet.WAITING_FOR_TIP)
|
||||
while (listener.msgAvailable) {
|
||||
listener.receiveOne(100 milliseconds)
|
||||
}
|
||||
sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header))
|
||||
awaitCond(wallet.stateName == ElectrumWallet.RUNNING)
|
||||
val ready = listener.expectMsgType[WalletReady]
|
||||
val ready = reconnect
|
||||
assert(ready.unconfirmedBalance == Satoshi(0))
|
||||
}
|
||||
|
||||
test("disconnect if server sent a block with an invalid difficulty target") {
|
||||
val last = wallet.stateData.blockchain.bestchain.last
|
||||
val chunk = makeHeaders(last.header, 2015 - (last.height % 2016))
|
||||
for (i <- 0 until chunk.length) {
|
||||
wallet ! HeaderSubscriptionResponse(last.height + i + 1, chunk(i))
|
||||
}
|
||||
awaitCond(wallet.stateData.blockchain.tip.header == chunk.last)
|
||||
val bad = {
|
||||
var template = makeHeader(chunk.last)
|
||||
template
|
||||
}
|
||||
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)
|
||||
|
@ -245,5 +226,187 @@ class ElectrumWalletSimulatedClientSpec extends TestKit(ActorSystem("test")) wit
|
|||
wallet ! ElectrumDisconnected
|
||||
awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED)
|
||||
assert(wallet.stateData.status.get(scriptHash).isEmpty)
|
||||
|
||||
reconnect
|
||||
}
|
||||
|
||||
test("handle pending transaction requests") {
|
||||
while (client.msgAvailable) {
|
||||
client.receiveOne(100 milliseconds)
|
||||
}
|
||||
val key = wallet.stateData.accountKeys(1)
|
||||
val scriptHash = computeScriptHashFromPublicKey(key.publicKey)
|
||||
wallet ! ScriptHashSubscriptionResponse(scriptHash, ByteVector32(ByteVector.fill(32)(2)).toHex)
|
||||
client.expectMsg(GetScriptHashHistory(scriptHash))
|
||||
|
||||
val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Satoshi(100000), ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0)
|
||||
wallet ! GetScriptHashHistoryResponse(scriptHash, TransactionHistoryItem(2, tx.txid) :: Nil)
|
||||
|
||||
// wallet will generate a new address and the corresponding subscription
|
||||
client.expectMsgType[ScriptHashSubscription]
|
||||
|
||||
while (listener.msgAvailable) {
|
||||
listener.receiveOne(100 milliseconds)
|
||||
}
|
||||
|
||||
client.expectMsg(GetTransaction(tx.txid))
|
||||
assert(wallet.stateData.pendingTransactionRequests == Set(tx.txid))
|
||||
}
|
||||
|
||||
test("handle disconnect/reconnect events") {
|
||||
val data = {
|
||||
val master = DeterministicWallet.generate(seed)
|
||||
val accountMaster = ElectrumWallet.accountKey(master, walletParameters.chainHash)
|
||||
val changeMaster = ElectrumWallet.changeKey(master, walletParameters.chainHash)
|
||||
val firstAccountKeys = (0 until walletParameters.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector
|
||||
val firstChangeKeys = (0 until walletParameters.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector
|
||||
val data1 = Data(walletParameters, Blockchain.fromGenesisBlock(Block.RegtestGenesisBlock.hash, Block.RegtestGenesisBlock.header), firstAccountKeys, firstChangeKeys)
|
||||
|
||||
val amount1 = Satoshi(1000000)
|
||||
val amount2 = Satoshi(1500000)
|
||||
|
||||
// transactions that send funds to our wallet
|
||||
val wallettxs = Seq(
|
||||
addOutputs(emptyTx, amount1, data1.accountKeys(0).publicKey),
|
||||
addOutputs(emptyTx, amount2, data1.accountKeys(1).publicKey),
|
||||
addOutputs(emptyTx, amount2, data1.accountKeys(2).publicKey),
|
||||
addOutputs(emptyTx, amount2, data1.accountKeys(3).publicKey)
|
||||
)
|
||||
val data2 = wallettxs.foldLeft(data1)(addTransaction)
|
||||
|
||||
// a tx that spend from our wallet to our wallet, plus change to our wallet
|
||||
val tx1 = {
|
||||
val tx = Transaction(version = 2,
|
||||
txIn = TxIn(OutPoint(wallettxs(0), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
|
||||
txOut = walletOutput(wallettxs(0).txOut(0).amount - Satoshi(50000), data2.accountKeys(2).publicKey) :: walletOutput(Satoshi(50000), data2.changeKeys(0).publicKey) :: Nil,
|
||||
lockTime = 0)
|
||||
data2.signTransaction(tx)
|
||||
}
|
||||
|
||||
// a tx that spend from our wallet to a random address, plus change to our wallet
|
||||
val tx2 = {
|
||||
val tx = Transaction(version = 2,
|
||||
txIn = TxIn(OutPoint(wallettxs(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil,
|
||||
txOut = TxOut(wallettxs(1).txOut(0).amount - Satoshi(50000), Script.pay2wpkh(fr.acinq.eclair.randomKey.publicKey)) :: walletOutput(Satoshi(50000), data2.changeKeys(1).publicKey) :: Nil,
|
||||
lockTime = 0)
|
||||
data2.signTransaction(tx)
|
||||
}
|
||||
val data3 = Seq(tx1, tx2).foldLeft(data2)(addTransaction)
|
||||
data3
|
||||
}
|
||||
|
||||
// simulated electrum server that disconnects after a given number of messages
|
||||
|
||||
var counter = 0
|
||||
var disconnectAfter = 10 // disconnect when counter % disconnectAfter == 0
|
||||
|
||||
client.setAutoPilot(new testkit.TestActor.AutoPilot {
|
||||
override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = {
|
||||
counter = msg match {
|
||||
case _:ScriptHashSubscription => counter
|
||||
case _ => counter + 1
|
||||
}
|
||||
msg match {
|
||||
case ScriptHashSubscription(scriptHash, replyTo) =>
|
||||
// we skip over these otherwise we would never converge (there are at least 20 such messages sent when we're
|
||||
// reconnected, one for each account/change key)
|
||||
replyTo ! ScriptHashSubscriptionResponse(scriptHash, data.status.getOrElse(scriptHash, ""))
|
||||
TestActor.KeepRunning
|
||||
case msg if counter % disconnectAfter == 0 =>
|
||||
// disconnect
|
||||
sender ! ElectrumClient.ElectrumDisconnected
|
||||
// and reconnect
|
||||
sender ! ElectrumClient.ElectrumReady(headers.length, headers.last, InetSocketAddress.createUnresolved("0.0.0.0", 9735))
|
||||
sender ! ElectrumClient.HeaderSubscriptionResponse(headers.length, headers.last)
|
||||
TestActor.KeepRunning
|
||||
case request@GetTransaction(txid) =>
|
||||
data.transactions.get(txid) match {
|
||||
case Some(tx) => sender ! GetTransactionResponse(tx)
|
||||
case None =>
|
||||
sender ! ServerError(request, Error(0, s"unknwown tx $txid"))
|
||||
}
|
||||
TestActor.KeepRunning
|
||||
case GetScriptHashHistory(scriptHash) =>
|
||||
sender ! GetScriptHashHistoryResponse(scriptHash, data.history.getOrElse(scriptHash, List()))
|
||||
TestActor.KeepRunning
|
||||
case GetHeaders(start, count, _) =>
|
||||
sender ! GetHeadersResponse(start, headers.drop(start - 1).take(count), 2016)
|
||||
TestActor.KeepRunning
|
||||
case HeaderSubscription(actor) => actor ! HeaderSubscriptionResponse(headers.length, headers.last)
|
||||
TestActor.KeepRunning
|
||||
case _ =>
|
||||
TestActor.KeepRunning
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
val sender = TestProbe()
|
||||
wallet ! ElectrumClient.ElectrumDisconnected
|
||||
wallet ! ElectrumClient.ElectrumReady(headers.length, headers.last, InetSocketAddress.createUnresolved("0.0.0.0", 9735))
|
||||
wallet ! ElectrumClient.HeaderSubscriptionResponse(headers.length, headers.last)
|
||||
|
||||
data.status.foreach { case (scriptHash, status) => sender.send(wallet, ScriptHashSubscriptionResponse(scriptHash, status)) }
|
||||
|
||||
val expected = data.transactions.keySet
|
||||
awaitCond {
|
||||
wallet.stateData.transactions.keySet == expected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ElectrumWalletSimulatedClientSpec {
|
||||
def makeHeader(previousHeader: BlockHeader, timestamp: Long): BlockHeader = {
|
||||
var template = previousHeader.copy(hashPreviousBlock = previousHeader.hash, time = timestamp, nonce = 0)
|
||||
while (!BlockHeader.checkProofOfWork(template)) {
|
||||
template = template.copy(nonce = template.nonce + 1)
|
||||
}
|
||||
template
|
||||
}
|
||||
|
||||
def makeHeader(previousHeader: BlockHeader): BlockHeader = makeHeader(previousHeader, previousHeader.time + 1)
|
||||
|
||||
def makeHeaders(previousHeader: BlockHeader, count: Int): Vector[BlockHeader] = {
|
||||
@tailrec
|
||||
def loop(acc: Vector[BlockHeader]): Vector[BlockHeader] = if (acc.length == count) acc else loop(acc :+ makeHeader(acc.last))
|
||||
|
||||
loop(Vector(makeHeader(previousHeader)))
|
||||
}
|
||||
|
||||
val emptyTx = Transaction(version = 2, txIn = Nil, txOut = Nil, lockTime = 0)
|
||||
|
||||
def walletOutput(amount: Satoshi, key: PublicKey) = TxOut(amount, ElectrumWallet.computePublicKeyScript(key))
|
||||
|
||||
def addOutputs(tx: Transaction, amount: Satoshi, keys: PublicKey*): Transaction = keys.foldLeft(tx) { case (t, k) => t.copy(txOut = t.txOut :+ walletOutput(amount, k)) }
|
||||
|
||||
def addToHistory(history: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]], scriptHash: ByteVector32, item : TransactionHistoryItem): Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]] = {
|
||||
history.get(scriptHash) match {
|
||||
case None => history + (scriptHash -> List(item))
|
||||
case Some(items) if items.contains(item) => history
|
||||
case _ => history.updated(scriptHash, history(scriptHash) :+ item)
|
||||
}
|
||||
}
|
||||
|
||||
def updateStatus(data: ElectrumWallet.Data) : ElectrumWallet.Data = {
|
||||
val status1 = data.history.mapValues(items => {
|
||||
val status = items.map(i => s"${i.tx_hash}:${i.height}:").mkString("")
|
||||
Crypto.sha256(ByteVector.view(status.getBytes())).toString()
|
||||
})
|
||||
data.copy(status = status1)
|
||||
}
|
||||
|
||||
def addTransaction(data: ElectrumWallet.Data, tx: Transaction) : ElectrumWallet.Data = {
|
||||
data.transactions.get(tx.txid) match {
|
||||
case Some(_) => data
|
||||
case None =>
|
||||
val history1 = tx.txOut.filter(o => data.isMine(o)).foldLeft(data.history) { case (a, b) =>
|
||||
addToHistory(a, Crypto.sha256(b.publicKeyScript).reverse, TransactionHistoryItem(100000, tx.txid))
|
||||
}
|
||||
val data1 = data.copy(history = history1, transactions = data.transactions + (tx.txid -> tx))
|
||||
val history2 = tx.txIn.filter(i => data1.isMine(i)).foldLeft(data1.history) { case (a, b) =>
|
||||
addToHistory(a, ElectrumWallet.computeScriptHashFromPublicKey(extractPubKeySpentFrom(b).get), TransactionHistoryItem(100000, tx.txid))
|
||||
}
|
||||
val data2 = data1.copy(history = history2)
|
||||
updateStatus(data2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue