1
0
mirror of https://github.com/ACINQ/eclair.git synced 2024-11-20 02:27:32 +01:00

Test preimage extraction from mempool (#1387)

Instead of waiting for htlc-success txs to be confirmed, eclair also looks
at mempool txs to detect preimages as soon as possible.

This has been the case for a very long time, but our integration tests
didn't showcase this correctly.

Refactored common watcher test helpers and added tests to
ZmqWatcherSpec.
This commit is contained in:
Bastien Teinturier 2020-04-27 18:22:07 +02:00 committed by GitHub
parent 22d476774d
commit 3809b023c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 213 additions and 121 deletions

View File

@ -16,28 +16,96 @@
package fr.acinq.eclair.blockchain package fr.acinq.eclair.blockchain
import fr.acinq.bitcoin.Transaction import akka.actor.{ActorRef, ActorSystem}
import org.scalatest.funsuite.AnyFunSuite import akka.testkit.TestProbe
import fr.acinq.bitcoin.Crypto.PrivateKey
import fr.acinq.bitcoin.{Base58, OutPoint, SIGHASH_ALL, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
import fr.acinq.eclair.LongToBtcAmount
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import org.json4s.JsonAST.{JString, JValue}
import org.scalatest.funsuite.AnyFunSuiteLike
/** /**
* Created by PM on 27/01/2017. * Created by PM on 27/01/2017.
*/ */
class WatcherSpec extends AnyFunSuite { class WatcherSpec extends AnyFunSuiteLike {
test("extract pay2wpkh pubkey script") { test("extract pay2wpkh pubkey script") {
val commitTx = Transaction.read("020000000001010ba75314a116c1e585d1454d079598c5f00edc8a21ebd9e4f3b64e5c318ff2a30100000000e832a680012e850100000000001600147d2a3fc37dba8e946e0238d7eeb6fb602be658200400473044022010d4f249861bb9828ddfd2cda91dc10b8f8ffd0f15c8a4a85a2d373d52f5e0ff02205356242878121676e3e823ceb3dc075d18fed015053badc8f8d754b8959a9178014730440220521002cf241311facf541b689e7229977bfceffa0e4ded785b4e6197af80bfa202204a168d1f7ee59c73ae09c3e0a854b20262b9969fe4ed69b15796dca3ea286582014752210365375134360808be0b4756ba8a2995488310ac4c69571f2b600aaba3ec6cc2d32103a0d9c18794f16dfe01d6d6716bcd1e97ecff2f39451ec48e1899af40f20a18bc52aec3dd9520") val commitTx = Transaction.read("020000000001010ba75314a116c1e585d1454d079598c5f00edc8a21ebd9e4f3b64e5c318ff2a30100000000e832a680012e850100000000001600147d2a3fc37dba8e946e0238d7eeb6fb602be658200400473044022010d4f249861bb9828ddfd2cda91dc10b8f8ffd0f15c8a4a85a2d373d52f5e0ff02205356242878121676e3e823ceb3dc075d18fed015053badc8f8d754b8959a9178014730440220521002cf241311facf541b689e7229977bfceffa0e4ded785b4e6197af80bfa202204a168d1f7ee59c73ae09c3e0a854b20262b9969fe4ed69b15796dca3ea286582014752210365375134360808be0b4756ba8a2995488310ac4c69571f2b600aaba3ec6cc2d32103a0d9c18794f16dfe01d6d6716bcd1e97ecff2f39451ec48e1899af40f20a18bc52aec3dd9520")
val claimMainTx = Transaction.read("020000000001012537488e9d066a8f3550cc9adc141a11668425e046e69e07f53bb831f3296cbf00000000000000000001bf8401000000000017a9143f398d81d3c42367b779ea869c7dd3b6826fbb7487024730440220477b961f6360ef6cb62a76898dcecbb130627c7e6a452646e3be601f04627c1f02202572313d0c0afecbfb0c7d0e47ba689427a54f3debaded6d406daa1f5da4918c01210291ed78158810ad867465377f5920036ea865a29b3a39a1b1808d0c3c351a4b4100000000") val claimMainTx = Transaction.read("020000000001012537488e9d066a8f3550cc9adc141a11668425e046e69e07f53bb831f3296cbf00000000000000000001bf8401000000000017a9143f398d81d3c42367b779ea869c7dd3b6826fbb7487024730440220477b961f6360ef6cb62a76898dcecbb130627c7e6a452646e3be601f04627c1f02202572313d0c0afecbfb0c7d0e47ba689427a54f3debaded6d406daa1f5da4918c01210291ed78158810ad867465377f5920036ea865a29b3a39a1b1808d0c3c351a4b4100000000")
assert(commitTx.txOut.head.publicKeyScript === WatchConfirmed.extractPublicKeyScript(claimMainTx.txIn.head.witness)) assert(commitTx.txOut.head.publicKeyScript === WatchConfirmed.extractPublicKeyScript(claimMainTx.txIn.head.witness))
} }
test("extract pay2wsh pubkey script") { test("extract pay2wsh pubkey script") {
val commitTx = Transaction.read("02000000000101fb98507ff5f47bcc5b4497a145e631f68b2b5fcf2752598bc54c8f33696e1c73000000000017f15b80015b3f0f0000000000220020345fc26988f6252d9d93ee95f2198e820db1a4d7c7ec557e4cc5d7e60750cc21040047304402202fd9cbc8446a10193f378269bf12d321aa972743c0a011089aff522de2a1414d02204dd65bf43e41fe911c7180e5e036d609646a798fa5c3f288ede73679978df36b01483045022100fced8966c2527cb175521c4eb41aaaee96838420fa5fce3d4730c0da37f6253502202dc9667530a9f79bc6444b54335467d2043c4b996da5fbca7496e0fa64ccc1bd0147522103a16c06d8626bad5d6d8ea8fee980c287590b9dedeb5857a3d0cd6c4b4e95631c2103d872e26e43f723523d2d8eff5f93a1b344fe51eb76bcfd4906315ae2fe35389a52ae620acc20") val commitTx = Transaction.read("02000000000101fb98507ff5f47bcc5b4497a145e631f68b2b5fcf2752598bc54c8f33696e1c73000000000017f15b80015b3f0f0000000000220020345fc26988f6252d9d93ee95f2198e820db1a4d7c7ec557e4cc5d7e60750cc21040047304402202fd9cbc8446a10193f378269bf12d321aa972743c0a011089aff522de2a1414d02204dd65bf43e41fe911c7180e5e036d609646a798fa5c3f288ede73679978df36b01483045022100fced8966c2527cb175521c4eb41aaaee96838420fa5fce3d4730c0da37f6253502202dc9667530a9f79bc6444b54335467d2043c4b996da5fbca7496e0fa64ccc1bd0147522103a16c06d8626bad5d6d8ea8fee980c287590b9dedeb5857a3d0cd6c4b4e95631c2103d872e26e43f723523d2d8eff5f93a1b344fe51eb76bcfd4906315ae2fe35389a52ae620acc20")
val claimMainDelayedTx = Transaction.read("02000000000101b285ffeb84c366f621fe33b6ff77a9b7578075b65e69c363d12c35aa422d98fd00000000009000000001e03e0f000000000017a9147407522166f1ed3030788b1b6a48803867d1797f8703483045022100fe9eefd010a80411ccae87590db3f54c1c04605170bdcd83c1e04222d474ef41022036db7fd3c07c0523c2cf72d80c7fe3bdc2d5028a8bc2864b478a707e8af627dc01004d63210298f7dada89d882c4ab971e7e914f4953249bad70333b29aa504bb67e5ce9239c67029000b275210328170f7e781c70ea679efc30383d3e03451ca350e2a8690f8ed3db9dabb3866768ac00000000") val claimMainDelayedTx = Transaction.read("02000000000101b285ffeb84c366f621fe33b6ff77a9b7578075b65e69c363d12c35aa422d98fd00000000009000000001e03e0f000000000017a9147407522166f1ed3030788b1b6a48803867d1797f8703483045022100fe9eefd010a80411ccae87590db3f54c1c04605170bdcd83c1e04222d474ef41022036db7fd3c07c0523c2cf72d80c7fe3bdc2d5028a8bc2864b478a707e8af627dc01004d63210298f7dada89d882c4ab971e7e914f4953249bad70333b29aa504bb67e5ce9239c67029000b275210328170f7e781c70ea679efc30383d3e03451ca350e2a8690f8ed3db9dabb3866768ac00000000")
assert(commitTx.txOut.head.publicKeyScript === WatchConfirmed.extractPublicKeyScript(claimMainDelayedTx.txIn.head.witness)) assert(commitTx.txOut.head.publicKeyScript === WatchConfirmed.extractPublicKeyScript(claimMainDelayedTx.txIn.head.witness))
} }
}
object WatcherSpec {
/**
* Create a new address and dumps its private key.
*/
def getNewAddress(bitcoincli: ActorRef)(implicit system: ActorSystem): (String, PrivateKey) = {
val probe = TestProbe()
probe.send(bitcoincli, BitcoinReq("getnewaddress"))
val JString(address) = probe.expectMsgType[JValue]
probe.send(bitcoincli, BitcoinReq("dumpprivkey", address))
val JString(wif) = probe.expectMsgType[JValue]
val (priv, true) = PrivateKey.fromBase58(wif, Base58.Prefix.SecretKeyTestnet)
(address, priv)
}
/**
* Send to a given address, without generating blocks to confirm.
*
* @return the corresponding transaction.
*/
def sendToAddress(bitcoincli: ActorRef, address: String, amount: Double)(implicit system: ActorSystem): Transaction = {
val probe = TestProbe()
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, amount))
val JString(txid) = probe.expectMsgType[JValue]
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
val JString(hex) = probe.expectMsgType[JValue]
Transaction.read(hex)
}
/**
* Create a chain of unspent txs.
*
* @param tx tx that sends funds to a p2wpkh of priv
* @param priv private key that tx sends funds to
* @return a (tx1, tx2) tuple where tx2 spends tx1 which spends tx
*/
def createUnspentTxChain(tx: Transaction, priv: PrivateKey): (Transaction, Transaction) = {
// tx sends funds to our key
val pub = priv.publicKey
val outputIndex = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(pub)))
val fee = 10000 sat
// tx1 spends tx
val tx1 = {
val tmp = Transaction(version = 2, txIn = TxIn(OutPoint(tx, outputIndex), Nil, TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(tx.txOut(outputIndex).amount - fee, Script.pay2wpkh(pub)) :: Nil, lockTime = 0)
val sig = Transaction.signInput(tmp, 0, Script.pay2pkh(pub), SIGHASH_ALL, tx.txOut(outputIndex).amount, SigVersion.SIGVERSION_WITNESS_V0, priv)
val tmp1 = tmp.updateWitness(0, ScriptWitness(sig :: pub.value :: Nil))
Transaction.correctlySpends(tmp1, tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
tmp1
}
// and tx2 spends tx1
val tx2 = {
val tmp = Transaction(version = 2, txIn = TxIn(OutPoint(tx1, 0), Nil, TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(tx1.txOut.head.amount - fee, Script.pay2wpkh(pub)) :: Nil, lockTime = 0)
val sig = Transaction.signInput(tmp, 0, Script.pay2pkh(pub), SIGHASH_ALL, tx1.txOut.head.amount, SigVersion.SIGVERSION_WITNESS_V0, priv)
val tmp1 = tmp.updateWitness(0, ScriptWitness(sig :: pub.value :: Nil))
Transaction.correctlySpends(tmp1, tx1 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
tmp1
}
(tx1, tx2)
}
} }

View File

@ -24,6 +24,7 @@ import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.{Block, ByteVector32, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut} import fr.acinq.bitcoin.{Block, ByteVector32, MilliBtc, OutPoint, Satoshi, Script, Transaction, TxIn, TxOut}
import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.FundTransactionResponse
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, JsonRPCError}
import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.transactions.Scripts
import fr.acinq.eclair.{LongToBtcAmount, addressToPublicKeyScript, randomKey} import fr.acinq.eclair.{LongToBtcAmount, addressToPublicKeyScript, randomKey}

View File

@ -17,7 +17,7 @@
package fr.acinq.eclair.blockchain.bitcoind package fr.acinq.eclair.blockchain.bitcoind
import java.io.File import java.io.File
import java.nio.file.{Files, StandardCopyOption} import java.nio.file.Files
import java.util.UUID import java.util.UUID
import akka.actor.{Actor, ActorRef, ActorSystem, Props} import akka.actor.{Actor, ActorRef, ActorSystem, Props}
@ -28,7 +28,7 @@ import fr.acinq.eclair.TestUtils
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BitcoinJsonRPCClient}
import fr.acinq.eclair.integration.IntegrationSpec import fr.acinq.eclair.integration.IntegrationSpec
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JArray, JDecimal, JInt, JString, JValue} import org.json4s.JsonAST._
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._ import scala.concurrent.duration._
@ -48,6 +48,8 @@ trait BitcoindService extends Logging {
val bitcoindZmqTxPort: Int = TestUtils.availablePort val bitcoindZmqTxPort: Int = TestUtils.availablePort
import BitcoindService._
import scala.sys.process._ import scala.sys.process._
val INTEGRATION_TMP_DIR = new File(TestUtils.BUILD_DIRECTORY, s"integration-${UUID.randomUUID()}") val INTEGRATION_TMP_DIR = new File(TestUtils.BUILD_DIRECTORY, s"integration-${UUID.randomUUID()}")
@ -60,8 +62,6 @@ trait BitcoindService extends Logging {
var bitcoinrpcclient: BitcoinJsonRPCClient = null var bitcoinrpcclient: BitcoinJsonRPCClient = null
var bitcoincli: ActorRef = null var bitcoincli: ActorRef = null
case class BitcoinReq(method: String, params: Any*)
def startBitcoind(): Unit = { def startBitcoind(): Unit = {
Files.createDirectories(PATH_BITCOIND_DATADIR.toPath) Files.createDirectories(PATH_BITCOIND_DATADIR.toPath)
if (!Files.exists(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)) { if (!Files.exists(new File(PATH_BITCOIND_DATADIR.toString, "bitcoin.conf").toPath)) {
@ -130,3 +130,9 @@ trait BitcoindService extends Logging {
} }
} }
object BitcoindService {
case class BitcoinReq(method: String, params: Any*)
}

View File

@ -22,17 +22,17 @@ import akka.pattern.pipe
import akka.testkit.{TestKit, TestProbe} import akka.testkit.{TestKit, TestProbe}
import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigFactory
import fr.acinq.bitcoin.Transaction import fr.acinq.bitcoin.Transaction
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, ExtendedBitcoinClient}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import org.json4s.DefaultFormats import org.json4s.DefaultFormats
import org.json4s.JsonAST.{JString, _} import org.json4s.JsonAST._
import org.scalatest.BeforeAndAfterAll import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.funsuite.AnyFunSuiteLike
import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.ExecutionContext.Implicits.global
import scala.jdk.CollectionConverters._ import scala.jdk.CollectionConverters._
class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with BitcoindService with AnyFunSuiteLike with BeforeAndAfterAll with Logging { class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with BitcoindService with AnyFunSuiteLike with BeforeAndAfterAll with Logging {
val commonConfig = ConfigFactory.parseMap(Map( val commonConfig = ConfigFactory.parseMap(Map(
@ -107,7 +107,7 @@ class ExtendedBitcoinClientSpec extends TestKit(ActorSystem("test")) with Bitcoi
signedTx signedTx
} }
bitcoinClient.invoke("sendrawtransaction", spendingTx).pipeTo(sender.ref) bitcoinClient.invoke("sendrawtransaction", spendingTx).pipeTo(sender.ref)
val JString(spendingTxid) = sender.expectMsgType[JValue] sender.expectMsgType[JValue]
// and publish the tx a fourth time to test idempotence // and publish the tx a fourth time to test idempotence
client.publishTransaction(tx).pipeTo(sender.ref) client.publishTransaction(tx).pipeTo(sender.ref)

View File

@ -16,22 +16,59 @@
package fr.acinq.eclair.blockchain.bitcoind package fr.acinq.eclair.blockchain.bitcoind
import fr.acinq.bitcoin.OutPoint import java.util.concurrent.atomic.AtomicLong
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed, WatchSpent, WatchSpentBasic}
import fr.acinq.eclair.channel.BITCOIN_FUNDING_SPENT
import fr.acinq.eclair.randomBytes32
import org.scalatest.funsuite.AnyFunSuite
class ZmqWatcherSpec extends AnyFunSuite { import akka.Done
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.testkit.{TestKit, TestProbe}
import fr.acinq.bitcoin.{OutPoint, Script}
import fr.acinq.eclair.blockchain.WatcherSpec._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.ZmqWatcher._
import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient
import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT}
import fr.acinq.eclair.randomBytes32
import grizzled.slf4j.Logging
import org.json4s.JsonAST.JValue
import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuiteLike
import scala.concurrent.Promise
import scala.concurrent.duration._
class ZmqWatcherSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteLike with BitcoindService with BeforeAndAfterAll with Logging {
var zmqBlock: ActorRef = _
var zmqTx: ActorRef = _
override def beforeAll(): Unit = {
logger.info("starting bitcoind")
startBitcoind()
waitForBitcoindReady()
logger.info("starting zmq actors")
val (zmqBlockConnected, zmqTxConnected) = (Promise[Done](), Promise[Done]())
zmqBlock = system.actorOf(Props(new ZMQActor(s"tcp://127.0.0.1:$bitcoindZmqBlockPort", Some(zmqBlockConnected))))
zmqTx = system.actorOf(Props(new ZMQActor(s"tcp://127.0.0.1:$bitcoindZmqTxPort", Some(zmqTxConnected))))
awaitCond(zmqBlockConnected.isCompleted && zmqTxConnected.isCompleted)
super.beforeAll()
}
override def afterAll(): Unit = {
logger.info("stopping zmq actors")
system.stop(zmqBlock)
system.stop(zmqTx)
logger.info("stopping bitcoind")
stopBitcoind()
super.afterAll()
TestKit.shutdownActorSystem(system)
}
test("add/remove watches from/to utxo map") { test("add/remove watches from/to utxo map") {
import ZmqWatcher._
val m0 = Map.empty[OutPoint, Set[Watch]] val m0 = Map.empty[OutPoint, Set[Watch]]
val txid = randomBytes32 val txid = randomBytes32
val outputIndex = 42 val outputIndex = 42
val utxo = OutPoint(txid.reverse, outputIndex) val utxo = OutPoint(txid.reverse, outputIndex)
val w1 = WatchSpent(null, txid, outputIndex, randomBytes32, BITCOIN_FUNDING_SPENT) val w1 = WatchSpent(null, txid, outputIndex, randomBytes32, BITCOIN_FUNDING_SPENT)
@ -63,5 +100,55 @@ class ZmqWatcherSpec extends AnyFunSuite {
assert(m10.isEmpty) assert(m10.isEmpty)
} }
test("watch for confirmed transactions") {
val probe = TestProbe()
val blockCount = new AtomicLong()
val watcher = system.actorOf(ZmqWatcher.props(blockCount, new ExtendedBitcoinClient(bitcoinrpcclient)))
val (address, _) = getNewAddress(bitcoincli)
val tx = sendToAddress(bitcoincli, address, 1.0)
val listener = TestProbe()
probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut.head.publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK))
probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut.head.publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK)) // setting the watch multiple times should be a no-op
generateBlocks(bitcoincli, 5)
assert(listener.expectMsgType[WatchEventConfirmed].tx.txid === tx.txid)
listener.expectNoMsg(1 second)
// If we try to watch a transaction that has already been confirmed, we should immediately receive a WatchEventConfirmed.
probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut.head.publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK))
assert(listener.expectMsgType[WatchEventConfirmed].tx.txid === tx.txid)
listener.expectNoMsg(1 second)
system.stop(watcher)
}
test("watch for spent transactions") {
val probe = TestProbe()
val blockCount = new AtomicLong()
val watcher = system.actorOf(ZmqWatcher.props(blockCount, new ExtendedBitcoinClient(bitcoinrpcclient)))
val (address, priv) = getNewAddress(bitcoincli)
val tx = sendToAddress(bitcoincli, address, 1.0)
val outputIndex = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(priv.publicKey)))
val (tx1, tx2) = createUnspentTxChain(tx, priv)
val listener = TestProbe()
probe.send(watcher, WatchSpent(listener.ref, tx, outputIndex, BITCOIN_FUNDING_SPENT))
listener.expectNoMsg(1 second)
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString()))
probe.expectMsgType[JValue]
// tx and tx1 aren't confirmed yet, but we trigger the WatchEventSpent when we see tx1 in the mempool.
listener.expectMsg(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx1))
// Let's confirm tx and tx1: seeing tx1 in a block should trigger WatchEventSpent again.
generateBlocks(bitcoincli, 2)
listener.expectMsg(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx1))
// Let's submit tx2, and set a watch after it has been confirmed this time.
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString()))
probe.expectMsgType[JValue]
listener.expectNoMsg(1 second)
generateBlocks(bitcoincli, 1)
probe.send(watcher, WatchSpent(listener.ref, tx1, 0, BITCOIN_FUNDING_SPENT))
listener.expectMsg(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx2))
system.stop(watcher)
}
} }

View File

@ -26,6 +26,7 @@ import com.whisk.docker.DockerReadyChecker
import fr.acinq.bitcoin.{Block, Btc, ByteVector32, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} import fr.acinq.bitcoin.{Block, Btc, ByteVector32, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
import fr.acinq.eclair.LongToBtcAmount import fr.acinq.eclair.LongToBtcAmount
import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionResponse, SignTransactionResponse} import fr.acinq.eclair.blockchain.bitcoind.BitcoinCoreWallet.{FundTransactionResponse, SignTransactionResponse}
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService} import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService}
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse, SSL} import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse, SSL}
import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress

View File

@ -21,16 +21,17 @@ import java.util.concurrent.atomic.AtomicLong
import akka.actor.{ActorSystem, Props} import akka.actor.{ActorSystem, Props}
import akka.testkit.{TestKit, TestProbe} import akka.testkit.{TestKit, TestProbe}
import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{ByteVector32, OutPoint, SIGHASH_ALL, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut}
import fr.acinq.bitcoin.{Base58, Bech32, ByteVector32, OutPoint, SIGHASH_ALL, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} import fr.acinq.eclair.blockchain.WatcherSpec._
import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.BitcoindService
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL
import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress
import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT} import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT}
import fr.acinq.eclair.{LongToBtcAmount, randomBytes32} import fr.acinq.eclair.{LongToBtcAmount, randomBytes32}
import grizzled.slf4j.Logging import grizzled.slf4j.Logging
import org.json4s.JsonAST.{JString, JValue} import org.json4s.JsonAST.JValue
import org.scalatest.BeforeAndAfterAll import org.scalatest.BeforeAndAfterAll
import org.scalatest.funsuite.AnyFunSuiteLike import org.scalatest.funsuite.AnyFunSuiteLike
import scodec.bits._ import scodec.bits._
@ -61,21 +62,14 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteL
val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress))))
val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient)))
probe.send(bitcoincli, BitcoinReq("getnewaddress")) val (address, _) = getNewAddress(bitcoincli)
val JString(address) = probe.expectMsgType[JValue] val tx = sendToAddress(bitcoincli, address, 1.0)
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
val JString(hex) = probe.expectMsgType[JValue]
val tx = Transaction.read(hex)
val listener = TestProbe() val listener = TestProbe()
probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut(0).publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK)) probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut(0).publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK))
generateBlocks(bitcoincli, 5) generateBlocks(bitcoincli, 5)
val confirmed = listener.expectMsgType[WatchEventConfirmed](20 seconds) val confirmed = listener.expectMsgType[WatchEventConfirmed](20 seconds)
assert(confirmed.tx.txid.toHex === txid) assert(confirmed.tx.txid === tx.txid)
system.stop(watcher) system.stop(watcher)
} }
@ -85,19 +79,8 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteL
val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress))))
val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient)))
probe.send(bitcoincli, BitcoinReq("getnewaddress")) val (address, priv) = getNewAddress(bitcoincli)
val JString(address) = probe.expectMsgType[JValue] val tx = sendToAddress(bitcoincli, address, 1.0)
probe.send(bitcoincli, BitcoinReq("dumpprivkey", address))
val JString(wif) = probe.expectMsgType[JValue]
val (priv, true) = PrivateKey.fromBase58(wif, Base58.Prefix.SecretKeyTestnet)
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
val JString(txid) = probe.expectMsgType[JValue](30 seconds)
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
val JString(hex) = probe.expectMsgType[JValue]
val tx = Transaction.read(hex)
// find the output for the address we generated and create a tx that spends it // find the output for the address we generated and create a tx that spends it
val pos = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(priv.publicKey))) val pos = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(priv.publicKey)))
@ -123,65 +106,22 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteL
system.stop(watcher) system.stop(watcher)
} }
/**
* Create a chain of unspent txs
*
* @param tx tx that sends funds to a p2wpkh of priv
* @param priv private key that tx sends funds to
* @return a (tx1, tx2) tuple where tx2 spends tx1 which spends tx
*/
def createUnspentTxChain(tx: Transaction, priv: PrivateKey): (Transaction, Transaction) = {
// tx sends funds to our key
val pub = priv.publicKey
val outputIndex = tx.txOut.indexWhere(_.publicKeyScript == Script.write(Script.pay2wpkh(pub)))
val fee = 10000 sat
val tx1 = {
val tmp = Transaction(version = 2, txIn = TxIn(OutPoint(tx, outputIndex), Nil, TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(tx.txOut(outputIndex).amount - fee, Script.pay2wpkh(pub)) :: Nil, lockTime = 0)
val sig = Transaction.signInput(tmp, 0, Script.pay2pkh(pub), SIGHASH_ALL, tx.txOut(outputIndex).amount, SigVersion.SIGVERSION_WITNESS_V0, priv)
val tmp1 = tmp.updateWitness(0, ScriptWitness(sig :: pub.value :: Nil))
Transaction.correctlySpends(tmp1, tx :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
tmp1
}
// tx1 spends tx
val tx2 = {
val tmp = Transaction(version = 2, txIn = TxIn(OutPoint(tx1, 0), Nil, TxIn.SEQUENCE_FINAL) :: Nil, txOut = TxOut(tx1.txOut(0).amount - fee, Script.pay2wpkh(pub)) :: Nil, lockTime = 0)
val sig = Transaction.signInput(tmp, 0, Script.pay2pkh(pub), SIGHASH_ALL, tx1.txOut(0).amount, SigVersion.SIGVERSION_WITNESS_V0, priv)
val tmp1 = tmp.updateWitness(0, ScriptWitness(sig :: pub.value :: Nil))
Transaction.correctlySpends(tmp1, tx1 :: Nil, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
tmp1
}
// and tx2 spends tx1
(tx1, tx2)
}
test("watch for mempool transactions (txs in mempool before we set the watch)") { test("watch for mempool transactions (txs in mempool before we set the watch)") {
val probe = TestProbe() val probe = TestProbe()
val blockCount = new AtomicLong() val blockCount = new AtomicLong()
val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress))))
probe.send(electrumClient, ElectrumClient.AddStatusListener(probe.ref)) probe.send(electrumClient, ElectrumClient.AddStatusListener(probe.ref))
probe.expectMsgType[ElectrumClient.ElectrumReady] probe.expectMsgType[ElectrumClient.ElectrumReady]
val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient)))
val priv = PrivateKey(ByteVector32.fromValidHex("01" * 32)) val (address, priv) = getNewAddress(bitcoincli)
val pub = priv.publicKey val tx = sendToAddress(bitcoincli, address, 1.0)
val address = Bech32.encodeWitnessAddress("bcrt", 0, pub.hash160)
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
val JString(hex) = probe.expectMsgType[JValue]
val tx = Transaction.read(hex)
val (tx1, tx2) = createUnspentTxChain(tx, priv) val (tx1, tx2) = createUnspentTxChain(tx, priv)
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString())) probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString()))
probe.expectMsgType[JValue] probe.expectMsgType[JValue]
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString())) probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString()))
probe.expectMsgType[JValue] probe.expectMsgType[JValue]
// wait until tx1 and tx2 are in the mempool (as seen by our ElectrumX server) // wait until tx1 and tx2 are in the mempool (as seen by our ElectrumX server)
awaitCond({ awaitCond({
probe.send(electrumClient, ElectrumClient.GetScriptHashHistory(ElectrumClient.computeScriptHash(tx2.txOut(0).publicKeyScript))) probe.send(electrumClient, ElectrumClient.GetScriptHashHistory(ElectrumClient.computeScriptHash(tx2.txOut(0).publicKeyScript)))
@ -205,21 +145,13 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteL
probe.expectMsgType[ElectrumClient.ElectrumReady] probe.expectMsgType[ElectrumClient.ElectrumReady]
val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient)))
val priv = PrivateKey(ByteVector32.fromValidHex("01" * 32)) val (address, priv) = getNewAddress(bitcoincli)
val pub = priv.publicKey val tx = sendToAddress(bitcoincli, address, 1.0)
val address = Bech32.encodeWitnessAddress("bcrt", 0, pub.hash160)
probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0))
val JString(txid) = probe.expectMsgType[JValue](3000 seconds)
probe.send(bitcoincli, BitcoinReq("getrawtransaction", txid))
val JString(hex) = probe.expectMsgType[JValue]
val tx = Transaction.read(hex)
val (tx1, tx2) = createUnspentTxChain(tx, priv) val (tx1, tx2) = createUnspentTxChain(tx, priv)
// here we set the watch * before * we publish our transactions // here we set the watch * before * we publish our transactions
val listener = TestProbe() val listener = TestProbe()
probe.send(watcher, WatchConfirmed(listener.ref, tx2.txid, tx2.txOut(0).publicKeyScript, 0, BITCOIN_FUNDING_DEPTHOK)) probe.send(watcher, WatchConfirmed(listener.ref, tx2.txid, tx2.txOut(0).publicKeyScript, 0, BITCOIN_FUNDING_DEPTHOK))
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString())) probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString()))
probe.expectMsgType[JValue] probe.expectMsgType[JValue]
probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString())) probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString()))
@ -247,7 +179,6 @@ class ElectrumWatcherSpec extends TestKit(ActorSystem("test")) with AnyFunSuiteL
val mainnetAddress = ElectrumServerAddress(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT) val mainnetAddress = ElectrumServerAddress(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT)
val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(mainnetAddress)))) val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(mainnetAddress))))
val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient)))
//Thread.sleep(10000)
val probe = TestProbe() val probe = TestProbe()
{ {

View File

@ -26,6 +26,7 @@ import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey}
import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction} import fr.acinq.bitcoin.{Base58, Base58Check, Bech32, Block, ByteVector32, Crypto, OP_0, OP_CHECKSIG, OP_DUP, OP_EQUAL, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, ScriptFlags, Transaction}
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService import fr.acinq.eclair.blockchain.bitcoind.BitcoindService
import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq
import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed} import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh} import fr.acinq.eclair.channel.Channel.{BroadcastChannelUpdate, PeriodicRefresh}
@ -886,16 +887,15 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
}, max = 20 seconds, interval = 1 second) }, max = 20 seconds, interval = 1 second)
// we then fulfill the htlc, which will make F redeem it on-chain // we then fulfill the htlc, which will make F redeem it on-chain
sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) sender.send(nodes("F1").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
// we then generate one block so that the htlc success tx gets written to the blockchain
generateBlocks(bitcoincli, 1)
// C will extract the preimage from the blockchain and fulfill the payment upstream
paymentSender.expectMsgType[PaymentSent](30 seconds)
// at this point F should have 1 recv transactions: the redeemed htlc // at this point F should have 1 recv transactions: the redeemed htlc
awaitCond({ awaitCond({
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0)) sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds) val res = sender.expectMsgType[JValue](10 seconds)
res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1 res.filter(_ \ "address" == JString(finalAddressF)).flatMap(_ \ "txids" \\ classOf[JString]).size == 1
}, max = 30 seconds, interval = 1 second) }, max = 30 seconds, interval = 1 second)
// we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters
// the mempool and fulfill the payment upstream.
paymentSender.expectMsgType[PaymentSent](30 seconds)
// we then generate enough blocks so that C gets its main delayed output // we then generate enough blocks so that C gets its main delayed output
generateBlocks(bitcoincli, 145) generateBlocks(bitcoincli, 145)
// and C will have its main output // and C will have its main output
@ -937,7 +937,6 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
val paymentSender = TestProbe() val paymentSender = TestProbe()
paymentSender.send(nodes("A").paymentInitiator, paymentReq) paymentSender.send(nodes("A").paymentInitiator, paymentReq)
paymentSender.expectMsgType[UUID](30 seconds) paymentSender.expectMsgType[UUID](30 seconds)
// F gets the htlc // F gets the htlc
val htlc = htlcReceiver.expectMsgType[IncomingPacket.FinalPacket].add val htlc = htlcReceiver.expectMsgType[IncomingPacket.FinalPacket].add
// now that we have the channel id, we retrieve channels default final addresses // now that we have the channel id, we retrieve channels default final addresses
@ -964,14 +963,13 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with BitcoindService
sender.expectMsg(ChannelCommandResponse.Ok) sender.expectMsg(ChannelCommandResponse.Ok)
// we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain) // we then fulfill the htlc (it won't be sent to C, and will be used to pull funds on-chain)
sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage))) sender.send(nodes("F2").register, Forward(htlc.channelId, CMD_FULFILL_HTLC(htlc.id, preimage)))
// we then generate one block so that the htlc success tx gets written to the blockchain // we don't need to generate blocks to confirm the htlc-success; C should extract the preimage as soon as it enters
sender.send(bitcoincli, BitcoinReq("getnewaddress")) // the mempool and fulfill the payment upstream.
val JString(address) = sender.expectMsgType[JValue]
generateBlocks(bitcoincli, 1, Some(address))
// C will extract the preimage from the blockchain and fulfill the payment upstream
paymentSender.expectMsgType[PaymentSent](30 seconds) paymentSender.expectMsgType[PaymentSent](30 seconds)
// at this point F should have 1 recv transactions: the redeemed htlc // at this point F should have 1 recv transactions: the redeemed htlc
// we then generate enough blocks so that F gets its htlc-success delayed output // we then generate enough blocks so that F gets its htlc-success delayed output
sender.send(bitcoincli, BitcoinReq("getnewaddress"))
val JString(address) = sender.expectMsgType[JValue]
generateBlocks(bitcoincli, 145, Some(address)) generateBlocks(bitcoincli, 145, Some(address))
// at this point F should have 1 recv transactions: the redeemed htlc // at this point F should have 1 recv transactions: the redeemed htlc
awaitCond({ awaitCond({