Skip to content

Commit

Permalink
Add support for push amount with dual funding (#2433)
Browse files Browse the repository at this point in the history
When using dual funding, both sides may push an initial amount to the remote
side. This is done with an experimental tlv that can be added to `open_channel2`
and `accept_channel2`.
  • Loading branch information
t-bast authored Sep 26, 2022
1 parent 3fdad68 commit 1b0ce80
Show file tree
Hide file tree
Showing 31 changed files with 289 additions and 112 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ case class INPUT_INIT_CHANNEL_INITIATOR(temporaryChannelId: ByteVector32,
case class INPUT_INIT_CHANNEL_NON_INITIATOR(temporaryChannelId: ByteVector32,
fundingContribution_opt: Option[Satoshi],
dualFunded: Boolean,
pushAmount_opt: Option[MilliSatoshi],
localParams: LocalParams,
remote: ActorRef,
remoteInit: Init,
Expand Down Expand Up @@ -498,11 +499,15 @@ final case class DATA_WAIT_FOR_ACCEPT_DUAL_FUNDED_CHANNEL(init: INPUT_INIT_CHANN
val channelId: ByteVector32 = lastSent.temporaryChannelId
}
final case class DATA_WAIT_FOR_DUAL_FUNDING_CREATED(channelId: ByteVector32,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
txBuilder: typed.ActorRef[InteractiveTxBuilder.Command],
deferred: Option[ChannelReady]) extends TransientChannelData
final case class DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED(commitments: Commitments,
fundingTx: SignedSharedTransaction,
fundingParams: InteractiveTxParams,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
previousFundingTxs: List[DualFundingTx],
waitingSince: BlockHeight, // how long have we been waiting for a funding tx to confirm
lastChecked: BlockHeight, // last time we checked if the channel was double-spent
Expand Down
26 changes: 17 additions & 9 deletions eclair-core/src/main/scala/fr/acinq/eclair/channel/Helpers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -138,10 +138,11 @@ object Helpers {
// MUST reject the channel.
if (nodeParams.chainHash != open.chainHash) return Left(InvalidChainHash(open.temporaryChannelId, local = nodeParams.chainHash, remote = open.chainHash))

// BOLT #2: Channel funding limits
if (open.fundingAmount < nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel) || open.fundingAmount > nodeParams.channelConf.maxFundingSatoshis) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingAmount, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), nodeParams.channelConf.maxFundingSatoshis))

// BOLT #2: Channel funding limits
if (open.fundingAmount >= Channel.MAX_FUNDING && !localFeatures.hasFeature(Features.Wumbo)) return Left(InvalidFundingAmount(open.temporaryChannelId, open.fundingAmount, nodeParams.channelConf.minFundingSatoshis(open.channelFlags.announceChannel), Channel.MAX_FUNDING))
// BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000.
if (open.pushAmount > open.fundingAmount) return Left(InvalidPushAmount(open.temporaryChannelId, open.pushAmount, open.fundingAmount.toMilliSatoshi))

// BOLT #2: The receiving node MUST fail the channel if: to_self_delay is unreasonably large.
if (open.toSelfDelay > Channel.MAX_TO_SELF_DELAY || open.toSelfDelay > nodeParams.channelConf.maxToLocalDelay) return Left(ToSelfDelayTooHigh(open.temporaryChannelId, open.toSelfDelay, nodeParams.channelConf.maxToLocalDelay))
Expand Down Expand Up @@ -222,6 +223,12 @@ object Helpers {
case None => // we agree on channel type
}

// BOLT #2: Channel funding limits
if (accept.fundingAmount > nodeParams.channelConf.maxFundingSatoshis) return Left(InvalidFundingAmount(accept.temporaryChannelId, accept.fundingAmount, 0 sat, nodeParams.channelConf.maxFundingSatoshis))

// BOLT #2: The receiving node MUST fail the channel if: push_msat is greater than funding_satoshis * 1000.
if (accept.pushAmount > accept.fundingAmount) return Left(InvalidPushAmount(accept.temporaryChannelId, accept.pushAmount, accept.fundingAmount.toMilliSatoshi))

if (accept.maxAcceptedHtlcs > Channel.MAX_ACCEPTED_HTLCS) return Left(InvalidMaxAcceptedHtlcs(accept.temporaryChannelId, accept.maxAcceptedHtlcs, Channel.MAX_ACCEPTED_HTLCS))

if (accept.dustLimit < Channel.MIN_DUST_LIMIT) return Left(DustLimitTooSmall(accept.temporaryChannelId, accept.dustLimit, Channel.MIN_DUST_LIMIT))
Expand Down Expand Up @@ -402,34 +409,35 @@ object Helpers {
*/
def makeFirstCommitTxs(keyManager: ChannelKeyManager, channelConfig: ChannelConfig, channelFeatures: ChannelFeatures, temporaryChannelId: ByteVector32,
localParams: LocalParams, remoteParams: RemoteParams,
localFundingAmount: Satoshi, remoteFundingAmount: Satoshi, pushMsat: MilliSatoshi,
localFundingAmount: Satoshi, remoteFundingAmount: Satoshi,
localPushAmount: MilliSatoshi, remotePushAmount: MilliSatoshi,
commitTxFeerate: FeeratePerKw,
fundingTxHash: ByteVector32, fundingTxOutputIndex: Int,
remoteFirstPerCommitmentPoint: PublicKey): Either[ChannelException, (CommitmentSpec, CommitTx, CommitmentSpec, CommitTx)] = {
val toLocalMsat = if (localParams.isInitiator) localFundingAmount.toMilliSatoshi - pushMsat else localFundingAmount.toMilliSatoshi + pushMsat
val toRemoteMsat = if (localParams.isInitiator) remoteFundingAmount.toMilliSatoshi + pushMsat else remoteFundingAmount.toMilliSatoshi - pushMsat
val fundingAmount = localFundingAmount + remoteFundingAmount
val toLocalMsat = localFundingAmount.toMilliSatoshi - localPushAmount + remotePushAmount
val toRemoteMsat = remoteFundingAmount.toMilliSatoshi + localPushAmount - remotePushAmount

val localSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toLocalMsat, toRemote = toRemoteMsat)
val remoteSpec = CommitmentSpec(Set.empty[DirectedHtlc], commitTxFeerate, toLocal = toRemoteMsat, toRemote = toLocalMsat)

if (!localParams.isInitiator) {
// they initiated the channel open, therefore they pay the fee: we need to make sure they can afford it!
val toRemoteMsat = remoteSpec.toLocal
val fees = commitTxTotalCost(remoteParams.dustLimit, remoteSpec, channelFeatures.commitmentFormat)
val reserve = if (channelFeatures.hasFeature(Features.DualFunding)) {
((localFundingAmount + remoteFundingAmount) / 100).max(localParams.dustLimit)
(fundingAmount / 100).max(localParams.dustLimit)
} else {
localParams.requestedChannelReserve_opt.get
}
val missing = toRemoteMsat.truncateToSatoshi - reserve - fees
if (missing < Satoshi(0)) {
if (missing < 0.sat) {
return Left(CannotAffordFees(temporaryChannelId, missing = -missing, reserve = reserve, fees = fees))
}
}

val fundingPubKey = keyManager.fundingPublicKey(localParams.fundingKeyPath)
val channelKeyPath = keyManager.keyPath(localParams, channelConfig)
val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, localFundingAmount + remoteFundingAmount, fundingPubKey.publicKey, remoteParams.fundingPubKey)
val commitmentInput = makeFundingInputInfo(fundingTxHash, fundingTxOutputIndex, fundingAmount, fundingPubKey.publicKey, remoteParams.fundingPubKey)
val localPerCommitmentPoint = keyManager.commitmentPoint(channelKeyPath, 0)
val (localCommitTx, _) = Commitments.makeLocalTxs(keyManager, channelConfig, channelFeatures, 0, localParams, remoteParams, commitmentInput, localPerCommitmentPoint, localSpec)
val (remoteCommitTx, _) = Commitments.makeRemoteTxs(keyManager, channelConfig, channelFeatures, 0, localParams, remoteParams, commitmentInput, remoteFirstPerCommitmentPoint, remoteSpec)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import fr.acinq.eclair.crypto.keymanager.ChannelKeyManager
import fr.acinq.eclair.transactions.Transactions
import fr.acinq.eclair.transactions.Transactions.TxOwner
import fr.acinq.eclair.wire.protocol._
import fr.acinq.eclair.{Logs, MilliSatoshiLong, UInt64, randomKey}
import fr.acinq.eclair.{Logs, MilliSatoshi, UInt64, randomKey}
import scodec.bits.ByteVector

import scala.concurrent.{ExecutionContext, Future}
Expand Down Expand Up @@ -197,6 +197,8 @@ object InteractiveTxBuilder {
def apply(remoteNodeId: PublicKey,
fundingParams: InteractiveTxParams,
keyManager: ChannelKeyManager,
localPushAmount: MilliSatoshi,

This comment has been minimized.

Copy link
@pm47

pm47 Nov 17, 2022

Member

@t-bast Shouldn't those have been put in InteractiveTxParams?

This comment has been minimized.

Copy link
@t-bast

t-bast Nov 17, 2022

Author Member

Good question, this is subtle, but I believe it's at the right place outside of InteractiveTxParams, because push_amount only applies to the commitment transaction, not the funding transaction.

remotePushAmount: MilliSatoshi,
localParams: LocalParams,
remoteParams: RemoteParams,
commitTxFeerate: FeeratePerKw,
Expand All @@ -213,7 +215,7 @@ object InteractiveTxBuilder {
Behaviors.withMdc(Logs.mdc(remoteNodeId_opt = Some(remoteNodeId), channelId_opt = Some(fundingParams.channelId))) {
Behaviors.receiveMessagePartial {
case Start(replyTo, previousTransactions) =>
val actor = new InteractiveTxBuilder(replyTo, fundingParams, keyManager, localParams, remoteParams, commitTxFeerate, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, wallet, previousTransactions, stash, context)
val actor = new InteractiveTxBuilder(replyTo, fundingParams, keyManager, localPushAmount, remotePushAmount, localParams, remoteParams, commitTxFeerate, remoteFirstPerCommitmentPoint, channelFlags, channelConfig, channelFeatures, wallet, previousTransactions, stash, context)
actor.start()
case Abort => Behaviors.stopped
}
Expand Down Expand Up @@ -263,6 +265,8 @@ object InteractiveTxBuilder {
private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Response],
fundingParams: InteractiveTxBuilder.InteractiveTxParams,
keyManager: ChannelKeyManager,
localPushAmount: MilliSatoshi,
remotePushAmount: MilliSatoshi,
localParams: LocalParams,
remoteParams: RemoteParams,
commitTxFeerate: FeeratePerKw,
Expand Down Expand Up @@ -701,7 +705,7 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon

def signCommitTx(completeTx: SharedTransaction, fundingOutputIndex: Int): Behavior[Command] = {
val fundingTx = completeTx.buildUnsignedTx()
Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, fundingParams.channelId, localParams, remoteParams, fundingParams.localAmount, fundingParams.remoteAmount, 0 msat, commitTxFeerate, fundingTx.hash, fundingOutputIndex, remoteFirstPerCommitmentPoint) match {
Funding.makeFirstCommitTxs(keyManager, channelConfig, channelFeatures, fundingParams.channelId, localParams, remoteParams, fundingParams.localAmount, fundingParams.remoteAmount, localPushAmount, remotePushAmount, commitTxFeerate, fundingTx.hash, fundingOutputIndex, remoteFirstPerCommitmentPoint) match {
case Left(cause) =>
replyTo ! RemoteFailure(cause)
unlockAndStop(completeTx)
Expand Down
Loading

0 comments on commit 1b0ce80

Please sign in to comment.