Skip to content

Commit 3d3766e

Browse files
authored
Clarify some comments and add tests (#1734)
This commit clarifies some parts of the code on which we regularly have questions during pull requests. We also add a test for an edge case in shutdown that was correctly handled, but not properly tested, to ensure non-regression.
1 parent 7819fae commit 3d3766e

File tree

3 files changed

+48
-5
lines changed

3 files changed

+48
-5
lines changed

eclair-core/src/main/scala/fr/acinq/eclair/channel/Commitments.scala

+4-3
Original file line numberDiff line numberDiff line change
@@ -267,9 +267,10 @@ object Commitments {
267267
* @return either Left(failure, error message) where failure is a failure message (see BOLT #4 and the Failure Message class) or Right(new commitments, updateAddHtlc)
268268
*/
269269
def sendAdd(commitments: Commitments, cmd: CMD_ADD_HTLC, blockHeight: Long, feeConf: OnChainFeeConf): Either[ChannelException, (Commitments, UpdateAddHtlc)] = {
270-
// our counterparty needs a reasonable amount of time to pull the funds from downstream before we can get refunded (see BOLT 2 and BOLT 11 for a calculation and rationale)
271-
val minExpiry = CltvExpiry(blockHeight)
272-
if (cmd.cltvExpiry <= minExpiry) {
270+
// we must ensure we're not relaying htlcs that are already expired, otherwise the downstream channel will instantly close
271+
// NB: we add a 3 blocks safety to reduce the probability of running into this when our bitcoin node is slightly outdated
272+
val minExpiry = CltvExpiry(blockHeight + 3)
273+
if (cmd.cltvExpiry < minExpiry) {
273274
return Left(ExpiryTooSmall(commitments.channelId, minimum = minExpiry, actual = cmd.cltvExpiry, blockCount = blockHeight))
274275
}
275276
// we don't want to use too high a refund timeout, because our funds will be locked during that time if the payment is never fulfilled

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/e/NormalStateSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class NormalStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike with
147147
val expiryTooSmall = CltvExpiry(currentBlockHeight)
148148
val add = CMD_ADD_HTLC(sender.ref, 500000000 msat, randomBytes32, expiryTooSmall, TestConstants.emptyOnionPacket, localOrigin(sender.ref))
149149
alice ! add
150-
val error = ExpiryTooSmall(channelId(alice), CltvExpiry(currentBlockHeight), expiryTooSmall, currentBlockHeight)
150+
val error = ExpiryTooSmall(channelId(alice), CltvExpiry(currentBlockHeight + 3), expiryTooSmall, currentBlockHeight)
151151
sender.expectMsg(RES_ADD_FAILED(add, error, Some(initialState.channelUpdate)))
152152
alice2bob.expectNoMsg(200 millis)
153153
}

eclair-core/src/test/scala/fr/acinq/eclair/channel/states/f/ShutdownStateSpec.scala

+43-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import fr.acinq.eclair.payment._
2828
import fr.acinq.eclair.payment.relay.Relayer._
2929
import fr.acinq.eclair.router.Router.ChannelHop
3030
import fr.acinq.eclair.wire.protocol.Onion.FinalLegacyPayload
31-
import fr.acinq.eclair.wire.protocol.{CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
31+
import fr.acinq.eclair.wire.protocol.{ClosingSigned, CommitSig, Error, FailureMessageCodecs, PermanentChannelFailure, RevokeAndAck, Shutdown, UpdateAddHtlc, UpdateFailHtlc, UpdateFailMalformedHtlc, UpdateFee, UpdateFulfillHtlc}
3232
import fr.acinq.eclair.{CltvExpiry, CltvExpiryDelta, MilliSatoshiLong, TestConstants, TestKitBaseClass, randomBytes32}
3333
import org.scalatest.funsuite.FixtureAnyFunSuiteLike
3434
import org.scalatest.{Outcome, Tag}
@@ -792,6 +792,48 @@ class ShutdownStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLike wit
792792
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
793793
}
794794

795+
test("recv BITCOIN_FUNDING_SPENT (revoked tx with updated commitment)") { f =>
796+
import f._
797+
val initialCommitTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
798+
assert(initialCommitTx.txOut.size === 4) // two main outputs + 2 htlc
799+
800+
// bob fulfills one of the pending htlc (commitment update while in shutdown state)
801+
fulfillHtlc(0, r1, bob, alice, bob2alice, alice2bob)
802+
crossSign(bob, alice, bob2alice, alice2bob)
803+
val revokedTx = bob.stateData.asInstanceOf[DATA_SHUTDOWN].commitments.localCommit.publishableTxs.commitTx.tx
804+
assert(revokedTx.txOut.size === 3) // two main outputs + 1 htlc
805+
806+
// bob fulfills the second pending htlc (and revokes the previous commitment)
807+
fulfillHtlc(1, r2, bob, alice, bob2alice, alice2bob)
808+
crossSign(bob, alice, bob2alice, alice2bob)
809+
alice2bob.expectMsgType[ClosingSigned] // no more htlcs in the commitment
810+
811+
// bob published the revoked tx
812+
alice ! WatchEventSpent(BITCOIN_FUNDING_SPENT, revokedTx)
813+
alice2bob.expectMsgType[Error]
814+
815+
val mainTx = alice2blockchain.expectMsgType[PublishAsap].tx
816+
val mainPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx
817+
val htlcPenaltyTx = alice2blockchain.expectMsgType[PublishAsap].tx
818+
assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(revokedTx))
819+
assert(alice2blockchain.expectMsgType[WatchConfirmed].event == BITCOIN_TX_CONFIRMED(mainTx))
820+
assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // main-penalty
821+
assert(alice2blockchain.expectMsgType[WatchSpent].event === BITCOIN_OUTPUT_SPENT) // htlc-penalty
822+
alice2blockchain.expectNoMsg(1 second)
823+
824+
Transaction.correctlySpends(mainTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
825+
Transaction.correctlySpends(mainPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
826+
Transaction.correctlySpends(htlcPenaltyTx, Seq(revokedTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
827+
828+
// two main outputs are 300 000 and 200 000, htlcs are 300 000 and 200 000
829+
assert(mainTx.txOut(0).amount === 286660.sat)
830+
assert(mainPenaltyTx.txOut(0).amount === 495160.sat)
831+
assert(htlcPenaltyTx.txOut(0).amount === 194540.sat)
832+
833+
awaitCond(alice.stateName == CLOSING)
834+
assert(alice.stateData.asInstanceOf[DATA_CLOSING].revokedCommitPublished.size == 1)
835+
}
836+
795837
test("recv CMD_CLOSE") { f =>
796838
import f._
797839
val sender = TestProbe()

0 commit comments

Comments
 (0)