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

close channel when an htlc times out in either commitment tx (#24)

This commit is contained in:
Pierre-Marie Padiou 2017-02-07 15:04:59 +01:00 committed by Fabrice Drouin
parent dadc52b4c4
commit 0d763e8551
4 changed files with 75 additions and 5 deletions

View file

@ -5,10 +5,11 @@ import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.bitcoin._
import fr.acinq.eclair._
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
import fr.acinq.eclair.channel.Helpers.{Closing, Funding}
import fr.acinq.eclair.crypto.{Generators, ShaChain}
import fr.acinq.eclair.payment.Binding
import fr.acinq.eclair.router.{Announcements, Router}
import fr.acinq.eclair.router.Announcements
import fr.acinq.eclair.transactions._
import fr.acinq.eclair.wire._
@ -309,7 +310,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, router: ActorRef, re
goto(CLOSING) using DATA_CLOSING(d.commitments, localCommitPublished = Some(localCommitPublished))
case Event(e: Error, d: DATA_WAIT_FOR_FUNDING_LOCKED_INTERNAL) =>
log.error(s"peer sent $e, closing connection") // see bolt #2: A node MUST fail the connection if it receives an err message
log.error(s"peer sent $e, closing connection")
// see bolt #2: A node MUST fail the connection if it receives an err message
val localCommitTx = d.commitments.localCommit.publishableTxs.commitTx.tx
blockchain ! PublishAsap(localCommitTx)
blockchain ! WatchConfirmed(self, localCommitTx.txid, d.params.minimumDepth, BITCOIN_LOCALCOMMIT_DONE)
@ -342,6 +344,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, router: ActorRef, re
context.system.scheduler.scheduleOnce(3 seconds, router, 'tick_broadcast)
case _ => log.info(s"channel ${d.channelId} won't be announced")
}
// this clock will be used to detect htlc timeouts
context.system.eventStream.subscribe(self, classOf[CurrentBlockCount])
goto(NORMAL) using d.copy(commitments = d.commitments.copy(remoteNextCommitInfo = Right(nextPerCommitmentPoint)))
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
@ -511,6 +515,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, router: ActorRef, re
case Failure(cause) => handleLocalError(cause, d)
}
case Event(CurrentBlockCount(count), d: DATA_NORMAL) if d.commitments.hasTimedoutHtlcs(count) =>
handleLocalError(new RuntimeException(s"one or more htlcs timedout at blockheight=$count, closing the channel"), d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_NORMAL) => handleRemoteSpentOther(tx, d)
@ -604,6 +611,9 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, router: ActorRef, re
case Failure(cause) => handleLocalError(cause, d)
}
case Event(CurrentBlockCount(count), d: DATA_SHUTDOWN) if d.commitments.hasTimedoutHtlcs(count) =>
handleLocalError(new RuntimeException(s"one or more htlcs timedout at blockheight=$count, closing the channel"), d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_SHUTDOWN) if tx.txid == d.commitments.remoteCommit.txid => handleRemoteSpentCurrent(tx, d)
case Event(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx: Transaction), d: DATA_SHUTDOWN) => handleRemoteSpentOther(tx, d)
@ -725,8 +735,8 @@ class Channel(val them: ActorRef, val blockchain: ActorRef, router: ActorRef, re
}, stateName, stateData)
stay
// because channels send CMD to each others when relaying payments
case Event("ok", _) => stay
// we only care about this event in NORMAL and SHUTDOWN state, and we never unregister to the event stream
case Event(CurrentBlockCount(_), _) => stay
}
onTransition {

View file

@ -39,6 +39,10 @@ case class Commitments(localParams: LocalParams, remoteParams: RemoteParams,
def hasNoPendingHtlcs: Boolean = localCommit.spec.htlcs.isEmpty && remoteCommit.spec.htlcs.isEmpty
def hasTimedoutHtlcs(blockheight: Long): Boolean =
localCommit.spec.htlcs.exists(htlc => htlc.direction == OUT && blockheight >= htlc.add.expiry) ||
remoteCommit.spec.htlcs.exists(htlc => htlc.direction == IN && blockheight >= htlc.add.expiry)
def addLocalProposal(proposal: UpdateMessage): Commitments = Commitments.addLocalProposal(this, proposal)
def addRemoteProposal(proposal: UpdateMessage): Commitments = Commitments.addRemoteProposal(this, proposal)

View file

@ -6,6 +6,7 @@ import fr.acinq.bitcoin.Crypto.Scalar
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, Script, ScriptFlags, Transaction}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.payment.{Binding, Local, Relayed}
@ -791,6 +792,37 @@ class NormalStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CurrentBlockCount (no htlc timed out)") { case (alice, bob, alice2bob, bob2alice, _, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
// actual test begins
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
sender.send(alice, CurrentBlockCount(1400))
awaitCond(alice.stateData == initialState)
}
}
test("recv CurrentBlockCount (an htlc timed out)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val (r, htlc) = addHtlc(50000000, alice, bob, alice2bob, bob2alice)
sign(alice, bob, alice2bob, bob2alice)
// actual test begins
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
val aliceCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
sender.send(alice, CurrentBlockCount(1441))
alice2blockchain.expectMsg(PublishAsap(aliceCommitTx))
val watch = alice2blockchain.expectMsgType[WatchConfirmed]
assert(watch.txId === aliceCommitTx.txid)
assert(watch.event === BITCOIN_LOCALCOMMIT_DONE)
}
}
test("recv BITCOIN_FUNDING_SPENT (their commit w/ htlc)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, bob2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()

View file

@ -4,9 +4,10 @@ import akka.actor.Props
import akka.testkit.{TestFSMRef, TestProbe}
import fr.acinq.bitcoin.Crypto.Scalar
import fr.acinq.bitcoin.{BinaryData, Crypto, Satoshi, ScriptFlags, Transaction}
import fr.acinq.eclair.{TestkitBaseClass, TestBitcoinClient}
import fr.acinq.eclair.{TestBitcoinClient, TestkitBaseClass}
import fr.acinq.eclair.TestConstants.{Alice, Bob}
import fr.acinq.eclair.blockchain._
import fr.acinq.eclair.blockchain.peer.CurrentBlockCount
import fr.acinq.eclair.channel.states.StateTestsHelperMethods
import fr.acinq.eclair.channel.{Data, State, _}
import fr.acinq.eclair.wire.{CommitSig, Error, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFulfillHtlc}
@ -339,6 +340,29 @@ class ShutdownStateSpec extends TestkitBaseClass with StateTestsHelperMethods {
}
}
test("recv CurrentBlockCount (no htlc timed out)") { case (alice, bob, alice2bob, bob2alice, _, _) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN]
sender.send(alice, CurrentBlockCount(1400))
awaitCond(alice.stateData == initialState)
}
}
test("recv CurrentBlockCount (an htlc timed out)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val sender = TestProbe()
val initialState = alice.stateData.asInstanceOf[DATA_SHUTDOWN]
val aliceCommitTx = initialState.commitments.localCommit.publishableTxs.commitTx.tx
sender.send(alice, CurrentBlockCount(1441))
alice2blockchain.expectMsg(PublishAsap(aliceCommitTx))
val watch = alice2blockchain.expectMsgType[WatchConfirmed]
assert(watch.txId === aliceCommitTx.txid)
assert(watch.event === BITCOIN_LOCALCOMMIT_DONE)
}
}
test("recv RevokeAndAck (unexpectedly)") { case (alice, bob, alice2bob, bob2alice, alice2blockchain, _) =>
within(30 seconds) {
val tx = alice.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx