From 06051c917ecd1b9a62232d12f97030824e237067 Mon Sep 17 00:00:00 2001 From: Boris Nagaev Date: Sun, 5 Nov 2023 19:39:35 -0300 Subject: [PATCH] chanbackup: add Single.CloseTxInputs field The field is optional. It stores inputs needed to produce signed commit tx using chantools scbforceclose, which calls function GetSignedCommitTx. Pass option WithCloseTxInputs(true) to functions producing chanbackup.Single: FetchBackupForChan, FetchStaticChanBackups, NewSubSwapper to tell them to include CloseTxInputs into backups. --- chanbackup/backup.go | 66 ++++++++++++++++++++++- chanbackup/pubsub.go | 26 +++++++-- chanbackup/single.go | 98 +++++++++++++++++++++++++++++++++- chanbackup/single_test.go | 108 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 291 insertions(+), 7 deletions(-) diff --git a/chanbackup/backup.go b/chanbackup/backup.go index 9a61d23cd3..6142702ba5 100644 --- a/chanbackup/backup.go +++ b/chanbackup/backup.go @@ -53,11 +53,60 @@ func assembleChanBackup(addrSource AddressSource, return &single, nil } +func buildCloseTxInputs(targetChan *channeldb.OpenChannel) *CloseTxInputs { + log.Debugf("Crafting CloseTxInputs for ChannelPoint(%v)", + targetChan.FundingOutpoint) + + localCommit := targetChan.LocalCommitment + + if localCommit.CommitTx == nil { + log.Infof("CommitTx is nil for ChannelPoint(%v), "+ + "skipping CloseTxInputs. This is possible when "+ + "DLP is active.", targetChan.FundingOutpoint) + + return nil + } + + inputs := &CloseTxInputs{ + CommitTx: localCommit.CommitTx, + CommitSig: localCommit.CommitSig, + } + + if targetChan.ChanType.IsTaproot() { + inputs.CommitHeight = localCommit.CommitHeight + } + + return inputs +} + +// BackupConfig contains options of backup creation process. +type BackupConfig struct { + // Whether to put CloseTxInputs into a backup. A backup with this data + // can be used by "chantools scbforceclose" command. + includeCloseTxInputs bool +} + +// BackupOption sets an option in BackupConfig. +type BackupOption func(*BackupConfig) + +// WithCloseTxInputs specifies if SCB must contain inputs needed to produce +// a force close transaction from it using "chantools scbforceclose". +func WithCloseTxInputs(include bool) BackupOption { + return func(c *BackupConfig) { + c.includeCloseTxInputs = include + } +} + // FetchBackupForChan attempts to create a plaintext static channel backup for // the target channel identified by its channel point. If we're unable to find // the target channel, then an error will be returned. func FetchBackupForChan(chanPoint wire.OutPoint, chanSource LiveChannelSource, - addrSource AddressSource) (*Single, error) { + addrSource AddressSource, options ...BackupOption) (*Single, error) { + + var config BackupConfig + for _, opt := range options { + opt(&config) + } // First, we'll query the channel source to see if the channel is known // and open within the database. @@ -75,13 +124,22 @@ func FetchBackupForChan(chanPoint wire.OutPoint, chanSource LiveChannelSource, return nil, fmt.Errorf("unable to create chan backup: %v", err) } + if config.includeCloseTxInputs { + staticChanBackup.CloseTxInputs = buildCloseTxInputs(targetChan) + } + return staticChanBackup, nil } // FetchStaticChanBackups will return a plaintext static channel back up for // all known active/open channels within the passed channel source. func FetchStaticChanBackups(chanSource LiveChannelSource, - addrSource AddressSource) ([]Single, error) { + addrSource AddressSource, options ...BackupOption) ([]Single, error) { + + var config BackupConfig + for _, opt := range options { + opt(&config) + } // First, we'll query the backup source for information concerning all // currently open and available channels. @@ -100,6 +158,10 @@ func FetchStaticChanBackups(chanSource LiveChannelSource, return nil, err } + if config.includeCloseTxInputs { + chanBackup.CloseTxInputs = buildCloseTxInputs(openChan) + } + staticChanBackups = append(staticChanBackups, *chanBackup) } diff --git a/chanbackup/pubsub.go b/chanbackup/pubsub.go index 3e1580ddd2..b69da62356 100644 --- a/chanbackup/pubsub.go +++ b/chanbackup/pubsub.go @@ -97,6 +97,10 @@ type SubSwapper struct { quit chan struct{} wg sync.WaitGroup + + // Whether to put CloseTxInputs into a backup. A backup with this data + // can be used by "chantools scbforceclose" command. + includeCloseTxInputs bool } // NewSubSwapper creates a new instance of the SubSwapper given the starting @@ -104,7 +108,13 @@ type SubSwapper struct { // updates, pack a multi backup, and swap the current best backup from its // storage location. func NewSubSwapper(startingChans []Single, chanNotifier ChannelNotifier, - keyRing keychain.KeyRing, backupSwapper Swapper) (*SubSwapper, error) { + keyRing keychain.KeyRing, backupSwapper Swapper, + options ...BackupOption) (*SubSwapper, error) { + + var config BackupConfig + for _, opt := range options { + opt(&config) + } // First, we'll subscribe to the latest set of channel updates given // the set of channels we already know of. @@ -130,6 +140,8 @@ func NewSubSwapper(startingChans []Single, chanNotifier ChannelNotifier, keyRing: keyRing, Swapper: backupSwapper, quit: make(chan struct{}), + + includeCloseTxInputs: config.includeCloseTxInputs, }, nil } @@ -265,9 +277,17 @@ func (s *SubSwapper) backupUpdater() { log.Debugf("Adding channel %v to backup state", newChan.FundingOutpoint) - s.backupState[newChan.FundingOutpoint] = NewSingle( - newChan.OpenChannel, newChan.Addrs, + single := NewSingle( + newChan.OpenChannel, + newChan.Addrs, ) + if s.includeCloseTxInputs { + inputs := buildCloseTxInputs( + newChan.OpenChannel, + ) + single.CloseTxInputs = inputs + } + s.backupState[newChan.FundingOutpoint] = single } // For all closed channels, we'll remove the prior diff --git a/chanbackup/single.go b/chanbackup/single.go index ab982d59c6..97b115ce1a 100644 --- a/chanbackup/single.go +++ b/chanbackup/single.go @@ -52,6 +52,10 @@ const ( // SimpleTaprootVersion is a version that denotes this channel is using // the musig2 based taproot commitment format. SimpleTaprootVersion = 5 + + // Byte mask used that is ORed to version byte on wire indicating that + // the backup has CloseTxInputs. + closeTxVersionMask = 1 << 7 ) // Single is a static description of an existing channel that can be used for @@ -138,6 +142,37 @@ type Single struct { // // - ScriptEnforcedLeaseVersion LeaseExpiry uint32 + + // CloseTxInputs contains data needed to produce a force close tx + // using "chantools scbforceclose". + // + // The field is optional. + // + // See WithCloseTxInputs. + CloseTxInputs *CloseTxInputs +} + +// CloseTxInputs contains data needed to produce a force close transaction +// using "chantools scbforceclose". +type CloseTxInputs struct { + // CommitTx is the latest version of the commitment state, broadcast + // able by us, but not signed. + // It can be signed by "chantools scbforceclose" command. + CommitTx *wire.MsgTx + + // CommitSig is one half of the signature required to fully complete + // the script for the commitment transaction above. This is the + // signature signed by the remote party for our version of the + // commitment transactions. + CommitSig []byte + + // CommitHeight is the update number that this ChannelDelta represents + // the total number of commitment updates to this point. This can be + // viewed as sort of a "commitment height" as this number is + // monotonically increasing. + // + // This field is filled only for taproot channels. + CommitHeight uint64 } // NewSingle creates a new static channel backup based on an existing open @@ -320,6 +355,33 @@ func (s *Single) Serialize(w io.Writer) error { } } + version := s.Version + if s.CloseTxInputs != nil { + version |= closeTxVersionMask + + err := s.CloseTxInputs.CommitTx.Serialize(&singleBytes) + if err != nil { + return err + } + + if err := lnwire.WriteElements( + &singleBytes, + uint16(len(s.CloseTxInputs.CommitSig)), + s.CloseTxInputs.CommitSig, + ); err != nil { + return err + } + + if s.Version == SimpleTaprootVersion { + if err := lnwire.WriteElements( + &singleBytes, + s.CloseTxInputs.CommitHeight, + ); err != nil { + return err + } + } + } + // TODO(yy): remove the type assertion when we finished refactoring db // into using write buffer. buf, ok := w.(*bytes.Buffer) @@ -329,7 +391,7 @@ func (s *Single) Serialize(w io.Writer) error { return lnwire.WriteElements( buf, - byte(s.Version), + byte(version), uint16(len(singleBytes.Bytes())), singleBytes.Bytes(), ) @@ -420,7 +482,9 @@ func (s *Single) Deserialize(r io.Reader) error { return err } - s.Version = SingleBackupVersion(version) + hasCloseTx := (version & closeTxVersionMask) != 0 + + s.Version = SingleBackupVersion(version &^ closeTxVersionMask) switch s.Version { case DefaultSingleVersion: @@ -533,6 +597,36 @@ func (s *Single) Deserialize(r io.Reader) error { } } + if hasCloseTx { + commitTx := &wire.MsgTx{} + if err := commitTx.Deserialize(r); err != nil { + return err + } + + var commitSigLen uint16 + if err := lnwire.ReadElement(r, &commitSigLen); err != nil { + return err + } + commitSig := make([]byte, commitSigLen) + if err := lnwire.ReadElement(r, commitSig); err != nil { + return err + } + + var commitHeight uint64 + if s.Version == SimpleTaprootVersion { + err := lnwire.ReadElement(r, &commitHeight) + if err != nil { + return err + } + } + + s.CloseTxInputs = &CloseTxInputs{ + CommitTx: commitTx, + CommitSig: commitSig, + CommitHeight: commitHeight, + } + } + return nil } diff --git a/chanbackup/single_test.go b/chanbackup/single_test.go index bf7088e8cb..aa990bd6c3 100644 --- a/chanbackup/single_test.go +++ b/chanbackup/single_test.go @@ -2,6 +2,7 @@ package chanbackup import ( "bytes" + "encoding/hex" "math" "math/rand" "net" @@ -95,6 +96,40 @@ func assertSingleEqual(t *testing.T, a, b Single) { a.Addresses[i], b.Addresses[i]) } } + + if (a.CloseTxInputs != nil) != (b.CloseTxInputs != nil) { + t.Fatalf("closeTxInputs mismatch: %v vs %v", + (a.CloseTxInputs != nil), (b.CloseTxInputs != nil)) + } + if a.CloseTxInputs != nil { + ai := a.CloseTxInputs + bi := b.CloseTxInputs + var abuf, bbuf bytes.Buffer + if err := ai.CommitTx.Serialize(&abuf); err != nil { + t.Fatalf("a.CommitTx.Serialize failed: %v", err) + } + if err := bi.CommitTx.Serialize(&bbuf); err != nil { + t.Fatalf("b.CommitTx.Serialize failed: %v", err) + } + aBytes := abuf.Bytes() + bBytes := bbuf.Bytes() + if !bytes.Equal(aBytes, bBytes) { + t.Fatalf("commitTx mismatch: %s vs %s", + hex.EncodeToString(aBytes), + hex.EncodeToString(bBytes)) + } + + if !bytes.Equal(ai.CommitSig, bi.CommitSig) { + t.Fatalf("commitSig mismatch: %s vs %s", + hex.EncodeToString(ai.CommitSig), + hex.EncodeToString(bi.CommitSig)) + } + + if ai.CommitHeight != bi.CommitHeight { + t.Fatalf("commitHeight mismatch: %d vs %d", + ai.CommitHeight, bi.CommitHeight) + } + } } func genRandomOpenChannelShell() (*channeldb.OpenChannel, error) { @@ -210,11 +245,23 @@ func TestSinglePackUnpack(t *testing.T) { keyRing := &lnencrypt.MockKeyRing{} + commitTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + {PreviousOutPoint: wire.OutPoint{Hash: [32]byte{1}}}, + }, + TxOut: []*wire.TxOut{ + {Value: 1e8, PkScript: []byte("1")}, + {Value: 2e8, PkScript: []byte("2")}, + }, + } + versionTestCases := []struct { // version is the pack/unpack version that we should use to // decode/encode the final SCB. version SingleBackupVersion + closeTxInputs *CloseTxInputs + // valid tests us if this test case should pass or not. valid bool }{ @@ -257,11 +304,72 @@ func TestSinglePackUnpack(t *testing.T) { version: 99, valid: false, }, + + // Versions with CloseTxInputs. + { + version: DefaultSingleVersion, + closeTxInputs: &CloseTxInputs{ + CommitTx: commitTx, + CommitSig: []byte("signature"), + }, + valid: true, + }, + { + version: TweaklessCommitVersion, + closeTxInputs: &CloseTxInputs{ + CommitTx: commitTx, + CommitSig: []byte("signature"), + }, + valid: true, + }, + { + version: AnchorsCommitVersion, + closeTxInputs: &CloseTxInputs{ + CommitTx: commitTx, + CommitSig: []byte("signature"), + }, + valid: true, + }, + { + version: ScriptEnforcedLeaseVersion, + closeTxInputs: &CloseTxInputs{ + CommitTx: commitTx, + CommitSig: []byte("signature"), + }, + valid: true, + }, + { + version: SimpleTaprootVersion, + closeTxInputs: &CloseTxInputs{ + CommitTx: commitTx, + CommitSig: []byte("signature"), + CommitHeight: 42, + }, + valid: true, + }, + { + version: 99, + closeTxInputs: &CloseTxInputs{ + CommitTx: commitTx, + CommitSig: []byte("signature"), + }, + valid: false, + }, + { + version: 99, + closeTxInputs: &CloseTxInputs{ + CommitTx: commitTx, + CommitSig: []byte("signature"), + CommitHeight: 42, + }, + valid: false, + }, } for i, versionCase := range versionTestCases { // First, we'll re-assign SCB version to what was indicated in // the test case. singleChanBackup.Version = versionCase.version + singleChanBackup.CloseTxInputs = versionCase.closeTxInputs var b bytes.Buffer