Skip to content

Commit

Permalink
Allow splicing on non dual-funded channels
Browse files Browse the repository at this point in the history
We currently allow splicing on top of a channel that was created without
using dual funding, but it results in an incorrect channel reserve being
used post-splice.
  • Loading branch information
t-bast committed Aug 22, 2023
1 parent 3547f87 commit 177c801
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ case class ChannelParams(channelId: ByteVector32,
def minDepthDualFunding(defaultMinDepth: Int, sharedTx: SharedTransaction): Option[Long] = minDepthDualFunding(defaultMinDepth, sharedTx.sharedOutput.amount, Some(sharedTx.remoteInputs.nonEmpty))

/** Channel reserve that applies to our funds. */
def localChannelReserveForCapacity(capacity: Satoshi): Satoshi = if (channelFeatures.hasFeature(Features.DualFunding)) {
def localChannelReserveForCapacity(capacity: Satoshi, isSplice: Boolean): Satoshi = if (channelFeatures.hasFeature(Features.DualFunding) || isSplice) {
(capacity / 100).max(remoteParams.dustLimit)
} else {
remoteParams.requestedChannelReserve_opt.get // this is guarded by a require() in Params
}

/** Channel reserve that applies to our peer's funds. */
def remoteChannelReserveForCapacity(capacity: Satoshi): Satoshi = if (channelFeatures.hasFeature(Features.DualFunding)) {
def remoteChannelReserveForCapacity(capacity: Satoshi, isSplice: Boolean): Satoshi = if (channelFeatures.hasFeature(Features.DualFunding) || isSplice) {
(capacity / 100).max(localParams.dustLimit)
} else {
localParams.requestedChannelReserve_opt.get // this is guarded by a require() in Params
Expand Down Expand Up @@ -264,10 +264,10 @@ case class Commitment(fundingTxIndex: Long,
val capacity: Satoshi = commitInput.txOut.amount

/** Channel reserve that applies to our funds. */
def localChannelReserve(params: ChannelParams): Satoshi = params.localChannelReserveForCapacity(capacity)
def localChannelReserve(params: ChannelParams): Satoshi = params.localChannelReserveForCapacity(capacity, fundingTxIndex > 0)

/** Channel reserve that applies to our peer's funds. */
def remoteChannelReserve(params: ChannelParams): Satoshi = params.remoteChannelReserveForCapacity(capacity)
def remoteChannelReserve(params: ChannelParams): Satoshi = params.remoteChannelReserveForCapacity(capacity, fundingTxIndex > 0)

// NB: when computing availableBalanceForSend and availableBalanceForReceive, the initiator keeps an extra buffer on
// top of its usual channel reserve to avoid getting channels stuck in case the on-chain feerate increases (see
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
stay() using d1 storing() sending signingSession1.localSigs calling endQuiescence(d1)
}
}
case _ if d.commitments.params.channelFeatures.hasFeature(Features.DualFunding) && d.commitments.latest.localFundingStatus.signedTx_opt.isEmpty && commit.batchSize == 1 =>
case _ if d.commitments.latest.localFundingStatus.isInstanceOf[LocalFundingStatus.DualFundedUnconfirmedFundingTx] && d.commitments.latest.localFundingStatus.signedTx_opt.isEmpty && commit.batchSize == 1 =>
// The latest funding transaction is unconfirmed and we're missing our peer's tx_signatures: any commit_sig
// that we receive before that should be ignored, it's either a retransmission of a commit_sig we've already
// received or a bug that will eventually lead to a force-close anyway.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -649,7 +649,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
}

val sharedInput_opt = fundingParams.sharedInput_opt.map(_ => {
val remoteReserve = channelParams.remoteChannelReserveForCapacity(fundingParams.fundingAmount)
val remoteReserve = channelParams.remoteChannelReserveForCapacity(fundingParams.fundingAmount, isSplice = true)
// We ignore the reserve requirement if we are splicing funds into the channel, which increases the size of the reserve.
if (sharedOutput.remoteAmount < remoteReserve && remoteOutputs.nonEmpty && localInputs.isEmpty) {
log.warn("invalid interactive tx: peer takes too much funds out and falls below the channel reserve ({} < {})", sharedOutput.remoteAmount, remoteReserve)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ trait ChannelStateTestsBase extends Assertions with Eventually {
.modify(_.onChainFeeConf.spendAnchorWithoutHtlcs).setToIf(tags.contains(ChannelStateTestsTags.DontSpendAnchorWithoutHtlcs))(false)
val wallet = wallet_opt match {
case Some(wallet) => wallet
case None => if (tags.contains(ChannelStateTestsTags.DualFunding)) new SingleKeyOnChainWallet() else new DummyOnChainWallet()
case None => if (tags.contains(ChannelStateTestsTags.DualFunding) || tags.contains(ChannelStateTestsTags.Splicing)) new SingleKeyOnChainWallet() else new DummyOnChainWallet()
}
val alice: TestFSMRef[ChannelState, ChannelData, Channel] = {
implicit val system: ActorSystem = systemA
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,35 @@ class NormalSplicesStateSpec extends TestKitBaseClass with FixtureAnyFunSuiteLik
assert(alice.stateData.asInstanceOf[DATA_NORMAL].commitments.latest.localCommit.spec.toRemote == 700_000_000.msat)
}

test("recv CMD_SPLICE (splice-in, non dual-funded channel)") { () =>
val f = init(tags = Set(ChannelStateTestsTags.DualFunding, ChannelStateTestsTags.Splicing))
import f._

reachNormal(f, tags = Set(ChannelStateTestsTags.Splicing)) // we open a non dual-funded channel
alice2bob.ignoreMsg { case _: ChannelUpdate => true }
bob2alice.ignoreMsg { case _: ChannelUpdate => true }
awaitCond(alice.stateName == NORMAL && bob.stateName == NORMAL)
val initialState = alice.stateData.asInstanceOf[DATA_NORMAL]
assert(!initialState.commitments.params.channelFeatures.hasFeature(Features.DualFunding))
assert(initialState.commitments.latest.capacity == 1_000_000.sat)
assert(initialState.commitments.latest.localCommit.spec.toLocal == 800_000_000.msat)
assert(initialState.commitments.latest.localCommit.spec.toRemote == 200_000_000.msat)
// The channel reserve is set by each participant when not using dual-funding.
assert(initialState.commitments.latest.localChannelReserve == 20_000.sat)
assert(initialState.commitments.latest.remoteChannelReserve == 10_000.sat)

// We can splice on top of a non dual-funded channel.
initiateSplice(f, spliceIn_opt = Some(SpliceIn(500_000 sat)))
val postSpliceState = alice.stateData.asInstanceOf[DATA_NORMAL]
assert(!postSpliceState.commitments.params.channelFeatures.hasFeature(Features.DualFunding))
assert(postSpliceState.commitments.latest.capacity == 1_500_000.sat)
assert(postSpliceState.commitments.latest.localCommit.spec.toLocal == 1_300_000_000.msat)
assert(postSpliceState.commitments.latest.localCommit.spec.toRemote == 200_000_000.msat)
// The channel reserve is now implicitly set to 1% of the channel capacity on both sides.
assert(postSpliceState.commitments.latest.localChannelReserve == 15_000.sat)
assert(postSpliceState.commitments.latest.remoteChannelReserve == 15_000.sat)
}

test("recv CMD_SPLICE (splice-out)") { f =>
import f._

Expand Down

0 comments on commit 177c801

Please sign in to comment.