Skip to content

Commit 5e35a21

Browse files
committed
Remove spurious interactive-tx commit_sig retransmission
We fully implement lightning/bolts#1214 to stop retransmitting `commit_sig` when our peer has already received it. We also correctly set `next_commitment_number` to let our peer know whether we have received their `commit_sig` or not. We also retransmit `tx_signatures` (and, if requested, `commit_sig`) after sending `channel_ready` in the 0-conf case. This was missing and was a bug.
1 parent 26f06c9 commit 5e35a21

File tree

6 files changed

+341
-154
lines changed

6 files changed

+341
-154
lines changed

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

+14-7
Original file line numberDiff line numberDiff line change
@@ -1296,20 +1296,27 @@ object Helpers {
12961296
* - add is the htlc in the downstream channel from which we extracted the preimage
12971297
* - preimage needs to be sent to the upstream channel
12981298
*/
1299-
def extractPreimages(localCommit: LocalCommit, tx: Transaction)(implicit log: LoggingAdapter): Set[(UpdateAddHtlc, ByteVector32)] = {
1299+
def extractPreimages(commitment: FullCommitment, tx: Transaction)(implicit log: LoggingAdapter): Set[(UpdateAddHtlc, ByteVector32)] = {
13001300
val htlcSuccess = tx.txIn.map(_.witness).collect(Scripts.extractPreimageFromHtlcSuccess)
13011301
htlcSuccess.foreach(r => log.info(s"extracted paymentPreimage=$r from tx=$tx (htlc-success)"))
13021302
val claimHtlcSuccess = tx.txIn.map(_.witness).collect(Scripts.extractPreimageFromClaimHtlcSuccess)
13031303
claimHtlcSuccess.foreach(r => log.info(s"extracted paymentPreimage=$r from tx=$tx (claim-htlc-success)"))
13041304
val paymentPreimages = (htlcSuccess ++ claimHtlcSuccess).toSet
13051305
paymentPreimages.flatMap { paymentPreimage =>
1306-
// we only consider htlcs in our local commitment, because we only care about outgoing htlcs, which disappear first in the remote commitment
1307-
// if an outgoing htlc is in the remote commitment, then:
1308-
// - either it is in the local commitment (it was never fulfilled)
1309-
// - or we have already received the fulfill and forwarded it upstream
1310-
localCommit.spec.htlcs.collect {
1311-
case OutgoingHtlc(add) if add.paymentHash == sha256(paymentPreimage) => (add, paymentPreimage)
1306+
val paymentHash = sha256(paymentPreimage)
1307+
// We only care about outgoing HTLCs when we're trying to learn a preimage to relay upstream.
1308+
// Note that we may have already relayed the fulfill upstream if we already saw the preimage.
1309+
val fromLocal = commitment.localCommit.spec.htlcs.collect {
1310+
case OutgoingHtlc(add) if add.paymentHash == paymentHash => (add, paymentPreimage)
13121311
}
1312+
// From the remote point of view, those are incoming HTLCs.
1313+
val fromRemote = commitment.remoteCommit.spec.htlcs.collect {
1314+
case IncomingHtlc(add) if add.paymentHash == paymentHash => (add, paymentPreimage)
1315+
}
1316+
val fromNextRemote = commitment.nextRemoteCommit_opt.map(_.commit.spec.htlcs).getOrElse(Set.empty).collect {
1317+
case IncomingHtlc(add) if add.paymentHash == paymentHash => (add, paymentPreimage)
1318+
}
1319+
fromLocal ++ fromRemote ++ fromNextRemote
13131320
}
13141321
}
13151322

eclair-core/src/main/scala/fr/acinq/eclair/channel/fsm/Channel.scala

+71-32
Original file line numberDiff line numberDiff line change
@@ -2023,7 +2023,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
20232023
// we watch it in order to extract payment preimage if funds are pulled by the counterparty
20242024
// we can then use these preimages to fulfill origin htlcs
20252025
log.debug(s"processing bitcoin output spent by txid=${tx.txid} tx=$tx")
2026-
val extracted = Closing.extractPreimages(d.commitments.latest.localCommit, tx)
2026+
val extracted = Closing.extractPreimages(d.commitments.latest, tx)
20272027
extracted.foreach { case (htlc, preimage) =>
20282028
d.commitments.originChannels.get(htlc.id) match {
20292029
case Some(origin) =>
@@ -2243,7 +2243,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
22432243
val nextFundingTlv: Set[ChannelReestablishTlv] = Set(ChannelReestablishTlv.NextFundingTlv(d.signingSession.fundingTx.txId))
22442244
val channelReestablish = ChannelReestablish(
22452245
channelId = d.channelId,
2246-
nextLocalCommitmentNumber = 1,
2246+
nextLocalCommitmentNumber = d.signingSession.nextLocalCommitmentNumber,
22472247
nextRemoteRevocationNumber = 0,
22482248
yourLastPerCommitmentSecret = PrivateKey(ByteVector32.Zeroes),
22492249
myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint,
@@ -2258,6 +2258,19 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
22582258
val yourLastPerCommitmentSecret = remotePerCommitmentSecrets.lastIndex.flatMap(remotePerCommitmentSecrets.getHash).getOrElse(ByteVector32.Zeroes)
22592259
val channelKeyPath = keyManager.keyPath(d.commitments.params.localParams, d.commitments.params.channelConfig)
22602260
val myCurrentPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, d.commitments.localCommitIndex)
2261+
// If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig.
2262+
val nextLocalCommitmentNumber = d match {
2263+
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match {
2264+
case DualFundingStatus.RbfWaitingForSigs(status) => status.nextLocalCommitmentNumber
2265+
case _ => d.commitments.localCommitIndex + 1
2266+
}
2267+
case d: DATA_NORMAL => d.spliceStatus match {
2268+
case SpliceStatus.SpliceWaitingForSigs(status) => status.nextLocalCommitmentNumber
2269+
case _ => d.commitments.localCommitIndex + 1
2270+
}
2271+
case _ => d.commitments.localCommitIndex + 1
2272+
}
2273+
// If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures.
22612274
val rbfTlv: Set[ChannelReestablishTlv] = d match {
22622275
case d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED => d.status match {
22632276
case DualFundingStatus.RbfWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId))
@@ -2282,7 +2295,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
22822295

22832296
val channelReestablish = ChannelReestablish(
22842297
channelId = d.channelId,
2285-
nextLocalCommitmentNumber = d.commitments.localCommitIndex + 1,
2298+
nextLocalCommitmentNumber = nextLocalCommitmentNumber,
22862299
nextRemoteRevocationNumber = d.commitments.remoteCommitIndex,
22872300
yourLastPerCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret),
22882301
myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint,
@@ -2323,8 +2336,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
23232336

23242337
case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_SIGNED) =>
23252338
channelReestablish.nextFundingTxId_opt match {
2326-
case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId =>
2327-
// We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig.
2339+
case Some(fundingTxId) if fundingTxId == d.signingSession.fundingTx.txId && channelReestablish.nextLocalCommitmentNumber == 0 =>
2340+
// They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received
2341+
// their commit_sig or their tx_signatures (depending on who must send tx_signatures first).
23282342
val commitSig = d.signingSession.remoteCommit.sign(keyManager, d.channelParams, d.signingSession.fundingTxIndex, d.signingSession.fundingParams.remoteFundingPubKey, d.signingSession.commitInput)
23292343
goto(WAIT_FOR_DUAL_FUNDING_SIGNED) sending commitSig
23302344
case _ => goto(WAIT_FOR_DUAL_FUNDING_SIGNED)
@@ -2335,20 +2349,25 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
23352349
case Some(fundingTxId) =>
23362350
d.status match {
23372351
case DualFundingStatus.RbfWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId =>
2338-
// We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig.
2339-
val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput)
2340-
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig
2352+
if (channelReestablish.nextLocalCommitmentNumber == 0) {
2353+
// They haven't received our commit_sig: we retransmit it.
2354+
// We're also waiting for signatures from them, and will send our tx_signatures once we receive them.
2355+
val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput)
2356+
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending commitSig
2357+
} else {
2358+
// They have already received our commit_sig, but we were waiting for them to send either commit_sig or
2359+
// tx_signatures first. We wait for their message before sending our tx_signatures.
2360+
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED)
2361+
}
23412362
case _ if d.latestFundingTx.sharedTx.txId == fundingTxId =>
2342-
val toSend = d.latestFundingTx.sharedTx match {
2343-
case fundingTx: InteractiveTxBuilder.PartiallySignedSharedTransaction =>
2344-
// We have not received their tx_signatures: we retransmit our commit_sig because we don't know if they received it.
2345-
val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
2346-
Seq(commitSig, fundingTx.localSigs)
2347-
case fundingTx: InteractiveTxBuilder.FullySignedSharedTransaction =>
2348-
// We've already received their tx_signatures, which means they've received and stored our commit_sig, we only need to retransmit our tx_signatures.
2349-
Seq(fundingTx.localSigs)
2363+
// We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
2364+
// and our commit_sig if they haven't received it already.
2365+
if (channelReestablish.nextLocalCommitmentNumber == 0) {
2366+
val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
2367+
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending Seq(commitSig, d.latestFundingTx.sharedTx.localSigs)
2368+
} else {
2369+
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending d.latestFundingTx.sharedTx.localSigs
23502370
}
2351-
goto(WAIT_FOR_DUAL_FUNDING_CONFIRMED) sending toSend
23522371
case _ =>
23532372
// The fundingTxId must be for an RBF attempt that we didn't store (we got disconnected before receiving
23542373
// their tx_complete): we tell them to abort that RBF attempt.
@@ -2362,10 +2381,27 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
23622381
val channelReady = createChannelReady(d.aliases, d.commitments.params)
23632382
goto(WAIT_FOR_CHANNEL_READY) sending channelReady
23642383

2365-
case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) =>
2384+
case Event(channelReestablish: ChannelReestablish, d: DATA_WAIT_FOR_DUAL_FUNDING_READY) =>
23662385
log.debug("re-sending channel_ready")
23672386
val channelReady = createChannelReady(d.aliases, d.commitments.params)
2368-
goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
2387+
// We've already received their commit_sig and sent our tx_signatures. We retransmit our tx_signatures
2388+
// and our commit_sig if they haven't received it already.
2389+
channelReestablish.nextFundingTxId_opt match {
2390+
case Some(fundingTxId) if fundingTxId == d.commitments.latest.fundingTxId =>
2391+
d.commitments.latest.localFundingStatus.localSigs_opt match {
2392+
case Some(txSigs) if channelReestablish.nextLocalCommitmentNumber == 0 =>
2393+
log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
2394+
val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
2395+
goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(commitSig, txSigs, channelReady)
2396+
case Some(txSigs) =>
2397+
log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
2398+
goto(WAIT_FOR_DUAL_FUNDING_READY) sending Seq(txSigs, channelReady)
2399+
case None =>
2400+
log.warning("cannot retransmit tx_signatures, we don't have them (status={})", d.commitments.latest.localFundingStatus)
2401+
goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
2402+
}
2403+
case _ => goto(WAIT_FOR_DUAL_FUNDING_READY) sending channelReady
2404+
}
23692405

23702406
case Event(channelReestablish: ChannelReestablish, d: DATA_NORMAL) =>
23712407
Syncing.checkSync(keyManager, d.commitments, channelReestablish) match {
@@ -2413,23 +2449,26 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
24132449
case Some(fundingTxId) =>
24142450
d.spliceStatus match {
24152451
case SpliceStatus.SpliceWaitingForSigs(signingSession) if signingSession.fundingTx.txId == fundingTxId =>
2416-
// We retransmit our commit_sig, and will send our tx_signatures once we've received their commit_sig.
2417-
log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId)
2418-
val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput)
2419-
sendQueue = sendQueue :+ commitSig
2452+
if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) {
2453+
// They haven't received our commit_sig: we retransmit it.
2454+
// We're also waiting for signatures from them, and will send our tx_signatures once we receive them.
2455+
log.info("re-sending commit_sig for splice attempt with fundingTxIndex={} fundingTxId={}", signingSession.fundingTxIndex, signingSession.fundingTx.txId)
2456+
val commitSig = signingSession.remoteCommit.sign(keyManager, d.commitments.params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubKey, signingSession.commitInput)
2457+
sendQueue = sendQueue :+ commitSig
2458+
}
24202459
d.spliceStatus
24212460
case _ if d.commitments.latest.fundingTxId == fundingTxId =>
24222461
d.commitments.latest.localFundingStatus match {
24232462
case dfu: LocalFundingStatus.DualFundedUnconfirmedFundingTx =>
2424-
dfu.sharedTx match {
2425-
case fundingTx: InteractiveTxBuilder.PartiallySignedSharedTransaction =>
2426-
// If we have not received their tx_signatures, we can't tell whether they had received our commit_sig, so we need to retransmit it
2427-
log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
2428-
val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
2429-
sendQueue = sendQueue :+ commitSig :+ fundingTx.localSigs
2430-
case fundingTx: InteractiveTxBuilder.FullySignedSharedTransaction =>
2431-
log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
2432-
sendQueue = sendQueue :+ fundingTx.localSigs
2463+
// We've already received their commit_sig and sent our tx_signatures. We retransmit our
2464+
// tx_signatures and our commit_sig if they haven't received it already.
2465+
if (channelReestablish.nextLocalCommitmentNumber == d.commitments.remoteCommitIndex) {
2466+
log.info("re-sending commit_sig and tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
2467+
val commitSig = d.commitments.latest.remoteCommit.sign(keyManager, d.commitments.params, d.commitments.latest.fundingTxIndex, d.commitments.latest.remoteFundingPubKey, d.commitments.latest.commitInput)
2468+
sendQueue = sendQueue :+ commitSig :+ dfu.sharedTx.localSigs
2469+
} else {
2470+
log.info("re-sending tx_signatures for fundingTxIndex={} fundingTxId={}", d.commitments.latest.fundingTxIndex, d.commitments.latest.fundingTxId)
2471+
sendQueue = sendQueue :+ dfu.sharedTx.localSigs
24332472
}
24342473
case fundingStatus =>
24352474
// They have not received our tx_signatures, but they must have received our commit_sig, otherwise we would be in the case above.

eclair-core/src/main/scala/fr/acinq/eclair/channel/fund/InteractiveTxBuilder.scala

+5
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,11 @@ object InteractiveTxSigningSession {
10821082
liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession {
10831083
val commitInput: InputInfo = localCommit.fold(_.commitTx.input, _.commitTxAndRemoteSig.commitTx.input)
10841084
val localCommitIndex: Long = localCommit.fold(_.index, _.index)
1085+
// This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not.
1086+
val nextLocalCommitmentNumber: Long = localCommit match {
1087+
case Left(unsignedCommit) => unsignedCommit.index
1088+
case Right(commit) => commit.index + 1
1089+
}
10851090

10861091
def receiveCommitSig(nodeParams: NodeParams, channelParams: ChannelParams, remoteCommitSig: CommitSig)(implicit log: LoggingAdapter): Either[ChannelException, InteractiveTxSigningSession] = {
10871092
localCommit match {

0 commit comments

Comments
 (0)