Skip to content

Commit

Permalink
Configure different rates based on lease duration
Browse files Browse the repository at this point in the history
Those rates are sent in `node_announcement` and `init`. We select the
rate in the remote `init` message for a lease duration of `0` (no strict
lease enforcement through CLTV in the commitment transaction).

This matches the proposal in lightning/bolts#878 (comment)
  • Loading branch information
t-bast committed Dec 7, 2023
1 parent d698cd9 commit c65c4cf
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ data class DustLimitTooLarge (override val channelId: Byte
data class ToSelfDelayTooHigh (override val channelId: ByteVector32, val toSelfDelay: CltvExpiryDelta, val max: CltvExpiryDelta) : ChannelException(channelId, "unreasonable to_self_delay=$toSelfDelay (max=$max)")
data class MissingLiquidityAds (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads field is missing")
data class InvalidLiquidityAdsSig (override val channelId: ByteVector32) : ChannelException(channelId, "liquidity ads signature is invalid")
data class LiquidityRatesRejected (override val channelId: ByteVector32) : ChannelException(channelId, "rejecting liquidity ads proposed rates")
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)")
data class InvalidLiquidityRates (override val channelId: ByteVector32) : ChannelException(channelId, "rejecting liquidity ads proposed rates")
data class ChannelFundingError (override val channelId: ByteVector32) : ChannelException(channelId, "channel funding error")
data class RbfAttemptAborted (override val channelId: ByteVector32) : ChannelException(channelId, "rbf attempt aborted")
data class SpliceAborted (override val channelId: ByteVector32) : ChannelException(channelId, "splice aborted")
Expand Down
4 changes: 4 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ object Helpers {
}
}

fun makeFundingPubKeyScript(localFundingPubkey: PublicKey, remoteFundingPubkey: PublicKey): ByteVector {
return write(pay2wsh(multiSig2of2(localFundingPubkey, remoteFundingPubkey))).toByteVector()
}

fun makeFundingInputInfo(
fundingTxId: TxId,
fundingTxOutputIndex: Int,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ data class InteractiveTxParams(

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

Expand Down Expand Up @@ -835,7 +835,7 @@ data class InteractiveTxSigningSession(
val channelKeys = channelParams.localParams.channelKeys(keyManager)
val unsignedTx = sharedTx.buildUnsignedTx()
val sharedOutputIndex = unsignedTx.txOut.indexOfFirst { it.publicKeyScript == fundingParams.fundingPubkeyScript(channelKeys) }
val liquidityFees = liquidityPurchased?.fees?.toMilliSatoshi() ?: 0.msat
val liquidityFees = liquidityPurchased?.fees?.total?.toMilliSatoshi() ?: 0.msat
return Helpers.Funding.makeCommitTxsWithoutHtlcs(
channelKeys,
channelParams.channelId,
Expand Down
17 changes: 13 additions & 4 deletions src/commonMain/kotlin/fr/acinq/lightning/channel/states/Normal.kt
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,16 @@ data class Normal(
fundingContributions = FundingContributions(emptyList(), emptyList()), // as non-initiator we don't contribute to this splice for now
previousTxs = emptyList()
)
val nextState = this@Normal.copy(spliceStatus = SpliceStatus.InProgress(replyTo = null, session, localPushAmount = 0.msat, remotePushAmount = cmd.message.pushAmount, liquidityPurchased = null, origins = cmd.message.origins))
val nextState = this@Normal.copy(
spliceStatus = SpliceStatus.InProgress(
replyTo = null,
session,
localPushAmount = 0.msat,
remotePushAmount = cmd.message.pushAmount,
liquidityPurchased = null,
origins = cmd.message.origins
)
)
Pair(nextState, listOf(ChannelAction.Message.Send(spliceAck)))
} else {
logger.info { "rejecting splice attempt: channel is not idle" }
Expand All @@ -398,14 +407,14 @@ data class Normal(
is SpliceAck -> when (spliceStatus) {
is SpliceStatus.Requested -> {
logger.info { "our peer accepted our splice request and will contribute ${cmd.message.fundingContribution} to the funding transaction" }
when (val liquidityPurchased = LiquidityAds.validateLeaseRates(
when (val liquidityPurchased = LiquidityAds.validateLease(
spliceStatus.command.requestRemoteFunding,
remoteNodeId,
channelId,
cmd.message.fundingPubkey,
Helpers.Funding.makeFundingPubKeyScript(spliceStatus.spliceInit.fundingPubkey, cmd.message.fundingPubkey),
cmd.message.fundingContribution,
spliceStatus.spliceInit.feerate,
cmd.message.willFund,
spliceStatus.command.requestRemoteFunding
)) {
is Either.Left -> {
logger.error { "rejecting liquidity proposal: ${liquidityPurchased.value.message}" }
Expand Down
13 changes: 10 additions & 3 deletions src/commonMain/kotlin/fr/acinq/lightning/io/Peer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ class Peer(
val currentTipFlow = MutableStateFlow<Pair<Int, BlockHeader>?>(null)
val onChainFeeratesFlow = MutableStateFlow<OnChainFeerates?>(null)
val swapInFeeratesFlow = MutableStateFlow<FeeratePerKw?>(null)
val liquidityRatesFlow = MutableStateFlow<LiquidityAds.LeaseRate?>(null)

private val _channelLogger = nodeParams.loggerFactory.newLogger(ChannelState::class)
private suspend fun ChannelState.process(cmd: ChannelCommand): Pair<ChannelState, List<ChannelAction>> {
Expand Down Expand Up @@ -570,17 +571,23 @@ class Peer(
}
}

suspend fun purchaseInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw, maxFee: Satoshi, leaseDuration: Int): ChannelCommand.Commitment.Splice.Response? {
suspend fun estimateFeeForInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw): LiquidityAds.LeaseFees {
val leaseRate = liquidityRatesFlow.filterNotNull().first { it.leaseDuration == 0 }
return leaseRate.fees(feerate, amount, amount)
}

suspend fun requestInboundLiquidity(amount: Satoshi, feerate: FeeratePerKw): ChannelCommand.Commitment.Splice.Response? {
return channels.values
.filterIsInstance<Normal>()
.firstOrNull()
?.let { channel ->
val leaseStart = currentTipFlow.filterNotNull().first().first
val leaseRate = liquidityRatesFlow.filterNotNull().first { it.leaseDuration == 0 }
val spliceCommand = ChannelCommand.Commitment.Splice.Request(
replyTo = CompletableDeferred(),
spliceIn = null,
spliceOut = null,
requestRemoteFunding = LiquidityAds.RequestRemoteFunding(amount, maxFee, leaseStart, leaseDuration),
requestRemoteFunding = LiquidityAds.RequestRemoteFunding(amount, leaseStart, leaseRate),
feerate = feerate
)
send(WrappedChannelCommand(channel.channelId, spliceCommand))
Expand Down Expand Up @@ -840,10 +847,10 @@ class Peer(
logger.error(error) { "feature validation error" }
// TODO: disconnect peer
}

else -> {
theirInit = msg
_connectionState.value = Connection.ESTABLISHED
msg.liquidityRates.forEach { liquidityRatesFlow.emit(it) }
_channels = _channels.mapValues { entry ->
val (state1, actions) = entry.value.process(ChannelCommand.Connected(ourInit, theirInit!!))
processActions(entry.key, peerConnection, actions)
Expand Down
22 changes: 16 additions & 6 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/ChannelTlv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -67,41 +67,51 @@ sealed class ChannelTlv : Tlv {
}

/** Request inbound liquidity from our peer. */
data class RequestFunds(val amount: Satoshi, val leaseExpiry: Int, val leaseDuration: Int) : ChannelTlv() {
data class RequestFunds(val amount: Satoshi, val leaseDuration: Int, val leaseExpiry: Int) : ChannelTlv() {
override val tag: Long get() = RequestFunds.tag

override fun write(out: Output) {
LightningCodecs.writeU64(amount.toLong(), out)
LightningCodecs.writeU16(leaseDuration, out)
LightningCodecs.writeU32(leaseExpiry, out)
LightningCodecs.writeU32(leaseDuration, out)
}

companion object : TlvValueReader<RequestFunds> {
const val tag: Long = 1337

override fun read(input: Input): RequestFunds = RequestFunds(
amount = LightningCodecs.u64(input).sat,
leaseDuration = LightningCodecs.u16(input),
leaseExpiry = LightningCodecs.u32(input),
leaseDuration = LightningCodecs.u32(input),
)
}
}

/** Liquidity rates applied to an incoming [[RequestFunds]]. */
data class WillFund(val sig: ByteVector64, val leaseRates: LiquidityAds.LeaseRates) : ChannelTlv() {
data class WillFund(val sig: ByteVector64, val fundingWeight: Int, val leaseFeeProportional: Int, val leaseFeeBase: Satoshi, val maxRelayFeeProportional: Int, val maxRelayFeeBase: MilliSatoshi) : ChannelTlv() {
override val tag: Long get() = WillFund.tag

fun leaseRate(leaseDuration: Int): LiquidityAds.LeaseRate = LiquidityAds.LeaseRate(leaseDuration, fundingWeight, leaseFeeProportional, leaseFeeBase, maxRelayFeeProportional, maxRelayFeeBase)

override fun write(out: Output) {
LightningCodecs.writeBytes(sig, out)
leaseRates.write(out)
LightningCodecs.writeU16(fundingWeight, out)
LightningCodecs.writeU16(leaseFeeProportional, out)
LightningCodecs.writeU32(leaseFeeBase.sat.toInt(), out)
LightningCodecs.writeU16(maxRelayFeeProportional, out)
LightningCodecs.writeU32(maxRelayFeeBase.msat.toInt(), out)
}

companion object : TlvValueReader<WillFund> {
const val tag: Long = 1337

override fun read(input: Input): WillFund = WillFund(
sig = LightningCodecs.bytes(input, 64).toByteVector64(),
leaseRates = LiquidityAds.LeaseRates.read(input),
fundingWeight = LightningCodecs.u16(input),
leaseFeeProportional = LightningCodecs.u16(input),
leaseFeeBase = LightningCodecs.u32(input).sat,
maxRelayFeeProportional = LightningCodecs.u16(input),
maxRelayFeeBase = LightningCodecs.u32(input).msat,
)
}
}
Expand Down
19 changes: 19 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/InitTlv.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,25 @@ sealed class InitTlv : Tlv {
}
}

/** Rates at which we sell inbound liquidity to remote peers. */
data class LiquidityAdsRates(val leaseRates: List<LiquidityAds.LeaseRate>) : InitTlv() {
override val tag: Long get() = LiquidityAdsRates.tag

override fun write(out: Output) {
leaseRates.forEach { it.write(out) }
}

companion object : TlvValueReader<LiquidityAdsRates> {
const val tag: Long = 1337

override fun read(input: Input): LiquidityAdsRates {
val count = input.availableBytes / 16
val rates = (0 until count).map { LiquidityAds.LeaseRate.read(input) }
return LiquidityAdsRates(rates)
}
}
}

data class PhoenixAndroidLegacyNodeId(val legacyNodeId: PublicKey, val signature: ByteVector64) : InitTlv() {
override val tag: Long get() = PhoenixAndroidLegacyNodeId.tag

Expand Down
34 changes: 19 additions & 15 deletions src/commonMain/kotlin/fr/acinq/lightning/wire/LightningMessages.kt
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,17 @@ interface ChannelMessage

data class Init(val features: Features, val tlvs: TlvStream<InitTlv> = TlvStream.empty()) : SetupMessage {
val networks = tlvs.get<InitTlv.Networks>()?.chainHashes ?: listOf()
val liquidityRates = tlvs.get<InitTlv.LiquidityAdsRates>()?.leaseRates ?: listOf()

constructor(features: Features, chainHashs: List<ByteVector32>) : this(features, TlvStream(InitTlv.Networks(chainHashs)))
constructor(features: Features, chainHashs: List<ByteVector32>, liquidityRates: List<LiquidityAds.LeaseRate>) : this(
features,
TlvStream(
setOfNotNull(
if (chainHashs.isNotEmpty()) InitTlv.Networks(chainHashs) else null,
if (liquidityRates.isNotEmpty()) InitTlv.LiquidityAdsRates(liquidityRates) else null,
)
)
)

override val type: Long get() = Init.type

Expand All @@ -191,18 +200,19 @@ data class Init(val features: Features, val tlvs: TlvStream<InitTlv> = TlvStream
LightningCodecs.writeU16(it.size, out)
LightningCodecs.writeBytes(it, out)
}
val tlvReaders = HashMap<Long, TlvValueReader<InitTlv>>()
@Suppress("UNCHECKED_CAST")
tlvReaders[InitTlv.Networks.tag] = InitTlv.Networks.Companion as TlvValueReader<InitTlv>
@Suppress("UNCHECKED_CAST")
tlvReaders[InitTlv.PhoenixAndroidLegacyNodeId.tag] = InitTlv.PhoenixAndroidLegacyNodeId.Companion as TlvValueReader<InitTlv>
val serializer = TlvStreamSerializer(false, tlvReaders)
serializer.write(tlvs, out)
TlvStreamSerializer(false, readers).write(tlvs, out)
}

companion object : LightningMessageReader<Init> {
const val type: Long = 16

@Suppress("UNCHECKED_CAST")
val readers = mapOf(
InitTlv.Networks.tag to InitTlv.Networks.Companion as TlvValueReader<InitTlv>,
InitTlv.LiquidityAdsRates.tag to InitTlv.LiquidityAdsRates.Companion as TlvValueReader<InitTlv>,
InitTlv.PhoenixAndroidLegacyNodeId.tag to InitTlv.PhoenixAndroidLegacyNodeId.Companion as TlvValueReader<InitTlv>,
)

override fun read(input: Input): Init {
val gflen = LightningCodecs.u16(input)
val globalFeatures = LightningCodecs.bytes(input, gflen)
Expand All @@ -211,13 +221,7 @@ data class Init(val features: Features, val tlvs: TlvStream<InitTlv> = TlvStream
val len = max(gflen, lflen)
// merge features together
val features = Features(ByteVector(globalFeatures.leftPaddedCopyOf(len).or(localFeatures.leftPaddedCopyOf(len))))
val tlvReaders = HashMap<Long, TlvValueReader<InitTlv>>()
@Suppress("UNCHECKED_CAST")
tlvReaders[InitTlv.Networks.tag] = InitTlv.Networks.Companion as TlvValueReader<InitTlv>
@Suppress("UNCHECKED_CAST")
tlvReaders[InitTlv.PhoenixAndroidLegacyNodeId.tag] = InitTlv.PhoenixAndroidLegacyNodeId.Companion as TlvValueReader<InitTlv>
val serializer = TlvStreamSerializer(false, tlvReaders)
val tlvs = serializer.read(input)
val tlvs = TlvStreamSerializer(false, readers).read(input)
return Init(features, tlvs)
}
}
Expand Down
Loading

0 comments on commit c65c4cf

Please sign in to comment.