From fcb314bf2cd4e52a2f949affc9d8672c65233532 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 10 Mar 2022 21:40:01 +0800 Subject: [PATCH 01/22] channeldb: add more verbose docs --- lnwallet/channel.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index df5b244d8..63cd9a36a 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -1036,26 +1036,29 @@ type updateLog struct { logIndex uint64 // htlcCounter is a monotonically increasing integer that tracks the - // total number of offered HTLC's by the owner of this update log. We - // use a distinct index for this purpose, as update's that remove - // entries from the log will be indexed using this counter. + // total number of offered HTLC's by the owner of this update log, + // hence the `Add` update type. We use a distinct index for this + // purpose, as update's that remove entries from the log will be + // indexed using this counter. htlcCounter uint64 // List is the updatelog itself, we embed this value so updateLog has // access to all the method of a list.List. *list.List - // updateIndex is an index that maps a particular entries index to the - // list element within the list.List above. + // updateIndex maps a `logIndex` to a particular update entry. It + // deals with the four update types: + // `Fail|MalformedFail|Settle|FeeUpdate` updateIndex map[uint64]*list.Element - // offerIndex is an index that maps the counter for offered HTLC's to - // their list element within the main list.List. + // htlcIndex maps a `htlcCounter` to an offered HTLC entry, hence the + // `Add` update. htlcIndex map[uint64]*list.Element // modifiedHtlcs is a set that keeps track of all the current modified - // htlcs. A modified HTLC is one that's present in the log, and has as - // a pending fail or settle that's attempting to consume it. + // htlcs, hence update types `Fail|MalformedFail|Settle`. A modified + // HTLC is one that's present in the log, and has as a pending fail or + // settle that's attempting to consume it. modifiedHtlcs map[uint64]struct{} } From 5f508f9278e53e1223d9fa3815d027d0bf877c54 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 16 Mar 2022 18:15:06 +0800 Subject: [PATCH 02/22] channeldb: replace method RevocationLogTail This commit replaces the method RevocationLogTail which in used in our unit test with a private method revocationLogTailCommitHeight. The new method returns the commit height only since that's what's needed in the test. --- channeldb/channel.go | 40 +++++++++++++++++---------------------- channeldb/channel_test.go | 8 ++++---- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index b17e398a8..b07843563 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2608,22 +2608,24 @@ func (c *OpenChannel) RemoveFwdPkgs(heights ...uint64) error { }, func() {}) } -// RevocationLogTail returns the "tail", or the end of the current revocation -// log. This entry represents the last previous state for the remote node's -// commitment chain. The ChannelDelta returned by this method will always lag -// one state behind the most current (unrevoked) state of the remote node's -// commitment chain. -func (c *OpenChannel) RevocationLogTail() (*ChannelCommitment, error) { +// revocationLogTailCommitHeight returns the commit height at the end of the +// revocation log. This entry represents the last previous state for the remote +// node's commitment chain. The ChannelDelta returned by this method will +// always lag one state behind the most current (unrevoked) state of the remote +// node's commitment chain. +// NOTE: used in unit test only. +func (c *OpenChannel) revocationLogTailCommitHeight() (uint64, error) { c.RLock() defer c.RUnlock() + var height uint64 + // If we haven't created any state updates yet, then we'll exit early as // there's nothing to be found on disk in the revocation bucket. if c.RemoteCommitment.CommitHeight == 0 { - return nil, nil + return height, nil } - var commit ChannelCommitment if err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, @@ -2638,27 +2640,19 @@ func (c *OpenChannel) RevocationLogTail() (*ChannelCommitment, error) { } // Once we have the bucket that stores the revocation log from - // this channel, we'll jump to the _last_ key in bucket. As we - // store the update number on disk in a big-endian format, - // this will retrieve the latest entry. + // this channel, we'll jump to the _last_ key in bucket. Since + // the key is the commit height, we'll decode the bytes and + // return it. cursor := logBucket.ReadCursor() - _, tailLogEntry := cursor.Last() - logEntryReader := bytes.NewReader(tailLogEntry) - - // Once we have the entry, we'll decode it into the channel - // delta pointer we created above. - var dbErr error - commit, dbErr = deserializeChanCommit(logEntryReader) - if dbErr != nil { - return dbErr - } + rawHeight, _ := cursor.Last() + height = byteOrder.Uint64(rawHeight) return nil }, func() {}); err != nil { - return nil, err + return height, err } - return &commit, nil + return height, nil } // CommitmentHeight returns the current commitment height. The commitment diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index b74d125fb..85ecd9eb6 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -791,11 +791,11 @@ func TestChannelStateTransition(t *testing.T) { // The state number recovered from the tail of the revocation log // should be identical to this current state. - logTail, err := channel.RevocationLogTail() + logTailHeight, err := channel.revocationLogTailCommitHeight() if err != nil { t.Fatalf("unable to retrieve log: %v", err) } - if logTail.CommitHeight != oldRemoteCommit.CommitHeight { + if logTailHeight != oldRemoteCommit.CommitHeight { t.Fatal("update number doesn't match") } @@ -827,11 +827,11 @@ func TestChannelStateTransition(t *testing.T) { // Once again, state number recovered from the tail of the revocation // log should be identical to this current state. - logTail, err = channel.RevocationLogTail() + logTailHeight, err = channel.revocationLogTailCommitHeight() if err != nil { t.Fatalf("unable to retrieve log: %v", err) } - if logTail.CommitHeight != oldRemoteCommit.CommitHeight { + if logTailHeight != oldRemoteCommit.CommitHeight { t.Fatal("update number doesn't match") } From fea97699ec24e51d2df753db3e28c1e2ac800c80 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Tue, 15 Mar 2022 15:23:44 +0800 Subject: [PATCH 03/22] contractcourt: remove unused bucket justice-txn We never writes data to this bucket so it's removed. --- contractcourt/breacharbiter.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/contractcourt/breacharbiter.go b/contractcourt/breacharbiter.go index 4ff90be81..1735d0cec 100644 --- a/contractcourt/breacharbiter.go +++ b/contractcourt/breacharbiter.go @@ -47,11 +47,6 @@ var ( // procedure, we can recover and continue from the persisted state. retributionBucket = []byte("retribution") - // justiceTxnBucket holds the finalized justice transactions for all - // breached contracts. Entries are added to the justice txn bucket just - // before broadcasting the sweep txn. - justiceTxnBucket = []byte("justice-txn") - // errBrarShuttingDown is an error returned if the breacharbiter has // been signalled to exit. errBrarShuttingDown = errors.New("breacharbiter shutting down") @@ -1578,18 +1573,7 @@ func (rs *RetributionStore) Remove(chanPoint *wire.OutPoint) error { // Remove the persisted retribution info and finalized justice // transaction. - if err := retBucket.Delete(chanBytes); err != nil { - return err - } - - // If we have not finalized this channel breach, we can exit - // early. - justiceBkt := tx.ReadWriteBucket(justiceTxnBucket) - if justiceBkt == nil { - return nil - } - - return justiceBkt.Delete(chanBytes) + return retBucket.Delete(chanBytes) }, func() {}) } From acde626ac95969d4cbec183fd9e7e4287e789457 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 20 Mar 2022 00:59:19 +0800 Subject: [PATCH 04/22] lnwallet+htlcswitch: remove the redundant param in OweCommitment This commit removes the bool param found in OweCommitment, which we only ever use `true`. --- htlcswitch/link.go | 4 ++-- lnwallet/channel.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 99b758ce9..9f2cfc494 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -1882,7 +1882,7 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // If both commitment chains are fully synced from our PoV, // then we don't need to reply with a signature as both sides // already have a commitment with the latest accepted. - if !l.channel.OweCommitment(true) { + if !l.channel.OweCommitment() { return } @@ -1967,7 +1967,7 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // processRemoteAdds. Also in case there are no local updates, // but there are still remote updates that are not in the remote // commit tx yet, send out an update. - if l.channel.OweCommitment(true) { + if l.channel.OweCommitment() { if !l.updateCommitTxOrFail() { return } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 63cd9a36a..30b1501ec 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -3825,7 +3825,7 @@ func (lc *LightningChannel) ProcessChanSyncMsg( // but died before the signature was sent. We re-transmit our // revocation, but also initiate a state transition to re-sync // them. - if lc.OweCommitment(true) { + if lc.OweCommitment() { commitSig, htlcSigs, _, err := lc.SignNextCommitment() switch { @@ -4539,11 +4539,11 @@ func (lc *LightningChannel) IsChannelClean() bool { // out a commitment signature because there are outstanding local updates and/or // updates in the local commit tx that aren't reflected in the remote commit tx // yet. -func (lc *LightningChannel) OweCommitment(local bool) bool { +func (lc *LightningChannel) OweCommitment() bool { lc.RLock() defer lc.RUnlock() - return lc.oweCommitment(local) + return lc.oweCommitment(true) } // oweCommitment is the internal version of OweCommitment. This function expects From 9a4589d54e7032b0a9e7ea30ceb038eca7595881 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 16 Mar 2022 20:02:00 +0800 Subject: [PATCH 05/22] lnwallet: remove unused field PendingHTLCs --- lnwallet/channel.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 30b1501ec..f27c55608 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2231,10 +2231,6 @@ type BreachRetribution struct { // RevokedStateNum is the revoked state number which was broadcast. RevokedStateNum uint64 - // PendingHTLCs is a slice of the HTLCs which were pending at this - // point within the channel's history transcript. - PendingHTLCs []channeldb.HTLC - // LocalOutputSignDesc is a SignDescriptor which is capable of // generating the signature necessary to sweep the output within the // BreachTransaction that pays directly us. @@ -2466,7 +2462,6 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, BreachTransaction: revokedSnapshot.CommitTx, BreachHeight: breachHeight, RevokedStateNum: stateNum, - PendingHTLCs: revokedSnapshot.Htlcs, LocalOutpoint: ourOutpoint, LocalOutputSignDesc: ourSignDesc, LocalDelay: ourDelay, From 291a8e4effb5f6c12247d0135c1b8cfd61e99406 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 6 Apr 2022 20:20:31 +0800 Subject: [PATCH 06/22] contractcourt: move breach handling into its own function This commit refactors the breach handling logic into its own function so that the related code can be unit tested. --- contractcourt/chain_watcher.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index 58d83d297..c9b5e7ef9 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -729,7 +729,6 @@ func (c *chainWatcher) handleKnownRemoteState( commitTxBroadcast := commitSpend.SpendingTx commitHash := commitTxBroadcast.TxHash() - spendHeight := uint32(commitSpend.SpendingHeight) switch { // If the spending transaction matches the current latest state, then @@ -780,8 +779,19 @@ func (c *chainWatcher) handleKnownRemoteState( return true, nil } + // This is neither a remote force close or a "future" commitment, we + // now check whether it's a remote breach and properly handle it. + return c.handlePossibleBreach(commitSpend, broadcastStateNum, chainSet) +} + +// handlePossibleBreach checks whether the remote has breached and dispatches a +// breach resolution to claim funds. +func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail, + broadcastStateNum uint64, chainSet *chainSet) (bool, error) { + // We check if we have a revoked state at this state num that matches // the spend transaction. + spendHeight := uint32(commitSpend.SpendingHeight) retribution, err := lnwallet.NewBreachRetribution( c.cfg.chanState, broadcastStateNum, spendHeight, ) @@ -801,6 +811,7 @@ func (c *chainWatcher) handleKnownRemoteState( // We found a revoked state at this height, but it could still be our // own broadcasted state we are looking at. Therefore check that the // commit matches before assuming it was a breach. + commitHash := commitSpend.SpendingTx.TxHash() if retribution.BreachTransaction.TxHash() != commitHash { return false, nil } From 22f8f6ed4ae868014b2df4e1d2e6fa47743a1680 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Apr 2022 02:04:50 +0800 Subject: [PATCH 07/22] channeldb+lnwallet: save initial balances during channel opening --- channeldb/channel.go | 21 ++++++++++++++++----- channeldb/channel_test.go | 2 ++ lnwallet/reservation.go | 6 ++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index b07843563..b8bb42cae 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -129,9 +129,9 @@ var ( // bucket for a given channel. frozenChanKey = []byte("frozen-chans") - // lastWasRevokeKey is a key that stores true when the last update we sent - // was a revocation and false when it was a commitment signature. This is - // nil in the case of new channels with no updates exchanged. + // lastWasRevokeKey is a key that stores true when the last update we + // sent was a revocation and false when it was a commitment signature. + // This is nil in the case of new channels with no updates exchanged. lastWasRevokeKey = []byte("last-was-revoke") ) @@ -657,6 +657,15 @@ type OpenChannel struct { // received within this channel. TotalMSatReceived lnwire.MilliSatoshi + // InitialLocalBalance is the balance we have during the channel + // opening. When we are not the initiator, this value represents the + // push amount. + InitialLocalBalance lnwire.MilliSatoshi + + // InitialRemoteBalance is the balance they have during the channel + // opening. + InitialRemoteBalance lnwire.MilliSatoshi + // LocalChanCfg is the channel configuration for the local node. LocalChanCfg ChannelConfig @@ -3322,7 +3331,8 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error { channel.chanStatus, channel.FundingBroadcastHeight, channel.NumConfsRequired, channel.ChannelFlags, channel.IdentityPub, channel.Capacity, channel.TotalMSatSent, - channel.TotalMSatReceived, + channel.TotalMSatReceived, channel.InitialLocalBalance, + channel.InitialRemoteBalance, ); err != nil { return err } @@ -3509,7 +3519,8 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error { &channel.chanStatus, &channel.FundingBroadcastHeight, &channel.NumConfsRequired, &channel.ChannelFlags, &channel.IdentityPub, &channel.Capacity, &channel.TotalMSatSent, - &channel.TotalMSatReceived, + &channel.TotalMSatReceived, &channel.InitialLocalBalance, + &channel.InitialRemoteBalance, ); err != nil { return err } diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 85ecd9eb6..b4b8afdeb 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -352,6 +352,8 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { Packager: NewChannelPackager(chanID), FundingTxn: channels.TestFundingTx, ThawHeight: uint32(defaultPendingHeight), + InitialLocalBalance: lnwire.MilliSatoshi(9000), + InitialRemoteBalance: lnwire.MilliSatoshi(3000), } } diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go index ee7de6485..c36ba170e 100644 --- a/lnwallet/reservation.go +++ b/lnwallet/reservation.go @@ -403,8 +403,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount, FeePerKw: btcutil.Amount(commitFeePerKw), CommitFee: commitFee, }, - ThawHeight: thawHeight, - Db: wallet.Cfg.Database, + ThawHeight: thawHeight, + Db: wallet.Cfg.Database, + InitialLocalBalance: ourBalance, + InitialRemoteBalance: theirBalance, }, pushMSat: pushMSat, pendingChanID: pendingChanID, From 8b289e79f54801405f1377f9d8f2e2324fce6167 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Apr 2022 11:45:05 +0800 Subject: [PATCH 08/22] channeldb+migration: export commonly used methods This commit exports several commonly used methods that can be used by later migrations. It also adds a channel commit deserializer. --- channeldb/migration24/migration.go | 4 +- channeldb/migration24/migration_test.go | 6 +- channeldb/migration_01_to_11/channel.go | 97 ++++++++++++++++++- channeldb/migration_01_to_11/codec.go | 12 +-- channeldb/migration_01_to_11/graph.go | 2 +- .../legacy_serialization.go | 2 +- .../migration_01_to_11/migrations_test.go | 6 +- 7 files changed, 109 insertions(+), 20 deletions(-) diff --git a/channeldb/migration24/migration.go b/channeldb/migration24/migration.go index 287955dbf..8382a9ba6 100644 --- a/channeldb/migration24/migration.go +++ b/channeldb/migration24/migration.go @@ -70,7 +70,7 @@ func MigrateFwdPkgCleanup(tx kvdb.RwTx) error { // Iterate over all close channels and remove their forwarding packages. for _, summery := range chanSummaries { - sourceBytes := makeLogKey(summery.ShortChanID.ToUint64()) + sourceBytes := MakeLogKey(summery.ShortChanID.ToUint64()) // First, we will try to find the corresponding bucket. If there // is not a nested bucket matching the ShortChanID, we will skip @@ -112,7 +112,7 @@ func deserializeCloseChannelSummary( } // makeLogKey converts a uint64 into an 8 byte array. -func makeLogKey(updateNum uint64) [8]byte { +func MakeLogKey(updateNum uint64) [8]byte { var key [8]byte binary.BigEndian.PutUint64(key[:], updateNum) return key diff --git a/channeldb/migration24/migration_test.go b/channeldb/migration24/migration_test.go index ea337e198..417c3d0ed 100644 --- a/channeldb/migration24/migration_test.go +++ b/channeldb/migration24/migration_test.go @@ -187,7 +187,7 @@ func genAfterMigration(deleted, untouched []int) func(kvdb.RwTx) error { // Reading deleted buckets should return nil for _, id := range deleted { chanID := lnwire.NewShortChanIDFromInt(uint64(id)) - sourceKey := makeLogKey(chanID.ToUint64()) + sourceKey := MakeLogKey(chanID.ToUint64()) sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:]) if sourceBkt != nil { return fmt.Errorf( @@ -200,7 +200,7 @@ func genAfterMigration(deleted, untouched []int) func(kvdb.RwTx) error { // Reading untouched buckets should return not nil for _, id := range untouched { chanID := lnwire.NewShortChanIDFromInt(uint64(id)) - sourceKey := makeLogKey(chanID.ToUint64()) + sourceKey := MakeLogKey(chanID.ToUint64()) sourceBkt := fwdPkgBkt.NestedReadBucket(sourceKey[:]) if sourceBkt == nil { return fmt.Errorf( @@ -259,7 +259,7 @@ func createTestFwdPkgBucket(tx kvdb.RwTx, chanID lnwire.ShortChannelID) error { return err } - source := makeLogKey(chanID.ToUint64()) + source := MakeLogKey(chanID.ToUint64()) if _, err := fwdPkgBkt.CreateBucketIfNotExists(source[:]); err != nil { return err } diff --git a/channeldb/migration_01_to_11/channel.go b/channeldb/migration_01_to_11/channel.go index 333bdf05f..47cbca355 100644 --- a/channeldb/migration_01_to_11/channel.go +++ b/channeldb/migration_01_to_11/channel.go @@ -616,7 +616,7 @@ func serializeChannelCloseSummary(w io.Writer, cs *ChannelCloseSummary) error { return err } - if err := writeChanConfig(w, &cs.LocalChanConfig); err != nil { + if err := WriteChanConfig(w, &cs.LocalChanConfig); err != nil { return err } @@ -680,7 +680,7 @@ func deserializeCloseChannelSummary(r io.Reader) (*ChannelCloseSummary, error) { return nil, err } - if err := readChanConfig(r, &c.LocalChanConfig); err != nil { + if err := ReadChanConfig(r, &c.LocalChanConfig); err != nil { return nil, err } @@ -731,7 +731,7 @@ func deserializeCloseChannelSummary(r io.Reader) (*ChannelCloseSummary, error) { return c, nil } -func writeChanConfig(b io.Writer, c *ChannelConfig) error { +func WriteChanConfig(b io.Writer, c *ChannelConfig) error { return WriteElements(b, c.DustLimit, c.MaxPendingAmount, c.ChanReserve, c.MinHTLC, c.MaxAcceptedHtlcs, c.CsvDelay, c.MultiSigKey, @@ -740,7 +740,7 @@ func writeChanConfig(b io.Writer, c *ChannelConfig) error { ) } -func readChanConfig(b io.Reader, c *ChannelConfig) error { +func ReadChanConfig(b io.Reader, c *ChannelConfig) error { return ReadElements(b, &c.DustLimit, &c.MaxPendingAmount, &c.ChanReserve, &c.MinHTLC, &c.MaxAcceptedHtlcs, &c.CsvDelay, @@ -749,3 +749,92 @@ func readChanConfig(b io.Reader, c *ChannelConfig) error { &c.HtlcBasePoint, ) } + +func DeserializeChanCommit(r io.Reader) (ChannelCommitment, error) { + var c ChannelCommitment + + err := ReadElements(r, + &c.CommitHeight, &c.LocalLogIndex, &c.LocalHtlcIndex, &c.RemoteLogIndex, + &c.RemoteHtlcIndex, &c.LocalBalance, &c.RemoteBalance, + &c.CommitFee, &c.FeePerKw, &c.CommitTx, &c.CommitSig, + ) + if err != nil { + return c, err + } + + c.Htlcs, err = DeserializeHtlcs(r) + if err != nil { + return c, err + } + + return c, nil +} + +// DeserializeHtlcs attempts to read out a slice of HTLC's from the passed +// io.Reader. The bytes within the passed reader MUST have been previously +// written to using the SerializeHtlcs function. +// +// NOTE: This API is NOT stable, the on-disk format will likely change in the +// future. +func DeserializeHtlcs(r io.Reader) ([]HTLC, error) { + var numHtlcs uint16 + if err := ReadElement(r, &numHtlcs); err != nil { + return nil, err + } + + var htlcs []HTLC + if numHtlcs == 0 { + return htlcs, nil + } + + htlcs = make([]HTLC, numHtlcs) + for i := uint16(0); i < numHtlcs; i++ { + if err := ReadElements(r, + &htlcs[i].Signature, &htlcs[i].RHash, &htlcs[i].Amt, + &htlcs[i].RefundTimeout, &htlcs[i].OutputIndex, + &htlcs[i].Incoming, &htlcs[i].OnionBlob, + &htlcs[i].HtlcIndex, &htlcs[i].LogIndex, + ); err != nil { + return htlcs, err + } + } + + return htlcs, nil +} + +func SerializeChanCommit(w io.Writer, c *ChannelCommitment) error { + if err := WriteElements(w, + c.CommitHeight, c.LocalLogIndex, c.LocalHtlcIndex, + c.RemoteLogIndex, c.RemoteHtlcIndex, c.LocalBalance, + c.RemoteBalance, c.CommitFee, c.FeePerKw, c.CommitTx, + c.CommitSig, + ); err != nil { + return err + } + + return SerializeHtlcs(w, c.Htlcs...) +} + +// SerializeHtlcs writes out the passed set of HTLC's into the passed writer +// using the current default on-disk serialization format. +// +// NOTE: This API is NOT stable, the on-disk format will likely change in the +// future. +func SerializeHtlcs(b io.Writer, htlcs ...HTLC) error { + numHtlcs := uint16(len(htlcs)) + if err := WriteElement(b, numHtlcs); err != nil { + return err + } + + for _, htlc := range htlcs { + if err := WriteElements(b, + htlc.Signature, htlc.RHash, htlc.Amt, htlc.RefundTimeout, + htlc.OutputIndex, htlc.Incoming, htlc.OnionBlob[:], + htlc.HtlcIndex, htlc.LogIndex, + ); err != nil { + return err + } + } + + return nil +} diff --git a/channeldb/migration_01_to_11/codec.go b/channeldb/migration_01_to_11/codec.go index 986bf9dbc..a87f6eda7 100644 --- a/channeldb/migration_01_to_11/codec.go +++ b/channeldb/migration_01_to_11/codec.go @@ -15,9 +15,9 @@ import ( "github.com/lightningnetwork/lnd/shachain" ) -// writeOutpoint writes an outpoint to the passed writer using the minimal +// WriteOutpoint writes an outpoint to the passed writer using the minimal // amount of bytes possible. -func writeOutpoint(w io.Writer, o *wire.OutPoint) error { +func WriteOutpoint(w io.Writer, o *wire.OutPoint) error { if _, err := w.Write(o.Hash[:]); err != nil { return err } @@ -28,9 +28,9 @@ func writeOutpoint(w io.Writer, o *wire.OutPoint) error { return nil } -// readOutpoint reads an outpoint from the passed reader that was previously +// ReadOutpoint reads an outpoint from the passed reader that was previously // written using the writeOutpoint struct. -func readOutpoint(r io.Reader, o *wire.OutPoint) error { +func ReadOutpoint(r io.Reader, o *wire.OutPoint) error { if _, err := io.ReadFull(r, o.Hash[:]); err != nil { return err } @@ -88,7 +88,7 @@ func WriteElement(w io.Writer, element interface{}) error { } case wire.OutPoint: - return writeOutpoint(w, &e) + return WriteOutpoint(w, &e) case lnwire.ShortChannelID: if err := binary.Write(w, byteOrder, e.ToUint64()); err != nil { @@ -258,7 +258,7 @@ func ReadElement(r io.Reader, element interface{}) error { } case *wire.OutPoint: - return readOutpoint(r, e) + return ReadOutpoint(r, e) case *lnwire.ShortChannelID: var a uint64 diff --git a/channeldb/migration_01_to_11/graph.go b/channeldb/migration_01_to_11/graph.go index 8e9c624d3..0f36a78ed 100644 --- a/channeldb/migration_01_to_11/graph.go +++ b/channeldb/migration_01_to_11/graph.go @@ -836,7 +836,7 @@ func deserializeChanEdgeInfo(r io.Reader) (ChannelEdgeInfo, error) { } edgeInfo.ChannelPoint = wire.OutPoint{} - if err := readOutpoint(r, &edgeInfo.ChannelPoint); err != nil { + if err := ReadOutpoint(r, &edgeInfo.ChannelPoint); err != nil { return ChannelEdgeInfo{}, err } if err := binary.Read(r, byteOrder, &edgeInfo.Capacity); err != nil { diff --git a/channeldb/migration_01_to_11/legacy_serialization.go b/channeldb/migration_01_to_11/legacy_serialization.go index 5d731bff6..f154f1804 100644 --- a/channeldb/migration_01_to_11/legacy_serialization.go +++ b/channeldb/migration_01_to_11/legacy_serialization.go @@ -34,7 +34,7 @@ func deserializeCloseChannelSummaryV6(r io.Reader) (*ChannelCloseSummary, error) return nil, err } - if err := readChanConfig(r, &c.LocalChanConfig); err != nil { + if err := ReadChanConfig(r, &c.LocalChanConfig); err != nil { return nil, err } diff --git a/channeldb/migration_01_to_11/migrations_test.go b/channeldb/migration_01_to_11/migrations_test.go index 5d7f91b88..9c47bdcdf 100644 --- a/channeldb/migration_01_to_11/migrations_test.go +++ b/channeldb/migration_01_to_11/migrations_test.go @@ -203,7 +203,7 @@ func TestMigrateOptionalChannelCloseSummaryFields(t *testing.T) { } var chanPointBuf bytes.Buffer - err = writeOutpoint(&chanPointBuf, &chanState.FundingOutpoint) + err = WriteOutpoint(&chanPointBuf, &chanState.FundingOutpoint) if err != nil { t.Fatalf("unable to write outpoint: %v", err) } @@ -303,7 +303,7 @@ func TestMigrateOptionalChannelCloseSummaryFields(t *testing.T) { t.Fatal(err) } - err = writeChanConfig(&buf, &cs.LocalChanConfig) + err = WriteChanConfig(&buf, &cs.LocalChanConfig) if err != nil { t.Fatal(err) } @@ -354,7 +354,7 @@ func TestMigrateOptionalChannelCloseSummaryFields(t *testing.T) { t.Fatal(err) } - err = writeChanConfig(&buf, &cs.LocalChanConfig) + err = WriteChanConfig(&buf, &cs.LocalChanConfig) if err != nil { t.Fatal(err) } From c9843fd206437768c3ae4767e4f99bd3a4b7c4a8 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Apr 2022 11:50:05 +0800 Subject: [PATCH 09/22] channeldb+migration: add migration to save the initial balances This commit adds a migration to patch the newly added fields, `InitialLocalBalance` and `InitialRemoteBalance` to channel info. --- channeldb/db.go | 7 + channeldb/migration25/channel.go | 722 +++++++++++++++++++++++++++++ channeldb/migration25/log.go | 14 + channeldb/migration25/migration.go | 211 +++++++++ 4 files changed, 954 insertions(+) create mode 100644 channeldb/migration25/channel.go create mode 100644 channeldb/migration25/log.go create mode 100644 channeldb/migration25/migration.go diff --git a/channeldb/db.go b/channeldb/db.go index 47ac204b8..b1e202c73 100644 --- a/channeldb/db.go +++ b/channeldb/db.go @@ -19,6 +19,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb/migration21" "github.com/lightningnetwork/lnd/channeldb/migration23" "github.com/lightningnetwork/lnd/channeldb/migration24" + "github.com/lightningnetwork/lnd/channeldb/migration25" "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11" "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/kvdb" @@ -205,6 +206,12 @@ var ( number: 24, migration: migration24.MigrateFwdPkgCleanup, }, + { + // Save the initial local/remote balances in channel + // info. + number: 25, + migration: migration25.MigrateInitialBalances, + }, } // Big endian is the preferred byte order, due to cursor scans over diff --git a/channeldb/migration25/channel.go b/channeldb/migration25/channel.go new file mode 100644 index 000000000..c8b93bb39 --- /dev/null +++ b/channeldb/migration25/channel.go @@ -0,0 +1,722 @@ +package migration25 + +import ( + "bytes" + "fmt" + "io" + "strconv" + "strings" + + lnwire "github.com/lightningnetwork/lnd/channeldb/migration/lnwire21" + mig24 "github.com/lightningnetwork/lnd/channeldb/migration24" + mig "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11" + "github.com/lightningnetwork/lnd/keychain" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/tlv" +) + +const ( + // A tlv type definition used to serialize and deserialize a KeyLocator + // from the database. + keyLocType tlv.Type = 1 +) + +var ( + // chanCommitmentKey can be accessed within the sub-bucket for a + // particular channel. This key stores the up to date commitment state + // for a particular channel party. Appending a 0 to the end of this key + // indicates it's the commitment for the local party, and appending a 1 + // to the end of this key indicates it's the commitment for the remote + // party. + chanCommitmentKey = []byte("chan-commitment-key") + + // revocationLogBucketLegacy is the legacy bucket where we store the + // revocation log in old format. + revocationLogBucketLegacy = []byte("revocation-log-key") + + // localUpfrontShutdownKey can be accessed within the bucket for a + // channel (identified by its chanPoint). This key stores an optional + // upfront shutdown script for the local peer. + localUpfrontShutdownKey = []byte("local-upfront-shutdown-key") + + // remoteUpfrontShutdownKey can be accessed within the bucket for a + // channel (identified by its chanPoint). This key stores an optional + // upfront shutdown script for the remote peer. + remoteUpfrontShutdownKey = []byte("remote-upfront-shutdown-key") + + // lastWasRevokeKey is a key that stores true when the last update we + // sent was a revocation and false when it was a commitment signature. + // This is nil in the case of new channels with no updates exchanged. + lastWasRevokeKey = []byte("last-was-revoke") + + // ErrNoChanInfoFound is returned when a particular channel does not + // have any channels state. + ErrNoChanInfoFound = fmt.Errorf("no chan info found") + + // ErrNoPastDeltas is returned when the channel delta bucket hasn't been + // created. + ErrNoPastDeltas = fmt.Errorf("channel has no recorded deltas") + + // ErrLogEntryNotFound is returned when we cannot find a log entry at + // the height requested in the revocation log. + ErrLogEntryNotFound = fmt.Errorf("log entry not found") + + // ErrNoCommitmentsFound is returned when a channel has not set + // commitment states. + ErrNoCommitmentsFound = fmt.Errorf("no commitments found") +) + +// ChannelType is an enum-like type that describes one of several possible +// channel types. Each open channel is associated with a particular type as the +// channel type may determine how higher level operations are conducted such as +// fee negotiation, channel closing, the format of HTLCs, etc. Structure-wise, +// a ChannelType is a bit field, with each bit denoting a modification from the +// base channel type of single funder. +type ChannelType uint8 + +const ( + // NOTE: iota isn't used here for this enum needs to be stable + // long-term as it will be persisted to the database. + + // SingleFunderBit represents a channel wherein one party solely funds + // the entire capacity of the channel. + SingleFunderBit ChannelType = 0 + + // DualFunderBit represents a channel wherein both parties contribute + // funds towards the total capacity of the channel. The channel may be + // funded symmetrically or asymmetrically. + DualFunderBit ChannelType = 1 << 0 + + // SingleFunderTweaklessBit is similar to the basic SingleFunder channel + // type, but it omits the tweak for one's key in the commitment + // transaction of the remote party. + SingleFunderTweaklessBit ChannelType = 1 << 1 + + // NoFundingTxBit denotes if we have the funding transaction locally on + // disk. This bit may be on if the funding transaction was crafted by a + // wallet external to the primary daemon. + NoFundingTxBit ChannelType = 1 << 2 + + // AnchorOutputsBit indicates that the channel makes use of anchor + // outputs to bump the commitment transaction's effective feerate. This + // channel type also uses a delayed to_remote output script. + 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 + + // ZeroHtlcTxFeeBit indicates that the channel should use zero-fee + // second-level HTLC transactions. + ZeroHtlcTxFeeBit ChannelType = 1 << 5 + + // LeaseExpirationBit indicates that the channel has been leased for a + // period of time, constraining every output that pays to the channel + // initiator with an additional CLTV of the lease maturity. + LeaseExpirationBit ChannelType = 1 << 6 +) + +// IsSingleFunder returns true if the channel type if one of the known single +// funder variants. +func (c ChannelType) IsSingleFunder() bool { + return c&DualFunderBit == 0 +} + +// IsDualFunder returns true if the ChannelType has the DualFunderBit set. +func (c ChannelType) IsDualFunder() bool { + return c&DualFunderBit == DualFunderBit +} + +// IsTweakless returns true if the target channel uses a commitment that +// doesn't tweak the key for the remote party. +func (c ChannelType) IsTweakless() bool { + return c&SingleFunderTweaklessBit == SingleFunderTweaklessBit +} + +// HasFundingTx returns true if this channel type is one that has a funding +// transaction stored locally. +func (c ChannelType) HasFundingTx() bool { + return c&NoFundingTxBit == 0 +} + +// HasAnchors returns true if this channel type has anchor outputs on its +// commitment. +func (c ChannelType) HasAnchors() bool { + return c&AnchorOutputsBit == AnchorOutputsBit +} + +// ZeroHtlcTxFee returns true if this channel type uses second-level HTLC +// transactions signed with zero-fee. +func (c ChannelType) ZeroHtlcTxFee() bool { + return c&ZeroHtlcTxFeeBit == ZeroHtlcTxFeeBit +} + +// 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 +} + +// HasLeaseExpiration returns true if the channel originated from a lease. +func (c ChannelType) HasLeaseExpiration() bool { + return c&LeaseExpirationBit == LeaseExpirationBit +} + +// ChannelStatus is a bit vector used to indicate whether an OpenChannel is in +// the default usable state, or a state where it shouldn't be used. +type ChannelStatus uint8 + +var ( + // ChanStatusDefault is the normal state of an open channel. + ChanStatusDefault ChannelStatus + + // ChanStatusBorked indicates that the channel has entered an + // irreconcilable state, triggered by a state desynchronization or + // channel breach. Channels in this state should never be added to the + // htlc switch. + ChanStatusBorked ChannelStatus = 1 + + // ChanStatusCommitBroadcasted indicates that a commitment for this + // channel has been broadcasted. + ChanStatusCommitBroadcasted ChannelStatus = 1 << 1 + + // ChanStatusLocalDataLoss indicates that we have lost channel state + // for this channel, and broadcasting our latest commitment might be + // considered a breach. + // + // TODO(halseh): actually enforce that we are not force closing such a + // channel. + ChanStatusLocalDataLoss ChannelStatus = 1 << 2 + + // ChanStatusRestored is a status flag that signals that the channel + // has been restored, and doesn't have all the fields a typical channel + // will have. + ChanStatusRestored ChannelStatus = 1 << 3 + + // ChanStatusCoopBroadcasted indicates that a cooperative close for + // this channel has been broadcasted. Older cooperatively closed + // channels will only have this status set. Newer ones will also have + // close initiator information stored using the local/remote initiator + // status. This status is set in conjunction with the initiator status + // so that we do not need to check multiple channel statues for + // cooperative closes. + ChanStatusCoopBroadcasted ChannelStatus = 1 << 4 + + // ChanStatusLocalCloseInitiator indicates that we initiated closing + // the channel. + ChanStatusLocalCloseInitiator ChannelStatus = 1 << 5 + + // ChanStatusRemoteCloseInitiator indicates that the remote node + // initiated closing the channel. + ChanStatusRemoteCloseInitiator ChannelStatus = 1 << 6 +) + +// chanStatusStrings maps a ChannelStatus to a human friendly string that +// describes that status. +var chanStatusStrings = map[ChannelStatus]string{ + ChanStatusDefault: "ChanStatusDefault", + ChanStatusBorked: "ChanStatusBorked", + ChanStatusCommitBroadcasted: "ChanStatusCommitBroadcasted", + ChanStatusLocalDataLoss: "ChanStatusLocalDataLoss", + ChanStatusRestored: "ChanStatusRestored", + ChanStatusCoopBroadcasted: "ChanStatusCoopBroadcasted", + ChanStatusLocalCloseInitiator: "ChanStatusLocalCloseInitiator", + ChanStatusRemoteCloseInitiator: "ChanStatusRemoteCloseInitiator", +} + +// orderedChanStatusFlags is an in-order list of all that channel status flags. +var orderedChanStatusFlags = []ChannelStatus{ + ChanStatusBorked, + ChanStatusCommitBroadcasted, + ChanStatusLocalDataLoss, + ChanStatusRestored, + ChanStatusCoopBroadcasted, + ChanStatusLocalCloseInitiator, + ChanStatusRemoteCloseInitiator, +} + +// String returns a human-readable representation of the ChannelStatus. +func (c ChannelStatus) String() string { + // If no flags are set, then this is the default case. + if c == ChanStatusDefault { + return chanStatusStrings[ChanStatusDefault] + } + + // Add individual bit flags. + statusStr := "" + for _, flag := range orderedChanStatusFlags { + if c&flag == flag { + statusStr += chanStatusStrings[flag] + "|" + c -= flag + } + } + + // Remove anything to the right of the final bar, including it as well. + statusStr = strings.TrimRight(statusStr, "|") + + // Add any remaining flags which aren't accounted for as hex. + if c != 0 { + statusStr += "|0x" + strconv.FormatUint(uint64(c), 16) + } + + // If this was purely an unknown flag, then remove the extra bar at the + // start of the string. + statusStr = strings.TrimLeft(statusStr, "|") + + return statusStr +} + +// OpenChannel embeds a mig.OpenChannel with the extra update-to-date fields. +// +// NOTE: doesn't have the Packager field as it's not used in current migration. +type OpenChannel struct { + mig.OpenChannel + + // ChanType denotes which type of channel this is. + ChanType ChannelType + + // chanStatus is the current status of this channel. If it is not in + // the state Default, it should not be used for forwarding payments. + chanStatus ChannelStatus + + // InitialLocalBalance is the balance we have during the channel + // opening. When we are not the initiator, this value represents the + // push amount. + InitialLocalBalance lnwire.MilliSatoshi + + // InitialRemoteBalance is the balance they have during the channel + // opening. + InitialRemoteBalance lnwire.MilliSatoshi + + // LocalShutdownScript is set to a pre-set script if the channel was + // opened by the local node with option_upfront_shutdown_script set. If + // the option was not set, the field is empty. + LocalShutdownScript lnwire.DeliveryAddress + + // RemoteShutdownScript is set to a pre-set script if the channel was + // opened by the remote node with option_upfront_shutdown_script set. + // If the option 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. If the value is lower than 500,000, then it's + // interpreted as a relative height, or an absolute height otherwise. + ThawHeight uint32 + + // LastWasRevoke is a boolean that determines if the last update we + // sent was a revocation (true) or a commitment signature (false). + LastWasRevoke bool + + // RevocationKeyLocator stores the KeyLocator information that we will + // need to derive the shachain root for this channel. This allows us to + // have private key isolation from lnd. + RevocationKeyLocator keychain.KeyLocator +} + +func (c *OpenChannel) hasChanStatus(status ChannelStatus) bool { + // Special case ChanStatusDefualt since it isn't actually flag, but a + // particular combination (or lack-there-of) of flags. + if status == ChanStatusDefault { + return c.chanStatus == ChanStatusDefault + } + + return c.chanStatus&status == status +} + +// FundingTxPresent returns true if expect the funding transcation to be found +// on disk or already populated within the passed open channel struct. +func (c *OpenChannel) FundingTxPresent() bool { + chanType := c.ChanType + + return chanType.IsSingleFunder() && chanType.HasFundingTx() && + c.IsInitiator && + !c.hasChanStatus(ChanStatusRestored) +} + +// fetchChanInfo deserializes the channel info based on the legacy boolean. +func fetchChanInfo(chanBucket kvdb.RBucket, c *OpenChannel, legacy bool) error { + infoBytes := chanBucket.Get(chanInfoKey) + if infoBytes == nil { + return ErrNoChanInfoFound + } + r := bytes.NewReader(infoBytes) + + var ( + chanType mig.ChannelType + chanStatus mig.ChannelStatus + ) + + if err := mig.ReadElements(r, + &chanType, &c.ChainHash, &c.FundingOutpoint, + &c.ShortChannelID, &c.IsPending, &c.IsInitiator, + &chanStatus, &c.FundingBroadcastHeight, + &c.NumConfsRequired, &c.ChannelFlags, + &c.IdentityPub, &c.Capacity, &c.TotalMSatSent, + &c.TotalMSatReceived, + ); err != nil { + return err + } + + c.ChanType = ChannelType(chanType) + c.chanStatus = ChannelStatus(chanStatus) + + // If this is not the legacy format, we need to read the extra two new + // fields. + if !legacy { + if err := mig.ReadElements(r, + &c.InitialLocalBalance, &c.InitialRemoteBalance, + ); err != nil { + return err + } + } + + // For single funder channels that we initiated and have the funding + // transaction to, read the funding txn. + if c.FundingTxPresent() { + if err := mig.ReadElement(r, &c.FundingTxn); err != nil { + return err + } + } + + if err := mig.ReadChanConfig(r, &c.LocalChanCfg); err != nil { + return err + } + if err := mig.ReadChanConfig(r, &c.RemoteChanCfg); err != nil { + return err + } + + // Retrieve the boolean stored under lastWasRevokeKey. + lastWasRevokeBytes := chanBucket.Get(lastWasRevokeKey) + if lastWasRevokeBytes == nil { + // If nothing has been stored under this key, we store false in + // the OpenChannel struct. + c.LastWasRevoke = false + } else { + // Otherwise, read the value into the LastWasRevoke field. + revokeReader := bytes.NewReader(lastWasRevokeBytes) + err := mig.ReadElements(revokeReader, &c.LastWasRevoke) + if err != nil { + return err + } + } + + keyLocRecord := MakeKeyLocRecord(keyLocType, &c.RevocationKeyLocator) + tlvStream, err := tlv.NewStream(keyLocRecord) + if err != nil { + return err + } + + if err := tlvStream.Decode(r); err != nil { + return err + } + + // Finally, read the optional shutdown scripts. + if err := GetOptionalUpfrontShutdownScript( + chanBucket, localUpfrontShutdownKey, &c.LocalShutdownScript, + ); err != nil { + return err + } + + return GetOptionalUpfrontShutdownScript( + chanBucket, remoteUpfrontShutdownKey, &c.RemoteShutdownScript, + ) +} + +// fetchChanInfo serializes the channel info based on the legacy boolean and +// saves it to disk. +func putChanInfo(chanBucket kvdb.RwBucket, c *OpenChannel, legacy bool) error { + var w bytes.Buffer + if err := mig.WriteElements(&w, + mig.ChannelType(c.ChanType), c.ChainHash, c.FundingOutpoint, + c.ShortChannelID, c.IsPending, c.IsInitiator, + mig.ChannelStatus(c.chanStatus), c.FundingBroadcastHeight, + c.NumConfsRequired, c.ChannelFlags, + c.IdentityPub, c.Capacity, c.TotalMSatSent, + c.TotalMSatReceived, + ); err != nil { + return err + } + + // If this is not legacy format, we need to write the extra two fields. + if !legacy { + if err := mig.WriteElements(&w, + c.InitialLocalBalance, c.InitialRemoteBalance, + ); err != nil { + return err + } + } + + // For single funder channels that we initiated, and we have the + // funding transaction, then write the funding txn. + if c.FundingTxPresent() { + if err := mig.WriteElement(&w, c.FundingTxn); err != nil { + return err + } + } + + if err := mig.WriteChanConfig(&w, &c.LocalChanCfg); err != nil { + return err + } + if err := mig.WriteChanConfig(&w, &c.RemoteChanCfg); err != nil { + return err + } + + // Write the RevocationKeyLocator as the first entry in a tlv stream. + keyLocRecord := MakeKeyLocRecord( + keyLocType, &c.RevocationKeyLocator, + ) + + tlvStream, err := tlv.NewStream(keyLocRecord) + if err != nil { + return err + } + + if err := tlvStream.Encode(&w); err != nil { + return err + } + + if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil { + return err + } + + // Finally, add optional shutdown scripts for the local and remote peer + // if they are present. + if err := PutOptionalUpfrontShutdownScript( + chanBucket, localUpfrontShutdownKey, c.LocalShutdownScript, + ); err != nil { + return err + } + + return PutOptionalUpfrontShutdownScript( + chanBucket, remoteUpfrontShutdownKey, c.RemoteShutdownScript, + ) +} + +// EKeyLocator is an encoder for keychain.KeyLocator. +func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { + if v, ok := val.(*keychain.KeyLocator); ok { + err := tlv.EUint32T(w, uint32(v.Family), buf) + if err != nil { + return err + } + + return tlv.EUint32T(w, v.Index, buf) + } + return tlv.NewTypeForEncodingErr(val, "keychain.KeyLocator") +} + +// DKeyLocator is a decoder for keychain.KeyLocator. +func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + if v, ok := val.(*keychain.KeyLocator); ok { + var family uint32 + err := tlv.DUint32(r, &family, buf, 4) + if err != nil { + return err + } + v.Family = keychain.KeyFamily(family) + + return tlv.DUint32(r, &v.Index, buf, 4) + } + return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) +} + +// MakeKeyLocRecord creates a Record out of a KeyLocator using the passed +// Type and the EKeyLocator and DKeyLocator functions. The size will always be +// 8 as KeyFamily is uint32 and the Index is uint32. +func MakeKeyLocRecord(typ tlv.Type, keyLoc *keychain.KeyLocator) tlv.Record { + return tlv.MakeStaticRecord(typ, keyLoc, 8, EKeyLocator, DKeyLocator) +} + +// PutOptionalUpfrontShutdownScript adds a shutdown script under the key +// provided if it has a non-zero length. +func PutOptionalUpfrontShutdownScript(chanBucket kvdb.RwBucket, key []byte, + script []byte) error { + // If the script is empty, we do not need to add anything. + if len(script) == 0 { + return nil + } + + var w bytes.Buffer + if err := mig.WriteElement(&w, script); err != nil { + return err + } + + return chanBucket.Put(key, w.Bytes()) +} + +// GetOptionalUpfrontShutdownScript reads the shutdown script stored under the +// key provided if it is present. Upfront shutdown scripts are optional, so the +// function returns with no error if the key is not present. +func GetOptionalUpfrontShutdownScript(chanBucket kvdb.RBucket, key []byte, + script *lnwire.DeliveryAddress) error { + + // Return early if the bucket does not exit, a shutdown script was not + // set. + bs := chanBucket.Get(key) + if bs == nil { + return nil + } + + var tempScript []byte + r := bytes.NewReader(bs) + if err := mig.ReadElement(r, &tempScript); err != nil { + return err + } + *script = tempScript + + return nil +} + +// FetchChanCommitments fetches both the local and remote commitments. This +// function is exported so it can be used by later migrations. +func FetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error { + var err error + + // If this is a restored channel, then we don't have any commitments to + // read. + if channel.hasChanStatus(ChanStatusRestored) { + return nil + } + + channel.LocalCommitment, err = FetchChanCommitment(chanBucket, true) + if err != nil { + return err + } + channel.RemoteCommitment, err = FetchChanCommitment(chanBucket, false) + if err != nil { + return err + } + + return nil +} + +// FetchChanCommitment fetches a channel commitment. This function is exported +// so it can be used by later migrations. +func FetchChanCommitment(chanBucket kvdb.RBucket, + local bool) (mig.ChannelCommitment, error) { + + commitKey := chanCommitmentKey + if local { + commitKey = append(commitKey, byte(0x00)) + } else { + commitKey = append(commitKey, byte(0x01)) + } + + commitBytes := chanBucket.Get(commitKey) + if commitBytes == nil { + return mig.ChannelCommitment{}, ErrNoCommitmentsFound + } + + r := bytes.NewReader(commitBytes) + return mig.DeserializeChanCommit(r) +} + +func PutChanCommitment(chanBucket kvdb.RwBucket, c *mig.ChannelCommitment, + local bool) error { + + commitKey := chanCommitmentKey + if local { + commitKey = append(commitKey, byte(0x00)) + } else { + commitKey = append(commitKey, byte(0x01)) + } + + var b bytes.Buffer + if err := mig.SerializeChanCommit(&b, c); err != nil { + return err + } + + return chanBucket.Put(commitKey, b.Bytes()) +} + +func PutChanCommitments(chanBucket kvdb.RwBucket, channel *OpenChannel) error { + // If this is a restored channel, then we don't have any commitments to + // write. + if channel.hasChanStatus(ChanStatusRestored) { + return nil + } + + err := PutChanCommitment( + chanBucket, &channel.LocalCommitment, true, + ) + if err != nil { + return err + } + + return PutChanCommitment( + chanBucket, &channel.RemoteCommitment, false, + ) +} + +// balancesAtHeight returns the local and remote balances on our commitment +// transactions as of a given height. This function is not exported as it's +// deprecated. +// +// NOTE: these are our balances *after* subtracting the commitment fee and +// anchor outputs. +func (c *OpenChannel) balancesAtHeight(chanBucket kvdb.RBucket, + height uint64) (lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) { + + // If our current commit is as the desired height, we can return our + // current balances. + if c.LocalCommitment.CommitHeight == height { + return c.LocalCommitment.LocalBalance, + c.LocalCommitment.RemoteBalance, nil + } + + // If our current remote commit is at the desired height, we can return + // the current balances. + if c.RemoteCommitment.CommitHeight == height { + return c.RemoteCommitment.LocalBalance, + c.RemoteCommitment.RemoteBalance, nil + } + + // If we are not currently on the height requested, we need to look up + // the previous height to obtain our balances at the given height. + commit, err := c.FindPreviousStateLegacy(chanBucket, height) + if err != nil { + return 0, 0, err + } + + return commit.LocalBalance, commit.RemoteBalance, nil +} + +// FindPreviousStateLegacy scans through the append-only log in an attempt to +// recover the previous channel state indicated by the update number. This +// method is intended to be used for obtaining the relevant data needed to +// claim all funds rightfully spendable in the case of an on-chain broadcast of +// the commitment transaction. +func (c *OpenChannel) FindPreviousStateLegacy(chanBucket kvdb.RBucket, + updateNum uint64) (*mig.ChannelCommitment, error) { + + c.RLock() + defer c.RUnlock() + + logBucket := chanBucket.NestedReadBucket(revocationLogBucketLegacy) + if logBucket == nil { + return nil, ErrNoPastDeltas + } + + commit, err := fetchChannelLogEntry(logBucket, updateNum) + if err != nil { + return nil, err + } + + return &commit, nil +} + +func fetchChannelLogEntry(log kvdb.RBucket, + updateNum uint64) (mig.ChannelCommitment, error) { + + logEntrykey := mig24.MakeLogKey(updateNum) + commitBytes := log.Get(logEntrykey[:]) + if commitBytes == nil { + return mig.ChannelCommitment{}, ErrLogEntryNotFound + } + + commitReader := bytes.NewReader(commitBytes) + return mig.DeserializeChanCommit(commitReader) +} diff --git a/channeldb/migration25/log.go b/channeldb/migration25/log.go new file mode 100644 index 000000000..e308d2755 --- /dev/null +++ b/channeldb/migration25/log.go @@ -0,0 +1,14 @@ +package migration25 + +import ( + "github.com/btcsuite/btclog" +) + +// log is a logger that is initialized as disabled. This means the package will +// not perform any logging by default until a logger is set. +var log = btclog.Disabled + +// UseLogger uses a specified Logger to output package logging info. +func UseLogger(logger btclog.Logger) { + log = logger +} diff --git a/channeldb/migration25/migration.go b/channeldb/migration25/migration.go new file mode 100644 index 000000000..db6e156e9 --- /dev/null +++ b/channeldb/migration25/migration.go @@ -0,0 +1,211 @@ +package migration25 + +import ( + "bytes" + "fmt" + + mig "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11" + "github.com/lightningnetwork/lnd/kvdb" +) + +var ( + // openChanBucket stores all the currently open channels. This bucket + // has a second, nested bucket which is keyed by a node's ID. Within + // that node ID bucket, all attributes required to track, update, and + // close a channel are stored. + openChannelBucket = []byte("open-chan-bucket") + + // chanInfoKey can be accessed within the bucket for a channel + // (identified by its chanPoint). This key stores all the static + // information for a channel which is decided at the end of the + // funding flow. + chanInfoKey = []byte("chan-info-key") + + // ErrNoChanDBExists is returned when a channel bucket hasn't been + // created. + ErrNoChanDBExists = fmt.Errorf("channel db has not yet been created") + + // ErrNoActiveChannels is returned when there is no active (open) + // channels within the database. + ErrNoActiveChannels = fmt.Errorf("no active channels exist") + + // ErrChannelNotFound is returned when we attempt to locate a channel + // for a specific chain, but it is not found. + ErrChannelNotFound = fmt.Errorf("channel not found") +) + +// MigrateInitialBalances patches the two new fields, InitialLocalBalance and +// InitialRemoteBalance, for all the open channels. It does so by reading the +// revocation log at height 0 to learn the initial balances and then updates +// the channel's info. +// The channel info is saved in the nested bucket which is accessible via +// nodePub:chainHash:chanPoint. If any of the sub-buckets turns out to be nil, +// we will log the error and continue to process the rest. +func MigrateInitialBalances(tx kvdb.RwTx) error { + log.Infof("Migrating initial local and remote balances...") + + openChanBucket := tx.ReadWriteBucket(openChannelBucket) + + // If no bucket is found, we can exit early. + if openChanBucket == nil { + return nil + } + + // Read a list of open channels. + channels, err := findOpenChannels(openChanBucket) + if err != nil { + return err + } + + // Migrate the balances. + for _, c := range channels { + if err := migrateBalances(tx, c); err != nil { + return err + } + } + + return err +} + +// findOpenChannels finds all open channels. +func findOpenChannels(openChanBucket kvdb.RBucket) ([]*OpenChannel, error) { + channels := []*OpenChannel{} + + // readChannel is a helper closure that reads the channel info from the + // channel bucket. + readChannel := func(chainBucket kvdb.RBucket, cp []byte) error { + c := &OpenChannel{} + + // Read the sub-bucket level 3. + chanBucket := chainBucket.NestedReadBucket( + cp, + ) + if chanBucket == nil { + log.Errorf("unable to read bucket for chanPoint=%x", cp) + return nil + } + // Get the old channel info. + if err := fetchChanInfo(chanBucket, c, true); err != nil { + return fmt.Errorf("unable to fetch chan info: %v", err) + } + + // Fetch the channel commitments, which are useful for freshly + // open channels as they don't have any revocation logs and + // their current commitments reflect the initial balances. + if err := FetchChanCommitments(chanBucket, c); err != nil { + return fmt.Errorf("unable to fetch chan commits: %v", + err) + } + + channels = append(channels, c) + + return nil + } + + // Iterate the root bucket. + err := openChanBucket.ForEach(func(nodePub, v []byte) error { + // Ensure that this is a key the same size as a pubkey, and + // also that it leads directly to a bucket. + if len(nodePub) != 33 || v != nil { + return nil + } + + // Read the sub-bucket level 1. + nodeChanBucket := openChanBucket.NestedReadBucket(nodePub) + if nodeChanBucket == nil { + log.Errorf("no bucket for node %x", nodePub) + return nil + } + + // Iterate the bucket. + return nodeChanBucket.ForEach(func(chainHash, _ []byte) error { + // Read the sub-bucket level 2. + chainBucket := nodeChanBucket.NestedReadBucket( + chainHash, + ) + if chainBucket == nil { + log.Errorf("unable to read bucket for chain=%x", + chainHash) + return nil + } + + // Iterate the bucket. + return chainBucket.ForEach(func(cp, _ []byte) error { + return readChannel(chainBucket, cp) + }) + }) + }) + + if err != nil { + return nil, err + } + + return channels, nil +} + +// migrateBalances queries the revocation log at height 0 to find the initial +// balances and save them to the channel info. +func migrateBalances(tx kvdb.RwTx, c *OpenChannel) error { + // Get the bucket. + chanBucket, err := fetchChanBucket(tx, c) + if err != nil { + return err + } + // Get the initial balances. + localAmt, remoteAmt, err := c.balancesAtHeight(chanBucket, 0) + if err != nil { + return fmt.Errorf("unable to get initial balances: %v", err) + } + + c.InitialLocalBalance = localAmt + c.InitialRemoteBalance = remoteAmt + + // Update the channel info. + if err := putChanInfo(chanBucket, c, false); err != nil { + return fmt.Errorf("unable to put chan info: %v", err) + } + + return nil +} + +// fetchChanBucket is a helper function that returns the bucket where a +// channel's data resides in given: the public key for the node, the outpoint, +// and the chainhash that the channel resides on. +func fetchChanBucket(tx kvdb.RwTx, c *OpenChannel) (kvdb.RwBucket, error) { + // First fetch the top level bucket which stores all data related to + // current, active channels. + openChanBucket := tx.ReadWriteBucket(openChannelBucket) + if openChanBucket == nil { + return nil, ErrNoChanDBExists + } + + // Within this top level bucket, fetch the bucket dedicated to storing + // open channel data specific to the remote node. + nodePub := c.IdentityPub.SerializeCompressed() + nodeChanBucket := openChanBucket.NestedReadWriteBucket(nodePub) + if nodeChanBucket == nil { + return nil, ErrNoActiveChannels + } + + // We'll then recurse down an additional layer in order to fetch the + // bucket for this particular chain. + chainBucket := nodeChanBucket.NestedReadWriteBucket(c.ChainHash[:]) + if chainBucket == nil { + return nil, ErrNoActiveChannels + } + + // With the bucket for the node and chain fetched, we can now go down + // another level, for this channel itself. + var chanPointBuf bytes.Buffer + err := mig.WriteOutpoint(&chanPointBuf, &c.FundingOutpoint) + if err != nil { + return nil, err + } + + chanBucket := chainBucket.NestedReadWriteBucket(chanPointBuf.Bytes()) + if chanBucket == nil { + return nil, ErrChannelNotFound + } + + return chanBucket, nil +} From 7eaf0d0089ea0b684258f4bfa0a9b690f17947b9 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 13 Apr 2022 01:06:42 +0800 Subject: [PATCH 10/22] channeldb+migration: add migration test for patching balances --- channeldb/migration25/migration_test.go | 391 ++++++++++++++++++++++++ 1 file changed, 391 insertions(+) create mode 100644 channeldb/migration25/migration_test.go diff --git a/channeldb/migration25/migration_test.go b/channeldb/migration25/migration_test.go new file mode 100644 index 000000000..339815cab --- /dev/null +++ b/channeldb/migration25/migration_test.go @@ -0,0 +1,391 @@ +package migration25 + +import ( + "bytes" + "fmt" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + + lnwire "github.com/lightningnetwork/lnd/channeldb/migration/lnwire21" + mig24 "github.com/lightningnetwork/lnd/channeldb/migration24" + mig "github.com/lightningnetwork/lnd/channeldb/migration_01_to_11" + "github.com/lightningnetwork/lnd/channeldb/migtest" + "github.com/lightningnetwork/lnd/kvdb" +) + +var ( + // Create dummy values to be stored in db. + dummyPrivKey, _ = btcec.NewPrivateKey() + dummyPubKey = dummyPrivKey.PubKey() + dummySig = []byte{1, 2, 3} + dummyTx = &wire.MsgTx{ + Version: 1, + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 0xffffffff, + }, + Sequence: 0xffffffff, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 5000000000, + }, + }, + LockTime: 5, + } + + dummyOp = wire.OutPoint{ + Hash: chainhash.Hash{}, + Index: 9, + } + dummyHTLC = mig.HTLC{ + Signature: dummySig, + RHash: [32]byte{}, + Amt: 100_000, + RefundTimeout: 731583, + OutputIndex: 1, + Incoming: true, + HtlcIndex: 1, + LogIndex: 1, + } + + // ourAmt and theirAmt are the initial balances found in the local + // channel commitment at height 0. + ourAmt = lnwire.MilliSatoshi(500_000) + theirAmt = lnwire.MilliSatoshi(1000_000) + + // ourAmtRevoke and theirAmtRevoke are the initial balances found in + // the revocation log at height 0. + // + // NOTE: they are made differently such that we can easily check the + // source when patching the balances. + ourAmtRevoke = lnwire.MilliSatoshi(501_000) + theirAmtRevoke = lnwire.MilliSatoshi(1001_000) + + // remoteCommit0 is the channel commitment at commit height 0. This is + // also the revocation log we will use to patch the initial balances. + remoteCommit0 = mig.ChannelCommitment{ + LocalBalance: ourAmtRevoke, + RemoteBalance: theirAmtRevoke, + CommitFee: btcutil.Amount(1), + FeePerKw: btcutil.Amount(1), + CommitTx: dummyTx, + CommitSig: dummySig, + Htlcs: []mig.HTLC{}, + } + + // localCommit0 is the channel commitment at commit height 0. This is + // the channel commitment we will use to patch the initial balances + // when there are no revocation logs. + localCommit0 = mig.ChannelCommitment{ + LocalBalance: ourAmt, + RemoteBalance: theirAmt, + CommitFee: btcutil.Amount(1), + FeePerKw: btcutil.Amount(1), + CommitTx: dummyTx, + CommitSig: dummySig, + Htlcs: []mig.HTLC{}, + } + + // remoteCommit1 and localCommit1 are the channel commitment at commit + // height 1. + remoteCommit1 = mig.ChannelCommitment{ + CommitHeight: 1, + LocalLogIndex: 1, + LocalHtlcIndex: 1, + RemoteLogIndex: 1, + RemoteHtlcIndex: 1, + LocalBalance: ourAmt - dummyHTLC.Amt, + RemoteBalance: theirAmt + dummyHTLC.Amt, + CommitFee: btcutil.Amount(1), + FeePerKw: btcutil.Amount(1), + CommitTx: dummyTx, + CommitSig: dummySig, + Htlcs: []mig.HTLC{dummyHTLC}, + } + localCommit1 = mig.ChannelCommitment{ + CommitHeight: 1, + LocalLogIndex: 1, + LocalHtlcIndex: 1, + RemoteLogIndex: 1, + RemoteHtlcIndex: 1, + LocalBalance: ourAmt - dummyHTLC.Amt, + RemoteBalance: theirAmt + dummyHTLC.Amt, + CommitFee: btcutil.Amount(1), + FeePerKw: btcutil.Amount(1), + CommitTx: dummyTx, + CommitSig: dummySig, + Htlcs: []mig.HTLC{dummyHTLC}, + } + + // openChannel0 is the OpenChannel at commit height 0. When this + // variable is used, we expect to patch the initial balances from its + // commitments. + openChannel0 = &OpenChannel{ + OpenChannel: mig.OpenChannel{ + IdentityPub: dummyPubKey, + FundingOutpoint: dummyOp, + LocalCommitment: localCommit0, + RemoteCommitment: remoteCommit0, + }, + } + + // openChannel1 is the OpenChannel at commit height 1. When this + // variable is used, we expect to patch the initial balances from the + // remote commitment at height 0. + openChannel1 = &OpenChannel{ + OpenChannel: mig.OpenChannel{ + IdentityPub: dummyPubKey, + FundingOutpoint: dummyOp, + LocalCommitment: localCommit1, + RemoteCommitment: remoteCommit1, + }, + } +) + +// TestMigrateInitialBalances checks that the proper initial balances are +// patched to the channel info. +func TestMigrateInitialBalances(t *testing.T) { + testCases := []struct { + name string + beforeMigrationFunc func(kvdb.RwTx) error + afterMigrationFunc func(kvdb.RwTx) error + shouldFail bool + }{ + { + // Test that we patch the initial balances using the + // revocation log. + name: "patch balance from revoke log", + beforeMigrationFunc: genBeforeMigration( + openChannel1, &remoteCommit0, + ), + afterMigrationFunc: genAfterMigration( + ourAmtRevoke, theirAmtRevoke, openChannel1, + ), + }, + { + // Test that we patch the initial balances using the + // channel's local commitment since at height 0, + // balances found in LocalCommitment reflect the + // initial balances. + name: "patch balance from local commit", + beforeMigrationFunc: genBeforeMigration( + openChannel0, nil, + ), + afterMigrationFunc: genAfterMigration( + ourAmt, theirAmt, openChannel0, + ), + }, + { + // Test that we patch the initial balances using the + // channel's local commitment even when there is a + // revocation log available. + name: "patch balance from local commit only", + beforeMigrationFunc: genBeforeMigration( + openChannel0, &remoteCommit0, + ), + afterMigrationFunc: genAfterMigration( + ourAmt, theirAmt, openChannel0, + ), + }, + { + // Test that when there is no revocation log the + // migration would fail. + name: "patch balance error on no revoke log", + beforeMigrationFunc: genBeforeMigration( + // Use nil to specify no revocation log will be + // created. + openChannel1, nil, + ), + afterMigrationFunc: genAfterMigration( + // Use nil to specify skipping the + // afterMigrationFunc. + 0, 0, nil, + ), + shouldFail: true, + }, + { + // Test that when the saved revocation log is not what + // we want the migration would fail. + name: "patch balance error on wrong revoke log", + beforeMigrationFunc: genBeforeMigration( + // Use the revocation log with the wrong + // height. + openChannel1, &remoteCommit1, + ), + afterMigrationFunc: genAfterMigration( + // Use nil to specify skipping the + // afterMigrationFunc. + 0, 0, nil, + ), + shouldFail: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + migtest.ApplyMigration( + t, + tc.beforeMigrationFunc, + tc.afterMigrationFunc, + MigrateInitialBalances, + tc.shouldFail, + ) + }) + } +} + +func genBeforeMigration(c *OpenChannel, + commit *mig.ChannelCommitment) func(kvdb.RwTx) error { + + return func(tx kvdb.RwTx) error { + if c.InitialLocalBalance != 0 { + return fmt.Errorf("non zero initial local amount") + } + + if c.InitialRemoteBalance != 0 { + return fmt.Errorf("non zero initial local amount") + } + + // Create the channel bucket. + chanBucket, err := createChanBucket(tx, c) + if err != nil { + return err + } + + // Save the channel info using legacy format. + if err := putChanInfo(chanBucket, c, true); err != nil { + return err + } + + // Save the channel commitments. + if err := PutChanCommitments(chanBucket, c); err != nil { + return err + } + + // If we have a remote commitment, save it as our revocation + // log. + if commit != nil { + err := putChannelLogEntryLegacy(chanBucket, commit) + if err != nil { + return err + } + } + + return nil + } +} + +func genAfterMigration(ourAmt, theirAmt lnwire.MilliSatoshi, + c *OpenChannel) func(kvdb.RwTx) error { + + return func(tx kvdb.RwTx) error { + // If the passed OpenChannel is nil, we will skip the + // afterMigrationFunc as it indicates an error is expected + // during the migration. + if c == nil { + return nil + } + + chanBucket, err := fetchChanBucket(tx, c) + if err != nil { + return err + } + + newChan := &OpenChannel{} + + // Fetch the channel info using the new format. + err = fetchChanInfo(chanBucket, newChan, false) + if err != nil { + return err + } + + // Check our initial amount is correct. + if newChan.InitialLocalBalance != ourAmt { + return fmt.Errorf("wrong local balance, got %d, "+ + "want %d", newChan.InitialLocalBalance, ourAmt) + } + + // Check their initial amount is correct. + if newChan.InitialRemoteBalance != theirAmt { + return fmt.Errorf("wrong remote balance, got %d, "+ + "want %d", newChan.InitialRemoteBalance, + theirAmt) + } + + // We also check the relevant channel info fields stay the + // same. + if !newChan.IdentityPub.IsEqual(c.IdentityPub) { + return fmt.Errorf("wrong IdentityPub") + } + if newChan.FundingOutpoint != c.FundingOutpoint { + return fmt.Errorf("wrong FundingOutpoint") + } + + return nil + } +} + +func createChanBucket(tx kvdb.RwTx, c *OpenChannel) (kvdb.RwBucket, error) { + // First fetch the top level bucket which stores all data related to + // current, active channels. + openChanBucket, err := tx.CreateTopLevelBucket(openChannelBucket) + if err != nil { + return nil, err + } + + // Within this top level bucket, fetch the bucket dedicated to storing + // open channel data specific to the remote node. + nodePub := c.IdentityPub.SerializeCompressed() + nodeChanBucket, err := openChanBucket.CreateBucketIfNotExists(nodePub) + if err != nil { + return nil, err + } + + // We'll then recurse down an additional layer in order to fetch the + // bucket for this particular chain. + chainBucket, err := nodeChanBucket.CreateBucketIfNotExists( + c.ChainHash[:], + ) + if err != nil { + return nil, err + } + + var chanPointBuf bytes.Buffer + err = mig.WriteOutpoint(&chanPointBuf, &c.FundingOutpoint) + if err != nil { + return nil, err + } + + // With the bucket for the node fetched, we can now go down another + // level, creating the bucket for this channel itself. + return chainBucket.CreateBucketIfNotExists(chanPointBuf.Bytes()) +} + +// putChannelLogEntryLegacy saves an old format revocation log to the bucket. +func putChannelLogEntryLegacy(chanBucket kvdb.RwBucket, + commit *mig.ChannelCommitment) error { + + logBucket, err := chanBucket.CreateBucketIfNotExists( + revocationLogBucketLegacy, + ) + if err != nil { + return err + } + + var b bytes.Buffer + if err := mig.SerializeChanCommit(&b, commit); err != nil { + return err + } + + logEntrykey := mig24.MakeLogKey(commit.CommitHeight) + return logBucket.Put(logEntrykey[:], b.Bytes()) +} From f42d1f2d62c97c74a6e1eca6ada0a562ece975e3 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Apr 2022 04:27:26 +0800 Subject: [PATCH 11/22] multi: remove function `BalancesAtHeight` This commit deletes the function `BalancesAtHeight` since its only usague is to find the push amount, which can be achieved by saving the initial balances. Another reason to remove it is to pave the way to incooperate our new revocation log. If we ever need this function again, we can add it back by visiting all the revocation logs to calculate the balances at a given height. --- channeldb/channel.go | 43 -------- channeldb/channel_test.go | 187 ---------------------------------- lntest/itest/lnd_misc_test.go | 31 ++++-- rpcserver.go | 12 +-- 4 files changed, 24 insertions(+), 249 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index b8bb42cae..44ee8ecdf 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -183,11 +183,6 @@ var ( // ErrMissingIndexEntry is returned when a caller attempts to close a // channel and the outpoint is missing from the index. ErrMissingIndexEntry = fmt.Errorf("missing outpoint from index") - - // errHeightNotFound is returned when a query for channel balances at - // a height that we have not reached yet is made. - errHeightNotReached = fmt.Errorf("height requested greater than " + - "current commit height") ) const ( @@ -1653,44 +1648,6 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment, return nil } -// BalancesAtHeight returns the local and remote balances on our commitment -// transactions as of a given height. -// -// NOTE: these are our balances *after* subtracting the commitment fee and -// anchor outputs. -func (c *OpenChannel) BalancesAtHeight(height uint64) (lnwire.MilliSatoshi, - lnwire.MilliSatoshi, error) { - - if height > c.LocalCommitment.CommitHeight && - height > c.RemoteCommitment.CommitHeight { - - return 0, 0, errHeightNotReached - } - - // If our current commit is as the desired height, we can return our - // current balances. - if c.LocalCommitment.CommitHeight == height { - return c.LocalCommitment.LocalBalance, - c.LocalCommitment.RemoteBalance, nil - } - - // If our current remote commit is at the desired height, we can return - // the current balances. - if c.RemoteCommitment.CommitHeight == height { - return c.RemoteCommitment.LocalBalance, - c.RemoteCommitment.RemoteBalance, nil - } - - // If we are not currently on the height requested, we need to look up - // the previous height to obtain our balances at the given height. - commit, err := c.FindPreviousState(height) - if err != nil { - return 0, 0, err - } - - return commit.LocalBalance, commit.RemoteBalance, nil -} - // ActiveHtlcs returns a slice of HTLC's which are currently active on *both* // commitment transactions. func (c *OpenChannel) ActiveHtlcs() []HTLC { diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index b4b8afdeb..c6033d9ed 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -78,25 +78,6 @@ type testChannelParams struct { // default channel that is creates for testing. type testChannelOption func(params *testChannelParams) -// channelCommitmentOption is an option which allows overwriting of the default -// commitment height and balances. The local boolean can be used to set these -// balances on the local or remote commit. -func channelCommitmentOption(height uint64, localBalance, - remoteBalance lnwire.MilliSatoshi, local bool) testChannelOption { - - return func(params *testChannelParams) { - if local { - params.channel.LocalCommitment.CommitHeight = height - params.channel.LocalCommitment.LocalBalance = localBalance - params.channel.LocalCommitment.RemoteBalance = remoteBalance - } else { - params.channel.RemoteCommitment.CommitHeight = height - params.channel.RemoteCommitment.LocalBalance = localBalance - params.channel.RemoteCommitment.RemoteBalance = remoteBalance - } - } -} - // pendingHeightOption is an option which can be used to set the height the // channel is marked as pending at. func pendingHeightOption(height uint32) testChannelOption { @@ -1415,174 +1396,6 @@ func TestCloseChannelStatus(t *testing.T) { } } -// TestBalanceAtHeight tests lookup of our local and remote balance at a given -// height. -func TestBalanceAtHeight(t *testing.T) { - const ( - // Values that will be set on our current local commit in - // memory. - localHeight = 2 - localLocalBalance = 1000 - localRemoteBalance = 1500 - - // Values that will be set on our current remote commit in - // memory. - remoteHeight = 3 - remoteLocalBalance = 2000 - remoteRemoteBalance = 2500 - - // Values that will be written to disk in the revocation log. - oldHeight = 0 - oldLocalBalance = 200 - oldRemoteBalance = 300 - - // Heights to test error cases. - unknownHeight = 1 - unreachedHeight = 4 - ) - - // putRevokedState is a helper function used to put commitments is - // the revocation log bucket to test lookup of balances at heights that - // are not our current height. - putRevokedState := func(c *OpenChannel, height uint64, local, - remote lnwire.MilliSatoshi) error { - - err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error { - chanBucket, err := fetchChanBucketRw( - tx, c.IdentityPub, &c.FundingOutpoint, - c.ChainHash, - ) - if err != nil { - return err - } - - logKey := revocationLogBucket - logBucket, err := chanBucket.CreateBucketIfNotExists( - logKey, - ) - if err != nil { - return err - } - - // Make a copy of our current commitment so we do not - // need to re-fill all the required fields and copy in - // our new desired values. - commit := c.LocalCommitment - commit.CommitHeight = height - commit.LocalBalance = local - commit.RemoteBalance = remote - - return appendChannelLogEntry(logBucket, &commit) - }, func() {}) - - return err - } - - tests := []struct { - name string - targetHeight uint64 - expectedLocalBalance lnwire.MilliSatoshi - expectedRemoteBalance lnwire.MilliSatoshi - expectedError error - }{ - { - name: "target is current local height", - targetHeight: localHeight, - expectedLocalBalance: localLocalBalance, - expectedRemoteBalance: localRemoteBalance, - expectedError: nil, - }, - { - name: "target is current remote height", - targetHeight: remoteHeight, - expectedLocalBalance: remoteLocalBalance, - expectedRemoteBalance: remoteRemoteBalance, - expectedError: nil, - }, - { - name: "need to lookup commit", - targetHeight: oldHeight, - expectedLocalBalance: oldLocalBalance, - expectedRemoteBalance: oldRemoteBalance, - expectedError: nil, - }, - { - name: "height not found", - targetHeight: unknownHeight, - expectedLocalBalance: 0, - expectedRemoteBalance: 0, - expectedError: ErrLogEntryNotFound, - }, - { - name: "height not reached", - targetHeight: unreachedHeight, - expectedLocalBalance: 0, - expectedRemoteBalance: 0, - expectedError: errHeightNotReached, - }, - } - - for _, test := range tests { - test := test - - t.Run(test.name, func(t *testing.T) { - t.Parallel() - - fullDB, cleanUp, err := MakeTestDB() - if err != nil { - t.Fatalf("unable to make test database: %v", - err) - } - defer cleanUp() - - cdb := fullDB.ChannelStateDB() - - // Create options to set the heights and balances of - // our local and remote commitments. - localCommitOpt := channelCommitmentOption( - localHeight, localLocalBalance, - localRemoteBalance, true, - ) - - remoteCommitOpt := channelCommitmentOption( - remoteHeight, remoteLocalBalance, - remoteRemoteBalance, false, - ) - - // Create an open channel. - channel := createTestChannel( - t, cdb, openChannelOption(), - localCommitOpt, remoteCommitOpt, - ) - - // Write an older commit to disk. - err = putRevokedState(channel, oldHeight, - oldLocalBalance, oldRemoteBalance) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - local, remote, err := channel.BalancesAtHeight( - test.targetHeight, - ) - if err != test.expectedError { - t.Fatalf("expected: %v, got: %v", - test.expectedError, err) - } - - if local != test.expectedLocalBalance { - t.Fatalf("expected local: %v, got: %v", - test.expectedLocalBalance, local) - } - - if remote != test.expectedRemoteBalance { - t.Fatalf("expected remote: %v, got: %v", - test.expectedRemoteBalance, remote) - } - }) - } -} - // TestHasChanStatus asserts the behavior of HasChanStatus by checking the // behavior of various status flags in addition to the special case of // ChanStatusDefault which is treated like a flag in the code base even though diff --git a/lntest/itest/lnd_misc_test.go b/lntest/itest/lnd_misc_test.go index f25d96b57..6fb934bb4 100644 --- a/lntest/itest/lnd_misc_test.go +++ b/lntest/itest/lnd_misc_test.go @@ -340,8 +340,8 @@ func testSphinxReplayPersistence(net *lntest.NetworkHarness, t *harnessTest) { // testListChannels checks that the response from ListChannels is correct. It // tests the values in all ChannelConstraints are returned as expected. Once -// ListChannels becomes mature, a test against all fields in ListChannels should -// be performed. +// ListChannels becomes mature, a test against all fields in ListChannels +// should be performed. func testListChannels(net *lntest.NetworkHarness, t *harnessTest) { ctxb := context.Background() @@ -369,8 +369,8 @@ func testListChannels(net *lntest.NetworkHarness, t *harnessTest) { net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, alice) // Open a channel with 100k satoshis between Alice and Bob with Alice - // being the sole funder of the channel. The minial HTLC amount is set to - // 4200 msats. + // being the sole funder of the channel. The minial HTLC amount is set + // to 4200 msats. const customizedMinHtlc = 4200 chanAmt := btcutil.Amount(100000) @@ -414,11 +414,19 @@ func testListChannels(net *lntest.NetworkHarness, t *harnessTest) { // Check the returned response is correct. aliceChannel := resp.Channels[0] + // Since Alice is the initiator, she pays the commit fee. + aliceBalance := int64(chanAmt) - aliceChannel.CommitFee + + // Check the balance related fields are correct. + require.Equal(t.t, aliceBalance, aliceChannel.LocalBalance) + require.Zero(t.t, aliceChannel.RemoteBalance) + require.Zero(t.t, aliceChannel.PushAmountSat) + // Calculate the dust limit we'll use for the test. dustLimit := lnwallet.DustLimitForSize(input.UnknownWitnessSize) - // defaultConstraints is a ChannelConstraints with default values. It is - // used to test against Alice's local channel constraints. + // defaultConstraints is a ChannelConstraints with default values. It + // is used to test against Alice's local channel constraints. defaultConstraints := &lnrpc.ChannelConstraints{ CsvDelay: 4, ChanReserveSat: 1000, @@ -464,9 +472,14 @@ func testListChannels(net *lntest.NetworkHarness, t *harnessTest) { ) } - // Check channel constraints match. Alice's local channel constraint should - // be equal to Bob's remote channel constraint, and her remote one should - // be equal to Bob's local one. + // Check the balance related fields are correct. + require.Equal(t.t, aliceBalance, bobChannel.RemoteBalance) + require.Zero(t.t, bobChannel.LocalBalance) + require.Zero(t.t, bobChannel.PushAmountSat) + + // Check channel constraints match. Alice's local channel constraint + // should be equal to Bob's remote channel constraint, and her remote + // one should be equal to Bob's local one. assertChannelConstraintsEqual( t, aliceChannel.LocalConstraints, bobChannel.RemoteConstraints, ) diff --git a/rpcserver.go b/rpcserver.go index a6b7d8572..f2f8fe506 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -4080,21 +4080,13 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel, channel.UnsettledBalance += channel.PendingHtlcs[i].Amount } - // Lookup our balances at height 0, because they will reflect any - // push amounts that may have been present when this channel was - // created. - localBalance, remoteBalance, err := dbChannel.BalancesAtHeight(0) - if err != nil { - return nil, err - } - // If we initiated opening the channel, the zero height remote balance // is the push amount. Otherwise, our starting balance is the push // amount. If there is no push amount, these values will simply be zero. if dbChannel.IsInitiator { - channel.PushAmountSat = uint64(remoteBalance.ToSatoshis()) + channel.PushAmountSat = uint64(dbChannel.InitialRemoteBalance) } else { - channel.PushAmountSat = uint64(localBalance.ToSatoshis()) + channel.PushAmountSat = uint64(dbChannel.InitialLocalBalance) } if len(dbChannel.LocalShutdownScript) > 0 { From 4609ac2008270a43ccc72deaa0f8a0af80f3de81 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 7 Apr 2022 02:17:57 +0800 Subject: [PATCH 12/22] channeldb: deprecate old revocation log bucket This commit marks the sub-bucket identified by "revocation-log-key" as deprecated. --- channeldb/channel.go | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 44ee8ecdf..ed0292930 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -117,12 +117,15 @@ var ( // TODO(roasbeef): rename to commit chain? commitDiffKey = []byte("commit-diff-key") - // revocationLogBucket is dedicated for storing the necessary delta - // state between channel updates required to re-construct a past state - // in order to punish a counterparty attempting a non-cooperative + // revocationLogBucketDeprecated is dedicated for storing the necessary + // delta state between channel updates required to re-construct a past + // state in order to punish a counterparty attempting a non-cooperative // 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") + // + // NOTE: deprecated. This bucket is kept for read-only in case the user + // choose not to migrate the old data. + revocationLogBucketDeprecated = []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 @@ -2361,7 +2364,7 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, // TODO(roasbeef): could make the deltas relative, would save // space, but then tradeoff for more disk-seeks to recover the // full state. - logKey := revocationLogBucket + logKey := revocationLogBucketDeprecated logBucket, err := chanBucket.CreateBucketIfNotExists(logKey) if err != nil { return err @@ -2600,7 +2603,7 @@ func (c *OpenChannel) revocationLogTailCommitHeight() (uint64, error) { return err } - logBucket := chanBucket.NestedReadBucket(revocationLogBucket) + logBucket := chanBucket.NestedReadBucket(revocationLogBucketDeprecated) if logBucket == nil { return ErrNoPastDeltas } @@ -2676,7 +2679,7 @@ func (c *OpenChannel) FindPreviousState(updateNum uint64) (*ChannelCommitment, e return err } - logBucket := chanBucket.NestedReadBucket(revocationLogBucket) + logBucket := chanBucket.NestedReadBucket(revocationLogBucketDeprecated) if logBucket == nil { return ErrNoPastDeltas } @@ -2890,9 +2893,9 @@ func (c *OpenChannel) CloseChannel(summary *ChannelCloseSummary, // With the base channel data deleted, attempt to delete the // information stored within the revocation log. - logBucket := chanBucket.NestedReadWriteBucket(revocationLogBucket) + logBucket := chanBucket.NestedReadWriteBucket(revocationLogBucketDeprecated) if logBucket != nil { - err = chanBucket.DeleteNestedBucket(revocationLogBucket) + err = chanBucket.DeleteNestedBucket(revocationLogBucketDeprecated) if err != nil { return err } From c41175f233e81c921f0adda5198978b6dfd28c0f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 7 Apr 2022 05:48:57 +0800 Subject: [PATCH 13/22] channeldb: re-introduce revocationLogBucket This commit adds `revocationLogBucket` using the new sub-bucket key "revocation-log" to store the minimal info needed. Two structs, `RevocationLog` and `HTLCEntry` are created to represent the disk records. --- channeldb/channel.go | 6 +- channeldb/revocation_log.go | 371 ++++++++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+), 4 deletions(-) create mode 100644 channeldb/revocation_log.go diff --git a/channeldb/channel.go b/channeldb/channel.go index ed0292930..4c3390747 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -179,10 +179,6 @@ var ( // channel. ErrChanBorked = fmt.Errorf("cannot mutate borked channel") - // ErrLogEntryNotFound is returned when we cannot find a log entry at - // the height requested in the revocation log. - ErrLogEntryNotFound = fmt.Errorf("log entry not found") - // ErrMissingIndexEntry is returned when a caller attempts to close a // channel and the outpoint is missing from the index. ErrMissingIndexEntry = fmt.Errorf("missing outpoint from index") @@ -1687,6 +1683,8 @@ func (c *OpenChannel) ActiveHtlcs() []HTLC { // // TODO(roasbeef): save space by using smaller ints at tail end? type HTLC struct { + // TODO(yy): can embed an HTLCEntry here. + // Signature is the signature for the second level covenant transaction // for this HTLC. The second level transaction is a timeout tx in the // case that this is an outgoing HTLC, and a success tx in the case diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go new file mode 100644 index 000000000..316f59198 --- /dev/null +++ b/channeldb/revocation_log.go @@ -0,0 +1,371 @@ +package channeldb + +import ( + "bytes" + "errors" + "io" + "math" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/tlv" +) + +// OutputIndexEmpty is used when the output index doesn't exist. +const OutputIndexEmpty = math.MaxUint16 + +var ( + // revocationLogBucket is a sub-bucket under openChannelBucket. This + // sub-bucket is dedicated for storing the minimal info required to + // re-construct a past state in order to punish a counterparty + // attempting a non-cooperative channel closure. + revocationLogBucket = []byte("revocation-log") + + // ErrLogEntryNotFound is returned when we cannot find a log entry at + // the height requested in the revocation log. + ErrLogEntryNotFound = errors.New("log entry not found") + + // ErrOutputIndexTooBig is returned when the output index is greater + // than uint16. + ErrOutputIndexTooBig = errors.New("output index is over uint16") +) + +// HTLCEntry specifies the minimal info needed to be stored on disk for ALL the +// historical HTLCs, which is useful for constructing RevocationLog when a +// breach is detected. +// +// NOTE: all the fields saved to disk use the primitive go types so they can be +// made into tlv records without further conversion. +type HTLCEntry struct { + // RHash is the payment hash of the HTLC. + RHash [32]byte + + // RefundTimeout is the absolute timeout on the HTLC that the sender + // must wait before reclaiming the funds in limbo. + RefundTimeout uint32 + + // OutputIndex is the output index for this particular HTLC output + // within the commitment transaction. + // + // NOTE: we use uint16 instead of int32 here to save us 2 bytes, which + // gives us a max number of HTLCs of 65K. + OutputIndex uint16 + + // Incoming denotes whether we're the receiver or the sender of this + // HTLC. + // + // NOTE: this field is the memory representation of the field + // incomingUint. + Incoming bool + + // Amt is the amount of satoshis this HTLC escrows. + // + // NOTE: this field is the memory representation of the field amtUint. + Amt btcutil.Amount + + // amtTlv is the uint64 format of Amt. This field is created so we can + // easily make it into a tlv record and save it to disk. + // + // NOTE: we keep this field for accounting purpose only. If the disk + // space becomes an issue, we could delete this field to save us extra + // 8 bytes. + amtTlv uint64 + + // incomingTlv is the uint8 format of Incoming. This field is created + // so we can easily make it into a tlv record and save it to disk. + incomingTlv uint8 +} + +// toTlvStream converts an HTLCEntry record into a tlv representation. +func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { + const ( + // A set of tlv type definitions used to serialize htlc entries + // to the database. We define it here instead of the head of + // the file to avoid naming conflicts. + // + // NOTE: A migration should be added whenever this list + // changes. + rHashType tlv.Type = 0 + refundTimeoutType tlv.Type = 1 + outputIndexType tlv.Type = 2 + incomingType tlv.Type = 3 + amtType tlv.Type = 4 + ) + + return tlv.NewStream( + tlv.MakePrimitiveRecord(rHashType, &h.RHash), + tlv.MakePrimitiveRecord( + refundTimeoutType, &h.RefundTimeout, + ), + tlv.MakePrimitiveRecord( + outputIndexType, &h.OutputIndex, + ), + tlv.MakePrimitiveRecord(incomingType, &h.incomingTlv), + tlv.MakePrimitiveRecord(amtType, &h.amtTlv), + ) +} + +// RevocationLog stores the info needed to construct a breach retribution. Its +// fields can be viewed as a subset of a ChannelCommitment's. In the database, +// all historical versions of the RevocationLog are saved using the +// CommitHeight as the key. +// +// NOTE: all the fields use the primitive go types so they can be made into tlv +// records without further conversion. +type RevocationLog struct { + // OurOutputIndex specifies our output index in this commitment. In a + // remote commitment transaction, this is the to remote output index. + OurOutputIndex uint16 + + // TheirOutputIndex specifies their output index in this commitment. In + // a remote commitment transaction, this is the to local output index. + TheirOutputIndex uint16 + + // CommitTxHash is the hash of the latest version of the commitment + // state, broadcast able by us. + CommitTxHash [32]byte + + // HTLCEntries is the set of HTLCEntry's that are pending at this + // particular commitment height. + HTLCEntries []*HTLCEntry +} + +// toTlvStream converts an RevocationLog record into a tlv representation. +func (rl *RevocationLog) toTlvStream() (*tlv.Stream, error) { + const ( + // A set of tlv type definitions used to serialize the body of + // revocation logs to the database. We define it here instead + // of the head of the file to avoid naming conflicts. + // + // NOTE: A migration should be added whenever this list + // changes. + ourOutputIndexType tlv.Type = 0 + theirOutputIndexType tlv.Type = 1 + commitTxHashType tlv.Type = 2 + ) + + return tlv.NewStream( + tlv.MakePrimitiveRecord(ourOutputIndexType, &rl.OurOutputIndex), + tlv.MakePrimitiveRecord( + theirOutputIndexType, &rl.TheirOutputIndex, + ), + tlv.MakePrimitiveRecord(commitTxHashType, &rl.CommitTxHash), + ) +} + +// putRevocationLog uses the fields `CommitTx` and `Htlcs` from a +// ChannelCommitment to construct a revocation log entry and saves them to +// disk. It also saves our output index and their output index, which are +// useful when creating breach retribution. +func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment, + ourOutputIndex, theirOutputIndex uint32) error { + + // Sanity check that the output indexes can be safely converted. + if ourOutputIndex > math.MaxUint16 { + return ErrOutputIndexTooBig + } + if theirOutputIndex > math.MaxUint16 { + return ErrOutputIndexTooBig + } + + rl := &RevocationLog{ + OurOutputIndex: uint16(ourOutputIndex), + TheirOutputIndex: uint16(theirOutputIndex), + CommitTxHash: commit.CommitTx.TxHash(), + HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)), + } + + for _, htlc := range commit.Htlcs { + // Skip dust HTLCs. + if htlc.OutputIndex < 0 { + continue + } + + // Sanity check that the output indexes can be safely + // converted. + if htlc.OutputIndex > math.MaxUint16 { + return ErrOutputIndexTooBig + } + + entry := &HTLCEntry{ + RHash: htlc.RHash, + RefundTimeout: htlc.RefundTimeout, + Incoming: htlc.Incoming, + OutputIndex: uint16(htlc.OutputIndex), + Amt: htlc.Amt.ToSatoshis(), + } + rl.HTLCEntries = append(rl.HTLCEntries, entry) + } + + var b bytes.Buffer + err := serializeRevocationLog(&b, rl) + if err != nil { + return err + } + + logEntrykey := makeLogKey(commit.CommitHeight) + return bucket.Put(logEntrykey[:], b.Bytes()) +} + +// fetchRevocationLog queries the revocation log bucket to find an log entry. +// Return an error if not found. +func fetchRevocationLog(log kvdb.RBucket, + updateNum uint64) (RevocationLog, error) { + + logEntrykey := makeLogKey(updateNum) + commitBytes := log.Get(logEntrykey[:]) + if commitBytes == nil { + return RevocationLog{}, ErrLogEntryNotFound + } + + commitReader := bytes.NewReader(commitBytes) + + return deserializeRevocationLog(commitReader) +} + +// serializeRevocationLog serializes a RevocationLog record based on tlv +// format. +func serializeRevocationLog(w io.Writer, rl *RevocationLog) error { + // Create the tlv stream. + tlvStream, err := rl.toTlvStream() + if err != nil { + return err + } + + // Write the tlv stream. + if err := writeTlvStream(w, tlvStream); err != nil { + return err + } + + // Write the HTLCs. + return serializeHTLCEntries(w, rl.HTLCEntries) +} + +// serializeHTLCEntries serializes a list of HTLCEntry records based on tlv +// format. +func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error { + for _, htlc := range htlcs { + // Patch the incomingTlv field. + if htlc.Incoming { + htlc.incomingTlv = 1 + } + + // Patch the amtTlv field. + htlc.amtTlv = uint64(htlc.Amt) + + // Create the tlv stream. + tlvStream, err := htlc.toTlvStream() + if err != nil { + return err + } + + // Write the tlv stream. + if err := writeTlvStream(w, tlvStream); err != nil { + return err + } + } + + return nil +} + +// deserializeRevocationLog deserializes a RevocationLog based on tlv format. +func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { + var rl RevocationLog + + // Create the tlv stream. + tlvStream, err := rl.toTlvStream() + if err != nil { + return rl, err + } + + // Read the tlv stream. + if err := readTlvStream(r, tlvStream); err != nil { + return rl, err + } + + // Read the HTLC entries. + rl.HTLCEntries, err = deserializeHTLCEntries(r) + + return rl, err +} + +// deserializeHTLCEntries deserializes a list of HTLC entries based on tlv +// format. +func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) { + var htlcs []*HTLCEntry + + for { + var htlc HTLCEntry + + // Create the tlv stream. + tlvStream, err := htlc.toTlvStream() + if err != nil { + return nil, err + } + + // Read the HTLC entry. + if err := readTlvStream(r, tlvStream); err != nil { + // We've reached the end when hitting an EOF. + if err == io.ErrUnexpectedEOF { + break + } + return nil, err + } + + // Patch the Incoming field. + if htlc.incomingTlv == 1 { + htlc.Incoming = true + } + + // Patch the Amt field. + htlc.Amt = btcutil.Amount(htlc.amtTlv) + + // Append the entry. + htlcs = append(htlcs, &htlc) + } + + return htlcs, nil +} + +// writeTlvStream is a helper function that encodes the tlv stream into the +// writer. +func writeTlvStream(w io.Writer, s *tlv.Stream) error { + var b bytes.Buffer + if err := s.Encode(&b); err != nil { + return err + } + // Write the stream's length as a varint. + err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{}) + if err != nil { + return err + } + + if _, err = w.Write(b.Bytes()); err != nil { + return err + } + + return nil +} + +// readTlvStream is a helper function that decodes the tlv stream from the +// reader. +func readTlvStream(r io.Reader, s *tlv.Stream) error { + var bodyLen uint64 + + // Read the stream's length. + bodyLen, err := tlv.ReadVarInt(r, &[8]byte{}) + switch { + // We'll convert any EOFs to ErrUnexpectedEOF, since this results in an + // invalid record. + case err == io.EOF: + return io.ErrUnexpectedEOF + + // Other unexpected errors. + case err != nil: + return err + } + + // TODO(yy): add overflow check. + lr := io.LimitReader(r, int64(bodyLen)) + return s.Decode(lr) +} From dc137bed8bb66ebfdbc1a4d6bd40772dfe385285 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Apr 2022 06:28:43 +0800 Subject: [PATCH 14/22] channel: move deprecated log handling into the new file --- channeldb/channel.go | 26 +------- channeldb/revocation_log.go | 120 ++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 24 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index 4c3390747..e6dc31b45 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -117,16 +117,6 @@ var ( // TODO(roasbeef): rename to commit chain? commitDiffKey = []byte("commit-diff-key") - // revocationLogBucketDeprecated is dedicated for storing the necessary - // delta state between channel updates required to re-construct a past - // state in order to punish a counterparty attempting a non-cooperative - // channel closure. This key should be accessed from within the - // sub-bucket of a target channel, identified by its channel point. - // - // NOTE: deprecated. This bucket is kept for read-only in case the user - // choose not to migrate the old data. - revocationLogBucketDeprecated = []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. @@ -2682,7 +2672,7 @@ func (c *OpenChannel) FindPreviousState(updateNum uint64) (*ChannelCommitment, e return ErrNoPastDeltas } - c, err := fetchChannelLogEntry(logBucket, updateNum) + c, err := fetchOldRevocationLog(logBucket, updateNum) if err != nil { return err } @@ -3653,6 +3643,7 @@ func makeLogKey(updateNum uint64) [8]byte { return key } +// TODO: delete func appendChannelLogEntry(log kvdb.RwBucket, commit *ChannelCommitment) error { @@ -3665,19 +3656,6 @@ func appendChannelLogEntry(log kvdb.RwBucket, return log.Put(logEntrykey[:], b.Bytes()) } -func fetchChannelLogEntry(log kvdb.RBucket, - updateNum uint64) (ChannelCommitment, error) { - - logEntrykey := makeLogKey(updateNum) - commitBytes := log.Get(logEntrykey[:]) - if commitBytes == nil { - return ChannelCommitment{}, ErrLogEntryNotFound - } - - commitReader := bytes.NewReader(commitBytes) - return deserializeChanCommit(commitReader) -} - func fetchThawHeight(chanBucket kvdb.RBucket) (uint32, error) { var height uint32 diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 316f59198..61a6521e0 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -15,6 +15,16 @@ import ( const OutputIndexEmpty = math.MaxUint16 var ( + // revocationLogBucketDeprecated is dedicated for storing the necessary + // delta state between channel updates required to re-construct a past + // state in order to punish a counterparty attempting a non-cooperative + // channel closure. This key should be accessed from within the + // sub-bucket of a target channel, identified by its channel point. + // + // Deprecated: This bucket is kept for read-only in case the user + // choose not to migrate the old data. + revocationLogBucketDeprecated = []byte("revocation-log-key") + // revocationLogBucket is a sub-bucket under openChannelBucket. This // sub-bucket is dedicated for storing the minimal info required to // re-construct a past state in order to punish a counterparty @@ -369,3 +379,113 @@ func readTlvStream(r io.Reader, s *tlv.Stream) error { lr := io.LimitReader(r, int64(bodyLen)) return s.Decode(lr) } + +// fetchOldRevocationLog finds the revocation log from the deprecated +// sub-bucket. +func fetchOldRevocationLog(log kvdb.RBucket, + updateNum uint64) (ChannelCommitment, error) { + + logEntrykey := makeLogKey(updateNum) + commitBytes := log.Get(logEntrykey[:]) + if commitBytes == nil { + return ChannelCommitment{}, ErrLogEntryNotFound + } + + commitReader := bytes.NewReader(commitBytes) + return deserializeChanCommit(commitReader) +} + +// fetchRevocationLogCompatible finds the revocation log from both the +// revocationLogBucket and revocationLogBucketDeprecated for compatibility +// concern. It returns three values, +// - RevocationLog, if this is non-nil, it means we've found the log in the +// new bucket. +// - ChannelCommitment, if this is non-nil, it means we've found the log in the +// old bucket. +// - error, this can happen if the log cannot be found in neither buckets. +func fetchRevocationLogCompatible(chanBucket kvdb.RBucket, + updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { + + // Look into the new bucket first. + logBucket := chanBucket.NestedReadBucket(revocationLogBucket) + if logBucket != nil { + rl, err := fetchRevocationLog(logBucket, updateNum) + // We've found the record, no need to visit the old bucket. + if err == nil { + return &rl, nil, nil + } + + // Return the error if it doesn't say the log cannot be found. + if err != ErrLogEntryNotFound { + return nil, nil, err + } + } + + // Otherwise, look into the old bucket and try to find the log there. + oldBucket := chanBucket.NestedReadBucket(revocationLogBucketDeprecated) + if oldBucket != nil { + c, err := fetchOldRevocationLog(oldBucket, updateNum) + if err != nil { + return nil, nil, err + } + + // Found an old record and return it. + return nil, &c, nil + } + + // If both the buckets are nil, then the sub-buckets haven't been + // created yet. + if logBucket == nil && oldBucket == nil { + return nil, nil, ErrNoPastDeltas + } + + // Otherwise, we've tried to query the new bucket but the log cannot be + // found. + return nil, nil, ErrLogEntryNotFound +} + +// fetchLogBucket returns a read bucket by visiting both the new and the old +// bucket. +func fetchLogBucket(chanBucket kvdb.RBucket) (kvdb.RBucket, error) { + logBucket := chanBucket.NestedReadBucket(revocationLogBucket) + if logBucket == nil { + logBucket = chanBucket.NestedReadBucket( + revocationLogBucketDeprecated, + ) + if logBucket == nil { + return nil, ErrNoPastDeltas + } + } + + return logBucket, nil +} + +// deleteLogBucket deletes the both the new and old revocation log buckets. +func deleteLogBucket(chanBucket kvdb.RwBucket) error { + // Check if the bucket exists and delete it. + logBucket := chanBucket.NestedReadWriteBucket( + revocationLogBucket, + ) + if logBucket != nil { + err := chanBucket.DeleteNestedBucket(revocationLogBucket) + if err != nil { + return err + } + } + + // We also check whether the old revocation log bucket exists + // and delete it if so. + oldLogBucket := chanBucket.NestedReadWriteBucket( + revocationLogBucketDeprecated, + ) + if oldLogBucket != nil { + err := chanBucket.DeleteNestedBucket( + revocationLogBucketDeprecated, + ) + if err != nil { + return err + } + } + + return nil +} From a129930a957c74e656fabee4ced51178e21b56d2 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Thu, 7 Apr 2022 07:22:24 +0800 Subject: [PATCH 15/22] channeldb: add unit test for revocation log --- channeldb/revocation_log_test.go | 563 +++++++++++++++++++++++++++++++ 1 file changed, 563 insertions(+) create mode 100644 channeldb/revocation_log_test.go diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go new file mode 100644 index 000000000..93ec784b2 --- /dev/null +++ b/channeldb/revocation_log_test.go @@ -0,0 +1,563 @@ +package channeldb + +import ( + "bytes" + "io" + "math" + "math/rand" + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntest/channels" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/tlv" + "github.com/stretchr/testify/require" +) + +const ( + // testType is used for creating a testing tlv record type. We use 10 + // here so it's easier to be recognized by its hex value, 0xa. + testType tlv.Type = 10 +) + +var ( + // testValue is used for creating tlv record. + testValue = uint8(255) // 0xff + + // testValueBytes is the tlv encoded testValue. + testValueBytes = []byte{ + 0x3, // total length = 3 + 0xa, // type = 10 + 0x1, // length = 1 + 0xff, // value = 255 + } + + testHTLCEntry = HTLCEntry{ + RefundTimeout: 100, + OutputIndex: 10, + Incoming: true, + Amt: 255, + amtTlv: 255, + incomingTlv: 1, + } + testHTLCEntryBytes = []byte{ + // Body length 58. + 0x39, + // Rhash tlv. + 0x0, 0x20, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + // RefundTimeout tlv. + 0x1, 0x4, 0x0, 0x0, 0x0, 0x64, + // OutputIndex tlv. + 0x2, 0x2, 0x0, 0xa, + // Incoming tlv. + 0x3, 0x1, 0x1, + // Amt tlv. + 0x4, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, + } + + testChannelCommit = ChannelCommitment{ + CommitHeight: 999, + LocalBalance: lnwire.MilliSatoshi(9000), + RemoteBalance: lnwire.MilliSatoshi(3000), + CommitFee: btcutil.Amount(rand.Int63()), + FeePerKw: btcutil.Amount(5000), + CommitTx: channels.TestFundingTx, + CommitSig: bytes.Repeat([]byte{1}, 71), + Htlcs: []HTLC{{ + RefundTimeout: testHTLCEntry.RefundTimeout, + OutputIndex: int32(testHTLCEntry.OutputIndex), + Incoming: testHTLCEntry.Incoming, + Amt: lnwire.NewMSatFromSatoshis( + testHTLCEntry.Amt, + ), + }}, + } + + testRevocationLog = RevocationLog{ + OurOutputIndex: 0, + TheirOutputIndex: 1, + CommitTxHash: testChannelCommit.CommitTx.TxHash(), + HTLCEntries: []*HTLCEntry{&testHTLCEntry}, + } + testRevocationLogBytes = []byte{ + // Body length 42. + 0x2a, + // OurOutputIndex tlv. + 0x0, 0x2, 0x0, 0x0, + // TheirOutputIndex tlv. + 0x1, 0x2, 0x0, 0x1, + // CommitTxHash tlv. + 0x2, 0x20, + 0x28, 0x76, 0x2, 0x59, 0x1d, 0x9d, 0x64, 0x86, + 0x6e, 0x60, 0x29, 0x23, 0x1d, 0x5e, 0xc5, 0xe6, + 0xbd, 0xf7, 0xd3, 0x9b, 0x16, 0x7d, 0x0, 0xff, + 0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd, + } +) + +func TestWriteTLVStream(t *testing.T) { + t.Parallel() + + // Create a dummy tlv stream for testing. + ts, err := tlv.NewStream( + tlv.MakePrimitiveRecord(testType, &testValue), + ) + require.NoError(t, err) + + // Write the tlv stream. + buf := bytes.NewBuffer([]byte{}) + err = writeTlvStream(buf, ts) + require.NoError(t, err) + + // Check the bytes are written as expected. + require.Equal(t, testValueBytes, buf.Bytes()) +} + +func TestReadTLVStream(t *testing.T) { + t.Parallel() + + var valueRead uint8 + + // Create a dummy tlv stream for testing. + ts, err := tlv.NewStream( + tlv.MakePrimitiveRecord(testType, &valueRead), + ) + require.NoError(t, err) + + // Read the tlv stream. + buf := bytes.NewBuffer(testValueBytes) + err = readTlvStream(buf, ts) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Equal(t, testValue, valueRead) +} + +func TestReadTLVStreamErr(t *testing.T) { + t.Parallel() + + var valueRead uint8 + + // Create a dummy tlv stream for testing. + ts, err := tlv.NewStream( + tlv.MakePrimitiveRecord(testType, &valueRead), + ) + require.NoError(t, err) + + // Use empty bytes to cause an EOF. + b := []byte{} + + // Read the tlv stream. + buf := bytes.NewBuffer(b) + err = readTlvStream(buf, ts) + require.ErrorIs(t, err, io.ErrUnexpectedEOF) + + // Check the bytes are not read. + require.Zero(t, valueRead) +} + +func TestSerializeHTLCEntries(t *testing.T) { + t.Parallel() + + // Copy the testHTLCEntry. + entry := testHTLCEntry + + // Set the internal fields to empty values so we can test the bytes are + // padded. + entry.incomingTlv = 0 + entry.amtTlv = 0 + + // Write the tlv stream. + buf := bytes.NewBuffer([]byte{}) + err := serializeHTLCEntries(buf, []*HTLCEntry{&entry}) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Equal(t, testHTLCEntryBytes, buf.Bytes()) +} + +func TestSerializeRevocationLog(t *testing.T) { + t.Parallel() + + // Copy the testRevocationLog. + rl := testRevocationLog + + // Write the tlv stream. + buf := bytes.NewBuffer([]byte{}) + err := serializeRevocationLog(buf, &rl) + require.NoError(t, err) + + // Check the expected bytes on the body of the revocation log. + bodyIndex := buf.Len() - len(testHTLCEntryBytes) + require.Equal(t, testRevocationLogBytes, buf.Bytes()[:bodyIndex]) +} + +func TestDerializeHTLCEntries(t *testing.T) { + t.Parallel() + + // Read the tlv stream. + buf := bytes.NewBuffer(testHTLCEntryBytes) + htlcs, err := deserializeHTLCEntries(buf) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Len(t, htlcs, 1) + require.Equal(t, &testHTLCEntry, htlcs[0]) +} + +func TestDerializeRevocationLog(t *testing.T) { + t.Parallel() + + // Construct the full bytes. + b := testRevocationLogBytes + b = append(b, testHTLCEntryBytes...) + + // Read the tlv stream. + buf := bytes.NewBuffer(b) + rl, err := deserializeRevocationLog(buf) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Len(t, rl.HTLCEntries, 1) + require.Equal(t, testRevocationLog, rl) +} + +func TestFetchLogBucket(t *testing.T) { + t.Parallel() + + fullDB, cleanUp, err := MakeTestDB() + require.NoError(t, err) + defer cleanUp() + + backend := fullDB.ChannelStateDB().backend + + // Test that when neither of the buckets exists, an error is returned. + err = kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket) + require.NoError(t, err) + + // Check an error is returned when there's no sub bucket. + _, err = fetchLogBucket(chanBucket) + return err + }, func() {}) + require.ErrorIs(t, err, ErrNoPastDeltas) + + // Test a successful fetch. + err = kvdb.Update(backend, func(tx kvdb.RwTx) error { + chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket) + require.NoError(t, err) + + _, err = chanBucket.CreateBucket(revocationLogBucket) + require.NoError(t, err) + + // Check an error is returned when there's no sub bucket. + _, err = fetchLogBucket(chanBucket) + return err + }, func() {}) + require.NoError(t, err) +} + +func TestDeleteLogBucket(t *testing.T) { + t.Parallel() + + fullDB, cleanUp, err := MakeTestDB() + require.NoError(t, err) + defer cleanUp() + + backend := fullDB.ChannelStateDB().backend + + err = kvdb.Update(backend, func(tx kvdb.RwTx) error { + // Create the buckets. + chanBucket, _, err := createTestRevocatoinLogBuckets(tx) + require.NoError(t, err) + + // Create the buckets again should give us an error. + _, _, err = createTestRevocatoinLogBuckets(tx) + require.ErrorIs(t, err, kvdb.ErrBucketExists) + + // Delete both buckets. + err = deleteLogBucket(chanBucket) + require.NoError(t, err) + + // Create the buckets again should give us NO error. + _, _, err = createTestRevocatoinLogBuckets(tx) + return err + }, func() {}) + require.NoError(t, err) +} + +func TestPutRevocationLog(t *testing.T) { + t.Parallel() + + // Create a test commit that has a large htlc output index. + testHtlc := HTLC{OutputIndex: math.MaxUint16 + 1} + testCommit := testChannelCommit + testCommit.Htlcs = []HTLC{testHtlc} + + // Create a test commit that has a dust HTLC. + testHtlcDust := HTLC{OutputIndex: -1} + testCommitDust := testChannelCommit + testCommitDust.Htlcs = append(testCommitDust.Htlcs, testHtlcDust) + + testCases := []struct { + name string + commit ChannelCommitment + ourIndex uint32 + theirIndex uint32 + expectedErr error + expectedLog RevocationLog + }{ + { + // Test a normal put operation. + name: "successful put", + commit: testChannelCommit, + ourIndex: 0, + theirIndex: 1, + expectedErr: nil, + expectedLog: testRevocationLog, + }, + { + // Test our index too big. + name: "our index too big", + commit: testChannelCommit, + ourIndex: math.MaxUint16 + 1, + theirIndex: 1, + expectedErr: ErrOutputIndexTooBig, + expectedLog: RevocationLog{}, + }, + { + // Test their index too big. + name: "their index too big", + commit: testChannelCommit, + ourIndex: 0, + theirIndex: math.MaxUint16 + 1, + expectedErr: ErrOutputIndexTooBig, + expectedLog: RevocationLog{}, + }, + { + // Test htlc output index too big. + name: "htlc index too big", + commit: testCommit, + ourIndex: 0, + theirIndex: 1, + expectedErr: ErrOutputIndexTooBig, + expectedLog: RevocationLog{}, + }, + { + // Test dust htlc is not saved. + name: "dust htlc not saved", + commit: testCommitDust, + ourIndex: 0, + theirIndex: 1, + expectedErr: nil, + expectedLog: testRevocationLog, + }, + } + + for _, tc := range testCases { + tc := tc + + fullDB, cleanUp, err := MakeTestDB() + require.NoError(t, err) + defer cleanUp() + + backend := fullDB.ChannelStateDB().backend + + // Construct the testing db transaction. + dbTx := func(tx kvdb.RwTx) (RevocationLog, error) { + // Create the buckets. + _, bucket, err := createTestRevocatoinLogBuckets(tx) + require.NoError(t, err) + + // Save the log. + err = putRevocationLog( + bucket, &tc.commit, tc.ourIndex, tc.theirIndex, + ) + if err != nil { + return RevocationLog{}, err + } + + // Read the saved log. + return fetchRevocationLog( + bucket, tc.commit.CommitHeight, + ) + } + + t.Run(tc.name, func(t *testing.T) { + var rl RevocationLog + err := kvdb.Update(backend, func(tx kvdb.RwTx) error { + record, err := dbTx(tx) + rl = record + return err + }, func() {}) + + require.Equal(t, tc.expectedErr, err) + require.Equal(t, tc.expectedLog, rl) + }) + } +} + +func TestFetchRevocationLogCompatible(t *testing.T) { + t.Parallel() + + knownHeight := testChannelCommit.CommitHeight + unknownHeight := knownHeight + 1 + logKey := makeLogKey(knownHeight) + + testCases := []struct { + name string + updateNum uint64 + expectedErr error + createRl bool + createCommit bool + expectRl bool + expectCommit bool + }{ + { + // Test we can fetch the new log. + name: "fetch new log", + updateNum: knownHeight, + expectedErr: nil, + createRl: true, + expectRl: true, + }, + { + // Test we can fetch the legacy log. + name: "fetch legacy log", + updateNum: knownHeight, + expectedErr: nil, + createCommit: true, + expectCommit: true, + }, + { + // Test we only fetch the new log when both logs exist. + name: "fetch new log only", + updateNum: knownHeight, + expectedErr: nil, + createRl: true, + createCommit: true, + expectRl: true, + }, + { + // Test no past deltas when the buckets do not exist. + name: "no buckets created", + updateNum: unknownHeight, + expectedErr: ErrNoPastDeltas, + }, + { + // Test no logs found when the height is unknown. + name: "no log found", + updateNum: unknownHeight, + expectedErr: ErrLogEntryNotFound, + createRl: true, + createCommit: true, + }, + } + + for _, tc := range testCases { + tc := tc + + fullDB, cleanUp, err := MakeTestDB() + require.NoError(t, err) + defer cleanUp() + + backend := fullDB.ChannelStateDB().backend + + var ( + rl *RevocationLog + commit *ChannelCommitment + ) + + // Setup the buckets and fill the test data if specified. + err = kvdb.Update(backend, func(tx kvdb.RwTx) error { + // Create the root bucket. + cb, err := tx.CreateTopLevelBucket(openChannelBucket) + require.NoError(t, err) + + // Create the revocation log if specified. + if tc.createRl { + lb, err := cb.CreateBucket(revocationLogBucket) + require.NoError(t, err) + + err = putRevocationLog( + lb, &testChannelCommit, 0, 1, + ) + require.NoError(t, err) + } + + // Create the channel commit if specified. + if tc.createCommit { + legacyBucket, err := cb.CreateBucket( + revocationLogBucketDeprecated, + ) + require.NoError(t, err) + + buf := bytes.NewBuffer([]byte{}) + err = serializeChanCommit( + buf, &testChannelCommit, + ) + require.NoError(t, err) + + err = legacyBucket.Put(logKey[:], buf.Bytes()) + require.NoError(t, err) + } + + return nil + }, func() {}) + + // Construct the testing db transaction. + dbTx := func(tx kvdb.RTx) error { + cb := tx.ReadBucket(openChannelBucket) + + rl, commit, err = fetchRevocationLogCompatible( + cb, tc.updateNum, + ) + return err + } + + t.Run(tc.name, func(t *testing.T) { + err := kvdb.View(backend, dbTx, func() {}) + require.Equal(t, tc.expectedErr, err) + + // Check the expected revocation log is returned. + if tc.expectRl { + require.NotNil(t, rl) + } else { + require.Nil(t, rl) + } + + // Check the expected channel commit is returned. + if tc.expectCommit { + require.NotNil(t, commit) + } else { + require.Nil(t, commit) + } + }) + } +} + +func createTestRevocatoinLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket, + kvdb.RwBucket, error) { + + chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket) + if err != nil { + return nil, nil, err + } + + logBucket, err := chanBucket.CreateBucket(revocationLogBucket) + if err != nil { + return nil, nil, err + } + + _, err = chanBucket.CreateBucket(revocationLogBucketDeprecated) + if err != nil { + return nil, nil, err + } + + return chanBucket, logBucket, nil +} From df810114cfc4c85193da118a9880ff379bca040f Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Apr 2022 05:37:46 +0800 Subject: [PATCH 16/22] lnwallet: add method to compute output indexes This commit adds a new method, findOutputIndexesFromRemote to compute the our/their output indexes. As we will see in the following commit, saving these two fields(4+4=8 bytes) will end up saving us some disk space. --- lnwallet/commitment.go | 75 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index be6952a1e..c4f4d931a 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -1,11 +1,13 @@ package lnwallet import ( + "bytes" "fmt" "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" @@ -960,3 +962,76 @@ func addHTLC(commitTx *wire.MsgTx, ourCommit bool, return nil } + +// findOutputIndexesFromRemote finds the index of our and their outputs from +// the remote commitment transaction. It derives the key ring to compute the +// output scripts and compares them against the outputs inside the commitment +// to find the match. +func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, + chanState *channeldb.OpenChannel) (uint32, uint32, error) { + + // Init the output indexes as empty. + ourIndex := uint32(channeldb.OutputIndexEmpty) + theirIndex := uint32(channeldb.OutputIndexEmpty) + + chanCommit := chanState.RemoteCommitment + _, commitmentPoint := btcec.PrivKeyFromBytes(revocationPreimage[:]) + + // With the commitment point generated, we can now derive the king ring + // which will be used to generate the output scripts. + keyRing := DeriveCommitmentKeys( + commitmentPoint, false, chanState.ChanType, + &chanState.LocalChanCfg, &chanState.RemoteChanCfg, + ) + + // Since it's remote commitment chain, we'd used the mirrored values. + // + // We use the remote's channel config for the csv delay. + theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay) + + // If we are the initiator of this channel, then it's be false from the + // remote's PoV. + isRemoteInitiator := !chanState.IsInitiator + + var leaseExpiry uint32 + if chanState.ChanType.HasLeaseExpiration() { + leaseExpiry = chanState.ThawHeight + } + + // Map the scripts from our PoV. When facing a local commitment, the to + // local output belongs to us and the to remote output belongs to them. + // When facing a remote commitment, the to local output belongs to them + // and the to remote output belongs to us. + + // Compute the to local script. From our PoV, when facing a remote + // commitment, the to local output belongs to them. + theirScript, err := CommitScriptToSelf( + chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, + keyRing.RevocationKey, theirDelay, leaseExpiry, + ) + if err != nil { + return ourIndex, theirIndex, err + } + + // Compute the to remote script. From our PoV, when facing a remote + // commitment, the to remote output belongs to us. + ourScript, _, err := CommitScriptToRemote( + chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, + leaseExpiry, + ) + if err != nil { + return ourIndex, theirIndex, err + } + + // Now compare the scripts to find our/their output index. + for i, txOut := range chanCommit.CommitTx.TxOut { + switch { + case bytes.Equal(txOut.PkScript, ourScript.PkScript): + ourIndex = uint32(i) + case bytes.Equal(txOut.PkScript, theirScript.PkScript): + theirIndex = uint32(i) + } + } + + return ourIndex, theirIndex, nil +} From 37b11c4503c332b985233c05fa082c794178a79d Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Fri, 8 Apr 2022 07:36:26 +0800 Subject: [PATCH 17/22] channeldb+lnwallet: store revocation log using the new format This commit removes the usage of the old revocation log bucket and starts to perform db operations using the new sub-bucket. --- channeldb/channel.go | 62 +++++++++++++------------------- channeldb/channel_test.go | 76 ++++++++++++++++++++++++++++++++++----- channeldb/db_test.go | 4 ++- lnwallet/channel.go | 20 +++++++++-- 4 files changed, 114 insertions(+), 48 deletions(-) diff --git a/channeldb/channel.go b/channeldb/channel.go index e6dc31b45..4ece47a57 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -2306,7 +2306,7 @@ func (c *OpenChannel) InsertNextRevocation(revKey *btcec.PublicKey) error { // set of local updates that the peer still needs to send us a signature for. // We store this set of updates in case we go down. func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, - updates []LogUpdate) error { + updates []LogUpdate, ourOutputIndex, theirOutputIndex uint32) error { c.Lock() defer c.Unlock() @@ -2352,7 +2352,7 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, // TODO(roasbeef): could make the deltas relative, would save // space, but then tradeoff for more disk-seeks to recover the // full state. - logKey := revocationLogBucketDeprecated + logKey := revocationLogBucket logBucket, err := chanBucket.CreateBucketIfNotExists(logKey) if err != nil { return err @@ -2379,9 +2379,10 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg, // With the commitment pointer swapped, we can now add the // revoked (prior) state to the revocation log. - // - // TODO(roasbeef): store less - err = appendChannelLogEntry(logBucket, &c.RemoteCommitment) + err = putRevocationLog( + logBucket, &c.RemoteCommitment, + ourOutputIndex, theirOutputIndex, + ) if err != nil { return err } @@ -2591,9 +2592,9 @@ func (c *OpenChannel) revocationLogTailCommitHeight() (uint64, error) { return err } - logBucket := chanBucket.NestedReadBucket(revocationLogBucketDeprecated) - if logBucket == nil { - return ErrNoPastDeltas + logBucket, err := fetchLogBucket(chanBucket) + if err != nil { + return err } // Once we have the bucket that stores the revocation log from @@ -2654,11 +2655,15 @@ func (c *OpenChannel) CommitmentHeight() (uint64, error) { // intended to be used for obtaining the relevant data needed to claim all // funds rightfully spendable in the case of an on-chain broadcast of the // commitment transaction. -func (c *OpenChannel) FindPreviousState(updateNum uint64) (*ChannelCommitment, error) { +func (c *OpenChannel) FindPreviousState( + updateNum uint64) (*RevocationLog, *ChannelCommitment, error) { + c.RLock() defer c.RUnlock() - var commit ChannelCommitment + commit := &ChannelCommitment{} + rl := &RevocationLog{} + err := kvdb.View(c.Db.backend, func(tx kvdb.RTx) error { chanBucket, err := fetchChanBucket( tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash, @@ -2667,24 +2672,24 @@ func (c *OpenChannel) FindPreviousState(updateNum uint64) (*ChannelCommitment, e return err } - logBucket := chanBucket.NestedReadBucket(revocationLogBucketDeprecated) - if logBucket == nil { - return ErrNoPastDeltas - } - - c, err := fetchOldRevocationLog(logBucket, updateNum) + // Find the revocation log from both the new and the old + // bucket. + r, c, err := fetchRevocationLogCompatible(chanBucket, updateNum) if err != nil { return err } + rl = r commit = c return nil }, func() {}) if err != nil { - return nil, err + return nil, nil, err } - return &commit, nil + // Either the `rl` or the `commit` is nil here. We return them as-is + // and leave it to the caller to decide its following action. + return rl, commit, nil } // ClosureType is an enum like structure that details exactly _how_ a channel @@ -2881,12 +2886,8 @@ func (c *OpenChannel) CloseChannel(summary *ChannelCloseSummary, // With the base channel data deleted, attempt to delete the // information stored within the revocation log. - logBucket := chanBucket.NestedReadWriteBucket(revocationLogBucketDeprecated) - if logBucket != nil { - err = chanBucket.DeleteNestedBucket(revocationLogBucketDeprecated) - if err != nil { - return err - } + if err := deleteLogBucket(chanBucket); err != nil { + return err } err = chainBucket.DeleteNestedBucket(chanPointBuf.Bytes()) @@ -3643,19 +3644,6 @@ func makeLogKey(updateNum uint64) [8]byte { return key } -// TODO: delete -func appendChannelLogEntry(log kvdb.RwBucket, - commit *ChannelCommitment) error { - - var b bytes.Buffer - if err := serializeChanCommit(&b, commit); err != nil { - return err - } - - logEntrykey := makeLogKey(commit.CommitHeight) - return log.Put(logEntrykey[:], b.Bytes()) -} - func fetchThawHeight(chanBucket kvdb.RBucket) (uint32, error) { var height uint32 diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index c6033d9ed..35607fd55 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -52,8 +52,17 @@ var ( Port: 18555, } - // keyLocIndex is the KeyLocator Index we use for TestKeyLocatorEncoding. + // keyLocIndex is the KeyLocator Index we use for + // TestKeyLocatorEncoding. keyLocIndex = uint32(2049) + + // dummyLocalOutputIndex specifics a default value for our output index + // in this test. + dummyLocalOutputIndex = uint32(0) + + // dummyRemoteOutIndex specifics a default value for their output index + // in this test. + dummyRemoteOutIndex = uint32(1) ) // testChannelParams is a struct which details the specifics of how a channel @@ -548,6 +557,32 @@ func assertCommitmentEqual(t *testing.T, a, b *ChannelCommitment) { } } +// assertRevocationLogEntryEqual asserts that, for all the fields of a given +// revocation log entry, their values match those on a given ChannelCommitment. +func assertRevocationLogEntryEqual(t *testing.T, c *ChannelCommitment, + r *RevocationLog) { + + // Check the common fields. + require.EqualValues( + t, r.CommitTxHash, c.CommitTx.TxHash(), "CommitTx mismatch", + ) + + // Now check the common fields from the HTLCs. + require.Equal(t, len(r.HTLCEntries), len(c.Htlcs), "HTLCs len mismatch") + for i, rHtlc := range r.HTLCEntries { + cHtlc := c.Htlcs[i] + require.Equal(t, rHtlc.RHash, cHtlc.RHash, "RHash mismatch") + require.Equal(t, rHtlc.Amt, cHtlc.Amt.ToSatoshis(), + "Amt mismatch") + require.Equal(t, rHtlc.RefundTimeout, cHtlc.RefundTimeout, + "RefundTimeout mismatch") + require.EqualValues(t, rHtlc.OutputIndex, cHtlc.OutputIndex, + "OutputIndex mismatch") + require.Equal(t, rHtlc.Incoming, cHtlc.Incoming, + "Incoming mismatch") + } +} + func TestChannelStateTransition(t *testing.T) { t.Parallel() @@ -748,7 +783,9 @@ func TestChannelStateTransition(t *testing.T) { fwdPkg := NewFwdPkg(channel.ShortChanID(), oldRemoteCommit.CommitHeight, diskCommitDiff.LogUpdates, nil) - err = channel.AdvanceCommitChainTail(fwdPkg, nil) + err = channel.AdvanceCommitChainTail( + fwdPkg, nil, dummyLocalOutputIndex, dummyRemoteOutIndex, + ) if err != nil { t.Fatalf("unable to append to revocation log: %v", err) } @@ -761,16 +798,24 @@ func TestChannelStateTransition(t *testing.T) { // We should be able to fetch the channel delta created above by its // update number with all the state properly reconstructed. - diskPrevCommit, err := channel.FindPreviousState( + diskPrevCommit, _, err := channel.FindPreviousState( oldRemoteCommit.CommitHeight, ) if err != nil { t.Fatalf("unable to fetch past delta: %v", err) } + // Check the output indexes are saved as expected. + require.EqualValues( + t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex, + ) + require.EqualValues( + t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex, + ) + // The two deltas (the original vs the on-disk version) should // identical, and all HTLC data should properly be retained. - assertCommitmentEqual(t, &oldRemoteCommit, diskPrevCommit) + assertRevocationLogEntryEqual(t, &oldRemoteCommit, diskPrevCommit) // The state number recovered from the tail of the revocation log // should be identical to this current state. @@ -796,17 +841,30 @@ func TestChannelStateTransition(t *testing.T) { fwdPkg = NewFwdPkg(channel.ShortChanID(), oldRemoteCommit.CommitHeight, nil, nil) - err = channel.AdvanceCommitChainTail(fwdPkg, nil) + err = channel.AdvanceCommitChainTail( + fwdPkg, nil, dummyLocalOutputIndex, dummyRemoteOutIndex, + ) if err != nil { t.Fatalf("unable to append to revocation log: %v", err) } // Once again, fetch the state and ensure it has been properly updated. - prevCommit, err := channel.FindPreviousState(oldRemoteCommit.CommitHeight) + prevCommit, _, err := channel.FindPreviousState( + oldRemoteCommit.CommitHeight, + ) if err != nil { t.Fatalf("unable to fetch past delta: %v", err) } - assertCommitmentEqual(t, &oldRemoteCommit, prevCommit) + + // Check the output indexes are saved as expected. + require.EqualValues( + t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex, + ) + require.EqualValues( + t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex, + ) + + assertRevocationLogEntryEqual(t, &oldRemoteCommit, prevCommit) // Once again, state number recovered from the tail of the revocation // log should be identical to this current state. @@ -860,7 +918,9 @@ func TestChannelStateTransition(t *testing.T) { // Attempting to find previous states on the channel should fail as the // revocation log has been deleted. - _, err = updatedChannel[0].FindPreviousState(oldRemoteCommit.CommitHeight) + _, _, err = updatedChannel[0].FindPreviousState( + oldRemoteCommit.CommitHeight, + ) if err == nil { t.Fatal("revocation log search should have failed") } diff --git a/channeldb/db_test.go b/channeldb/db_test.go index 6cc5c6d44..b741a2ad7 100644 --- a/channeldb/db_test.go +++ b/channeldb/db_test.go @@ -409,7 +409,9 @@ func TestRestoreChannelShells(t *testing.T) { if err != ErrNoRestoredChannelMutation { t.Fatalf("able to mutate restored channel") } - err = channel.AdvanceCommitChainTail(nil, nil) + err = channel.AdvanceCommitChainTail( + nil, nil, dummyLocalOutputIndex, dummyRemoteOutIndex, + ) if err != ErrNoRestoredChannelMutation { t.Fatalf("able to mutate restored channel") } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index f27c55608..b9cde8dc8 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2282,7 +2282,7 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // Query the on-disk revocation log for the snapshot which was recorded // at this particular state num. - revokedSnapshot, err := chanState.FindPreviousState(stateNum) + _, revokedSnapshot, err := chanState.FindPreviousState(stateNum) if err != nil { return nil, err } @@ -4872,12 +4872,28 @@ func (lc *LightningChannel) ReceiveRevocation(revMsg *lnwire.RevokeAndAck) ( source, remoteChainTail, addUpdates, settleFailUpdates, ) + // We will soon be saving the current remote commitment to revocation + // log bucket, which is `lc.channelState.RemoteCommitment`. After that, + // the `RemoteCommitment` will be replaced with a newer version found + // in `CommitDiff`. Thus we need to compute the output indexes here + // before the change since the indexes are meant for the current, + // revoked remote commitment. + ourOutputIndex, theirOutputIndex, err := findOutputIndexesFromRemote( + revocation, lc.channelState, + ) + if err != nil { + return nil, nil, nil, nil, err + } + // At this point, the revocation has been accepted, and we've rotated // the current revocation key+hash for the remote party. Therefore we // sync now to ensure the revocation producer state is consistent with // the current commitment height and also to advance the on-disk // commitment chain. - err = lc.channelState.AdvanceCommitChainTail(fwdPkg, localPeerUpdates) + err = lc.channelState.AdvanceCommitChainTail( + fwdPkg, localPeerUpdates, + ourOutputIndex, theirOutputIndex, + ) if err != nil { return nil, nil, nil, nil, err } From 953767961e1beb53600e019e040a2b8db4a0cc3a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Mon, 11 Apr 2022 01:58:55 +0800 Subject: [PATCH 18/22] multi: use txid instead of tx in BreachRetribution This commit replaces the field `BreachTransaction` with `BreachTxHash` as there's no need to pass that large raw transaction. --- contractcourt/breacharbiter.go | 2 +- contractcourt/breacharbiter_test.go | 8 ++++---- contractcourt/chain_watcher.go | 2 +- lnwallet/channel.go | 8 ++++---- watchtower/wtclient/backup_task.go | 2 +- .../wtclient/backup_task_internal_test.go | 6 +++--- watchtower/wtclient/client_test.go | 18 +++++++++--------- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/contractcourt/breacharbiter.go b/contractcourt/breacharbiter.go index 1735d0cec..b5f7583cc 100644 --- a/contractcourt/breacharbiter.go +++ b/contractcourt/breacharbiter.go @@ -1243,7 +1243,7 @@ func newRetributionInfo(chanPoint *wire.OutPoint, } return &retributionInfo{ - commitHash: breachInfo.BreachTransaction.TxHash(), + commitHash: breachInfo.BreachTxHash, chainHash: breachInfo.ChainHash, chanPoint: *chanPoint, breachedOutputs: breachedOutputs, diff --git a/contractcourt/breacharbiter_test.go b/contractcourt/breacharbiter_test.go index 3cb16c084..4c8863ded 100644 --- a/contractcourt/breacharbiter_test.go +++ b/contractcourt/breacharbiter_test.go @@ -1051,7 +1051,7 @@ func TestBreachHandoffSuccess(t *testing.T) { processACK <- brarErr }, BreachRetribution: &lnwallet.BreachRetribution{ - BreachTransaction: bobClose.CloseTx, + BreachTxHash: bobClose.CloseTx.TxHash(), LocalOutputSignDesc: &input.SignDescriptor{ Output: &wire.TxOut{ PkScript: breachKeys[0], @@ -1085,7 +1085,7 @@ func TestBreachHandoffSuccess(t *testing.T) { processACK <- brarErr }, BreachRetribution: &lnwallet.BreachRetribution{ - BreachTransaction: bobClose.CloseTx, + BreachTxHash: bobClose.CloseTx.TxHash(), LocalOutputSignDesc: &input.SignDescriptor{ Output: &wire.TxOut{ PkScript: breachKeys[0], @@ -1137,7 +1137,7 @@ func TestBreachHandoffFail(t *testing.T) { processACK <- brarErr }, BreachRetribution: &lnwallet.BreachRetribution{ - BreachTransaction: bobClose.CloseTx, + BreachTxHash: bobClose.CloseTx.TxHash(), LocalOutputSignDesc: &input.SignDescriptor{ Output: &wire.TxOut{ PkScript: breachKeys[0], @@ -1179,7 +1179,7 @@ func TestBreachHandoffFail(t *testing.T) { processACK <- brarErr }, BreachRetribution: &lnwallet.BreachRetribution{ - BreachTransaction: bobClose.CloseTx, + BreachTxHash: bobClose.CloseTx.TxHash(), LocalOutputSignDesc: &input.SignDescriptor{ Output: &wire.TxOut{ PkScript: breachKeys[0], diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index c9b5e7ef9..f9d919023 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -812,7 +812,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail, // own broadcasted state we are looking at. Therefore check that the // commit matches before assuming it was a breach. commitHash := commitSpend.SpendingTx.TxHash() - if retribution.BreachTransaction.TxHash() != commitHash { + if retribution.BreachTxHash != commitHash { return false, nil } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index b9cde8dc8..b151b9efa 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -2213,10 +2213,10 @@ type HtlcRetribution struct { // transaction. The BreachRetribution is then sent over the ContractBreach // channel in order to allow the subscriber of the channel to dispatch justice. type BreachRetribution struct { - // BreachTransaction is the transaction which breached the channel + // BreachTxHash is the transaction hash which breached the channel // contract by spending from the funding multi-sig with a revoked // commitment transaction. - BreachTransaction *wire.MsgTx + BreachTxHash chainhash.Hash // BreachHeight records the block height confirming the breach // transaction, used as a height hint when registering for @@ -2233,7 +2233,7 @@ type BreachRetribution struct { // LocalOutputSignDesc is a SignDescriptor which is capable of // generating the signature necessary to sweep the output within the - // BreachTransaction that pays directly us. + // breach transaction that pays directly us. // // NOTE: A nil value indicates that the local output is considered dust // according to the remote party's dust limit. @@ -2458,8 +2458,8 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // BreachRetribution struct which houses all the data necessary to // swiftly bring justice to the cheating remote party. return &BreachRetribution{ + BreachTxHash: commitHash, ChainHash: chanState.ChainHash, - BreachTransaction: revokedSnapshot.CommitTx, BreachHeight: breachHeight, RevokedStateNum: stateNum, LocalOutpoint: ourOutpoint, diff --git a/watchtower/wtclient/backup_task.go b/watchtower/wtclient/backup_task.go index 6f9d2082a..5ac179c0a 100644 --- a/watchtower/wtclient/backup_task.go +++ b/watchtower/wtclient/backup_task.go @@ -346,7 +346,7 @@ func (t *backupTask) craftSessionPayload( } } - breachTxID := t.breachInfo.BreachTransaction.TxHash() + breachTxID := t.breachInfo.BreachTxHash // Compute the breach key as SHA256(txid). hint, key := blob.NewBreachHintAndKeyFromHash(&breachTxID) diff --git a/watchtower/wtclient/backup_task_internal_test.go b/watchtower/wtclient/backup_task_internal_test.go index 790283328..661f5e144 100644 --- a/watchtower/wtclient/backup_task_internal_test.go +++ b/watchtower/wtclient/backup_task_internal_test.go @@ -124,8 +124,8 @@ func genTaskTest( // the breach transaction, which we will continue to modify. breachTxn := wire.NewMsgTx(2) breachInfo := &lnwallet.BreachRetribution{ - RevokedStateNum: stateNum, - BreachTransaction: breachTxn, + RevokedStateNum: stateNum, + BreachTxHash: breachTxn.TxHash(), KeyRing: &lnwallet.CommitmentKeyRing{ RevocationKey: revPK, ToLocalKey: toLocalPK, @@ -607,7 +607,7 @@ func testBackupTask(t *testing.T, test backupTaskTest) { } // Verify that the breach hint matches the breach txid's prefix. - breachTxID := test.breachInfo.BreachTransaction.TxHash() + breachTxID := test.breachInfo.BreachTxHash expHint := blob.NewBreachHintFromHash(&breachTxID) if hint != expHint { t.Fatalf("breach hint mismatch, want: %x, got: %v", diff --git a/watchtower/wtclient/client_test.go b/watchtower/wtclient/client_test.go index 5f091223e..fcd256cd1 100644 --- a/watchtower/wtclient/client_test.go +++ b/watchtower/wtclient/client_test.go @@ -10,6 +10,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" @@ -300,7 +301,7 @@ func (c *mockChannel) createRemoteCommitTx(t *testing.T) { } retribution := &lnwallet.BreachRetribution{ - BreachTransaction: commitTxn, + BreachTxHash: commitTxn.TxHash(), RevokedStateNum: c.commitHeight, KeyRing: commitKeyRing, RemoteDelay: c.csvDelay, @@ -360,13 +361,15 @@ func (c *mockChannel) receivePayment(t *testing.T, amt lnwire.MilliSatoshi) { } // getState retrieves the channel's commitment and retribution at state i. -func (c *mockChannel) getState(i uint64) (*wire.MsgTx, *lnwallet.BreachRetribution) { +func (c *mockChannel) getState( + i uint64) (chainhash.Hash, *lnwallet.BreachRetribution) { + c.mu.Lock() defer c.mu.Unlock() retribution := c.retributions[i] - return retribution.BreachTransaction, retribution + return retribution.BreachTxHash, retribution } type testHarness struct { @@ -608,8 +611,7 @@ func (h *testHarness) advanceChannelN(id uint64, n int) []blob.BreachHint { var hints []blob.BreachHint for i := uint64(0); i < uint64(n); i++ { channel.advanceState(h.t) - commitTx, _ := h.channel(id).getState(i) - breachTxID := commitTx.TxHash() + breachTxID, _ := h.channel(id).getState(i) hints = append(hints, blob.NewBreachHintFromHash(&breachTxID)) } @@ -654,8 +656,7 @@ func (h *testHarness) sendPayments(id, from, to uint64, var hints []blob.BreachHint for i := from; i < to; i++ { h.channel(id).sendPayment(h.t, amt) - commitTx, _ := channel.getState(i) - breachTxID := commitTx.TxHash() + breachTxID, _ := channel.getState(i) hints = append(hints, blob.NewBreachHintFromHash(&breachTxID)) } @@ -675,8 +676,7 @@ func (h *testHarness) recvPayments(id, from, to uint64, var hints []blob.BreachHint for i := from; i < to; i++ { channel.receivePayment(h.t, amt) - commitTx, _ := channel.getState(i) - breachTxID := commitTx.TxHash() + breachTxID, _ := channel.getState(i) hints = append(hints, blob.NewBreachHintFromHash(&breachTxID)) } From 22d98ca6d530e29d75fa3074cefc87d0802a24d0 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Wed, 13 Apr 2022 22:33:07 +0800 Subject: [PATCH 19/22] multi: use new revocation log when creating breach retribution This commit changes the `NewBreachRetribution` to use the new revocation log format, while maintaining the compatibilty to use an older revocation log format. Unit tests have been added to make sure a breach retribution can be created in both log formats. This also means the watch tower needs to pass the relevant commit tx at its backup height when creating the breach retribution during backing up. This is achieved by recording the current remote commitment state before advancing the remote commitment chain. --- contractcourt/breacharbiter_test.go | 4 +- contractcourt/chain_watcher.go | 1 + htlcswitch/link.go | 16 +- lnwallet/channel.go | 410 ++++++++++++++++++++-------- lnwallet/channel_test.go | 389 +++++++++++++++++++++++++- 5 files changed, 693 insertions(+), 127 deletions(-) diff --git a/contractcourt/breacharbiter_test.go b/contractcourt/breacharbiter_test.go index 4c8863ded..63161d6e5 100644 --- a/contractcourt/breacharbiter_test.go +++ b/contractcourt/breacharbiter_test.go @@ -1621,7 +1621,7 @@ func testBreachSpends(t *testing.T, test breachTest) { // Notify the breach arbiter about the breach. retribution, err := lnwallet.NewBreachRetribution( - alice.State(), height, 1, + alice.State(), height, 1, forceCloseTx, ) if err != nil { t.Fatalf("unable to create breach retribution: %v", err) @@ -1837,7 +1837,7 @@ func TestBreachDelayedJusticeConfirmation(t *testing.T) { // Notify the breach arbiter about the breach. retribution, err := lnwallet.NewBreachRetribution( - alice.State(), height, uint32(blockHeight), + alice.State(), height, uint32(blockHeight), forceCloseTx, ) if err != nil { t.Fatalf("unable to create breach retribution: %v", err) diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go index f9d919023..6ba4c2795 100644 --- a/contractcourt/chain_watcher.go +++ b/contractcourt/chain_watcher.go @@ -794,6 +794,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail, spendHeight := uint32(commitSpend.SpendingHeight) retribution, err := lnwallet.NewBreachRetribution( c.cfg.chanState, broadcastStateNum, spendHeight, + commitSpend.SpendingTx, ) switch { diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 9f2cfc494..f8623eadb 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -1897,9 +1897,16 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { // We've received a revocation from the remote chain, if valid, // this moves the remote chain forward, and expands our // revocation window. - fwdPkg, adds, settleFails, remoteHTLCs, err := l.channel.ReceiveRevocation( - msg, - ) + // + // Before advancing our remote chain, we will record the + // current commit tx, which is used by the TowerClient to + // create backups. + oldCommitTx := l.channel.State().RemoteCommitment.CommitTx + + // We now process the message and advance our remote commit + // chain. + fwdPkg, adds, settleFails, remoteHTLCs, err := l.channel. + ReceiveRevocation(msg) if err != nil { // TODO(halseth): force close? l.fail(LinkFailureError{code: ErrInvalidRevocation}, @@ -1928,10 +1935,13 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { } // If we have a tower client for this channel type, we'll + // create a backup for the current state. if l.cfg.TowerClient != nil { state := l.channel.State() breachInfo, err := lnwallet.NewBreachRetribution( state, state.RemoteCommitment.CommitHeight-1, 0, + // OldCommitTx is the breaching tx at height-1. + oldCommitTx, ) if err != nil { l.fail(LinkFailureError{code: ErrInternalError}, diff --git a/lnwallet/channel.go b/lnwallet/channel.go index b151b9efa..d4b63ddcc 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -96,6 +96,17 @@ var ( // both parties can retrieve their funds. ErrCommitSyncRemoteDataLoss = fmt.Errorf("possible remote commitment " + "state data loss") + + // ErrNoRevocationLogFound is returned when both the returned logs are + // nil from querying the revocation log bucket. In theory this should + // never happen as the query will return `ErrLogEntryNotFound`, yet + // we'd still perform a sanity check to make sure at least one of the + // logs is non-nil. + ErrNoRevocationLogFound = errors.New("no revocation log found") + + // ErrOutputIndexOutOfRange is returned when an output index is greater + // than or equal to the length of a given transaction's outputs. + ErrOutputIndexOutOfRange = errors.New("output index is out of range") ) // ErrCommitSyncLocalDataLoss is returned in the case that we receive a valid @@ -2278,16 +2289,22 @@ type BreachRetribution struct { // passed channel, at a particular revoked state number, and one which targets // the passed commitment transaction. func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, - breachHeight uint32) (*BreachRetribution, error) { + breachHeight uint32, spendTx *wire.MsgTx) (*BreachRetribution, error) { // Query the on-disk revocation log for the snapshot which was recorded - // at this particular state num. - _, revokedSnapshot, err := chanState.FindPreviousState(stateNum) + // at this particular state num. Based on whether a legacy revocation + // log is returned or not, we will process them differently. + revokedLog, revokedLogLegacy, err := chanState.FindPreviousState( + stateNum, + ) if err != nil { return nil, err } - commitHash := revokedSnapshot.CommitTx.TxHash() + // Sanity check that at least one of the logs is returned. + if revokedLog == nil && revokedLogLegacy == nil { + return nil, ErrNoRevocationLogFound + } // With the state number broadcast known, we can now derive/restore the // proper revocation preimage necessary to sweep the remote party's @@ -2310,22 +2327,14 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, // Next, reconstruct the scripts as they were present at this state // number so we can have the proper witness script to sign and include // within the final witness. - theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay) - isRemoteInitiator := !chanState.IsInitiator var leaseExpiry uint32 if chanState.ChanType.HasLeaseExpiration() { leaseExpiry = chanState.ThawHeight } - theirScript, err := CommitScriptToSelf( - chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, - keyRing.RevocationKey, theirDelay, leaseExpiry, - ) - if err != nil { - return nil, err - } - // Since it is the remote breach we are reconstructing, the output going - // to us will be a to-remote script with our local params. + // Since it is the remote breach we are reconstructing, the output + // going to us will be a to-remote script with our local params. + isRemoteInitiator := !chanState.IsInitiator ourScript, ourDelay, err := CommitScriptToRemote( chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, leaseExpiry, @@ -2334,15 +2343,248 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, return nil, err } - // In order to fully populate the breach retribution struct, we'll need - // to find the exact index of the commitment outputs. + theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay) + theirScript, err := CommitScriptToSelf( + chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, + keyRing.RevocationKey, theirDelay, leaseExpiry, + ) + if err != nil { + return nil, err + } + + // Define an empty breach retribution that will be overwritten based on + // different version of the revocation log found. + var br *BreachRetribution + + // Define our and their amounts, that will be overwritten below. + var ourAmt, theirAmt int64 + + // If the returned *RevocationLog is non-nil, use it to derive the info + // we need. + if revokedLog != nil { + br, ourAmt, theirAmt, err = createBreachRetribution( + revokedLog, spendTx, chanState, keyRing, + commitmentSecret, leaseExpiry, + ) + if err != nil { + return nil, err + } + } else { + // The returned revocation log is in legacy format, which is a + // *ChannelCommitment. + // + // NOTE: this branch is kept for compatibility such that for + // old nodes which refuse to migrate the legacy revocation log + // data can still function. This branch can be deleted once we + // are confident that no legacy format is in use. + br, ourAmt, theirAmt, err = createBreachRetributionLegacy( + revokedLogLegacy, chanState, keyRing, commitmentSecret, + ourScript, theirScript, leaseExpiry, + ) + if err != nil { + return nil, err + } + } + + // Conditionally instantiate a sign descriptor for each of the + // commitment outputs. If either is considered dust using the remote + // party's dust limit, the respective sign descriptor will be nil. + // + // If our balance exceeds the remote party's dust limit, instantiate + // the sign descriptor for our output. + if ourAmt >= int64(chanState.RemoteChanCfg.DustLimit) { + br.LocalOutputSignDesc = &input.SignDescriptor{ + SingleTweak: keyRing.LocalCommitKeyTweak, + KeyDesc: chanState.LocalChanCfg.PaymentBasePoint, + WitnessScript: ourScript.WitnessScript, + Output: &wire.TxOut{ + PkScript: ourScript.PkScript, + Value: ourAmt, + }, + HashType: txscript.SigHashAll, + } + } + + // Similarly, if their balance exceeds the remote party's dust limit, + // assemble the sign descriptor for their output, which we can sweep. + if theirAmt >= int64(chanState.RemoteChanCfg.DustLimit) { + br.RemoteOutputSignDesc = &input.SignDescriptor{ + KeyDesc: chanState.LocalChanCfg. + RevocationBasePoint, + DoubleTweak: commitmentSecret, + WitnessScript: theirScript.WitnessScript, + Output: &wire.TxOut{ + PkScript: theirScript.PkScript, + Value: theirAmt, + }, + HashType: txscript.SigHashAll, + } + } + + // Finally, with all the necessary data constructed, we can pad the + // BreachRetribution struct which houses all the data necessary to + // swiftly bring justice to the cheating remote party. + br.BreachHeight = breachHeight + br.RevokedStateNum = stateNum + br.LocalDelay = ourDelay + br.RemoteDelay = theirDelay + + return br, nil +} + +// createHtlcRetribution is a helper function to construct an HtlcRetribution +// based on the passed params. +func createHtlcRetribution(chanState *channeldb.OpenChannel, + keyRing *CommitmentKeyRing, commitHash chainhash.Hash, + commitmentSecret *btcec.PrivateKey, leaseExpiry uint32, + htlc *channeldb.HTLCEntry) (HtlcRetribution, error) { + + var emptyRetribution HtlcRetribution + + theirDelay := uint32(chanState.RemoteChanCfg.CsvDelay) + isRemoteInitiator := !chanState.IsInitiator + + // We'll generate the original second level witness script now, as + // we'll need it if we're revoking an HTLC output on the remote + // commitment transaction, and *they* go to the second level. + secondLevelScript, err := SecondLevelHtlcScript( + chanState.ChanType, isRemoteInitiator, + keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay, + leaseExpiry, + ) + if err != nil { + return emptyRetribution, err + } + + // If this is an incoming HTLC, then this means that they were the + // sender of the HTLC (relative to us). So we'll re-generate the sender + // HTLC script. Otherwise, is this was an outgoing HTLC that we sent, + // then from the PoV of the remote commitment state, they're the + // receiver of this HTLC. + htlcPkScript, htlcWitnessScript, err := genHtlcScript( + chanState.ChanType, htlc.Incoming, false, + htlc.RefundTimeout, htlc.RHash, keyRing, + ) + if err != nil { + return emptyRetribution, err + } + + return HtlcRetribution{ + SignDesc: input.SignDescriptor{ + KeyDesc: chanState.LocalChanCfg. + RevocationBasePoint, + DoubleTweak: commitmentSecret, + WitnessScript: htlcWitnessScript, + Output: &wire.TxOut{ + PkScript: htlcPkScript, + Value: int64(htlc.Amt), + }, + HashType: txscript.SigHashAll, + }, + OutPoint: wire.OutPoint{ + Hash: commitHash, + Index: uint32(htlc.OutputIndex), + }, + SecondLevelWitnessScript: secondLevelScript.WitnessScript, + IsIncoming: htlc.Incoming, + }, nil +} + +// createBreachRetribution creates a partially initiated BreachRetribution +// using a RevocationLog. Returns the constructed retribution, our amount, +// their amount, and a possible non-nil error. +func createBreachRetribution(revokedLog *channeldb.RevocationLog, + spendTx *wire.MsgTx, chanState *channeldb.OpenChannel, + keyRing *CommitmentKeyRing, commitmentSecret *btcec.PrivateKey, + leaseExpiry uint32) (*BreachRetribution, int64, int64, error) { + + commitHash := revokedLog.CommitTxHash + + // Create the htlc retributions. + htlcRetributions := make([]HtlcRetribution, len(revokedLog.HTLCEntries)) + for i, htlc := range revokedLog.HTLCEntries { + hr, err := createHtlcRetribution( + chanState, keyRing, commitHash, + commitmentSecret, leaseExpiry, htlc, + ) + if err != nil { + return nil, 0, 0, err + } + htlcRetributions[i] = hr + } + + var ourAmt, theirAmt int64 + + // Construct the our outpoint. + ourOutpoint := wire.OutPoint{ + Hash: commitHash, + } + if revokedLog.OurOutputIndex != channeldb.OutputIndexEmpty { + ourOutpoint.Index = uint32(revokedLog.OurOutputIndex) + + // Sanity check that OurOutputIndex is within range. + if int(ourOutpoint.Index) >= len(spendTx.TxOut) { + return nil, 0, 0, fmt.Errorf("%w: ours=%v, "+ + "len(TxOut)=%v", ErrOutputIndexOutOfRange, + ourOutpoint.Index, len(spendTx.TxOut), + ) + } + // Read the amounts from the breach transaction. + // + // NOTE: ourAmt here includes commit fee and anchor amount(if + // enabled). + ourAmt = spendTx.TxOut[ourOutpoint.Index].Value + } + + // Construct the their outpoint. + theirOutpoint := wire.OutPoint{ + Hash: commitHash, + } + if revokedLog.TheirOutputIndex != channeldb.OutputIndexEmpty { + theirOutpoint.Index = uint32(revokedLog.TheirOutputIndex) + + // Sanity check that TheirOutputIndex is within range. + if int(revokedLog.TheirOutputIndex) >= len(spendTx.TxOut) { + return nil, 0, 0, fmt.Errorf("%w: theirs=%v, "+ + "len(TxOut)=%v", ErrOutputIndexOutOfRange, + revokedLog.TheirOutputIndex, len(spendTx.TxOut), + ) + } + + // Read the amounts from the breach transaction. + theirAmt = spendTx.TxOut[theirOutpoint.Index].Value + } + + return &BreachRetribution{ + BreachTxHash: commitHash, + ChainHash: chanState.ChainHash, + LocalOutpoint: ourOutpoint, + RemoteOutpoint: theirOutpoint, + HtlcRetributions: htlcRetributions, + KeyRing: keyRing, + }, ourAmt, theirAmt, nil +} + +// createBreachRetributionLegacy creates a partially initiated +// BreachRetribution using a ChannelCommitment. Returns the constructed +// retribution, our amount, their amount, and a possible non-nil error. +func createBreachRetributionLegacy(revokedLog *channeldb.ChannelCommitment, + chanState *channeldb.OpenChannel, keyRing *CommitmentKeyRing, + commitmentSecret *btcec.PrivateKey, + ourScript, theirScript *ScriptInfo, + leaseExpiry uint32) (*BreachRetribution, int64, int64, error) { + + commitHash := revokedLog.CommitTx.TxHash() ourOutpoint := wire.OutPoint{ Hash: commitHash, } theirOutpoint := wire.OutPoint{ Hash: commitHash, } - for i, txOut := range revokedSnapshot.CommitTx.TxOut { + + // In order to fully populate the breach retribution struct, we'll need + // to find the exact index of the commitment outputs. + for i, txOut := range revokedLog.CommitTx.TxOut { switch { case bytes.Equal(txOut.PkScript, ourScript.PkScript): ourOutpoint.Index = uint32(i) @@ -2351,126 +2593,52 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64, } } - // Conditionally instantiate a sign descriptor for each of the - // commitment outputs. If either is considered dust using the remote - // party's dust limit, the respective sign descriptor will be nil. - var ( - ourSignDesc *input.SignDescriptor - theirSignDesc *input.SignDescriptor - ) - - // Compute the balances in satoshis. - ourAmt := revokedSnapshot.LocalBalance.ToSatoshis() - theirAmt := revokedSnapshot.RemoteBalance.ToSatoshis() - - // If our balance exceeds the remote party's dust limit, instantiate - // the sign descriptor for our output. - if ourAmt >= chanState.RemoteChanCfg.DustLimit { - ourSignDesc = &input.SignDescriptor{ - SingleTweak: keyRing.LocalCommitKeyTweak, - KeyDesc: chanState.LocalChanCfg.PaymentBasePoint, - WitnessScript: ourScript.WitnessScript, - Output: &wire.TxOut{ - PkScript: ourScript.PkScript, - Value: int64(ourAmt), - }, - HashType: txscript.SigHashAll, - } - } - - // Similarly, if their balance exceeds the remote party's dust limit, - // assemble the sign descriptor for their output, which we can sweep. - if theirAmt >= chanState.RemoteChanCfg.DustLimit { - theirSignDesc = &input.SignDescriptor{ - KeyDesc: chanState.LocalChanCfg.RevocationBasePoint, - DoubleTweak: commitmentSecret, - WitnessScript: theirScript.WitnessScript, - Output: &wire.TxOut{ - PkScript: theirScript.PkScript, - Value: int64(theirAmt), - }, - HashType: txscript.SigHashAll, - } - } - // With the commitment outputs located, we'll now generate all the // retribution structs for each of the HTLC transactions active on the // remote commitment transaction. - htlcRetributions := make([]HtlcRetribution, 0, len(revokedSnapshot.Htlcs)) - for _, htlc := range revokedSnapshot.Htlcs { + htlcRetributions := make([]HtlcRetribution, len(revokedLog.Htlcs)) + for i, htlc := range revokedLog.Htlcs { // If the HTLC is dust, then we'll skip it as it doesn't have // an output on the commitment transaction. if HtlcIsDust( chanState.ChanType, htlc.Incoming, false, - chainfee.SatPerKWeight(revokedSnapshot.FeePerKw), - htlc.Amt.ToSatoshis(), chanState.RemoteChanCfg.DustLimit, + chainfee.SatPerKWeight(revokedLog.FeePerKw), + htlc.Amt.ToSatoshis(), + chanState.RemoteChanCfg.DustLimit, ) { + continue } - // We'll generate the original second level witness script now, - // as we'll need it if we're revoking an HTLC output on the - // remote commitment transaction, and *they* go to the second - // level. - secondLevelScript, err := SecondLevelHtlcScript( - chanState.ChanType, isRemoteInitiator, - keyRing.RevocationKey, keyRing.ToLocalKey, theirDelay, - leaseExpiry, + entry := &channeldb.HTLCEntry{ + RHash: htlc.RHash, + RefundTimeout: htlc.RefundTimeout, + OutputIndex: uint16(htlc.OutputIndex), + Incoming: htlc.Incoming, + Amt: htlc.Amt.ToSatoshis(), + } + hr, err := createHtlcRetribution( + chanState, keyRing, commitHash, + commitmentSecret, leaseExpiry, entry, ) if err != nil { - return nil, err + return nil, 0, 0, err } - - // If this is an incoming HTLC, then this means that they were - // the sender of the HTLC (relative to us). So we'll - // re-generate the sender HTLC script. Otherwise, is this was - // an outgoing HTLC that we sent, then from the PoV of the - // remote commitment state, they're the receiver of this HTLC. - htlcPkScript, htlcWitnessScript, err := genHtlcScript( - chanState.ChanType, htlc.Incoming, false, - htlc.RefundTimeout, htlc.RHash, keyRing, - ) - if err != nil { - return nil, err - } - - htlcRetributions = append(htlcRetributions, HtlcRetribution{ - SignDesc: input.SignDescriptor{ - KeyDesc: chanState.LocalChanCfg.RevocationBasePoint, - DoubleTweak: commitmentSecret, - WitnessScript: htlcWitnessScript, - Output: &wire.TxOut{ - PkScript: htlcPkScript, - Value: int64(htlc.Amt.ToSatoshis()), - }, - HashType: txscript.SigHashAll, - }, - OutPoint: wire.OutPoint{ - Hash: commitHash, - Index: uint32(htlc.OutputIndex), - }, - SecondLevelWitnessScript: secondLevelScript.WitnessScript, - IsIncoming: htlc.Incoming, - }) + htlcRetributions[i] = hr } - // Finally, with all the necessary data constructed, we can create the - // BreachRetribution struct which houses all the data necessary to - // swiftly bring justice to the cheating remote party. + // Compute the balances in satoshis. + ourAmt := int64(revokedLog.LocalBalance.ToSatoshis()) + theirAmt := int64(revokedLog.RemoteBalance.ToSatoshis()) + return &BreachRetribution{ - BreachTxHash: commitHash, - ChainHash: chanState.ChainHash, - BreachHeight: breachHeight, - RevokedStateNum: stateNum, - LocalOutpoint: ourOutpoint, - LocalOutputSignDesc: ourSignDesc, - LocalDelay: ourDelay, - RemoteOutpoint: theirOutpoint, - RemoteOutputSignDesc: theirSignDesc, - RemoteDelay: theirDelay, - HtlcRetributions: htlcRetributions, - KeyRing: keyRing, - }, nil + BreachTxHash: commitHash, + ChainHash: chanState.ChainHash, + LocalOutpoint: ourOutpoint, + RemoteOutpoint: theirOutpoint, + HtlcRetributions: htlcRetributions, + KeyRing: keyRing, + }, ourAmt, theirAmt, nil } // HtlcIsDust determines if an HTLC output is dust or not depending on two diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index 4962c61d6..812eb4b0e 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -14,6 +14,7 @@ import ( "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" @@ -7106,8 +7107,9 @@ func TestNewBreachRetributionSkipsDustHtlcs(t *testing.T) { // At this point, we'll now simulate a contract breach by Bob using the // NewBreachRetribution method. + breachTx := aliceChannel.channelState.RemoteCommitment.CommitTx breachRet, err := NewBreachRetribution( - aliceChannel.channelState, revokedStateNum, 100, + aliceChannel.channelState, revokedStateNum, 100, breachTx, ) if err != nil { t.Fatalf("unable to create breach retribution: %v", err) @@ -10249,3 +10251,388 @@ func testGetDustSum(t *testing.T, chantype channeldb.ChannelType) { checkDust(bobChannel, htlc2Amt+htlc3Amt, htlc2Amt+htlc3Amt) } } + +// deriveDummyRetributionParams is a helper function that derives a list of +// dummy params to assist retribution creation related tests. +func deriveDummyRetributionParams(chanState *channeldb.OpenChannel) (uint32, + *CommitmentKeyRing, chainhash.Hash) { + + config := chanState.RemoteChanCfg + commitHash := chanState.RemoteCommitment.CommitTx.TxHash() + keyRing := DeriveCommitmentKeys( + config.RevocationBasePoint.PubKey, false, chanState.ChanType, + &chanState.LocalChanCfg, &chanState.RemoteChanCfg, + ) + leaseExpiry := chanState.ThawHeight + return leaseExpiry, keyRing, commitHash +} + +// TestCreateHtlcRetribution checks that `createHtlcRetribution` behaves as +// epxected. +func TestCreateHtlcRetribution(t *testing.T) { + t.Parallel() + + // Create a dummy private key and an HTLC amount for testing. + dummyPrivate, _ := btcec.PrivKeyFromBytes([]byte{1}) + testAmt := btcutil.Amount(100) + + // Create a test channel. + aliceChannel, _, cleanUp, err := CreateTestChannels( + channeldb.ZeroHtlcTxFeeBit, + ) + require.NoError(t, err) + defer cleanUp() + + // Prepare the params needed to call the function. Note that the values + // here are not necessary "cryptography-correct", we just use them to + // construct the htlc retribution. + leaseExpiry, keyRing, commitHash := deriveDummyRetributionParams( + aliceChannel.channelState, + ) + htlc := &channeldb.HTLCEntry{ + Amt: testAmt, + Incoming: true, + OutputIndex: 1, + } + + // Create the htlc retribution. + hr, err := createHtlcRetribution( + aliceChannel.channelState, keyRing, commitHash, + dummyPrivate, leaseExpiry, htlc, + ) + // Expect no error. + require.NoError(t, err) + + // Check the fields have expected values. + require.EqualValues(t, testAmt, hr.SignDesc.Output.Value) + require.Equal(t, commitHash, hr.OutPoint.Hash) + require.EqualValues(t, htlc.OutputIndex, hr.OutPoint.Index) + require.Equal(t, htlc.Incoming, hr.IsIncoming) +} + +// TestCreateBreachRetribution checks that `createBreachRetribution` behaves as +// epxected. +func TestCreateBreachRetribution(t *testing.T) { + t.Parallel() + + // Create dummy values for the test. + dummyPrivate, _ := btcec.PrivKeyFromBytes([]byte{1}) + testAmt := int64(100) + ourAmt := int64(1000) + theirAmt := int64(2000) + localIndex := uint32(0) + remoteIndex := uint32(1) + htlcIndex := uint32(2) + + // Create a dummy breach tx, which has our output located at index 0 + // and theirs at 1. + spendTx := &wire.MsgTx{ + TxOut: []*wire.TxOut{ + {Value: ourAmt}, + {Value: theirAmt}, + {Value: testAmt}, + }, + } + + // Create a test channel. + aliceChannel, _, cleanUp, err := CreateTestChannels( + channeldb.ZeroHtlcTxFeeBit, + ) + require.NoError(t, err) + defer cleanUp() + + // Prepare the params needed to call the function. Note that the values + // here are not necessary "cryptography-correct", we just use them to + // construct the retribution. + leaseExpiry, keyRing, commitHash := deriveDummyRetributionParams( + aliceChannel.channelState, + ) + htlc := &channeldb.HTLCEntry{ + Amt: btcutil.Amount(testAmt), + Incoming: true, + OutputIndex: uint16(htlcIndex), + } + + // Create a dummy revocation log. + revokedLog := channeldb.RevocationLog{ + CommitTxHash: commitHash, + OurOutputIndex: uint16(localIndex), + TheirOutputIndex: uint16(remoteIndex), + HTLCEntries: []*channeldb.HTLCEntry{htlc}, + } + + // Create a log with an empty local output index. + revokedLogNoLocal := revokedLog + revokedLogNoLocal.OurOutputIndex = channeldb.OutputIndexEmpty + + // Create a log with an empty remote output index. + revokedLogNoRemote := revokedLog + revokedLogNoRemote.TheirOutputIndex = channeldb.OutputIndexEmpty + + testCases := []struct { + name string + revocationLog *channeldb.RevocationLog + expectedErr error + expectedOurAmt int64 + expectedTheirAmt int64 + }{ + { + name: "create retribution successfully", + revocationLog: &revokedLog, + expectedErr: nil, + expectedOurAmt: ourAmt, + expectedTheirAmt: theirAmt, + }, + { + name: "fail due to our index too big", + revocationLog: &channeldb.RevocationLog{ + OurOutputIndex: uint16(htlcIndex + 1), + }, + expectedErr: ErrOutputIndexOutOfRange, + }, + { + name: "fail due to their index too big", + revocationLog: &channeldb.RevocationLog{ + TheirOutputIndex: uint16(htlcIndex + 1), + }, + expectedErr: ErrOutputIndexOutOfRange, + }, + { + name: "empty local output index", + revocationLog: &revokedLogNoLocal, + expectedErr: nil, + expectedOurAmt: 0, + expectedTheirAmt: theirAmt, + }, + { + name: "empty remote output index", + revocationLog: &revokedLogNoRemote, + expectedErr: nil, + expectedOurAmt: ourAmt, + expectedTheirAmt: 0, + }, + } + + // assertRetribution is a helper closure that checks a given breach + // retribution has the expected values on certain fields. + assertRetribution := func(br *BreachRetribution, our, their int64) { + chainHash := aliceChannel.channelState.ChainHash + require.Equal(t, commitHash, br.BreachTxHash) + require.Equal(t, chainHash, br.ChainHash) + + // Construct local outpoint, we only have the index when the + // amount is not zero. + local := wire.OutPoint{ + Hash: commitHash, + } + if our != 0 { + local.Index = localIndex + } + + // Construct remote outpoint, we only have the index when the + // amount is not zero. + remote := wire.OutPoint{ + Hash: commitHash, + } + if their != 0 { + remote.Index = remoteIndex + } + + require.Equal(t, local, br.LocalOutpoint) + require.Equal(t, remote, br.RemoteOutpoint) + + for _, hr := range br.HtlcRetributions { + require.EqualValues(t, testAmt, + hr.SignDesc.Output.Value) + require.Equal(t, commitHash, hr.OutPoint.Hash) + require.EqualValues(t, htlcIndex, hr.OutPoint.Index) + require.Equal(t, htlc.Incoming, hr.IsIncoming) + } + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + br, our, their, err := createBreachRetribution( + tc.revocationLog, spendTx, + aliceChannel.channelState, keyRing, + dummyPrivate, leaseExpiry, + ) + + // Check the error if expected. + if tc.expectedErr != nil { + require.ErrorIs(t, err, tc.expectedErr) + } else { + // Otherwise we expect no error. + require.NoError(t, err) + + // Check the amounts and the contructed partial + // retribution are returned as expected. + require.Equal(t, tc.expectedOurAmt, our) + require.Equal(t, tc.expectedTheirAmt, their) + assertRetribution(br, our, their) + } + }) + } +} + +// TestCreateBreachRetributionLegacy checks that +// `createBreachRetributionLegacy` behaves as expected. +func TestCreateBreachRetributionLegacy(t *testing.T) { + t.Parallel() + + // Create dummy values for the test. + dummyPrivate, _ := btcec.PrivKeyFromBytes([]byte{1}) + + // Create a test channel. + aliceChannel, _, cleanUp, err := CreateTestChannels( + channeldb.ZeroHtlcTxFeeBit, + ) + require.NoError(t, err) + defer cleanUp() + + // Prepare the params needed to call the function. Note that the values + // here are not necessary "cryptography-correct", we just use them to + // construct the retribution. + leaseExpiry, keyRing, _ := deriveDummyRetributionParams( + aliceChannel.channelState, + ) + + // Use the remote commitment as our revocation log. + revokedLog := aliceChannel.channelState.RemoteCommitment + + ourOp := revokedLog.CommitTx.TxOut[0] + theirOp := revokedLog.CommitTx.TxOut[1] + + // Create the dummy scripts. + ourScript := &ScriptInfo{ + PkScript: ourOp.PkScript, + } + theirScript := &ScriptInfo{ + PkScript: theirOp.PkScript, + } + + // Create the breach retribution using the legacy format. + br, ourAmt, theirAmt, err := createBreachRetributionLegacy( + &revokedLog, aliceChannel.channelState, keyRing, + dummyPrivate, ourScript, theirScript, leaseExpiry, + ) + require.NoError(t, err) + + // Check the commitHash and chainHash. + commitHash := revokedLog.CommitTx.TxHash() + chainHash := aliceChannel.channelState.ChainHash + require.Equal(t, commitHash, br.BreachTxHash) + require.Equal(t, chainHash, br.ChainHash) + + // Check the outpoints. + local := wire.OutPoint{ + Hash: commitHash, + Index: 0, + } + remote := wire.OutPoint{ + Hash: commitHash, + Index: 1, + } + require.Equal(t, local, br.LocalOutpoint) + require.Equal(t, remote, br.RemoteOutpoint) + + // Validate the amounts, note that in the legacy format, our amount is + // not directly the amount found in the to local output. Rather, it's + // the local output value minus the commit fee and anchor value(if + // present). + require.EqualValues(t, revokedLog.LocalBalance.ToSatoshis(), ourAmt) + require.Equal(t, theirOp.Value, theirAmt) +} + +// TestNewBreachRetribution tests that the function `NewBreachRetribution` +// behaves as expected. +func TestNewBreachRetribution(t *testing.T) { + t.Run("non-anchor", func(t *testing.T) { + testNewBreachRetribution(t, channeldb.ZeroHtlcTxFeeBit) + }) + t.Run("anchor", func(t *testing.T) { + chanType := channeldb.SingleFunderTweaklessBit | + channeldb.AnchorOutputsBit + testNewBreachRetribution(t, chanType) + }) +} + +// testNewBreachRetribution takes a channel type and tests the function +// `NewBreachRetribution`. +func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) { + t.Parallel() + + aliceChannel, bobChannel, cleanUp, err := CreateTestChannels(chanType) + require.NoError(t, err) + defer cleanUp() + + breachHeight := uint32(101) + stateNum := uint64(0) + chainHash := aliceChannel.channelState.ChainHash + theirDelay := uint32(aliceChannel.channelState.RemoteChanCfg.CsvDelay) + breachTx := aliceChannel.channelState.RemoteCommitment.CommitTx + + // Create a breach retribution at height 0, which should give us an + // error as there are no past delta state saved as revocation logs yet. + _, err = NewBreachRetribution( + aliceChannel.channelState, stateNum, breachHeight, breachTx, + ) + require.ErrorIs(t, err, channeldb.ErrNoPastDeltas) + + // We now force a state transition which will give us a revocation log + // at height 0. + txid := aliceChannel.channelState.RemoteCommitment.CommitTx.TxHash() + err = ForceStateTransition(aliceChannel, bobChannel) + require.NoError(t, err) + + // assertRetribution is a helper closure that checks a given breach + // retribution has the expected values on certain fields. + assertRetribution := func(br *BreachRetribution, + localIndex, remoteIndex uint32) { + + require.Equal(t, txid, br.BreachTxHash) + require.Equal(t, chainHash, br.ChainHash) + require.Equal(t, breachHeight, br.BreachHeight) + require.Equal(t, stateNum, br.RevokedStateNum) + require.Equal(t, theirDelay, br.RemoteDelay) + + local := wire.OutPoint{ + Hash: txid, + Index: localIndex, + } + remote := wire.OutPoint{ + Hash: txid, + Index: remoteIndex, + } + + if chanType.HasAnchors() { + // For anchor channels, we expect the local delay to be + // 1 otherwise 0. + require.EqualValues(t, 1, br.LocalDelay) + } else { + require.Zero(t, br.LocalDelay) + } + + require.Equal(t, local, br.LocalOutpoint) + require.Equal(t, remote, br.RemoteOutpoint) + } + + // Create the retribution again and we should expect it to be created + // successfully. + br, err := NewBreachRetribution( + aliceChannel.channelState, stateNum, breachHeight, breachTx, + ) + require.NoError(t, err) + + // Check the retribution is as expected. + t.Log(spew.Sdump(breachTx)) + assertRetribution(br, 1, 0) + + // Create the retribution using a stateNum+1 and we should expect an + // error. + _, err = NewBreachRetribution( + aliceChannel.channelState, stateNum+1, breachHeight, breachTx, + ) + require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound) +} From 7c5daaf7c2510432dd6f639ccd6bb2b27fc7e53a Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Apr 2022 07:48:32 +0800 Subject: [PATCH 20/22] mod: update tlv's package version --- go.mod | 4 ++-- go.sum | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index b5a8ea45f..76463214d 100644 --- a/go.mod +++ b/go.mod @@ -42,12 +42,12 @@ require ( github.com/lightningnetwork/lnd/kvdb v1.3.1 github.com/lightningnetwork/lnd/queue v1.1.0 github.com/lightningnetwork/lnd/ticker v1.1.0 - github.com/lightningnetwork/lnd/tlv v1.0.2 + github.com/lightningnetwork/lnd/tlv v1.0.3 github.com/lightningnetwork/lnd/tor v1.0.0 github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 github.com/miekg/dns v1.1.43 github.com/prometheus/client_golang v1.11.0 - github.com/stretchr/testify v1.7.0 + github.com/stretchr/testify v1.7.1 github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 github.com/urfave/cli v1.22.4 gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect diff --git a/go.sum b/go.sum index c61a46f1a..f79db5f93 100644 --- a/go.sum +++ b/go.sum @@ -487,8 +487,9 @@ github.com/lightningnetwork/lnd/queue v1.1.0/go.mod h1:YTkTVZCxz8tAYreH27EO3s857 github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0= github.com/lightningnetwork/lnd/ticker v1.1.0 h1:ShoBiRP3pIxZHaETndfQ5kEe+S4NdAY1hiX7YbZ4QE4= github.com/lightningnetwork/lnd/ticker v1.1.0/go.mod h1:ubqbSVCn6RlE0LazXuBr7/Zi6QT0uQo++OgIRBxQUrk= -github.com/lightningnetwork/lnd/tlv v1.0.2 h1:LG7H3Uw/mHYGnEeHRPg+STavAH+UsFvuBflD0PzcYFQ= github.com/lightningnetwork/lnd/tlv v1.0.2/go.mod h1:fICAfsqk1IOsC1J7G9IdsWX1EqWRMqEDCNxZJSKr9C4= +github.com/lightningnetwork/lnd/tlv v1.0.3 h1:0xBZcPuXagP6f7TY/RnLNR4igE21ov6qUdTr5NyvhhI= +github.com/lightningnetwork/lnd/tlv v1.0.3/go.mod h1:dzR/aZetBri+ZY/fHbwV06fNn/3UID6htQzbHfREFdo= github.com/lightningnetwork/lnd/tor v1.0.0 h1:wvEc7I+Y7IOtPglVP3cVBbYhiVhc7uTd7cMF9gQRzwA= github.com/lightningnetwork/lnd/tor v1.0.0/go.mod h1:RDtaAdwfAm+ONuPYwUhNIH1RAvKPv+75lHPOegUcz64= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= @@ -637,8 +638,9 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= From b288284566e5eda2be2cdec81738754fa8c1a673 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Apr 2022 07:50:04 +0800 Subject: [PATCH 21/22] channeldb: use BigSize to encode htlc amount This commit uses bigsize record to encode the htlc amount, which could save us 3 more bytes if the encoded value is no greater than roughly 0.043 bitcoin. The uint test has been updated with a more realistic values to reflect the actual gain. --- channeldb/revocation_log.go | 60 +++++++++++++++++++++- channeldb/revocation_log_test.go | 87 +++++++++++++++++++++++++++----- 2 files changed, 131 insertions(+), 16 deletions(-) diff --git a/channeldb/revocation_log.go b/channeldb/revocation_log.go index 61a6521e0..64e22d069 100644 --- a/channeldb/revocation_log.go +++ b/channeldb/revocation_log.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightningnetwork/lnd/kvdb" + "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/tlv" ) @@ -43,6 +44,16 @@ var ( // HTLCEntry specifies the minimal info needed to be stored on disk for ALL the // historical HTLCs, which is useful for constructing RevocationLog when a // breach is detected. +// The actual size of each HTLCEntry varies based on its RHash and Amt(sat), +// summarized as follows, +// +// | RHash empty | Amt<=252 | Amt<=65,535 | Amt<=4,294,967,295 | otherwise | +// |:-----------:|:--------:|:-----------:|:------------------:|:---------:| +// | true | 19 | 21 | 23 | 26 | +// | false | 51 | 53 | 55 | 58 | +// +// So the size varies from 19 bytes to 58 bytes, where most likely to be 23 or +// 55 bytes. // // NOTE: all the fields saved to disk use the primitive go types so they can be // made into tlv records without further conversion. @@ -86,6 +97,46 @@ type HTLCEntry struct { incomingTlv uint8 } +// RHashLen is used by MakeDynamicRecord to return the size of the RHash. +// +// NOTE: for zero hash, we return a length 0. +func (h *HTLCEntry) RHashLen() uint64 { + if h.RHash == lntypes.ZeroHash { + return 0 + } + return 32 +} + +// RHashEncoder is the customized encoder which skips encoding the empty hash. +func RHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error { + v, ok := val.(*[32]byte) + if !ok { + return tlv.NewTypeForEncodingErr(val, "RHash") + } + + // If the value is an empty hash, we will skip encoding it. + if *v == lntypes.ZeroHash { + return nil + } + + return tlv.EBytes32(w, v, buf) +} + +// RHashDecoder is the customized decoder which skips decoding the empty hash. +func RHashDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error { + v, ok := val.(*[32]byte) + if !ok { + return tlv.NewTypeForEncodingErr(val, "RHash") + } + + // If the length is zero, we will skip encoding the empty hash. + if l == 0 { + return nil + } + + return tlv.DBytes32(r, v, buf, 32) +} + // toTlvStream converts an HTLCEntry record into a tlv representation. func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { const ( @@ -103,7 +154,10 @@ func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { ) return tlv.NewStream( - tlv.MakePrimitiveRecord(rHashType, &h.RHash), + tlv.MakeDynamicRecord( + rHashType, &h.RHash, h.RHashLen, + RHashEncoder, RHashDecoder, + ), tlv.MakePrimitiveRecord( refundTimeoutType, &h.RefundTimeout, ), @@ -111,7 +165,9 @@ func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { outputIndexType, &h.OutputIndex, ), tlv.MakePrimitiveRecord(incomingType, &h.incomingTlv), - tlv.MakePrimitiveRecord(amtType, &h.amtTlv), + // We will save 3 bytes if the amount is less or equal to + // 4,294,967,295 msat, or roughly 0.043 bitcoin. + tlv.MakeBigSizeRecord(amtType, &h.amtTlv), ) } diff --git a/channeldb/revocation_log_test.go b/channeldb/revocation_log_test.go index 93ec784b2..1b17c9b28 100644 --- a/channeldb/revocation_log_test.go +++ b/channeldb/revocation_log_test.go @@ -34,30 +34,26 @@ var ( } testHTLCEntry = HTLCEntry{ - RefundTimeout: 100, + RefundTimeout: 740_000, OutputIndex: 10, Incoming: true, - Amt: 255, - amtTlv: 255, + Amt: 1000_000, + amtTlv: 1000_000, incomingTlv: 1, } testHTLCEntryBytes = []byte{ - // Body length 58. - 0x39, + // Body length 23. + 0x16, // Rhash tlv. - 0x0, 0x20, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, // RefundTimeout tlv. - 0x1, 0x4, 0x0, 0x0, 0x0, 0x64, + 0x1, 0x4, 0x0, 0xb, 0x4a, 0xa0, // OutputIndex tlv. 0x2, 0x2, 0x0, 0xa, // Incoming tlv. 0x3, 0x1, 0x1, // Amt tlv. - 0x4, 0x8, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xff, + 0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40, } testChannelCommit = ChannelCommitment{ @@ -161,7 +157,7 @@ func TestReadTLVStreamErr(t *testing.T) { require.Zero(t, valueRead) } -func TestSerializeHTLCEntries(t *testing.T) { +func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) { t.Parallel() // Copy the testHTLCEntry. @@ -181,6 +177,37 @@ func TestSerializeHTLCEntries(t *testing.T) { require.Equal(t, testHTLCEntryBytes, buf.Bytes()) } +func TestSerializeHTLCEntries(t *testing.T) { + t.Parallel() + + // Copy the testHTLCEntry. + entry := testHTLCEntry + + // Create a fake rHash. + rHashBytes := bytes.Repeat([]byte{10}, 32) + copy(entry.RHash[:], rHashBytes) + + // Construct the serialized bytes. + // + // Exclude the first 3 bytes, which are total length, RHash type and + // RHash length(0). + partialBytes := testHTLCEntryBytes[3:] + + // Write the total length and RHash tlv. + expectedBytes := []byte{0x36, 0x0, 0x20} + expectedBytes = append(expectedBytes, rHashBytes...) + + // Append the rest. + expectedBytes = append(expectedBytes, partialBytes...) + + buf := bytes.NewBuffer([]byte{}) + err := serializeHTLCEntries(buf, []*HTLCEntry{&entry}) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Equal(t, expectedBytes, buf.Bytes()) +} + func TestSerializeRevocationLog(t *testing.T) { t.Parallel() @@ -197,7 +224,7 @@ func TestSerializeRevocationLog(t *testing.T) { require.Equal(t, testRevocationLogBytes, buf.Bytes()[:bodyIndex]) } -func TestDerializeHTLCEntries(t *testing.T) { +func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) { t.Parallel() // Read the tlv stream. @@ -210,6 +237,38 @@ func TestDerializeHTLCEntries(t *testing.T) { require.Equal(t, &testHTLCEntry, htlcs[0]) } +func TestDerializeHTLCEntries(t *testing.T) { + t.Parallel() + + // Copy the testHTLCEntry. + entry := testHTLCEntry + + // Create a fake rHash. + rHashBytes := bytes.Repeat([]byte{10}, 32) + copy(entry.RHash[:], rHashBytes) + + // Construct the serialized bytes. + // + // Exclude the first 3 bytes, which are total length, RHash type and + // RHash length(0). + partialBytes := testHTLCEntryBytes[3:] + + // Write the total length and RHash tlv. + testBytes := append([]byte{0x36, 0x0, 0x20}, rHashBytes...) + + // Append the rest. + testBytes = append(testBytes, partialBytes...) + + // Read the tlv stream. + buf := bytes.NewBuffer(testBytes) + htlcs, err := deserializeHTLCEntries(buf) + require.NoError(t, err) + + // Check the bytes are read as expected. + require.Len(t, htlcs, 1) + require.Equal(t, &entry, htlcs[0]) +} + func TestDerializeRevocationLog(t *testing.T) { t.Parallel() From 591dbb180bfb7b6dfe68177b88fb91e3ab514b0e Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sat, 16 Apr 2022 09:50:33 +0800 Subject: [PATCH 22/22] docs: add release note for revocation log removal --- docs/release-notes/release-notes-0.15.0.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/release-notes/release-notes-0.15.0.md b/docs/release-notes/release-notes-0.15.0.md index eec475793..11a5a2484 100644 --- a/docs/release-notes/release-notes-0.15.0.md +++ b/docs/release-notes/release-notes-0.15.0.md @@ -227,6 +227,11 @@ then watch it on chain. Taproot script spends are also supported through the * Add [htlc expiry protection](https://github.com/lightningnetwork/lnd/pull/6212) to the htlc interceptor API. +* In order to safely advance commitment state, `lnd` saves the past states and + constructs a justice transaction in case of a remote breach. The states can + grow very large on disk given a busy operating channel, [which is now changed + with a space deduction over (at least) 96 percents.](https://github.com/lightningnetwork/lnd/pull/6347) + ## Documentation * Improved instructions on [how to build lnd for mobile](https://github.com/lightningnetwork/lnd/pull/6085).