1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-03-03 17:36:56 +01:00

Added an integration test on revoked tx handling (#172)

The scenario was already tested at a lower level, but this is
more realistic, with a real bitcoin core.

Note that we currently only steal the counterparty's *main output*,
we ignore pending htlcs. From an incentive point-of-view, it is an
acceptable tradeoff because the amount of in-flight htlcs should
typically be far less than the main outputs (and can be configured
with `max-htlc-value-in-flight-msat`).
This commit is contained in:
Pierre-Marie Padiou 2017-10-03 18:43:36 +02:00 committed by GitHub
parent 2fc1d7096f
commit 0d180032a4
2 changed files with 62 additions and 6 deletions

View file

@ -42,6 +42,7 @@ class LocalPaymentHandler(nodeParams: NodeParams) extends Actor with ActorLoggin
case Some(amount) if MilliSatoshi(htlc.amountMsat) < amount => sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectPaymentAmount), commit = true)
case Some(amount) if MilliSatoshi(htlc.amountMsat) > amount * 2 => sender ! CMD_FAIL_HTLC(htlc.id, Right(IncorrectPaymentAmount), commit = true)
case _ =>
log.info(s"received payment for paymentHash=${htlc.paymentHash} amountMsat=${htlc.amountMsat}")
// amount is correct or was not specified in the payment request
sender ! CMD_FULFILL_HTLC(htlc.id, r, commit = true)
context.system.eventStream.publish(PaymentReceived(MilliSatoshi(htlc.amountMsat), htlc.paymentHash))

View file

@ -4,12 +4,12 @@ import java.io.{File, PrintWriter}
import java.nio.file.Files
import java.util.{Properties, UUID}
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.actor.{Actor, ActorRef, ActorSystem, PoisonPill, Props}
import akka.pattern.pipe
import akka.testkit.{TestKit, TestProbe}
import com.typesafe.config.{Config, ConfigFactory}
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script}
import fr.acinq.bitcoin.{Base58, Base58Check, BinaryData, Block, Crypto, MilliSatoshi, OP_CHECKSIG, OP_DUP, OP_EQUALVERIFY, OP_HASH160, OP_PUSHDATA, Satoshi, Script, Transaction}
import fr.acinq.eclair.blockchain.rpc.{BitcoinJsonRPCClient, ExtendedBitcoinClient}
import fr.acinq.eclair.blockchain.{Watch, WatchConfirmed}
import fr.acinq.eclair.channel.Register.Forward
@ -28,6 +28,7 @@ import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import org.scalatest.{BeforeAndAfterAll, FunSuiteLike}
import scala.compat.Platform
import scala.concurrent.Await
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
@ -127,6 +128,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
instantiateEclairNode("F2", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F2", "eclair.server.port" -> 29736, "eclair.api.port" -> 28086, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
instantiateEclairNode("F3", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F3", "eclair.server.port" -> 29737, "eclair.api.port" -> 28087, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
instantiateEclairNode("F4", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F4", "eclair.server.port" -> 29738, "eclair.api.port" -> 28088, "eclair.payment-handler" -> "noop")).withFallback(commonConfig))
instantiateEclairNode("F5", ConfigFactory.parseMap(Map("eclair.node-alias" -> "F5", "eclair.server.port" -> 29739, "eclair.api.port" -> 28089)).withFallback(commonConfig))
}
def connect(node1: Kit, node2: Kit, fundingSatoshis: Long, pushMsat: Long) = {
@ -164,6 +166,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
connect(nodes("C"), nodes("F2"), 5000000, 0)
connect(nodes("C"), nodes("F3"), 5000000, 0)
connect(nodes("C"), nodes("F4"), 5000000, 0)
connect(nodes("C"), nodes("F5"), 5000000, 0)
val sender = TestProbe()
val eventListener = TestProbe()
@ -246,7 +249,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
// generating more blocks so that all funding txes are buried under at least 6 blocks
sender.send(bitcoincli, BitcoinReq("generate", 4))
sender.expectMsgType[JValue]
awaitAnnouncements(nodes, 9, 9, 18)
awaitAnnouncements(nodes, 10, 10, 20)
}
test("send an HTLC A->D") {
@ -437,7 +440,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 1
}, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16)
awaitAnnouncements(nodes.filter(_._1 == "A"), 9, 9, 18)
}
test("propagate a fulfill upstream when a downstream htlc is redeemed on-chain (remote commit)") {
@ -502,7 +505,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 1
}, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14)
awaitAnnouncements(nodes.filter(_._1 == "A"), 8, 8, 16)
}
test("propagate a failure upstream when a downstream htlc times out (local commit)") {
@ -548,7 +551,7 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 2
}, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12)
awaitAnnouncements(nodes.filter(_._1 == "A"), 7, 7, 14)
}
test("propagate a failure upstream when a downstream htlc times out (remote commit)") {
@ -596,6 +599,58 @@ class IntegrationSpec extends TestKit(ActorSystem("test")) with FunSuiteLike wit
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 2
}, max = 30 seconds, interval = 1 second)
awaitAnnouncements(nodes.filter(_._1 == "A"), 6, 6, 12)
}
test("punish a node that has published a revoked commit tx") {
val sender = TestProbe()
// first we make sure we are in sync with current blockchain height
sender.send(bitcoincli, BitcoinReq("getblockcount"))
val currentBlockCount = sender.expectMsgType[JValue](10 seconds).extract[Long]
awaitCond(Globals.blockCount.get() == currentBlockCount, max = 20 seconds, interval = 1 second)
// first we send 3 mBTC to F so that it has a balance
val amountMsat = MilliSatoshi(300000000L)
sender.send(nodes("F5").paymentHandler, ReceivePayment(amountMsat, "1 coffee"))
val pr = sender.expectMsgType[PaymentRequest]
val sendReq = SendPayment(300000000L, pr.paymentHash, nodes("F5").nodeParams.privateKey.publicKey)
sender.send(nodes("A").paymentInitiator, sendReq)
sender.expectMsgType[PaymentSucceeded]
// then we find the id of F's only channel
sender.send(nodes("F5").register, 'channels)
val channelId = sender.expectMsgType[Map[BinaryData, ActorRef]].head._1
// we then wait for F to have a main output
awaitCond({
sender.send(nodes("F5").register, Forward(channelId, CMD_GETSTATEDATA))
sender.expectMsgType[DATA_NORMAL].commitments.localCommit.index == 2
}, max = 5 seconds)
// and we use it to get its current commitment tx
sender.send(nodes("F5").register, Forward(channelId, CMD_GETSTATEDATA))
val localCommitTxF = sender.expectMsgType[DATA_NORMAL].commitments.localCommit.publishableTxs
// we now send some more money to F so that it creates a new commitment tx
val amountMsat1 = MilliSatoshi(100000000L)
sender.send(nodes("F5").paymentHandler, ReceivePayment(amountMsat1, "1 coffee"))
val pr1 = sender.expectMsgType[PaymentRequest]
val sendReq1 = SendPayment(100000000L, pr1.paymentHash, nodes("F5").nodeParams.privateKey.publicKey)
sender.send(nodes("A").paymentInitiator, sendReq1)
sender.expectMsgType[PaymentSucceeded]
// we also retrieve C's default final address
sender.send(nodes("C").register, Forward(channelId, CMD_GETSTATEDATA))
val finalAddressC = scriptPubKeyToAddress(sender.expectMsgType[DATA_NORMAL].commitments.localParams.defaultFinalScriptPubKey)
// and we retrieve transactions already received so that we don't take them into account when evaluating the outcome of this test
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds)
val previouslyReceivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
// then we publish F's previous commit tx
sender.send(bitcoincli, BitcoinReq("sendrawtransaction", Transaction.write(localCommitTxF.commitTx.tx).toString()))
sender.expectMsgType[JValue](10000 seconds)
// at this point C should have 2 recv transactions: its previous main output and the one it took from F as a punishment
awaitCond({
sender.send(bitcoincli, BitcoinReq("listreceivedbyaddress", 0))
val res = sender.expectMsgType[JValue](10 seconds)
val receivedByC = res.filter(_ \ "address" == JString(finalAddressC)).flatMap(_ \ "txids" \\ classOf[JString])
(receivedByC diff previouslyReceivedByC).size == 2
}, max = 30 seconds, interval = 1 second)
// this will remove the channel
awaitAnnouncements(nodes.filter(_._1 == "A"), 5, 5, 10)
}