-
Notifications
You must be signed in to change notification settings - Fork 2.1k
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
Changes from all commits
5b5e44d
f6cb2ee
d5cc840
3a29f47
08ca9fc
2098b36
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
// 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 | ||
|
@@ -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 | ||
|
@@ -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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This won't work without changes to the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do you say this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, I misread it as just There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} | ||
|
||
|
@@ -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 | ||
} | ||
|
||
|
@@ -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 { | ||
|
@@ -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 { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 a There was a problem hiding this comment. Choose a reason for hiding this commentThe 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). There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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. | ||
|
@@ -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 ( | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
There was a problem hiding this comment.
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()
andDecode()
method.