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

multi: log additional information for local force closures #7787

Closed
Closed
Show file tree
Hide file tree
Changes from all 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
111 changes: 111 additions & 0 deletions channeldb/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3121,6 +3121,28 @@ const (
Abandoned ClosureType = 5
)

// LocalForceCloseInfo encapsulates information that led to a channel being
// force closed with the initiator being local. Currently local force closes
// can be (1) purely user initiated, (2) automatic due to certain link errors,
// or (3) automatic due to certain on-chain HTLC conditions. As such, only one
// of the three fields in this struct will ever have meaningful information. But
// this may be extended in the future. For example, we may decide to log HTLC
// information that doesn't automatically result in a force close, but leaves
// the channel in a state whereby the user has no option but to force close.
type LocalForceCloseInfo struct {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we use this in the contractcourt directly? I see that the HtlcActions isn't exactly the same type, but maybe we could convert? Otherwise we need encode/decode code in two places.
If we share this struct, it can have a public Encode() and Decode() method.

// UserInitiated is true iff the force close was specifically initiated
// by the user.
UserInitiated bool

// LinkFailureError is a non-empty string iff the force close was
// automatically initiated following from a link failure.
LinkFailureError string

// HtlcActions is a non-empty map iff the force close was automatically
// initiated by an on-chain trigger such as HTLC timeout.
HtlcActions map[string][]HTLC
}

// ChannelCloseSummary contains the final state of a channel at the point it
// was closed. Once a channel is closed, all the information pertaining to that
// channel within the openChannelBucket is deleted, and a compact summary is
Expand Down Expand Up @@ -3200,6 +3222,11 @@ type ChannelCloseSummary struct {
// LastChanSyncMsg is the ChannelReestablish message for this channel
// for the state at the point where it was closed.
LastChanSyncMsg *lnwire.ChannelReestablish

// LocalFCInfo encapsulates information that led to a channel being
// force closed with the initiator being local. This will be nil if the
// initiator of the force close was remote, or it wasn't a force close.
LocalFCInfo *LocalForceCloseInfo
}

// CloseChannel closes a previously active Lightning channel. Closing a channel
Expand Down Expand Up @@ -3579,6 +3606,18 @@ func serializeChannelCloseSummary(w io.Writer, cs *ChannelCloseSummary) error {
}
}

// Write whether the local force close info is present.
if err := WriteElements(w, cs.LocalFCInfo != nil); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

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

This won't work without changes to the WriteElement and ReadElement functions in channeldb/codec.go.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do you say this? bool is handled by WriteElement.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Oh, I misread it as just WriteElements(w, cs.LocalFCInfo), missed the != nil part. So maybe extract to a bool to make it more obvious?

Copy link
Contributor

Choose a reason for hiding this comment

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

That is the general pattern of writing it looking at the function.

return err
}

// Write the local force close info, if present.
if cs.LocalFCInfo != nil {
if err := writeLocalFCInfo(w, cs.LocalFCInfo); err != nil {
return err
}
}

return nil
}

Expand Down Expand Up @@ -3660,6 +3699,25 @@ func deserializeCloseChannelSummary(r io.Reader) (*ChannelCloseSummary, error) {
c.LastChanSyncMsg = chanSync
}

// Check if we have local force close info to read
var hasLocalFCInfo bool
err = ReadElements(r, &hasLocalFCInfo)
if errors.Is(err, io.EOF) {
return c, nil
} else if err != nil {
return nil, err
}

// If local force close info is present, read it.
if hasLocalFCInfo {
var localFCInfo LocalForceCloseInfo
err = readLocalFCInfo(r, &localFCInfo)
if err != nil {
return nil, err
}
c.LocalFCInfo = &localFCInfo
}

return c, nil
}

Expand All @@ -3672,6 +3730,28 @@ func writeChanConfig(b io.Writer, c *ChannelConfig) error {
)
}

func writeLocalFCInfo(b io.Writer, info *LocalForceCloseInfo) error {
err := WriteElements(b, info.UserInitiated,
[]byte(info.LinkFailureError), uint8(len(info.HtlcActions)))
if err != nil {
return err
}

for action, htlcs := range info.HtlcActions {
err = WriteElement(b, []byte(action))
if err != nil {
return err
}

err = SerializeHtlcs(b, htlcs...)
if err != nil {
return err
}
}

return nil
}

// fundingTxPresent returns true if expect the funding transcation to be found
// on disk or already populated within the passed open channel struct.
func fundingTxPresent(channel *OpenChannel) bool {
Expand Down Expand Up @@ -3878,6 +3958,37 @@ func readChanConfig(b io.Reader, c *ChannelConfig) error {
)
}

func readLocalFCInfo(b io.Reader, info *LocalForceCloseInfo) error {
var (
linkErrorBytes []byte
numHtlcMapKeys uint8
)
err := ReadElements(b, &info.UserInitiated, &linkErrorBytes,
&numHtlcMapKeys)
if err != nil {
return err
}

info.LinkFailureError = string(linkErrorBytes)
info.HtlcActions = make(map[string][]HTLC)
for i := uint8(0); i < numHtlcMapKeys; i++ {
var actionBytes []byte
err = ReadElements(b, &actionBytes)
if err != nil {
return err
}

htlcs, err := DeserializeHtlcs(b)
if err != nil {
return err
}

info.HtlcActions[string(actionBytes)] = htlcs
}

return nil
}

func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error {
infoBytes := chanBucket.Get(chanInfoKey)
if infoBytes == nil {
Expand Down
170 changes: 170 additions & 0 deletions contractcourt/briefcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,28 @@ func (c *ContractResolutions) IsEmpty() bool {
c.AnchorResolution == nil && c.BreachResolution == nil
}

// localForceCloseInfo encapsulates information that led to a channel being
// force closed with the initiator being local. Currently local force closes
// can be (1) purely user initiated, (2) automatic due to certain link errors,
// or (3) automatic due to certain on-chain HTLC conditions. As such, only one
// of the three fields in this struct will ever have meaningful information. But
// this may be extended in the future. For example, we may decide to log HTLC
// information that doesn't automatically result in a force close, but leaves
// the channel in a state whereby the user has no option but to force close.
type localForceCloseInfo struct {
// userInitiated is true iff the force close was specifically initiated
// by the user.
userInitiated bool

// linkFailureError is a non-empty string iff the force close was
// automatically initiated following from a link failure.
linkFailureError string

// htlcActions is a non-empty map iff the force close was automatically
// initiated by an on-chain trigger such as HTLC timeout.
htlcActions ChainActionMap
Copy link
Contributor

Choose a reason for hiding this comment

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

Hello @guggero would be a good idea to just indicate that the closure was due to ahtlcAction and we just put the particular htlc action responsible for the closure. Would there be need for the actual htlc?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I guess it would be nice to be able to correlate the closure resulting from an HTLC with the actual HTLC in the channel (to see why it timed out, what its CLTV delta was and so on).

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay thanks.

}

// ArbitratorLog is the primary source of persistent storage for the
// ChannelArbitrator. The log stores the current state of the
// ChannelArbitrator's internal state machine, any items that are required to
Expand Down Expand Up @@ -119,6 +141,17 @@ type ArbitratorLog interface {
// introduced.
FetchChainActions() (ChainActionMap, error)

// LogLocalForceCloseInfo records the passed-in localForceCloseInfo
// object within the ArbitratorLog for deferred consumption. This is
// typically invoked when the channel force closure is first initiated
// either by the user or automatically (link error, HTLC actions, etc.).
LogLocalForceCloseInfo(localForceCloseInfo) error

// FetchLocalForceCloseInfo attempts to fetch the localForceCloseInfo
// object that was previously logged in the ArbitratorLog. We use this
// information when actually marking the channel as closed.
FetchLocalForceCloseInfo() (*localForceCloseInfo, error)

// WipeHistory is to be called ONLY once *all* contracts have been
// fully resolved, and the channel closure if finalized. This method
// will delete all on-disk state within the persistent log.
Expand Down Expand Up @@ -363,6 +396,10 @@ var (
// store the confirmed active HTLC sets once we learn that a channel
// has closed out on chain.
commitSetKey = []byte("commit-set")

// localForceCloseInfoKey is the key that we use to store the
// localForceCloseInfo object within the log, if any.
localForceCloseInfoKey = []byte("local-force-close-info")
)

var (
Expand Down Expand Up @@ -1008,6 +1045,139 @@ func (b *boltArbitratorLog) FetchChainActions() (ChainActionMap, error) {
return actionsMap, nil
}

// LogLocalForceCloseInfo records the passed-in localForceCloseInfo
// object within the ArbitratorLog for deferred consumption. This is
// typically invoked when the channel force closure is first initiated
// either by the user or automatically (link error, HTLC actions, etc.).
func (b *boltArbitratorLog) LogLocalForceCloseInfo(
info localForceCloseInfo) error {

return kvdb.Update(b.db, func(tx kvdb.RwTx) error {
scopeBucket, err := tx.CreateTopLevelBucket(b.scopeKey[:])
if err != nil {
return err
}

b := new(bytes.Buffer)

// first byte is userInitiated bool
err = binary.Write(b, endian, info.userInitiated)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We should use TLV for any new data structure that we save to the DB. That will make it way easier to extend this info structure as well. See the example in fetchChanInfo() in channeldb/channel.go.

if err != nil {
return err
}

// second byte is length of linkFailureError string
err = binary.Write(b, endian, uint8(len(info.linkFailureError)))
if err != nil {
return err
}

// next bytes are the actual linkFailureError string
_, err = b.WriteString(info.linkFailureError)
if err != nil {
return err
}

// next byte is number of key:value pairs in htlcActions map
err = binary.Write(b, endian, uint8(len(info.htlcActions)))
if err != nil {
return err
}

// next bytes are actual map data
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: all inline comments should also be full sentences with proper capitalization and punctuation.

for action, htlcs := range info.htlcActions {
err = binary.Write(b, endian, action)
if err != nil {
return err
}

err = channeldb.SerializeHtlcs(b, htlcs...)
if err != nil {
return err
}
}

return scopeBucket.Put(localForceCloseInfoKey, b.Bytes())
}, func() {})
}

// FetchLocalForceCloseInfo attempts to fetch the localForceCloseInfo
// object that was previously logged in the ArbitratorLog. We use this
// information when actually marking the channel as closed.
func (b *boltArbitratorLog) FetchLocalForceCloseInfo() (*localForceCloseInfo,
error) {

info := &localForceCloseInfo{
htlcActions: make(ChainActionMap),
}
infoExists := false
err := kvdb.View(b.db, func(tx kvdb.RTx) error {
scopeBucket := tx.ReadBucket(b.scopeKey[:])
if scopeBucket == nil {
return errScopeBucketNoExist
}

infoBytes := scopeBucket.Get(localForceCloseInfoKey)
if len(infoBytes) == 0 {
return nil
}

infoExists = true
infoReader := bytes.NewReader(infoBytes)
userInitiated := new(bool)
err := binary.Read(infoReader, endian, userInitiated)
if err != nil {
return err
}
info.userInitiated = *userInitiated

linkFailureErrorLength := new(uint8)
err = binary.Read(infoReader, endian, linkFailureErrorLength)
if err != nil {
return err
}
linkFailureErrorBytes := make([]byte, *linkFailureErrorLength)
_, err = infoReader.Read(linkFailureErrorBytes)
if err != nil {
return err
}
info.linkFailureError = string(linkFailureErrorBytes)

numActionsKeys := new(uint8)
err = binary.Read(infoReader, endian, numActionsKeys)
if err != nil {
return err
}
for i := uint8(0); i < *numActionsKeys; i++ {
var action ChainAction
err = binary.Read(infoReader, endian, &action)
if err != nil {
return err
}

htlcs, err := channeldb.DeserializeHtlcs(infoReader)
if err != nil {
return err
}

info.htlcActions[action] = htlcs
}

return nil
}, func() {
info = &localForceCloseInfo{
htlcActions: make(ChainActionMap),
}
infoExists = false
})

if err != nil || !infoExists {
return nil, err
}

return info, nil
}

// InsertConfirmedCommitSet stores the known set of active HTLCs at the time
// channel closure. We'll use this to reconstruct our set of chain actions anew
// based on the confirmed and pending commitment state.
Expand Down
Loading