Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for using last_funding_locked tlv in channel_reestablish #3007

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1270,8 +1270,6 @@ case class Commitments(params: ChannelParams,
// This ensures that we only have to send splice_locked for the latest commitment instead of sending it for every commitment.
// A side-effect is that previous commitments that are implicitly locked don't necessarily have their status correctly set.
// That's why we look at locked commitments separately and then select the one with the oldest fundingTxIndex.
val lastLocalLocked_opt = active.find(_.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked])
val lastRemoteLocked_opt = active.find(_.remoteFundingStatus == RemoteFundingStatus.Locked)
val lastLocked_opt = (lastLocalLocked_opt, lastRemoteLocked_opt) match {
// We select the locked commitment with the smaller value for fundingTxIndex, but both have to be defined.
// If both have the same fundingTxIndex, they must actually be the same commitment, because:
Expand All @@ -1280,13 +1278,13 @@ case class Commitments(params: ChannelParams,
// - we don't allow creating a splice on top of an unconfirmed transaction that has RBF attempts (because it
// would become invalid if another of the RBF attempts end up being confirmed)
case (Some(lastLocalLocked), Some(lastRemoteLocked)) => Some(Seq(lastLocalLocked, lastRemoteLocked).minBy(_.fundingTxIndex))
// Special case for the initial funding tx, we only require a local lock because channel_ready doesn't explicitly reference a funding tx.
// Special case for the initial funding tx, we only require a local lock because our peer may have never sent channel_ready.
case (Some(lastLocalLocked), None) if lastLocalLocked.fundingTxIndex == 0 => Some(lastLocalLocked)
case _ => None
}
lastLocked_opt match {
case Some(lastLocked) =>
// all commitments older than this one are inactive
// All commitments older than this one, and RBF alternatives, become inactive.
val inactive1 = active.filter(c => c.fundingTxId != lastLocked.fundingTxId && c.fundingTxIndex <= lastLocked.fundingTxIndex)
inactive1.foreach(c => log.info("deactivating commitment fundingTxIndex={} fundingTxId={}", c.fundingTxIndex, c.fundingTxId))
copy(
Expand Down Expand Up @@ -1347,6 +1345,9 @@ case class Commitments(params: ChannelParams,
def resolveCommitment(shortChannelId: RealShortChannelId): Option[Commitment] = {
all.find(c => c.shortChannelId_opt.contains(shortChannelId))
}

val lastLocalLocked_opt: Option[Commitment] = active.filter(_.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked]).sortBy(_.fundingTxIndex).lastOption
val lastRemoteLocked_opt: Option[Commitment] = active.filter(c => c.remoteFundingStatus == RemoteFundingStatus.Locked).sortBy(_.fundingTxIndex).lastOption
Comment on lines +1349 to +1350
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move this with the other vals at the beginning of the class definition?

}

object Commitments {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,8 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
var announcementSigsStash = Map.empty[RealShortChannelId, AnnouncementSignatures]
// we record the announcement_signatures messages we already sent to avoid unnecessary retransmission
var announcementSigsSent = Set.empty[RealShortChannelId]
// we keep track of the splice_locked we sent after channel_reestablish to avoid sending it again
private var spliceLockedSentAfterReestablish: Option[RealShortChannelId] = None
Comment on lines +225 to +226
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will need a Map of the splice_locked messages we sent to keep track of our local nonces for taproot, so this is a good opportunity to do it right now, otherwise we'll need to change this code anyway...

Can you turn this into private var spliceLockedSent = Map.empty[RealShortChannelId, SpliceLocked]? And implement a mecanism to prune the oldest RealShortChannelIds when we exceed 10 elements, like we do for announcement signatures?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use TxId as the key instead of RealShortChannelId because we might have sent SpliceLocked for a zero conf channel?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, that's probably what we need here!


private def trimAnnouncementSigsStashIfNeeded(): Unit = {
if (announcementSigsStash.size >= 10) {
Expand Down Expand Up @@ -775,10 +777,14 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with

case Event(c: CurrentFeerates.BitcoinCore, d: DATA_NORMAL) => handleCurrentFeerate(c, d)

case Event(_: ChannelReady, _: DATA_NORMAL) =>
// This happens on reconnection, because channel_ready is sent again if the channel hasn't been used yet,
// otherwise we cannot be sure that it was correctly received before disconnecting.
stay()
case Event(_: ChannelReady, d: DATA_NORMAL) =>
// After a reconnection, if the channel hasn't been used yet, our peer cannot be sure we received their channel_ready
// so they will resend it. Their remote funding status must also be set to Locked if it wasn't already.
// NB: Their remote funding status will be stored when the commitment is next updated, or channel_ready will
// be sent again if a reconnection occurs first.
stay() using d.copy(commitments = d.commitments.copy(active = d.commitments.active.collect {
case c if c.fundingTxIndex == 0 => c.copy(remoteFundingStatus = RemoteFundingStatus.Locked)
}))
Comment on lines +785 to +787
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point using collect is really dangerous: it will drop every other commitment (e.g. pending splices)! You must use map here to ensure you don't modify the other commitments:

Suggested change
stay() using d.copy(commitments = d.commitments.copy(active = d.commitments.active.collect {
case c if c.fundingTxIndex == 0 => c.copy(remoteFundingStatus = RemoteFundingStatus.Locked)
}))
stay() using d.copy(commitments = d.commitments.copy(active = d.commitments.active.map {
case c if c.fundingTxIndex == 0 => c.copy(remoteFundingStatus = RemoteFundingStatus.Locked)
case c => c
}))


// Channels are publicly announced if both parties want it: we ignore this message if we don't want to announce the channel.
case Event(remoteAnnSigs: AnnouncementSignatures, d: DATA_NORMAL) if d.commitments.announceChannel =>
Expand Down Expand Up @@ -1379,6 +1385,18 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case Event(msg: SpliceLocked, d: DATA_NORMAL) =>
d.commitments.updateRemoteFundingStatus(msg.fundingTxId, d.lastAnnouncedFundingTxId_opt) match {
case Right((commitments1, commitment)) =>
// If we already have a signed channel announcement for this commitment, then we are receiving splice_locked
// again after a reconnection and must retransmit our splice_locked and new announcement_signatures. Nodes
// retransmit splice_locked after a reconnection when they have received splice_locked but NOT matching signatures
// before the last disconnect. If a matching splice_locked has already been sent since reconnecting, then do not
// retransmit splice_locked to avoid a loop.
// NB: It is important both nodes retransmit splice_locked after reconnecting to ensure new Taproot nonces
// are exchanged for channel announcements.
val spliceLocked_opt = d.lastAnnouncement_opt.collect {
case ann if !spliceLockedSentAfterReestablish.contains(ann.shortChannelId) && commitment.shortChannelId_opt.contains(ann.shortChannelId) =>
spliceLockedSentAfterReestablish = Some(ann.shortChannelId)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this is necessary? Can splice_locked be sent again if it has already been sent? I thought it was safer to set spliceLockedSentAfterReestablish but we could take it out and the loop after channel_reestablish will still not occur.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can splice_locked be sent again if it has already been sent?

Yes it can, this is harmless. I think we should simplify this once you've introduced the spliceLockedSent map as requested in my other comment to do the following when we receive splice_locked:

  • check the spliceLockedSent map: if it contains this scid, we don't need to send anything (we may have already re-sent splice_locked on reestablish)
  • otherwise, check if the local funding status is Locked, and if it is, retransmit our splice_locked
  • that means we may unnecessarily retransmit (if their channel_reestablish said that they already received it), but it's harmless, simplifies the logic (no need to look at the announcement at all), and shouldn't happen often

SpliceLocked(d.channelId, msg.fundingTxId)
}
// If the commitment is confirmed, we were waiting to receive the remote splice_locked before sending our announcement_signatures.
val localAnnSigs_opt = if (d.commitments.announceChannel) commitment.signAnnouncement(nodeParams, commitments1.params) else None
localAnnSigs_opt match {
Expand All @@ -1391,7 +1409,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
}
maybeEmitEventsPostSplice(d.aliases, d.commitments, commitments1, d.lastAnnouncement_opt)
maybeUpdateMaxHtlcAmount(d.channelUpdate.htlcMaximumMsat, commitments1)
stay() using d.copy(commitments = commitments1) storing() sending localAnnSigs_opt.toSeq
stay() using d.copy(commitments = commitments1) storing() sending spliceLocked_opt.toSeq ++ localAnnSigs_opt.toSeq
case Left(_) => stay()
}

Expand Down Expand Up @@ -2227,13 +2245,19 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
}
case _ => Set.empty
}
val lastFundingLockedTlvs: Set[ChannelReestablishTlv] =
if (d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype) && d.commitments.params.localParams.initFeatures.hasFeature(Features.SplicePrototype)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to gate this on init features, we're using odd TLVs which should be ignored by nodes that don't understand them. It's simpler to always send those TLVs and they may be used outside of splicing when lnd folks for taproot channel announcements (which will potentially require channel_ready retransmission).

It's only helpful to look at init features when we receive channel_reestablish, because if our peer supports splicing and does NOT set your_last_funding_locked that means we MUST retransmit channel_ready or splice_locked, regardless of other checks around the commitment_number.

By the way, we are missing channel_ready retransmission based on those TLVs: the condition line 2350 must be updated to always retransmit channel_ready if d.commitments.params.remoteParams.initFeatures.hasFeature(Features.SplicePrototype) && channelReestablish.yourLastFundingLocked_opt.isEmpty.

d.commitments.lastLocalLocked_opt.map(c => ChannelReestablishTlv.MyCurrentFundingLockedTlv(c.fundingTxId)).toSet ++
d.commitments.lastRemoteLocked_opt.map(c => ChannelReestablishTlv.YourLastFundingLockedTlv(c.fundingTxId)).toSet
} else Set.empty

val channelReestablish = ChannelReestablish(
channelId = d.channelId,
nextLocalCommitmentNumber = d.commitments.localCommitIndex + 1,
nextRemoteRevocationNumber = d.commitments.remoteCommitIndex,
yourLastPerCommitmentSecret = PrivateKey(yourLastPerCommitmentSecret),
myCurrentPerCommitmentPoint = myCurrentPerCommitmentPoint,
tlvStream = TlvStream(rbfTlv)
tlvStream = TlvStream(rbfTlv ++ lastFundingLockedTlvs)
)
// we update local/remote connection-local global/local features, we don't persist it right now
val d1 = Helpers.updateFeatures(d, localInit, remoteInit)
Expand Down Expand Up @@ -2371,34 +2395,46 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
case None => d.spliceStatus
}

// re-send splice_locked (must come *after* potentially retransmitting tx_signatures)
// NB: there is a key difference between channel_ready and splice_confirmed:
// Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding
// transaction confirmed by our counterparty; we either missed their splice_locked or it confirmed while disconnected.
val commitments1: Commitments = channelReestablish.myCurrentFundingLocked_opt.flatMap(myCurrentFundingLocked =>
d.commitments.updateRemoteFundingStatus(myCurrentFundingLocked, d.lastAnnouncedFundingTxId_opt).toOption.map(_._1))
.getOrElse(d.commitments)
Comment on lines +2398 to +2402
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find it dangerous to have multiple places where we update the commitments and rely on numbering variable names, it requires scanning the whole block of code to ensure we're using the right version...can you instead move this change below, where we do the discardUnsignedUpdates, and chain the two updates like this:

// Prune previous funding transactions and RBF attempts if we already sent splice_locked for the last funding
// transaction that is also locked by our counterparty; we either missed their splice_locked or it confirmed
// while disconnected.
val commitments1: Commitments = channelReestablish.myCurrentFundingLocked_opt
  .flatMap(remoteFundingTxLocked => d.commitments.updateRemoteFundingStatus(remoteFundingTxLocked, d.lastAnnouncedFundingTxId_opt).toOption.map(_._1))
  .getOrElse(d.commitments)
  // We then clean up unsigned updates that haven't been received before the disconnection.
  .discardUnsignedUpdates()


// Retransmit splice_locked (must come *after* potentially retransmitting tx_signatures):
// 1) If they did not receive our last splice_locked;
// 2) or the last splice_locked they received is different from what we sent;
// 3) or (public channels only) we have not received their announcement_signatures for our latest locked commitment.
Comment on lines +2405 to +2407
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite confusing, because the second statement is contained in the first one: if the last splice_locked they received doesn't match what we sent, it means they did not receive our last splice_locked?

Suggested change
// 1) If they did not receive our last splice_locked;
// 2) or the last splice_locked they received is different from what we sent;
// 3) or (public channels only) we have not received their announcement_signatures for our latest locked commitment.
// 1) If they did not receive our last splice_locked
// 2) or if this is a public channel and we have not created the channel_announcement yet

// NB: there is a key difference between channel_ready and splice_locked:
// - channel_ready: a non-zero commitment index implies that both sides have seen the channel_ready
// - splice_confirmed: the commitment index can be updated as long as it is compatible with all splices, so
// we must keep sending our most recent splice_locked at each reconnection
val spliceLocked = d.commitments.active
.filter(c => c.fundingTxIndex > 0) // only consider splice txs
.collectFirst { case c if c.localFundingStatus.isInstanceOf[LocalFundingStatus.Locked] =>
// - splice_locked: the commitment index can be updated as long as it is compatible with all splices
// We must send our most recent splice_locked until our counterparty receives it and, for a public
// channel, also sends their announcement signatures.
val spliceLocked = commitments1.lastLocalLocked_opt
.filter(_.fundingTxIndex > 0) // only consider splice txs
.collect { case c if !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId) ||
(commitments1.announceChannel && d.lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId))) =>
Comment on lines +2415 to +2416
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those conditions are quite hard to parse, it's helpful to decompose them with comments:

          val spliceLocked = commitments1.lastLocalLocked_opt match {
            case None => None
            // We only send splice_locked for splice transactions.
            case Some(c) if c.fundingTxIndex == 0 => None
            case Some(c) =>
              // If our peer has not received our splice_locked, we retransmit it.
              val notReceivedByRemote = !channelReestablish.yourLastFundingLocked_opt.contains(c.fundingTxId)
              // If this is a public channel and we haven't announced the splice, we retransmit our splice_locked and
              // will exchange announcement_signatures afterwards.
              val notAnnouncedYet = commitments1.announceChannel && d.lastAnnouncement_opt.forall(ann => !c.shortChannelId_opt.contains(ann.shortChannelId))
              if (notReceivedByRemote || notAnnouncedYet) {
                log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId)
                spliceLockedSentAfterReestablish = c.shortChannelId_opt
                Some(SpliceLocked(d.channelId, c.fundingTxId))
              } else {
                None
              }
          }

log.debug("re-sending splice_locked for fundingTxId={}", c.fundingTxId)
spliceLockedSentAfterReestablish = c.shortChannelId_opt
SpliceLocked(d.channelId, c.fundingTxId)
}
}
sendQueue = sendQueue ++ spliceLocked

// we may need to retransmit updates and/or commit_sig and/or revocation
sendQueue = sendQueue ++ syncSuccess.retransmit

// then we clean up unsigned updates
val commitments1 = d.commitments.discardUnsignedUpdates()
val commitments2 = commitments1.discardUnsignedUpdates()

commitments1.remoteNextCommitInfo match {
commitments2.remoteNextCommitInfo match {
case Left(_) =>
// we expect them to (re-)send the revocation immediately
startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments1.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout)
startSingleTimer(RevocationTimeout.toString, RevocationTimeout(commitments2.remoteCommitIndex, peer), nodeParams.channelConf.revocationTimeout)
case _ => ()
}

// do I have something to sign?
if (commitments1.changes.localHasChanges) {
if (commitments2.changes.localHasChanges) {
self ! CMD_SIGN()
}

Expand Down Expand Up @@ -2446,11 +2482,11 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with

// We tell the peer that the channel is ready to process payments that may be queued.
if (!shutdownInProgress) {
val fundingTxIndex = commitments1.active.map(_.fundingTxIndex).min
val fundingTxIndex = commitments2.active.map(_.fundingTxIndex).min
peer ! ChannelReadyForPayments(self, remoteNodeId, d.channelId, fundingTxIndex)
}

goto(NORMAL) using d.copy(commitments = commitments1, spliceStatus = spliceStatus1) sending sendQueue
goto(NORMAL) using d.copy(commitments = commitments2, spliceStatus = spliceStatus1) sending sendQueue
}

case Event(c: CMD_ADD_HTLC, d: DATA_NORMAL) => handleAddDisconnected(c, d)
Expand Down Expand Up @@ -2869,6 +2905,7 @@ class Channel(val nodeParams: NodeParams, val wallet: OnChainChannelFunder with
sigStash = Nil
announcementSigsStash = Map.empty
announcementSigsSent = Set.empty
spliceLockedSentAfterReestablish = None
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ trait ChannelOpenDualFunded extends DualFundingHandlers with ErrorHandlers {
| . |
| . |
WAIT_FOR_DUAL_FUNDING_LOCKED | | WAIT_FOR_DUAL_FUNDING_LOCKED
| funding_locked funding_locked |
| channel_ready channel_ready |
|---------------- ---------------|
| \/ |
| /\ |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,10 @@ trait CommonFundingHandlers extends CommonHandlers {
// We need to periodically re-send channel updates, otherwise channel will be considered stale and get pruned by network.
context.system.scheduler.scheduleWithFixedDelay(initialDelay = REFRESH_CHANNEL_UPDATE_INTERVAL, delay = REFRESH_CHANNEL_UPDATE_INTERVAL, receiver = self, message = BroadcastChannelUpdate(PeriodicRefresh))
val commitments1 = commitments.modify(_.remoteNextCommitInfo).setTo(Right(channelReady.nextPerCommitmentPoint))
// Set the remote status for all initial funding commitments to Locked. If there are RBF attempts, only one will be confirmed locally.
val commitments2 = commitments1.copy(active = commitments.active.collect { case c if c.fundingTxIndex == 0 => c.copy(remoteFundingStatus = RemoteFundingStatus.Locked) })
Comment on lines 146 to +148
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The collect seems fishy here, because it drops other commitments. Even though it's safe with the way we use it, it's not the responsibility of this function to drop the other attempts, that is done when we locally see one of the funding txs confirm. It would be better to use a map, and cleaner to do all changes in a single call:

Suggested change
val commitments1 = commitments.modify(_.remoteNextCommitInfo).setTo(Right(channelReady.nextPerCommitmentPoint))
// Set the remote status for all initial funding commitments to Locked. If there are RBF attempts, only one will be confirmed locally.
val commitments2 = commitments1.copy(active = commitments.active.collect { case c if c.fundingTxIndex == 0 => c.copy(remoteFundingStatus = RemoteFundingStatus.Locked) })
val commitments1 = commitments.copy(
// Set the remote status for all initial funding commitments to Locked. If there are RBF attempts, only one can be confirmed locally.
active = commitments.active.map {
case c if c.fundingTxIndex == 0 => c.copy(remoteFundingStatus = RemoteFundingStatus.Locked)
case c => c
},
remoteNextCommitInfo = Right(channelReady.nextPerCommitmentPoint)
)

peer ! ChannelReadyForPayments(self, remoteNodeId, commitments.channelId, fundingTxIndex = 0)
DATA_NORMAL(commitments1, aliases1, None, initialChannelUpdate, None, None, None, SpliceStatus.NoSplice)
DATA_NORMAL(commitments2, aliases1, None, initialChannelUpdate, None, None, None, SpliceStatus.NoSplice)
}

def delayEarlyAnnouncementSigs(remoteAnnSigs: AnnouncementSignatures): Unit = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,24 @@ sealed trait ChannelReestablishTlv extends Tlv
object ChannelReestablishTlv {

case class NextFundingTlv(txId: TxId) extends ChannelReestablishTlv
case class YourLastFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv
case class MyCurrentFundingLockedTlv(txId: TxId) extends ChannelReestablishTlv

object NextFundingTlv {
val codec: Codec[NextFundingTlv] = tlvField(txIdAsHash)
}

object YourLastFundingLockedTlv {
val codec: Codec[YourLastFundingLockedTlv] = tlvField("your_last_funding_locked_txid" | txIdAsHash)
}
object MyCurrentFundingLockedTlv {
val codec: Codec[MyCurrentFundingLockedTlv] = tlvField("my_current_funding_locked_txid" | txIdAsHash)
}

val channelReestablishTlvCodec: Codec[TlvStream[ChannelReestablishTlv]] = tlvStream(discriminated[ChannelReestablishTlv].by(varint)
.typecase(UInt64(0), NextFundingTlv.codec)
.typecase(UInt64(1), YourLastFundingLockedTlv.codec)
.typecase(UInt64(3), MyCurrentFundingLockedTlv.codec)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ case class ChannelReestablish(channelId: ByteVector32,
myCurrentPerCommitmentPoint: PublicKey,
tlvStream: TlvStream[ChannelReestablishTlv] = TlvStream.empty) extends ChannelMessage with HasChannelId {
val nextFundingTxId_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.NextFundingTlv].map(_.txId)
val myCurrentFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.MyCurrentFundingLockedTlv].map(_.txId)
val yourLastFundingLocked_opt: Option[TxId] = tlvStream.get[ChannelReestablishTlv.YourLastFundingLockedTlv].map(_.txId)
}

case class OpenChannel(chainHash: BlockHash,
Expand Down
Loading