Skip to content

Commit

Permalink
Update splice to handle pending committed htlcs
Browse files Browse the repository at this point in the history
  • Loading branch information
remyers committed Aug 7, 2023
1 parent 47e0b83 commit a41254f
Show file tree
Hide file tree
Showing 4 changed files with 36 additions and 27 deletions.
35 changes: 18 additions & 17 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -367,35 +367,36 @@ object Helpers {
fundingTxHash: ByteVector32, fundingTxOutputIndex: Int,
remoteFundingPubKey: PublicKey,
remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] =
makeCommitTxsWithoutHtlcs(keyManager, params,
makeCommitTxs(keyManager, params,
fundingAmount = localFundingAmount + remoteFundingAmount,
toLocal = localFundingAmount.toMilliSatoshi - localPushAmount + remotePushAmount,
toRemote = remoteFundingAmount.toMilliSatoshi + localPushAmount - remotePushAmount,
commitTxFeerate,
fundingTxIndex = 0,
fundingTxHash, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remotePerCommitmentPoint = remoteFirstPerCommitmentPoint, commitmentIndex = 0)
fundingTxHash, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remotePerCommitmentPoint = remoteFirstPerCommitmentPoint, commitmentIndex = 0, Set.empty[DirectedHtlc])

/**
* This creates commitment transactions for both sides at an arbitrary `commitmentIndex`. There are no htlcs, only
* local/remote balances are provided.
* This creates commitment transactions for both sides at an arbitrary `commitmentIndex` and with (optional) `htlc`
* outputs.
*/
def makeCommitTxsWithoutHtlcs(keyManager: ChannelKeyManager,
params: ChannelParams,
fundingAmount: Satoshi,
toLocal: MilliSatoshi, toRemote: MilliSatoshi,
commitTxFeerate: FeeratePerKw,
fundingTxIndex: Long,
fundingTxHash: ByteVector32, fundingTxOutputIndex: Int,
remoteFundingPubKey: PublicKey,
remotePerCommitmentPoint: PublicKey,
commitmentIndex: Long): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
def makeCommitTxs(keyManager: ChannelKeyManager,
params: ChannelParams,
fundingAmount: Satoshi,
toLocal: MilliSatoshi, toRemote: MilliSatoshi,
commitTxFeerate: FeeratePerKw,
fundingTxIndex: Long,
fundingTxHash: ByteVector32, fundingTxOutputIndex: Int,
remoteFundingPubKey: PublicKey,
remotePerCommitmentPoint: PublicKey,
commitmentIndex: Long,
localHtlcs: Set[DirectedHtlc]): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
import params._
val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toLocal, toRemote = toRemote)
val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toRemote, toRemote = toLocal)
val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote)
val remoteSpec = CommitmentSpec(localHtlcs.map(_.opposite), commitTxFeerate, toLocal = toRemote, toRemote = toLocal)

if (!localParams.isInitiator) {
// They initiated the channel open, therefore they pay the fee: we need to make sure they can afford it!
// Note that the reserve may not be always be met: we could be using dual funding with a large funding amount on
// Note that the reserve may not always be met: we could be using dual funding with a large funding amount on
// our side and a small funding amount on their side. But we shouldn't care as long as they can pay the fees for
// the commitment transaction.
val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import fr.acinq.eclair.channel.fund.InteractiveTxBuilder.Purpose
import fr.acinq.eclair.channel.fund.InteractiveTxSigningSession.UnsignedLocalCommit
import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
import fr.acinq.eclair.transactions.Transactions.{CommitTx, HtlcTx, InputInfo, TxOwner}
import fr.acinq.eclair.transactions.{CommitmentSpec, Scripts, Transactions}
import fr.acinq.eclair.transactions.{CommitmentSpec, DirectedHtlc, Scripts, Transactions}
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Logs, MilliSatoshi, MilliSatoshiLong, NodeParams, UInt64}
import scodec.bits.ByteVector
Expand Down Expand Up @@ -165,6 +165,7 @@ object InteractiveTxBuilder {
def remotePerCommitmentPoint: PublicKey
def commitTxFeerate: FeeratePerKw
def fundingTxIndex: Long
def localHtlcs: Set[DirectedHtlc]
}
case class FundingTx(commitTxFeerate: FeeratePerKw, remotePerCommitmentPoint: PublicKey) extends Purpose {
override val previousLocalBalance: MilliSatoshi = 0 msat
Expand All @@ -173,6 +174,7 @@ object InteractiveTxBuilder {
override val localCommitIndex: Long = 0
override val remoteCommitIndex: Long = 0
override val fundingTxIndex: Long = 0
override val localHtlcs: Set[DirectedHtlc] = Set.empty
}
case class SpliceTx(parentCommitment: Commitment) extends Purpose {
override val previousLocalBalance: MilliSatoshi = parentCommitment.localCommit.spec.toLocal
Expand All @@ -183,6 +185,7 @@ object InteractiveTxBuilder {
override val remotePerCommitmentPoint: PublicKey = parentCommitment.remoteCommit.remotePerCommitmentPoint
override val commitTxFeerate: FeeratePerKw = parentCommitment.localCommit.spec.commitTxFeerate
override val fundingTxIndex: Long = parentCommitment.fundingTxIndex + 1
override val localHtlcs: Set[DirectedHtlc] = parentCommitment.localCommit.spec.htlcs
}
/**
* @param previousTransactions interactive transactions are replaceable and can be RBF-ed, but we need to make sure that
Expand All @@ -197,6 +200,7 @@ object InteractiveTxBuilder {
override val remotePerCommitmentPoint: PublicKey = replacedCommitment.remoteCommit.remotePerCommitmentPoint
override val commitTxFeerate: FeeratePerKw = replacedCommitment.localCommit.spec.commitTxFeerate
override val fundingTxIndex: Long = replacedCommitment.fundingTxIndex
override val localHtlcs: Set[DirectedHtlc] = replacedCommitment.localCommit.spec.htlcs
}
// @formatter:on

Expand Down Expand Up @@ -247,9 +251,9 @@ object InteractiveTxBuilder {
case class Remote(serialId: UInt64, amount: Satoshi, pubkeyScript: ByteVector) extends Output with Incoming

/** The shared output can be added by us or by our peer, depending on who initiated the protocol. */
case class Shared(serialId: UInt64, pubkeyScript: ByteVector, localAmount: MilliSatoshi, remoteAmount: MilliSatoshi) extends Output with Incoming with Outgoing {
case class Shared(serialId: UInt64, pubkeyScript: ByteVector, localAmount: MilliSatoshi, remoteAmount: MilliSatoshi, htlcsAmount: MilliSatoshi = 0 msat) extends Output with Incoming with Outgoing {
// Note that the truncation is a no-op: the sum of balances in a channel must be a satoshi amount.
override val amount: Satoshi = (localAmount + remoteAmount).truncateToSatoshi
override val amount: Satoshi = (localAmount + remoteAmount + htlcsAmount).truncateToSatoshi
}
}

Expand Down Expand Up @@ -504,7 +508,8 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
} else if (!MutualClose.isValidFinalScriptPubkey(addOutput.pubkeyScript, allowAnySegwit = true)) {
Left(InvalidSpliceOutputScript(fundingParams.channelId, addOutput.serialId, addOutput.pubkeyScript))
} else if (addOutput.pubkeyScript == fundingPubkeyScript) {
Right(Output.Shared(addOutput.serialId, addOutput.pubkeyScript, purpose.previousLocalBalance + fundingParams.localContribution, purpose.previousRemoteBalance + fundingParams.remoteContribution))
val htlcsAmount = fundingParams.sharedInput_opt.map(_.info.txOut.amount - purpose.previousLocalBalance - purpose.previousRemoteBalance).getOrElse(0 msat)
Right(Output.Shared(addOutput.serialId, addOutput.pubkeyScript, purpose.previousLocalBalance + fundingParams.localContribution, purpose.previousRemoteBalance + fundingParams.remoteContribution, htlcsAmount))
} else {
Right(Output.Remote(addOutput.serialId, addOutput.amount, addOutput.pubkeyScript))
}
Expand Down Expand Up @@ -722,15 +727,15 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
private def signCommitTx(completeTx: SharedTransaction): Behavior[Command] = {
val fundingTx = completeTx.buildUnsignedTx()
val fundingOutputIndex = fundingTx.txOut.indexWhere(_.publicKeyScript == fundingPubkeyScript)
Funding.makeCommitTxsWithoutHtlcs(keyManager, channelParams,
Funding.makeCommitTxs(keyManager, channelParams,
fundingAmount = fundingParams.fundingAmount,
toLocal = completeTx.sharedOutput.localAmount - localPushAmount + remotePushAmount,
toRemote = completeTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount,
purpose.commitTxFeerate,
fundingTxIndex = purpose.fundingTxIndex,
fundingTx.hash, fundingOutputIndex,
remotePerCommitmentPoint = purpose.remotePerCommitmentPoint, remoteFundingPubKey = fundingParams.remoteFundingPubKey,
commitmentIndex = purpose.localCommitIndex) match {
commitmentIndex = purpose.localCommitIndex, localHtlcs = purpose.localHtlcs) match {
case Left(cause) =>
replyTo ! RemoteFailure(cause)
unlockAndStop(completeTx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import fr.acinq.eclair.blockchain.fee.FeeratePerKw
import fr.acinq.eclair.channel.fund.InteractiveTxBuilder._
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.wire.protocol.TxAddInput
import fr.acinq.eclair.{Logs, UInt64}
import fr.acinq.eclair.{Logs, MilliSatoshiLong, UInt64}
import scodec.bits.ByteVector

import scala.concurrent.{ExecutionContext, Future}
Expand Down Expand Up @@ -157,7 +157,8 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response
// contribute to the RBF attempt.
if (fundingParams.isInitiator) {
val sharedInput = fundingParams.sharedInput_opt.toSeq.map(sharedInput => Input.Shared(UInt64(0), sharedInput.info.outPoint, 0xfffffffdL, purpose.previousLocalBalance, purpose.previousRemoteBalance))
val sharedOutput = Output.Shared(UInt64(0), fundingPubkeyScript, purpose.previousLocalBalance + fundingParams.localContribution, purpose.previousRemoteBalance + fundingParams.remoteContribution)
val htlcsAmount = fundingParams.sharedInput_opt.map(_.info.txOut.amount - purpose.previousLocalBalance - purpose.previousRemoteBalance).getOrElse(0 msat)
val sharedOutput = Output.Shared(UInt64(0), fundingPubkeyScript, purpose.previousLocalBalance + fundingParams.localContribution, purpose.previousRemoteBalance + fundingParams.remoteContribution, htlcsAmount)
val nonChangeOutputs = fundingParams.localOutputs.map(txOut => Output.Local.NonChange(UInt64(0), txOut.amount, txOut.publicKeyScript))
val fundingContributions = sortFundingContributions(fundingParams, sharedInput ++ previousWalletInputs, sharedOutput +: nonChangeOutputs)
replyTo ! fundingContributions
Expand Down Expand Up @@ -232,7 +233,8 @@ private class InteractiveTxFunder(replyTo: ActorRef[InteractiveTxFunder.Response
val fundingContributions = if (fundingParams.isInitiator) {
// The initiator is responsible for adding the shared output and the shared input.
val inputs = inputDetails.usableInputs
val fundingOutput = Output.Shared(UInt64(0), fundingPubkeyScript, purpose.previousLocalBalance + fundingParams.localContribution, purpose.previousRemoteBalance + fundingParams.remoteContribution)
val htlcsAmount = fundingParams.sharedInput_opt.map(_.info.txOut.amount - purpose.previousLocalBalance - purpose.previousRemoteBalance).getOrElse(0 msat)
val fundingOutput = Output.Shared(UInt64(0), fundingPubkeyScript, purpose.previousLocalBalance + fundingParams.localContribution, purpose.previousRemoteBalance + fundingParams.remoteContribution, htlcsAmount)
val outputs = Seq(fundingOutput) ++ nonChangeOutputs ++ changeOutput_opt.toSeq
sortFundingContributions(fundingParams, inputs, outputs)
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ private[channel] object ChannelCodecs4 {
("serialId" | uint64) ::
("scriptPubKey" | lengthDelimited(bytes)) ::
("localAmount" | millisatoshi) ::
("remoteAmount" | millisatoshi)).as[InteractiveTxBuilder.Output.Shared])
("remoteAmount" | millisatoshi) ::
("htlcsAmount" | millisatoshi)).as[InteractiveTxBuilder.Output.Shared])

private val localOnlyInteractiveTxInputCodec: Codec[InteractiveTxBuilder.Input.Local] = (
("serialId" | uint64) ::
Expand Down

0 comments on commit a41254f

Please sign in to comment.