Skip to content

Commit

Permalink
Store liquidity purchase as outgoing payment
Browse files Browse the repository at this point in the history
We store liquidity purchases as on-chain outgoing payments, like what we
do for splice-outs and splice-cpfp.
  • Loading branch information
t-bast committed Dec 8, 2023
1 parent c65c4cf commit 91d6c2c
Show file tree
Hide file tree
Showing 8 changed files with 179 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ sealed class ChannelAction {
abstract val txId: TxId
data class ViaSpliceOut(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId) : StoreOutgoingPayment()
data class ViaSpliceCpfp(override val miningFees: Satoshi, override val txId: TxId) : StoreOutgoingPayment()
data class ViaInboundLiquidityRequest(override val txId: TxId, val lease: LiquidityAds.Lease) : StoreOutgoingPayment() {
override val miningFees: Satoshi = lease.fees.miningFee
}
data class ViaClose(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId, val isSentToDefaultAddress: Boolean, val closingType: ChannelClosingType) : StoreOutgoingPayment()
}
data class SetLocked(val txId: TxId) : Storage()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -751,8 +751,9 @@ data class InteractiveTxSigningSession(
val fundingParams: InteractiveTxParams,
val fundingTxIndex: Long,
val fundingTx: PartiallySignedSharedTransaction,
val liquidityPurchased: LiquidityAds.Lease?,
val localCommit: Either<UnsignedLocalCommit, LocalCommit>,
val remoteCommit: RemoteCommit
val remoteCommit: RemoteCommit,
) {

// Example flow:
Expand Down Expand Up @@ -871,7 +872,7 @@ data class InteractiveTxSigningSession(
val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf())
val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint)
val signedFundingTx = sharedTx.sign(keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId)
Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig)
Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityPurchased, Either.Left(unsignedLocalCommit), remoteCommit), commitSig)
}
}

Expand Down
18 changes: 13 additions & 5 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ data class Normal(
logger.info { "waiting for tx_sigs" }
Pair(this@Normal.copy(spliceStatus = spliceStatus.copy(session = signingSession1)), listOf())
}
is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, cmd.message.channelData)
is InteractiveTxSigningSessionAction.SendTxSigs -> sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityPurchased, cmd.message.channelData)
}
}
ignoreRetransmittedCommitSig(cmd.message) -> {
Expand Down Expand Up @@ -565,7 +565,7 @@ data class Normal(
}
is Either.Right -> {
val action: InteractiveTxSigningSessionAction.SendTxSigs = res.value
sendSpliceTxSigs(spliceStatus.origins, action, cmd.message.channelData)
sendSpliceTxSigs(spliceStatus.origins, action, spliceStatus.session.liquidityPurchased, cmd.message.channelData)
}
}
}
Expand Down Expand Up @@ -712,7 +712,12 @@ data class Normal(
}
}

private fun ChannelContext.sendSpliceTxSigs(origins: List<Origin.PayToOpenOrigin>, action: InteractiveTxSigningSessionAction.SendTxSigs, remoteChannelData: EncryptedChannelData): Pair<Normal, List<ChannelAction>> {
private fun ChannelContext.sendSpliceTxSigs(
origins: List<Origin.PayToOpenOrigin>,
action: InteractiveTxSigningSessionAction.SendTxSigs,
liquidityPurchase: LiquidityAds.Lease?,
remoteChannelData: EncryptedChannelData
): Pair<Normal, List<ChannelAction>> {
logger.info { "sending tx_sigs" }
// We watch for confirmation in all cases, to allow pruning outdated commitments when transactions confirm.
val fundingMinDepth = Helpers.minDepthForFunding(staticParams.nodeParams, action.fundingTx.fundingParams.fundingAmount)
Expand Down Expand Up @@ -754,13 +759,16 @@ data class Normal(
txId = action.fundingTx.txId
)
})
// If we initiated the splice but there are no new inputs or outputs, it's a cpfp
if (action.fundingTx.fundingParams.isInitiator && action.fundingTx.sharedTx.tx.localInputs.isEmpty() && action.fundingTx.fundingParams.localOutputs.isEmpty()) add(
// If we initiated the splice but there are no new inputs on either side and no new output on our side, it's a cpfp
if (action.fundingTx.fundingParams.isInitiator && action.fundingTx.sharedTx.tx.localInputs.isEmpty() && action.fundingTx.sharedTx.tx.remoteInputs.isEmpty() && action.fundingTx.fundingParams.localOutputs.isEmpty()) add(
ChannelAction.Storage.StoreOutgoingPayment.ViaSpliceCpfp(
miningFees = action.fundingTx.sharedTx.tx.fees,
txId = action.fundingTx.txId
)
)
liquidityPurchase?.let {
add(ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest(txId = action.fundingTx.txId, lease = it))
}
if (staticParams.useZeroConf) {
logger.info { "channel is using 0-conf, sending splice_locked right away" }
val spliceLocked = SpliceLocked(channelId, action.fundingTx.txId)
Expand Down
16 changes: 16 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/db/PaymentsDb.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import fr.acinq.lightning.payment.FinalFailure
import fr.acinq.lightning.payment.PaymentRequest
import fr.acinq.lightning.utils.*
import fr.acinq.lightning.wire.FailureMessage
import fr.acinq.lightning.wire.LiquidityAds

interface PaymentsDb : IncomingPaymentsDb, OutgoingPaymentsDb {
/**
Expand Down Expand Up @@ -387,6 +388,21 @@ data class SpliceCpfpOutgoingPayment(
override val completedAt: Long? = confirmedAt
}

data class InboundLiquidityOutgoingPayment(
override val id: UUID,
override val channelId: ByteVector32,
override val txId: TxId,
val lease: LiquidityAds.Lease,
override val createdAt: Long,
override val confirmedAt: Long?,
override val lockedAt: Long?,
) : OnChainOutgoingPayment() {
override val amount: MilliSatoshi = lease.fees.total.toMilliSatoshi()
override val miningFees: Satoshi = lease.fees.miningFee
override val fees: MilliSatoshi = lease.fees.total.toMilliSatoshi()
override val completedAt: Long? = confirmedAt
}

enum class ChannelClosingType {
Mutual, Local, Remote, Revoked, Other;
}
Expand Down
10 changes: 10 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,16 @@ class Peer(
confirmedAt = null,
lockedAt = null
)
is ChannelAction.Storage.StoreOutgoingPayment.ViaInboundLiquidityRequest ->
InboundLiquidityOutgoingPayment(
id = UUID.randomUUID(),
channelId = channelId,
txId = action.txId,
lease = action.lease,
createdAt = currentTimestampMillis(),
confirmedAt = null,
lockedAt = null
)
is ChannelAction.Storage.StoreOutgoingPayment.ViaClose ->
ChannelCloseOutgoingPayment(
id = UUID.randomUUID(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fr.acinq.lightning.serialization.v4
import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.io.ByteArrayInput
import fr.acinq.bitcoin.io.Input
import fr.acinq.bitcoin.io.readNBytes
import fr.acinq.lightning.CltvExpiryDelta
import fr.acinq.lightning.Features
import fr.acinq.lightning.ShortChannelId
Expand Down Expand Up @@ -307,43 +308,60 @@ object Deserialization {
else -> error("unknown discriminator $discriminator for class ${SignedSharedTransaction::class}")
}

private fun Input.readInteractiveTxSigningSession(): InteractiveTxSigningSession = InteractiveTxSigningSession(
fundingParams = readInteractiveTxParams(),
fundingTxIndex = readNumber(),
fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction,
localCommit = readEither(
readLeft = {
InteractiveTxSigningSession.Companion.UnsignedLocalCommit(
index = readNumber(),
spec = readCommitmentSpecWithHtlcs(),
commitTx = readTransactionWithInputInfo() as CommitTx,
htlcTxs = readCollection { readTransactionWithInputInfo() as HtlcTx }.toList(),
)
},
readRight = {
LocalCommit(
index = readNumber(),
spec = readCommitmentSpecWithHtlcs(),
publishableTxs = PublishableTxs(
commitTx = readTransactionWithInputInfo() as CommitTx,
htlcTxsAndSigs = readCollection {
HtlcTxAndSigs(
txinfo = readTransactionWithInputInfo() as HtlcTx,
localSig = readByteVector64(),
remoteSig = readByteVector64()
)
}.toList()
)
private fun Input.readUnsignedLocalCommitWithHtlcs(): InteractiveTxSigningSession.Companion.UnsignedLocalCommit = InteractiveTxSigningSession.Companion.UnsignedLocalCommit(
index = readNumber(),
spec = readCommitmentSpecWithHtlcs(),
commitTx = readTransactionWithInputInfo() as CommitTx,
htlcTxs = readCollection { readTransactionWithInputInfo() as HtlcTx }.toList(),
)

private fun Input.readLocalCommitWithHtlcs(): LocalCommit = LocalCommit(
index = readNumber(),
spec = readCommitmentSpecWithHtlcs(),
publishableTxs = PublishableTxs(
commitTx = readTransactionWithInputInfo() as CommitTx,
htlcTxsAndSigs = readCollection {
HtlcTxAndSigs(
txinfo = readTransactionWithInputInfo() as HtlcTx,
localSig = readByteVector64(),
remoteSig = readByteVector64()
)
},
}.toList()
)
)

private fun Input.readLiquidityPurchase(): LiquidityAds.Lease = LiquidityAds.Lease(
amount = readNumber().sat,
fees = LiquidityAds.LeaseFees(miningFee = readNumber().sat, serviceFee = readNumber().sat),
sellerSig = readByteVector64(),
witness = LiquidityAds.LeaseWitness(
fundingScript = readNBytes(readNumber().toInt())!!.toByteVector(),
leaseDuration = readNumber().toInt(),
leaseEnd = readNumber().toInt(),
maxRelayFeeProportional = readNumber().toInt(),
maxRelayFeeBase = readNumber().msat,
),
remoteCommit = RemoteCommit(
)

private fun Input.readInteractiveTxSigningSession(): InteractiveTxSigningSession {
val fundingParams = readInteractiveTxParams()
val fundingTxIndex = readNumber()
val fundingTx = readSignedSharedTransaction() as PartiallySignedSharedTransaction
val (liquidityPurchase, localCommit) = when (val discriminator = read()) {
0 -> Pair(null, Either.Left(readUnsignedLocalCommitWithHtlcs()))
1 -> Pair(null, Either.Right(readLocalCommitWithHtlcs()))
2 -> Pair(readLiquidityPurchase(), Either.Left(readUnsignedLocalCommitWithHtlcs()))
3 -> Pair(readLiquidityPurchase(), Either.Right(readLocalCommitWithHtlcs()))
else -> error("unknown discriminator $discriminator for class ${InteractiveTxSigningSession::class}")
}
val remoteCommit = RemoteCommit(
index = readNumber(),
spec = readCommitmentSpecWithHtlcs(),
txid = readTxId(),
remotePerCommitmentPoint = readPublicKey()
)
)
return InteractiveTxSigningSession(fundingParams, fundingTxIndex, fundingTx, liquidityPurchase, localCommit, remoteCommit)
}

private fun Input.readChannelOrigin(): Origin = when (val discriminator = read()) {
0x01 -> Origin.PayToOpenOrigin(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import fr.acinq.lightning.transactions.Transactions.TransactionWithInputInfo.*
import fr.acinq.lightning.utils.Either
import fr.acinq.lightning.wire.LightningCodecs
import fr.acinq.lightning.wire.LightningMessage
import fr.acinq.lightning.wire.LiquidityAds

/**
* Serialization for [ChannelStateWithCommitments].
Expand Down Expand Up @@ -356,31 +357,68 @@ object Serialization {
}
}

private fun Output.writeUnsignedLocalCommitWithHtlcs(localCommit: InteractiveTxSigningSession.Companion.UnsignedLocalCommit) {
writeNumber(localCommit.index)
writeCommitmentSpecWithHtlcs(localCommit.spec)
writeTransactionWithInputInfo(localCommit.commitTx)
writeCollection(localCommit.htlcTxs) { writeTransactionWithInputInfo(it) }
}

private fun Output.writeLocalCommitWithHtlcs(localCommit: LocalCommit) {
writeNumber(localCommit.index)
writeCommitmentSpecWithHtlcs(localCommit.spec)
localCommit.publishableTxs.run {
writeTransactionWithInputInfo(commitTx)
writeCollection(htlcTxsAndSigs) { htlc ->
writeTransactionWithInputInfo(htlc.txinfo)
writeByteVector64(htlc.localSig)
writeByteVector64(htlc.remoteSig)
}
}
}

private fun Output.writeLiquidityPurchase(lease: LiquidityAds.Lease) {
writeNumber(lease.amount.toLong())
writeNumber(lease.fees.miningFee.toLong())
writeNumber(lease.fees.serviceFee.toLong())
writeByteVector64(lease.sellerSig)
writeNumber(lease.witness.fundingScript.size())
write(lease.witness.fundingScript.toByteArray())
writeNumber(lease.witness.leaseDuration)
writeNumber(lease.witness.leaseEnd)
writeNumber(lease.witness.maxRelayFeeProportional)
writeNumber(lease.witness.maxRelayFeeBase.toLong())
}

private fun Output.writeInteractiveTxSigningSession(s: InteractiveTxSigningSession) = s.run {
writeInteractiveTxParams(fundingParams)
writeNumber(s.fundingTxIndex)
writeSignedSharedTransaction(fundingTx)
// We don't bother removing the duplication across HTLCs: this is a short-lived state during which the channel cannot be used for payments.
writeEither(localCommit,
writeLeft = { localCommit ->
writeNumber(localCommit.index)
writeCommitmentSpecWithHtlcs(localCommit.spec)
writeTransactionWithInputInfo(localCommit.commitTx)
writeCollection(localCommit.htlcTxs) { writeTransactionWithInputInfo(it) }
},
writeRight = { localCommit ->
writeNumber(localCommit.index)
writeCommitmentSpecWithHtlcs(localCommit.spec)
localCommit.publishableTxs.run {
writeTransactionWithInputInfo(commitTx)
writeCollection(htlcTxsAndSigs) { htlc ->
writeTransactionWithInputInfo(htlc.txinfo)
writeByteVector64(htlc.localSig)
writeByteVector64(htlc.remoteSig)
}
when (liquidityPurchased) {
null -> when (localCommit) {
is Either.Left -> {
writeNumber(0)
writeUnsignedLocalCommitWithHtlcs(localCommit.value)
}
is Either.Right -> {
writeNumber(1)
writeLocalCommitWithHtlcs(localCommit.value)
}
}
)
else -> when (localCommit) {
is Either.Left -> {
writeNumber(2)
writeLiquidityPurchase(liquidityPurchased)
writeUnsignedLocalCommitWithHtlcs(localCommit.value)
}
is Either.Right -> {
writeNumber(3)
writeLiquidityPurchase(liquidityPurchased)
writeLocalCommitWithHtlcs(localCommit.value)
}
}
}
remoteCommit.run {
writeNumber(index)
writeCommitmentSpecWithHtlcs(spec)
Expand Down
Loading

0 comments on commit 91d6c2c

Please sign in to comment.