Skip to content

Commit

Permalink
Use bitcoin-kmp 0.20.0 (#695)
Browse files Browse the repository at this point in the history
Includes support for testnet4.
  • Loading branch information
sstone authored Sep 16, 2024
1 parent f3ba141 commit 1493200
Show file tree
Hide file tree
Showing 18 changed files with 71 additions and 76 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ val currentOs = org.gradle.internal.os.OperatingSystem.current()

kotlin {

val bitcoinKmpVersion = "0.19.0" // when upgrading bitcoin-kmp, keep secpJniJvmVersion in sync!
val bitcoinKmpVersion = "0.20.0" // when upgrading bitcoin-kmp, keep secpJniJvmVersion in sync!
val secpJniJvmVersion = "0.15.0"

val serializationVersion = "1.6.2"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -779,7 +779,7 @@ object Helpers {
val sig = Transactions.sign(it, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret))
val signedTx = Transactions.addSigs(it, sig)
// we need to make sure that the tx is indeed valid
when (runTrying { Transaction.correctlySpends(signedTx.tx, listOf(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) {
when (runTrying { signedTx.tx.correctlySpends(listOf(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) {
is Try.Success -> signedTx
is Try.Failure -> null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction,
}
}
val fullySignedTx = FullySignedSharedTransaction(tx, localSigs, remoteSigs, sharedSigs)
return when (runTrying { Transaction.correctlySpends(fullySignedTx.signedTx, tx.spentOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) {
return when (runTrying { fullySignedTx.signedTx.correctlySpends(tx.spentOutputs, ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) {
is Try.Success -> fullySignedTx
is Try.Failure -> null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ data class LegacyWaitForFundingConfirmed(
when (cmd.watch) {
is WatchEventConfirmed -> {
val result = runTrying {
Transaction.correctlySpends(commitments.latest.localCommit.publishableTxs.commitTx.tx, listOf(cmd.watch.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
commitments.latest.localCommit.publishableTxs.commitTx.tx.correctlySpends(listOf(cmd.watch.tx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}
if (result is Try.Failure) {
logger.error { "funding tx verification failed: ${result.error}" }
Expand Down
29 changes: 14 additions & 15 deletions src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,17 @@ interface KeyManager {
private val master: DeterministicWallet.ExtendedPrivateKey,
val account: Long
) {
private val xpriv = DeterministicWallet.derivePrivateKey(master, bip84BasePath(chain) / hardened(account))
private val xpriv = master.derivePrivateKey(bip84BasePath(chain) / hardened(account))

val xpub: String = DeterministicWallet.encode(
input = DeterministicWallet.publicKey(xpriv),
val xpub: String = xpriv.extendedPublicKey.encode(
prefix = when (chain) {
Chain.Testnet, Chain.Regtest, Chain.Signet -> DeterministicWallet.vpub
Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> DeterministicWallet.vpub
Chain.Mainnet -> DeterministicWallet.zpub
}
)

fun privateKey(addressIndex: Long): PrivateKey {
return DeterministicWallet.derivePrivateKey(xpriv, KeyPath.empty / 0 / addressIndex).privateKey
return xpriv.derivePrivateKey(KeyPath.empty / 0 / addressIndex).privateKey
}

fun pubkeyScript(addressIndex: Long): ByteVector {
Expand All @@ -102,7 +101,7 @@ interface KeyManager {

companion object {
fun bip84BasePath(chain: Chain) = when (chain) {
Chain.Regtest, Chain.Testnet, Chain.Signet -> KeyPath.empty / hardened(84) / hardened(1)
Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(84) / hardened(1)
Chain.Mainnet -> KeyPath.empty / hardened(84) / hardened(0)
}
}
Expand All @@ -121,18 +120,18 @@ interface KeyManager {
val remoteServerPublicKey: PublicKey,
val refundDelay: Int = DefaultSwapInParams.RefundDelay
) {
private val userExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserKeyPath(chain))
private val userRefundExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInUserRefundKeyPath(chain))
private val userExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = master.derivePrivateKey(swapInUserKeyPath(chain))
private val userRefundExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = master.derivePrivateKey(swapInUserRefundKeyPath(chain))

val userPrivateKey: PrivateKey = userExtendedPrivateKey.privateKey
val userPublicKey: PublicKey = userPrivateKey.publicKey()

private val localServerExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = DeterministicWallet.derivePrivateKey(master, swapInLocalServerKeyPath(chain))
fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = DeterministicWallet.derivePrivateKey(localServerExtendedPrivateKey, perUserPath(remoteNodeId)).privateKey
private val localServerExtendedPrivateKey: DeterministicWallet.ExtendedPrivateKey = master.derivePrivateKey(swapInLocalServerKeyPath(chain))
fun localServerPrivateKey(remoteNodeId: PublicKey): PrivateKey = localServerExtendedPrivateKey.derivePrivateKey(perUserPath(remoteNodeId)).privateKey

// legacy p2wsh-based swap-in protocol, with a fixed on-chain address
val legacySwapInProtocol = SwapInProtocolLegacy(userPublicKey, remoteServerPublicKey, refundDelay)
val legacyDescriptor = SwapInProtocolLegacy.descriptor(chain, DeterministicWallet.publicKey(master), DeterministicWallet.publicKey(userExtendedPrivateKey), remoteServerPublicKey, refundDelay)
val legacyDescriptor = SwapInProtocolLegacy.descriptor(chain, master.extendedPublicKey, userExtendedPrivateKey.extendedPublicKey, remoteServerPublicKey, refundDelay)

fun signSwapInputUserLegacy(fundingTx: Transaction, index: Int, parentTxOuts: List<TxOut>): ByteVector64 {
return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()], userPrivateKey)
Expand All @@ -152,7 +151,7 @@ interface KeyManager {
* @return the swap-in protocol that matches the input public key script
*/
fun getSwapInProtocol(addressIndex: Int): SwapInProtocol {
val userRefundPrivateKey: PrivateKey = DeterministicWallet.derivePrivateKey(userRefundExtendedPrivateKey, addressIndex.toLong()).privateKey
val userRefundPrivateKey: PrivateKey = userRefundExtendedPrivateKey.derivePrivateKey(addressIndex.toLong()).privateKey
val userRefundPublicKey: PublicKey = userRefundPrivateKey.publicKey()
return SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay)
}
Expand Down Expand Up @@ -189,7 +188,7 @@ interface KeyManager {
tx.updateWitness(inputIndex, legacySwapInProtocol.witnessRefund(sig))
}
else -> {
val userRefundPrivateKey: PrivateKey = DeterministicWallet.derivePrivateKey(userRefundExtendedPrivateKey, addressIndex.toLong()).privateKey
val userRefundPrivateKey: PrivateKey = userRefundExtendedPrivateKey.derivePrivateKey(addressIndex.toLong()).privateKey
val swapInProtocol = getSwapInProtocol(addressIndex)
val sig = swapInProtocol.signSwapInputRefund(tx, inputIndex, utxos.map { it.txOut }, userRefundPrivateKey)
tx.updateWitness(inputIndex, swapInProtocol.witnessRefund(sig))
Expand Down Expand Up @@ -237,7 +236,7 @@ interface KeyManager {

companion object {
private fun swapInKeyBasePath(chain: Chain) = when (chain) {
Chain.Regtest, Chain.Testnet, Chain.Signet -> KeyPath.empty / hardened(51) / hardened(0)
Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(51) / hardened(0)
Chain.Mainnet -> KeyPath.empty / hardened(52) / hardened(0)
}

Expand All @@ -248,7 +247,7 @@ interface KeyManager {
fun swapInUserRefundKeyPath(chain: Chain) = swapInKeyBasePath(chain) / hardened(2) / 0L

fun encodedSwapInUserKeyPath(chain: Chain) = when (chain) {
Chain.Regtest, Chain.Testnet, Chain.Signet -> "51h/0h/0h"
Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> "51h/0h/0h"
Chain.Mainnet -> "52h/0h/0h"
}

Expand Down
19 changes: 9 additions & 10 deletions src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package fr.acinq.lightning.crypto

import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey
import fr.acinq.bitcoin.DeterministicWallet.hardened
import fr.acinq.bitcoin.crypto.Pack
import fr.acinq.lightning.Lightning.secureRandom
Expand Down Expand Up @@ -38,8 +37,8 @@ data class LocalKeyManager(val seed: ByteVector, val chain: Chain, val remoteSwa
private val master = DeterministicWallet.generate(seed)

override val nodeKeys: KeyManager.NodeKeys = KeyManager.NodeKeys(
legacyNodeKey = @Suppress("DEPRECATION") derivePrivateKey(master, eclairNodeKeyBasePath(chain)),
nodeKey = derivePrivateKey(master, nodeKeyBasePath(chain)),
legacyNodeKey = @Suppress("DEPRECATION") master.derivePrivateKey(eclairNodeKeyBasePath(chain)),
nodeKey = master.derivePrivateKey(nodeKeyBasePath(chain)),
)

override val finalOnChainWallet: KeyManager.Bip84OnChainKeys = KeyManager.Bip84OnChainKeys(chain, master, account = 0)
Expand All @@ -50,7 +49,7 @@ data class LocalKeyManager(val seed: ByteVector, val chain: Chain, val remoteSwa
else -> DeterministicWallet.tpub
}
require(prefix == expectedPrefix) { "unexpected swap-in xpub prefix $prefix (expected $expectedPrefix)" }
val remoteSwapInPublicKey = DeterministicWallet.derivePublicKey(xpub, KeyManager.SwapInOnChainKeys.perUserPath(nodeKeys.nodeKey.publicKey)).publicKey
val remoteSwapInPublicKey = xpub.derivePublicKey(KeyManager.SwapInOnChainKeys.perUserPath(nodeKeys.nodeKey.publicKey)).publicKey
KeyManager.SwapInOnChainKeys(chain, master, remoteSwapInPublicKey)
}

Expand All @@ -60,9 +59,9 @@ data class LocalKeyManager(val seed: ByteVector, val chain: Chain, val remoteSwa
* This method offers direct access to the master key derivation. It should only be used for some advanced usage
* like (LNURL-auth, data encryption).
*/
fun derivePrivateKey(keyPath: KeyPath): DeterministicWallet.ExtendedPrivateKey = derivePrivateKey(master, keyPath)
fun derivePrivateKey(keyPath: KeyPath): DeterministicWallet.ExtendedPrivateKey = master.derivePrivateKey(keyPath)

fun privateKey(keyPath: KeyPath): PrivateKey = derivePrivateKey(master, keyPath).privateKey
fun privateKey(keyPath: KeyPath): PrivateKey = master.derivePrivateKey(keyPath).privateKey

override fun newFundingKeyPath(isInitiator: Boolean): KeyPath {
val last = hardened(if (isInitiator) 1 else 0)
Expand All @@ -72,7 +71,7 @@ data class LocalKeyManager(val seed: ByteVector, val chain: Chain, val remoteSwa

override fun channelKeys(fundingKeyPath: KeyPath): KeyManager.ChannelKeys {
// We use a different funding key for each splice, with a derivation based on the fundingTxIndex.
val fundingKey: (Long) -> PrivateKey = { index -> derivePrivateKey(master, channelKeyBasePath / fundingKeyPath / hardened(index)).privateKey }
val fundingKey: (Long) -> PrivateKey = { index -> master.derivePrivateKey(channelKeyBasePath / fundingKeyPath / hardened(index)).privateKey }
// We use the initial funding pubkey to compute the channel key path, and we use the recovery process even
// in the normal case, which guarantees it works all the time.
val initialFundingPubkey = fundingKey(0).publicKey()
Expand Down Expand Up @@ -150,19 +149,19 @@ data class LocalKeyManager(val seed: ByteVector, val chain: Chain, val remoteSwa
}

fun channelKeyBasePath(chain: Chain) = when (chain) {
Chain.Regtest, Chain.Testnet, Chain.Signet -> KeyPath.empty / hardened(48) / hardened(1)
Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(48) / hardened(1)
Chain.Mainnet -> KeyPath.empty / hardened(50) / hardened(1)
}

/** Path for node keys generated by eclair-core */
@Deprecated("used for backward-compat with eclair-core", replaceWith = ReplaceWith("nodeKeyBasePath(chain)"))
fun eclairNodeKeyBasePath(chain: Chain) = when (chain) {
Chain.Regtest, Chain.Testnet, Chain.Signet -> KeyPath.empty / hardened(46) / hardened(0)
Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(46) / hardened(0)
Chain.Mainnet -> KeyPath.empty / hardened(47) / hardened(0)
}

fun nodeKeyBasePath(chain: Chain) = when (chain) {
Chain.Regtest, Chain.Testnet, Chain.Signet -> KeyPath.empty / hardened(48) / hardened(0)
Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(48) / hardened(0)
Chain.Mainnet -> KeyPath.empty / hardened(50) / hardened(0)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -673,7 +673,8 @@ object JsonSerializers {
chain = when {
o.chains.isEmpty() -> Chain.Mainnet.name
o.chains.contains(Chain.Mainnet.chainHash) && o.chains.size == 1 -> Chain.Mainnet.name
o.chains.contains(Chain.Testnet.chainHash) && o.chains.size == 1 -> Chain.Testnet.name
o.chains.contains(Chain.Testnet3.chainHash) && o.chains.size == 1 -> Chain.Testnet3.name
o.chains.contains(Chain.Testnet4.chainHash) && o.chains.size == 1 -> Chain.Testnet4.name
o.chains.contains(Chain.Regtest.chainHash) && o.chains.size == 1 -> Chain.Regtest.name
else -> "unknown"
}.lowercase(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ data class Bolt11Invoice(

private val prefixes = mapOf(
Chain.Regtest to "lnbcrt",
Chain.Testnet to "lntb",
Chain.Testnet3 to "lntb",
Chain.Testnet4 to "lntb",
Chain.Mainnet to "lnbc"
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ data class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: Pub
// The script path contains a refund script, generated from this policy: and_v(v:pk(user),older(refundDelay)).
// It does not depend upon the user's or server's key, just the user's refund key and the refund delay.
private val refundScript = listOf(OP_PUSHDATA(userRefundKey.xOnly()), OP_CHECKSIGVERIFY, OP_PUSHDATA(Script.encodeNumber(refundDelay)), OP_CHECKSEQUENCEVERIFY)
private val scriptTree = ScriptTree.Leaf(0, refundScript)
private val scriptTree = ScriptTree.Leaf(refundScript)
val pubkeyScript: List<ScriptElt> = Script.pay2tr(internalPublicKey, scriptTree)
val serializedPubkeyScript = Script.write(pubkeyScript).byteVector()

Expand All @@ -46,7 +46,7 @@ data class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: Pub

fun signSwapInputRefund(fundingTx: Transaction, index: Int, parentTxOuts: List<TxOut>, userPrivateKey: PrivateKey): ByteVector64 {
require(userPrivateKey.publicKey() == userRefundKey) { "refund private key does not match expected public key: are you using the user key instead of the refund key?" }
return Transaction.signInputTaprootScriptPath(userPrivateKey, fundingTx, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, scriptTree.hash())
return fundingTx.signInputTaprootScriptPath(userPrivateKey, index, parentTxOuts, SigHash.SIGHASH_DEFAULT, scriptTree.hash())
}

fun signSwapInputServer(fundingTx: Transaction, index: Int, parentTxOuts: List<TxOut>, serverPrivateKey: PrivateKey, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce): Either<Throwable, ByteVector32> {
Expand All @@ -62,7 +62,7 @@ data class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: Pub
Chain.Mainnet -> DeterministicWallet.xprv
else -> DeterministicWallet.tprv
}
val xpriv = DeterministicWallet.encode(masterRefundKey, prefix)
val xpriv = masterRefundKey.encode(prefix)
val desc = "tr(${internalPubKey.value},and_v(v:pk($xpriv/*),older($refundDelay)))"
val checksum = Descriptor.checksum(desc)
return "$desc#$checksum"
Expand All @@ -74,7 +74,7 @@ data class SwapInProtocol(val userPublicKey: PublicKey, val serverPublicKey: Pub
Chain.Mainnet -> DeterministicWallet.xpub
else -> DeterministicWallet.tpub
}
val xpub = DeterministicWallet.encode(masterRefundKey, prefix)
val xpub = masterRefundKey.encode(prefix)
val desc = "tr(${internalPubKey.value},and_v(v:pk($xpub/*),older($refundDelay)))"
val checksum = Descriptor.checksum(desc)
return "$desc#$checksum"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ object Transactions {
.also { check(Scripts.der(it, SigHash.SIGHASH_ALL).size() == 72) { "Should be 72 bytes but is ${Scripts.der(it, SigHash.SIGHASH_ALL).size()} bytes" } }

fun sign(tx: Transaction, inputIndex: Int, redeemScript: ByteArray, amount: Satoshi, key: PrivateKey, sigHash: Int = SigHash.SIGHASH_ALL): ByteVector64 {
val sigDER = Transaction.signInput(tx, inputIndex, redeemScript, sigHash, amount, SigVersion.SIGVERSION_WITNESS_V0, key)
val sigDER = tx.signInput(inputIndex, redeemScript, sigHash, amount, SigVersion.SIGVERSION_WITNESS_V0, key)
return Crypto.der2compact(sigDER)
}

Expand Down Expand Up @@ -873,11 +873,11 @@ object Transactions {
}

fun checkSpendable(txinfo: TransactionWithInputInfo): Try<Unit> = runTrying {
Transaction.correctlySpends(txinfo.tx, mapOf(txinfo.tx.txIn.first().outPoint to txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
txinfo.tx.correctlySpends(mapOf(txinfo.tx.txIn.first().outPoint to txinfo.input.txOut), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS)
}

fun checkSig(txinfo: TransactionWithInputInfo, sig: ByteVector64, pubKey: PublicKey, sigHash: Int = SigHash.SIGHASH_ALL): Boolean {
val data = Transaction.hashForSigning(txinfo.tx, 0, txinfo.input.redeemScript.toByteArray(), sigHash, txinfo.input.txOut.amount, SigVersion.SIGVERSION_WITNESS_V0)
val data = txinfo.tx.hashForSigning(0, txinfo.input.redeemScript.toByteArray(), sigHash, txinfo.input.txOut.amount, SigVersion.SIGVERSION_WITNESS_V0)
return Crypto.verifySignature(data, sig, pubKey)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ class SwapInWalletTestsCommon : LightningTestSuite() {
@Test
fun `swap-in wallet test`() = runSuspendTest(timeout = 15.seconds) {
val mnemonics = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".split(" ")
val keyManager = LocalKeyManager(MnemonicCode.toSeed(mnemonics, "").toByteVector(), Chain.Testnet, TestConstants.aliceSwapInServerXpub)
val keyManager = LocalKeyManager(MnemonicCode.toSeed(mnemonics, "").toByteVector(), Chain.Testnet3, TestConstants.aliceSwapInServerXpub)
val client = connectToTestnetServer()
val wallet = SwapInWallet(Chain.Testnet, keyManager.swapInOnChainWallet, client, this, loggerFactory)
val wallet = SwapInWallet(Chain.Testnet3, keyManager.swapInOnChainWallet, client, this, loggerFactory)

// addresses 0 to 3 have funds on them, the current address is the 4th
assertEquals(4, wallet.swapInAddressFlow.filterNotNull().first().second)
Expand Down
Loading

0 comments on commit 1493200

Please sign in to comment.