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:
parent
dadc52b4c4
commit
0d763e8551
4 changed files with 75 additions and 5 deletions
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue