Merge pull request #4081 from Roasbeef/frozen-chans

multi: introduce new frozen channel variant for external funding
This commit is contained in:
Olaoluwa Osuntokun 2020-03-23 17:37:40 -07:00 committed by GitHub
commit 75abb3f41f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 1008 additions and 760 deletions

View File

@ -347,6 +347,21 @@ func (c *channelCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, b
"instead have %v", spew.Sdump(msg))
}
// As we're the responder to this shutdown (the other party
// wants to close), we'll check if this is a frozen channel or
// not. If the channel is frozen as we were also the initiator
// of the channel opening, then we'll deny their close attempt.
chanInitiator := c.cfg.channel.IsInitiator()
if !chanInitiator && c.cfg.channel.State().ChanType.IsFrozen() &&
c.negotiationHeight < c.cfg.channel.State().ThawHeight {
return nil, false, fmt.Errorf("initiator attempting "+
"to co-op close frozen ChannelPoint(%v) "+
"(current_height=%v, thaw_height=%v)",
c.chanPoint, c.negotiationHeight,
c.cfg.channel.State().ThawHeight)
}
// If the remote node opened the channel with option upfront shutdown
// script, check that the script they provided matches.
if err := maybeMatchScript(
@ -382,7 +397,7 @@ func (c *channelCloser) ProcessCloseMsg(msg lnwire.Message) ([]lnwire.Message, b
// We'll also craft our initial close proposal in order to keep
// the negotiation moving, but only if we're the negotiator.
if c.cfg.channel.IsInitiator() {
if chanInitiator {
closeSigned, err := c.proposeCloseSigned(c.idealFeeSat)
if err != nil {
return nil, false, err

View File

@ -102,6 +102,11 @@ var (
// channel closure. This key should be accessed from within the
// sub-bucket of a target channel, identified by its channel point.
revocationLogBucket = []byte("revocation-log-key")
// frozenChanKey is the key where we store the information for any
// active "frozen" channels. This key is present only in the leaf
// bucket for a given channel.
frozenChanKey = []byte("frozen-chans")
)
var (
@ -191,6 +196,11 @@ const (
// channel type also uses a delayed to_remote output script. If bit is
// set, we'll find the size of the anchor outputs in the database.
AnchorOutputsBit ChannelType = 1 << 3
// FrozenBit indicates that the channel is a frozen channel, meaning
// that only the responder can decide to cooperatively close the
// channel.
FrozenBit ChannelType = 1 << 4
)
// IsSingleFunder returns true if the channel type if one of the known single
@ -222,6 +232,13 @@ func (c ChannelType) HasAnchors() bool {
return c&AnchorOutputsBit == AnchorOutputsBit
}
// IsFrozen returns true if the channel is considered to be "frozen". A frozen
// channel means that only the responder can initiate a cooperative channel
// closure.
func (c ChannelType) IsFrozen() bool {
return c&FrozenBit == FrozenBit
}
// ChannelConstraints represents a set of constraints meant to allow a node to
// limit their exposure, enact flow control and ensure that all HTLCs are
// economically relevant. This struct will be mirrored for both sides of the
@ -635,6 +652,11 @@ type OpenChannel struct {
// was not set, the field is empty.
RemoteShutdownScript lnwire.DeliveryAddress
// ThawHeight is the height when a frozen channel once again becomes a
// normal channel. If this is zero, then there're no restrictions on
// this channel.
ThawHeight uint32
// TODO(roasbeef): eww
Db *DB
@ -1229,6 +1251,17 @@ func putOpenChannel(chanBucket kvdb.RwBucket, channel *OpenChannel) error {
return fmt.Errorf("unable to store chan commitments: %v", err)
}
// Next, if this is a frozen channel, we'll add in the axillary
// information we need to store.
if channel.ChanType.IsFrozen() {
err := storeThawHeight(
chanBucket, channel.ThawHeight,
)
if err != nil {
return fmt.Errorf("unable to store thaw height: %v", err)
}
}
// Finally, we'll write out the revocation state for both parties
// within a distinct key space.
if err := putChanRevocationState(chanBucket, channel); err != nil {
@ -1259,6 +1292,18 @@ func fetchOpenChannel(chanBucket kvdb.ReadBucket,
return nil, fmt.Errorf("unable to fetch chan commitments: %v", err)
}
// Next, if this is a frozen channel, we'll add in the axillary
// information we need to store.
if channel.ChanType.IsFrozen() {
thawHeight, err := fetchThawHeight(chanBucket)
if err != nil {
return nil, fmt.Errorf("unable to store thaw "+
"height: %v", err)
}
channel.ThawHeight = thawHeight
}
// Finally, we'll retrieve the current revocation state so we can
// properly
if err := fetchChanRevocationState(chanBucket, channel); err != nil {
@ -2517,6 +2562,15 @@ func (c *OpenChannel) CloseChannel(summary *ChannelCloseSummary,
return err
}
// We'll also remove the channel from the frozen channel bucket
// if we need to.
if c.ChanType.IsFrozen() {
err := deleteThawHeight(chanBucket)
if err != nil {
return err
}
}
// With the base channel data deleted, attempt to delete the
// information stored within the revocation log.
logBucket := chanBucket.NestedReadWriteBucket(revocationLogBucket)
@ -3213,3 +3267,29 @@ func fetchChannelLogEntry(log kvdb.ReadBucket,
commitReader := bytes.NewReader(commitBytes)
return deserializeChanCommit(commitReader)
}
func fetchThawHeight(chanBucket kvdb.ReadBucket) (uint32, error) {
var height uint32
heightBytes := chanBucket.Get(frozenChanKey)
heightReader := bytes.NewReader(heightBytes)
if err := ReadElements(heightReader, &height); err != nil {
return 0, err
}
return height, nil
}
func storeThawHeight(chanBucket kvdb.RwBucket, height uint32) error {
var heightBuf bytes.Buffer
if err := WriteElements(&heightBuf, height); err != nil {
return err
}
return chanBucket.Put(frozenChanKey, heightBuf.Bytes())
}
func deleteThawHeight(chanBucket kvdb.RwBucket) error {
return chanBucket.Delete(frozenChanKey)
}

View File

@ -349,7 +349,7 @@ func createTestChannelState(t *testing.T, cdb *DB) *OpenChannel {
chanID := lnwire.NewShortChanIDFromInt(uint64(rand.Int63()))
return &OpenChannel{
ChanType: SingleFunderBit,
ChanType: SingleFunderBit | FrozenBit,
ChainHash: key,
FundingOutpoint: wire.OutPoint{Hash: key, Index: rand.Uint32()},
ShortChannelID: chanID,
@ -387,6 +387,7 @@ func createTestChannelState(t *testing.T, cdb *DB) *OpenChannel {
Db: cdb,
Packager: NewChannelPackager(chanID),
FundingTxn: testTx,
ThawHeight: uint32(defaultPendingHeight),
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1437,6 +1437,15 @@ message Channel {
amount to us.
*/
uint64 push_amount_sat = 27;
/**
This uint32 indicates if this channel is to be considered 'frozen'. A
frozen channel doest not allow a cooperative channel close by the
initiator. The thaw_height is the height that this restriction stops
applying to the channel. This field is optional, not setting it or using a
value of zero will mean the channel has no additional restrictions.
*/
uint32 thaw_height = 28;
}
message ListChannelsRequest {
@ -1881,6 +1890,14 @@ message ChanPointShim {
channel ID.
*/
bytes pending_chan_id = 5;
/**
This uint32 indicates if this channel is to be considered 'frozen'. A
frozen channel does not allow a cooperative channel close by the
initiator. The thaw_height is the height that this restriction stops
applying to the channel.
*/
uint32 thaw_height = 6;
}
message FundingShim {

View File

@ -1847,6 +1847,11 @@
"type": "string",
"format": "byte",
"description": "*\nIf non-zero, then this will be used as the pending channel ID on the wire\nprotocol to initate the funding request. This is an optional field, and\nshould only be set if the responder is already expecting a specific pending\nchannel ID."
},
"thaw_height": {
"type": "integer",
"format": "int64",
"description": "*\nThis uint32 indicates if this channel is to be considered 'frozen'. A\nfrozen channel does not allow a cooperative channel close by the\ninitiator. The thaw_height is the height that this restriction stops\napplying to the channel."
}
}
},
@ -2002,6 +2007,11 @@
"type": "string",
"format": "uint64",
"description": "The amount that the initiator of the channel optionally pushed to the remote\nparty on channel open. This amount will be zero if the channel initiator did\nnot push any funds to the remote peer. If the initiator field is true, we\npushed this amount to our peer, if it is false, the remote peer pushed this\namount to us."
},
"thaw_height": {
"type": "integer",
"format": "int64",
"description": "*\nThis uint32 indicates if this channel is to be considered 'frozen'. A\nfrozen channel doest not allow a cooperative channel close by the\ninitiator. The thaw_height is the height that this restriction stops\napplying to the channel. This field is optional, not setting it or using a\nvalue of zero will mean the channel has no additional restrictions."
}
}
},

View File

@ -15060,16 +15060,23 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
t.Fatalf("unable to gen pending chan ID: %v", err)
}
_, currentHeight, err := net.Miner.Node.GetBestBlock()
if err != nil {
t.Fatalf("unable to get current blockheight %v", err)
}
// Now that we have the pending channel ID, Dave (our responder) will
// register the intent to receive a new channel funding workflow using
// the pending channel ID.
chanPointShim := &lnrpc.ChanPointShim{
Amt: int64(chanSize),
ChanPoint: &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
FundingTxidBytes: txid[:],
},
chanPoint := &lnrpc.ChannelPoint{
FundingTxid: &lnrpc.ChannelPoint_FundingTxidBytes{
FundingTxidBytes: txid[:],
},
}
thawHeight := uint32(currentHeight + 10)
chanPointShim := &lnrpc.ChanPointShim{
Amt: int64(chanSize),
ChanPoint: chanPoint,
LocalKey: &lnrpc.KeyDescriptor{
RawKeyBytes: daveFundingKey.RawKeyBytes,
KeyLoc: &lnrpc.KeyLocator{
@ -15079,6 +15086,7 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
},
RemoteKey: carolFundingKey.RawKeyBytes,
PendingChanId: pendingChanID[:],
ThawHeight: thawHeight,
}
fundingShim := &lnrpc.FundingShim{
Shim: &lnrpc.FundingShim_ChanPointShim{
@ -15121,13 +15129,24 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
// At this point, we'll now carry out the normal basic channel funding
// test as everything should now proceed as normal (a regular channel
// funding flow).
_, _, closeChans, err := basicChannelFundingTest(
carolChan, daveChan, _, err := basicChannelFundingTest(
t, net, carol, dave, fundingShim,
)
if err != nil {
t.Fatalf("unable to open channels: %v", err)
}
// Both channels should be marked as frozen with the proper thaw
// height.
if carolChan.ThawHeight != thawHeight {
t.Fatalf("expected thaw height of %v, got %v",
carolChan.ThawHeight, thawHeight)
}
if daveChan.ThawHeight != thawHeight {
t.Fatalf("expected thaw height of %v, got %v",
daveChan.ThawHeight, thawHeight)
}
// Next, to make sure the channel functions as normal, we'll make some
// payments within the channel.
payAmt := btcutil.Amount(100000)
@ -15148,10 +15167,24 @@ func testExternalFundingChanPoint(net *lntest.NetworkHarness, t *harnessTest) {
t.Fatalf("unable to make payments between Carol and Dave")
}
// To conclude, we'll close the newly created channel between Carol and
// Dave.
closeChans()
// Now that the channels are open, and we've confirmed that they're
// operational, we'll now ensure that the channels are frozen as
// intended (if requested).
//
// First, we'll try to close the channel as Carol, the initiator. This
// should fail as a frozen channel only allows the responder to
// initiate a channel close.
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
_, _, err = net.CloseChannel(ctxt, carol, chanPoint, false)
if err == nil {
t.Fatalf("carol wasn't denied a co-op close attempt for a " +
"frozen channel")
}
// Next we'll try but this time with Dave (the responder) as the
// initiator. This time the channel should be closed as normal.
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
closeChannelAndAssert(ctxt, t, net, dave, chanPoint, false)
}
type testCase struct {

View File

@ -30,6 +30,11 @@ type ShimIntent struct {
// chanPoint is the final channel point for the to be created channel.
chanPoint *wire.OutPoint
// thawHeight, if non-zero is the height where this channel will become
// a normal channel. Until this height, it's considered frozen, so it
// can only be cooperatively closed by the responding party.
thawHeight uint32
}
// FundingOutput returns the witness script, and the output that creates the
@ -87,6 +92,12 @@ func (s *ShimIntent) ChanPoint() (*wire.OutPoint, error) {
return s.chanPoint, nil
}
// ThawHeight returns the height where this channel goes back to being a normal
// channel.
func (s *ShimIntent) ThawHeight() uint32 {
return s.thawHeight
}
// FundingKeys couples our multi-sig key along with the remote party's key.
type FundingKeys struct {
// LocalKey is our multi-sig key.
@ -131,12 +142,17 @@ type CannedAssembler struct {
// initiator indicates if we're the initiator or the channel or not.
initiator bool
// thawHeight, if non-zero is the height where this channel will become
// a normal channel. Until this height, it's considered frozen, so it
// can only be cooperatively closed by the responding party.
thawHeight uint32
}
// NewCannedAssembler creates a new CannedAssembler from the material required
// to construct a funding output and channel point.
func NewCannedAssembler(chanPoint wire.OutPoint, fundingAmt btcutil.Amount,
localKey *keychain.KeyDescriptor,
func NewCannedAssembler(thawHeight uint32, chanPoint wire.OutPoint,
fundingAmt btcutil.Amount, localKey *keychain.KeyDescriptor,
remoteKey *btcec.PublicKey, initiator bool) *CannedAssembler {
return &CannedAssembler{
@ -145,6 +161,7 @@ func NewCannedAssembler(chanPoint wire.OutPoint, fundingAmt btcutil.Amount,
remoteKey: remoteKey,
fundingAmt: fundingAmt,
chanPoint: chanPoint,
thawHeight: thawHeight,
}
}
@ -162,9 +179,10 @@ func (c *CannedAssembler) ProvisionChannel(req *Request) (Intent, error) {
}
intent := &ShimIntent{
localKey: c.localKey,
remoteKey: c.remoteKey,
chanPoint: &c.chanPoint,
localKey: c.localKey,
remoteKey: c.remoteKey,
chanPoint: &c.chanPoint,
thawHeight: c.thawHeight,
}
if c.initiator {

View File

@ -701,7 +701,7 @@ func testCancelNonExistentReservation(miner *rpctest.Harness,
res, err := lnwallet.NewChannelReservation(
10000, 10000, feePerKw, alice, 22, 10, &testHdSeed,
lnwire.FFAnnounceChannel, lnwallet.CommitmentTypeTweakless,
nil, [32]byte{},
nil, [32]byte{}, 0,
)
if err != nil {
t.Fatalf("unable to create res: %v", err)
@ -796,8 +796,8 @@ func assertContributionInitPopulated(t *testing.T, c *lnwallet.ChannelContributi
func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
alice, bob *lnwallet.LightningWallet, t *testing.T,
commitType lnwallet.CommitmentType,
aliceChanFunder chanfunding.Assembler,
fetchFundingTx func() *wire.MsgTx, pendingChanID [32]byte) {
aliceChanFunder chanfunding.Assembler, fetchFundingTx func() *wire.MsgTx,
pendingChanID [32]byte, thawHeight uint32) {
// For this scenario, Alice will be the channel initiator while bob
// will act as the responder to the workflow.
@ -1045,6 +1045,24 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
t.Fatalf("incorrect transaction was mined")
}
// If a frozen channel was requested, then we expect that both channel
// types show as being a frozen channel type.
aliceChanFrozen := aliceChannels[0].ChanType.IsFrozen()
bobChanFrozen := bobChannels[0].ChanType.IsFrozen()
if thawHeight != 0 && (!aliceChanFrozen || !bobChanFrozen) {
t.Fatalf("expected both alice and bob to have frozen chans: "+
"alice_frozen=%v, bob_frozen=%v", aliceChanFrozen,
bobChanFrozen)
}
if thawHeight != bobChannels[0].ThawHeight {
t.Fatalf("wrong thaw height: expected %v got %v", thawHeight,
bobChannels[0].ThawHeight)
}
if thawHeight != aliceChannels[0].ThawHeight {
t.Fatalf("wrong thaw height: expected %v got %v", thawHeight,
aliceChannels[0].ThawHeight)
}
assertReservationDeleted(aliceChanReservation, t)
assertReservationDeleted(bobChanReservation, t)
}
@ -2546,8 +2564,8 @@ var walletTests = []walletTestCase{
testSingleFunderReservationWorkflow(
miner, alice, bob, t,
lnwallet.CommitmentTypeLegacy, nil, nil,
[32]byte{},
lnwallet.CommitmentTypeLegacy, nil,
nil, [32]byte{}, 0,
)
},
},
@ -2558,8 +2576,8 @@ var walletTests = []walletTestCase{
testSingleFunderReservationWorkflow(
miner, alice, bob, t,
lnwallet.CommitmentTypeTweakless, nil, nil,
[32]byte{},
lnwallet.CommitmentTypeTweakless, nil,
nil, [32]byte{}, 0,
)
},
},
@ -2777,12 +2795,13 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness,
// Now that we have the fully constructed funding transaction, we'll
// create a new shim external funder out of it for Alice, and prep a
// shim intent for Bob.
thawHeight := uint32(200)
aliceExternalFunder := chanfunding.NewCannedAssembler(
*chanPoint, btcutil.Amount(chanAmt), &aliceFundingKey,
thawHeight, *chanPoint, btcutil.Amount(chanAmt), &aliceFundingKey,
bobFundingKey.PubKey, true,
)
bobShimIntent, err := chanfunding.NewCannedAssembler(
*chanPoint, btcutil.Amount(chanAmt), &bobFundingKey,
thawHeight, *chanPoint, btcutil.Amount(chanAmt), &bobFundingKey,
aliceFundingKey.PubKey, false,
).ProvisionChannel(&chanfunding.Request{
LocalAmt: btcutil.Amount(chanAmt),
@ -2816,7 +2835,7 @@ func testSingleFunderExternalFundingTx(miner *rpctest.Harness,
miner, alice, bob, t, lnwallet.CommitmentTypeTweakless,
aliceExternalFunder, func() *wire.MsgTx {
return fundingTx
}, pendingChanID,
}, pendingChanID, thawHeight,
)
}

View File

@ -171,7 +171,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash,
flags lnwire.FundingFlag, commitType CommitmentType,
fundingAssembler chanfunding.Assembler,
pendingChanID [32]byte) (*ChannelReservation, error) {
pendingChanID [32]byte, thawHeight uint32) (*ChannelReservation, error) {
var (
ourBalance lnwire.MilliSatoshi
@ -306,6 +306,12 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
chanType |= channeldb.AnchorOutputsBit
}
// If the channel is meant to be frozen, then we'll set the frozen bit
// now so once the channel is open, it can be interpreted properly.
if thawHeight != 0 {
chanType |= channeldb.FrozenBit
}
return &ChannelReservation{
ourContribution: &ChannelContribution{
FundingAmount: ourBalance.ToSatoshis(),
@ -334,7 +340,8 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
FeePerKw: btcutil.Amount(commitFeePerKw),
CommitFee: commitFee,
},
Db: wallet.Cfg.Database,
ThawHeight: thawHeight,
Db: wallet.Cfg.Database,
},
pushMSat: pushMSat,
pendingChanID: pendingChanID,

View File

@ -560,6 +560,26 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
remoteFundingAmt = fundingIntent.RemoteFundingAmt()
}
// If this is a shim intent, then it may be attempting to use an
// existing set of keys for the funding workflow. In this case, we'll
// make a simple wrapper keychain.KeyRing that will proxy certain
// derivation calls to future callers.
var (
keyRing keychain.KeyRing = l.SecretKeyRing
thawHeight uint32
)
if shimIntent, ok := fundingIntent.(*chanfunding.ShimIntent); ok {
keyRing = &shimKeyRing{
KeyRing: keyRing,
ShimIntent: shimIntent,
}
// As this was a registered shim intent, we'll obtain the thaw
// height of the intent, if present at all. If this is
// non-zero, then we'll mark this as the proper channel type.
thawHeight = shimIntent.ThawHeight()
}
// The total channel capacity will be the size of the funding output we
// created plus the remote contribution.
capacity := localFundingAmt + remoteFundingAmt
@ -569,6 +589,7 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
capacity, localFundingAmt, req.CommitFeePerKw, l, id,
req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags,
req.CommitType, req.ChanFunder, req.PendingChanID,
thawHeight,
)
if err != nil {
if fundingIntent != nil {
@ -580,19 +601,6 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
return
}
var keyRing keychain.KeyRing = l.SecretKeyRing
// If this is a shim intent, then it may be attempting to use an
// existing set of keys for the funding workflow. In this case, we'll
// make a simple wrapper keychain.KeyRing that will proxy certain
// derivation calls to future callers.
if shimIntent, ok := fundingIntent.(*chanfunding.ShimIntent); ok {
keyRing = &shimKeyRing{
KeyRing: keyRing,
ShimIntent: shimIntent,
}
}
err = l.initOurContribution(
reservation, fundingIntent, req.NodeAddr, req.NodeID, keyRing,
)

View File

@ -1470,8 +1470,8 @@ func extractOpenChannelMinConfs(in *lnrpc.OpenChannelRequest) (int32, error) {
// newFundingShimAssembler returns a new fully populated
// chanfunding.CannedAssembler using a FundingShim obtained from an RPC caller.
func newFundingShimAssembler(chanPointShim *lnrpc.ChanPointShim,
initiator bool, keyRing keychain.KeyRing) (chanfunding.Assembler, error) {
func newFundingShimAssembler(chanPointShim *lnrpc.ChanPointShim, initiator bool,
keyRing keychain.KeyRing) (chanfunding.Assembler, error) {
// Perform some basic sanity checks to ensure that all the expected
// fields are populated.
@ -1545,8 +1545,9 @@ func newFundingShimAssembler(chanPointShim *lnrpc.ChanPointShim,
// With all the parts assembled, we can now make the canned assembler
// to pass into the wallet.
return chanfunding.NewCannedAssembler(
*chanPoint, btcutil.Amount(chanPointShim.Amt),
&localKeyDesc, remoteKey, initiator,
chanPointShim.ThawHeight, *chanPoint,
btcutil.Amount(chanPointShim.Amt), &localKeyDesc,
remoteKey, initiator,
), nil
}
@ -1956,15 +1957,25 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
return err
}
// If this is a frozen channel, then we only allow the close to proceed
// if we were the responder to this channel.
_, bestHeight, err := r.server.cc.chainIO.GetBestBlock()
if err != nil {
return err
}
if channel.State().ChanType.IsFrozen() && channel.IsInitiator() &&
uint32(bestHeight) < channel.State().ThawHeight {
return fmt.Errorf("cannot co-op close frozen channel as "+
"initiator until height=%v, (current_height=%v)",
channel.State().ThawHeight, bestHeight)
}
// If a force closure was requested, then we'll handle all the details
// around the creation and broadcast of the unilateral closure
// transaction here rather than going to the switch as we don't require
// interaction from the peer.
if force {
_, bestHeight, err := r.server.cc.chainIO.GetBestBlock()
if err != nil {
return err
}
// As we're force closing this channel, as a precaution, we'll
// ensure that the switch doesn't continue to see this channel
@ -3179,6 +3190,7 @@ func createRPCOpenChannel(r *rpcServer, graph *channeldb.ChannelGraph,
RemoteChanReserveSat: int64(dbChannel.RemoteChanCfg.ChanReserve),
StaticRemoteKey: commitmentType == lnrpc.CommitmentType_STATIC_REMOTE_KEY,
CommitmentType: commitmentType,
ThawHeight: dbChannel.ThawHeight,
}
for i, htlc := range localCommit.Htlcs {