Skip to content

Commit

Permalink
Improve swap-in protocol with taproot and musig2 (#563)
Browse files Browse the repository at this point in the history
* Move swap-in related methods into their own class

* Add musig2-based swap-in protocol

* Use different user keys for the common and refund paths

This allows us to easily rotate swap-in addresses and generate a single generic taproot descriptor (for bitcoin core 26 and newer) that can be used to recover
swap-in funds once the refund delay has passed, assuming that:
- user and server keys are static
- user refund keys follow BIP derivation

* Add a musig2 secret nonce field to local/remote musing2 swap-in classes

It makes the code cleaner and we get rid of the secret nonces map.
These nonces are replaced with dummy values whenever this classes are serialized, which is safe since they're never reused for signing txs.

* Rework TxComplete to use implicit ordering for musig2 nonces

Instead of sending an explicit serialId -> nonce map, we send a list of public nonces ordered by serial id.
This matches how signatures are sent in TxSignatures.

* Address review comments
- add a pubkey script to the SharedInput() class (we don't need the full TxOut which we can recreate)
- remove aggregate nonce check ins FullySignedTx: code already handles transactions that are not properly signed
- generate musig2 nonces when we send TxAddInput

* Use musig2 helpers to simplify swap-in protocol (#592)

We use the musig2 helpers exposed by ACINQ/bitcoin-kmp#114 to simplify the swap-in protocol and hide all of the musig2 internal details (key aggregation cache, control block, internal taproot key, opaque session object, nonce aggregation).

The code is simpler to reason about and signing is more similar to signing normal single-sig inputs.

* Rework recovery procedure

The current recovery process needed to be updated to derive the correct master priv key from the seed by specifying our custom BIP32 path (m/52h/0h/2h/0) when we create the wallet.

We also export 2 descriptor methods: one to get the private swap-in wallet descriptor, which can be used as-is, and the other to get the public swap-in wallet descriptor, which can be used to create a watch-only wallet to monitor swap-in funds and to recovery funds using our recovery procedure.

Both descriptor use the refund master key, and not the master key itself because we use hardened paths to derive the refund key, which means that it is not possible to compute the refund master public key from the master public: importing the descriptor would fail.

---------

Co-authored-by: Bastien Teinturier <31281497+t-bast@users.noreply.github.com>
  • Loading branch information
sstone and t-bast authored Feb 15, 2024
1 parent 34e6834 commit 9785182
Show file tree
Hide file tree
Showing 31 changed files with 1,229 additions and 461 deletions.
163 changes: 89 additions & 74 deletions RECOVERY.md

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ val currentOs = org.gradle.internal.os.OperatingSystem.current()

kotlin {

val bitcoinKmpVersion = "0.16.0" // when upgrading bitcoin-kmp, keep secpJniJvmVersion in sync!
val secpJniJvmVersion = "0.13.0"
val bitcoinKmpVersion = "0.17.0" // when upgrading bitcoin-kmp, keep secpJniJvmVersion in sync!
val secpJniJvmVersion = "0.14.0"

val serializationVersion = "1.6.2"
val coroutineVersion = "1.7.3"
Expand Down Expand Up @@ -138,6 +138,11 @@ kotlin {
languageSettings.optIn("kotlin.ExperimentalStdlibApi")
}
}

configurations.all {
// do not cache changing (i.e. SNAPSHOT) dependencies
resolutionStrategy.cacheChangingModulesFor(0, TimeUnit.SECONDS)
}

targets.all {
compilations.all {
Expand Down
4 changes: 2 additions & 2 deletions publishing/lightning-kmp-snapshot-deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ mvn deploy:deploy-file -DrepositoryId=ossrh -Durl=https://oss.sonatype.org/conte
-Djavadoc=$ARTIFACT_ID_BASE-$VERSION-javadoc.jar
popd
pushd .
for i in iosarm64 iosx64 jvm linuxx64; do
for i in iosarm64 iossimulatorarm64 iosx64 jvm linuxx64; do
cd fr/acinq/lightning/lightning-kmp-$i/$VERSION
if [ $i == iosarm64 ] || [ $i == iosx64 ]; then
if [ $i == iosarm64 ] || [ $i == iossimulatorarm64 ] || [ $i == iosx64 ]; then
mvn deploy:deploy-file -DrepositoryId=ossrh -Durl=https://oss.sonatype.org/content/repositories/snapshots/ \
-DpomFile=$ARTIFACT_ID_BASE-$i-$VERSION.pom \
-Dfile=$ARTIFACT_ID_BASE-$i-$VERSION.klib \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,17 +167,9 @@ class ElectrumMiniWallet(
}

fun computeScriptHash(bitcoinAddress: String): ByteVector32? {
return when (val result = Bitcoin.addressToPublicKeyScript(chainHash, bitcoinAddress)) {
is AddressToPublicKeyScriptResult.Failure -> {
logger.error { "cannot subscribe to $bitcoinAddress ($result)" }
null
}

is AddressToPublicKeyScriptResult.Success -> {
val pubkeyScript = ByteVector(Script.write(result.script))
return ElectrumClient.computeScriptHash(pubkeyScript)
}
}
return Bitcoin.addressToPublicKeyScript(chainHash, bitcoinAddress)
.map { ElectrumClient.computeScriptHash(Script.write(it).byteVector()) }
.right
}

job = launch {
Expand Down
340 changes: 279 additions & 61 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ sealed class ChannelState {
// this code is only executed for the first transition to Closing, so there can only be one transaction here
val closingTx = newState.mutualClosePublished.first()
val finalAmount = closingTx.toLocalOutput?.amount ?: 0.sat
val address = closingTx.toLocalOutput?.publicKeyScript?.let { Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, it.toByteArray()).result } ?: "unknown"
val address = closingTx.toLocalOutput?.publicKeyScript?.let { Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, it.toByteArray()).right } ?: "unknown"
listOf(
ChannelAction.Storage.StoreOutgoingPayment.ViaClose(
amount = finalAmount,
Expand Down Expand Up @@ -142,7 +142,7 @@ sealed class ChannelState {
val address = Bitcoin.addressFromPublicKeyScript(
chainHash = staticParams.nodeParams.chainHash,
pubkeyScript = oldState.commitments.params.localParams.defaultFinalScriptPubKey.toByteArray() // force close always send to the default script
).result ?: "unknown"
).right ?: "unknown"
listOf(
ChannelAction.Storage.StoreOutgoingPayment.ViaClose(
amount = channelBalance.truncateToSatoshi(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,7 @@ data class Normal(
targetFeerate = cmd.message.feerate
)
val session = InteractiveTxSession(
staticParams.remoteNodeId,
channelKeys(),
keyManager.swapInOnChainWallet,
fundingParams,
Expand Down Expand Up @@ -557,6 +558,7 @@ data class Normal(
is Either.Right -> {
// The splice initiator always sends the first interactive-tx message.
val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(
staticParams.remoteNodeId,
channelKeys(),
keyManager.swapInOnChainWallet,
fundingParams,
Expand Down Expand Up @@ -603,6 +605,7 @@ data class Normal(
is InteractiveTxSessionAction.SignSharedTx -> {
val parentCommitment = commitments.active.first()
val signingSession = InteractiveTxSigningSession.create(
interactiveTxSession,
keyManager,
commitments.params,
spliceStatus.spliceSession.fundingParams,
Expand Down Expand Up @@ -880,7 +883,7 @@ data class Normal(
ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceOut(
amount = txOut.amount,
miningFees = action.fundingTx.sharedTx.tx.localFees.truncateToSatoshi(),
address = Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, txOut.publicKeyScript.toByteArray()).result ?: "unknown",
address = Bitcoin.addressFromPublicKeyScript(staticParams.nodeParams.chainHash, txOut.publicKeyScript.toByteArray()).right ?: "unknown",
txId = action.fundingTx.txId
)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ data class WaitForAcceptChannel(
}
is Either.Right -> {
// The channel initiator always sends the first interactive-tx message.
val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value).send()
val (interactiveTxSession, interactiveTxAction) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value).send()
when (interactiveTxAction) {
is InteractiveTxSessionAction.SendMessage -> {
val nextState = WaitForFundingCreated(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ data class WaitForFundingConfirmed(
addAll(latestFundingTx.sharedTx.tx.localInputs.map { Either.Left(it) })
addAll(latestFundingTx.sharedTx.tx.localOutputs.map { Either.Right(it) })
}
val session = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }, commitments.latest.localCommit.spec.htlcs)
val session = InteractiveTxSession(staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, SharedFundingInputBalances(0.msat, 0.msat, 0.msat), toSend, previousFundingTxs.map { it.sharedTx }, commitments.latest.localCommit.spec.htlcs)
val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session))
Pair(nextState, listOf(ChannelAction.Message.Send(TxAckRbf(channelId, fundingParams.localContribution))))
}
Expand Down Expand Up @@ -142,7 +142,7 @@ data class WaitForFundingConfirmed(
Pair(this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.RbfAborted), listOf(ChannelAction.Message.Send(TxAbort(channelId, ChannelFundingError(channelId).message))))
}
is Either.Right -> {
val (session, action) = InteractiveTxSession(channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), contributions.value, previousFundingTxs.map { it.sharedTx }).send()
val (session, action) = InteractiveTxSession(staticParams.remoteNodeId, channelKeys(), keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), contributions.value, previousFundingTxs.map { it.sharedTx }).send()
when (action) {
is InteractiveTxSessionAction.SendMessage -> {
val nextState = this@WaitForFundingConfirmed.copy(rbfStatus = RbfStatus.InProgress(session))
Expand All @@ -169,6 +169,7 @@ data class WaitForFundingConfirmed(
is InteractiveTxSessionAction.SignSharedTx -> {
val replacedCommitment = commitments.latest
val signingSession = InteractiveTxSigningSession.create(
rbfSession1,
keyManager,
commitments.params,
rbfSession1.fundingParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ data class WaitForFundingCreated(
is InteractiveTxSessionAction.SignSharedTx -> {
val channelParams = ChannelParams(channelId, channelConfig, channelFeatures, localParams, remoteParams, channelFlags)
val signingSession = InteractiveTxSigningSession.create(
interactiveTxSession1,
keyManager,
channelParams,
interactiveTxSession.fundingParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ data class WaitForOpenChannel(
Pair(Aborted, listOf(ChannelAction.Message.Send(Error(temporaryChannelId, ChannelFundingError(temporaryChannelId).message))))
}
is Either.Right -> {
val interactiveTxSession = InteractiveTxSession(channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value)
val interactiveTxSession = InteractiveTxSession(staticParams.remoteNodeId, channelKeys, keyManager.swapInOnChainWallet, fundingParams, 0.msat, 0.msat, emptySet(), fundingContributions.value)
val nextState = WaitForFundingCreated(
localParams,
remoteParams,
Expand Down
Loading

0 comments on commit 9785182

Please sign in to comment.