From 3b8d4eb67ca326f7e3227ee91f7bf09c0c452c5c Mon Sep 17 00:00:00 2001 From: t-bast Date: Wed, 9 Jul 2025 09:25:12 +0200 Subject: [PATCH] Refactor `ChannelKeys` and `KeyManager` We refactor our `ChannelKeys` to use the same model as `eclair`. This is a first step before refactoring transactions and channel data similarly to what was done in `eclair` in preparation for taproot channels. Backwards-compatibility is guaranteed by our unit tests for the local key manager and transactions building. --- .../blockchain/electrum/ElectrumMiniWallet.kt | 6 +- .../blockchain/electrum/FinalWallet.kt | 4 +- .../blockchain/electrum/SwapInWallet.kt | 4 +- .../acinq/lightning/channel/ChannelCommand.kt | 4 +- .../fr/acinq/lightning/channel/Commitments.kt | 215 ++++--- .../fr/acinq/lightning/channel/Helpers.kt | 262 ++++----- .../acinq/lightning/channel/InteractiveTx.kt | 91 +-- .../acinq/lightning/channel/states/Channel.kt | 13 +- .../acinq/lightning/channel/states/Closing.kt | 4 +- .../acinq/lightning/channel/states/Normal.kt | 4 +- .../acinq/lightning/channel/states/Syncing.kt | 18 +- .../channel/states/WaitForAcceptChannel.kt | 2 +- .../lightning/channel/states/WaitForInit.kt | 12 +- .../channel/states/WaitForOpenChannel.kt | 10 +- .../acinq/lightning/crypto/Bolt3Derivation.kt | 48 -- .../fr/acinq/lightning/crypto/KeyManager.kt | 549 +++++++++++------- .../acinq/lightning/crypto/LocalKeyManager.kt | 59 +- .../acinq/lightning/json/JsonSerializers.kt | 7 - .../lightning/transactions/Transactions.kt | 58 +- .../channel/InteractiveTxTestsCommon.kt | 36 +- .../fr/acinq/lightning/channel/TestsHelper.kt | 62 +- .../crypto/LocalKeyManagerTestsCommon.kt | 55 +- .../transactions/AnchorOutputsTestsCommon.kt | 85 ++- .../transactions/TransactionsTestsCommon.kt | 138 +++-- 24 files changed, 898 insertions(+), 848 deletions(-) delete mode 100644 modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/Bolt3Derivation.kt diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt index 21c6875c3..088f76e15 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/ElectrumMiniWallet.kt @@ -4,7 +4,7 @@ import co.touchlab.kermit.Logger import fr.acinq.bitcoin.* import fr.acinq.lightning.SwapInParams import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.SwapInOnChainKeys import fr.acinq.lightning.logging.debug import fr.acinq.lightning.logging.info import fr.acinq.lightning.utils.sat @@ -96,9 +96,9 @@ data class WalletState(val addresses: Map) { }.coerceAtLeast(0) /** Builds a transaction spending all expired utxos and computes the mining fee. The transaction is fully signed but not published. */ - fun spendExpiredSwapIn(swapInKeys: KeyManager.SwapInOnChainKeys, scriptPubKey: ByteVector, feerate: FeeratePerKw): Pair? { + fun spendExpiredSwapIn(swapInKeys: SwapInOnChainKeys, scriptPubKey: ByteVector, feerate: FeeratePerKw): Pair? { val utxos = readyForRefund.map { - KeyManager.SwapInOnChainKeys.SwapInUtxo( + SwapInOnChainKeys.SwapInUtxo( txOut = it.txOut, outPoint = it.outPoint, addressIndex = it.addressMeta.indexOrNull diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/FinalWallet.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/FinalWallet.kt index b5989abcd..5c3f7ab44 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/FinalWallet.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/FinalWallet.kt @@ -2,7 +2,7 @@ package fr.acinq.lightning.blockchain.electrum import fr.acinq.bitcoin.* import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.Bip84OnChainKeys import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.logging.info import fr.acinq.lightning.transactions.Transactions @@ -14,7 +14,7 @@ import kotlinx.coroutines.launch class FinalWallet( private val chain: Chain, - private val finalWalletKeys: KeyManager.Bip84OnChainKeys, + private val finalWalletKeys: Bip84OnChainKeys, electrum: IElectrumClient, scope: CoroutineScope, loggerFactory: LoggerFactory diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWallet.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWallet.kt index d773b97f8..03a5cb63c 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWallet.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/blockchain/electrum/SwapInWallet.kt @@ -1,7 +1,7 @@ package fr.acinq.lightning.blockchain.electrum import fr.acinq.bitcoin.Chain -import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.SwapInOnChainKeys import fr.acinq.lightning.logging.LoggerFactory import fr.acinq.lightning.logging.info import kotlinx.coroutines.CoroutineScope @@ -13,7 +13,7 @@ import kotlinx.coroutines.launch class SwapInWallet( chain: Chain, - swapInKeys: KeyManager.SwapInOnChainKeys, + swapInKeys: SwapInOnChainKeys, electrum: IElectrumClient, scope: CoroutineScope, loggerFactory: LoggerFactory diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt index 59b98a4be..723ee170a 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt @@ -7,7 +7,7 @@ import fr.acinq.lightning.blockchain.WatchTriggered import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.states.PersistedChannelState -import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.utils.UUID import fr.acinq.lightning.wire.FailureMessage import fr.acinq.lightning.wire.LightningMessage @@ -36,7 +36,7 @@ sealed class ChannelCommand { val requestRemoteFunding: LiquidityAds.RequestFunding?, val channelOrigin: Origin?, ) : Init() { - fun temporaryChannelId(keyManager: KeyManager): ByteVector32 = keyManager.channelKeys(localParams.fundingKeyPath).temporaryChannelId + fun temporaryChannelId(channelKeys: ChannelKeys): ByteVector32 = (ByteVector(ByteArray(33) { 0 }) + channelKeys.revocationBasePoint.value).sha256() } data class NonInitiator( diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt index 4e5d5976c..38aef002f 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Commitments.kt @@ -13,9 +13,9 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.blockchain.fee.FeerateTolerance import fr.acinq.lightning.channel.states.Channel import fr.acinq.lightning.channel.states.ChannelContext -import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForCommitment -import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForRevocation -import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.ChannelKeys +import fr.acinq.lightning.crypto.LocalCommitmentKeys +import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.payment.OutgoingPaymentPacket @@ -99,50 +99,54 @@ data class PublishableTxs(val commitTx: CommitTx, val htlcTxsAndSigs: List { + fun fromCommitSig( + channelParams: ChannelParams, + commitKeys: LocalCommitmentKeys, + fundingKey: PrivateKey, + remoteFundingPubKey: PublicKey, + commitInput: Transactions.InputInfo, + commit: CommitSig, + localCommitIndex: Long, + spec: CommitmentSpec, + log: MDCLogger + ): Either { val (localCommitTx, sortedHtlcTxs) = Commitments.makeLocalTxs( - keyManager, + channelParams = channelParams, + commitKeys = commitKeys, commitTxNumber = localCommitIndex, - params.localParams, - params.remoteParams, - fundingTxIndex = fundingTxIndex, + localFundingKey = fundingKey, remoteFundingPubKey = remoteFundingPubKey, - commitInput, - localPerCommitmentPoint = localPerCommitmentPoint, - spec + commitmentInput = commitInput, + spec = spec, ) - val sig = Transactions.sign(localCommitTx, keyManager.fundingKey(fundingTxIndex)) - + val sig = Transactions.sign(localCommitTx, fundingKey) // no need to compute htlc sigs if commit sig doesn't check out - val signedCommitTx = Transactions.addSigs(localCommitTx, keyManager.fundingPubKey(fundingTxIndex), remoteFundingPubKey, sig, commit.signature) + val signedCommitTx = Transactions.addSigs(localCommitTx, fundingKey.publicKey(), remoteFundingPubKey, sig, commit.signature) when (val check = Transactions.checkSpendable(signedCommitTx)) { is Try.Failure -> { log.error(check.error) { "remote signature $commit is invalid" } - return Either.Left(InvalidCommitmentSignature(params.channelId, signedCommitTx.tx.txid)) + return Either.Left(InvalidCommitmentSignature(channelParams.channelId, signedCommitTx.tx.txid)) } else -> {} } if (commit.htlcSignatures.size != sortedHtlcTxs.size) { - return Either.Left(HtlcSigCountMismatch(params.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) + return Either.Left(HtlcSigCountMismatch(channelParams.channelId, sortedHtlcTxs.size, commit.htlcSignatures.size)) } - val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, keyManager.htlcKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) } - val remoteHtlcPubkey = params.remoteParams.htlcBasepoint.deriveForCommitment(localPerCommitmentPoint) + val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, commitKeys.ourHtlcKey, SigHash.SIGHASH_ALL) } // combine the sigs to make signed txs val htlcTxsAndSigs = Triple(sortedHtlcTxs, htlcSigs, commit.htlcSignatures).zipped().map { (htlcTx, localSig, remoteSig) -> when (htlcTx) { is HtlcTx.HtlcTimeoutTx -> { if (Transactions.checkSpendable(Transactions.addSigs(htlcTx, localSig, remoteSig)).isFailure) { - return Either.Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) + return Either.Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid)) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) } is HtlcTx.HtlcSuccessTx -> { // we can't check that htlc-success tx are spendable because we need the payment preimage; thus we only check the remote sig // which was created with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - if (!Transactions.checkSig(htlcTx, remoteSig, remoteHtlcPubkey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { - return Either.Left(InvalidHtlcSignature(params.channelId, htlcTx.tx.txid)) + if (!Transactions.checkSig(htlcTx, remoteSig, commitKeys.theirHtlcPublicKey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY)) { + return Either.Left(InvalidHtlcSignature(channelParams.channelId, htlcTx.tx.txid)) } HtlcTxAndSigs(htlcTx, localSig, remoteSig) } @@ -155,29 +159,30 @@ data class LocalCommit(val index: Long, val spec: CommitmentSpec, val publishabl /** The remote commitment maps to a commitment transaction that only our peer can sign and broadcast. */ data class RemoteCommit(val index: Long, val spec: CommitmentSpec, val txid: TxId, val remotePerCommitmentPoint: PublicKey) { - fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, batchSize: Int): CommitSig { + fun sign(channelParams: ChannelParams, channelKeys: ChannelKeys, fundingTxIndex: Long, remoteFundingPubKey: PublicKey, commitInput: Transactions.InputInfo, batchSize: Int): CommitSig { + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val commitKeys = channelKeys.remoteCommitmentKeys(channelParams, remotePerCommitmentPoint) val (remoteCommitTx, sortedHtlcsTxs) = Commitments.makeRemoteTxs( - channelKeys, - index, - params.localParams, - params.remoteParams, - fundingTxIndex = fundingTxIndex, + channelParams = channelParams, + commitKeys = commitKeys, + commitTxNumber = index, + localFundingKey = fundingKey, remoteFundingPubKey = remoteFundingPubKey, - commitInput, - remotePerCommitmentPoint = remotePerCommitmentPoint, - spec + commitmentInput = commitInput, + spec = spec ) - val sig = Transactions.sign(remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val sig = Transactions.sign(remoteCommitTx, fundingKey) // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - val htlcSigs = sortedHtlcsTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + val htlcSigs = sortedHtlcsTxs.map { Transactions.sign(it, commitKeys.ourHtlcKey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } val tlvs = buildSet { if (batchSize > 1) add(CommitSigTlv.Batch(batchSize)) } - return CommitSig(params.channelId, sig, htlcSigs.toList(), TlvStream(tlvs)) + return CommitSig(channelParams.channelId, sig, htlcSigs.toList(), TlvStream(tlvs)) } - fun sign(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, signingSession: InteractiveTxSigningSession): CommitSig = - sign(channelKeys, params, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput, batchSize = 1) + fun sign(channelParams: ChannelParams, channelKeys: ChannelKeys, signingSession: InteractiveTxSigningSession): CommitSig { + return sign(channelParams, channelKeys, signingSession.fundingTxIndex, signingSession.fundingParams.remoteFundingPubkey, signingSession.commitInput, batchSize = 1) + } } /** We have the next remote commit when we've sent our commit_sig but haven't yet received their revoke_and_ack. */ @@ -208,8 +213,11 @@ sealed class RemoteFundingStatus { data class Commitment( val fundingTxIndex: Long, val remoteFundingPubkey: PublicKey, - val localFundingStatus: LocalFundingStatus, val remoteFundingStatus: RemoteFundingStatus, - val localCommit: LocalCommit, val remoteCommit: RemoteCommit, val nextRemoteCommit: NextRemoteCommit? + val localFundingStatus: LocalFundingStatus, + val remoteFundingStatus: RemoteFundingStatus, + val localCommit: LocalCommit, + val remoteCommit: RemoteCommit, + val nextRemoteCommit: NextRemoteCommit? ) { val commitInput = localCommit.publishableTxs.commitTx.input val fundingTxId: TxId = commitInput.outPoint.txid @@ -470,24 +478,23 @@ data class Commitment( } } - fun sendCommit(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, log: MDCLogger): Pair { + fun sendCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: RemoteCommitmentKeys, changes: CommitmentChanges, remoteNextPerCommitmentPoint: PublicKey, batchSize: Int, log: MDCLogger): Pair { + val fundingKey = channelKeys.fundingKey(fundingTxIndex) // remote commitment will include all local changes + remote acked changes val spec = CommitmentSpec.reduce(remoteCommit.spec, changes.remoteChanges.acked, changes.localChanges.proposed) val (remoteCommitTx, sortedHtlcTxs) = Commitments.makeRemoteTxs( - channelKeys, + channelParams = params, + commitKeys = commitKeys, commitTxNumber = remoteCommit.index + 1, - params.localParams, - params.remoteParams, - fundingTxIndex = fundingTxIndex, + localFundingKey = fundingKey, remoteFundingPubKey = remoteFundingPubkey, - commitInput, - remotePerCommitmentPoint = remoteNextPerCommitmentPoint, - spec + commitmentInput = commitInput, + spec = spec ) - val sig = Transactions.sign(remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val sig = Transactions.sign(remoteCommitTx, fundingKey) // we sign our peer's HTLC txs with SIGHASH_SINGLE || SIGHASH_ANYONECANPAY - val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remoteNextPerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } + val htlcSigs = sortedHtlcTxs.map { Transactions.sign(it, commitKeys.ourHtlcKey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } // NB: IN/OUT htlcs are inverted because this is the remote commit log.info { @@ -500,8 +507,16 @@ data class Commitment( if (spec.htlcs.isEmpty()) { val alternativeSigs = Commitments.alternativeFeerates.map { feerate -> val alternativeSpec = spec.copy(feerate = feerate) - val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs(channelKeys, commitTxNumber = remoteCommit.index + 1, params.localParams, params.remoteParams, fundingTxIndex = fundingTxIndex, remoteFundingPubKey = remoteFundingPubkey, commitInput, remotePerCommitmentPoint = remoteNextPerCommitmentPoint, alternativeSpec) - val alternativeSig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( + channelParams = params, + commitKeys = commitKeys, + commitTxNumber = remoteCommit.index + 1, + localFundingKey = fundingKey, + remoteFundingPubKey = remoteFundingPubkey, + commitmentInput = commitInput, + spec = alternativeSpec + ) + val alternativeSig = Transactions.sign(alternativeRemoteCommitTx, fundingKey) CommitSigTlv.AlternativeFeerateSig(feerate, alternativeSig) } add(CommitSigTlv.AlternativeFeerateSigs(alternativeSigs)) @@ -515,7 +530,7 @@ data class Commitment( return Pair(commitment1, commitSig) } - fun receiveCommit(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, changes: CommitmentChanges, commit: CommitSig, log: MDCLogger): Either { + fun receiveCommit(params: ChannelParams, channelKeys: ChannelKeys, commitKeys: LocalCommitmentKeys, changes: CommitmentChanges, commit: CommitSig, log: MDCLogger): Either { // they sent us a signature for *their* view of *our* next commit tx // so in terms of rev.hashes and indexes we have: // ourCommit.index -> our current revocation hash, which is about to become our old revocation hash @@ -524,14 +539,9 @@ data class Commitment( // 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 - - // check that their signature is valid - // signatures are now optional in the commit message, and will be sent only if the other party is actually - // receiving money i.e its commit tx has one output for them + val fundingKey = channelKeys.fundingKey(fundingTxIndex) val spec = CommitmentSpec.reduce(localCommit.spec, changes.localChanges.acked, changes.remoteChanges.proposed) - val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommit.index + 1) - - return LocalCommit.fromCommitSig(channelKeys, params, fundingTxIndex, remoteFundingPubkey, commitInput, commit, localCommit.index + 1, spec, localPerCommitmentPoint, log).map { localCommit1 -> + return LocalCommit.fromCommitSig(params, commitKeys, fundingKey, remoteFundingPubkey, commitInput, commit, localCommit.index + 1, spec, log).map { localCommit1 -> log.info { val htlcsIn = spec.htlcs.incomings().map { it.id }.joinToString(",") val htlcsOut = spec.htlcs.outgoings().map { it.id }.joinToString(",") @@ -762,10 +772,11 @@ data class Commitments( return failure?.let { Either.Left(it) } ?: Either.Right(copy(changes = changes1)) } - fun sendCommit(channelKeys: KeyManager.ChannelKeys, log: MDCLogger): Either> { + fun sendCommit(channelKeys: ChannelKeys, log: MDCLogger): Either> { val remoteNextPerCommitmentPoint = remoteNextCommitInfo.right ?: return Either.Left(CannotSignBeforeRevocation(channelId)) + val commitKeys = channelKeys.remoteCommitmentKeys(params, remoteNextPerCommitmentPoint) if (!changes.localHasChanges()) return Either.Left(CannotSignWithoutChanges(channelId)) - val (active1, sigs) = active.map { it.sendCommit(channelKeys, params, changes, remoteNextPerCommitmentPoint, active.size, log) }.unzip() + val (active1, sigs) = active.map { it.sendCommit(params, channelKeys, commitKeys, changes, remoteNextPerCommitmentPoint, active.size, log) }.unzip() val commitments1 = copy( active = active1, remoteNextCommitInfo = Either.Left(WaitingForRevocation(localCommitIndex)), @@ -777,7 +788,7 @@ data class Commitments( return Either.Right(Pair(commitments1, CommitSigs.fromSigs(sigs))) } - fun receiveCommit(commits: CommitSigs, channelKeys: KeyManager.ChannelKeys, log: MDCLogger): Either> { + fun receiveCommit(commits: CommitSigs, channelKeys: ChannelKeys, log: MDCLogger): Either> { // We may receive more commit_sig than the number of active commitments, because there can be a race where we send splice_locked // while our peer is sending us a batch of commit_sig. When that happens, we simply need to discard the commit_sig that belong // to commitments we deactivated. @@ -788,9 +799,10 @@ data class Commitments( if (sigs.size < active.size) { return Either.Left(CommitSigCountMismatch(channelId, active.size, sigs.size)) } + val commitKeys = channelKeys.localCommitmentKeys(params, 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(channelKeys, params, changes, it.second, log)) { + when (val commitment1 = it.first.receiveCommit(params, channelKeys, commitKeys, changes, it.second, log)) { is Either.Left -> return Either.Left(commitment1.value) is Either.Right -> commitment1.value } @@ -1018,7 +1030,8 @@ data class Commitments( * Our peer may publish an alternative version of their commitment using a different feerate. * This function lists all the alternative commitments they have signatures for. */ - fun alternativeFeerateCommits(commitments: Commitments, channelKeys: KeyManager.ChannelKeys): List { + fun alternativeFeerateCommits(commitments: Commitments, channelKeys: ChannelKeys): List { + val localFundingKey = channelKeys.fundingKey(commitments.latest.fundingTxIndex) return buildList { add(commitments.latest.remoteCommit) commitments.latest.nextRemoteCommit?.let { add(it.commit) } @@ -1027,79 +1040,57 @@ data class Commitments( }.flatMap { remoteCommit -> alternativeFeerates.map { feerate -> val alternativeSpec = remoteCommit.spec.copy(feerate = feerate) - val (alternativeRemoteCommitTx, _) = makeRemoteTxs(channelKeys, remoteCommit.index, commitments.params.localParams, commitments.params.remoteParams, commitments.latest.fundingTxIndex, commitments.latest.remoteFundingPubkey, commitments.latest.commitInput, remoteCommit.remotePerCommitmentPoint, alternativeSpec) + val commitKeys = channelKeys.remoteCommitmentKeys(commitments.params, remoteCommit.remotePerCommitmentPoint) + val (alternativeRemoteCommitTx, _) = makeRemoteTxs(commitments.params, commitKeys, remoteCommit.index, localFundingKey, commitments.latest.remoteFundingPubkey, commitments.latest.commitInput, alternativeSpec) RemoteCommit(remoteCommit.index, alternativeSpec, alternativeRemoteCommitTx.tx.txid, remoteCommit.remotePerCommitmentPoint) } } } fun makeLocalTxs( - channelKeys: KeyManager.ChannelKeys, + channelParams: ChannelParams, + commitKeys: LocalCommitmentKeys, commitTxNumber: Long, - localParams: LocalParams, - remoteParams: RemoteParams, - fundingTxIndex: Long, + localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitmentInput: Transactions.InputInfo, - localPerCommitmentPoint: PublicKey, spec: CommitmentSpec ): Pair> { - val localDelayedPaymentPubkey = channelKeys.delayedPaymentBasepoint.deriveForCommitment(localPerCommitmentPoint) - val localHtlcPubkey = channelKeys.htlcBasepoint.deriveForCommitment(localPerCommitmentPoint) - val remotePaymentPubkey = remoteParams.paymentBasepoint - val remoteHtlcPubkey = remoteParams.htlcBasepoint.deriveForCommitment(localPerCommitmentPoint) - val localRevocationPubkey = remoteParams.revocationBasepoint.deriveForRevocation(localPerCommitmentPoint) - val localPaymentBasepoint = channelKeys.paymentBasepoint val outputs = makeCommitTxOutputs( - channelKeys.fundingPubKey(fundingTxIndex), - remoteFundingPubKey, - localParams.paysCommitTxFees, - localParams.dustLimit, - localRevocationPubkey, - remoteParams.toSelfDelay, - localDelayedPaymentPubkey, - remotePaymentPubkey, - localHtlcPubkey, - remoteHtlcPubkey, - spec + localFundingPubkey = localFundingKey.publicKey(), + remoteFundingPubkey = remoteFundingPubKey, + commitKeys = commitKeys.publicKeys, + payCommitTxFees = channelParams.localParams.paysCommitTxFees, + dustLimit = channelParams.localParams.dustLimit, + toSelfDelay = channelParams.remoteParams.toSelfDelay, + spec = spec ) - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, localPaymentBasepoint, remoteParams.paymentBasepoint, localParams.isChannelOpener, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localParams.dustLimit, localRevocationPubkey, remoteParams.toSelfDelay, localDelayedPaymentPubkey, spec.feerate, outputs) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, commitKeys.ourPaymentBasePoint, channelParams.remoteParams.paymentBasepoint, channelParams.localParams.isChannelOpener, outputs) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, commitKeys.publicKeys, channelParams.localParams.dustLimit, channelParams.remoteParams.toSelfDelay, spec.feerate, outputs) return Pair(commitTx, htlcTxs) } fun makeRemoteTxs( - channelKeys: KeyManager.ChannelKeys, + channelParams: ChannelParams, + commitKeys: RemoteCommitmentKeys, commitTxNumber: Long, - localParams: LocalParams, - remoteParams: RemoteParams, - fundingTxIndex: Long, + localFundingKey: PrivateKey, remoteFundingPubKey: PublicKey, commitmentInput: Transactions.InputInfo, - remotePerCommitmentPoint: PublicKey, spec: CommitmentSpec ): Pair> { - val localPaymentPubkey = channelKeys.paymentBasepoint - val localHtlcPubkey = channelKeys.htlcBasepoint.deriveForCommitment(remotePerCommitmentPoint) - val remoteDelayedPaymentPubkey = remoteParams.delayedPaymentBasepoint.deriveForCommitment(remotePerCommitmentPoint) - val remoteHtlcPubkey = remoteParams.htlcBasepoint.deriveForCommitment(remotePerCommitmentPoint) - val remoteRevocationPubkey = channelKeys.revocationBasepoint.deriveForRevocation(remotePerCommitmentPoint) val outputs = makeCommitTxOutputs( - remoteFundingPubKey, - channelKeys.fundingPubKey(fundingTxIndex), - !localParams.paysCommitTxFees, - remoteParams.dustLimit, - remoteRevocationPubkey, - localParams.toSelfDelay, - remoteDelayedPaymentPubkey, - localPaymentPubkey, - remoteHtlcPubkey, - localHtlcPubkey, - spec + localFundingPubkey = remoteFundingPubKey, + remoteFundingPubkey = localFundingKey.publicKey(), + commitKeys = commitKeys.publicKeys, + payCommitTxFees = !channelParams.localParams.paysCommitTxFees, + dustLimit = channelParams.remoteParams.dustLimit, + toSelfDelay = channelParams.localParams.toSelfDelay, + spec = spec ) // NB: we are creating the remote commit tx, so local/remote parameters are inverted. - val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, remoteParams.paymentBasepoint, localPaymentPubkey, !localParams.isChannelOpener, outputs) - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, remoteParams.dustLimit, remoteRevocationPubkey, localParams.toSelfDelay, remoteDelayedPaymentPubkey, spec.feerate, outputs) + val commitTx = Transactions.makeCommitTx(commitmentInput, commitTxNumber, channelParams.remoteParams.paymentBasepoint, commitKeys.ourPaymentBasePoint, !channelParams.localParams.isChannelOpener, outputs) + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, commitKeys.publicKeys, channelParams.remoteParams.dustLimit, channelParams.localParams.toSelfDelay, spec.feerate, outputs) return Pair(commitTx, htlcTxs) } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt index 5e3275ff6..64732207c 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt @@ -16,9 +16,9 @@ import fr.acinq.lightning.blockchain.fee.FeerateTolerance import fr.acinq.lightning.blockchain.fee.OnChainFeerates import fr.acinq.lightning.channel.Helpers.Closing.inputsAlreadySpent import fr.acinq.lightning.channel.states.Channel -import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForCommitment -import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForRevocation -import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.ChannelKeys +import fr.acinq.lightning.crypto.LocalCommitmentKeys +import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.logging.LoggingContext import fr.acinq.lightning.transactions.* @@ -217,10 +217,7 @@ object Helpers { * @return (localSpec, localTx, remoteSpec, remoteTx, fundingTxOutput) */ fun makeCommitTxs( - channelKeys: KeyManager.ChannelKeys, - channelId: ByteVector32, - localParams: LocalParams, - remoteParams: RemoteParams, + channelParams: ChannelParams, fundingAmount: Satoshi, toLocal: MilliSatoshi, toRemote: MilliSatoshi, @@ -228,53 +225,47 @@ object Helpers { localCommitmentIndex: Long, remoteCommitmentIndex: Long, commitTxFeerate: FeeratePerKw, - fundingTxIndex: Long, fundingTxId: TxId, fundingTxOutputIndex: Int, + localFundingKey: PrivateKey, remoteFundingPubkey: PublicKey, - remotePerCommitmentPoint: PublicKey + localCommitKeys: LocalCommitmentKeys, + remoteCommitKeys: RemoteCommitmentKeys ): Either { val localSpec = CommitmentSpec(localHtlcs, commitTxFeerate, toLocal = toLocal, toRemote = toRemote) val remoteSpec = CommitmentSpec(localHtlcs.map { it.opposite() }.toSet(), commitTxFeerate, toLocal = toRemote, toRemote = toLocal) - if (!localParams.paysCommitTxFees) { + if (!channelParams.localParams.paysCommitTxFees) { // They are responsible for paying the commitment transaction 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 // 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 = commitTxFee(remoteParams.dustLimit, remoteSpec) + val fees = commitTxFee(channelParams.remoteParams.dustLimit, remoteSpec) val missing = fees - remoteSpec.toLocal.truncateToSatoshi() if (missing > 0.sat) { - return Either.Left(CannotAffordFirstCommitFees(channelId, missing = missing, fees = fees)) + return Either.Left(CannotAffordFirstCommitFees(channelParams.channelId, missing = missing, fees = fees)) } } - val fundingPubKey = channelKeys.fundingPubKey(fundingTxIndex) - val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, fundingPubKey, remoteFundingPubkey) - val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommitmentIndex) + val commitmentInput = makeFundingInputInfo(fundingTxId, fundingTxOutputIndex, fundingAmount, localFundingKey.publicKey(), remoteFundingPubkey) val (localCommitTx, localHtlcTxs) = Commitments.makeLocalTxs( - channelKeys, + channelParams = channelParams, + commitKeys = localCommitKeys, commitTxNumber = localCommitmentIndex, - localParams, - remoteParams, - fundingTxIndex = fundingTxIndex, + localFundingKey = localFundingKey, remoteFundingPubKey = remoteFundingPubkey, - commitmentInput, - localPerCommitmentPoint = localPerCommitmentPoint, - localSpec + commitmentInput = commitmentInput, + spec = localSpec ) val (remoteCommitTx, remoteHtlcTxs) = Commitments.makeRemoteTxs( - channelKeys, + channelParams = channelParams, + commitKeys = remoteCommitKeys, commitTxNumber = remoteCommitmentIndex, - localParams, - remoteParams, - fundingTxIndex = fundingTxIndex, + localFundingKey = localFundingKey, remoteFundingPubKey = remoteFundingPubkey, - commitmentInput, - remotePerCommitmentPoint = remotePerCommitmentPoint, - remoteSpec + commitmentInput = commitmentInput, + spec = remoteSpec ) - return Either.Right(PairOfCommitTxs(localSpec, localCommitTx, localHtlcTxs, remoteSpec, remoteCommitTx, remoteHtlcTxs)) } @@ -305,7 +296,7 @@ object Helpers { /** We are the closer: we sign closing transactions for which we pay the fees. */ fun makeClosingTxs( - channelKeys: KeyManager.ChannelKeys, + channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, @@ -347,7 +338,7 @@ object Helpers { * are not using our latest script (race condition between our closing_complete and theirs). */ fun signClosingTx( - channelKeys: KeyManager.ChannelKeys, + channelKeys: ChannelKeys, commitment: FullCommitment, localScriptPubkey: ByteVector, remoteScriptPubkey: ByteVector, @@ -395,7 +386,7 @@ object Helpers { * for their next closing_sig that will match our latest closing_complete. */ fun receiveClosingSig( - channelKeys: KeyManager.ChannelKeys, + channelKeys: ChannelKeys, commitment: FullCommitment, closingTxs: Transactions.ClosingTxs, closingSig: ClosingSig @@ -426,33 +417,29 @@ object Helpers { * @param commitment our commitment data, which includes payment preimages. * @return a list of transactions (one per output that we can claim). */ - fun LoggingContext.claimCurrentLocalCommitTxOutputs(channelKeys: KeyManager.ChannelKeys, commitment: FullCommitment, tx: Transaction, feerates: OnChainFeerates): LocalCommitPublished { - val localCommit = commitment.localCommit - val localParams = commitment.params.localParams - require(localCommit.publishableTxs.commitTx.tx.txid == tx.txid) { "txid mismatch, provided tx is not the current local commit tx" } - val localPerCommitmentPoint = channelKeys.commitmentPoint(commitment.localCommit.index) - val localRevocationPubkey = commitment.params.remoteParams.revocationBasepoint.deriveForRevocation(localPerCommitmentPoint) - val localDelayedPubkey = channelKeys.delayedPaymentBasepoint.deriveForCommitment(localPerCommitmentPoint) + fun LoggingContext.claimCurrentLocalCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, commitTx: Transaction, feerates: OnChainFeerates): LocalCommitPublished { + require(commitment.localCommit.publishableTxs.commitTx.tx.txid == commitTx.txid) { "txid mismatch, provided tx is not the current local commit tx" } + val commitKeys = channelKeys.localCommitmentKeys(commitment.params, commitment.localCommit.index) val feerateDelayed = feerates.claimMainFeerate // first we will claim our main output as soon as the delay is over val mainDelayedTx = generateTx("main-delayed-output") { Transactions.makeClaimLocalDelayedOutputTx( - tx, - localParams.dustLimit, - localRevocationPubkey, + commitTx, + commitment.params.localParams.dustLimit, + commitKeys.revocationPublicKey, commitment.params.remoteParams.toSelfDelay, - localDelayedPubkey, - localParams.defaultFinalScriptPubKey.toByteArray(), + commitKeys.ourDelayedPaymentKey.publicKey(), + commitment.params.localParams.defaultFinalScriptPubKey.toByteArray(), feerateDelayed ) }?.let { - val sig = Transactions.sign(it, channelKeys.delayedPaymentKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) + val sig = Transactions.sign(it, commitKeys.ourDelayedPaymentKey, SigHash.SIGHASH_ALL) Transactions.addSigs(it, sig) } // We collect all the preimages we wanted to reveal to our peer. - val hash2Preimage = commitment.changes.localChanges.all.filterIsInstance().map { it.paymentPreimage }.associate { r -> r.sha256() to r } + val hash2Preimage = commitment.changes.localChanges.all.filterIsInstance().map { it.paymentPreimage }.associateBy { r -> r.sha256() } // We collect incoming HTLCs that we started failing but didn't cross-sign. val failedIncomingHtlcs = commitment.changes.localChanges.all.mapNotNull { when (it) { @@ -462,7 +449,7 @@ object Helpers { } }.toSet() - val htlcTxs = localCommit.publishableTxs.htlcTxsAndSigs.mapNotNull { + val htlcTxs = commitment.localCommit.publishableTxs.htlcTxsAndSigs.mapNotNull { when (it.txinfo) { is HtlcSuccessTx -> when { // We immediately spend incoming htlcs for which we have the preimage. @@ -487,21 +474,21 @@ object Helpers { generateTx("claim-htlc-delayed") { Transactions.makeClaimLocalDelayedOutputTx( txInfo.tx, - localParams.dustLimit, - localRevocationPubkey, + commitment.params.localParams.dustLimit, + commitKeys.revocationPublicKey, commitment.params.remoteParams.toSelfDelay, - localDelayedPubkey, - localParams.defaultFinalScriptPubKey.toByteArray(), + commitKeys.ourDelayedPaymentKey.publicKey(), + commitment.params.localParams.defaultFinalScriptPubKey.toByteArray(), feerateDelayed ) }?.let { - val sig = Transactions.sign(it, channelKeys.delayedPaymentKey.deriveForCommitment(localPerCommitmentPoint), SigHash.SIGHASH_ALL) + val sig = Transactions.sign(it, commitKeys.ourDelayedPaymentKey, SigHash.SIGHASH_ALL) Transactions.addSigs(it, sig) } } return LocalCommitPublished( - commitTx = tx, + commitTx = commitTx, claimMainDelayedOutputTx = mainDelayedTx, htlcTxs = htlcTxs, claimHtlcDelayedTxs = htlcDelayedTxs, @@ -515,50 +502,38 @@ object Helpers { * * @param commitment our commitment data, which includes payment preimages. * @param remoteCommit the remote commitment data to use to claim outputs (it can be their current or next commitment). - * @param tx the remote commitment transaction that has just been published. + * @param commitTx the remote commitment transaction that has just been published. * @return a list of transactions (one per output that we can claim). */ - fun LoggingContext.claimRemoteCommitTxOutputs(channelKeys: KeyManager.ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, tx: Transaction, feerates: OnChainFeerates): RemoteCommitPublished { - val localParams = commitment.params.localParams - val remoteParams = commitment.params.remoteParams - val commitInput = commitment.commitInput + fun LoggingContext.claimRemoteCommitTxOutputs(channelKeys: ChannelKeys, commitment: FullCommitment, remoteCommit: RemoteCommit, commitTx: Transaction, feerates: OnChainFeerates): RemoteCommitPublished { + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val commitKeys = channelKeys.remoteCommitmentKeys(commitment.params, remoteCommit.remotePerCommitmentPoint) val (remoteCommitTx, _) = Commitments.makeRemoteTxs( - channelKeys, + channelParams = commitment.params, + commitKeys = commitKeys, commitTxNumber = remoteCommit.index, - localParams, - remoteParams, - fundingTxIndex = commitment.fundingTxIndex, + localFundingKey = fundingKey, remoteFundingPubKey = commitment.remoteFundingPubkey, - commitInput, - remoteCommit.remotePerCommitmentPoint, - remoteCommit.spec + commitmentInput = commitment.commitInput, + spec = remoteCommit.spec ) - require(remoteCommitTx.tx.txid == tx.txid) { "txid mismatch, provided tx is not the current remote commit tx" } + require(remoteCommitTx.tx.txid == commitTx.txid) { "txid mismatch, provided tx is not the current remote commit tx" } - val localPaymentPubkey = channelKeys.paymentBasepoint - val localHtlcPubkey = channelKeys.htlcBasepoint.deriveForCommitment(remoteCommit.remotePerCommitmentPoint) - val remoteDelayedPaymentPubkey = remoteParams.delayedPaymentBasepoint.deriveForCommitment(remoteCommit.remotePerCommitmentPoint) - val remoteHtlcPubkey = remoteParams.htlcBasepoint.deriveForCommitment(remoteCommit.remotePerCommitmentPoint) - val remoteRevocationPubkey = channelKeys.revocationBasepoint.deriveForRevocation(remoteCommit.remotePerCommitmentPoint) val outputs = makeCommitTxOutputs( - commitment.remoteFundingPubkey, - channelKeys.fundingPubKey(commitment.fundingTxIndex), - !localParams.paysCommitTxFees, - remoteParams.dustLimit, - remoteRevocationPubkey, - localParams.toSelfDelay, - remoteDelayedPaymentPubkey, - localPaymentPubkey, - remoteHtlcPubkey, - localHtlcPubkey, - remoteCommit.spec + localFundingPubkey = fundingKey.publicKey(), + remoteFundingPubkey = commitment.remoteFundingPubkey, + commitKeys = commitKeys.publicKeys, + payCommitTxFees = !commitment.params.localParams.paysCommitTxFees, + dustLimit = commitment.params.remoteParams.dustLimit, + toSelfDelay = commitment.params.localParams.toSelfDelay, + spec = remoteCommit.spec ) // We need to use a rather high fee for htlc-claim because we compete with the counterparty. val feerateClaimHtlc = feerates.fastFeerate // We collect all the preimages we wanted to reveal to our peer. - val hash2Preimage = commitment.changes.localChanges.all.filterIsInstance().map { it.paymentPreimage }.associate { r -> r.sha256() to r } + val hash2Preimage = commitment.changes.localChanges.all.filterIsInstance().map { it.paymentPreimage }.associateBy { r -> r.sha256() } // We collect incoming HTLCs that we started failing but didn't cross-sign. val failedIncomingHtlcs = commitment.changes.localChanges.all.mapNotNull { when (it) { @@ -574,21 +549,21 @@ object Helpers { is OutgoingHtlc -> { generateTx("claim-htlc-success") { Transactions.makeClaimHtlcSuccessTx( - remoteCommitTx.tx, - outputs, - localParams.dustLimit, - localHtlcPubkey, - remoteHtlcPubkey, - remoteRevocationPubkey, - localParams.defaultFinalScriptPubKey.toByteArray(), - htlc.add, - feerateClaimHtlc + commitTx = remoteCommitTx.tx, + outputs = outputs, + localDustLimit = commitment.params.localParams.dustLimit, + localHtlcPubkey = commitKeys.ourHtlcKey.publicKey(), + remoteHtlcPubkey = commitKeys.theirHtlcPublicKey, + remoteRevocationPubkey = commitKeys.revocationPublicKey, + localFinalScriptPubKey = commitment.params.localParams.defaultFinalScriptPubKey.toByteArray(), + htlc = htlc.add, + feerate = feerateClaimHtlc ) }?.let { claimHtlcTx -> when { // We immediately spend incoming htlcs for which we have the preimage. hash2Preimage.containsKey(htlc.add.paymentHash) -> { - val sig = Transactions.sign(claimHtlcTx, channelKeys.htlcKey.deriveForCommitment(remoteCommit.remotePerCommitmentPoint), SigHash.SIGHASH_ALL) + val sig = Transactions.sign(claimHtlcTx, commitKeys.ourHtlcKey, SigHash.SIGHASH_ALL) Pair(claimHtlcTx.input.outPoint, Transactions.addSigs(claimHtlcTx, sig, hash2Preimage[htlc.add.paymentHash]!!)) } // We can ignore incoming htlcs that we started failing: our peer will claim them after the timeout. @@ -607,18 +582,18 @@ object Helpers { // back after the timeout. generateTx("claim-htlc-timeout") { Transactions.makeClaimHtlcTimeoutTx( - remoteCommitTx.tx, - outputs, - localParams.dustLimit, - localHtlcPubkey, - remoteHtlcPubkey, - remoteRevocationPubkey, - localParams.defaultFinalScriptPubKey.toByteArray(), - htlc.add, - feerateClaimHtlc + commitTx = remoteCommitTx.tx, + outputs = outputs, + localDustLimit = commitment.params.localParams.dustLimit, + localHtlcPubkey = commitKeys.ourHtlcKey.publicKey(), + remoteHtlcPubkey = commitKeys.theirHtlcPublicKey, + remoteRevocationPubkey = commitKeys.revocationPublicKey, + localFinalScriptPubKey = commitment.params.localParams.defaultFinalScriptPubKey.toByteArray(), + htlc = htlc.add, + feerate = feerateClaimHtlc ) }?.let { claimHtlcTx -> - val sig = Transactions.sign(claimHtlcTx, channelKeys.htlcKey.deriveForCommitment(remoteCommit.remotePerCommitmentPoint), SigHash.SIGHASH_ALL) + val sig = Transactions.sign(claimHtlcTx, commitKeys.ourHtlcKey, SigHash.SIGHASH_ALL) Pair(claimHtlcTx.input.outPoint, Transactions.addSigs(claimHtlcTx, sig)) } } @@ -626,32 +601,29 @@ object Helpers { }.toMap() // We claim our output and add the htlc txs we just created. - return claimRemoteCommitMainOutput(channelKeys, commitment.params, tx, feerates.claimMainFeerate).copy(claimHtlcTxs = claimHtlcTxs) + return claimRemoteCommitMainOutput(commitKeys, commitTx, commitment.params.localParams.dustLimit, commitment.params.localParams.defaultFinalScriptPubKey, feerates.claimMainFeerate).copy(claimHtlcTxs = claimHtlcTxs) } /** * Claim our main output only from their commit tx. * - * @param tx the remote commitment transaction that has just been published. + * @param commitTx the remote commitment transaction that has just been published. * @return a transaction to claim our main output. */ - internal fun LoggingContext.claimRemoteCommitMainOutput(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, tx: Transaction, claimMainFeerate: FeeratePerKw): RemoteCommitPublished { - val localPaymentPoint = channelKeys.paymentBasepoint - + internal fun LoggingContext.claimRemoteCommitMainOutput(commitKeys: RemoteCommitmentKeys, commitTx: Transaction, dustLimit: Satoshi, finalScriptPubKey: ByteVector, claimMainFeerate: FeeratePerKw): RemoteCommitPublished { val mainTx = generateTx("claim-remote-delayed-output") { Transactions.makeClaimRemoteDelayedOutputTx( - tx, - params.localParams.dustLimit, - localPaymentPoint, - params.localParams.defaultFinalScriptPubKey, - claimMainFeerate + commitTx = commitTx, + localDustLimit = dustLimit, + localPaymentPubkey = commitKeys.ourPaymentKey.publicKey(), + localFinalScriptPubKey = finalScriptPubKey, + feerate = claimMainFeerate ) }?.let { - val sig = Transactions.sign(it, channelKeys.paymentKey) + val sig = Transactions.sign(it, commitKeys.ourPaymentKey) Transactions.addSigs(it, sig) } - - return RemoteCommitPublished(commitTx = tx, claimMainOutputTx = mainTx) + return RemoteCommitPublished(commitTx = commitTx, claimMainOutputTx = mainTx) } /** @@ -665,11 +637,11 @@ object Helpers { * * This function returns the per-commitment secret in the first case, and null in the other cases. */ - fun getRemotePerCommitmentSecret(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, remotePerCommitmentSecrets: ShaChain, tx: Transaction): Pair? { + fun getRemotePerCommitmentSecret(params: ChannelParams, channelKeys: ChannelKeys, remotePerCommitmentSecrets: ShaChain, commitTx: Transaction): Pair? { // a valid tx will always have at least one input, but this ensures we don't throw in tests - val sequence = tx.txIn.first().sequence - val obscuredTxNumber = Transactions.decodeTxNumber(sequence, tx.lockTime) - val localPaymentPoint = channelKeys.paymentBasepoint + val sequence = commitTx.txIn.first().sequence + val obscuredTxNumber = Transactions.decodeTxNumber(sequence, commitTx.lockTime) + val localPaymentPoint = channelKeys.paymentBasePoint // this tx has been published by remote, so we need to invert local/remote params val commitmentNumber = Transactions.obscuredCommitTxNumber(obscuredTxNumber, !params.localParams.isChannelOpener, params.remoteParams.paymentBasepoint, localPaymentPoint) if (commitmentNumber > 0xffffffffffffL) { @@ -685,12 +657,10 @@ object Helpers { * When a revoked commitment transaction spending the funding tx is detected, we build a set of transactions that * will punish our peer by stealing all their funds. */ - fun LoggingContext.claimRevokedRemoteCommitTxOutputs(channelKeys: KeyManager.ChannelKeys, params: ChannelParams, remotePerCommitmentSecret: PrivateKey, commitTx: Transaction, feerates: OnChainFeerates): RevokedCommitPublished { - val localPaymentPoint = channelKeys.paymentBasepoint + fun LoggingContext.claimRevokedRemoteCommitTxOutputs(params: ChannelParams, channelKeys: ChannelKeys, commitTx: Transaction, remotePerCommitmentSecret: PrivateKey, feerates: OnChainFeerates): RevokedCommitPublished { val remotePerCommitmentPoint = remotePerCommitmentSecret.publicKey() - val remoteDelayedPaymentPubkey = params.remoteParams.delayedPaymentBasepoint.deriveForCommitment(remotePerCommitmentPoint) - val remoteRevocationPubkey = channelKeys.revocationBasepoint.deriveForRevocation(remotePerCommitmentPoint) - + val commitKeys = channelKeys.remoteCommitmentKeys(params, remotePerCommitmentPoint) + val revocationKey = channelKeys.revocationKey(remotePerCommitmentSecret) val feerateMain = feerates.claimMainFeerate // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty val feeratePenalty = feerates.fastFeerate @@ -700,12 +670,12 @@ object Helpers { Transactions.makeClaimRemoteDelayedOutputTx( commitTx, params.localParams.dustLimit, - localPaymentPoint, + commitKeys.ourPaymentKey.publicKey(), params.localParams.defaultFinalScriptPubKey, feerateMain ) }?.let { - val sig = Transactions.sign(it, channelKeys.paymentKey) + val sig = Transactions.sign(it, commitKeys.ourPaymentKey) Transactions.addSigs(it, sig) } @@ -714,14 +684,14 @@ object Helpers { Transactions.makeMainPenaltyTx( commitTx, params.localParams.dustLimit, - remoteRevocationPubkey, + commitKeys.revocationPublicKey, params.localParams.defaultFinalScriptPubKey.toByteArray(), params.localParams.toSelfDelay, - remoteDelayedPaymentPubkey, + commitKeys.theirDelayedPaymentPublicKey, feeratePenalty ) }?.let { - val sig = Transactions.sign(it, channelKeys.revocationKey.deriveForRevocation(remotePerCommitmentSecret)) + val sig = Transactions.sign(it, revocationKey) Transactions.addSigs(it, sig) } @@ -732,8 +702,8 @@ object Helpers { * Once we've fetched htlc information for a revoked commitment from the DB, we create penalty transactions to claim all htlc outputs. */ fun LoggingContext.claimRevokedRemoteCommitTxHtlcOutputs( - channelKeys: KeyManager.ChannelKeys, params: ChannelParams, + channelKeys: ChannelKeys, revokedCommitPublished: RevokedCommitPublished, feerates: OnChainFeerates, htlcInfos: List @@ -741,18 +711,15 @@ object Helpers { // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty val feeratePenalty = feerates.fastFeerate val remotePerCommitmentPoint = revokedCommitPublished.remotePerCommitmentSecret.publicKey() - val remoteRevocationPubkey = channelKeys.revocationBasepoint.deriveForRevocation(remotePerCommitmentPoint) - val remoteHtlcPubkey = params.remoteParams.htlcBasepoint.deriveForCommitment(remotePerCommitmentPoint) - val localHtlcPubkey = channelKeys.htlcBasepoint.deriveForCommitment(remotePerCommitmentPoint) - + val commitKeys = channelKeys.remoteCommitmentKeys(params, remotePerCommitmentPoint) + val revocationKey = channelKeys.revocationKey(revokedCommitPublished.remotePerCommitmentSecret) // we retrieve the information needed to rebuild htlc scripts logger.info { "found ${htlcInfos.size} htlcs for txid=${revokedCommitPublished.commitTx.txid}" } val htlcsRedeemScripts = htlcInfos.flatMap { htlcInfo -> - val htlcReceived = Scripts.htlcReceived(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash), htlcInfo.cltvExpiry) - val htlcOffered = Scripts.htlcOffered(remoteHtlcPubkey, localHtlcPubkey, remoteRevocationPubkey, ripemd160(htlcInfo.paymentHash)) + val htlcReceived = Scripts.htlcReceived(commitKeys.theirHtlcPublicKey, commitKeys.ourHtlcKey.publicKey(), commitKeys.revocationPublicKey, ripemd160(htlcInfo.paymentHash), htlcInfo.cltvExpiry) + val htlcOffered = Scripts.htlcOffered(commitKeys.theirHtlcPublicKey, commitKeys.ourHtlcKey.publicKey(), commitKeys.revocationPublicKey, ripemd160(htlcInfo.paymentHash)) listOf(htlcReceived, htlcOffered) }.associate { redeemScript -> write(pay2wsh(redeemScript)).toByteVector() to write(redeemScript).toByteVector() } - // and finally we steal the htlc outputs val htlcPenaltyTxs = revokedCommitPublished.commitTx.txOut.mapIndexedNotNull { outputIndex, txOut -> htlcsRedeemScripts[txOut.publicKeyScript]?.let { redeemScript -> @@ -766,12 +733,11 @@ object Helpers { feeratePenalty ) }?.let { htlcPenaltyTx -> - val sig = Transactions.sign(htlcPenaltyTx, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret)) - Transactions.addSigs(htlcPenaltyTx, sig, remoteRevocationPubkey) + val sig = Transactions.sign(htlcPenaltyTx, revocationKey) + Transactions.addSigs(htlcPenaltyTx, sig, commitKeys.revocationPublicKey) } } } - return revokedCommitPublished.copy(htlcPenaltyTxs = htlcPenaltyTxs) } @@ -789,8 +755,8 @@ object Helpers { * lockTime (thanks to the use of sighash_single | sighash_anyonecanpay), so we may need to claim multiple outputs. */ fun LoggingContext.claimRevokedHtlcTxOutputs( - channelKeys: KeyManager.ChannelKeys, params: ChannelParams, + channelKeys: ChannelKeys, revokedCommitPublished: RevokedCommitPublished, htlcTx: Transaction, feerates: OnChainFeerates @@ -803,23 +769,23 @@ object Helpers { val spendsHtlcOutput = htlcTx.txIn.any { htlcOutputs.contains(it.outPoint) } if (spendsHtlcOutput) { val remotePerCommitmentPoint = revokedCommitPublished.remotePerCommitmentSecret.publicKey() - val remoteDelayedPaymentPubkey = params.remoteParams.delayedPaymentBasepoint.deriveForCommitment(remotePerCommitmentPoint) - val remoteRevocationPubkey = channelKeys.revocationBasepoint.deriveForRevocation(remotePerCommitmentPoint) + val commitKeys = channelKeys.remoteCommitmentKeys(params, remotePerCommitmentPoint) + val revocationKey = channelKeys.revocationKey(revokedCommitPublished.remotePerCommitmentSecret) // we need to use a high fee here for punishment txs because after a delay they can be spent by the counterparty val feeratePenalty = feerates.fastFeerate val penaltyTxs = Transactions.makeClaimDelayedOutputPenaltyTxs( htlcTx, params.localParams.dustLimit, - remoteRevocationPubkey, + commitKeys.revocationPublicKey, params.localParams.toSelfDelay, - remoteDelayedPaymentPubkey, + commitKeys.theirDelayedPaymentPublicKey, params.localParams.defaultFinalScriptPubKey.toByteArray(), feeratePenalty ).mapNotNull { claimDelayedOutputPenaltyTx -> generateTx("claim-htlc-delayed-penalty") { claimDelayedOutputPenaltyTx }?.let { - val sig = Transactions.sign(it, channelKeys.revocationKey.deriveForRevocation(revokedCommitPublished.remotePerCommitmentSecret)) + val sig = Transactions.sign(it, revocationKey) val signedTx = Transactions.addSigs(it, sig) // we need to make sure that the tx is indeed valid when (runTrying { signedTx.tx.correctlySpends(listOf(htlcTx), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) }) { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt index 4cc8ffb80..d25bce87c 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt @@ -13,8 +13,9 @@ import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForCommitment +import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.SwapInOnChainKeys import fr.acinq.lightning.logging.MDCLogger import fr.acinq.lightning.transactions.* import fr.acinq.lightning.utils.* @@ -29,7 +30,7 @@ import kotlinx.coroutines.CompletableDeferred sealed class SharedFundingInput { abstract val info: Transactions.InputInfo abstract val weight: Int - abstract fun sign(channelKeys: KeyManager.ChannelKeys, tx: Transaction): ByteVector64 + abstract fun sign(channelKeys: ChannelKeys, tx: Transaction): ByteVector64 data class Multisig2of2(override val info: Transactions.InputInfo, val fundingTxIndex: Long, val remoteFundingPubkey: PublicKey) : SharedFundingInput() { @@ -42,7 +43,7 @@ sealed class SharedFundingInput { // This value was computed assuming 73 bytes signatures (worst-case scenario). override val weight: Int = Multisig2of2.weight - override fun sign(channelKeys: KeyManager.ChannelKeys, tx: Transaction): ByteVector64 { + override fun sign(channelKeys: ChannelKeys, tx: Transaction): ByteVector64 { val fundingKey = channelKeys.fundingKey(fundingTxIndex) return Transactions.sign(Transactions.TransactionWithInputInfo.SpliceTx(info, tx), fundingKey) } @@ -94,9 +95,9 @@ data class InteractiveTxParams( // BOLT 2: the initiator's serial IDs MUST use even values and the non-initiator odd values. val serialIdParity = if (isInitiator) 0 else 1 - fun fundingPubkeyScript(channelKeys: KeyManager.ChannelKeys): ByteVector { + fun fundingPubkeyScript(channelKeys: ChannelKeys): ByteVector { val fundingTxIndex = (sharedInput as? SharedFundingInput.Multisig2of2)?.let { it.fundingTxIndex + 1 } ?: 0 - return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey) + return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingKey(fundingTxIndex).publicKey(), remoteFundingPubkey) } fun liquidityFees(purchase: LiquidityAds.Purchase?): MilliSatoshi = purchase?.let { l -> @@ -266,8 +267,8 @@ data class FundingContributions(val inputs: List, v * @param walletInputs 2-of-2 swap-in wallet inputs. */ fun create( - channelKeys: KeyManager.ChannelKeys, - swapInKeys: KeyManager.SwapInOnChainKeys, + channelKeys: ChannelKeys, + swapInKeys: SwapInOnChainKeys, params: InteractiveTxParams, walletInputs: List, liquidityPurchase: LiquidityAds.Purchase? @@ -282,8 +283,8 @@ data class FundingContributions(val inputs: List, v * @param changePubKey if provided, a corresponding p2wpkh change output will be created. */ fun create( - channelKeys: KeyManager.ChannelKeys, - swapInKeys: KeyManager.SwapInOnChainKeys, + channelKeys: ChannelKeys, + swapInKeys: SwapInOnChainKeys, params: InteractiveTxParams, sharedUtxo: Pair?, walletInputs: List, @@ -460,7 +461,8 @@ data class SharedTransaction( fun sign(session: InteractiveTxSession, keyManager: KeyManager, fundingParams: InteractiveTxParams, localParams: LocalParams, remoteNodeId: PublicKey): PartiallySignedSharedTransaction { val unsignedTx = buildUnsignedTx() - val sharedSig = fundingParams.sharedInput?.sign(keyManager.channelKeys(localParams.fundingKeyPath), unsignedTx) + val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) + val sharedSig = fundingParams.sharedInput?.sign(channelKeys, unsignedTx) // NB: the order in this list must match the order of the transaction's inputs. val previousOutputs = unsignedTx.txIn.map { spentOutputs[it.outPoint]!! } @@ -538,7 +540,7 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, override val txId: TxId = localSigs.txId override val signedTx = null - fun addRemoteSigs(channelKeys: KeyManager.ChannelKeys, fundingParams: InteractiveTxParams, remoteSigs: TxSignatures): FullySignedSharedTransaction? { + fun addRemoteSigs(channelKeys: ChannelKeys, fundingParams: InteractiveTxParams, remoteSigs: TxSignatures): FullySignedSharedTransaction? { if (localSigs.swapInUserSigs.size != tx.localInputs.filterIsInstance().size) return null if (localSigs.swapInUserPartialSigs.size != tx.localInputs.filterIsInstance().size) return null if (remoteSigs.swapInUserSigs.size != tx.remoteInputs.filterIsInstance().size) return null @@ -552,7 +554,7 @@ data class PartiallySignedSharedTransaction(override val tx: SharedTransaction, is SharedFundingInput.Multisig2of2 -> Scripts.witness2of2( localSigs.previousFundingTxSig ?: return null, remoteSigs.previousFundingTxSig ?: return null, - channelKeys.fundingPubKey(it.fundingTxIndex), + channelKeys.fundingKey(it.fundingTxIndex).publicKey(), it.remoteFundingPubkey, ) } @@ -641,8 +643,8 @@ sealed class InteractiveTxSessionAction { data class InteractiveTxSession( val remoteNodeId: PublicKey, - val channelKeys: KeyManager.ChannelKeys, - val swapInKeys: KeyManager.SwapInOnChainKeys, + val channelKeys: ChannelKeys, + val swapInKeys: SwapInOnChainKeys, val fundingParams: InteractiveTxParams, val previousFunding: SharedFundingInputBalances, val toSend: List>, @@ -675,8 +677,8 @@ data class InteractiveTxSession( constructor( remoteNodeId: PublicKey, - channelKeys: KeyManager.ChannelKeys, - swapInKeys: KeyManager.SwapInOnChainKeys, + channelKeys: ChannelKeys, + swapInKeys: SwapInOnChainKeys, fundingParams: InteractiveTxParams, previousLocalBalance: MilliSatoshi, previousRemoteBalance: MilliSatoshi, @@ -1034,12 +1036,22 @@ data class InteractiveTxSigningSession( is Either.Right -> localCommit.value.index + 1 } - fun receiveCommitSig(channelKeys: KeyManager.ChannelKeys, channelParams: ChannelParams, remoteCommitSig: CommitSig, currentBlockHeight: Long, logger: MDCLogger): Pair { + fun receiveCommitSig(channelKeys: ChannelKeys, channelParams: ChannelParams, remoteCommitSig: CommitSig, currentBlockHeight: Long, logger: MDCLogger): Pair { return when (localCommit) { is Either.Left -> { val localCommitIndex = localCommit.value.index - val localPerCommitmentPoint = channelKeys.commitmentPoint(localCommitIndex) - when (val signedLocalCommit = LocalCommit.fromCommitSig(channelKeys, channelParams, fundingTxIndex, fundingParams.remoteFundingPubkey, commitInput, remoteCommitSig, localCommitIndex, localCommit.value.spec, localPerCommitmentPoint, logger)) { + val commitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitIndex) + when (val signedLocalCommit = LocalCommit.fromCommitSig( + channelParams = channelParams, + commitKeys = commitKeys, + fundingKey = channelKeys.fundingKey(fundingTxIndex), + remoteFundingPubKey = fundingParams.remoteFundingPubkey, + commitInput = commitInput, + commit = remoteCommitSig, + localCommitIndex = localCommitIndex, + spec = localCommit.value.spec, + log = logger + )) { is Either.Left -> { val fundingKey = channelKeys.fundingKey(fundingTxIndex) val localSigOfLocalTx = Transactions.sign(localCommit.value.commitTx, fundingKey) @@ -1067,7 +1079,7 @@ data class InteractiveTxSigningSession( } } - fun receiveTxSigs(channelKeys: KeyManager.ChannelKeys, remoteTxSigs: TxSignatures, currentBlockHeight: Long): Either { + fun receiveTxSigs(channelKeys: ChannelKeys, remoteTxSigs: TxSignatures, currentBlockHeight: Long): Either { return when (localCommit) { is Either.Left -> Either.Left(InteractiveTxSigningSessionAction.AbortFundingAttempt(UnexpectedFundingSignatures(fundingParams.channelId))) is Either.Right -> when (val fullySignedTx = fundingTx.addRemoteSigs(channelKeys, fundingParams, remoteTxSigs)) { @@ -1100,42 +1112,43 @@ data class InteractiveTxSigningSession( localHtlcs: Set ): Either> { val channelKeys = channelParams.localParams.channelKeys(keyManager) + val fundingKey = channelKeys.fundingKey(fundingTxIndex) + val localCommitKeys = channelKeys.localCommitmentKeys(channelParams, localCommitmentIndex) + val remoteCommitKeys = channelKeys.remoteCommitmentKeys(channelParams, remotePerCommitmentPoint) val unsignedTx = sharedTx.buildUnsignedTx() val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) } val liquidityFees = fundingParams.liquidityFees(liquidityPurchase) return Helpers.Funding.makeCommitTxs( - channelKeys, - channelParams.channelId, - channelParams.localParams, channelParams.remoteParams, + channelParams = channelParams, fundingAmount = sharedTx.sharedOutput.amount, toLocal = sharedTx.sharedOutput.localAmount - liquidityFees, toRemote = sharedTx.sharedOutput.remoteAmount + liquidityFees, localHtlcs = localHtlcs, localCommitmentIndex = localCommitmentIndex, remoteCommitmentIndex = remoteCommitmentIndex, - commitTxFeerate, - fundingTxIndex = fundingTxIndex, fundingTxId = unsignedTx.txid, fundingTxOutputIndex = sharedOutputIndex, + commitTxFeerate = commitTxFeerate, + fundingTxId = unsignedTx.txid, + fundingTxOutputIndex = sharedOutputIndex, + localFundingKey = fundingKey, remoteFundingPubkey = fundingParams.remoteFundingPubkey, - remotePerCommitmentPoint = remotePerCommitmentPoint + localCommitKeys = localCommitKeys, + remoteCommitKeys = remoteCommitKeys, ).map { firstCommitTx -> - val localSigOfRemoteCommitTx = Transactions.sign(firstCommitTx.remoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) - val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { Transactions.sign(it, channelKeys.htlcKey.deriveForCommitment(remotePerCommitmentPoint), SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } - + val localSigOfRemoteCommitTx = Transactions.sign(firstCommitTx.remoteCommitTx, fundingKey) + val localSigsOfRemoteHtlcTxs = firstCommitTx.remoteHtlcTxs.map { Transactions.sign(it, remoteCommitKeys.ourHtlcKey, SigHash.SIGHASH_SINGLE or SigHash.SIGHASH_ANYONECANPAY) } val alternativeSigs = if (firstCommitTx.remoteHtlcTxs.isEmpty()) { val commitSigTlvs = Commitments.alternativeFeerates.map { feerate -> val alternativeSpec = firstCommitTx.remoteSpec.copy(feerate = feerate) val (alternativeRemoteCommitTx, _) = Commitments.makeRemoteTxs( - channelKeys, - remoteCommitmentIndex, - channelParams.localParams, - channelParams.remoteParams, - fundingTxIndex, - fundingParams.remoteFundingPubkey, - firstCommitTx.remoteCommitTx.input, - remotePerCommitmentPoint, - alternativeSpec + channelParams = channelParams, + commitKeys = remoteCommitKeys, + commitTxNumber = remoteCommitmentIndex, + localFundingKey = fundingKey, + remoteFundingPubKey = fundingParams.remoteFundingPubkey, + commitmentInput = firstCommitTx.remoteCommitTx.input, + spec = alternativeSpec ) - val sig = Transactions.sign(alternativeRemoteCommitTx, channelKeys.fundingKey(fundingTxIndex)) + val sig = Transactions.sign(alternativeRemoteCommitTx, fundingKey) CommitSigTlv.AlternativeFeerateSig(feerate, sig) } TlvStream(CommitSigTlv.AlternativeFeerateSigs(commitSigTlvs) as CommitSigTlv) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt index ce0a8f982..c2eccc712 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Channel.kt @@ -16,6 +16,7 @@ import fr.acinq.lightning.channel.Helpers.Closing.claimRemoteCommitMainOutput import fr.acinq.lightning.channel.Helpers.Closing.claimRemoteCommitTxOutputs import fr.acinq.lightning.channel.Helpers.Closing.claimRevokedRemoteCommitTxOutputs import fr.acinq.lightning.channel.Helpers.Closing.getRemotePerCommitmentSecret +import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.db.ChannelCloseOutgoingPayment.ChannelClosingType import fr.acinq.lightning.logging.LoggingContext @@ -347,7 +348,7 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { val paysCommitTxFees: Boolean get() = commitments.params.localParams.paysCommitTxFees val remoteNodeId: PublicKey get() = commitments.remoteNodeId - fun ChannelContext.channelKeys(): KeyManager.ChannelKeys = commitments.params.localParams.channelKeys(keyManager) + fun ChannelContext.channelKeys(): ChannelKeys = commitments.params.localParams.channelKeys(keyManager) abstract fun updateCommitments(input: Commitments): ChannelStateWithCommitments @@ -525,9 +526,9 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { internal suspend fun ChannelContext.handleRemoteSpentOther(tx: Transaction): Pair> { logger.warning { "funding tx spent in txid=${tx.txid}" } - return getRemotePerCommitmentSecret(channelKeys(), commitments.params, commitments.remotePerCommitmentSecrets, tx)?.let { (remotePerCommitmentSecret, commitmentNumber) -> + return getRemotePerCommitmentSecret(commitments.params, channelKeys(), commitments.remotePerCommitmentSecrets, tx)?.let { (remotePerCommitmentSecret, commitmentNumber) -> logger.warning { "txid=${tx.txid} was a revoked commitment, publishing the penalty tx" } - val revokedCommitPublished = claimRevokedRemoteCommitTxOutputs(channelKeys(), commitments.params, remotePerCommitmentSecret, tx, currentOnChainFeerates()) + val revokedCommitPublished = claimRevokedRemoteCommitTxOutputs(commitments.params, channelKeys(), tx, remotePerCommitmentSecret, currentOnChainFeerates()) val ex = FundingTxSpent(channelId, tx.txid) val error = Error(channelId, ex.message) val nextState = when (this@ChannelStateWithCommitments) { @@ -559,7 +560,8 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { when (this@ChannelStateWithCommitments) { is WaitForRemotePublishFutureCommitment -> { logger.warning { "they published their future commit (because we asked them to) in txid=${tx.txid}" } - val remoteCommitPublished = claimRemoteCommitMainOutput(channelKeys(), commitments.params, tx, currentOnChainFeerates().claimMainFeerate) + val commitKeys = channelKeys().remoteCommitmentKeys(commitments.params, remoteChannelReestablish.myCurrentPerCommitmentPoint) + val remoteCommitPublished = claimRemoteCommitMainOutput(commitKeys, tx, commitments.params.localParams.dustLimit, commitments.params.localParams.defaultFinalScriptPubKey, currentOnChainFeerates().claimMainFeerate) val nextState = Closing( commitments = commitments, waitingSinceBlock = currentBlockHeight.toLong(), @@ -589,7 +591,8 @@ sealed class ChannelStateWithCommitments : PersistedChannelState() { } else -> { logger.warning { "they published an alternative commitment with feerate=${remoteCommit.spec.feerate} txid=${tx.txid}" } - val remoteCommitPublished = claimRemoteCommitMainOutput(channelKeys(), commitments.params, tx, currentOnChainFeerates().claimMainFeerate) + val commitKeys = channelKeys().remoteCommitmentKeys(commitments.params, remoteCommit.remotePerCommitmentPoint) + val remoteCommitPublished = claimRemoteCommitMainOutput(commitKeys, tx, commitments.params.localParams.dustLimit, commitments.params.localParams.defaultFinalScriptPubKey, currentOnChainFeerates().claimMainFeerate) val nextState = when (this@ChannelStateWithCommitments) { is Closing -> this@ChannelStateWithCommitments.copy(remoteCommitPublished = remoteCommitPublished) is Negotiating -> Closing(commitments, waitingSinceBlock, proposedClosingTxs.flatMap { it.all }, publishedClosingTxs, remoteCommitPublished = remoteCommitPublished) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt index 5ecae2449..b66db0103 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Closing.kt @@ -282,7 +282,7 @@ data class Closing( } val revokedCommitPublishActions = mutableListOf() val revokedCommitPublished1 = revokedCommitPublished.map { rev -> - val (newRevokedCommitPublished, penaltyTxs) = claimRevokedHtlcTxOutputs(channelKeys(), commitments.params, rev, watch.spendingTx, currentOnChainFeerates()) + val (newRevokedCommitPublished, penaltyTxs) = claimRevokedHtlcTxOutputs(commitments.params, channelKeys(), rev, watch.spendingTx, currentOnChainFeerates()) penaltyTxs.forEach { revokedCommitPublishActions += ChannelAction.Blockchain.PublishTx(it) revokedCommitPublishActions += ChannelAction.Blockchain.SendWatch(WatchSpent(channelId, watch.spendingTx, it.input.outPoint.index.toInt(), WatchSpent.ClosingOutputSpent(it.amountIn))) @@ -306,7 +306,7 @@ data class Closing( is ChannelCommand.Closing.GetHtlcInfosResponse -> { val index = revokedCommitPublished.indexOfFirst { it.commitTx.txid == cmd.revokedCommitTxId } if (index >= 0) { - val revokedCommitPublished1 = claimRevokedRemoteCommitTxHtlcOutputs(channelKeys(), commitments.params, revokedCommitPublished[index], currentOnChainFeerates(), cmd.htlcInfos) + val revokedCommitPublished1 = claimRevokedRemoteCommitTxHtlcOutputs(commitments.params, channelKeys(), revokedCommitPublished[index], currentOnChainFeerates(), cmd.htlcInfos) val nextState = copy(revokedCommitPublished = revokedCommitPublished.updated(index, revokedCommitPublished1)) val actions = buildList { add(ChannelAction.Storage.StoreState(nextState)) diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt index 2ef5a76b7..0c536a5e4 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt @@ -391,7 +391,7 @@ data class Normal( fundingContribution = fundingContribution, lockTime = currentBlockHeight.toLong(), feerate = spliceStatus.command.feerate, - fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), + fundingPubkey = channelKeys().fundingKey(parentCommitment.fundingTxIndex + 1).publicKey(), requestFunding = spliceStatus.command.requestRemoteFunding, ) logger.info { "initiating splice with local.amount=${spliceInit.fundingContribution}" } @@ -430,7 +430,7 @@ data class Normal( val spliceAck = SpliceAck( channelId, fundingContribution = 0.sat, // only remote contributes to the splice - fundingPubkey = channelKeys().fundingPubKey(parentCommitment.fundingTxIndex + 1), + fundingPubkey = channelKeys().fundingKey(parentCommitment.fundingTxIndex + 1).publicKey(), willFund = null, ) val fundingParams = InteractiveTxParams( diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt index 8cf097078..2a923a862 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/Syncing.kt @@ -7,7 +7,7 @@ import fr.acinq.lightning.blockchain.WatchConfirmed import fr.acinq.lightning.blockchain.WatchConfirmedTriggered import fr.acinq.lightning.blockchain.WatchSpentTriggered import fr.acinq.lightning.channel.* -import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.* @@ -15,7 +15,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: val channelId = state.channelId - fun ChannelContext.channelKeys(): KeyManager.ChannelKeys = when (state) { + fun ChannelContext.channelKeys(): ChannelKeys = when (state) { is WaitForFundingSigned -> state.channelParams.localParams.channelKeys(keyManager) is ChannelStateWithCommitments -> state.commitments.params.localParams.channelKeys(keyManager) } @@ -34,7 +34,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // They haven't received our commit_sig: we retransmit it, and will send our tx_signatures once we've received // their commit_sig or their tx_signatures (depending on who must send tx_signatures first). logger.info { "re-sending commit_sig for channel creation with fundingTxId=${state.signingSession.fundingTx.txId}" } - val commitSig = state.signingSession.remoteCommit.sign(channelKeys(), state.channelParams, state.signingSession) + val commitSig = state.signingSession.remoteCommit.sign(state.channelParams, channelKeys(), state.signingSession) add(ChannelAction.Message.Send(commitSig)) } } @@ -50,7 +50,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // They haven't received our commit_sig: we retransmit it. // We're waiting for signatures from them, and will send our tx_signatures once we receive them. logger.info { "re-sending commit_sig for rbf attempt with fundingTxId=${cmd.message.nextFundingTxId}" } - val commitSig = state.rbfStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.rbfStatus.session) + val commitSig = state.rbfStatus.session.remoteCommit.sign(state.commitments.params, channelKeys(), state.rbfStatus.session) add(ChannelAction.Message.Send(commitSig)) } } @@ -62,8 +62,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (cmd.message.nextLocalCommitmentNumber == 0L) { logger.info { "re-sending commit_sig for fundingTxId=${cmd.message.nextFundingTxId}" } val commitSig = state.commitments.latest.remoteCommit.sign( - channelKeys(), state.commitments.params, + channelKeys(), fundingTxIndex = 0, state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput, @@ -93,8 +93,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (cmd.message.nextLocalCommitmentNumber == 0L) { logger.info { "re-sending commit_sig for fundingTxId=${state.commitments.latest.fundingTxId}" } val commitSig = state.commitments.latest.remoteCommit.sign( - channelKeys(), state.commitments.params, + channelKeys(), fundingTxIndex = state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput, @@ -146,7 +146,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // They haven't received our commit_sig: we retransmit it. // We're waiting for signatures from them, and will send our tx_signatures once we receive them. logger.info { "re-sending commit_sig for splice attempt with fundingTxIndex=${state.spliceStatus.session.fundingTxIndex} fundingTxId=${state.spliceStatus.session.fundingTx.txId}" } - val commitSig = state.spliceStatus.session.remoteCommit.sign(channelKeys(), state.commitments.params, state.spliceStatus.session) + val commitSig = state.spliceStatus.session.remoteCommit.sign(state.commitments.params, channelKeys(), state.spliceStatus.session) actions.add(ChannelAction.Message.Send(commitSig)) } state.spliceStatus @@ -158,8 +158,8 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: if (cmd.message.nextLocalCommitmentNumber == state.commitments.remoteCommitIndex) { logger.info { "re-sending commit_sig for fundingTxIndex=${state.commitments.latest.fundingTxIndex} fundingTxId=${state.commitments.latest.fundingTxId}" } val commitSig = state.commitments.latest.remoteCommit.sign( - channelKeys(), state.commitments.params, + channelKeys(), fundingTxIndex = state.commitments.latest.fundingTxIndex, state.commitments.latest.remoteFundingPubkey, state.commitments.latest.commitInput, @@ -418,7 +418,7 @@ data class Syncing(val state: PersistedChannelState, val channelReestablishSent: // Note that we ignore errors and simply skip failures to sign: we've already signed those updates before // the disconnection, so we don't expect any error here unless our peer sends an invalid nonce. In that // case, we simply won't send back our commit_sig until they fix their node. - c.nextRemoteCommit?.commit?.sign(channelKeys, channelParams, c.fundingTxIndex, c.remoteFundingPubkey, commitInput, batchSize) + c.nextRemoteCommit?.commit?.sign(channelParams, channelKeys, c.fundingTxIndex, c.remoteFundingPubkey, commitInput, batchSize) }) val retransmit = when (retransmitRevocation) { null -> buildList { diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt index 7d1ae667e..5f57b4a7d 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForAcceptChannel.kt @@ -127,7 +127,7 @@ data class WaitForAcceptChannel( is Either.Left -> { logger.error(res.value) { "invalid ${cmd.message::class} in state ${this::class}" } init.replyTo.complete(ChannelFundingResponse.Failure.InvalidChannelParameters(res.value)) - return Pair(Aborted, listOf(ChannelAction.Message.Send(Error(init.temporaryChannelId(keyManager), res.value.message)))) + return Pair(Aborted, listOf(ChannelAction.Message.Send(Error(temporaryChannelId, res.value.message)))) } } } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt index 916f74db0..b7a22dc46 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForInit.kt @@ -30,7 +30,7 @@ data object WaitForInit : ChannelState() { val channelKeys = keyManager.channelKeys(cmd.localParams.fundingKeyPath) val open = OpenDualFundedChannel( chainHash = staticParams.nodeParams.chainHash, - temporaryChannelId = cmd.temporaryChannelId(keyManager), + temporaryChannelId = cmd.temporaryChannelId(channelKeys), fundingFeerate = cmd.fundingTxFeerate, commitmentFeerate = cmd.commitTxFeerate, fundingAmount = cmd.fundingAmount, @@ -40,11 +40,11 @@ data object WaitForInit : ChannelState() { toSelfDelay = cmd.localParams.toSelfDelay, maxAcceptedHtlcs = cmd.localParams.maxAcceptedHtlcs, lockTime = currentBlockHeight.toLong(), - fundingPubkey = channelKeys.fundingPubKey(0), - revocationBasepoint = channelKeys.revocationBasepoint, - paymentBasepoint = channelKeys.paymentBasepoint, - delayedPaymentBasepoint = channelKeys.delayedPaymentBasepoint, - htlcBasepoint = channelKeys.htlcBasepoint, + fundingPubkey = channelKeys.fundingKey(0).publicKey(), + revocationBasepoint = channelKeys.revocationBasePoint, + paymentBasepoint = channelKeys.paymentBasePoint, + delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint, + htlcBasepoint = channelKeys.htlcBasePoint, firstPerCommitmentPoint = channelKeys.commitmentPoint(0), secondPerCommitmentPoint = channelKeys.commitmentPoint(1), channelFlags = cmd.channelFlags, diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt index 2b4bbeac5..d6a2eab13 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/channel/states/WaitForOpenChannel.kt @@ -41,7 +41,7 @@ data class WaitForOpenChannel( val channelFeatures = ChannelFeatures(channelType, localFeatures = localParams.features, remoteFeatures = remoteInit.features) val minimumDepth = if (staticParams.useZeroConf) 0 else staticParams.nodeParams.minDepthBlocks val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) - val localFundingPubkey = channelKeys.fundingPubKey(0) + val localFundingPubkey = channelKeys.fundingKey(0).publicKey() val fundingScript = Helpers.Funding.makeFundingPubKeyScript(localFundingPubkey, open.fundingPubkey) val requestFunding = open.requestFunding val willFund = when { @@ -60,10 +60,10 @@ data class WaitForOpenChannel( toSelfDelay = localParams.toSelfDelay, maxAcceptedHtlcs = localParams.maxAcceptedHtlcs, fundingPubkey = localFundingPubkey, - revocationBasepoint = channelKeys.revocationBasepoint, - paymentBasepoint = channelKeys.paymentBasepoint, - delayedPaymentBasepoint = channelKeys.delayedPaymentBasepoint, - htlcBasepoint = channelKeys.htlcBasepoint, + revocationBasepoint = channelKeys.revocationBasePoint, + paymentBasepoint = channelKeys.paymentBasePoint, + delayedPaymentBasepoint = channelKeys.delayedPaymentBasePoint, + htlcBasepoint = channelKeys.htlcBasePoint, firstPerCommitmentPoint = channelKeys.commitmentPoint(0), secondPerCommitmentPoint = channelKeys.commitmentPoint(1), tlvStream = TlvStream( diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/Bolt3Derivation.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/Bolt3Derivation.kt deleted file mode 100644 index a1fad3ed1..000000000 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/Bolt3Derivation.kt +++ /dev/null @@ -1,48 +0,0 @@ -package fr.acinq.lightning.crypto - -import fr.acinq.bitcoin.ByteVector32 -import fr.acinq.bitcoin.Crypto.sha256 -import fr.acinq.bitcoin.PrivateKey -import fr.acinq.bitcoin.PublicKey - -/** - * BOLT 3 Key derivation scheme. - */ -object Bolt3Derivation { - - fun perCommitSecret(seed: ByteVector32, index: Long): PrivateKey = PrivateKey(ShaChain.shaChainFromSeed(seed, 0xFFFFFFFFFFFFL - index)) - - fun perCommitPoint(seed: ByteVector32, index: Long): PublicKey = perCommitSecret(seed, index).publicKey() - - private fun derivePrivKey(secret: PrivateKey, perCommitPoint: PublicKey): PrivateKey { - // secretkey = basepoint-secret + SHA256(per-commitment-point || basepoint) - return secret + (PrivateKey(sha256(perCommitPoint.value + secret.publicKey().value))) - } - - fun PrivateKey.deriveForCommitment(perCommitPoint: PublicKey): PrivateKey = derivePrivKey(this, perCommitPoint) - - private fun derivePubKey(basePoint: PublicKey, perCommitPoint: PublicKey): PublicKey { - //pubkey = basepoint + SHA256(per-commitment-point || basepoint)*G - val a = PrivateKey(sha256(perCommitPoint.value + basePoint.value)) - return basePoint + a.publicKey() - } - - fun PublicKey.deriveForCommitment(perCommitPoint: PublicKey): PublicKey = derivePubKey(this, perCommitPoint) - - private fun revocationPubKey(basePoint: PublicKey, perCommitPoint: PublicKey): PublicKey { - val a = PrivateKey(sha256(basePoint.value + perCommitPoint.value)) - val b = PrivateKey(sha256(perCommitPoint.value + basePoint.value)) - return (basePoint * a) + (perCommitPoint * b) - } - - fun PublicKey.deriveForRevocation(perCommitPoint: PublicKey): PublicKey = revocationPubKey(this, perCommitPoint) - - private fun revocationPrivKey(secret: PrivateKey, perCommitSecret: PrivateKey): PrivateKey { - val a = PrivateKey(sha256(secret.publicKey().value + perCommitSecret.publicKey().value)) - val b = PrivateKey(sha256(perCommitSecret.publicKey().value + secret.publicKey().value)) - return (secret * a) + (perCommitSecret * b) - } - - fun PrivateKey.deriveForRevocation(perCommitSecret: PrivateKey): PrivateKey = revocationPrivKey(this, perCommitSecret) - -} diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt index 0a1767f8d..9ad4c8a4b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/KeyManager.kt @@ -8,6 +8,7 @@ import fr.acinq.bitcoin.io.ByteArrayInput import fr.acinq.bitcoin.utils.Either import fr.acinq.lightning.DefaultSwapInParams import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.channel.ChannelParams import fr.acinq.lightning.transactions.SwapInProtocol import fr.acinq.lightning.transactions.SwapInProtocolLegacy import fr.acinq.lightning.transactions.Transactions @@ -16,248 +17,392 @@ import fr.acinq.lightning.utils.sum import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.LightningCodecs -interface KeyManager { - - val nodeKeys: NodeKeys - - /** - * Picks a random funding key path for a new channel. - * @param isInitiator true if we are the channel initiator - */ - fun newFundingKeyPath(isInitiator: Boolean): KeyPath - - /** - * Generate channel-specific keys and secrets - * @params fundingKeyPath funding public key BIP32 path - * @return channel keys and secrets - */ - fun channelKeys(fundingKeyPath: KeyPath): ChannelKeys - - val finalOnChainWallet: Bip84OnChainKeys - - val swapInOnChainWallet: SwapInOnChainKeys +/** + * Keys used for the node. They are used to generate the node id, to secure communication with other peers, and + * to sign network-wide public announcements. + */ +data class NodeKeys( + /** The node key that the same seed would have produced on the legacy eclair-based Phoenix implementation on Android. Useful for recovery for users who didn't upgrade in time. */ + val legacyNodeKey: DeterministicWallet.ExtendedPrivateKey, + val nodeKey: DeterministicWallet.ExtendedPrivateKey +) + +/** + * Each commitment transaction uses a set of many cryptographic keys for the various spending paths of all its outputs. + * Some of those keys are static for the channel lifetime, but others change every time we update the commitment + * transaction. + * + * This class can be used indifferently for the local or remote commitment transaction. Beware that when it applies to + * the remote transaction, the "local" prefix is for their keys and the "remote" prefix is for our keys. + * + * See Bolt 3 for more details. + * + * @param localDelayedPaymentPublicKey key used for delayed outputs of the transaction owner (main balance and outputs of HTLC transactions). + * @param remotePaymentPublicKey key used for the main balance of the transaction non-owner (not delayed). + * @param localHtlcPublicKey key used to sign HTLC transactions by the transaction owner. + * @param remoteHtlcPublicKey key used to sign HTLC transactions by the transaction non-owner. + * @param revocationPublicKey key used to revoke this commitment after signing the next one (by revealing the per-commitment secret). + */ +data class CommitmentPublicKeys( + val localDelayedPaymentPublicKey: PublicKey, + val remotePaymentPublicKey: PublicKey, + val localHtlcPublicKey: PublicKey, + val remoteHtlcPublicKey: PublicKey, + val revocationPublicKey: PublicKey +) + +/** + * Keys used for our local commitment. + * WARNING: these private keys must never be stored on disk, in a database, or logged. + */ +data class LocalCommitmentKeys( + val ourDelayedPaymentKey: PrivateKey, + val theirPaymentPublicKey: PublicKey, + val ourPaymentBasePoint: PublicKey, + val ourHtlcKey: PrivateKey, + val theirHtlcPublicKey: PublicKey, + val revocationPublicKey: PublicKey +) { + val publicKeys: CommitmentPublicKeys = CommitmentPublicKeys( + localDelayedPaymentPublicKey = ourDelayedPaymentKey.publicKey(), + remotePaymentPublicKey = theirPaymentPublicKey, + localHtlcPublicKey = ourHtlcKey.publicKey(), + remoteHtlcPublicKey = theirHtlcPublicKey, + revocationPublicKey = revocationPublicKey + ) +} - /** - * Keys used for the node. They are used to generate the node id, to secure communication with other peers, and - * to sign network-wide public announcements. - */ - data class NodeKeys( - /** The node key that the same seed would have produced on the legacy eclair-based Phoenix implementation on Android. Useful to automate the migration. */ - val legacyNodeKey: DeterministicWallet.ExtendedPrivateKey, - val nodeKey: DeterministicWallet.ExtendedPrivateKey, +/** + * Keys used for the remote commitment. + * WARNING: these private keys must never be stored on disk, in a database, or logged. + */ +data class RemoteCommitmentKeys( + val ourPaymentKey: PrivateKey, + val theirDelayedPaymentPublicKey: PublicKey, + val ourPaymentBasePoint: PublicKey, + val ourHtlcKey: PrivateKey, + val theirHtlcPublicKey: PublicKey, + val revocationPublicKey: PublicKey +) { + // Since this is the remote commitment, local is them and remote is us. + val publicKeys: CommitmentPublicKeys = CommitmentPublicKeys( + localDelayedPaymentPublicKey = theirDelayedPaymentPublicKey, + remotePaymentPublicKey = ourPaymentKey.publicKey(), + localHtlcPublicKey = theirHtlcPublicKey, + remoteHtlcPublicKey = ourHtlcKey.publicKey(), + revocationPublicKey = revocationPublicKey ) +} - /** - * Secrets and keys for a given channel. - * How these keys are generated depends on the [KeyManager] implementation. - */ - data class ChannelKeys( - val fundingKeyPath: KeyPath, - val fundingKey: (Long) -> PrivateKey, - val paymentKey: PrivateKey, - val delayedPaymentKey: PrivateKey, - val htlcKey: PrivateKey, - val revocationKey: PrivateKey, - val shaSeed: ByteVector32, - ) { - fun fundingPubKey(index: Long): PublicKey = fundingKey(index).publicKey() - val htlcBasepoint: PublicKey = htlcKey.publicKey() - val paymentBasepoint: PublicKey = paymentKey.publicKey() - val delayedPaymentBasepoint: PublicKey = delayedPaymentKey.publicKey() - val revocationBasepoint: PublicKey = revocationKey.publicKey() - val temporaryChannelId: ByteVector32 = (ByteVector(ByteArray(33) { 0 }) + revocationBasepoint.value).sha256() - fun commitmentPoint(index: Long): PublicKey = Bolt3Derivation.perCommitPoint(shaSeed, index) - fun commitmentSecret(index: Long): PrivateKey = Bolt3Derivation.perCommitSecret(shaSeed, index) +/** + * Keys used for a specific channel instance: + * - funding keys (channel funding, splicing and closing) + * - commitment "base" keys, which are static for the channel lifetime + * - per-commitment keys, which change everytime we create a new commitment transaction: + * - derived from the commitment "base" keys + * - and tweaked with a per-commitment point + * + * WARNING: these private keys must never be stored on disk, in a database, or logged. + */ +data class ChannelKeys( + private val fundingMasterKey: DeterministicWallet.ExtendedPrivateKey, + private val commitmentMasterKey: DeterministicWallet.ExtendedPrivateKey +) { + fun fundingKey(fundingTxIndex: Long): PrivateKey = fundingMasterKey.derivePrivateKey(hardened(fundingTxIndex)).privateKey + + val revocationBaseSecret: PrivateKey = commitmentMasterKey.derivePrivateKey(hardened(1)).privateKey + val revocationBasePoint: PublicKey = revocationBaseSecret.publicKey() + val paymentBaseSecret: PrivateKey = commitmentMasterKey.derivePrivateKey(hardened(2)).privateKey + val paymentBasePoint: PublicKey = paymentBaseSecret.publicKey() + val delayedPaymentBaseSecret: PrivateKey = commitmentMasterKey.derivePrivateKey(hardened(3)).privateKey + val delayedPaymentBasePoint: PublicKey = delayedPaymentBaseSecret.publicKey() + val htlcBaseSecret: PrivateKey = commitmentMasterKey.derivePrivateKey(hardened(4)).privateKey + val htlcBasePoint: PublicKey = htlcBaseSecret.publicKey() + + // @formatter:off + // Per-commitment keys are derived using a sha-chain, which provides efficient storage and retrieval mechanisms. + val shaSeed: ByteVector32 = Crypto.sha256(commitmentMasterKey.derivePrivateKey(hardened(5)).privateKey.value.toByteArray() + ByteArray(1) { 1 }).byteVector32() + fun commitmentSecret(localCommitmentNumber: Long): PrivateKey = PrivateKey(ShaChain.shaChainFromSeed(shaSeed, 0xFFFFFFFFFFFFL - localCommitmentNumber)) + fun commitmentPoint(localCommitmentNumber: Long): PublicKey = commitmentSecret(localCommitmentNumber).publicKey() + // @formatter:on + + /** Derive our local delayed payment key for our main output in the local commitment transaction. */ + private fun delayedPaymentKey(commitmentPoint: PublicKey): PrivateKey = derivePerCommitmentKey(delayedPaymentBaseSecret, commitmentPoint) + + /** Derive our HTLC key for our HTLC transactions, in either the local or remote commitment transaction. */ + private fun htlcKey(commitmentPoint: PublicKey): PrivateKey = derivePerCommitmentKey(htlcBaseSecret, commitmentPoint) + + /** With the remote per-commitment secret, we can derive the private key to spend revoked commitments. */ + fun revocationKey(remoteCommitmentSecret: PrivateKey): PrivateKey = revocationKey(revocationBaseSecret, remoteCommitmentSecret) + + fun localCommitmentKeys(channelParams: ChannelParams, localCommitIndex: Long): LocalCommitmentKeys { + val localPerCommitmentPoint = commitmentPoint(localCommitIndex) + return LocalCommitmentKeys( + ourDelayedPaymentKey = delayedPaymentKey(localPerCommitmentPoint), + theirPaymentPublicKey = channelParams.remoteParams.paymentBasepoint, + ourPaymentBasePoint = paymentBasePoint, + ourHtlcKey = htlcKey(localPerCommitmentPoint), + theirHtlcPublicKey = remotePerCommitmentPublicKey(channelParams.remoteParams.htlcBasepoint, localPerCommitmentPoint), + revocationPublicKey = revocationPublicKey(channelParams.remoteParams.revocationBasepoint, localPerCommitmentPoint) + ) } - data class Bip84OnChainKeys( - private val chain: Chain, - private val master: DeterministicWallet.ExtendedPrivateKey, - val account: Long - ) { - private val xpriv = master.derivePrivateKey(bip84BasePath(chain) / hardened(account)) - - val xpub: String = xpriv.extendedPublicKey.encode( - prefix = when (chain) { - Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> DeterministicWallet.vpub - Chain.Mainnet -> DeterministicWallet.zpub - } + fun remoteCommitmentKeys(channelParams: ChannelParams, remotePerCommitmentPoint: PublicKey): RemoteCommitmentKeys { + return RemoteCommitmentKeys( + ourPaymentKey = paymentBaseSecret, + theirDelayedPaymentPublicKey = remotePerCommitmentPublicKey(channelParams.remoteParams.delayedPaymentBasepoint, remotePerCommitmentPoint), + ourPaymentBasePoint = paymentBasePoint, + ourHtlcKey = htlcKey(remotePerCommitmentPoint), + theirHtlcPublicKey = remotePerCommitmentPublicKey(channelParams.remoteParams.htlcBasepoint, remotePerCommitmentPoint), + revocationPublicKey = revocationPublicKey(revocationBasePoint, remotePerCommitmentPoint) ) + } - fun privateKey(addressIndex: Long): PrivateKey { - return xpriv.derivePrivateKey(KeyPath.empty / 0 / addressIndex).privateKey + companion object { + /** Derive the local per-commitment key for the base key provided. */ + fun derivePerCommitmentKey(baseSecret: PrivateKey, commitmentPoint: PublicKey): PrivateKey { + // secretkey = basepoint-secret + SHA256(per-commitment-point || basepoint) + return baseSecret + PrivateKey(Crypto.sha256(commitmentPoint.value + baseSecret.publicKey().value)) } - fun pubkeyScript(addressIndex: Long): ByteVector { - val priv = privateKey(addressIndex) - val pub = priv.publicKey() - val script = Script.pay2wpkh(pub) - return Script.write(script).toByteVector() + /** Derive the remote per-commitment key for the base point provided. */ + fun remotePerCommitmentPublicKey(basePoint: PublicKey, commitmentPoint: PublicKey): PublicKey { + // pubkey = basepoint + SHA256(per-commitment-point || basepoint)*G + return basePoint + PrivateKey(Crypto.sha256(commitmentPoint.value + basePoint.value)).publicKey() } - fun address(addressIndex: Long): String { - return Bitcoin.computeP2WpkhAddress(privateKey(addressIndex).publicKey(), chain.chainHash) + /** Derive the revocation private key from our local base revocation key and the remote per-commitment secret. */ + fun revocationKey(baseKey: PrivateKey, remoteCommitmentSecret: PrivateKey): PrivateKey { + val a = PrivateKey(Crypto.sha256(baseKey.publicKey().value + remoteCommitmentSecret.publicKey().value)) + val b = PrivateKey(Crypto.sha256(remoteCommitmentSecret.publicKey().value + baseKey.publicKey().value)) + return (baseKey * a) + (remoteCommitmentSecret * b) } - companion object { - fun bip84BasePath(chain: Chain) = when (chain) { - Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(84) / hardened(1) - Chain.Mainnet -> KeyPath.empty / hardened(84) / hardened(0) - } + /** + * We create two distinct revocation public keys: + * - one for the local commitment using the remote revocation base point and our local per-commitment point + * - one for the remote commitment using our revocation base point and the remote per-commitment point + * + * The owner of the commitment transaction is providing the per-commitment point, which ensures that they can revoke + * their previous commitment transactions by revealing the corresponding secret. + */ + fun revocationPublicKey(revocationBasePoint: PublicKey, commitmentPoint: PublicKey): PublicKey { + val a = PrivateKey(Crypto.sha256(revocationBasePoint.value + commitmentPoint.value)) + val b = PrivateKey(Crypto.sha256(commitmentPoint.value + revocationBasePoint.value)) + return (revocationBasePoint * a) + (commitmentPoint * b) } } +} - /** - * We use a specific kind of swap-in where users send funds to a 2-of-2 multisig with a timelock refund. - * Once confirmed, the swap-in utxos can be spent by one of two paths: - * - with a signature from both [userPublicKey] and [remoteServerPublicKey] - * - with a signature from [userPublicKey] after the [refundDelay] - * The keys used are static across swaps to make recovery easier. - */ - data class SwapInOnChainKeys( - private val chain: Chain, - private val master: DeterministicWallet.ExtendedPrivateKey, - val remoteServerPublicKey: PublicKey, - val refundDelay: Int = DefaultSwapInParams.RefundDelay - ) { - 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 = 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, master.extendedPublicKey, userExtendedPrivateKey.extendedPublicKey, remoteServerPublicKey, refundDelay) - - fun signSwapInputUserLegacy(fundingTx: Transaction, index: Int, parentTxOuts: List): ByteVector64 { - return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()], userPrivateKey) +data class Bip84OnChainKeys( + private val chain: Chain, + private val master: DeterministicWallet.ExtendedPrivateKey, + val account: Long +) { + private val xpriv = master.derivePrivateKey(bip84BasePath(chain) / hardened(account)) + val xpub: String = xpriv.extendedPublicKey.encode( + prefix = when (chain) { + Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> DeterministicWallet.vpub + Chain.Mainnet -> DeterministicWallet.zpub } + ) - // this is a private descriptor that can be used as-is to recover swap-in funds once the refund delay has passed - // it is compatible with address rotation as long as refund keys are derived directly from userRefundExtendedPrivateKey - // README: it includes the user's master refund private key and is not safe to share !! - val privateDescriptor = SwapInProtocol.privateDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, userRefundExtendedPrivateKey) + fun privateKey(addressIndex: Long): PrivateKey { + return xpriv.derivePrivateKey(KeyPath.empty / 0 / addressIndex).privateKey + } - // this is the public version of the above descriptor. It can be used to monitor a user's swap-in transaction - // README: it cannot be used to derive private keys, but it can be used to derive swap-in addresses - val publicDescriptor = SwapInProtocol.publicDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, DeterministicWallet.publicKey(userRefundExtendedPrivateKey)) + fun pubkeyScript(addressIndex: Long): ByteVector { + val priv = privateKey(addressIndex) + val pub = priv.publicKey() + val script = Script.pay2wpkh(pub) + return Script.write(script).toByteVector() + } - /** - * @param addressIndex address index - * @return the swap-in protocol that matches the input public key script - */ - fun getSwapInProtocol(addressIndex: Int): SwapInProtocol { - val userRefundPrivateKey: PrivateKey = userRefundExtendedPrivateKey.derivePrivateKey(addressIndex.toLong()).privateKey - val userRefundPublicKey: PublicKey = userRefundPrivateKey.publicKey() - return SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) - } + fun address(addressIndex: Long): String { + return Bitcoin.computeP2WpkhAddress(privateKey(addressIndex).publicKey(), chain.chainHash) + } - fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce, addressIndex: Int): Either { - val swapInProtocol = getSwapInProtocol(addressIndex) - return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, privateNonce, userNonce, serverNonce) + companion object { + fun bip84BasePath(chain: Chain) = when (chain) { + Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(84) / hardened(1) + Chain.Mainnet -> KeyPath.empty / hardened(84) / hardened(0) } + } +} - data class SwapInUtxo(val txOut: TxOut, val outPoint: OutPoint, val addressIndex: Int?) +/** + * We use a specific kind of swap-in where users send funds to a 2-of-2 multisig with a timelock refund. + * Once confirmed, the swap-in utxos can be spent by one of two paths: + * - with a signature from both [userPublicKey] and [remoteServerPublicKey] + * - with a signature from [userPublicKey] after the [refundDelay] + * The keys used are static across swaps to make recovery easier. + */ +data class SwapInOnChainKeys( + private val chain: Chain, + private val master: DeterministicWallet.ExtendedPrivateKey, + val remoteServerPublicKey: PublicKey, + val refundDelay: Int = DefaultSwapInParams.RefundDelay +) { + 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 = 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, master.extendedPublicKey, userExtendedPrivateKey.extendedPublicKey, remoteServerPublicKey, refundDelay) + + fun signSwapInputUserLegacy(fundingTx: Transaction, index: Int, parentTxOuts: List): ByteVector64 { + return legacySwapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts[fundingTx.txIn[index].outPoint.index.toInt()], userPrivateKey) + } - /** - * Create a recovery transaction that spends swap-in outputs after their refund delay has passed. - * @param utxos a list of swap-in utxos - * @param scriptPubKey pubkey script to send funds to - * @param feerate fee rate for the refund transaction - * @return a signed transaction that spends our swap-in transaction. It cannot be published until `swapInTx` has enough confirmations - */ - fun createRecoveryTransaction(utxos: List, scriptPubKey: ByteVector, feerate: FeeratePerKw): Transaction? { - return if (utxos.isEmpty()) { - null - } else { - val unsignedTx = Transaction( - version = 2, - txIn = utxos.map { TxIn(it.outPoint, sequence = refundDelay.toLong()) }, - txOut = listOf(TxOut(0.sat, scriptPubKey)), - lockTime = 0 - ) - - fun sign(tx: Transaction, inputIndex: Int, utxo: SwapInUtxo): Transaction { - return when (val addressIndex = utxo.addressIndex) { - null -> { - val sig = legacySwapInProtocol.signSwapInputUser(tx, inputIndex, utxo.txOut, userPrivateKey) - tx.updateWitness(inputIndex, legacySwapInProtocol.witnessRefund(sig)) - } - else -> { - 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)) - } + // this is a private descriptor that can be used as-is to recover swap-in funds once the refund delay has passed + // it is compatible with address rotation as long as refund keys are derived directly from userRefundExtendedPrivateKey + // README: it includes the user's master refund private key and is not safe to share !! + val privateDescriptor = SwapInProtocol.privateDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, userRefundExtendedPrivateKey) + + // this is the public version of the above descriptor. It can be used to monitor a user's swap-in transaction + // README: it cannot be used to derive private keys, but it can be used to derive swap-in addresses + val publicDescriptor = SwapInProtocol.publicDescriptor(chain, userPublicKey, remoteServerPublicKey, refundDelay, DeterministicWallet.publicKey(userRefundExtendedPrivateKey)) + + /** + * @param addressIndex address index + * @return the swap-in protocol that matches the input public key script + */ + fun getSwapInProtocol(addressIndex: Int): SwapInProtocol { + val userRefundPrivateKey: PrivateKey = userRefundExtendedPrivateKey.derivePrivateKey(addressIndex.toLong()).privateKey + val userRefundPublicKey: PublicKey = userRefundPrivateKey.publicKey() + return SwapInProtocol(userPublicKey, remoteServerPublicKey, userRefundPublicKey, refundDelay) + } + + fun signSwapInputUser(fundingTx: Transaction, index: Int, parentTxOuts: List, privateNonce: SecretNonce, userNonce: IndividualNonce, serverNonce: IndividualNonce, addressIndex: Int): Either { + val swapInProtocol = getSwapInProtocol(addressIndex) + return swapInProtocol.signSwapInputUser(fundingTx, index, parentTxOuts, userPrivateKey, privateNonce, userNonce, serverNonce) + } + + data class SwapInUtxo(val txOut: TxOut, val outPoint: OutPoint, val addressIndex: Int?) + + /** + * Create a recovery transaction that spends swap-in outputs after their refund delay has passed. + * @param utxos a list of swap-in utxos + * @param scriptPubKey pubkey script to send funds to + * @param feerate fee rate for the refund transaction + * @return a signed transaction that spends our swap-in transaction. It cannot be published until `swapInTx` has enough confirmations + */ + fun createRecoveryTransaction(utxos: List, scriptPubKey: ByteVector, feerate: FeeratePerKw): Transaction? { + return if (utxos.isEmpty()) { + null + } else { + val unsignedTx = Transaction( + version = 2, + txIn = utxos.map { TxIn(it.outPoint, sequence = refundDelay.toLong()) }, + txOut = listOf(TxOut(0.sat, scriptPubKey)), + lockTime = 0 + ) + + fun sign(tx: Transaction, inputIndex: Int, utxo: SwapInUtxo): Transaction { + return when (val addressIndex = utxo.addressIndex) { + null -> { + val sig = legacySwapInProtocol.signSwapInputUser(tx, inputIndex, utxo.txOut, userPrivateKey) + tx.updateWitness(inputIndex, legacySwapInProtocol.witnessRefund(sig)) + } + else -> { + 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)) } } + } - val fees = run { - val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> sign(tx, index, utxo) } - Transactions.weight2fee(feerate, recoveryTx.weight()) - } - val inputAmount = utxos.map { it.txOut.amount }.sum() - val outputAmount = inputAmount - fees - val unsignedTx1 = unsignedTx.copy(txOut = listOf(TxOut(outputAmount, scriptPubKey))) - val signedTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> sign(tx, index, utxo) } - signedTx + val fees = run { + val recoveryTx = utxos.foldIndexed(unsignedTx) { index, tx, utxo -> sign(tx, index, utxo) } + Transactions.weight2fee(feerate, recoveryTx.weight()) } + val inputAmount = utxos.map { it.txOut.amount }.sum() + val outputAmount = inputAmount - fees + val unsignedTx1 = unsignedTx.copy(txOut = listOf(TxOut(outputAmount, scriptPubKey))) + val signedTx = utxos.foldIndexed(unsignedTx1) { index, tx, utxo -> sign(tx, index, utxo) } + signedTx } + } - /** - * Create a recovery transaction that spends a swap-in transaction after the refund delay has passed - * @param swapInTx swap-in transaction - * @param address address to send funds to - * @param feerate fee rate for the refund transaction - * @return a signed transaction that spends our swap-in transaction. It cannot be published until `swapInTx` has enough confirmations - */ - fun createRecoveryTransaction(swapInTx: Transaction, address: String, feerate: FeeratePerKw): Transaction? { - val swapInProtocols = (0 until 100).map { getSwapInProtocol(it) } - val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(legacySwapInProtocol.pubkeyScript)) || swapInProtocols.find { p -> p.serializedPubkeyScript == it.publicKeyScript } != null } - return if (utxos.isEmpty()) { - null - } else { - Bitcoin.addressToPublicKeyScript(chain.chainHash, address).right?.let { script -> - val swapInUtxos = utxos.map { txOut -> - SwapInUtxo( - txOut = txOut, - outPoint = OutPoint(swapInTx, swapInTx.txOut.indexOf(txOut).toLong()), - addressIndex = if (Script.isPay2wsh(txOut.publicKeyScript.toByteArray())) null else swapInProtocols.indexOfFirst { it.serializedPubkeyScript == txOut.publicKeyScript } - ) - } - createRecoveryTransaction(swapInUtxos, ByteVector(Script.write(script)), feerate) + /** + * Create a recovery transaction that spends a swap-in transaction after the refund delay has passed + * @param swapInTx swap-in transaction + * @param address address to send funds to + * @param feerate fee rate for the refund transaction + * @return a signed transaction that spends our swap-in transaction. It cannot be published until `swapInTx` has enough confirmations + */ + fun createRecoveryTransaction(swapInTx: Transaction, address: String, feerate: FeeratePerKw): Transaction? { + val swapInProtocols = (0 until 100).map { getSwapInProtocol(it) } + val utxos = swapInTx.txOut.filter { it.publicKeyScript.contentEquals(Script.write(legacySwapInProtocol.pubkeyScript)) || swapInProtocols.find { p -> p.serializedPubkeyScript == it.publicKeyScript } != null } + return if (utxos.isEmpty()) { + null + } else { + Bitcoin.addressToPublicKeyScript(chain.chainHash, address).right?.let { script -> + val swapInUtxos = utxos.map { txOut -> + SwapInUtxo( + txOut = txOut, + outPoint = OutPoint(swapInTx, swapInTx.txOut.indexOf(txOut).toLong()), + addressIndex = if (Script.isPay2wsh(txOut.publicKeyScript.toByteArray())) null else swapInProtocols.indexOfFirst { it.serializedPubkeyScript == txOut.publicKeyScript } + ) } + createRecoveryTransaction(swapInUtxos, ByteVector(Script.write(script)), feerate) } } + } - companion object { - private fun swapInKeyBasePath(chain: Chain) = when (chain) { - Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(51) / hardened(0) - Chain.Mainnet -> KeyPath.empty / hardened(52) / hardened(0) - } + companion object { + private fun swapInKeyBasePath(chain: Chain) = when (chain) { + Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> KeyPath.empty / hardened(51) / hardened(0) + Chain.Mainnet -> KeyPath.empty / hardened(52) / hardened(0) + } - fun swapInUserKeyPath(chain: Chain) = swapInKeyBasePath(chain) / hardened(0) + fun swapInUserKeyPath(chain: Chain) = swapInKeyBasePath(chain) / hardened(0) - fun swapInLocalServerKeyPath(chain: Chain) = swapInKeyBasePath(chain) / hardened(1) + fun swapInLocalServerKeyPath(chain: Chain) = swapInKeyBasePath(chain) / hardened(1) - fun swapInUserRefundKeyPath(chain: Chain) = swapInKeyBasePath(chain) / hardened(2) / 0L + fun swapInUserRefundKeyPath(chain: Chain) = swapInKeyBasePath(chain) / hardened(2) / 0L - fun encodedSwapInUserKeyPath(chain: Chain) = when (chain) { - Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> "51h/0h/0h" - Chain.Mainnet -> "52h/0h/0h" - } + fun encodedSwapInUserKeyPath(chain: Chain) = when (chain) { + Chain.Testnet4, Chain.Testnet3, Chain.Regtest, Chain.Signet -> "51h/0h/0h" + Chain.Mainnet -> "52h/0h/0h" + } - /** Swap-in servers use a different swap-in key for different users. */ - fun perUserPath(remoteNodeId: PublicKey): KeyPath { - // We hash the remote node_id and break it into 2-byte values to get non-hardened path indices. - val h = ByteArrayInput(Crypto.sha256(remoteNodeId.value)) - return KeyPath((0 until 16).map { _ -> LightningCodecs.u16(h).toLong() }) - } + /** Swap-in servers use a different swap-in key for different users. */ + fun perUserPath(remoteNodeId: PublicKey): KeyPath { + // We hash the remote node_id and break it into 2-byte values to get non-hardened path indices. + val h = ByteArrayInput(Crypto.sha256(remoteNodeId.value)) + return KeyPath((0 until 16).map { _ -> LightningCodecs.u16(h).toLong() }) } } +} + +interface KeyManager { + + val nodeKeys: NodeKeys + val finalOnChainWallet: Bip84OnChainKeys + val swapInOnChainWallet: SwapInOnChainKeys + + /** + * Create a BIP32 funding key path a new channel. + * This function must return a unique path every time it is called. + * This guarantees that unrelated channels use different BIP32 key paths and thus unrelated keys. + * + * @param isChannelOpener true if we initiated the channel open: this must be used to derive different key paths. + */ + fun newFundingKeyPath(isChannelOpener: Boolean): KeyPath + + /** + * Create channel keys based on a funding key path obtained using [newFundingKeyPath]. + * This function is deterministic: it must always return the same result when called with the same arguments. + * This allows re-creating the channel keys based on the seed and its main parameters. + */ + fun channelKeys(fundingKeyPath: KeyPath): ChannelKeys } diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt index 4d627303e..aeb7c293b 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/crypto/LocalKeyManager.kt @@ -4,17 +4,13 @@ import fr.acinq.bitcoin.* import fr.acinq.bitcoin.DeterministicWallet.hardened import fr.acinq.bitcoin.crypto.Pack import fr.acinq.lightning.Lightning.secureRandom -import fr.acinq.lightning.crypto.LocalKeyManager.Companion.channelKeyPath /** - * An implementation of [KeyManager] that supports deterministic derivation for [KeyManager.ChannelKeys] based - * on the initial funding pubkey. - * - * Specifically, for channel keys there are two paths: - * - `fundingKeyPath`: chosen at random using [newFundingKeyPath] - * - `channelKeyPath`: computed from `fundingKeyPath` using [channelKeyPath] + * An implementation of [KeyManager] that supports deterministic derivation of keys, based on an initial + * random funding key. * * The resulting paths looks like so on mainnet: + * * ``` * node key: * 50' / 0' @@ -22,35 +18,37 @@ import fr.acinq.lightning.crypto.LocalKeyManager.Companion.channelKeyPath * funding keys: * 50' / 1' / / <0' or 1'> / ' * - * others channel basepoint keys (payment, revocation, htlc, etc.): - * 50' / 1' / / <1'-5'> + * commitment basepoint keys (payment, revocation, htlc, etc.): + * 50' / 1' / / <1'-5'> * * bip-84 on-chain keys: * 84' / 0' / ' / <0' or 1'> / * ``` * - * @param seed seed from which the channel keys will be derived - * @param remoteSwapInExtendedPublicKey xpub belonging to our swap-in server, that must be used in our swap address + * Where the commitmentKeyPath is generated by hashing the funding key at index 0. + * + * @param seed seed from which the channel funding keys will be derived. + * @param remoteSwapInExtendedPublicKey xpub belonging to our swap-in server, that must be used in our swap address. */ data class LocalKeyManager(val seed: ByteVector, val chain: Chain, val remoteSwapInExtendedPublicKey: String) : KeyManager { private val master = DeterministicWallet.generate(seed) - override val nodeKeys: KeyManager.NodeKeys = KeyManager.NodeKeys( + override val nodeKeys: NodeKeys = NodeKeys( legacyNodeKey = @Suppress("DEPRECATION") master.derivePrivateKey(eclairNodeKeyBasePath(chain)), - nodeKey = master.derivePrivateKey(nodeKeyBasePath(chain)), + nodeKey = master.derivePrivateKey(nodeKeyBasePath(chain)) ) - override val finalOnChainWallet: KeyManager.Bip84OnChainKeys = KeyManager.Bip84OnChainKeys(chain, master, account = 0) - override val swapInOnChainWallet: KeyManager.SwapInOnChainKeys = run { + override val finalOnChainWallet: Bip84OnChainKeys = Bip84OnChainKeys(chain, master, account = 0) + override val swapInOnChainWallet: SwapInOnChainKeys = run { val (prefix, xpub) = DeterministicWallet.ExtendedPublicKey.decode(remoteSwapInExtendedPublicKey) val expectedPrefix = when (chain) { Chain.Mainnet -> DeterministicWallet.xpub else -> DeterministicWallet.tpub } require(prefix == expectedPrefix) { "unexpected swap-in xpub prefix $prefix (expected $expectedPrefix)" } - val remoteSwapInPublicKey = xpub.derivePublicKey(KeyManager.SwapInOnChainKeys.perUserPath(nodeKeys.nodeKey.publicKey)).publicKey - KeyManager.SwapInOnChainKeys(chain, master, remoteSwapInPublicKey) + val remoteSwapInPublicKey = xpub.derivePublicKey(SwapInOnChainKeys.perUserPath(nodeKeys.nodeKey.publicKey)).publicKey + SwapInOnChainKeys(chain, master, remoteSwapInPublicKey) } private val channelKeyBasePath: KeyPath = channelKeyBasePath(chain) @@ -63,28 +61,19 @@ data class LocalKeyManager(val seed: ByteVector, val chain: Chain, val remoteSwa fun privateKey(keyPath: KeyPath): PrivateKey = master.derivePrivateKey(keyPath).privateKey - override fun newFundingKeyPath(isInitiator: Boolean): KeyPath { - val last = hardened(if (isInitiator) 1 else 0) + override fun newFundingKeyPath(isChannelOpener: Boolean): KeyPath { + val last = hardened(if (isChannelOpener) 1 else 0) fun next() = secureRandom.nextInt().toLong() and 0xFFFFFFFF return KeyPath.empty / next() / next() / next() / next() / next() / next() / next() / next() / last } - 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 -> 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() - val recoveredChannelKeys = recoverChannelKeys(initialFundingPubkey) - return KeyManager.ChannelKeys( - fundingKeyPath, - fundingKey = fundingKey, - paymentKey = recoveredChannelKeys.paymentKey, - delayedPaymentKey = recoveredChannelKeys.delayedPaymentKey, - htlcKey = recoveredChannelKeys.htlcKey, - revocationKey = recoveredChannelKeys.revocationKey, - shaSeed = recoveredChannelKeys.shaSeed - ) + override fun channelKeys(fundingKeyPath: KeyPath): ChannelKeys { + val fundingMasterKey = master.derivePrivateKey(channelKeyBasePath / fundingKeyPath) + // We use the initial funding pubkey to compute the key path for commitment keys. + val fundingPublicKey = fundingMasterKey.derivePrivateKey(hardened(0)).publicKey + val keyPath = channelKeyPath(fundingPublicKey) + val commitmentMasterKey = master.derivePrivateKey(channelKeyBasePath / keyPath) + return ChannelKeys(fundingMasterKey = fundingMasterKey, commitmentMasterKey = commitmentMasterKey) } /** diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt index 7cff2d348..ce38a7937 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/json/JsonSerializers.kt @@ -37,7 +37,6 @@ JsonSerializers.ChannelFeaturesSerializer::class, JsonSerializers.FeaturesSerializer::class, JsonSerializers.ShortChannelIdSerializer::class, - JsonSerializers.ChannelKeysSerializer::class, JsonSerializers.TransactionSerializer::class, JsonSerializers.OutPointSerializer::class, JsonSerializers.TxOutSerializer::class, @@ -116,7 +115,6 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.* import fr.acinq.lightning.channel.InteractiveTxSigningSession.Companion.UnsignedLocalCommit import fr.acinq.lightning.channel.states.* -import fr.acinq.lightning.crypto.KeyManager import fr.acinq.lightning.crypto.RouteBlinding import fr.acinq.lightning.crypto.ShaChain import fr.acinq.lightning.payment.Bolt11Invoice @@ -467,11 +465,6 @@ object JsonSerializers { object CltvExpiryDeltaSerializer : LongSerializer({ it.toLong() }) object FeeratePerKwSerializer : LongSerializer({ it.toLong() }) - object ChannelKeysSerializer : SurrogateSerializer( - transform = { it.fundingKeyPath }, - delegateSerializer = KeyPathSerializer - ) - @Serializer(forClass = LocalCommitPublished::class) object LocalCommitPublishedSerializer diff --git a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt index 501a0729f..cc97133dc 100644 --- a/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt +++ b/modules/core/src/commonMain/kotlin/fr/acinq/lightning/transactions/Transactions.kt @@ -24,6 +24,7 @@ import fr.acinq.lightning.CltvExpiryDelta import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.Commitments +import fr.acinq.lightning.crypto.CommitmentPublicKeys import fr.acinq.lightning.transactions.CommitmentOutput.InHtlc import fr.acinq.lightning.transactions.CommitmentOutput.OutHtlc import fr.acinq.lightning.utils.* @@ -324,19 +325,15 @@ object Transactions { fun makeCommitTxOutputs( localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey, - localPaysCommitTxFees: Boolean, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, - remotePaymentPubkey: PublicKey, - localHtlcPubkey: PublicKey, - remoteHtlcPubkey: PublicKey, + commitKeys: CommitmentPublicKeys, + payCommitTxFees: Boolean, + dustLimit: Satoshi, + toSelfDelay: CltvExpiryDelta, spec: CommitmentSpec ): TransactionsCommitmentOutputs { - val commitFee = commitTxFee(localDustLimit, spec) + val commitFee = commitTxFee(dustLimit, spec) - val (toLocalAmount: Satoshi, toRemoteAmount: Satoshi) = if (localPaysCommitTxFees) { + val (toLocalAmount, toRemoteAmount) = if (payCommitTxFees) { Pair(spec.toLocal.truncateToSatoshi() - commitFee, spec.toRemote.truncateToSatoshi()) } else { Pair(spec.toLocal.truncateToSatoshi(), spec.toRemote.truncateToSatoshi() - commitFee) @@ -344,26 +341,26 @@ object Transactions { val outputs = ArrayList>() - if (toLocalAmount >= localDustLimit) outputs.add( + if (toLocalAmount >= dustLimit) outputs.add( CommitmentOutputLink( - TxOut(toLocalAmount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey))), - Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey), + TxOut(toLocalAmount, Script.pay2wsh(Scripts.toLocalDelayed(commitKeys.revocationPublicKey, toSelfDelay, commitKeys.localDelayedPaymentPublicKey))), + Scripts.toLocalDelayed(commitKeys.revocationPublicKey, toSelfDelay, commitKeys.localDelayedPaymentPublicKey), CommitmentOutput.ToLocal ) ) - if (toRemoteAmount >= localDustLimit) { + if (toRemoteAmount >= dustLimit) { outputs.add( CommitmentOutputLink( - TxOut(toRemoteAmount, Script.pay2wsh(Scripts.toRemoteDelayed(remotePaymentPubkey))), - Scripts.toRemoteDelayed(remotePaymentPubkey), + TxOut(toRemoteAmount, Script.pay2wsh(Scripts.toRemoteDelayed(commitKeys.remotePaymentPublicKey))), + Scripts.toRemoteDelayed(commitKeys.remotePaymentPublicKey), CommitmentOutput.ToRemote ) ) } - val untrimmedHtlcs = trimOfferedHtlcs(localDustLimit, spec).isNotEmpty() || trimReceivedHtlcs(localDustLimit, spec).isNotEmpty() - if (untrimmedHtlcs || toLocalAmount >= localDustLimit) + val untrimmedHtlcs = trimOfferedHtlcs(dustLimit, spec).isNotEmpty() || trimReceivedHtlcs(dustLimit, spec).isNotEmpty() + if (untrimmedHtlcs || toLocalAmount >= dustLimit) outputs.add( CommitmentOutputLink( TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2wsh(Scripts.toAnchor(localFundingPubkey))), @@ -371,7 +368,7 @@ object Transactions { CommitmentOutput.ToLocalAnchor(localFundingPubkey) ) ) - if (untrimmedHtlcs || toRemoteAmount >= localDustLimit) + if (untrimmedHtlcs || toRemoteAmount >= dustLimit) outputs.add( CommitmentOutputLink( TxOut(Commitments.ANCHOR_AMOUNT, Script.pay2wsh(Scripts.toAnchor(remoteFundingPubkey))), @@ -380,13 +377,13 @@ object Transactions { ) ) - trimOfferedHtlcs(localDustLimit, spec).forEach { htlc -> - val redeemScript = Scripts.htlcOffered(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray())) + trimOfferedHtlcs(dustLimit, spec).forEach { htlc -> + val redeemScript = Scripts.htlcOffered(commitKeys.localHtlcPublicKey, commitKeys.remoteHtlcPublicKey, commitKeys.revocationPublicKey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray())) outputs.add(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, OutHtlc(htlc))) } - trimReceivedHtlcs(localDustLimit, spec).forEach { htlc -> - val redeemScript = Scripts.htlcReceived(localHtlcPubkey, remoteHtlcPubkey, localRevocationPubkey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray()), htlc.add.cltvExpiry) + trimReceivedHtlcs(dustLimit, spec).forEach { htlc -> + val redeemScript = Scripts.htlcReceived(commitKeys.localHtlcPublicKey, commitKeys.remoteHtlcPublicKey, commitKeys.revocationPublicKey, Crypto.ripemd160(htlc.add.paymentHash.toByteArray()), htlc.add.cltvExpiry) outputs.add(CommitmentOutputLink(TxOut(htlc.add.amountMsat.truncateToSatoshi(), Script.pay2wsh(redeemScript)), redeemScript, InHtlc(htlc))) } @@ -453,7 +450,7 @@ object Transactions { outputIndex: Int, localDustLimit: Satoshi, localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, + toSelfDelay: CltvExpiryDelta, localDelayedPaymentPubkey: PublicKey, feerate: FeeratePerKw ): TxResult { @@ -468,7 +465,7 @@ object Transactions { val tx = Transaction( version = 2, txIn = listOf(TxIn(input.outPoint, ByteVector.empty, 1L)), - txOut = listOf(TxOut(amount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey)))), + txOut = listOf(TxOut(amount, Script.pay2wsh(Scripts.toLocalDelayed(localRevocationPubkey, toSelfDelay, localDelayedPaymentPubkey)))), lockTime = 0 ) TxResult.Success(TransactionWithInputInfo.HtlcTx.HtlcSuccessTx(input, tx, htlc.paymentHash, htlc.id)) @@ -477,10 +474,9 @@ object Transactions { fun makeHtlcTxs( commitTx: Transaction, - localDustLimit: Satoshi, - localRevocationPubkey: PublicKey, - toLocalDelay: CltvExpiryDelta, - localDelayedPaymentPubkey: PublicKey, + commitKeys: CommitmentPublicKeys, + dustLimit: Satoshi, + toSelfDelay: CltvExpiryDelta, feerate: FeeratePerKw, outputs: TransactionsCommitmentOutputs ): List { @@ -488,7 +484,7 @@ object Transactions { .mapIndexedNotNull map@{ outputIndex, link -> val outHtlc = link.commitmentOutput as? OutHtlc ?: return@map null val co = CommitmentOutputLink(link.output, link.redeemScript, outHtlc) - makeHtlcTimeoutTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feerate) + makeHtlcTimeoutTx(commitTx, co, outputIndex, dustLimit, commitKeys.revocationPublicKey, toSelfDelay, commitKeys.localDelayedPaymentPublicKey, feerate) } .mapNotNull { (it as? TxResult.Success)?.result } @@ -496,7 +492,7 @@ object Transactions { .mapIndexedNotNull map@{ outputIndex, link -> val inHtlc = link.commitmentOutput as? InHtlc ?: return@map null val co = CommitmentOutputLink(link.output, link.redeemScript, inHtlc) - makeHtlcSuccessTx(commitTx, co, outputIndex, localDustLimit, localRevocationPubkey, toLocalDelay, localDelayedPaymentPubkey, feerate) + makeHtlcSuccessTx(commitTx, co, outputIndex, dustLimit, commitKeys.revocationPublicKey, toSelfDelay, commitKeys.localDelayedPaymentPublicKey, feerate) } .mapNotNull { (it as? TxResult.Success)?.result } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt index cb266c319..bdcb59eb8 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/InteractiveTxTestsCommon.kt @@ -8,7 +8,9 @@ import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.MilliSatoshi import fr.acinq.lightning.blockchain.electrum.WalletState import fr.acinq.lightning.blockchain.fee.FeeratePerKw +import fr.acinq.lightning.crypto.ChannelKeys import fr.acinq.lightning.crypto.KeyManager +import fr.acinq.lightning.crypto.SwapInOnChainKeys import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite import fr.acinq.lightning.transactions.Scripts @@ -778,7 +780,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `cannot contribute unusable or invalid inputs`() { - val channelKeys = TestConstants.Alice.keyManager.run { channelKeys(newFundingKeyPath(isInitiator = true)) } + val channelKeys = TestConstants.Alice.keyManager.run { channelKeys(newFundingKeyPath(isChannelOpener = true)) } val swapInKeys = TestConstants.Alice.keyManager.swapInOnChainWallet val privKey = randomKey() val pubKey = privKey.publicKey() @@ -811,7 +813,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { @Test fun `cannot pay liquidity ads fees`() { - val channelKeys = TestConstants.Alice.keyManager.run { channelKeys(newFundingKeyPath(isInitiator = true)) } + val channelKeys = TestConstants.Alice.keyManager.run { channelKeys(newFundingKeyPath(isChannelOpener = true)) } val swapInKeys = TestConstants.Alice.keyManager.swapInOnChainWallet val walletKey = randomKey().publicKey() val fundingParams = InteractiveTxParams(randomBytes32(), true, 0.sat, 250_000.sat, walletKey, 0, 660.sat, FeeratePerKw(2500.sat)) @@ -1268,12 +1270,12 @@ class InteractiveTxTestsCommon : LightningTestSuite() { data class Fixture( val channelId: ByteVector32, val keyManagerA: KeyManager, - val channelKeysA: KeyManager.ChannelKeys, + val channelKeysA: ChannelKeys, val localParamsA: LocalParams, val fundingParamsA: InteractiveTxParams, val fundingContributionsA: FundingContributions, val keyManagerB: KeyManager, - val channelKeysB: KeyManager.ChannelKeys, + val channelKeysB: ChannelKeys, val localParamsB: LocalParams, val fundingParamsB: InteractiveTxParams, val fundingContributionsB: FundingContributions @@ -1302,8 +1304,8 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val channelKeysB = localParamsB.channelKeys(TestConstants.Bob.keyManager) val swapInKeysA = TestConstants.Alice.keyManager.swapInOnChainWallet val swapInKeysB = TestConstants.Bob.keyManager.swapInOnChainWallet - val fundingPubkeyA = channelKeysA.fundingPubKey(fundingTxIndex) - val fundingPubkeyB = channelKeysB.fundingPubKey(fundingTxIndex) + val fundingPubkeyA = channelKeysA.fundingKey(fundingTxIndex).publicKey() + val fundingPubkeyB = channelKeysB.fundingKey(fundingTxIndex).publicKey() val fundingParamsA = InteractiveTxParams(channelId, true, fundingAmountA, fundingAmountB, fundingPubkeyB, lockTime, dustLimit, targetFeerate) val fundingParamsB = InteractiveTxParams(channelId, false, fundingAmountB, fundingAmountA, fundingPubkeyA, lockTime, dustLimit, targetFeerate) val walletA = createWallet(swapInKeysA, utxosA, legacyUtxosA) @@ -1333,15 +1335,15 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val channelKeysA = localParamsA.channelKeys(TestConstants.Alice.keyManager) val channelKeysB = localParamsB.channelKeys(TestConstants.Bob.keyManager) val swapInKeysA = TestConstants.Alice.keyManager.swapInOnChainWallet - val fundingPubkeyA = channelKeysA.fundingPubKey(fundingTxIndex) - val fundingPubkeyB = channelKeysB.fundingPubKey(fundingTxIndex) + val fundingPubkeyA = channelKeysA.fundingKey(fundingTxIndex).publicKey() + val fundingPubkeyB = channelKeysB.fundingKey(fundingTxIndex).publicKey() val redeemScript = Scripts.multiSig2of2(fundingPubkeyA, fundingPubkeyB) val fundingScript = Script.write(Script.pay2wsh(redeemScript)).byteVector() val previousFundingAmount = (balanceA + balanceB).truncateToSatoshi() val previousFundingTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(previousFundingAmount, fundingScript)), 0) val inputInfo = Transactions.InputInfo(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0], redeemScript) - val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, channelKeysB.fundingPubKey(fundingTxIndex)) - val nextFundingPubkeyB = channelKeysB.fundingPubKey(fundingTxIndex + 1) + val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, fundingPubkeyB) + val nextFundingPubkeyB = channelKeysB.fundingKey(fundingTxIndex + 1).publicKey() val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, lockTime, dustLimit, targetFeerate) return FundingContributions.create(channelKeysA, swapInKeysA, fundingParamsA, Pair(sharedInputA, SharedFundingInputBalances(balanceA, balanceB, 0.msat)), listOf(), outputsA, null, randomKey().publicKey()) } @@ -1368,17 +1370,17 @@ class InteractiveTxTestsCommon : LightningTestSuite() { val channelKeysB = localParamsB.channelKeys(TestConstants.Bob.keyManager) val swapInKeysA = TestConstants.Alice.keyManager.swapInOnChainWallet val swapInKeysB = TestConstants.Bob.keyManager.swapInOnChainWallet - val fundingPubkeyA = channelKeysA.fundingPubKey(fundingTxIndex) - val fundingPubkeyB = channelKeysB.fundingPubKey(fundingTxIndex) + val fundingPubkeyA = channelKeysA.fundingKey(fundingTxIndex).publicKey() + val fundingPubkeyB = channelKeysB.fundingKey(fundingTxIndex).publicKey() val redeemScript = Scripts.multiSig2of2(fundingPubkeyA, fundingPubkeyB) val fundingScript = Script.write(Script.pay2wsh(redeemScript)).byteVector() val previousFundingAmount = (balanceA + balanceB).truncateToSatoshi() val previousFundingTx = Transaction(2, listOf(TxIn(OutPoint(TxId(randomBytes32()), 0), 0)), listOf(TxOut(previousFundingAmount, fundingScript)), 0) val inputInfo = Transactions.InputInfo(OutPoint(previousFundingTx, 0), previousFundingTx.txOut[0], redeemScript) - val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, channelKeysB.fundingPubKey(fundingTxIndex)) - val sharedInputB = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, channelKeysA.fundingPubKey(fundingTxIndex)) - val nextFundingPubkeyA = channelKeysA.fundingPubKey(fundingTxIndex + 1) - val nextFundingPubkeyB = channelKeysB.fundingPubKey(fundingTxIndex + 1) + val sharedInputA = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, fundingPubkeyB) + val sharedInputB = SharedFundingInput.Multisig2of2(inputInfo, fundingTxIndex, fundingPubkeyA) + val nextFundingPubkeyA = channelKeysA.fundingKey(fundingTxIndex + 1).publicKey() + val nextFundingPubkeyB = channelKeysB.fundingKey(fundingTxIndex + 1).publicKey() val fundingParamsA = InteractiveTxParams(channelId, true, fundingContributionA, fundingContributionB, sharedInputA, nextFundingPubkeyB, outputsA, lockTime, dustLimit, targetFeerate) val fundingParamsB = InteractiveTxParams(channelId, false, fundingContributionB, fundingContributionA, sharedInputB, nextFundingPubkeyA, outputsB, lockTime, dustLimit, targetFeerate) val walletA = createWallet(swapInKeysA, utxosA) @@ -1417,7 +1419,7 @@ class InteractiveTxTestsCommon : LightningTestSuite() { return action1 } - private fun createWallet(onChainKeys: KeyManager.SwapInOnChainKeys, amounts: List, legacyAmounts: List = listOf()): List { + private fun createWallet(onChainKeys: SwapInOnChainKeys, amounts: List, legacyAmounts: List = listOf()): List { return amounts.withIndex().map { amount -> val txIn = listOf(TxIn(OutPoint(TxId(randomBytes32()), 2), 0)) val txOut = listOf(TxOut(amount.value, onChainKeys.getSwapInProtocol(amount.index).pubkeyScript), TxOut(150.sat, Script.pay2wpkh(randomKey().publicKey()))) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt index dd42542ca..d5c812a26 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/channel/TestsHelper.kt @@ -194,29 +194,28 @@ object TestsHelper { val bobChannelParams = TestConstants.Bob.channelParams(payCommitTxFees = channelFlags.nonInitiatorPaysCommitFees).copy(features = bobFeatures.initFeatures()) val aliceInit = Init(aliceFeatures) val bobInit = Init(bobFeatures) - val (alice1, actionsAlice1) = alice.process( - ChannelCommand.Init.Initiator( - CompletableDeferred(), - aliceFundingAmount, - createWallet(aliceNodeParams.keyManager, aliceFundingAmount + 3500.sat).second, - FeeratePerKw.CommitmentFeerate, - TestConstants.feeratePerKw, - aliceChannelParams, - bobInit, - channelFlags, - ChannelConfig.standard, - channelType, - requestRemoteFunding?.let { - when (channelOrigin) { - is Origin.OffChainPayment -> LiquidityAds.RequestFunding(it, TestConstants.fundingRates.findRate(it)!!, LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(channelOrigin.paymentHash))) - else -> LiquidityAds.RequestFunding(it, TestConstants.fundingRates.findRate(it)!!, LiquidityAds.PaymentDetails.FromChannelBalance) - } - }, - channelOrigin, - ) + val cmd = ChannelCommand.Init.Initiator( + CompletableDeferred(), + aliceFundingAmount, + createWallet(aliceNodeParams.keyManager, aliceFundingAmount + 3500.sat).second, + FeeratePerKw.CommitmentFeerate, + TestConstants.feeratePerKw, + aliceChannelParams, + bobInit, + channelFlags, + ChannelConfig.standard, + channelType, + requestRemoteFunding?.let { + when (channelOrigin) { + is Origin.OffChainPayment -> LiquidityAds.RequestFunding(it, TestConstants.fundingRates.findRate(it)!!, LiquidityAds.PaymentDetails.FromFutureHtlc(listOf(channelOrigin.paymentHash))) + else -> LiquidityAds.RequestFunding(it, TestConstants.fundingRates.findRate(it)!!, LiquidityAds.PaymentDetails.FromChannelBalance) + } + }, + channelOrigin, ) + val (alice1, actionsAlice1) = alice.process(cmd) assertIs>(alice1) - val temporaryChannelId = aliceChannelParams.channelKeys(alice.ctx.keyManager).temporaryChannelId + val temporaryChannelId = cmd.temporaryChannelId(aliceChannelParams.channelKeys(alice.ctx.keyManager)) val bobWallet = if (bobFundingAmount > 0.sat) createWallet(bobNodeParams.keyManager, bobFundingAmount + 1500.sat).second else listOf() val (bob1, _) = bob.process( ChannelCommand.Init.NonInitiator( @@ -433,24 +432,23 @@ object TestsHelper { fun useAlternativeCommitSig(s: LNChannel, commitment: Commitment, alternative: CommitSigTlv.AlternativeFeerateSig): Transaction { val channelKeys = s.commitments.params.localParams.channelKeys(s.ctx.keyManager) + val fundingKey = channelKeys.fundingKey(commitment.fundingTxIndex) + val commitKeys = channelKeys.localCommitmentKeys(s.commitments.params, commitment.localCommit.index) val alternativeSpec = commitment.localCommit.spec.copy(feerate = alternative.feerate) val fundingTxIndex = commitment.fundingTxIndex val commitInput = commitment.commitInput val remoteFundingPubKey = commitment.remoteFundingPubkey - val localPerCommitmentPoint = channelKeys.commitmentPoint(commitment.localCommit.index) val (localCommitTx, _) = Commitments.makeLocalTxs( - channelKeys, - commitment.localCommit.index, - s.commitments.params.localParams, - s.commitments.params.remoteParams, - fundingTxIndex, - remoteFundingPubKey, - commitInput, - localPerCommitmentPoint, - alternativeSpec + channelParams = s.commitments.params, + commitKeys = commitKeys, + commitTxNumber = commitment.localCommit.index, + localFundingKey = fundingKey, + remoteFundingPubKey = remoteFundingPubKey, + commitmentInput = commitInput, + spec = alternativeSpec ) val localSig = Transactions.sign(localCommitTx, channelKeys.fundingKey(fundingTxIndex)) - val signedCommitTx = Transactions.addSigs(localCommitTx, channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubKey, localSig, alternative.sig) + val signedCommitTx = Transactions.addSigs(localCommitTx, fundingKey.publicKey(), remoteFundingPubKey, localSig, alternative.sig) assertTrue(Transactions.checkSpendable(signedCommitTx).isSuccess) return signedCommitTx.tx } diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt index 0a519fb34..83b3a18bc 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/crypto/LocalKeyManagerTestsCommon.kt @@ -45,11 +45,10 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { // if this test fails it means that we cannot restore channels created with older versions of lightning-kmp without // some kind of migration process val errorMsg = "channel key generation is broken !!!" - assertEquals(fundingKeyPath, channelKeys.fundingKeyPath, errorMsg) assertEquals(PrivateKey.fromHex("cd85f39fad742e5c742eeab16f5f1acaa9d9c48977767c7daa4708a47b7222ec"), channelKeys.fundingKey(0), errorMsg) - assertEquals(PrivateKey.fromHex("ad635d9d4919e5657a9f306963a5976b533e9d70c8defa454f1bd958fae316c8"), channelKeys.paymentKey, errorMsg) - assertEquals(PrivateKey.fromHex("0f3c23df3feec614117de23d0b3f014174271826a16e59a17d9ebb655cc55e3f"), channelKeys.delayedPaymentKey, errorMsg) - assertEquals(PrivateKey.fromHex("ee211f583f3b1b1fb10dca7c82708d985fde641e83e28080f669eb496de85113"), channelKeys.revocationKey, errorMsg) + assertEquals(PrivateKey.fromHex("ad635d9d4919e5657a9f306963a5976b533e9d70c8defa454f1bd958fae316c8"), channelKeys.paymentBaseSecret, errorMsg) + assertEquals(PrivateKey.fromHex("0f3c23df3feec614117de23d0b3f014174271826a16e59a17d9ebb655cc55e3f"), channelKeys.delayedPaymentBaseSecret, errorMsg) + assertEquals(PrivateKey.fromHex("ee211f583f3b1b1fb10dca7c82708d985fde641e83e28080f669eb496de85113"), channelKeys.revocationBaseSecret, errorMsg) assertEquals(ByteVector32.fromValidHex("6255a59ea8155d41e62cddef2c8c63a077f75e23fd3eec1fd4881f6851412518"), channelKeys.shaSeed, errorMsg) } @@ -62,7 +61,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val fundingKeyPath = KeyPath("1") val channelKeys1 = keyManager1.channelKeys(fundingKeyPath) val channelKeys2 = keyManager2.channelKeys(fundingKeyPath) - assertNotEquals(channelKeys1.fundingPubKey(0), channelKeys2.fundingPubKey(0)) + assertNotEquals(channelKeys1.fundingKey(0).publicKey(), channelKeys2.fundingKey(0).publicKey()) assertNotEquals(channelKeys1.commitmentPoint(1), channelKeys2.commitmentPoint(1)) } @@ -86,15 +85,14 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val seed = ByteVector("17b086b228025fa8f4416324b6ba2ec36e68570ae2fc3d392520969f2a9d0c1501") val keyManager = LocalKeyManager(seed, Chain.Regtest, TestConstants.aliceSwapInServerXpub) val fundingKeyPath = makeFundingKeyPath(ByteVector("be4fa97c62b9f88437a3be577b31eb48f2165c7bc252194a15ff92d995778cfb"), isInitiator = true) - val localParams = TestConstants.Alice.channelParams(payCommitTxFees = true).copy(fundingKeyPath = fundingKeyPath) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) - assertEquals(channelKeys.fundingPubKey(0), PrivateKey.fromHex("730c0f99408dbfbff00146acf84183ce539fabeeb22c143212f459d71374f715").publicKey()) - assertEquals(channelKeys.revocationBasepoint, PrivateKey.fromHex("ef2aa0a9b4d0bdbc5ee5025f0d16285dc9d17228af1b2cc1e1456252c2d9d207").publicKey()) - assertEquals(channelKeys.paymentBasepoint, PrivateKey.fromHex("e1b76bd22587f88f0903c65aa47f4862152297b4e8dcf3af1f60e762a4ab04e5").publicKey()) - assertEquals(channelKeys.delayedPaymentBasepoint, PrivateKey.fromHex("93d78a9604571baab6882344747a9372f8d0b9e01b569b431314699e397b73e6").publicKey()) - assertEquals(channelKeys.htlcBasepoint, PrivateKey.fromHex("b08ab019cfc8a2b28992d3915ed217b71a596bc85dc766e0fb1fee805ef531c1").publicKey()) + assertEquals(channelKeys.fundingKey(0).publicKey(), PrivateKey.fromHex("730c0f99408dbfbff00146acf84183ce539fabeeb22c143212f459d71374f715").publicKey()) + assertEquals(channelKeys.revocationBasePoint, PrivateKey.fromHex("ef2aa0a9b4d0bdbc5ee5025f0d16285dc9d17228af1b2cc1e1456252c2d9d207").publicKey()) + assertEquals(channelKeys.paymentBasePoint, PrivateKey.fromHex("e1b76bd22587f88f0903c65aa47f4862152297b4e8dcf3af1f60e762a4ab04e5").publicKey()) + assertEquals(channelKeys.delayedPaymentBasePoint, PrivateKey.fromHex("93d78a9604571baab6882344747a9372f8d0b9e01b569b431314699e397b73e6").publicKey()) + assertEquals(channelKeys.htlcBasePoint, PrivateKey.fromHex("b08ab019cfc8a2b28992d3915ed217b71a596bc85dc766e0fb1fee805ef531c1").publicKey()) assertEquals(channelKeys.commitmentSecret(0).value, ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("5de1ddde2a94029007f18676b3e9f0141782b95a4aa84061711e554d4111dbb3"), 0xFFFFFFFFFFFFL)) } @@ -103,15 +101,14 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val seed = ByteVector("aeb3e9b5642cd4523e9e09164047f60adb413633549c3c6189192921311894d501") val keyManager = LocalKeyManager(seed, Chain.Regtest, TestConstants.aliceSwapInServerXpub) val fundingKeyPath = makeFundingKeyPath(ByteVector("06535806c1aa73971ec4877a5e2e684fa636136c073810f190b63eefc58ca488"), isInitiator = false) - val localParams = TestConstants.Alice.channelParams(payCommitTxFees = true).copy(fundingKeyPath = fundingKeyPath) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) - assertEquals(channelKeys.fundingPubKey(0), PrivateKey.fromHex("cd85f39fad742e5c742eeab16f5f1acaa9d9c48977767c7daa4708a47b7222ec").publicKey()) - assertEquals(channelKeys.revocationBasepoint, PrivateKey.fromHex("ee211f583f3b1b1fb10dca7c82708d985fde641e83e28080f669eb496de85113").publicKey()) - assertEquals(channelKeys.paymentBasepoint, PrivateKey.fromHex("ad635d9d4919e5657a9f306963a5976b533e9d70c8defa454f1bd958fae316c8").publicKey()) - assertEquals(channelKeys.delayedPaymentBasepoint, PrivateKey.fromHex("0f3c23df3feec614117de23d0b3f014174271826a16e59a17d9ebb655cc55e3f").publicKey()) - assertEquals(channelKeys.htlcBasepoint, PrivateKey.fromHex("664ca828a0510950f24859b62203af192ccc1188f20eb87de33c76e7e04ab0d4").publicKey()) + assertEquals(channelKeys.fundingKey(0).publicKey(), PrivateKey.fromHex("cd85f39fad742e5c742eeab16f5f1acaa9d9c48977767c7daa4708a47b7222ec").publicKey()) + assertEquals(channelKeys.revocationBasePoint, PrivateKey.fromHex("ee211f583f3b1b1fb10dca7c82708d985fde641e83e28080f669eb496de85113").publicKey()) + assertEquals(channelKeys.paymentBasePoint, PrivateKey.fromHex("ad635d9d4919e5657a9f306963a5976b533e9d70c8defa454f1bd958fae316c8").publicKey()) + assertEquals(channelKeys.delayedPaymentBasePoint, PrivateKey.fromHex("0f3c23df3feec614117de23d0b3f014174271826a16e59a17d9ebb655cc55e3f").publicKey()) + assertEquals(channelKeys.htlcBasePoint, PrivateKey.fromHex("664ca828a0510950f24859b62203af192ccc1188f20eb87de33c76e7e04ab0d4").publicKey()) assertEquals(channelKeys.commitmentSecret(0).value, ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("6255a59ea8155d41e62cddef2c8c63a077f75e23fd3eec1fd4881f6851412518"), 0xFFFFFFFFFFFFL)) } @@ -120,15 +117,14 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val seed = ByteVector("d8d5431487c2b19ee6486aad6c3bdfb99d10b727bade7fa848e2ab7901c15bff01") val keyManager = LocalKeyManager(seed, Chain.Mainnet, DeterministicWallet.encode(dummyExtendedPubkey, testnet = false)) val fundingKeyPath = makeFundingKeyPath(ByteVector("ec1c41cd6be2b6e4ef46c1107f6c51fbb2066d7e1f7720bde4715af233ae1322"), isInitiator = true) - val localParams = TestConstants.Alice.channelParams(payCommitTxFees = true).copy(fundingKeyPath = fundingKeyPath) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) - assertEquals(channelKeys.fundingPubKey(0), PrivateKey.fromHex("b3b3f1af2ef961ee7aa62451a93a1fd57ea126c81008e5d95ced822cca30da6e").publicKey()) - assertEquals(channelKeys.revocationBasepoint, PrivateKey.fromHex("119ae90789c0b9a68e5cfa2eee08b62cc668b2cd758403dfa7eabde1dc0b6d0a").publicKey()) - assertEquals(channelKeys.paymentBasepoint, PrivateKey.fromHex("882003004cf9c58003f4be161c0ea72879ea9bae8893fd37fb0b3980e0bed0f7").publicKey()) - assertEquals(channelKeys.delayedPaymentBasepoint, PrivateKey.fromHex("7bf712af4006aefeef189b91346f5e3f9a470cc4be9fff9b2ef290032c1bfd3b").publicKey()) - assertEquals(channelKeys.htlcBasepoint, PrivateKey.fromHex("17c685f22bce6f9f1c704477f8ecc7c89b1bf20536fcd30c48fc13666f8d62aa").publicKey()) + assertEquals(channelKeys.fundingKey(0).publicKey(), PrivateKey.fromHex("b3b3f1af2ef961ee7aa62451a93a1fd57ea126c81008e5d95ced822cca30da6e").publicKey()) + assertEquals(channelKeys.revocationBasePoint, PrivateKey.fromHex("119ae90789c0b9a68e5cfa2eee08b62cc668b2cd758403dfa7eabde1dc0b6d0a").publicKey()) + assertEquals(channelKeys.paymentBasePoint, PrivateKey.fromHex("882003004cf9c58003f4be161c0ea72879ea9bae8893fd37fb0b3980e0bed0f7").publicKey()) + assertEquals(channelKeys.delayedPaymentBasePoint, PrivateKey.fromHex("7bf712af4006aefeef189b91346f5e3f9a470cc4be9fff9b2ef290032c1bfd3b").publicKey()) + assertEquals(channelKeys.htlcBasePoint, PrivateKey.fromHex("17c685f22bce6f9f1c704477f8ecc7c89b1bf20536fcd30c48fc13666f8d62aa").publicKey()) assertEquals(channelKeys.commitmentSecret(0).value, ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("cb94d016a90a5558d0d53f928046be41f0584acd8993a399bbd2cb40e5376dac"), 0xFFFFFFFFFFFFL)) } @@ -137,15 +133,14 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val seed = ByteVector("4b809dd593b36131c454d60c2f7bdfd49d12ec455e5b657c47a9ca0f5dfc5eef01") val keyManager = LocalKeyManager(seed, Chain.Mainnet, DeterministicWallet.encode(dummyExtendedPubkey, testnet = false)) val fundingKeyPath = makeFundingKeyPath(ByteVector("2b4f045be5303d53f9d3a84a1e70c12251168dc29f300cf9cece0ec85cd8182b"), isInitiator = false) - val localParams = TestConstants.Alice.channelParams(payCommitTxFees = true).copy(fundingKeyPath = fundingKeyPath) val channelKeys = keyManager.channelKeys(localParams.fundingKeyPath) - assertEquals(channelKeys.fundingPubKey(0), PrivateKey.fromHex("033880995016c275e725da625e4a78ea8c3215ab8ea54145fa3124bbb2e4a3d4").publicKey()) - assertEquals(channelKeys.revocationBasepoint, PrivateKey.fromHex("16d8dd5e6a22de173288cdb7905cfbbcd9efab99471eb735ff95cb7fbdf43e45").publicKey()) - assertEquals(channelKeys.paymentBasepoint, PrivateKey.fromHex("1682a3b6ebcee107156c49f5d7e29423b1abcc396add6357e9e2d0721881fda0").publicKey()) - assertEquals(channelKeys.delayedPaymentBasepoint, PrivateKey.fromHex("2f047edff3e96d16d726a265ddb95d61f695d34b1861f10f80c1758271b00523").publicKey()) - assertEquals(channelKeys.htlcBasepoint, PrivateKey.fromHex("3e740f7d7d214db23ca17b9586e22f004497dbef585781f5a864ed794ad695c6").publicKey()) + assertEquals(channelKeys.fundingKey(0).publicKey(), PrivateKey.fromHex("033880995016c275e725da625e4a78ea8c3215ab8ea54145fa3124bbb2e4a3d4").publicKey()) + assertEquals(channelKeys.revocationBasePoint, PrivateKey.fromHex("16d8dd5e6a22de173288cdb7905cfbbcd9efab99471eb735ff95cb7fbdf43e45").publicKey()) + assertEquals(channelKeys.paymentBasePoint, PrivateKey.fromHex("1682a3b6ebcee107156c49f5d7e29423b1abcc396add6357e9e2d0721881fda0").publicKey()) + assertEquals(channelKeys.delayedPaymentBasePoint, PrivateKey.fromHex("2f047edff3e96d16d726a265ddb95d61f695d34b1861f10f80c1758271b00523").publicKey()) + assertEquals(channelKeys.htlcBasePoint, PrivateKey.fromHex("3e740f7d7d214db23ca17b9586e22f004497dbef585781f5a864ed794ad695c6").publicKey()) assertEquals(channelKeys.commitmentSecret(0).value, ShaChain.shaChainFromSeed(ByteVector32.fromValidHex("a7968178e0472a53eb5a45bb86d8c4591509fbaeba1e223acc80cc28d37b4804"), 0xFFFFFFFFFFFFL)) } @@ -218,7 +213,7 @@ class LocalKeyManagerTestsCommon : LightningTestSuite() { val chain = Chain.Regtest val userPublicKey = PrivateKey.fromHex("0101010101010101010101010101010101010101010101010101010101010101").publicKey() val remoteServerPublicKey = PrivateKey.fromHex("0202020202020202020202020202020202020202020202020202020202020202").publicKey() - val userRefundExtendedPrivateKey = master.derivePrivateKey(KeyManager.SwapInOnChainKeys.swapInUserRefundKeyPath(chain)) + val userRefundExtendedPrivateKey = master.derivePrivateKey(SwapInOnChainKeys.swapInUserRefundKeyPath(chain)) val refundDelay = 2590 assertEquals( "tr(1fc559d9c96c5953895d3150e64ebf3dd696a0b08e758650b48ff6251d7e60d1,and_v(v:pk(tprv8hWm2EfcAbMerYoXeHA9w6faUqXdiQeWfSxxWpzh3Yc1FAjB2vv1sbBNY1dX3HraotvBAEeY2hzz1X4vc3SC516K1ebBvLYrkA6LstQdbNX/*),older(2590)))#90ftphf9", diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt index aeed6f94d..bf1b62092 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/AnchorOutputsTestsCommon.kt @@ -5,19 +5,20 @@ import fr.acinq.bitcoin.utils.Try import fr.acinq.bitcoin.utils.runTrying import fr.acinq.lightning.CltvExpiry import fr.acinq.lightning.CltvExpiryDelta +import fr.acinq.lightning.Feature import fr.acinq.lightning.Lightning.randomBytes32 import fr.acinq.lightning.Lightning.randomKey import fr.acinq.lightning.blockchain.fee.FeeratePerKw -import fr.acinq.lightning.channel.Commitments -import fr.acinq.lightning.channel.LocalParams -import fr.acinq.lightning.channel.RemoteParams -import fr.acinq.lightning.crypto.Bolt3Derivation.deriveForCommitment -import fr.acinq.lightning.crypto.KeyManager.ChannelKeys +import fr.acinq.lightning.channel.* +import fr.acinq.lightning.crypto.ChannelKeys +import fr.acinq.lightning.crypto.LocalCommitmentKeys import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.TestHelpers import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcSuccessTx import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.HtlcTx.HtlcTimeoutTx -import fr.acinq.lightning.utils.* +import fr.acinq.lightning.utils.msat +import fr.acinq.lightning.utils.sat +import fr.acinq.lightning.utils.toByteVector import fr.acinq.lightning.wire.UpdateAddHtlc import kotlinx.serialization.json.Json import kotlin.test.Test @@ -28,38 +29,31 @@ class AnchorOutputsTestsCommon { val local_funding_privkey = PrivateKey.fromHex("30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f374901") val local_funding_pubkey = PublicKey.fromHex(" 023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb") val remote_funding_pubkey = PublicKey.fromHex("030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1") - val local_privkey = PrivateKey.fromHex("bb13b121cdc357cd2e608b0aea294afca36e2b34cf958e2e6451a2f27469449101") - val localpubkey = PublicKey.fromHex("030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e7") - val remotepubkey = PublicKey.fromHex("0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b") - val local_delayedpubkey = PublicKey.fromHex("03fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c") val local_revocation_pubkey = PublicKey.fromHex("0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19") val remote_funding_privkey = PrivateKey.fromHex("1552dfba4f6cf29a62a0af13c8d6981d36d0ef8d61ba10fb0fe90da7634d7e1301") val local_payment_basepoint_secret = PrivateKey.fromHex("111111111111111111111111111111111111111111111111111111111111111101") - val remote_revocation_basepoint_secret = PrivateKey.fromHex("222222222222222222222222222222222222222222222222222222222222222201") val local_delayed_payment_basepoint_secret = PrivateKey.fromHex("333333333333333333333333333333333333333333333333333333333333333301") val remote_payment_basepoint_secret = PrivateKey.fromHex("444444444444444444444444444444444444444444444444444444444444444401") - val local_per_commitment_secret = PrivateKey.fromHex("1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a0908070605040302010001") - - // From remote_revocation_basepoint_secret val remote_revocation_basepoint = PublicKey.fromHex("02466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f27") - - // From local_delayed_payment_basepoint_secret - val local_delayed_payment_basepoint = PublicKey.fromHex("023c72addb4fdf09af94f0c94d7fe92a386a7e70cf8a1d85916386bb2535c7b1b1") val local_per_commitment_point = PublicKey.fromHex("025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486") - val remote_privkey = PrivateKey.fromHex("8deba327a7cc6d638ab0eb025770400a6184afcba6713c210d8d10e199ff2fda01") - - // From local_delayed_payment_basepoint_secret, local_per_commitment_point and local_delayed_payment_basepoint - val local_delayed_privkey = PrivateKey.fromHex("adf3464ce9c2f230fd2582fda4c6965e4993ca5524e8c9580e3df0cf226981ad01") - - val local_htlc_privkey = local_payment_basepoint_secret.deriveForCommitment(local_per_commitment_point) - val local_payment_privkey = local_payment_basepoint_secret - val local_delayed_payment_privkey = local_delayed_payment_basepoint_secret.deriveForCommitment(local_per_commitment_point) - - val remote_htlc_privkey = remote_payment_basepoint_secret.deriveForCommitment(local_per_commitment_point) + val local_htlc_privkey = ChannelKeys.derivePerCommitmentKey(local_payment_basepoint_secret, local_per_commitment_point) + val local_delayed_payment_privkey = ChannelKeys.derivePerCommitmentKey(local_delayed_payment_basepoint_secret, local_per_commitment_point) + val remote_htlc_privkey = ChannelKeys.derivePerCommitmentKey(remote_payment_basepoint_secret, local_per_commitment_point) val remote_payment_privkey = remote_payment_basepoint_secret - val funding_tx = - Transaction.read("0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000") + // Keys used by the local node to spend outputs of its local commitment. + val localCommitmentKeys = LocalCommitmentKeys( + ourDelayedPaymentKey = local_delayed_payment_privkey, + theirPaymentPublicKey = remote_payment_privkey.publicKey(), + ourPaymentBasePoint = local_payment_basepoint_secret.publicKey(), + ourHtlcKey = local_htlc_privkey, + theirHtlcPublicKey = remote_htlc_privkey.publicKey(), + revocationPublicKey = local_revocation_pubkey + ) + + val funding_tx = Transaction.read( + "0200000001adbb20ea41a8423ea937e76e8151636bf6093b70eaff942930d20576600521fd000000006b48304502210090587b6201e166ad6af0227d3036a9454223d49a1f11839c1a362184340ef0240220577f7cd5cca78719405cbf1de7414ac027f0239ef6e214c90fcaab0454d84b3b012103535b32d5eb0a6ed0982a0479bbadc9868d9836f6ba94dd5a63be16d875069184ffffffff028096980000000000220020c015c4a6be010e21657068fc2e6a9d02b27ebe4d490a25846f7237f104d1a3cd20256d29010000001600143ca33c2e4446f4a305f23c80df8ad1afdcf652f900000000" + ) val commitTxInput = Transactions.InputInfo( OutPoint(funding_tx, 0), funding_tx.txOut[0], @@ -82,7 +76,6 @@ class AnchorOutputsTestsCommon { // high level tests which calls Commitments methods to generate transactions private fun runHighLevelTest(testCase: TestCase) { - val channelKeys = ChannelKeys(KeyPath.empty, { local_funding_privkey }, local_payment_basepoint_secret, local_delayed_payment_basepoint_secret, local_payment_basepoint_secret, local_payment_basepoint_secret, randomBytes32()) val localParams = LocalParams( TestConstants.Alice.nodeParams.nodeId, KeyPath.empty, @@ -104,6 +97,14 @@ class AnchorOutputsTestsCommon { PrivateKey.fromHex("444444444444444444444444444444444444444444444444444444444444444401").publicKey(), TestConstants.Bob.nodeParams.features ) + val channelParams = ChannelParams( + channelId = randomBytes32(), + channelConfig = ChannelConfig.standard, + channelFeatures = ChannelFeatures(setOf(Feature.StaticRemoteKey, Feature.AnchorOutputs)), + localParams = localParams, + remoteParams = remoteParams, + channelFlags = ChannelFlags(announceChannel = false, nonInitiatorPaysCommitFees = false) + ) val spec = CommitmentSpec( if (testCase.UseTestHtlcs) htlcs.toSet() else setOf(), FeeratePerKw(testCase.FeePerKw.sat), @@ -132,12 +133,12 @@ class AnchorOutputsTestsCommon { */ val (commitTx, htlcTxs) = Commitments.makeLocalTxs( - channelKeys, - 42, localParams, remoteParams, - fundingTxIndex = 0, + channelParams, + localCommitmentKeys, + 42, + local_funding_privkey, remote_funding_pubkey, Transactions.InputInfo(OutPoint(funding_tx, 0), funding_tx.txOut[0], Scripts.multiSig2of2(local_funding_pubkey, remote_funding_pubkey)), - local_per_commitment_point, spec ) @@ -145,8 +146,8 @@ class AnchorOutputsTestsCommon { val remoteSig = Transactions.sign(commitTx, remote_funding_privkey) val signedTx = Transactions.addSigs(commitTx, local_funding_pubkey, remote_funding_pubkey, localSig, remoteSig) assertEquals(Transaction.read(testCase.ExpectedCommitmentTxHex), signedTx.tx) - val txs = testCase.HtlcDescs.map { Transaction.read(it.ResolutionTxHex).txid to Transaction.read(it.ResolutionTxHex) }.toMap() - val remoteHtlcSigs = testCase.HtlcDescs.map { Transaction.read(it.ResolutionTxHex).txid to ByteVector(it.RemoteSigHex) }.toMap() + val txs = testCase.HtlcDescs.associate { Transaction.read(it.ResolutionTxHex).txid to Transaction.read(it.ResolutionTxHex) } + val remoteHtlcSigs = testCase.HtlcDescs.associate { Transaction.read(it.ResolutionTxHex).txid to ByteVector(it.RemoteSigHex) } assertTrue { remoteHtlcSigs.keys.containsAll(htlcTxs.map { it.tx.txid }) } htlcTxs.forEach { htlcTx -> val localHtlcSig = Transactions.sign(htlcTx, local_htlc_privkey, SigHash.SIGHASH_ALL) @@ -171,14 +172,10 @@ class AnchorOutputsTestsCommon { val outputs = Transactions.makeCommitTxOutputs( local_funding_pubkey, remote_funding_pubkey, + localCommitmentKeys.publicKeys, true, 546.sat, - local_revocation_pubkey, CltvExpiryDelta(144), - local_delayed_payment_privkey.publicKey(), - remote_payment_privkey.publicKey(), - local_htlc_privkey.publicKey(), - remote_htlc_privkey.publicKey(), spec ) val commitTx = Transactions.makeCommitTx( @@ -194,9 +191,9 @@ class AnchorOutputsTestsCommon { val signedTx = Transactions.addSigs(commitTx, local_funding_pubkey, remote_funding_pubkey, localSig, remoteSig) assertEquals(testCase.ExpectedCommitmentTx, signedTx.tx) - val txs = testCase.HtlcDescs.map { it.ResolutionTx.txid to it.ResolutionTx }.toMap() - val remoteHtlcSigs = testCase.HtlcDescs.map { it.ResolutionTx.txid to ByteVector(it.RemoteSigHex) }.toMap() - val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, 546.sat, local_revocation_pubkey, CltvExpiryDelta(144), local_delayedpubkey, spec.feerate, outputs) + val txs = testCase.HtlcDescs.associate { it.ResolutionTx.txid to it.ResolutionTx } + val remoteHtlcSigs = testCase.HtlcDescs.associate { it.ResolutionTx.txid to ByteVector(it.RemoteSigHex) } + val htlcTxs = Transactions.makeHtlcTxs(commitTx.tx, localCommitmentKeys.publicKeys, 546.sat, CltvExpiryDelta(144), spec.feerate, outputs) assertTrue { remoteHtlcSigs.keys.containsAll(htlcTxs.map { it.tx.txid }) } htlcTxs.forEach { htlcTx -> val localHtlcSig = Transactions.sign(htlcTx, local_htlc_privkey, SigHash.SIGHASH_ALL) diff --git a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt index a0411eb1d..2ff413e07 100644 --- a/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt +++ b/modules/core/src/commonTest/kotlin/fr/acinq/lightning/transactions/TransactionsTestsCommon.kt @@ -17,6 +17,8 @@ import fr.acinq.lightning.blockchain.fee.FeeratePerByte import fr.acinq.lightning.blockchain.fee.FeeratePerKw import fr.acinq.lightning.channel.Commitments import fr.acinq.lightning.channel.Helpers.Funding +import fr.acinq.lightning.crypto.LocalCommitmentKeys +import fr.acinq.lightning.crypto.RemoteCommitmentKeys import fr.acinq.lightning.io.AddLiquidityForIncomingPayment import fr.acinq.lightning.tests.TestConstants import fr.acinq.lightning.tests.utils.LightningTestSuite @@ -65,14 +67,33 @@ import kotlin.test.* class TransactionsTestsCommon : LightningTestSuite() { - private val localFundingPriv = PrivateKey(randomBytes32()) - private val remoteFundingPriv = PrivateKey(randomBytes32()) - private val localRevocationPriv = PrivateKey(randomBytes32()) - private val localPaymentPriv = PrivateKey(randomBytes32()) - private val localDelayedPaymentPriv = PrivateKey(randomBytes32()) - private val remotePaymentPriv = PrivateKey(randomBytes32()) - private val localHtlcPriv = PrivateKey(randomBytes32()) - private val remoteHtlcPriv = PrivateKey(randomBytes32()) + private val localFundingPriv = randomKey() + private val remoteFundingPriv = randomKey() + private val localRevocationPriv = randomKey() + private val localPaymentPriv = randomKey() + private val localPaymentBasePoint = randomKey().publicKey() + private val localDelayedPaymentPriv = randomKey() + private val remotePaymentPriv = randomKey() + private val localHtlcPriv = randomKey() + private val remoteHtlcPriv = randomKey() + // Keys used by the local node to spend outputs of its local commitment. + private val localKeys = LocalCommitmentKeys( + ourDelayedPaymentKey = localDelayedPaymentPriv, + theirPaymentPublicKey = remotePaymentPriv.publicKey(), + ourPaymentBasePoint = localPaymentBasePoint, + ourHtlcKey = localHtlcPriv, + theirHtlcPublicKey = remoteHtlcPriv.publicKey(), + revocationPublicKey = localRevocationPriv.publicKey(), + ) + // Keys used by the remote node to spend outputs of our local commitment. + private val remoteKeys = RemoteCommitmentKeys( + ourPaymentKey = remotePaymentPriv, + theirDelayedPaymentPublicKey = localDelayedPaymentPriv.publicKey(), + ourPaymentBasePoint = localPaymentBasePoint, + ourHtlcKey = remoteHtlcPriv, + theirHtlcPublicKey = localHtlcPriv.publicKey(), + revocationPublicKey = localRevocationPriv.publicKey(), + ) private val commitInput = Funding.makeFundingInputInfo(TxId(randomBytes32()), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) private val toLocalDelay = CltvExpiryDelta(144) private val localDustLimit = 546.sat @@ -167,20 +188,15 @@ class TransactionsTestsCommon : LightningTestSuite() { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000).msat, ByteVector32(sha256(paymentPreimage)), CltvExpiryDelta(144).toCltvExpiry(blockHeight.toLong()), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(setOf(OutgoingHtlc(htlc)), feeratePerKw, toLocal = 0.msat, toRemote = 0.msat) - val outputs = - makeCommitTxOutputs( - localFundingPriv.publicKey(), - remoteFundingPriv.publicKey(), - true, - localDustLimit, - localRevocationPriv.publicKey(), - toLocalDelay, - localDelayedPaymentPriv.publicKey(), - remotePaymentPriv.publicKey(), - localHtlcPriv.publicKey(), - remoteHtlcPriv.publicKey(), - spec - ) + val outputs = makeCommitTxOutputs( + localFundingPriv.publicKey(), + remoteFundingPriv.publicKey(), + localKeys.publicKeys, + true, + localDustLimit, + toLocalDelay, + spec + ) val commitTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = outputs.map { it.output }, lockTime = 0) val claimHtlcSuccessTx = makeClaimHtlcSuccessTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey(), localHtlcPriv.publicKey(), localRevocationPriv.publicKey(), finalPubKeyScript, htlc, feeratePerKw) @@ -196,20 +212,15 @@ class TransactionsTestsCommon : LightningTestSuite() { val paymentPreimage = randomBytes32() val htlc = UpdateAddHtlc(ByteVector32.Zeroes, 0, (20000 * 1000).msat, ByteVector32(sha256(paymentPreimage)), toLocalDelay.toCltvExpiry(blockHeight.toLong()), TestConstants.emptyOnionPacket) val spec = CommitmentSpec(setOf(IncomingHtlc(htlc)), feeratePerKw, toLocal = 0.msat, toRemote = 0.msat) - val outputs = - makeCommitTxOutputs( - localFundingPriv.publicKey(), - remoteFundingPriv.publicKey(), - true, - localDustLimit, - localRevocationPriv.publicKey(), - toLocalDelay, - localDelayedPaymentPriv.publicKey(), - remotePaymentPriv.publicKey(), - localHtlcPriv.publicKey(), - remoteHtlcPriv.publicKey(), - spec - ) + val outputs = makeCommitTxOutputs( + localFundingPriv.publicKey(), + remoteFundingPriv.publicKey(), + localKeys.publicKeys, + true, + localDustLimit, + toLocalDelay, + spec + ) val commitTx = Transaction(version = 2, txIn = listOf(TxIn(OutPoint(TxId(ByteVector32.Zeroes), 0), TxIn.SEQUENCE_FINAL)), txOut = outputs.map { it.output }, lockTime = 0) val claimHtlcTimeoutTx = makeClaimHtlcTimeoutTx(commitTx, outputs, localDustLimit, remoteHtlcPriv.publicKey(), localHtlcPriv.publicKey(), localRevocationPriv.publicKey(), finalPubKeyScript, htlc, feeratePerKw) @@ -265,14 +276,10 @@ class TransactionsTestsCommon : LightningTestSuite() { val outputs = makeCommitTxOutputs( localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), + localKeys.publicKeys, true, localDustLimit, - localRevocationPriv.publicKey(), toLocalDelay, - localDelayedPaymentPriv.publicKey(), - remotePaymentPriv.publicKey(), - localHtlcPriv.publicKey(), - remoteHtlcPriv.publicKey(), spec ) @@ -291,7 +298,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val check = ((commitTx.tx.txIn.first().sequence and 0xffffffL) shl 24) or (commitTx.tx.lockTime and 0xffffffL) assertEquals(commitTxNumber, check xor num) } - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), spec.feerate, outputs) + val htlcTxs = makeHtlcTxs(commitTx.tx, localKeys.publicKeys, localDustLimit, toLocalDelay, spec.feerate, outputs) assertEquals(4, htlcTxs.size) val htlcSuccessTxs = htlcTxs.filterIsInstance() assertEquals(2, htlcSuccessTxs.size) // htlc2 and htlc4 @@ -661,6 +668,14 @@ class TransactionsTestsCommon : LightningTestSuite() { val remotePaymentPriv = PrivateKey.fromHex("a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6a6") val localHtlcPriv = PrivateKey.fromHex("a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7a7") val remoteHtlcPriv = PrivateKey.fromHex("a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8a8") + val localKeys = LocalCommitmentKeys( + ourDelayedPaymentKey = localDelayedPaymentPriv, + theirPaymentPublicKey = remotePaymentPriv.publicKey(), + ourPaymentBasePoint = localPaymentBasePoint, + ourHtlcKey = localHtlcPriv, + theirHtlcPublicKey = remoteHtlcPriv.publicKey(), + revocationPublicKey = localRevocationPriv.publicKey(), + ) val commitInput = Funding.makeFundingInputInfo(TxId("a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0"), 0, 1.btc, localFundingPriv.publicKey(), remoteFundingPriv.publicKey()) // htlc1 and htlc2 are two regular incoming HTLCs with different amounts. @@ -690,25 +705,20 @@ class TransactionsTestsCommon : LightningTestSuite() { val commitTxNumber = 0x404142434446L val (commitTx, outputs, htlcTxs) = run { - val outputs = - makeCommitTxOutputs( - localFundingPriv.publicKey(), - remoteFundingPriv.publicKey(), - true, - localDustLimit, - localRevocationPriv.publicKey(), - toLocalDelay, - localDelayedPaymentPriv.publicKey(), - remotePaymentPriv.publicKey(), - localHtlcPriv.publicKey(), - remoteHtlcPriv.publicKey(), - spec - ) + val outputs = makeCommitTxOutputs( + localFundingPriv.publicKey(), + remoteFundingPriv.publicKey(), + localKeys.publicKeys, + true, + localDustLimit, + toLocalDelay, + spec + ) val txInfo = makeCommitTx(commitInput, commitTxNumber, localPaymentPriv.publicKey(), remotePaymentPriv.publicKey(), true, outputs) val localSig = sign(txInfo, localPaymentPriv) val remoteSig = sign(txInfo, remotePaymentPriv) val commitTx = addSigs(txInfo, localFundingPriv.publicKey(), remoteFundingPriv.publicKey(), localSig, remoteSig) - val htlcTxs = makeHtlcTxs(commitTx.tx, localDustLimit, localRevocationPriv.publicKey(), toLocalDelay, localDelayedPaymentPriv.publicKey(), feerate, outputs) + val htlcTxs = makeHtlcTxs(commitTx.tx, localKeys.publicKeys, localDustLimit, toLocalDelay, feerate, outputs) Triple(commitTx, outputs, htlcTxs) } @@ -747,10 +757,10 @@ class TransactionsTestsCommon : LightningTestSuite() { assertNotNull(closingTxs.localAndRemote) assertNotNull(closingTxs.localOnly) assertNull(closingTxs.remoteOnly) - val localAndRemote = closingTxs.localAndRemote?.toLocalOutput!! + val localAndRemote = closingTxs.localAndRemote.toLocalOutput!! assertEquals(localPubKeyScript, localAndRemote.publicKeyScript) assertEquals(145_000.sat, localAndRemote.amount) - val localOnly = closingTxs.localOnly?.toLocalOutput!! + val localOnly = closingTxs.localOnly.toLocalOutput!! assertEquals(localPubKeyScript, localOnly.publicKeyScript) assertEquals(145_000.sat, localOnly.amount) } @@ -761,10 +771,10 @@ class TransactionsTestsCommon : LightningTestSuite() { assertNotNull(closingTxs.localAndRemote) assertNotNull(closingTxs.localOnly) assertNull(closingTxs.remoteOnly) - val localAndRemote = closingTxs.localAndRemote?.toLocalOutput!! + val localAndRemote = closingTxs.localAndRemote.toLocalOutput!! assertEquals(localPubKeyScript, localAndRemote.publicKeyScript) assertEquals(150_000.sat, localAndRemote.amount) - val localOnly = closingTxs.localOnly?.toLocalOutput!! + val localOnly = closingTxs.localOnly.toLocalOutput!! assertEquals(localPubKeyScript, localOnly.publicKeyScript) assertEquals(150_000.sat, localOnly.amount) } @@ -774,8 +784,8 @@ class TransactionsTestsCommon : LightningTestSuite() { val closingTxs = makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByThem(800.sat), 0, localPubKeyScript, remotePubKeyScript) assertEquals(1, closingTxs.all.size) assertNotNull(closingTxs.localOnly) - assertEquals(1, closingTxs.localOnly!!.tx.txOut.size) - val toLocal = closingTxs.localOnly?.toLocalOutput!! + assertEquals(1, closingTxs.localOnly.tx.txOut.size) + val toLocal = closingTxs.localOnly.toLocalOutput!! assertEquals(localPubKeyScript, toLocal.publicKeyScript) assertEquals(150_000.sat, toLocal.amount) } @@ -785,7 +795,7 @@ class TransactionsTestsCommon : LightningTestSuite() { val closingTxs = makeClosingTxs(commitInput, spec, Transactions.ClosingTxFee.PaidByUs(800.sat), 0, localPubKeyScript, remotePubKeyScript) assertEquals(1, closingTxs.all.size) assertNotNull(closingTxs.remoteOnly) - assertNull(closingTxs.remoteOnly?.toLocalOutput) + assertNull(closingTxs.remoteOnly.toLocalOutput) } run { // Both outputs are trimmed: