Skip to content

Commit

Permalink
Add basic liquidity purchase information to funding txs
Browse files Browse the repository at this point in the history
We record whether a liquidity purchase happened as part of every funding
transaction with the purchased amount and fees. We don't record how the
payment was made, because:

- it creates more backwards-compatibility risk if the liquidity ads spec
  changes
- it's most likely unnecessary once the purchase happened, and can only
  be stored in the liquidity DB for auditing
  • Loading branch information
t-bast committed Oct 9, 2024
1 parent 1b749e1 commit 76c0a42
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,8 @@ sealed trait LocalFundingStatus {
def signedTx_opt: Option[Transaction]
/** We store local signatures for the purpose of retransmitting if the funding/splicing flow is interrupted. */
def localSigs_opt: Option[TxSignatures]
/** Basic information about the liquidity purchase negotiated in this transaction, if any. */
def liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]
}
object LocalFundingStatus {
sealed trait NotLocked extends LocalFundingStatus
Expand All @@ -453,15 +455,16 @@ object LocalFundingStatus {
*/
case class SingleFundedUnconfirmedFundingTx(signedTx_opt: Option[Transaction]) extends UnconfirmedFundingTx with NotLocked {
override val localSigs_opt: Option[TxSignatures] = None
override val liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo] = None
}
case class DualFundedUnconfirmedFundingTx(sharedTx: SignedSharedTransaction, createdAt: BlockHeight, fundingParams: InteractiveTxParams) extends UnconfirmedFundingTx with NotLocked {
case class DualFundedUnconfirmedFundingTx(sharedTx: SignedSharedTransaction, createdAt: BlockHeight, fundingParams: InteractiveTxParams, liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends UnconfirmedFundingTx with NotLocked {
override val signedTx_opt: Option[Transaction] = sharedTx.signedTx_opt
override val localSigs_opt: Option[TxSignatures] = Some(sharedTx.localSigs)
}
case class ZeroconfPublishedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures]) extends UnconfirmedFundingTx with Locked {
case class ZeroconfPublishedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends UnconfirmedFundingTx with Locked {
override val signedTx_opt: Option[Transaction] = Some(tx)
}
case class ConfirmedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures]) extends LocalFundingStatus with Locked {
case class ConfirmedFundingTx(tx: Transaction, localSigs_opt: Option[TxSignatures], liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends LocalFundingStatus with Locked {
override val signedTx_opt: Option[Transaction] = Some(tx)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,10 @@ case class Commitments(params: ChannelParams,
def localFundingSigs(fundingTxId: TxId): Option[TxSignatures] = {
all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.localSigs_opt)
}

def liquidityPurchase(fundingTxId: TxId): Option[LiquidityAds.PurchaseBasicInfo] = {
all.find(_.fundingTxId == fundingTxId).flatMap(_.localFundingStatus.liquidityPurchase_opt)
}

/**
* Update the local/remote funding status
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1105,7 +1105,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with

case Event(msg: TxSignatures, d: DATA_NORMAL) =>
d.commitments.latest.localFundingStatus match {
case dfu@LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _) if fundingTx.txId == msg.txId =>
case dfu@LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _, _) if fundingTx.txId == msg.txId =>
// we already sent our tx_signatures
InteractiveTxSigningSession.addRemoteSigs(keyManager, d.commitments.params, dfu.fundingParams, fundingTx, msg) match {
case Left(cause) =>
Expand Down Expand Up @@ -1149,7 +1149,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
}

case Event(w: WatchPublishedTriggered, d: DATA_NORMAL) =>
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid))
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
case Right((commitments1, _)) =>
watchFundingConfirmed(w.tx.txid, Some(nodeParams.channelConf.minDepthBlocks), delay_opt = None)
Expand Down Expand Up @@ -1910,7 +1910,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case d: DATA_NORMAL => d.spliceStatus match {
case SpliceStatus.SpliceWaitingForSigs(status) => Set(ChannelReestablishTlv.NextFundingTlv(status.fundingTx.txId))
case _ => d.commitments.latest.localFundingStatus match {
case LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _) => Set(ChannelReestablishTlv.NextFundingTlv(fundingTx.txId))
case LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx: PartiallySignedSharedTransaction, _, _, _) => Set(ChannelReestablishTlv.NextFundingTlv(fundingTx.txId))
case _ => Set.empty
}
}
Expand Down Expand Up @@ -2300,11 +2300,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
// slightly before us. In that case, the WatchConfirmed may trigger first, and it would be inefficient to let the
// WatchPublished override our funding status: it will make us set a new WatchConfirmed that will instantly
// trigger and rewrite the funding status again.
val alreadyConfirmed = d.commitments.active.map(_.localFundingStatus).collect { case LocalFundingStatus.ConfirmedFundingTx(tx, _) => tx }.exists(_.txid == w.tx.txid)
val alreadyConfirmed = d.commitments.active.map(_.localFundingStatus).collect { case LocalFundingStatus.ConfirmedFundingTx(tx, _, _) => tx }.exists(_.txid == w.tx.txid)
if (alreadyConfirmed) {
stay()
} else {
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid))
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
case Right((commitments1, _)) =>
log.info(s"zero-conf funding txid=${w.tx.txid} has been published")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -712,7 +712,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {

case Event(w: WatchPublishedTriggered, d: DATA_WAIT_FOR_DUAL_FUNDING_CONFIRMED) =>
log.info("funding txid={} was successfully published for zero-conf channelId={}", w.tx.txid, d.channelId)
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid))
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
case Right((commitments1, _)) =>
// we still watch the funding tx for confirmation even if we can use the zero-conf channel right away
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ trait ChannelOpenSingleFunded extends SingleFundingHandlers with ErrorHandlers {
stay() using d.copy(deferred = Some(remoteChannelReady)) // no need to store, they will re-send if we get disconnected

case Event(w: WatchPublishedTriggered, d: DATA_WAIT_FOR_FUNDING_CONFIRMED) =>
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, None)
val fundingStatus = LocalFundingStatus.ZeroconfPublishedFundingTx(w.tx, None, None)
d.commitments.updateLocalFundingStatus(w.tx.txid, fundingStatus) match {
case Right((commitments1, _)) =>
log.info("funding txid={} was successfully published for zero-conf channelId={}", w.tx.txid, d.channelId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ trait CommonFundingHandlers extends CommonHandlers {
}
case _ => () // in the dual-funding case, we have already verified the funding tx
}
val fundingStatus = ConfirmedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid))
val fundingStatus = ConfirmedFundingTx(w.tx, d.commitments.localFundingSigs(w.tx.txid), d.commitments.liquidityPurchase(w.tx.txid))
context.system.eventStream.publish(TransactionConfirmed(d.channelId, remoteNodeId, w.tx))
// When a splice transaction confirms, it double-spends all the commitment transactions that only applied to the
// previous funding transaction. Our peer cannot publish the corresponding revoked commitments anymore, so we can
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -865,7 +865,15 @@ private class InteractiveTxBuilder(replyTo: ActorRef[InteractiveTxBuilder.Respon
)
context.system.eventStream ! EventStream.Publish(ChannelLiquidityPurchased(replyTo.toClassic, channelParams.channelId, remoteNodeId, purchase))
}
replyTo ! Succeeded(InteractiveTxSigningSession.WaitingForSigs(fundingParams, purpose.fundingTxIndex, signedTx, Left(localCommit), remoteCommit), commitSig, liquidityPurchase_opt)
val signingSession = InteractiveTxSigningSession.WaitingForSigs(
fundingParams,
purpose.fundingTxIndex,
signedTx,
Left(localCommit),
remoteCommit,
liquidityPurchase_opt.map(_.basicInfo(isBuyer = fundingParams.isInitiator))
)
replyTo ! Succeeded(signingSession, commitSig, liquidityPurchase_opt)
Behaviors.stopped
case WalletFailure(t) =>
log.error("could not sign funding transaction: ", t)
Expand Down Expand Up @@ -1050,7 +1058,8 @@ object InteractiveTxSigningSession {
fundingTxIndex: Long,
fundingTx: PartiallySignedSharedTransaction,
localCommit: Either[UnsignedLocalCommit, LocalCommit],
remoteCommit: RemoteCommit) extends InteractiveTxSigningSession {
remoteCommit: RemoteCommit,
liquidityPurchase_opt: Option[LiquidityAds.PurchaseBasicInfo]) extends InteractiveTxSigningSession {
val commitInput: InputInfo = localCommit.fold(_.commitTx.input, _.commitTxAndRemoteSig.commitTx.input)
val localCommitIndex: Long = localCommit.fold(_.index, _.index)

Expand All @@ -1061,7 +1070,7 @@ object InteractiveTxSigningSession {
val localPerCommitmentPoint = nodeParams.channelKeyManager.commitmentPoint(channelKeyPath, localCommitIndex)
LocalCommit.fromCommitSig(nodeParams.channelKeyManager, channelParams, fundingTx.txId, fundingTxIndex, fundingParams.remoteFundingPubKey, commitInput, remoteCommitSig, localCommitIndex, unsignedLocalCommit.spec, localPerCommitmentPoint).map { signedLocalCommit =>
if (shouldSignFirst(fundingParams.isInitiator, channelParams, fundingTx.tx)) {
val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, nodeParams.currentBlockHeight, fundingParams)
val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fundingTx, nodeParams.currentBlockHeight, fundingParams, liquidityPurchase_opt)
val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None)
SendingSigs(fundingStatus, commitment, fundingTx.localSigs)
} else {
Expand All @@ -1086,7 +1095,7 @@ object InteractiveTxSigningSession {
Left(f)
case Right(fullySignedTx) =>
log.info("interactive-tx fully signed with {} local inputs, {} remote inputs, {} local outputs and {} remote outputs", fullySignedTx.tx.localInputs.length, fullySignedTx.tx.remoteInputs.length, fullySignedTx.tx.localOutputs.length, fullySignedTx.tx.remoteOutputs.length)
val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, nodeParams.currentBlockHeight, fundingParams)
val fundingStatus = LocalFundingStatus.DualFundedUnconfirmedFundingTx(fullySignedTx, nodeParams.currentBlockHeight, fundingParams, liquidityPurchase_opt)
val commitment = Commitment(fundingTxIndex, remoteCommit.index, fundingParams.remoteFundingPubKey, fundingStatus, RemoteFundingStatus.NotLocked, signedLocalCommit, remoteCommit, None)
Right(SendingSigs(fundingStatus, commitment, fullySignedTx.localSigs))
}
Expand Down
Loading

0 comments on commit 76c0a42

Please sign in to comment.