Skip to content

Commit 71c6860

Browse files
Liquidity Ads (#561)
Implement a prototype for liquidity ads, compatible with ACINQ/eclair#2550 Note that we only implement the buyer side, which limits testing. The specification is available here: lightning/bolts#878 We currently don't add CLTV locks to the commitment transactions, for simplicity's sake.
1 parent 978c52b commit 71c6860

File tree

33 files changed

+837
-173
lines changed

33 files changed

+837
-173
lines changed

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelAction.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ sealed class ChannelAction {
8787
abstract val txId: TxId
8888
data class ViaSpliceOut(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId) : StoreOutgoingPayment()
8989
data class ViaSpliceCpfp(override val miningFees: Satoshi, override val txId: TxId) : StoreOutgoingPayment()
90+
data class ViaInboundLiquidityRequest(override val txId: TxId, override val miningFees: Satoshi, val lease: LiquidityAds.Lease) : StoreOutgoingPayment()
9091
data class ViaClose(val amount: Satoshi, override val miningFees: Satoshi, val address: String, override val txId: TxId, val isSentToDefaultAddress: Boolean, val closingType: ChannelClosingType) : StoreOutgoingPayment()
9192
}
9293
data class SetLocked(val txId: TxId) : Storage()

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelCommand.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import fr.acinq.lightning.utils.UUID
1313
import fr.acinq.lightning.utils.msat
1414
import fr.acinq.lightning.wire.FailureMessage
1515
import fr.acinq.lightning.wire.LightningMessage
16+
import fr.acinq.lightning.wire.LiquidityAds
1617
import fr.acinq.lightning.wire.OnionRoutingPacket
1718
import kotlinx.coroutines.CompletableDeferred
1819
import fr.acinq.lightning.wire.Init as InitMessage
@@ -83,14 +84,20 @@ sealed class ChannelCommand {
8384
data class UpdateFee(val feerate: FeeratePerKw, val commit: Boolean = false) : Commitment(), ForbiddenDuringSplice
8485
object CheckHtlcTimeout : Commitment()
8586
sealed class Splice : Commitment() {
86-
data class Request(val replyTo: CompletableDeferred<Response>, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val feerate: FeeratePerKw, val origins: List<Origin.PayToOpenOrigin> = emptyList()) : Splice() {
87+
data class Request(val replyTo: CompletableDeferred<Response>, val spliceIn: SpliceIn?, val spliceOut: SpliceOut?, val requestRemoteFunding: LiquidityAds.RequestRemoteFunding?, val feerate: FeeratePerKw, val origins: List<Origin.PayToOpenOrigin> = emptyList()) : Splice() {
8788
val pushAmount: MilliSatoshi = spliceIn?.pushAmount ?: 0.msat
8889
val spliceOutputs: List<TxOut> = spliceOut?.let { listOf(TxOut(it.amount, it.scriptPubKey)) } ?: emptyList()
8990

9091
data class SpliceIn(val walletInputs: List<WalletState.Utxo>, val pushAmount: MilliSatoshi = 0.msat)
9192
data class SpliceOut(val amount: Satoshi, val scriptPubKey: ByteVector)
9293
}
9394

95+
/**
96+
* @param miningFee on-chain fee that will be paid for the splice transaction.
97+
* @param serviceFee service-fee that will be paid to the remote node for a service they provide with the splice transaction.
98+
*/
99+
data class Fees(val miningFee: Satoshi, val serviceFee: MilliSatoshi)
100+
94101
sealed class Response {
95102
/**
96103
* This response doesn't fully guarantee that the splice will confirm, because our peer may potentially double-spend
@@ -101,14 +108,16 @@ sealed class ChannelCommand {
101108
val fundingTxIndex: Long,
102109
val fundingTxId: TxId,
103110
val capacity: Satoshi,
104-
val balance: MilliSatoshi
111+
val balance: MilliSatoshi,
112+
val liquidityLease: LiquidityAds.Lease?,
105113
) : Response()
106114

107115
sealed class Failure : Response() {
108116
object InsufficientFunds : Failure()
109117
object InvalidSpliceOutPubKeyScript : Failure()
110118
object SpliceAlreadyInProgress : Failure()
111119
object ChannelNotIdle : Failure()
120+
data class InvalidLiquidityAds(val reason: ChannelException) : Failure()
112121
data class FundingFailure(val reason: FundingContributionFailure) : Failure()
113122
object CannotStartSession : Failure()
114123
data class InteractiveTxSessionFailed(val reason: InteractiveTxSessionAction.RemoteFailure) : Failure()

src/commonMain/kotlin/fr/acinq/lightning/channel/ChannelException.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ data class MissingChannelType (override val channelId: Byte
2525
data class DustLimitTooSmall (override val channelId: ByteVector32, val dustLimit: Satoshi, val min: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is too small (min=$min)")
2626
data class DustLimitTooLarge (override val channelId: ByteVector32, val dustLimit: Satoshi, val max: Satoshi) : ChannelException(channelId, "dustLimit=$dustLimit is too large (max=$max)")
2727
data class ToSelfDelayTooHigh (override val channelId: ByteVector32, val toSelfDelay: CltvExpiryDelta, val max: CltvExpiryDelta) : ChannelException(channelId, "unreasonable to_self_delay=$toSelfDelay (max=$max)")
28+
data class MissingLiquidityAds (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads field is missing")
29+
data class InvalidLiquidityAdsSig (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads signature is invalid")
30+
data class InvalidLiquidityAdsAmount (override val channelId: ByteVector32, val proposed: Satoshi, val min: Satoshi) : ChannelException(channelId, "liquidity ads funding amount is too low (expected at least $min, got $proposed)")
31+
data class InvalidLiquidityRates (override val channelId: ByteVector32) : ChannelException(channelId, "rejecting liquidity ads proposed rates")
2832
data class ChannelFundingError (override val channelId: ByteVector32) : ChannelException(channelId, "channel funding error")
2933
data class RbfAttemptAborted (override val channelId: ByteVector32) : ChannelException(channelId, "rbf attempt aborted")
3034
data class SpliceAborted (override val channelId: ByteVector32) : ChannelException(channelId, "splice aborted")

src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ object Helpers {
257257
}
258258
}
259259

260+
fun makeFundingPubKeyScript(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey): ByteVector {
261+
return write(pay2wsh(multiSig2of2(localFundingPubkey, remoteFundingPubkey))).toByteVector()
262+
}
263+
260264
fun makeFundingInputInfo(
261265
fundingTxId: TxId,
262266
fundingTxOutputIndex: Int,

src/commonMain/kotlin/fr/acinq/lightning/channel/InteractiveTx.kt

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ data class InteractiveTxParams(
8686

8787
fun fundingPubkeyScript(channelKeys: KeyManager.ChannelKeys): ByteVector {
8888
val fundingTxIndex = (sharedInput as? SharedFundingInput.Multisig2of2)?.let { it.fundingTxIndex + 1 } ?: 0
89-
return Script.write(Script.pay2wsh(Scripts.multiSig2of2(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey))).toByteVector()
89+
return Helpers.Funding.makeFundingPubKeyScript(channelKeys.fundingPubKey(fundingTxIndex), remoteFundingPubkey)
9090
}
9191
}
9292

@@ -751,8 +751,9 @@ data class InteractiveTxSigningSession(
751751
val fundingParams: InteractiveTxParams,
752752
val fundingTxIndex: Long,
753753
val fundingTx: PartiallySignedSharedTransaction,
754+
val liquidityLease: LiquidityAds.Lease?,
754755
val localCommit: Either<UnsignedLocalCommit, LocalCommit>,
755-
val remoteCommit: RemoteCommit
756+
val remoteCommit: RemoteCommit,
756757
) {
757758

758759
// Example flow:
@@ -826,6 +827,7 @@ data class InteractiveTxSigningSession(
826827
sharedTx: SharedTransaction,
827828
localPushAmount: MilliSatoshi,
828829
remotePushAmount: MilliSatoshi,
830+
liquidityLease: LiquidityAds.Lease?,
829831
localCommitmentIndex: Long,
830832
remoteCommitmentIndex: Long,
831833
commitTxFeerate: FeeratePerKw,
@@ -834,13 +836,14 @@ data class InteractiveTxSigningSession(
834836
val channelKeys = channelParams.localParams.channelKeys(keyManager)
835837
val unsignedTx = sharedTx.buildUnsignedTx()
836838
val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) }
839+
val liquidityFees = liquidityLease?.fees?.total?.toMilliSatoshi() ?: 0.msat
837840
return Helpers.Funding.makeCommitTxsWithoutHtlcs(
838841
channelKeys,
839842
channelParams.channelId,
840843
channelParams.localParams, channelParams.remoteParams,
841844
fundingAmount = sharedTx.sharedOutput.amount,
842-
toLocal = sharedTx.sharedOutput.localAmount - localPushAmount + remotePushAmount,
843-
toRemote = sharedTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount,
845+
toLocal = sharedTx.sharedOutput.localAmount - localPushAmount + remotePushAmount - liquidityFees,
846+
toRemote = sharedTx.sharedOutput.remoteAmount - remotePushAmount + localPushAmount + liquidityFees,
844847
localCommitmentIndex = localCommitmentIndex,
845848
remoteCommitmentIndex = remoteCommitmentIndex,
846849
commitTxFeerate,
@@ -869,7 +872,7 @@ data class InteractiveTxSigningSession(
869872
val unsignedLocalCommit = UnsignedLocalCommit(localCommitmentIndex, firstCommitTx.localSpec, firstCommitTx.localCommitTx, listOf())
870873
val remoteCommit = RemoteCommit(remoteCommitmentIndex, firstCommitTx.remoteSpec, firstCommitTx.remoteCommitTx.tx.txid, remotePerCommitmentPoint)
871874
val signedFundingTx = sharedTx.sign(keyManager, fundingParams, channelParams.localParams, channelParams.remoteParams.nodeId)
872-
Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, Either.Left(unsignedLocalCommit), remoteCommit), commitSig)
875+
Pair(InteractiveTxSigningSession(fundingParams, fundingTxIndex, signedFundingTx, liquidityLease, Either.Left(unsignedLocalCommit), remoteCommit), commitSig)
873876
}
874877
}
875878

@@ -900,7 +903,14 @@ sealed class RbfStatus {
900903
sealed class SpliceStatus {
901904
object None : SpliceStatus()
902905
data class Requested(val command: ChannelCommand.Commitment.Splice.Request, val spliceInit: SpliceInit) : SpliceStatus()
903-
data class InProgress(val replyTo: CompletableDeferred<ChannelCommand.Commitment.Splice.Response>?, val spliceSession: InteractiveTxSession, val localPushAmount: MilliSatoshi, val remotePushAmount: MilliSatoshi, val origins: List<Origin.PayToOpenOrigin>) : SpliceStatus()
906+
data class InProgress(
907+
val replyTo: CompletableDeferred<ChannelCommand.Commitment.Splice.Response>?,
908+
val spliceSession: InteractiveTxSession,
909+
val localPushAmount: MilliSatoshi,
910+
val remotePushAmount: MilliSatoshi,
911+
val liquidityLease: LiquidityAds.Lease?,
912+
val origins: List<Origin.PayToOpenOrigin>
913+
) : SpliceStatus()
904914
data class WaitingForSigs(val session: InteractiveTxSigningSession, val origins: List<Origin.PayToOpenOrigin>) : SpliceStatus()
905915
object Aborted : SpliceStatus()
906916
}

src/commonMain/kotlin/fr/acinq/lightning/channel/states/LegacyWaitForFundingLocked.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ data class LegacyWaitForFundingLocked(
4747
null,
4848
null,
4949
null,
50-
SpliceStatus.None
50+
SpliceStatus.None,
51+
listOf(),
5152
)
5253
val actions = listOf(
5354
ChannelAction.Storage.StoreState(nextState),

0 commit comments

Comments
 (0)