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 #8072

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
90 changes: 90 additions & 0 deletions channeldb/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -3252,6 +3252,96 @@ const (
Abandoned ClosureType = 5
)

// LocalForceCloseInitiator is a type that gives information about what led to a
// channel being force closed locally.
type LocalForceCloseInitiator string
Copy link
Member

Choose a reason for hiding this comment

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

We should use uint8 plus a String method to make this more space-efficient, something like

lnd/feature/set.go

Lines 49 to 64 in 0df507e

func (s Set) String() string {
switch s {
case SetInit:
return "SetInit"
case SetLegacyGlobal:
return "SetLegacyGlobal"
case SetNodeAnn:
return "SetNodeAnn"
case SetInvoice:
return "SetInvoice"
case SetInvoiceAmp:
return "SetInvoiceAmp"
default:
return "SetUnknown"
}
}

Copy link
Member

Choose a reason for hiding this comment

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

Also this should be called ForceCloseInitiator?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also, my intial approach was to do just as you have stated concerning making it unit8 instead of a string but that would mean, creating a LocalForceCloseInitiator type for each of the LinkFailureError messages. Making the initiator a string, I can easily convert any link failure error message to the LocalForceCloseInitiator on the go instead of duplicating all the LinkFailure error message type the channel.go file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ref:

type LinkError struct {
// msg returns the wire failure associated with the error.
// This value should *not* be nil, because we should always
// know the failure type for failures which occur at our own
// node.
msg lnwire.FailureMessage
// FailureDetail enriches the wire error with additional information.
FailureDetail
}
to the structure of LinkError type.


const (
// UserInitiated indicates that the force close was specifically
// initiated by the user.
UserInitiated LocalForceCloseInitiator = "user initiated"

// ChainActionsInitiated indicates that the force close was
// automatically initiated by an on-chain trigger such as HTLC timeout.
ChainActionsInitiated LocalForceCloseInitiator = "chain action" +
" initiated"

// localForceCloseInitiatorType is used to serialize/deserialize
// localForceCloseInitiator.
localForceCloseInitiatorType tlv.Type = 0
)

// String returns the human-readable format of the LocalForceCloseInitiator.
func (l *LocalForceCloseInitiator) String() string {
return string(*l)
}

// SerializeLocalForceCloseInitiator writes out the passed set of
// LocalForceCloseInitiator to the passed writer.
func SerializeLocalForceCloseInitiator(w io.Writer,
lc *LocalForceCloseInitiator) error {

localForceCloseReasonByte := []byte(*lc)

tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(localForceCloseInitiatorType,
&localForceCloseReasonByte),
)

if err != nil {
return err
}
var b bytes.Buffer
err = tlvStream.Encode(&b)
if err != nil {
return err
}

err = binary.Write(w, byteOrder, uint64(b.Len()))
if err != nil {
return err
}

if _, err = w.Write(b.Bytes()); err != nil {
return err
}

return nil
}

// DeserializeLocalForceCloseInitiator reads out the ForceCloseInitiator from
// the reader.
func DeserializeLocalForceCloseInitiator(r io.Reader) (LocalForceCloseInitiator,
error) {

var (
lc []byte
)

tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(
localForceCloseInitiatorType, &lc,
),
)

Copy link
Member

Choose a reason for hiding this comment

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

no need for the newline?

if err != nil {
return LocalForceCloseInitiator(lc), err
}

var bodyLen int64
err = binary.Read(r, byteOrder, &bodyLen)
if err != nil {
return LocalForceCloseInitiator(lc), err
}

lr := io.LimitReader(r, bodyLen)
if err = tlvStream.Decode(lr); err != nil {
return LocalForceCloseInitiator(lc), err
}

return LocalForceCloseInitiator(lc), nil
}

// 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
42 changes: 42 additions & 0 deletions channeldb/channel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1633,3 +1633,45 @@ func TestOnionBlobIncorrectLength(t *testing.T) {
_, err := DeserializeHtlcs(&b)
require.ErrorIs(t, err, ErrOnionBlobLength)
}

// TestLocalForceCloseInitiatorEncoding tests serializing and deserializing
// LocalForceCloseInitiator.
func TestLocalForceCloseInitiatorEncoding(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
data LocalForceCloseInitiator
}{
{
name: "chain action initiated",
data: ChainActionsInitiated,
},
{
name: "user initiated",
data: ChainActionsInitiated,
},
{
name: "custom initiator",
data: LocalForceCloseInitiator("invalid update"),
},
}

for _, testCase := range testCases {
testCase := testCase

t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

var b bytes.Buffer
err := SerializeLocalForceCloseInitiator(&b,
&testCase.data)
require.NoError(t, err)

r := bytes.NewReader(b.Bytes())
data, err := DeserializeLocalForceCloseInitiator(r)
require.NoError(t, err)
require.Equal(t, testCase.data, data)
})
}
}
77 changes: 77 additions & 0 deletions contractcourt/briefcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ type ArbitratorLog interface {
// introduced.
FetchChainActions() (ChainActionMap, error)

// LogLocalForceCloseInitiator records the passed-in
// localForceCloseInitiator 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.).
LogLocalForceCloseInitiator(
initiator channeldb.LocalForceCloseInitiator) error

// FetchLocalForceCloseInitiator attempts to fetch the
// localForceCloseInitiator object that was previously logged in the
// ArbitratorLog.
FetchLocalForceCloseInitiator() (channeldb.LocalForceCloseInitiator,
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 @@ -366,6 +380,10 @@ var (
// taprootDataKey is the key we'll use to store taproot specific data
// for the set of channels we'll need to sweep/claim.
taprootDataKey = []byte("taproot-data")

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

var (
Expand Down Expand Up @@ -1047,6 +1065,65 @@ func (b *boltArbitratorLog) FetchChainActions() (ChainActionMap, error) {
return actionsMap, nil
}

// LogLocalForceCloseInitiator records the passed-in localForceCloseInitiator
// 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) LogLocalForceCloseInitiator(
info channeldb.LocalForceCloseInitiator) error {

return kvdb.Update(b.db, func(tx kvdb.RwTx) error {
scopeBucket, err := tx.CreateTopLevelBucket(b.scopeKey[:])
if err != nil {
return err
}
var buf bytes.Buffer
if err := channeldb.SerializeLocalForceCloseInitiator(&buf,
&info); err != nil {
return err
}

return scopeBucket.Put(localForceCloseInitiatorKey, buf.Bytes())
}, func() {})
}

// FetchLocalForceCloseInitiator attempts to fetch the localForceCloseInitiator
// object that was previously logged in the ArbitratorLog.
func (b *boltArbitratorLog) FetchLocalForceCloseInitiator() (
channeldb.LocalForceCloseInitiator,
error) {

var init channeldb.LocalForceCloseInitiator
err := kvdb.View(b.db, func(tx kvdb.RTx) error {
hieblmi marked this conversation as resolved.
Show resolved Hide resolved
scopeBucket := tx.ReadBucket(b.scopeKey[:])
if scopeBucket == nil {
return errScopeBucketNoExist
}

infoBytes := scopeBucket.Get(localForceCloseInitiatorKey)
if infoBytes == nil {
return nil
}

infoReader := bytes.NewReader(infoBytes)
lcInfo, err := channeldb.DeserializeLocalForceCloseInitiator(
infoReader)
if err != nil {
return err
}

init = lcInfo

return nil
}, func() {})

if err != nil {
return "", err
}

return init, 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
15 changes: 15 additions & 0 deletions contractcourt/channel_arbitrator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type mockArbitratorLog struct {
failCommitState ArbitratorState
resolutions *ContractResolutions
resolvers map[ContractResolver]struct{}
localFCInfo channeldb.LocalForceCloseInitiator

commitSet *CommitSet

Expand Down Expand Up @@ -134,6 +135,20 @@ func (b *mockArbitratorLog) FetchChainActions() (ChainActionMap, error) {
return nil, nil
}

func (b *mockArbitratorLog) LogLocalForceCloseInitiator(
init channeldb.LocalForceCloseInitiator) error {

b.localFCInfo = init
return nil
}

func (b *mockArbitratorLog) FetchLocalForceCloseInitiator() (
channeldb.LocalForceCloseInitiator,
error) {

return b.localFCInfo, nil
}

func (b *mockArbitratorLog) InsertConfirmedCommitSet(c *CommitSet) error {
b.commitSet = c
return nil
Expand Down