htlcswitch: keep final htlc outcome

This commit is contained in:
Joost Jager committed 2022-10-27 16:42:36 +02:00
1 parent b85cda2a1d
commit 28256b7ea8
20 files changed
+466 -111

No files matched your search

+163 -9
View File
@@ -16,6 +16,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/walletdb"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
@@ -135,6 +136,26 @@ var (
// 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")
// finalHtlcsBucket contains the htlcs that have been resolved
// definitively. Within this bucket, there is a sub-bucket for each
// channel. In each channel bucket, the htlc indices are stored along
// with final outcome.
//
// final-htlcs -> chanID -> htlcIndex -> outcome
//
// 'outcome' is a byte value that encodes:
//
// | true false
// ------+------------------
// bit 0 | settled failed
// bit 1 | offchain onchain
//
// This bucket is positioned at the root level, because its contents
// will be kept independent of the channel lifecycle. This is to avoid
// the situation where a channel force-closes autonomously and the user
// not being able to query for htlc outcomes anymore.
finalHtlcsBucket = []byte("final-htlcs")
)
var (
@@ -618,6 +639,20 @@ func (c ChannelStatus) String() string {
return statusStr
}
// FinalHtlcByte defines a byte type that encodes information about the final
// htlc resolution.
type FinalHtlcByte byte
const (
// FinalHtlcSettledBit is the bit that encodes whether the htlc was
// settled or failed.
FinalHtlcSettledBit FinalHtlcByte = 1 << 0
// FinalHtlcOffchainBit is the bit that encodes whether the htlc was
// resolved offchain or onchain.
FinalHtlcOffchainBit FinalHtlcByte = 1 << 1
)
// OpenChannel encapsulates the persistent and dynamic state of an open channel
// with a remote node. An open channel supports several options for on-disk
// serialization depending on the exact context. Full (upon channel creation)
@@ -1043,6 +1078,26 @@ func fetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey,
return chanBucket, nil
}
func fetchFinalHtlcsBucketRw(tx kvdb.RwTx,
chanID lnwire.ShortChannelID) (kvdb.RwBucket, error) {
finalHtlcsBucket, err := tx.CreateTopLevelBucket(finalHtlcsBucket)
if err != nil {
return nil, err
}
var chanIDBytes [8]byte
byteOrder.PutUint64(chanIDBytes[:], chanID.ToUint64())
chanBucket, err := finalHtlcsBucket.CreateBucketIfNotExists(
chanIDBytes[:],
)
if err != nil {
return nil, err
}
return chanBucket, nil
}
// fullSync syncs the contents of an OpenChannel while re-using an existing
// database transaction.
func (c *OpenChannel) fullSync(tx kvdb.RwTx) error {
@@ -1739,8 +1794,12 @@ func syncNewChannel(tx kvdb.RwTx, c *OpenChannel, addrs []net.Addr) error {
// persisted to be able to produce a valid commit signature if a restart would
// occur. This method its to be called when we revoke our prior commitment
// state.
//
// A map is returned of all the htlc resolutions that were locked in in this
// commitment. Keys correspond to htlc indices and values indicate whether the
// htlc was settled or failed.
func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment,
unsignedAckedUpdates []LogUpdate) error {
unsignedAckedUpdates []LogUpdate) (map[uint64]bool, error) {
c.Lock()
defer c.Unlock()
@@ -1749,9 +1808,11 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment,
// state as all, as it's impossible to do so in a protocol compliant
// manner.
if c.hasChanStatus(ChanStatusRestored) {
return ErrNoRestoredChannelMutation
return nil, ErrNoRestoredChannelMutation
}
var finalHtlcs = make(map[uint64]bool)
err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error {
chanBucket, err := fetchChanBucketRw(
tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash,
@@ -1822,17 +1883,35 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment,
return err
}
var validUpdates []LogUpdate
// Get the bucket where settled htlcs are recorded.
finalHtlcsBucket, err := fetchFinalHtlcsBucketRw(
tx, c.ShortChannelID,
)
if err != nil {
return err
}
var unsignedUpdates []LogUpdate
for _, upd := range updates {
// Filter for updates that are not on our local
// commitment.
// Gather updates that are not on our local commitment.
if upd.LogIndex >= newCommitment.LocalLogIndex {
validUpdates = append(validUpdates, upd)
unsignedUpdates = append(unsignedUpdates, upd)
continue
}
// The update was locked in. If the update was a
// resolution, then store it in the database.
err := processFinalHtlc(
finalHtlcsBucket, upd, finalHtlcs,
)
if err != nil {
return err
}
}
var b3 bytes.Buffer
err = serializeLogUpdates(&b3, validUpdates)
err = serializeLogUpdates(&b3, unsignedUpdates)
if err != nil {
return fmt.Errorf("unable to serialize log updates: %v", err)
}
@@ -1843,12 +1922,57 @@ func (c *OpenChannel) UpdateCommitment(newCommitment *ChannelCommitment,
}
return nil
}, func() {})
}, func() {
finalHtlcs = make(map[uint64]bool)
})
if err != nil {
return nil, err
}
c.LocalCommitment = *newCommitment
return finalHtlcs, nil
}
// processFinalHtlc stores a final htlc outcome in the database if signaled via
// the supplied log update. An in-memory htlcs map is updated too.
func processFinalHtlc(finalHtlcsBucket walletdb.ReadWriteBucket, upd LogUpdate,
finalHtlcs map[uint64]bool) error {
var (
settled bool
id uint64
)
switch msg := upd.UpdateMsg.(type) {
case *lnwire.UpdateFulfillHTLC:
settled = true
id = msg.ID
case *lnwire.UpdateFailHTLC:
settled = false
id = msg.ID
case *lnwire.UpdateFailMalformedHTLC:
settled = false
id = msg.ID
default:
return nil
}
err := putFinalHtlc(
finalHtlcsBucket, id,
FinalHtlcInfo{
Settled: settled,
Offchain: true,
},
)
if err != nil {
return err
}
c.LocalCommitment = *newCommitment
finalHtlcs[id] = settled
return nil
}
@@ -2681,6 +2805,36 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg,
return nil
}
// FinalHtlcInfo contains information about the final outcome of an htlc.
type FinalHtlcInfo struct {
// Settled is true is the htlc was settled. If false, the htlc was
// failed.
Settled bool
// Offchain indicates whether the htlc was resolved off-chain or
// on-chain.
Offchain bool
}
// putFinalHtlc writes the final htlc outcome to the database. Additionally it
// records whether the htlc was resolved off-chain or on-chain.
func putFinalHtlc(finalHtlcsBucket kvdb.RwBucket, id uint64,
info FinalHtlcInfo) error {
var key [8]byte
byteOrder.PutUint64(key[:], id)
var finalHtlcByte FinalHtlcByte
if info.Settled {
finalHtlcByte |= FinalHtlcSettledBit
}
if info.Offchain {
finalHtlcByte |= FinalHtlcOffchainBit
}
return finalHtlcsBucket.Put(key[:], []byte{byte(finalHtlcByte)})
}
// NextLocalHtlcIndex returns the next unallocated local htlc index. To ensure
// this always returns the next index that has been not been allocated, this
// will first try to examine any pending commitments, before falling back to the
+39 -1
View File
@@ -641,7 +641,7 @@ func TestChannelStateTransition(t *testing.T) {
},
}
err = channel.UpdateCommitment(&commitment, unsignedAckedUpdates)
_, err = channel.UpdateCommitment(&commitment, unsignedAckedUpdates)
require.NoError(t, err, "unable to update commitment")
// Assert that update is correctly written to the database.
@@ -1457,3 +1457,41 @@ func TestKeyLocatorEncoding(t *testing.T) {
// version are equal.
require.Equal(t, keyLoc, decodedKeyLoc)
}
// TestFinalHtlcs tests final htlc storage and retrieval.
func TestFinalHtlcs(t *testing.T) {
t.Parallel()
fullDB, err := MakeTestDB(t)
require.NoError(t, err, "unable to make test database")
cdb := fullDB.ChannelStateDB()
chanID := lnwire.ShortChannelID{
BlockHeight: 1,
TxIndex: 2,
TxPosition: 3,
}
// Test offchain final htlcs.
const offchainHtlcID = 1
err = kvdb.Update(cdb.backend, func(tx kvdb.RwTx) error {
bucket, err := fetchFinalHtlcsBucketRw(
tx, chanID,
)
require.NoError(t, err)
return putFinalHtlc(bucket, offchainHtlcID, FinalHtlcInfo{
Settled: true,
Offchain: true,
})
}, func() {})
require.NoError(t, err)
// Test onchain final htlcs.
const onchainHtlcID = 2
err = cdb.PutOnchainFinalHtlcOutcome(chanID, onchainHtlcID, true)
require.NoError(t, err)
}
+21
View File
@@ -1652,6 +1652,27 @@ func (c *ChannelStateDB) FetchHistoricalChannel(outPoint *wire.OutPoint) (
return channel, nil
}
// PutOnchainFinalHtlcOutcome stores the final on-chain outcome of an htlc in
// the database.
func (c *ChannelStateDB) PutOnchainFinalHtlcOutcome(
chanID lnwire.ShortChannelID, htlcID uint64, settled bool) error {
return kvdb.Update(c.backend, func(tx kvdb.RwTx) error {
finalHtlcsBucket, err := fetchFinalHtlcsBucketRw(tx, chanID)
if err != nil {
return err
}
return putFinalHtlc(
finalHtlcsBucket, htlcID,
FinalHtlcInfo{
Settled: settled,
Offchain: false,
},
)
}, func() {})
}
// MakeTestDB creates a new instance of the ChannelDB for testing purposes.
// A callback which cleans up the created temporary directories is also
// returned and intended to be executed after the test completes.
+1 -1
View File
@@ -355,7 +355,7 @@ func TestRestoreChannelShells(t *testing.T) {
// Ensure that it isn't possible to modify the commitment state machine
// of this restored channel.
channel := nodeChans[0]
err = channel.UpdateCommitment(nil, nil)
_, err = channel.UpdateCommitment(nil, nil)
if err != ErrNoRestoredChannelMutation {
t.Fatalf("able to mutate restored channel")
}