Skip to content
Open
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 @@ -161,6 +161,13 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

@Serializable
object Splicing : Feature() {
override val rfcName get() = "option_splice"
override val mandatory get() = 62
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node)
}

// The following features have not been standardised, hence the high feature bits to avoid conflicts.

/** This feature bit should be activated when a node accepts having their channel reserve set to 0. */
Expand Down Expand Up @@ -196,13 +203,6 @@ sealed class Feature {
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init, FeatureScope.Node, FeatureScope.Invoice)
}

@Serializable
object ExperimentalSplice : Feature() {
override val rfcName get() = "splice_experimental"
override val mandatory get() = 154
override val scopes: Set<FeatureScope> get() = setOf(FeatureScope.Init)
}

@Serializable
object OnTheFlyFunding : Feature() {
override val rfcName get() = "on_the_fly_funding"
Expand Down Expand Up @@ -295,11 +295,11 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.ChannelType,
Feature.PaymentMetadata,
Feature.SimpleClose,
Feature.Splicing,
Feature.ExperimentalTrampolinePayment,
Feature.ZeroReserveChannels,
Feature.WakeUpNotificationClient,
Feature.WakeUpNotificationProvider,
Feature.ExperimentalSplice,
Feature.OnTheFlyFunding,
Feature.FundingFeeCredit,
Feature.SimpleTaprootChannels
Expand Down Expand Up @@ -335,7 +335,6 @@ data class Features(val activated: Map<Feature, FeatureSupport>, val unknown: Se
Feature.AnchorOutputs to listOf(Feature.StaticRemoteKey),
Feature.SimpleClose to listOf(Feature.ShutdownAnySegwit),
Feature.ExperimentalTrampolinePayment to listOf(Feature.PaymentSecret),
Feature.OnTheFlyFunding to listOf(Feature.ExperimentalSplice),
Feature.FundingFeeCredit to listOf(Feature.OnTheFlyFunding)
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ data class NodeParams(
require(features.hasFeature(Feature.PaymentSecret, FeatureSupport.Mandatory)) { "${Feature.PaymentSecret.rfcName} should be mandatory" }
require(features.hasFeature(Feature.ChannelType, FeatureSupport.Mandatory)) { "${Feature.ChannelType.rfcName} should be mandatory" }
require(features.hasFeature(Feature.DualFunding, FeatureSupport.Mandatory)) { "${Feature.DualFunding.rfcName} should be mandatory" }
require(features.hasFeature(Feature.Splicing, FeatureSupport.Mandatory)) { "${Feature.Splicing.rfcName} should be mandatory" }
require(features.hasFeature(Feature.RouteBlinding)) { "${Feature.RouteBlinding.rfcName} should be supported" }
require(features.hasFeature(Feature.ShutdownAnySegwit, FeatureSupport.Mandatory)) { "${Feature.ShutdownAnySegwit.rfcName} should be mandatory" }
require(features.hasFeature(Feature.SimpleClose, FeatureSupport.Mandatory)) { "${Feature.SimpleClose.rfcName} should be mandatory" }
Expand Down Expand Up @@ -203,10 +204,10 @@ data class NodeParams(
Feature.ChannelType to FeatureSupport.Mandatory,
Feature.PaymentMetadata to FeatureSupport.Optional,
Feature.SimpleClose to FeatureSupport.Mandatory,
Feature.Splicing to FeatureSupport.Mandatory,
Feature.ExperimentalTrampolinePayment to FeatureSupport.Optional,
Feature.ZeroReserveChannels to FeatureSupport.Optional,
Feature.WakeUpNotificationClient to FeatureSupport.Optional,
Feature.ExperimentalSplice to FeatureSupport.Optional,
Feature.OnTheFlyFunding to FeatureSupport.Optional,
Feature.FundingFeeCredit to FeatureSupport.Optional,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI
remoteFundingPubKey: PublicKey,
commitInput: Transactions.InputInfo,
commitmentFormat: Transactions.CommitmentFormat,
batchSize: Int,
remoteNonce: IndividualNonce?,
logger: MDCLogger
): Either<ChannelException, CommitSig> {
Expand All @@ -194,15 +193,15 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI
return when (commitmentFormat) {
Transactions.CommitmentFormat.AnchorOutputs -> {
val sig = remoteCommitTx.sign(fundingKey, remoteFundingPubKey)
Either.Right(CommitSig(channelParams.channelId, sig, htlcSigs, batchSize))
Either.Right(CommitSig(channelParams.channelId, commitInput.outPoint.txid, sig, htlcSigs))
}
Transactions.CommitmentFormat.SimpleTaprootChannels -> when (remoteNonce) {
null -> Either.Left(MissingCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index))
else -> {
val localNonce = NonceGenerator.signingNonce(fundingKey.publicKey(), remoteFundingPubKey, commitInput.outPoint.txid)
when (val psig = remoteCommitTx.partialSign(fundingKey, remoteFundingPubKey, mapOf(), localNonce, listOf(localNonce.publicNonce, remoteNonce))) {
is Either.Left -> Either.Left(InvalidCommitNonce(channelParams.channelId, commitInput.outPoint.txid, index))
is Either.Right -> Either.Right(CommitSig(channelParams.channelId, psig.value, htlcSigs, batchSize))
is Either.Right -> Either.Right(CommitSig(channelParams.channelId, commitInput.outPoint.txid, psig.value, htlcSigs))
}
}
}
Expand All @@ -218,7 +217,6 @@ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxI
signingSession.fundingParams.remoteFundingPubkey,
signingSession.commitInput(channelKeys),
signingSession.fundingParams.commitmentFormat,
batchSize = 1,
remoteNonce,
logger
)
Expand Down Expand Up @@ -563,7 +561,6 @@ data class Commitment(
commitKeys: RemoteCommitmentKeys,
changes: CommitmentChanges,
remoteNextPerCommitmentPoint: PublicKey,
batchSize: Int,
nextRemoteNonce: IndividualNonce?,
logger: MDCLogger
): Either<ChannelException, Pair<Commitment, CommitSig>> {
Expand Down Expand Up @@ -601,7 +598,7 @@ data class Commitment(
val htlcsOut = spec.htlcs.incomings().map { it.id }.joinToString(",")
"built remote commit number=${remoteCommit.index + 1} toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txId=${remoteCommitTx.tx.txid} fundingTxId=$fundingTxId"
}
val commitSig = CommitSig(params.channelId, sig, htlcSigs.toList(), batchSize)
val commitSig = CommitSig(params.channelId, fundingTxId, sig, htlcSigs.toList())
val commitment1 = copy(nextRemoteCommit = RemoteCommit(remoteCommit.index + 1, spec, remoteCommitTx.tx.txid, remoteNextPerCommitmentPoint))
return Either.Right(Pair(commitment1, commitSig))
}
Expand Down Expand Up @@ -693,6 +690,8 @@ data class Commitments(
// We always use the last commitment that was created, to make sure we never go back in time.
val latest = FullCommitment(channelParams, changes, active.first())

fun lastLocalLocked(zeroConf: Boolean): Commitment? = active.find { zeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }

val all = buildList {
addAll(active)
addAll(inactive)
Expand Down Expand Up @@ -868,7 +867,7 @@ data class Commitments(
val (active1, sigs) = active.map { c ->
val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remoteNextPerCommitmentPoint)
val remoteNonce = remoteCommitNonces[c.fundingTxId]
when (val res = c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, remoteNonce, logger)) {
when (val res = c.sendCommit(channelParams, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, remoteNonce, logger)) {
is Either.Left -> return Either.Left(res.left)
is Either.Right -> res.value
}
Expand Down Expand Up @@ -896,9 +895,12 @@ data class Commitments(
return Either.Left(CommitSigCountMismatch(channelId, active.size, sigs.size))
}
val commitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitIndex + 1)
// Signatures are sent in order (most recent first), calling `zip` will drop trailing sigs that are for deactivated/pruned commitments.
val active1 = active.zip(sigs).map {
when (val commitment1 = it.first.receiveCommit(channelParams, channelKeys, commitKeys, changes, it.second, logger)) {
val active1 = active.withIndex().map { (i, c) ->
// If the funding_txid isn't provided, we assume that signatures are sent in order (most recent first).
// This ensures that the case where we have a batch of a single element (no pending splice) works correctly.
// This also ensures that we get a signature failure when our set of funding txs doesn't match with our peer.
val commit = sigs.find { it.fundingTxId == c.fundingTxId } ?: sigs[i]
when (val commitment1 = c.receiveCommit(channelParams, channelKeys, commitKeys, changes, commit, logger)) {
is Either.Left -> return Either.Left(commitment1.value)
is Either.Right -> commitment1.value
}
Expand Down Expand Up @@ -1067,7 +1069,7 @@ data class Commitments(
// This ensures that we only have to send splice_locked for the latest commitment instead of sending it for every commitment.
// A side-effect is that previous commitments that are implicitly locked don't necessarily have their status correctly set.
// That's why we look at locked commitments separately and then select the one with the oldest fundingTxIndex.
val lastLocalLocked = active.find { staticParams.useZeroConf || it.localFundingStatus is LocalFundingStatus.ConfirmedFundingTx }
val lastLocal = lastLocalLocked(staticParams.useZeroConf)
val lastRemoteLocked = active.find { it.remoteFundingStatus == RemoteFundingStatus.Locked }
return when {
// We select the locked commitment with the smaller value for fundingTxIndex, but both have to be defined.
Expand All @@ -1076,9 +1078,9 @@ data class Commitments(
// - transactions with the same fundingTxIndex double-spend each other, so only one of them can confirm
// - we don't allow creating a splice on top of an unconfirmed transaction that has RBF attempts (because it
// would become invalid if another of the RBF attempts end up being confirmed)
lastLocalLocked != null && lastRemoteLocked != null -> listOf(lastLocalLocked, lastRemoteLocked).minByOrNull { it.fundingTxIndex }
lastLocal != null && lastRemoteLocked != null -> listOf(lastLocal, lastRemoteLocked).minByOrNull { it.fundingTxIndex }
// Special case for the initial funding tx, we only require a local lock because channel_ready doesn't explicitly reference a funding tx.
lastLocalLocked != null && lastLocalLocked.fundingTxIndex == 0L -> lastLocalLocked
lastLocal != null && lastLocal.fundingTxIndex == 0L -> lastLocal
else -> null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -731,7 +731,7 @@ data class InteractiveTxSession(
fundingParams,
localCommitIndex,
SharedFundingInputBalances(previousLocalBalance, previousRemoteBalance, localHtlcs.map { it.add.amountMsat }.sum()),
fundingContributions.inputs.map { i -> Either.Left<InteractiveTxInput.Outgoing>(i) } + fundingContributions.outputs.map { o -> Either.Right<InteractiveTxOutput.Outgoing>(o) },
fundingContributions.inputs.map { i -> Either.Left(i) } + fundingContributions.outputs.map { o -> Either.Right(o) },
previousTxs,
localHtlcs,
localFundingNonce = fundingParams.sharedInput?.let {
Expand Down Expand Up @@ -1106,11 +1106,8 @@ data class InteractiveTxSigningSession(
// +-------+ +-------+
val fundingTxId: TxId = fundingTx.txId
val localCommitIndex = localCommit.fold({ it.index }, { it.index })
// This value tells our peer whether we need them to retransmit their commit_sig on reconnection or not.
val nextLocalCommitmentNumber = when (localCommit) {
is Either.Left -> localCommit.value.index
is Either.Right -> localCommit.value.index + 1
}
// If we haven't received the remote commit_sig, we will request a retransmission on reconnection.
val retransmitRemoteCommitSig: Boolean = localCommit.isLeft

fun localFundingKey(channelKeys: ChannelKeys): PrivateKey = fundingParams.fundingKey(channelKeys)

Expand Down Expand Up @@ -1273,7 +1270,7 @@ data class InteractiveTxSigningSession(
val htlcsOut = spec.htlcs.incomings().map { it.id }.joinToString(",")
"built remote commit number=$remoteCommitmentIndex toLocalMsat=${spec.toLocal.toLong()} toRemoteMsat=${spec.toRemote.toLong()} htlc_in=$htlcsIn htlc_out=$htlcsOut feeratePerKw=${spec.feerate} txId=${firstCommitTx.remoteCommitTx.tx.txid} fundingTxId=${unsignedTx.txid}"
}
val commitSig = CommitSig(channelParams.channelId, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs, batchSize = 1)
val commitSig = CommitSig(channelParams.channelId, unsignedTx.txid, localSigOfRemoteCommitTx, localSigsOfRemoteHtlcTxs)
// We haven't received the remote commit_sig: we don't have local htlc txs yet.
val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx.tx.txid)
val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,37 +317,27 @@ sealed class PersistedChannelState : ChannelState() {
}
ChannelReestablish(
channelId = channelId,
nextLocalCommitmentNumber = state.signingSession.nextLocalCommitmentNumber,
nextLocalCommitmentNumber = 1,
nextRemoteRevocationNumber = 0,
yourLastCommitmentSecret = PrivateKey(ByteVector32.Zeroes),
myCurrentPerCommitmentPoint = myFirstPerCommitmentPoint,
nextCommitNonces = nextCommitNonce?.let { listOf(nextFundingTxId to it) } ?: listOf(),
nextFundingTxId = nextFundingTxId,
retransmitCommitSig = state.signingSession.retransmitRemoteCommitSig,
currentCommitNonce = currentCommitNonce
)
}
is ChannelStateWithCommitments -> {
val channelKeys = channelKeys()
val yourLastPerCommitmentSecret = state.commitments.remotePerCommitmentSecrets.lastIndex?.let { state.commitments.remotePerCommitmentSecrets.getHash(it) } ?: ByteVector32.Zeroes
val myCurrentPerCommitmentPoint = channelKeys.commitmentPoint(state.commitments.localCommitIndex)
// If we disconnected while signing a funding transaction, we may need our peer to retransmit their commit_sig.
val nextLocalCommitmentNumber = when (state) {
is WaitForFundingConfirmed -> when (state.rbfStatus) {
is RbfStatus.WaitingForSigs -> state.rbfStatus.session.nextLocalCommitmentNumber
else -> state.commitments.localCommitIndex + 1
}
is Normal -> when (state.spliceStatus) {
is SpliceStatus.WaitingForSigs -> state.spliceStatus.session.nextLocalCommitmentNumber
else -> state.commitments.localCommitIndex + 1
}
else -> state.commitments.localCommitIndex + 1
}
// If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures.
val unsignedFundingTxId = when (state) {
// If we disconnected while signing a funding transaction, we may need our peer to (re)transmit their tx_signatures and commit_sig.
val (unsignedFundingTxId, retransmitCommitSig) = when (state) {
is WaitForFundingConfirmed -> state.getUnsignedFundingTxId()
is Normal -> state.getUnsignedFundingTxId()
else -> null
else -> Pair(null, false)
}
val lastFundingLocked = state.commitments.lastLocalLocked(staticParams.useZeroConf)
// We send our verification nonces for all active commitments.
val nextCommitNonces = state.commitments.active.mapNotNull { c ->
when (c.commitmentFormat) {
Expand Down Expand Up @@ -378,12 +368,14 @@ sealed class PersistedChannelState : ChannelState() {
}
ChannelReestablish(
channelId = channelId,
nextLocalCommitmentNumber = nextLocalCommitmentNumber,
nextLocalCommitmentNumber = state.commitments.localCommitIndex + 1,
nextRemoteRevocationNumber = state.commitments.remoteCommitIndex,
yourLastCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret),
myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint,
nextCommitNonces = nextCommitNonces + listOfNotNull(interactiveTxNextCommitNonce),
nextFundingTxId = unsignedFundingTxId,
retransmitCommitSig = retransmitCommitSig,
currentFundingLocked = lastFundingLocked?.fundingTxId,
currentCommitNonce = interactiveTxCurrentCommitNonce,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -935,11 +935,14 @@ data class Normal(
return Pair(nextState, actions)
}

/** If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction. */
fun getUnsignedFundingTxId(): TxId? = when {
spliceStatus is SpliceStatus.WaitingForSigs -> spliceStatus.session.fundingTx.txId
commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx && commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction -> commitments.latest.localFundingStatus.txId
else -> null
/**
* If we haven't completed the signing steps of an interactive-tx session, we will ask our peer to retransmit signatures for the corresponding transaction.
* The second parameter should be set to true when commit_sig also needs to be retransmitted.
*/
fun getUnsignedFundingTxId(): Pair<TxId?, Boolean> = when {
spliceStatus is SpliceStatus.WaitingForSigs -> Pair(spliceStatus.session.fundingTx.txId, spliceStatus.session.retransmitRemoteCommitSig)
commitments.latest.localFundingStatus is LocalFundingStatus.UnconfirmedFundingTx && commitments.latest.localFundingStatus.sharedTx is PartiallySignedSharedTransaction -> Pair(commitments.latest.localFundingStatus.txId, false)
else -> Pair(null, false)
}

/** Returns true if the [shortChannelId] matches one of our commitments or our alias. */
Expand Down
Loading