Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Splice with pending committed htlcs #2720

Merged
merged 1 commit into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,30 @@ case class CommitTxAndRemoteSig(commitTx: CommitTx, remoteSig: ByteVector64)
/** The local commitment maps to a commitment transaction that we can sign and broadcast if necessary. */
case class LocalCommit(index: Long, spec: CommitmentSpec, commitTxAndRemoteSig: CommitTxAndRemoteSig, htlcTxsAndRemoteSigs: List[HtlcTxAndRemoteSig])

object LocalCommit {
def fromCommitSig(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxId: ByteVector32,
fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo,
commit: CommitSig, localCommitIndex: Long, spec: CommitmentSpec, localPerCommitmentPoint: PublicKey): Either[ChannelException, LocalCommit] = {
val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommitIndex, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec)
if (!checkSig(localCommitTx, commit.signature, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) {
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
}
val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index)
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
return Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size))
}
val remoteHtlcPubkey = Generators.derivePubKey(params.remoteParams.htlcBasepoint, localPerCommitmentPoint)
val htlcTxsAndRemoteSigs = sortedHtlcTxs.zip(commit.htlcSignatures).toList.map {
case (htlcTx: HtlcTx, remoteSig) =>
if (!checkSig(htlcTx, remoteSig, remoteHtlcPubkey, TxOwner.Remote, params.commitmentFormat)) {
return Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid))
}
HtlcTxAndRemoteSig(htlcTx, remoteSig)
}
Right(LocalCommit(localCommitIndex, spec, CommitTxAndRemoteSig(localCommitTx, commit.signature), htlcTxsAndRemoteSigs))
}
}

/** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */
case class RemoteCommit(index: Long, spec: CommitmentSpec, txid: ByteVector32, remotePerCommitmentPoint: PublicKey) {
def sign(keyManager: ChannelKeyManager, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: InputInfo): CommitSig = {
Expand Down Expand Up @@ -619,27 +643,12 @@ case class Commitment(fundingTxIndex: Long,
// ourCommit.index + 2 -> which is about to become our next revocation hash
// we will reply to this sig with our old revocation hash preimage (at index) and our next revocation hash (at index + 1)
// and will increment our index
val localCommitIndex = localCommit.index + 1
val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed)
val (localCommitTx, htlcTxs) = Commitment.makeLocalTxs(keyManager, params.channelConfig, params.channelFeatures, localCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex, remoteFundingPubKey, commitInput, localPerCommitmentPoint, spec)
log.info(s"built local commit number=${localCommit.index + 1} toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","))
if (!checkSig(localCommitTx, commit.signature, remoteFundingPubKey, TxOwner.Remote, params.commitmentFormat)) {
return Left(InvalidCommitmentSignature(params.channelId, fundingTxId, fundingTxIndex, localCommitTx.tx))
}
val sortedHtlcTxs: Seq[HtlcTx] = htlcTxs.sortBy(_.input.outPoint.index)
if (commit.htlcSignatures.size != sortedHtlcTxs.size) {
return Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size))
}
val remoteHtlcPubkey = Generators.derivePubKey(params.remoteParams.htlcBasepoint, localPerCommitmentPoint)
val htlcTxsAndRemoteSigs = sortedHtlcTxs.zip(commit.htlcSignatures).toList.map {
case (htlcTx: HtlcTx, remoteSig) =>
if (!checkSig(htlcTx, remoteSig, remoteHtlcPubkey, TxOwner.Remote, params.commitmentFormat)) {
return Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid))
}
HtlcTxAndRemoteSig(htlcTx, remoteSig)
LocalCommit.fromCommitSig(keyManager, params, fundingTxId, fundingTxIndex, remoteFundingPubKey, commitInput, commit, localCommitIndex, spec, localPerCommitmentPoint).map { localCommit1 =>
log.info(s"built local commit number=$localCommitIndex toLocalMsat=${spec.toLocal.toLong} toRemoteMsat=${spec.toRemote.toLong} htlc_in={} htlc_out={} feeratePerKw=${spec.commitTxFeerate} txid=${localCommit1.commitTxAndRemoteSig.commitTx.tx.txid} fundingTxId=$fundingTxId", spec.htlcs.collect(DirectedHtlc.incoming).map(_.id).mkString(","), spec.htlcs.collect(DirectedHtlc.outgoing).map(_.id).mkString(","))
copy(localCommit = localCommit1)
}
// update our commitment data
val localCommit1 = LocalCommit(localCommit.index + 1, spec, CommitTxAndRemoteSig(localCommitTx, commit.signature), htlcTxsAndRemoteSigs)
Right(copy(localCommit = localCommit1))
}

/** Return a fully signed commit tx, that can be published as-is. */
Expand Down
51 changes: 29 additions & 22 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ object Helpers {
/**
* Creates both sides' first commitment transaction.
*
* @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput)
* @return (localSpec, localTx, remoteSpec, remoteTx)
*/
def makeFirstCommitTxs(keyManager: ChannelKeyManager,
params: ChannelParams,
Expand All @@ -366,36 +366,43 @@ object Helpers {
commitTxFeerate: FeeratePerKw,
fundingTxHash: ByteVector32, fundingTxOutputIndex: Int,
remoteFundingPubKey: PublicKey,
remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] =
makeCommitTxsWithoutHtlcs(keyManager, params,
remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
makeCommitTxs(keyManager, params,
fundingAmount = localFundingAmount + remoteFundingAmount,
toLocal = localFundingAmount.toMilliSatoshi - localPushAmount + remotePushAmount,
toRemote = remoteFundingAmount.toMilliSatoshi + localPushAmount - remotePushAmount,
localHtlcs = Set.empty,
commitTxFeerate,
fundingTxIndex = 0,
fundingTxHash, fundingTxOutputIndex, remoteFundingPubKey = remoteFundingPubKey, remotePerCommitmentPoint = remoteFirstPerCommitmentPoint, commitmentIndex = 0)
fundingTxHash, fundingTxOutputIndex,
remoteFundingPubKey = remoteFundingPubKey, remotePerCommitmentPoint = remoteFirstPerCommitmentPoint,
commitmentIndex = 0).map {
case (localSpec, localCommit, remoteSpec, remoteCommit, _) => (localSpec, localCommit, remoteSpec, remoteCommit)
}
}

/**
* 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. This function should only be used when commitments are synchronized (local and remote htlcs match).
*/
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,
localHtlcs: Set[DirectedHtlc],
commitTxFeerate: FeeratePerKw,
fundingTxIndex: Long,
fundingTxHash: ByteVector32, fundingTxOutputIndex: Int,
remoteFundingPubKey: PublicKey,
remotePerCommitmentPoint: PublicKey,
commitmentIndex: Long): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx, Seq[HtlcTx])] = {
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 All @@ -410,9 +417,9 @@ object Helpers {
val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteFundingPubKey)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, commitmentIndex)
val (localCommitTx, _) = Commitment.makeLocalTxs(keyManager, channelConfig, channelFeatures, commitmentIndex, localParams, remoteParams, fundingTxIndex, remoteFundingPubKey, commitmentInput, localPerCommitmentPoint, localSpec)
val (remoteCommitTx, _) = Commitment.makeRemoteTxs(keyManager, channelConfig, channelFeatures, commitmentIndex, localParams, remoteParams, fundingTxIndex, remoteFundingPubKey, commitmentInput, remotePerCommitmentPoint, remoteSpec)

Right(localSpec, localCommitTx, remoteSpec, remoteCommitTx)
val (remoteCommitTx, htlcTxs) = Commitment.makeRemoteTxs(keyManager, channelConfig, channelFeatures, commitmentIndex, localParams, remoteParams, fundingTxIndex, remoteFundingPubKey, commitmentInput, remotePerCommitmentPoint, remoteSpec)
val sortedHtlcTxs = htlcTxs.sortBy(_.input.outPoint.index)
Right(localSpec, localCommitTx, remoteSpec, remoteCommitTx, sortedHtlcTxs)
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2650,8 +2650,9 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
spliceInAmount = cmd.additionalLocalFunding,
spliceOut = cmd.spliceOutputs,
targetFeerate = targetFeerate)
if (parentCommitment.localCommit.spec.toLocal + fundingContribution < parentCommitment.localChannelReserve(d.commitments.params)) {
log.warning("cannot do splice: insufficient funds")
val commitTxFees = Transactions.commitTxTotalCost(d.commitments.params.remoteParams.dustLimit, parentCommitment.remoteCommit.spec, d.commitments.params.commitmentFormat)
if (parentCommitment.localCommit.spec.toLocal + fundingContribution < parentCommitment.localChannelReserve(d.commitments.params).max(commitTxFees)) {
log.warning(s"cannot do splice: insufficient funds (commitTxFees=$commitTxFees reserve=${parentCommitment.localChannelReserve(d.commitments.params)})")
Left(InvalidSpliceRequest(d.channelId))
} else if (cmd.spliceOut_opt.map(_.scriptPubKey).exists(!MutualClose.isValidFinalScriptPubkey(_, allowAnySegwit = true))) {
log.warning("cannot do splice: invalid splice-out script")
Expand Down
Loading