Skip to content

Commit 3b714df

Browse files
authored
Refactor HTLC-penalty txs creation (#3071)
We previously split the creation of HTLC-penalty transactions between `Helpers.scala` and `Transactions.scala`. It makes more sense to group everything in `Transactions.scala` and is a pre-requisite for the next wave of transaction refactoring.
1 parent 76eb6cb commit 3b714df

File tree

3 files changed

+59
-67
lines changed

3 files changed

+59
-67
lines changed

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

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ import fr.acinq.eclair.blockchain.OnChainPubkeyCache
2626
import fr.acinq.eclair.blockchain.fee._
2727
import fr.acinq.eclair.channel.fsm.Channel
2828
import fr.acinq.eclair.channel.fsm.Channel.REFRESH_CHANNEL_UPDATE_INTERVAL
29-
import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
3029
import fr.acinq.eclair.crypto.ShaChain
30+
import fr.acinq.eclair.crypto.keymanager.{ChannelKeys, LocalCommitmentKeys, RemoteCommitmentKeys}
3131
import fr.acinq.eclair.db.ChannelsDb
3232
import fr.acinq.eclair.payment.relay.Relayer.RelayFees
3333
import fr.acinq.eclair.router.Announcements
@@ -1170,10 +1170,10 @@ object Helpers {
11701170
val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret)
11711171

11721172
val feerateMain = onChainFeeConf.getClosingFeerate(feerates)
1173-
// we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty
1173+
// We need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty.
11741174
val feeratePenalty = feerates.fast
11751175

1176-
// first we will claim our main output right away
1176+
// First we will claim our main output right away.
11771177
val mainTx = commitKeys.ourPaymentKey match {
11781178
case Left(_) =>
11791179
log.info("channel uses option_static_remotekey to pay directly to our wallet, there is nothing to do")
@@ -1194,34 +1194,24 @@ object Helpers {
11941194
}
11951195
}
11961196

1197-
// then we punish them by stealing their main output
1197+
// Then we punish them by stealing their main output.
11981198
val mainPenaltyTx = withTxGenerationLog("main-penalty") {
11991199
Transactions.makeMainPenaltyTx(commitKeys, commitTx, localParams.dustLimit, finalScriptPubKey, localParams.toSelfDelay, feeratePenalty).map(txinfo => {
12001200
val sig = txinfo.sign(revocationKey, TxOwner.Local, commitmentFormat, Map.empty)
12011201
txinfo.addSigs(sig)
12021202
})
12031203
}
12041204

1205-
// we retrieve the information needed to rebuild htlc scripts
1205+
// We retrieve the historical information needed to rebuild htlc scripts.
12061206
val htlcInfos = db.listHtlcInfos(channelId, commitmentNumber)
12071207
log.info("got {} htlcs for commitmentNumber={}", htlcInfos.size, commitmentNumber)
1208-
val htlcsRedeemScripts = (
1209-
htlcInfos.map { case (paymentHash, cltvExpiry) => Scripts.htlcReceived(commitKeys.publicKeys, paymentHash, cltvExpiry, commitmentFormat) } ++
1210-
htlcInfos.map { case (paymentHash, _) => Scripts.htlcOffered(commitKeys.publicKeys, paymentHash, commitmentFormat) }
1211-
)
1212-
.map(redeemScript => Script.write(pay2wsh(redeemScript)) -> Script.write(redeemScript))
1213-
.toMap
1214-
1215-
// and finally we steal the htlc outputs
1216-
val htlcPenaltyTxs = commitTx.txOut.zipWithIndex.collect { case (txOut, outputIndex) if htlcsRedeemScripts.contains(txOut.publicKeyScript) =>
1217-
val htlcRedeemScript = htlcsRedeemScripts(txOut.publicKeyScript)
1208+
// And finally we steal the htlc outputs.
1209+
val htlcPenaltyTxs = Transactions.makeHtlcPenaltyTxs(commitKeys, commitTx, htlcInfos, localParams.dustLimit, finalScriptPubKey, feeratePenalty, commitmentFormat).flatMap { htlcPenalty =>
12181210
withTxGenerationLog("htlc-penalty") {
1219-
Transactions.makeHtlcPenaltyTx(commitKeys, commitTx, outputIndex, htlcRedeemScript, localParams.dustLimit, finalScriptPubKey, feeratePenalty).map(htlcPenalty => {
1220-
val sig = htlcPenalty.sign(revocationKey, TxOwner.Local, commitmentFormat, Map.empty)
1221-
htlcPenalty.addSigs(commitKeys, sig)
1222-
})
1211+
val sig = htlcPenalty.sign(revocationKey, TxOwner.Local, commitmentFormat, Map.empty)
1212+
Right(htlcPenalty.addSigs(commitKeys, sig))
12231213
}
1224-
}.toList.flatten
1214+
}.toList
12251215

12261216
RevokedCommitPublished(
12271217
commitTx = commitTx,

eclair-core/src/main/scala/fr/acinq/eclair/transactions/Transactions.scala

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -868,6 +868,7 @@ object Transactions {
868868
def makeClaimHtlcDelayedOutputPenaltyTxs(keys: RemoteCommitmentKeys, htlcTx: Transaction, localDustLimit: Satoshi, toLocalDelay: CltvExpiryDelta, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Seq[Either[TxGenerationSkipped, ClaimHtlcDelayedOutputPenaltyTx]] = {
869869
val redeemScript = toLocalDelayed(keys.publicKeys, toLocalDelay)
870870
val pubkeyScript = write(pay2wsh(redeemScript))
871+
// Note that we check *all* outputs of the tx, because it could spend a batch of HTLC outputs from the commit tx.
871872
findPubKeyScriptIndexes(htlcTx, pubkeyScript) match {
872873
case Left(skip) => Seq(Left(skip))
873874
case Right(outputIndexes) => outputIndexes.map(outputIndex => {
@@ -908,7 +909,35 @@ object Transactions {
908909
}
909910
}
910911

911-
def makeHtlcPenaltyTx(keys: RemoteCommitmentKeys, commitTx: Transaction, htlcOutputIndex: Int, redeemScript: ByteVector, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, HtlcPenaltyTx] = {
912+
def makeHtlcPenaltyTxs(keys: RemoteCommitmentKeys,
913+
commitTx: Transaction,
914+
htlcs: Seq[(ByteVector32, CltvExpiry)],
915+
localDustLimit: Satoshi,
916+
localFinalScriptPubKey: ByteVector,
917+
feeratePerKw: FeeratePerKw,
918+
commitmentFormat: CommitmentFormat): Seq[HtlcPenaltyTx] = {
919+
// We create the output scripts for the corresponding HTLCs.
920+
val redeemInfos: Map[ByteVector, (ByteVector, ByteVector32, CltvExpiry)] = htlcs.flatMap {
921+
case (paymentHash, expiry) =>
922+
// We don't know if this was an incoming or outgoing HTLC, so we try both cases.
923+
val offered = htlcOffered(keys.publicKeys, paymentHash, commitmentFormat)
924+
val received = htlcReceived(keys.publicKeys, paymentHash, expiry, commitmentFormat)
925+
Seq(
926+
write(pay2wsh(offered)) -> (write(offered), paymentHash, expiry),
927+
write(pay2wsh(received)) -> (write(received), paymentHash, expiry)
928+
)
929+
}.toMap
930+
// We check every output of the commitment transaction, and create an HTLC-penalty transaction if it is an HTLC output.
931+
commitTx.txOut.zipWithIndex.map {
932+
case (txOut, outputIndex) =>
933+
redeemInfos.get(txOut.publicKeyScript) match {
934+
case Some((redeemScript, _, _)) => makeHtlcPenaltyTx(keys, commitTx, outputIndex, redeemScript, localDustLimit, localFinalScriptPubKey, feeratePerKw)
935+
case None => Left(OutputNotFound)
936+
}
937+
}.collect { case Right(tx) => tx }
938+
}
939+
940+
private def makeHtlcPenaltyTx(keys: RemoteCommitmentKeys, commitTx: Transaction, htlcOutputIndex: Int, redeemScript: ByteVector, localDustLimit: Satoshi, localFinalScriptPubKey: ByteVector, feeratePerKw: FeeratePerKw): Either[TxGenerationSkipped, HtlcPenaltyTx] = {
912941
val input = InputInfo(OutPoint(commitTx, htlcOutputIndex), commitTx.txOut(htlcOutputIndex), redeemScript)
913942
val unsignedTx = Transaction(
914943
version = 2,

eclair-core/src/test/scala/fr/acinq/eclair/transactions/TransactionsSpec.scala

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import fr.acinq.eclair._
2626
import fr.acinq.eclair.blockchain.fee.{ConfirmationTarget, FeeratePerKw}
2727
import fr.acinq.eclair.channel.Helpers.Funding
2828
import fr.acinq.eclair.crypto.keymanager.{LocalCommitmentKeys, RemoteCommitmentKeys}
29-
import fr.acinq.eclair.transactions.CommitmentOutput.{InHtlc, OutHtlc}
29+
import fr.acinq.eclair.transactions.CommitmentOutput.OutHtlc
3030
import fr.acinq.eclair.transactions.Scripts._
3131
import fr.acinq.eclair.transactions.Transactions.AnchorOutputsCommitmentFormat.anchorAmount
3232
import fr.acinq.eclair.transactions.Transactions._
@@ -174,9 +174,10 @@ class TransactionsSpec extends AnyFunSuite with Logging {
174174
val redeemScript = htlcReceived(localKeys.publicKeys, htlc.paymentHash, htlc.cltvExpiry, DefaultCommitmentFormat)
175175
val pubKeyScript = write(pay2wsh(redeemScript))
176176
val commitTx = Transaction(version = 2, txIn = Nil, txOut = TxOut(htlc.amountMsat.truncateToSatoshi, pubKeyScript) :: Nil, lockTime = 0)
177-
val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(remoteKeys, commitTx, 0, Script.write(redeemScript), localDustLimit, finalPubKeyScript, feeratePerKw)
177+
val htlcPenaltyTxs = makeHtlcPenaltyTxs(remoteKeys, commitTx, Seq((htlc.paymentHash, htlc.cltvExpiry)), localDustLimit, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat)
178+
assert(htlcPenaltyTxs.size == 1)
178179
// we use dummy signatures to compute the weight
179-
val weight = htlcPenaltyTx.addSigs(remoteKeys, PlaceHolderSig).tx.weight()
180+
val weight = htlcPenaltyTxs.head.addSigs(remoteKeys, PlaceHolderSig).tx.weight()
180181
assert(htlcPenaltyWeight == weight)
181182
}
182183
{
@@ -414,16 +415,15 @@ class TransactionsSpec extends AnyFunSuite with Logging {
414415
assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit)))
415416
}
416417
{
417-
// remote spends offered HTLC output with revocation key
418-
val script = Script.write(Scripts.htlcOffered(remoteKeys.publicKeys, htlc1.paymentHash, DefaultCommitmentFormat))
419-
val Some(htlcOutputIndex) = outputs.zipWithIndex.find {
420-
case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(someHtlc))), _) => someHtlc.id == htlc1.id
421-
case _ => false
422-
}.map(_._2)
423-
val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(remoteKeys, commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw)
424-
val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty)
425-
val signed = htlcPenaltyTx.addSigs(remoteKeys, sig)
426-
assert(checkSpendable(signed).isSuccess)
418+
// remote spends HTLC outputs with revocation key
419+
val htlcs = spec.htlcs.map(_.add).map(add => (add.paymentHash, add.cltvExpiry)).toSeq
420+
val htlcPenaltyTxs = makeHtlcPenaltyTxs(remoteKeys, commitTx.tx, htlcs, localDustLimit, finalPubKeyScript, feeratePerKw, DefaultCommitmentFormat)
421+
assert(htlcPenaltyTxs.size == 4) // the first 4 htlcs are above the dust limit
422+
htlcPenaltyTxs.foreach(htlcPenaltyTx => {
423+
val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty)
424+
val signed = htlcPenaltyTx.addSigs(remoteKeys, sig)
425+
assert(checkSpendable(signed).isSuccess)
426+
})
427427
}
428428
{
429429
// remote spends htlc2's htlc-success tx with revocation key
@@ -435,18 +435,6 @@ class TransactionsSpec extends AnyFunSuite with Logging {
435435
val claimHtlcDelayedPenaltyTx1 = makeClaimHtlcDelayedOutputPenaltyTxs(remoteKeys, htlcSuccessTxs(0).tx, localDustLimit, toLocalDelay, finalPubKeyScript, feeratePerKw)
436436
assert(claimHtlcDelayedPenaltyTx1 == Seq(Left(AmountBelowDustLimit)))
437437
}
438-
{
439-
// remote spends received HTLC output with revocation key
440-
val script = Script.write(Scripts.htlcReceived(remoteKeys.publicKeys, htlc2.paymentHash, htlc2.cltvExpiry, DefaultCommitmentFormat))
441-
val Some(htlcOutputIndex) = outputs.zipWithIndex.find {
442-
case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(someHtlc))), _) => someHtlc.id == htlc2.id
443-
case _ => false
444-
}.map(_._2)
445-
val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(remoteKeys, commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw)
446-
val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, DefaultCommitmentFormat, Map.empty)
447-
val signed = htlcPenaltyTx.addSigs(remoteKeys, sig)
448-
assert(checkSpendable(signed).isSuccess)
449-
}
450438
}
451439

452440
test("generate valid commitment with some outputs that don't materialize (anchor outputs)") {
@@ -748,30 +736,15 @@ class TransactionsSpec extends AnyFunSuite with Logging {
748736
assert(claimed.map(_.input.outPoint).toSet.size == 3)
749737
}
750738
{
751-
// remote spends offered htlc output with revocation key
752-
val script = Script.write(Scripts.htlcOffered(remoteKeys.publicKeys, htlc1.paymentHash, UnsafeLegacyAnchorOutputsCommitmentFormat))
753-
val Some(htlcOutputIndex) = commitTxOutputs.zipWithIndex.find {
754-
case (CommitmentOutputLink(_, _, OutHtlc(OutgoingHtlc(someHtlc))), _) => someHtlc.id == htlc1.id
755-
case _ => false
756-
}.map(_._2)
757-
val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(remoteKeys, commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw)
758-
val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty)
759-
val signed = htlcPenaltyTx.addSigs(remoteKeys, sig)
760-
assert(checkSpendable(signed).isSuccess)
761-
}
762-
{
763-
// remote spends received htlc output with revocation key
764-
for (htlc <- Seq(htlc2a, htlc2b)) {
765-
val script = Script.write(Scripts.htlcReceived(remoteKeys.publicKeys, htlc.paymentHash, htlc.cltvExpiry, UnsafeLegacyAnchorOutputsCommitmentFormat))
766-
val Some(htlcOutputIndex) = commitTxOutputs.zipWithIndex.find {
767-
case (CommitmentOutputLink(_, _, InHtlc(IncomingHtlc(someHtlc))), _) => someHtlc.id == htlc.id
768-
case _ => false
769-
}.map(_._2)
770-
val Right(htlcPenaltyTx) = makeHtlcPenaltyTx(remoteKeys, commitTx.tx, htlcOutputIndex, script, localDustLimit, finalPubKeyScript, feeratePerKw)
739+
// remote spends htlc outputs with revocation key
740+
val htlcs = spec.htlcs.map(_.add).map(add => (add.paymentHash, add.cltvExpiry)).toSeq
741+
val htlcPenaltyTxs = makeHtlcPenaltyTxs(remoteKeys, commitTx.tx, htlcs, localDustLimit, finalPubKeyScript, feeratePerKw, UnsafeLegacyAnchorOutputsCommitmentFormat)
742+
assert(htlcPenaltyTxs.size == 5) // the first 5 htlcs are above the dust limit
743+
htlcPenaltyTxs.foreach(htlcPenaltyTx => {
771744
val sig = htlcPenaltyTx.sign(localRevocationPriv, TxOwner.Local, UnsafeLegacyAnchorOutputsCommitmentFormat, Map.empty)
772745
val signed = htlcPenaltyTx.addSigs(remoteKeys, sig)
773746
assert(checkSpendable(signed).isSuccess)
774-
}
747+
})
775748
}
776749
}
777750

0 commit comments

Comments
 (0)