Merge pull request #9030 from lightningnetwork/extract-part1-from-staging-branch

[custom channels 1/5]: extract PART1 from mega staging branch
This commit is contained in:
Oliver Gugger authored and GitHub committed 2024-08-28 07:06:53 -06:00
commit 5c3a8e949c
43 files changed
+2457 -785

No files matched your search

+5
View File
@@ -24,6 +24,7 @@ import (
"github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify" "github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
@@ -63,6 +64,10 @@ type Config struct {
// state. // state.
ChanStateDB *channeldb.ChannelStateDB ChanStateDB *channeldb.ChannelStateDB
// AuxLeafStore is an optional store that can be used to store auxiliary
// leaves for certain custom channel types.
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
// BlockCache is the main cache for storing block information. // BlockCache is the main cache for storing block information.
BlockCache *blockcache.BlockCache BlockCache *blockcache.BlockCache
+355 -103
View File
@@ -226,28 +226,109 @@ const (
// A tlv type definition used to serialize an outpoint's indexStatus // A tlv type definition used to serialize an outpoint's indexStatus
// for use in the outpoint index. // for use in the outpoint index.
indexStatusType tlv.Type = 0 indexStatusType tlv.Type = 0
// A tlv type definition used to serialize and deserialize a KeyLocator
// from the database.
keyLocType tlv.Type = 1
// A tlv type used to serialize and deserialize the
// `InitialLocalBalance` field.
initialLocalBalanceType tlv.Type = 2
// A tlv type used to serialize and deserialize the
// `InitialRemoteBalance` field.
initialRemoteBalanceType tlv.Type = 3
// A tlv type definition used to serialize and deserialize the
// confirmed ShortChannelID for a zero-conf channel.
realScidType tlv.Type = 4
// A tlv type definition used to serialize and deserialize the
// Memo for the channel channel.
channelMemoType tlv.Type = 5
) )
// openChannelTlvData houses the new data fields that are stored for each
// channel in a TLV stream within the root bucket. This is stored as a TLV
// stream appended to the existing hard-coded fields in the channel's root
// bucket. New fields being added to the channel state should be added here.
//
// NOTE: This struct is used for serialization purposes only and its fields
// should be accessed via the OpenChannel struct while in memory.
type openChannelTlvData struct {
// revokeKeyLoc is the key locator for the revocation key.
revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord]
// initialLocalBalance is the initial local balance of the channel.
initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64]
// initialRemoteBalance is the initial remote balance of the channel.
initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64]
// realScid is the real short channel ID of the channel corresponding to
// the on-chain outpoint.
realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID]
// memo is an optional text field that gives context to the user about
// the channel.
memo tlv.OptionalRecordT[tlv.TlvType5, []byte]
// tapscriptRoot is the optional Tapscript root the channel funding
// output commits to.
tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte]
// customBlob is an optional TLV encoded blob of data representing
// custom channel funding information.
customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob]
}
// encode serializes the openChannelTlvData to the given io.Writer.
func (c *openChannelTlvData) encode(w io.Writer) error {
tlvRecords := []tlv.Record{
c.revokeKeyLoc.Record(),
c.initialLocalBalance.Record(),
c.initialRemoteBalance.Record(),
c.realScid.Record(),
}
c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) {
tlvRecords = append(tlvRecords, memo.Record())
})
c.tapscriptRoot.WhenSome(
func(root tlv.RecordT[tlv.TlvType6, [32]byte]) {
tlvRecords = append(tlvRecords, root.Record())
},
)
c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType7, tlv.Blob]) {
tlvRecords = append(tlvRecords, blob.Record())
})
// Create the tlv stream.
tlvStream, err := tlv.NewStream(tlvRecords...)
if err != nil {
return err
}
return tlvStream.Encode(w)
}
// decode deserializes the openChannelTlvData from the given io.Reader.
func (c *openChannelTlvData) decode(r io.Reader) error {
memo := c.memo.Zero()
tapscriptRoot := c.tapscriptRoot.Zero()
blob := c.customBlob.Zero()
// Create the tlv stream.
tlvStream, err := tlv.NewStream(
c.revokeKeyLoc.Record(),
c.initialLocalBalance.Record(),
c.initialRemoteBalance.Record(),
c.realScid.Record(),
memo.Record(),
tapscriptRoot.Record(),
blob.Record(),
)
if err != nil {
return err
}
tlvs, err := tlvStream.DecodeWithParsedTypes(r)
if err != nil {
return err
}
if _, ok := tlvs[memo.TlvType()]; ok {
c.memo = tlv.SomeRecordT(memo)
}
if _, ok := tlvs[tapscriptRoot.TlvType()]; ok {
c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot)
}
if _, ok := tlvs[c.customBlob.TlvType()]; ok {
c.customBlob = tlv.SomeRecordT(blob)
}
return nil
}
// indexStatus is an enum-like type that describes what state the // indexStatus is an enum-like type that describes what state the
// outpoint is in. Currently only two possible values. // outpoint is in. Currently only two possible values.
type indexStatus uint8 type indexStatus uint8
@@ -325,6 +406,11 @@ const (
// SimpleTaprootFeatureBit indicates that the simple-taproot-chans // SimpleTaprootFeatureBit indicates that the simple-taproot-chans
// feature bit was negotiated during the lifetime of the channel. // feature bit was negotiated during the lifetime of the channel.
SimpleTaprootFeatureBit ChannelType = 1 << 10 SimpleTaprootFeatureBit ChannelType = 1 << 10
// TapscriptRootBit indicates that this is a MuSig2 channel with a top
// level tapscript commitment. This MUST be set along with the
// SimpleTaprootFeatureBit.
TapscriptRootBit ChannelType = 1 << 11
) )
// IsSingleFunder returns true if the channel type if one of the known single // IsSingleFunder returns true if the channel type if one of the known single
@@ -395,6 +481,12 @@ func (c ChannelType) IsTaproot() bool {
return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit
} }
// HasTapscriptRoot returns true if the channel is using a top level tapscript
// root commitment.
func (c ChannelType) HasTapscriptRoot() bool {
return c&TapscriptRootBit == TapscriptRootBit
}
// ChannelStateBounds are the parameters from OpenChannel and AcceptChannel // ChannelStateBounds are the parameters from OpenChannel and AcceptChannel
// that are responsible for providing bounds on the state space of the abstract // that are responsible for providing bounds on the state space of the abstract
// channel state. These values must be remembered for normal channel operation // channel state. These values must be remembered for normal channel operation
@@ -496,6 +588,53 @@ type ChannelConfig struct {
HtlcBasePoint keychain.KeyDescriptor HtlcBasePoint keychain.KeyDescriptor
} }
// commitTlvData stores all the optional data that may be stored as a TLV stream
// at the _end_ of the normal serialized commit on disk.
type commitTlvData struct {
// customBlob is a custom blob that may store extra data for custom
// channels.
customBlob tlv.OptionalRecordT[tlv.TlvType1, tlv.Blob]
}
// encode encodes the aux data into the passed io.Writer.
func (c *commitTlvData) encode(w io.Writer) error {
var tlvRecords []tlv.Record
c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType1, tlv.Blob]) {
tlvRecords = append(tlvRecords, blob.Record())
})
// Create the tlv stream.
tlvStream, err := tlv.NewStream(tlvRecords...)
if err != nil {
return err
}
return tlvStream.Encode(w)
}
// decode attempts to decode the aux data from the passed io.Reader.
func (c *commitTlvData) decode(r io.Reader) error {
blob := c.customBlob.Zero()
tlvStream, err := tlv.NewStream(
blob.Record(),
)
if err != nil {
return err
}
tlvs, err := tlvStream.DecodeWithParsedTypes(r)
if err != nil {
return err
}
if _, ok := tlvs[c.customBlob.TlvType()]; ok {
c.customBlob = tlv.SomeRecordT(blob)
}
return nil
}
// ChannelCommitment is a snapshot of the commitment state at a particular // ChannelCommitment is a snapshot of the commitment state at a particular
// point in the commitment chain. With each state transition, a snapshot of the // point in the commitment chain. With each state transition, a snapshot of the
// current state along with all non-settled HTLCs are recorded. These snapshots // current state along with all non-settled HTLCs are recorded. These snapshots
@@ -562,6 +701,11 @@ type ChannelCommitment struct {
// able by us. // able by us.
CommitTx *wire.MsgTx CommitTx *wire.MsgTx
// CustomBlob is an optional blob that can be used to store information
// specific to a custom channel type. This may track some custom
// specific state for this given commitment.
CustomBlob fn.Option[tlv.Blob]
// CommitSig is one half of the signature required to fully complete // CommitSig is one half of the signature required to fully complete
// the script for the commitment transaction above. This is the // the script for the commitment transaction above. This is the
// signature signed by the remote party for our version of the // signature signed by the remote party for our version of the
@@ -571,9 +715,26 @@ type ChannelCommitment struct {
// Htlcs is the set of HTLC's that are pending at this particular // Htlcs is the set of HTLC's that are pending at this particular
// commitment height. // commitment height.
Htlcs []HTLC Htlcs []HTLC
}
// TODO(roasbeef): pending commit pointer? // amendTlvData updates the channel with the given auxiliary TLV data.
// * lets just walk through func (c *ChannelCommitment) amendTlvData(auxData commitTlvData) {
auxData.customBlob.WhenSomeV(func(blob tlv.Blob) {
c.CustomBlob = fn.Some(blob)
})
}
// extractTlvData creates a new commitTlvData from the given commitment.
func (c *ChannelCommitment) extractTlvData() commitTlvData {
var auxData commitTlvData
c.CustomBlob.WhenSome(func(blob tlv.Blob) {
auxData.customBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType1](blob),
)
})
return auxData
} }
// ChannelStatus is a bit vector used to indicate whether an OpenChannel is in // ChannelStatus is a bit vector used to indicate whether an OpenChannel is in
@@ -867,6 +1028,16 @@ type OpenChannel struct {
// channel that will be useful to our future selves. // channel that will be useful to our future selves.
Memo []byte Memo []byte
// TapscriptRoot is an optional tapscript root used to derive the MuSig2
// funding output.
TapscriptRoot fn.Option[chainhash.Hash]
// CustomBlob is an optional blob that can be used to store information
// specific to a custom channel type. This information is only created
// at channel funding time, and after wards is to be considered
// immutable.
CustomBlob fn.Option[tlv.Blob]
// TODO(roasbeef): eww // TODO(roasbeef): eww
Db *ChannelStateDB Db *ChannelStateDB
@@ -1025,6 +1196,64 @@ func (c *OpenChannel) SetBroadcastHeight(height uint32) {
c.FundingBroadcastHeight = height c.FundingBroadcastHeight = height
} }
// amendTlvData updates the channel with the given auxiliary TLV data.
func (c *OpenChannel) amendTlvData(auxData openChannelTlvData) {
c.RevocationKeyLocator = auxData.revokeKeyLoc.Val.KeyLocator
c.InitialLocalBalance = lnwire.MilliSatoshi(
auxData.initialLocalBalance.Val,
)
c.InitialRemoteBalance = lnwire.MilliSatoshi(
auxData.initialRemoteBalance.Val,
)
c.confirmedScid = auxData.realScid.Val
auxData.memo.WhenSomeV(func(memo []byte) {
c.Memo = memo
})
auxData.tapscriptRoot.WhenSomeV(func(h [32]byte) {
c.TapscriptRoot = fn.Some[chainhash.Hash](h)
})
auxData.customBlob.WhenSomeV(func(blob tlv.Blob) {
c.CustomBlob = fn.Some(blob)
})
}
// extractTlvData creates a new openChannelTlvData from the given channel.
func (c *OpenChannel) extractTlvData() openChannelTlvData {
auxData := openChannelTlvData{
revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1](
keyLocRecord{c.RevocationKeyLocator},
),
initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2](
uint64(c.InitialLocalBalance),
),
initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3](
uint64(c.InitialRemoteBalance),
),
realScid: tlv.NewRecordT[tlv.TlvType4](
c.confirmedScid,
),
}
if len(c.Memo) != 0 {
auxData.memo = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType5](c.Memo),
)
}
c.TapscriptRoot.WhenSome(func(h chainhash.Hash) {
auxData.tapscriptRoot = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte](h),
)
})
c.CustomBlob.WhenSome(func(blob tlv.Blob) {
auxData.customBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType7](blob),
)
})
return auxData
}
// Refresh updates the in-memory channel state using the latest state observed // Refresh updates the in-memory channel state using the latest state observed
// on disk. // on disk.
func (c *OpenChannel) Refresh() error { func (c *OpenChannel) Refresh() error {
@@ -2351,6 +2580,12 @@ type HTLC struct {
// HTLC. It is stored in the ExtraData field, which is used to store // HTLC. It is stored in the ExtraData field, which is used to store
// a TLV stream of additional information associated with the HTLC. // a TLV stream of additional information associated with the HTLC.
BlindingPoint lnwire.BlindingPointRecord BlindingPoint lnwire.BlindingPointRecord
// CustomRecords is a set of custom TLV records that are associated with
// this HTLC. These records are used to store additional information
// about the HTLC that is not part of the standard HTLC fields. This
// field is encoded within the ExtraData field.
CustomRecords lnwire.CustomRecords
} }
// serializeExtraData encodes a TLV stream of extra data to be stored with a // serializeExtraData encodes a TLV stream of extra data to be stored with a
@@ -2369,6 +2604,11 @@ func (h *HTLC) serializeExtraData() error {
records = append(records, &b) records = append(records, &b)
}) })
records, err := h.CustomRecords.ExtendRecordProducers(records)
if err != nil {
return err
}
return h.ExtraData.PackRecords(records...) return h.ExtraData.PackRecords(records...)
} }
@@ -2390,8 +2630,19 @@ func (h *HTLC) deserializeExtraData() error {
if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil { if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil {
h.BlindingPoint = tlv.SomeRecordT(blindingPoint) h.BlindingPoint = tlv.SomeRecordT(blindingPoint)
// Remove the entry from the TLV map. Anything left in the map
// will be included in the custom records field.
delete(tlvMap, h.BlindingPoint.TlvType())
} }
// Set the custom records field to the remaining TLV records.
customRecords, err := lnwire.NewCustomRecords(tlvMap)
if err != nil {
return err
}
h.CustomRecords = customRecords
return nil return nil
} }
@@ -2529,6 +2780,8 @@ func (h *HTLC) Copy() HTLC {
copy(clone.Signature[:], h.Signature) copy(clone.Signature[:], h.Signature)
copy(clone.RHash[:], h.RHash[:]) copy(clone.RHash[:], h.RHash[:])
copy(clone.ExtraData, h.ExtraData) copy(clone.ExtraData, h.ExtraData)
clone.BlindingPoint = h.BlindingPoint
clone.CustomRecords = h.CustomRecords.Copy()
return clone return clone
} }
@@ -2690,6 +2943,14 @@ func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl
} }
} }
// We'll also encode the commit aux data stream here. We do this here
// rather than above (at the call to serializeChanCommit), to ensure
// backwards compat for reads to existing non-custom channels.
auxData := diff.Commitment.extractTlvData()
if err := auxData.encode(w); err != nil {
return fmt.Errorf("unable to write aux data: %w", err)
}
return nil return nil
} }
@@ -2750,6 +3011,17 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) {
} }
} }
// As a final step, we'll read out any aux commit data that we have at
// the end of this byte stream. We do this here to ensure backward
// compatibility, as otherwise we risk erroneously reading into the
// wrong field.
var auxData commitTlvData
if err := auxData.decode(r); err != nil {
return nil, fmt.Errorf("unable to decode aux data: %w", err)
}
d.Commitment.amendTlvData(auxData)
return &d, nil return &d, nil
} }
@@ -3728,6 +4000,13 @@ func (c *OpenChannel) Snapshot() *ChannelSnapshot {
}, },
} }
localCommit.CustomBlob.WhenSome(func(blob tlv.Blob) {
blobCopy := make([]byte, len(blob))
copy(blobCopy, blob)
snapshot.ChannelCommitment.CustomBlob = fn.Some(blobCopy)
})
// Copy over the current set of HTLCs to ensure the caller can't mutate // Copy over the current set of HTLCs to ensure the caller can't mutate
// our internal state. // our internal state.
snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs)) snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs))
@@ -4030,32 +4309,9 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error {
return err return err
} }
// Convert balance fields into uint64. auxData := channel.extractTlvData()
localBalance := uint64(channel.InitialLocalBalance) if err := auxData.encode(&w); err != nil {
remoteBalance := uint64(channel.InitialRemoteBalance) return fmt.Errorf("unable to encode aux data: %w", err)
// Create the tlv stream.
tlvStream, err := tlv.NewStream(
// Write the RevocationKeyLocator as the first entry in a tlv
// stream.
MakeKeyLocRecord(
keyLocType, &channel.RevocationKeyLocator,
),
tlv.MakePrimitiveRecord(
initialLocalBalanceType, &localBalance,
),
tlv.MakePrimitiveRecord(
initialRemoteBalanceType, &remoteBalance,
),
MakeScidRecord(realScidType, &channel.confirmedScid),
tlv.MakePrimitiveRecord(channelMemoType, &channel.Memo),
)
if err != nil {
return err
}
if err := tlvStream.Encode(&w); err != nil {
return err
} }
if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil { if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil {
@@ -4142,6 +4398,12 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment,
return err return err
} }
// Before we write to disk, we'll also write our aux data as well.
auxData := c.extractTlvData()
if err := auxData.encode(&b); err != nil {
return fmt.Errorf("unable to write aux data: %w", err)
}
return chanBucket.Put(commitKey, b.Bytes()) return chanBucket.Put(commitKey, b.Bytes())
} }
@@ -4244,45 +4506,14 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error {
} }
} }
// Create balance fields in uint64, and Memo field as byte slice. var auxData openChannelTlvData
var ( if err := auxData.decode(r); err != nil {
localBalance uint64 return fmt.Errorf("unable to decode aux data: %w", err)
remoteBalance uint64
memo []byte
)
// Create the tlv stream.
tlvStream, err := tlv.NewStream(
// Write the RevocationKeyLocator as the first entry in a tlv
// stream.
MakeKeyLocRecord(
keyLocType, &channel.RevocationKeyLocator,
),
tlv.MakePrimitiveRecord(
initialLocalBalanceType, &localBalance,
),
tlv.MakePrimitiveRecord(
initialRemoteBalanceType, &remoteBalance,
),
MakeScidRecord(realScidType, &channel.confirmedScid),
tlv.MakePrimitiveRecord(channelMemoType, &memo),
)
if err != nil {
return err
} }
if err := tlvStream.Decode(r); err != nil { // Assign all the relevant fields from the aux data into the actual
return err // open channel.
} channel.amendTlvData(auxData)
// Attach the balance fields.
channel.InitialLocalBalance = lnwire.MilliSatoshi(localBalance)
channel.InitialRemoteBalance = lnwire.MilliSatoshi(remoteBalance)
// Attach the memo field if non-empty.
if len(memo) > 0 {
channel.Memo = memo
}
channel.Packager = NewChannelPackager(channel.ShortChannelID) channel.Packager = NewChannelPackager(channel.ShortChannelID)
@@ -4318,7 +4549,9 @@ func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) {
return c, nil return c, nil
} }
func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment, error) { func fetchChanCommitment(chanBucket kvdb.RBucket,
local bool) (ChannelCommitment, error) {
var commitKey []byte var commitKey []byte
if local { if local {
commitKey = append(chanCommitmentKey, byte(0x00)) commitKey = append(chanCommitmentKey, byte(0x00))
@@ -4332,7 +4565,23 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment
} }
r := bytes.NewReader(commitBytes) r := bytes.NewReader(commitBytes)
return deserializeChanCommit(r) chanCommit, err := deserializeChanCommit(r)
if err != nil {
return ChannelCommitment{}, fmt.Errorf("unable to decode "+
"chan commit: %w", err)
}
// We'll also check to see if we have any aux data stored as the end of
// the stream.
var auxData commitTlvData
if err := auxData.decode(r); err != nil {
return ChannelCommitment{}, fmt.Errorf("unable to decode "+
"chan aux data: %w", err)
}
chanCommit.amendTlvData(auxData)
return chanCommit, nil
} }
func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error { func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error {
@@ -4440,6 +4689,25 @@ func deleteThawHeight(chanBucket kvdb.RwBucket) error {
return chanBucket.Delete(frozenChanKey) return chanBucket.Delete(frozenChanKey)
} }
// keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the
// tlv.RecordProducer interface.
type keyLocRecord struct {
keychain.KeyLocator
}
// Record 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.
//
// NOTE: This is part of the tlv.RecordProducer interface.
func (k *keyLocRecord) Record() tlv.Record {
// Note that we set the type here as zero, as when used with a
// tlv.RecordT, the type param will be used as the type.
return tlv.MakeStaticRecord(
0, &k.KeyLocator, 8, EKeyLocator, DKeyLocator,
)
}
// EKeyLocator is an encoder for keychain.KeyLocator. // EKeyLocator is an encoder for keychain.KeyLocator.
func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error { func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*keychain.KeyLocator); ok { if v, ok := val.(*keychain.KeyLocator); ok {
@@ -4468,22 +4736,6 @@ func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8) 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)
}
// MakeScidRecord creates a Record out of a ShortChannelID using the passed
// Type and the EShortChannelID and DShortChannelID functions. The size will
// always be 8 for the ShortChannelID.
func MakeScidRecord(typ tlv.Type, scid *lnwire.ShortChannelID) tlv.Record {
return tlv.MakeStaticRecord(
typ, scid, 8, lnwire.EShortChannelID, lnwire.DShortChannelID,
)
}
// ShutdownInfo contains various info about the shutdown initiation of a // ShutdownInfo contains various info about the shutdown initiation of a
// channel. // channel.
type ShutdownInfo struct { type ShutdownInfo struct {
+58 -16
View File
@@ -17,6 +17,7 @@ import (
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnmock" "github.com/lightningnetwork/lnd/lnmock"
@@ -173,7 +174,7 @@ func fundingPointOption(chanPoint wire.OutPoint) testChannelOption {
} }
// channelIDOption is an option which sets the short channel ID of the channel. // channelIDOption is an option which sets the short channel ID of the channel.
var channelIDOption = func(chanID lnwire.ShortChannelID) testChannelOption { func channelIDOption(chanID lnwire.ShortChannelID) testChannelOption {
return func(params *testChannelParams) { return func(params *testChannelParams) {
params.channel.ShortChannelID = chanID params.channel.ShortChannelID = chanID
} }
@@ -326,6 +327,9 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
uniqueOutputIndex.Add(1) uniqueOutputIndex.Add(1)
op := wire.OutPoint{Hash: key, Index: uniqueOutputIndex.Load()} op := wire.OutPoint{Hash: key, Index: uniqueOutputIndex.Load()}
var tapscriptRoot chainhash.Hash
copy(tapscriptRoot[:], bytes.Repeat([]byte{1}, 32))
return &OpenChannel{ return &OpenChannel{
ChanType: SingleFunderBit | FrozenBit, ChanType: SingleFunderBit | FrozenBit,
ChainHash: key, ChainHash: key,
@@ -347,6 +351,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
FeePerKw: btcutil.Amount(5000), FeePerKw: btcutil.Amount(5000),
CommitTx: channels.TestFundingTx, CommitTx: channels.TestFundingTx,
CommitSig: bytes.Repeat([]byte{1}, 71), CommitSig: bytes.Repeat([]byte{1}, 71),
CustomBlob: fn.Some([]byte{1, 2, 3}),
}, },
RemoteCommitment: ChannelCommitment{ RemoteCommitment: ChannelCommitment{
CommitHeight: 0, CommitHeight: 0,
@@ -356,6 +361,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
FeePerKw: btcutil.Amount(5000), FeePerKw: btcutil.Amount(5000),
CommitTx: channels.TestFundingTx, CommitTx: channels.TestFundingTx,
CommitSig: bytes.Repeat([]byte{1}, 71), CommitSig: bytes.Repeat([]byte{1}, 71),
CustomBlob: fn.Some([]byte{4, 5, 6}),
}, },
NumConfsRequired: 4, NumConfsRequired: 4,
RemoteCurrentRevocation: privKey.PubKey(), RemoteCurrentRevocation: privKey.PubKey(),
@@ -368,6 +374,9 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
ThawHeight: uint32(defaultPendingHeight), ThawHeight: uint32(defaultPendingHeight),
InitialLocalBalance: lnwire.MilliSatoshi(9000), InitialLocalBalance: lnwire.MilliSatoshi(9000),
InitialRemoteBalance: lnwire.MilliSatoshi(3000), InitialRemoteBalance: lnwire.MilliSatoshi(3000),
Memo: []byte("test"),
TapscriptRoot: fn.Some(tapscriptRoot),
CustomBlob: fn.Some([]byte{1, 2, 3}),
} }
} }
@@ -575,24 +584,32 @@ func assertCommitmentEqual(t *testing.T, a, b *ChannelCommitment) {
func assertRevocationLogEntryEqual(t *testing.T, c *ChannelCommitment, func assertRevocationLogEntryEqual(t *testing.T, c *ChannelCommitment,
r *RevocationLog) { r *RevocationLog) {
t.Helper()
// Check the common fields. // Check the common fields.
require.EqualValues( require.EqualValues(
t, r.CommitTxHash, c.CommitTx.TxHash(), "CommitTx mismatch", t, r.CommitTxHash.Val, c.CommitTx.TxHash(), "CommitTx mismatch",
) )
// Now check the common fields from the HTLCs. // Now check the common fields from the HTLCs.
require.Equal(t, len(r.HTLCEntries), len(c.Htlcs), "HTLCs len mismatch") require.Equal(t, len(r.HTLCEntries), len(c.Htlcs), "HTLCs len mismatch")
for i, rHtlc := range r.HTLCEntries { for i, rHtlc := range r.HTLCEntries {
cHtlc := c.Htlcs[i] cHtlc := c.Htlcs[i]
require.Equal(t, rHtlc.RHash, cHtlc.RHash, "RHash mismatch") require.Equal(t, rHtlc.RHash.Val[:], cHtlc.RHash[:], "RHash")
require.Equal(t, rHtlc.Amt, cHtlc.Amt.ToSatoshis(), require.Equal(
"Amt mismatch") t, rHtlc.Amt.Val.Int(), cHtlc.Amt.ToSatoshis(), "Amt",
require.Equal(t, rHtlc.RefundTimeout, cHtlc.RefundTimeout, )
"RefundTimeout mismatch") require.Equal(
require.EqualValues(t, rHtlc.OutputIndex, cHtlc.OutputIndex, t, rHtlc.RefundTimeout.Val, cHtlc.RefundTimeout,
"OutputIndex mismatch") "RefundTimeout",
require.Equal(t, rHtlc.Incoming, cHtlc.Incoming, )
"Incoming mismatch") require.EqualValues(
t, rHtlc.OutputIndex.Val, cHtlc.OutputIndex,
"OutputIndex",
)
require.Equal(
t, rHtlc.Incoming.Val, cHtlc.Incoming, "Incoming",
)
} }
} }
@@ -657,6 +674,7 @@ func TestChannelStateTransition(t *testing.T) {
CommitTx: newTx, CommitTx: newTx,
CommitSig: newSig, CommitSig: newSig,
Htlcs: htlcs, Htlcs: htlcs,
CustomBlob: fn.Some([]byte{4, 5, 6}),
} }
// First update the local node's broadcastable state and also add a // First update the local node's broadcastable state and also add a
@@ -694,9 +712,14 @@ func TestChannelStateTransition(t *testing.T) {
// have been updated. // have been updated.
updatedChannel, err := cdb.FetchOpenChannels(channel.IdentityPub) updatedChannel, err := cdb.FetchOpenChannels(channel.IdentityPub)
require.NoError(t, err, "unable to fetch updated channel") require.NoError(t, err, "unable to fetch updated channel")
assertCommitmentEqual(t, &commitment, &updatedChannel[0].LocalCommitment)
assertCommitmentEqual(
t, &commitment, &updatedChannel[0].LocalCommitment,
)
numDiskUpdates, err := updatedChannel[0].CommitmentHeight() numDiskUpdates, err := updatedChannel[0].CommitmentHeight()
require.NoError(t, err, "unable to read commitment height from disk") require.NoError(t, err, "unable to read commitment height from disk")
if numDiskUpdates != uint64(commitment.CommitHeight) { if numDiskUpdates != uint64(commitment.CommitHeight) {
t.Fatalf("num disk updates doesn't match: %v vs %v", t.Fatalf("num disk updates doesn't match: %v vs %v",
numDiskUpdates, commitment.CommitHeight) numDiskUpdates, commitment.CommitHeight)
@@ -799,10 +822,10 @@ func TestChannelStateTransition(t *testing.T) {
// Check the output indexes are saved as expected. // Check the output indexes are saved as expected.
require.EqualValues( require.EqualValues(
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex, t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val,
) )
require.EqualValues( require.EqualValues(
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex, t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val,
) )
// The two deltas (the original vs the on-disk version) should // The two deltas (the original vs the on-disk version) should
@@ -844,10 +867,10 @@ func TestChannelStateTransition(t *testing.T) {
// Check the output indexes are saved as expected. // Check the output indexes are saved as expected.
require.EqualValues( require.EqualValues(
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex, t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val,
) )
require.EqualValues( require.EqualValues(
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex, t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val,
) )
assertRevocationLogEntryEqual(t, &oldRemoteCommit, prevCommit) assertRevocationLogEntryEqual(t, &oldRemoteCommit, prevCommit)
@@ -1642,6 +1665,24 @@ func TestHTLCsExtraData(t *testing.T) {
), ),
} }
// Custom channel data htlc with a blinding point.
customDataHTLC := HTLC{
Signature: testSig.Serialize(),
Incoming: false,
Amt: 10,
RHash: key,
RefundTimeout: 1,
OnionBlob: lnmock.MockOnion(),
BlindingPoint: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
pubKey,
),
),
CustomRecords: map[uint64][]byte{
uint64(lnwire.MinCustomRecordsTlvType + 3): {1, 2, 3},
},
}
testCases := []struct { testCases := []struct {
name string name string
htlcs []HTLC htlcs []HTLC
@@ -1663,6 +1704,7 @@ func TestHTLCsExtraData(t *testing.T) {
mockHtlc, mockHtlc,
blindingPointHTLC, blindingPointHTLC,
mockHtlc, mockHtlc,
customDataHTLC,
}, },
}, },
} }
+272 -174
View File
@@ -7,6 +7,7 @@ import (
"math" "math"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -16,16 +17,15 @@ import (
const ( const (
// OutputIndexEmpty is used when the output index doesn't exist. // OutputIndexEmpty is used when the output index doesn't exist.
OutputIndexEmpty = math.MaxUint16 OutputIndexEmpty = math.MaxUint16
)
// A set of tlv type definitions used to serialize the body of type (
// revocation logs to the database. // BigSizeAmount is a type alias for a TLV record of a btcutil.Amount.
// BigSizeAmount = tlv.BigSizeT[btcutil.Amount]
// NOTE: A migration should be added whenever this list changes.
revLogOurOutputIndexType tlv.Type = 0 // BigSizeMilliSatoshi is a type alias for a TLV record of a
revLogTheirOutputIndexType tlv.Type = 1 // lnwire.MilliSatoshi.
revLogCommitTxHashType tlv.Type = 2 BigSizeMilliSatoshi = tlv.BigSizeT[lnwire.MilliSatoshi]
revLogOurBalanceType tlv.Type = 3
revLogTheirBalanceType tlv.Type = 4
) )
var ( var (
@@ -54,6 +54,74 @@ var (
ErrOutputIndexTooBig = errors.New("output index is over uint16") ErrOutputIndexTooBig = errors.New("output index is over uint16")
) )
// SparsePayHash is a type alias for a 32 byte array, which when serialized is
// able to save some space by not including an empty payment hash on disk.
type SparsePayHash [32]byte
// NewSparsePayHash creates a new SparsePayHash from a 32 byte array.
func NewSparsePayHash(rHash [32]byte) SparsePayHash {
return SparsePayHash(rHash)
}
// Record returns a tlv record for the SparsePayHash.
func (s *SparsePayHash) Record() tlv.Record {
// We use a zero for the type here, as this'll be used along with the
// RecordT type.
return tlv.MakeDynamicRecord(
0, s, s.hashLen,
sparseHashEncoder, sparseHashDecoder,
)
}
// hashLen is used by MakeDynamicRecord to return the size of the RHash.
//
// NOTE: for zero hash, we return a length 0.
func (s *SparsePayHash) hashLen() uint64 {
if bytes.Equal(s[:], lntypes.ZeroHash[:]) {
return 0
}
return 32
}
// sparseHashEncoder is the customized encoder which skips encoding the empty
// hash.
func sparseHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
v, ok := val.(*SparsePayHash)
if !ok {
return tlv.NewTypeForEncodingErr(val, "SparsePayHash")
}
// If the value is an empty hash, we will skip encoding it.
if bytes.Equal(v[:], lntypes.ZeroHash[:]) {
return nil
}
vArray := (*[32]byte)(v)
return tlv.EBytes32(w, vArray, buf)
}
// sparseHashDecoder is the customized decoder which skips decoding the empty
// hash.
func sparseHashDecoder(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
v, ok := val.(*SparsePayHash)
if !ok {
return tlv.NewTypeForEncodingErr(val, "SparsePayHash")
}
// If the length is zero, we will skip encoding the empty hash.
if l == 0 {
return nil
}
vArray := (*[32]byte)(v)
return tlv.DBytes32(r, vArray, buf, 32)
}
// HTLCEntry specifies the minimal info needed to be stored on disk for ALL the // HTLCEntry specifies the minimal info needed to be stored on disk for ALL the
// historical HTLCs, which is useful for constructing RevocationLog when a // historical HTLCs, which is useful for constructing RevocationLog when a
// breach is detected. // breach is detected.
@@ -72,116 +140,90 @@ var (
// made into tlv records without further conversion. // made into tlv records without further conversion.
type HTLCEntry struct { type HTLCEntry struct {
// RHash is the payment hash of the HTLC. // RHash is the payment hash of the HTLC.
RHash [32]byte RHash tlv.RecordT[tlv.TlvType0, SparsePayHash]
// RefundTimeout is the absolute timeout on the HTLC that the sender // RefundTimeout is the absolute timeout on the HTLC that the sender
// must wait before reclaiming the funds in limbo. // must wait before reclaiming the funds in limbo.
RefundTimeout uint32 RefundTimeout tlv.RecordT[tlv.TlvType1, uint32]
// OutputIndex is the output index for this particular HTLC output // OutputIndex is the output index for this particular HTLC output
// within the commitment transaction. // within the commitment transaction.
// //
// NOTE: we use uint16 instead of int32 here to save us 2 bytes, which // NOTE: we use uint16 instead of int32 here to save us 2 bytes, which
// gives us a max number of HTLCs of 65K. // gives us a max number of HTLCs of 65K.
OutputIndex uint16 OutputIndex tlv.RecordT[tlv.TlvType2, uint16]
// Incoming denotes whether we're the receiver or the sender of this // Incoming denotes whether we're the receiver or the sender of this
// HTLC. // HTLC.
// Incoming tlv.RecordT[tlv.TlvType3, bool]
// NOTE: this field is the memory representation of the field
// incomingUint.
Incoming bool
// Amt is the amount of satoshis this HTLC escrows. // Amt is the amount of satoshis this HTLC escrows.
// Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]]
// 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 // CustomBlob is an optional blob that can be used to store information
// easily make it into a tlv record and save it to disk. // specific to revocation handling for a custom channel type.
// CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob]
// 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 // HtlcIndex is the index of the HTLC in the channel.
// so we can easily make it into a tlv record and save it to disk. HtlcIndex tlv.OptionalRecordT[tlv.TlvType6, uint16]
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. // toTlvStream converts an HTLCEntry record into a tlv representation.
func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) { func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) {
const ( records := []tlv.Record{
// A set of tlv type definitions used to serialize htlc entries h.RHash.Record(),
// to the database. We define it here instead of the head of h.RefundTimeout.Record(),
// the file to avoid naming conflicts. h.OutputIndex.Record(),
// h.Incoming.Record(),
// NOTE: A migration should be added whenever this list h.Amt.Record(),
// changes. }
rHashType tlv.Type = 0
refundTimeoutType tlv.Type = 1
outputIndexType tlv.Type = 2
incomingType tlv.Type = 3
amtType tlv.Type = 4
)
return tlv.NewStream( h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) {
tlv.MakeDynamicRecord( records = append(records, r.Record())
rHashType, &h.RHash, h.RHashLen, })
RHashEncoder, RHashDecoder,
h.HtlcIndex.WhenSome(func(r tlv.RecordT[tlv.TlvType6, uint16]) {
records = append(records, r.Record())
})
tlv.SortRecords(records)
return tlv.NewStream(records...)
}
// NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC.
func NewHTLCEntryFromHTLC(htlc HTLC) (*HTLCEntry, error) {
h := &HTLCEntry{
RHash: tlv.NewRecordT[tlv.TlvType0](
NewSparsePayHash(htlc.RHash),
), ),
tlv.MakePrimitiveRecord( RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1](
refundTimeoutType, &h.RefundTimeout, htlc.RefundTimeout,
), ),
tlv.MakePrimitiveRecord( OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2](
outputIndexType, &h.OutputIndex, uint16(htlc.OutputIndex),
), ),
tlv.MakePrimitiveRecord(incomingType, &h.incomingTlv), Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](htlc.Incoming),
// We will save 3 bytes if the amount is less or equal to Amt: tlv.NewRecordT[tlv.TlvType4](
// 4,294,967,295 msat, or roughly 0.043 bitcoin. tlv.NewBigSizeT(htlc.Amt.ToSatoshis()),
tlv.MakeBigSizeRecord(amtType, &h.amtTlv), ),
) HtlcIndex: tlv.SomeRecordT(tlv.NewPrimitiveRecord[tlv.TlvType6](
uint16(htlc.HtlcIndex),
)),
}
if len(htlc.CustomRecords) != 0 {
blob, err := htlc.CustomRecords.Serialize()
if err != nil {
return nil, err
}
h.CustomBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
)
}
return h, nil
} }
// RevocationLog stores the info needed to construct a breach retribution. Its // RevocationLog stores the info needed to construct a breach retribution. Its
@@ -191,15 +233,15 @@ func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) {
type RevocationLog struct { type RevocationLog struct {
// OurOutputIndex specifies our output index in this commitment. In a // OurOutputIndex specifies our output index in this commitment. In a
// remote commitment transaction, this is the to remote output index. // remote commitment transaction, this is the to remote output index.
OurOutputIndex uint16 OurOutputIndex tlv.RecordT[tlv.TlvType0, uint16]
// TheirOutputIndex specifies their output index in this commitment. In // TheirOutputIndex specifies their output index in this commitment. In
// a remote commitment transaction, this is the to local output index. // a remote commitment transaction, this is the to local output index.
TheirOutputIndex uint16 TheirOutputIndex tlv.RecordT[tlv.TlvType1, uint16]
// CommitTxHash is the hash of the latest version of the commitment // CommitTxHash is the hash of the latest version of the commitment
// state, broadcast able by us. // state, broadcast able by us.
CommitTxHash [32]byte CommitTxHash tlv.RecordT[tlv.TlvType2, [32]byte]
// HTLCEntries is the set of HTLCEntry's that are pending at this // HTLCEntries is the set of HTLCEntry's that are pending at this
// particular commitment height. // particular commitment height.
@@ -209,21 +251,65 @@ type RevocationLog struct {
// directly spendable by us. In other words, it is the value of the // directly spendable by us. In other words, it is the value of the
// to_remote output on the remote parties' commitment transaction. // to_remote output on the remote parties' commitment transaction.
// //
// NOTE: this is a pointer so that it is clear if the value is zero or // NOTE: this is an option so that it is clear if the value is zero or
// nil. Since migration 30 of the channeldb initially did not include // nil. Since migration 30 of the channeldb initially did not include
// this field, it could be the case that the field is not present for // this field, it could be the case that the field is not present for
// all revocation logs. // all revocation logs.
OurBalance *lnwire.MilliSatoshi OurBalance tlv.OptionalRecordT[tlv.TlvType3, BigSizeMilliSatoshi]
// TheirBalance is the current available balance within the channel // TheirBalance is the current available balance within the channel
// directly spendable by the remote node. In other words, it is the // directly spendable by the remote node. In other words, it is the
// value of the to_local output on the remote parties' commitment. // value of the to_local output on the remote parties' commitment.
// //
// NOTE: this is a pointer so that it is clear if the value is zero or // NOTE: this is an option so that it is clear if the value is zero or
// nil. Since migration 30 of the channeldb initially did not include // nil. Since migration 30 of the channeldb initially did not include
// this field, it could be the case that the field is not present for // this field, it could be the case that the field is not present for
// all revocation logs. // all revocation logs.
TheirBalance *lnwire.MilliSatoshi TheirBalance tlv.OptionalRecordT[tlv.TlvType4, BigSizeMilliSatoshi]
// CustomBlob is an optional blob that can be used to store information
// specific to a custom channel type. This information is only created
// at channel funding time, and after wards is to be considered
// immutable.
CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob]
}
// NewRevocationLog creates a new RevocationLog from the given parameters.
func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16,
commitHash [32]byte, ourBalance,
theirBalance fn.Option[lnwire.MilliSatoshi], htlcs []*HTLCEntry,
customBlob fn.Option[tlv.Blob]) RevocationLog {
rl := RevocationLog{
OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0](
ourOutputIndex,
),
TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1](
theirOutputIndex,
),
CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2](commitHash),
HTLCEntries: htlcs,
}
ourBalance.WhenSome(func(balance lnwire.MilliSatoshi) {
rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3](
tlv.NewBigSizeT(balance),
))
})
theirBalance.WhenSome(func(balance lnwire.MilliSatoshi) {
rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4](
tlv.NewBigSizeT(balance),
))
})
customBlob.WhenSome(func(blob tlv.Blob) {
rl.CustomBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
)
})
return rl
} }
// putRevocationLog uses the fields `CommitTx` and `Htlcs` from a // putRevocationLog uses the fields `CommitTx` and `Htlcs` from a
@@ -242,15 +328,32 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment,
} }
rl := &RevocationLog{ rl := &RevocationLog{
OurOutputIndex: uint16(ourOutputIndex), OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0](
TheirOutputIndex: uint16(theirOutputIndex), uint16(ourOutputIndex),
CommitTxHash: commit.CommitTx.TxHash(), ),
HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)), TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1](
uint16(theirOutputIndex),
),
CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2, [32]byte](
commit.CommitTx.TxHash(),
),
HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)),
} }
commit.CustomBlob.WhenSome(func(blob tlv.Blob) {
rl.CustomBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
)
})
if !noAmtData { if !noAmtData {
rl.OurBalance = &commit.LocalBalance rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3](
rl.TheirBalance = &commit.RemoteBalance tlv.NewBigSizeT(commit.LocalBalance),
))
rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4](
tlv.NewBigSizeT(commit.RemoteBalance),
))
} }
for _, htlc := range commit.Htlcs { for _, htlc := range commit.Htlcs {
@@ -265,12 +368,9 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment,
return ErrOutputIndexTooBig return ErrOutputIndexTooBig
} }
entry := &HTLCEntry{ entry, err := NewHTLCEntryFromHTLC(htlc)
RHash: htlc.RHash, if err != nil {
RefundTimeout: htlc.RefundTimeout, return err
Incoming: htlc.Incoming,
OutputIndex: uint16(htlc.OutputIndex),
Amt: htlc.Amt.ToSatoshis(),
} }
rl.HTLCEntries = append(rl.HTLCEntries, entry) rl.HTLCEntries = append(rl.HTLCEntries, entry)
} }
@@ -306,31 +406,27 @@ func fetchRevocationLog(log kvdb.RBucket,
func serializeRevocationLog(w io.Writer, rl *RevocationLog) error { func serializeRevocationLog(w io.Writer, rl *RevocationLog) error {
// Add the tlv records for all non-optional fields. // Add the tlv records for all non-optional fields.
records := []tlv.Record{ records := []tlv.Record{
tlv.MakePrimitiveRecord( rl.OurOutputIndex.Record(),
revLogOurOutputIndexType, &rl.OurOutputIndex, rl.TheirOutputIndex.Record(),
), rl.CommitTxHash.Record(),
tlv.MakePrimitiveRecord(
revLogTheirOutputIndexType, &rl.TheirOutputIndex,
),
tlv.MakePrimitiveRecord(
revLogCommitTxHashType, &rl.CommitTxHash,
),
} }
// Now we add any optional fields that are non-nil. // Now we add any optional fields that are non-nil.
if rl.OurBalance != nil { rl.OurBalance.WhenSome(
lb := uint64(*rl.OurBalance) func(r tlv.RecordT[tlv.TlvType3, BigSizeMilliSatoshi]) {
records = append(records, tlv.MakeBigSizeRecord( records = append(records, r.Record())
revLogOurBalanceType, &lb, },
)) )
}
if rl.TheirBalance != nil { rl.TheirBalance.WhenSome(
rb := uint64(*rl.TheirBalance) func(r tlv.RecordT[tlv.TlvType4, BigSizeMilliSatoshi]) {
records = append(records, tlv.MakeBigSizeRecord( records = append(records, r.Record())
revLogTheirBalanceType, &rb, },
)) )
}
rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) {
records = append(records, r.Record())
})
// Create the tlv stream. // Create the tlv stream.
tlvStream, err := tlv.NewStream(records...) tlvStream, err := tlv.NewStream(records...)
@@ -351,14 +447,6 @@ func serializeRevocationLog(w io.Writer, rl *RevocationLog) error {
// format. // format.
func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error { func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error {
for _, htlc := range htlcs { 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. // Create the tlv stream.
tlvStream, err := htlc.toTlvStream() tlvStream, err := htlc.toTlvStream()
if err != nil { if err != nil {
@@ -376,27 +464,20 @@ func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error {
// deserializeRevocationLog deserializes a RevocationLog based on tlv format. // deserializeRevocationLog deserializes a RevocationLog based on tlv format.
func deserializeRevocationLog(r io.Reader) (RevocationLog, error) { func deserializeRevocationLog(r io.Reader) (RevocationLog, error) {
var ( var rl RevocationLog
rl RevocationLog
ourBalance uint64 ourBalance := rl.OurBalance.Zero()
theirBalance uint64 theirBalance := rl.TheirBalance.Zero()
) customBlob := rl.CustomBlob.Zero()
// Create the tlv stream. // Create the tlv stream.
tlvStream, err := tlv.NewStream( tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord( rl.OurOutputIndex.Record(),
revLogOurOutputIndexType, &rl.OurOutputIndex, rl.TheirOutputIndex.Record(),
), rl.CommitTxHash.Record(),
tlv.MakePrimitiveRecord( ourBalance.Record(),
revLogTheirOutputIndexType, &rl.TheirOutputIndex, theirBalance.Record(),
), customBlob.Record(),
tlv.MakePrimitiveRecord(
revLogCommitTxHashType, &rl.CommitTxHash,
),
tlv.MakeBigSizeRecord(revLogOurBalanceType, &ourBalance),
tlv.MakeBigSizeRecord(
revLogTheirBalanceType, &theirBalance,
),
) )
if err != nil { if err != nil {
return rl, err return rl, err
@@ -408,14 +489,16 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) {
return rl, err return rl, err
} }
if t, ok := parsedTypes[revLogOurBalanceType]; ok && t == nil { if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil {
lb := lnwire.MilliSatoshi(ourBalance) rl.OurBalance = tlv.SomeRecordT(ourBalance)
rl.OurBalance = &lb
} }
if t, ok := parsedTypes[revLogTheirBalanceType]; ok && t == nil { if t, ok := parsedTypes[theirBalance.TlvType()]; ok && t == nil {
rb := lnwire.MilliSatoshi(theirBalance) rl.TheirBalance = tlv.SomeRecordT(theirBalance)
rl.TheirBalance = &rb }
if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil {
rl.CustomBlob = tlv.SomeRecordT(customBlob)
} }
// Read the HTLC entries. // Read the HTLC entries.
@@ -432,14 +515,28 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) {
for { for {
var htlc HTLCEntry var htlc HTLCEntry
customBlob := htlc.CustomBlob.Zero()
htlcIndex := htlc.HtlcIndex.Zero()
// Create the tlv stream. // Create the tlv stream.
tlvStream, err := htlc.toTlvStream() records := []tlv.Record{
htlc.RHash.Record(),
htlc.RefundTimeout.Record(),
htlc.OutputIndex.Record(),
htlc.Incoming.Record(),
htlc.Amt.Record(),
customBlob.Record(),
htlcIndex.Record(),
}
tlvStream, err := tlv.NewStream(records...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Read the HTLC entry. // Read the HTLC entry.
if _, err := readTlvStream(r, tlvStream); err != nil { parsedTypes, err := readTlvStream(r, tlvStream)
if err != nil {
// We've reached the end when hitting an EOF. // We've reached the end when hitting an EOF.
if err == io.ErrUnexpectedEOF { if err == io.ErrUnexpectedEOF {
break break
@@ -447,13 +544,13 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) {
return nil, err return nil, err
} }
// Patch the Incoming field. if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil {
if htlc.incomingTlv == 1 { htlc.CustomBlob = tlv.SomeRecordT(customBlob)
htlc.Incoming = true
} }
// Patch the Amt field. if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil {
htlc.Amt = btcutil.Amount(htlc.amtTlv) htlc.HtlcIndex = tlv.SomeRecordT(htlcIndex)
}
// Append the entry. // Append the entry.
htlcs = append(htlcs, &htlc) htlcs = append(htlcs, &htlc)
@@ -469,6 +566,7 @@ func writeTlvStream(w io.Writer, s *tlv.Stream) error {
if err := s.Encode(&b); err != nil { if err := s.Encode(&b); err != nil {
return err return err
} }
// Write the stream's length as a varint. // Write the stream's length as a varint.
err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{}) err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{})
if err != nil { if err != nil {
+128 -49
View File
@@ -8,6 +8,7 @@ import (
"testing" "testing"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lntest/channels" "github.com/lightningnetwork/lnd/lntest/channels"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -33,17 +34,38 @@ var (
0xff, // value = 255 0xff, // value = 255
} }
customRecords = lnwire.CustomRecords{
lnwire.MinCustomRecordsTlvType + 1: []byte("custom data"),
}
blobBytes = []byte{
// Corresponds to the encoded version of the above custom
// records.
0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73, 0x74,
0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
}
testHTLCEntry = HTLCEntry{ testHTLCEntry = HTLCEntry{
RefundTimeout: 740_000, RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32](
OutputIndex: 10, 740_000,
Incoming: true, ),
Amt: 1000_000, OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16](
amtTlv: 1000_000, 10,
incomingTlv: 1, ),
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](true),
Amt: tlv.NewRecordT[tlv.TlvType4](
tlv.NewBigSizeT(btcutil.Amount(1_000_000)),
),
CustomBlob: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType5](blobBytes),
),
HtlcIndex: tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType6, uint16](0x33),
),
} }
testHTLCEntryBytes = []byte{ testHTLCEntryBytes = []byte{
// Body length 23. // Body length 45.
0x16, 0x2d,
// Rhash tlv. // Rhash tlv.
0x0, 0x0, 0x0, 0x0,
// RefundTimeout tlv. // RefundTimeout tlv.
@@ -54,6 +76,45 @@ var (
0x3, 0x1, 0x1, 0x3, 0x1, 0x1,
// Amt tlv. // Amt tlv.
0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40, 0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40,
// Custom blob tlv.
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
// HLTC index tlv.
0x6, 0x2, 0x0, 0x33,
}
testHTLCEntryHash = HTLCEntry{
RHash: tlv.NewPrimitiveRecord[tlv.TlvType0](NewSparsePayHash(
[32]byte{0x33, 0x44, 0x55},
)),
RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32](
740_000,
),
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16](
10,
),
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](true),
Amt: tlv.NewRecordT[tlv.TlvType4](
tlv.NewBigSizeT(btcutil.Amount(1_000_000)),
),
}
testHTLCEntryHashBytes = []byte{
// Body length 54.
0x36,
// Rhash tlv.
0x0, 0x20,
0x33, 0x44, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// RefundTimeout tlv.
0x1, 0x4, 0x0, 0xb, 0x4a, 0xa0,
// OutputIndex tlv.
0x2, 0x2, 0x0, 0xa,
// Incoming tlv.
0x3, 0x1, 0x1,
// Amt tlv.
0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40,
} }
localBalance = lnwire.MilliSatoshi(9000) localBalance = lnwire.MilliSatoshi(9000)
@@ -68,24 +129,29 @@ var (
CommitTx: channels.TestFundingTx, CommitTx: channels.TestFundingTx,
CommitSig: bytes.Repeat([]byte{1}, 71), CommitSig: bytes.Repeat([]byte{1}, 71),
Htlcs: []HTLC{{ Htlcs: []HTLC{{
RefundTimeout: testHTLCEntry.RefundTimeout, RefundTimeout: testHTLCEntry.RefundTimeout.Val,
OutputIndex: int32(testHTLCEntry.OutputIndex), OutputIndex: int32(testHTLCEntry.OutputIndex.Val),
Incoming: testHTLCEntry.Incoming, HtlcIndex: uint64(
Amt: lnwire.NewMSatFromSatoshis( testHTLCEntry.HtlcIndex.ValOpt().
testHTLCEntry.Amt, UnsafeFromSome(),
), ),
Incoming: testHTLCEntry.Incoming.Val,
Amt: lnwire.NewMSatFromSatoshis(
testHTLCEntry.Amt.Val.Int(),
),
CustomRecords: customRecords,
}}, }},
CustomBlob: fn.Some(blobBytes),
} }
testRevocationLogNoAmts = RevocationLog{ testRevocationLogNoAmts = NewRevocationLog(
OurOutputIndex: 0, 0, 1, testChannelCommit.CommitTx.TxHash(),
TheirOutputIndex: 1, fn.None[lnwire.MilliSatoshi](), fn.None[lnwire.MilliSatoshi](),
CommitTxHash: testChannelCommit.CommitTx.TxHash(), []*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes),
HTLCEntries: []*HTLCEntry{&testHTLCEntry}, )
}
testRevocationLogNoAmtsBytes = []byte{ testRevocationLogNoAmtsBytes = []byte{
// Body length 42. // Body length 61.
0x2a, 0x3d,
// OurOutputIndex tlv. // OurOutputIndex tlv.
0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0,
// TheirOutputIndex tlv. // TheirOutputIndex tlv.
@@ -96,19 +162,19 @@ var (
0x6e, 0x60, 0x29, 0x23, 0x1d, 0x5e, 0xc5, 0xe6, 0x6e, 0x60, 0x29, 0x23, 0x1d, 0x5e, 0xc5, 0xe6,
0xbd, 0xf7, 0xd3, 0x9b, 0x16, 0x7d, 0x0, 0xff, 0xbd, 0xf7, 0xd3, 0x9b, 0x16, 0x7d, 0x0, 0xff,
0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd, 0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd,
// Custom blob tlv.
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
} }
testRevocationLogWithAmts = RevocationLog{ testRevocationLogWithAmts = NewRevocationLog(
OurOutputIndex: 0, 0, 1, testChannelCommit.CommitTx.TxHash(),
TheirOutputIndex: 1, fn.Some(localBalance), fn.Some(remoteBalance),
CommitTxHash: testChannelCommit.CommitTx.TxHash(), []*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes),
HTLCEntries: []*HTLCEntry{&testHTLCEntry}, )
OurBalance: &localBalance,
TheirBalance: &remoteBalance,
}
testRevocationLogWithAmtsBytes = []byte{ testRevocationLogWithAmtsBytes = []byte{
// Body length 52. // Body length 71.
0x34, 0x47,
// OurOutputIndex tlv. // OurOutputIndex tlv.
0x0, 0x2, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0,
// TheirOutputIndex tlv. // TheirOutputIndex tlv.
@@ -123,6 +189,9 @@ var (
0x3, 0x3, 0xfd, 0x23, 0x28, 0x3, 0x3, 0xfd, 0x23, 0x28,
// Remote Balance. // Remote Balance.
0x4, 0x3, 0xfd, 0x0b, 0xb8, 0x4, 0x3, 0xfd, 0x0b, 0xb8,
// Custom blob tlv.
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
} }
) )
@@ -193,11 +262,6 @@ func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) {
// Copy the testHTLCEntry. // Copy the testHTLCEntry.
entry := 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. // Write the tlv stream.
buf := bytes.NewBuffer([]byte{}) buf := bytes.NewBuffer([]byte{})
err := serializeHTLCEntries(buf, []*HTLCEntry{&entry}) err := serializeHTLCEntries(buf, []*HTLCEntry{&entry})
@@ -207,6 +271,21 @@ func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) {
require.Equal(t, testHTLCEntryBytes, buf.Bytes()) require.Equal(t, testHTLCEntryBytes, buf.Bytes())
} }
func TestSerializeHTLCEntriesWithRHash(t *testing.T) {
t.Parallel()
// Copy the testHTLCEntry.
entry := testHTLCEntryHash
// 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, testHTLCEntryHashBytes, buf.Bytes())
}
func TestSerializeHTLCEntries(t *testing.T) { func TestSerializeHTLCEntries(t *testing.T) {
t.Parallel() t.Parallel()
@@ -215,7 +294,7 @@ func TestSerializeHTLCEntries(t *testing.T) {
// Create a fake rHash. // Create a fake rHash.
rHashBytes := bytes.Repeat([]byte{10}, 32) rHashBytes := bytes.Repeat([]byte{10}, 32)
copy(entry.RHash[:], rHashBytes) copy(entry.RHash.Val[:], rHashBytes)
// Construct the serialized bytes. // Construct the serialized bytes.
// //
@@ -224,7 +303,7 @@ func TestSerializeHTLCEntries(t *testing.T) {
partialBytes := testHTLCEntryBytes[3:] partialBytes := testHTLCEntryBytes[3:]
// Write the total length and RHash tlv. // Write the total length and RHash tlv.
expectedBytes := []byte{0x36, 0x0, 0x20} expectedBytes := []byte{0x4d, 0x0, 0x20}
expectedBytes = append(expectedBytes, rHashBytes...) expectedBytes = append(expectedBytes, rHashBytes...)
// Append the rest. // Append the rest.
@@ -269,7 +348,7 @@ func TestSerializeAndDeserializeRevLog(t *testing.T) {
t, &test.revLog, test.revLogBytes, t, &test.revLog, test.revLogBytes,
) )
testDerializeRevocationLog( testDeserializeRevocationLog(
t, &test.revLog, test.revLogBytes, t, &test.revLog, test.revLogBytes,
) )
}) })
@@ -293,7 +372,7 @@ func testSerializeRevocationLog(t *testing.T, rl *RevocationLog,
require.Equal(t, revLogBytes, buf.Bytes()[:bodyIndex]) require.Equal(t, revLogBytes, buf.Bytes()[:bodyIndex])
} }
func testDerializeRevocationLog(t *testing.T, revLog *RevocationLog, func testDeserializeRevocationLog(t *testing.T, revLog *RevocationLog,
revLogBytes []byte) { revLogBytes []byte) {
// Construct the full bytes. // Construct the full bytes.
@@ -309,7 +388,7 @@ func testDerializeRevocationLog(t *testing.T, revLog *RevocationLog,
require.Equal(t, *revLog, rl) require.Equal(t, *revLog, rl)
} }
func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) { func TestDeserializeHTLCEntriesEmptyRHash(t *testing.T) {
t.Parallel() t.Parallel()
// Read the tlv stream. // Read the tlv stream.
@@ -322,7 +401,7 @@ func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) {
require.Equal(t, &testHTLCEntry, htlcs[0]) require.Equal(t, &testHTLCEntry, htlcs[0])
} }
func TestDerializeHTLCEntries(t *testing.T) { func TestDeserializeHTLCEntries(t *testing.T) {
t.Parallel() t.Parallel()
// Copy the testHTLCEntry. // Copy the testHTLCEntry.
@@ -330,7 +409,7 @@ func TestDerializeHTLCEntries(t *testing.T) {
// Create a fake rHash. // Create a fake rHash.
rHashBytes := bytes.Repeat([]byte{10}, 32) rHashBytes := bytes.Repeat([]byte{10}, 32)
copy(entry.RHash[:], rHashBytes) copy(entry.RHash.Val[:], rHashBytes)
// Construct the serialized bytes. // Construct the serialized bytes.
// //
@@ -339,7 +418,7 @@ func TestDerializeHTLCEntries(t *testing.T) {
partialBytes := testHTLCEntryBytes[3:] partialBytes := testHTLCEntryBytes[3:]
// Write the total length and RHash tlv. // Write the total length and RHash tlv.
testBytes := append([]byte{0x36, 0x0, 0x20}, rHashBytes...) testBytes := append([]byte{0x4d, 0x0, 0x20}, rHashBytes...)
// Append the rest. // Append the rest.
testBytes = append(testBytes, partialBytes...) testBytes = append(testBytes, partialBytes...)
@@ -398,11 +477,11 @@ func TestDeleteLogBucket(t *testing.T) {
err = kvdb.Update(backend, func(tx kvdb.RwTx) error { err = kvdb.Update(backend, func(tx kvdb.RwTx) error {
// Create the buckets. // Create the buckets.
chanBucket, _, err := createTestRevocatoinLogBuckets(tx) chanBucket, _, err := createTestRevocationLogBuckets(tx)
require.NoError(t, err) require.NoError(t, err)
// Create the buckets again should give us an error. // Create the buckets again should give us an error.
_, _, err = createTestRevocatoinLogBuckets(tx) _, _, err = createTestRevocationLogBuckets(tx)
require.ErrorIs(t, err, kvdb.ErrBucketExists) require.ErrorIs(t, err, kvdb.ErrBucketExists)
// Delete both buckets. // Delete both buckets.
@@ -410,7 +489,7 @@ func TestDeleteLogBucket(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create the buckets again should give us NO error. // Create the buckets again should give us NO error.
_, _, err = createTestRevocatoinLogBuckets(tx) _, _, err = createTestRevocationLogBuckets(tx)
return err return err
}, func() {}) }, func() {})
require.NoError(t, err) require.NoError(t, err)
@@ -516,7 +595,7 @@ func TestPutRevocationLog(t *testing.T) {
// Construct the testing db transaction. // Construct the testing db transaction.
dbTx := func(tx kvdb.RwTx) (RevocationLog, error) { dbTx := func(tx kvdb.RwTx) (RevocationLog, error) {
// Create the buckets. // Create the buckets.
_, bucket, err := createTestRevocatoinLogBuckets(tx) _, bucket, err := createTestRevocationLogBuckets(tx)
require.NoError(t, err) require.NoError(t, err)
// Save the log. // Save the log.
@@ -686,7 +765,7 @@ func TestFetchRevocationLogCompatible(t *testing.T) {
} }
} }
func createTestRevocatoinLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket, func createTestRevocationLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket,
kvdb.RwBucket, error) { kvdb.RwBucket, error) {
chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket) chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket)
+24 -12
View File
@@ -105,7 +105,7 @@ type DatabaseBuilder interface {
type WalletConfigBuilder interface { type WalletConfigBuilder interface {
// BuildWalletConfig is responsible for creating or unlocking and then // BuildWalletConfig is responsible for creating or unlocking and then
// fully initializing a wallet. // fully initializing a wallet.
BuildWalletConfig(context.Context, *DatabaseInstances, BuildWalletConfig(context.Context, *DatabaseInstances, *AuxComponents,
*rpcperms.InterceptorChain, *rpcperms.InterceptorChain,
[]*ListenerWithSignal) (*chainreg.PartialChainControl, []*ListenerWithSignal) (*chainreg.PartialChainControl,
*btcwallet.Config, func(), error) *btcwallet.Config, func(), error)
@@ -120,14 +120,6 @@ type ChainControlBuilder interface {
*btcwallet.Config) (*chainreg.ChainControl, func(), error) *btcwallet.Config) (*chainreg.ChainControl, func(), error)
} }
// AuxComponents is a set of auxiliary components that can be used by lnd for
// certain custom channel types.
type AuxComponents struct {
// MsgRouter is an optional message router that if set will be used in
// place of a new blank default message router.
MsgRouter fn.Option[msgmux.Router]
}
// ImplementationCfg is a struct that holds all configuration items for // ImplementationCfg is a struct that holds all configuration items for
// components that can be implemented outside lnd itself. // components that can be implemented outside lnd itself.
type ImplementationCfg struct { type ImplementationCfg struct {
@@ -160,6 +152,18 @@ type ImplementationCfg struct {
AuxComponents AuxComponents
} }
// AuxComponents is a set of auxiliary components that can be used by lnd for
// certain custom channel types.
type AuxComponents struct {
// AuxLeafStore is an optional data source that can be used by custom
// channels to fetch+store various data.
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
// MsgRouter is an optional message router that if set will be used in
// place of a new blank default message router.
MsgRouter fn.Option[msgmux.Router]
}
// DefaultWalletImpl is the default implementation of our normal, btcwallet // DefaultWalletImpl is the default implementation of our normal, btcwallet
// backed configuration. // backed configuration.
type DefaultWalletImpl struct { type DefaultWalletImpl struct {
@@ -242,7 +246,8 @@ func (d *DefaultWalletImpl) Permissions() map[string][]bakery.Op {
// //
// NOTE: This is part of the WalletConfigBuilder interface. // NOTE: This is part of the WalletConfigBuilder interface.
func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context, func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
dbs *DatabaseInstances, interceptorChain *rpcperms.InterceptorChain, dbs *DatabaseInstances, aux *AuxComponents,
interceptorChain *rpcperms.InterceptorChain,
grpcListeners []*ListenerWithSignal) (*chainreg.PartialChainControl, grpcListeners []*ListenerWithSignal) (*chainreg.PartialChainControl,
*btcwallet.Config, func(), error) { *btcwallet.Config, func(), error) {
@@ -562,6 +567,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
HeightHintDB: dbs.HeightHintDB, HeightHintDB: dbs.HeightHintDB,
ChanStateDB: dbs.ChanStateDB.ChannelStateDB(), ChanStateDB: dbs.ChanStateDB.ChannelStateDB(),
NeutrinoCS: neutrinoCS, NeutrinoCS: neutrinoCS,
AuxLeafStore: aux.AuxLeafStore,
ActiveNetParams: d.cfg.ActiveNetParams, ActiveNetParams: d.cfg.ActiveNetParams,
FeeURL: d.cfg.FeeURL, FeeURL: d.cfg.FeeURL,
Fee: &lncfg.Fee{ Fee: &lncfg.Fee{
@@ -625,8 +631,9 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
// proxyBlockEpoch proxies a block epoch subsections to the underlying neutrino // proxyBlockEpoch proxies a block epoch subsections to the underlying neutrino
// rebroadcaster client. // rebroadcaster client.
func proxyBlockEpoch(notifier chainntnfs.ChainNotifier, func proxyBlockEpoch(
) func() (*blockntfns.Subscription, error) { notifier chainntnfs.ChainNotifier) func() (*blockntfns.Subscription,
error) {
return func() (*blockntfns.Subscription, error) { return func() (*blockntfns.Subscription, error) {
blockEpoch, err := notifier.RegisterBlockEpochNtfn( blockEpoch, err := notifier.RegisterBlockEpochNtfn(
@@ -717,6 +724,7 @@ func (d *DefaultWalletImpl) BuildChainControl(
ChainIO: walletController, ChainIO: walletController,
NetParams: *walletConfig.NetParams, NetParams: *walletConfig.NetParams,
CoinSelectionStrategy: walletConfig.CoinSelectionStrategy, CoinSelectionStrategy: walletConfig.CoinSelectionStrategy,
AuxLeafStore: partialChainControl.Cfg.AuxLeafStore,
} }
// The broadcast is already always active for neutrino nodes, so we // The broadcast is already always active for neutrino nodes, so we
@@ -899,6 +907,10 @@ type DatabaseInstances struct {
// for native SQL queries for tables that already support it. This may // for native SQL queries for tables that already support it. This may
// be nil if the use-native-sql flag was not set. // be nil if the use-native-sql flag was not set.
NativeSQLStore *sqldb.BaseDB NativeSQLStore *sqldb.BaseDB
// AuxLeafStore is an optional data source that can be used by custom
// channels to fetch+store various data.
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
} }
// DefaultDatabaseBuilder is a type that builds the default database backends // DefaultDatabaseBuilder is a type that builds the default database backends
+3
View File
@@ -22,6 +22,7 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntest/channels" "github.com/lightningnetwork/lnd/lntest/channels"
@@ -1590,6 +1591,7 @@ func testBreachSpends(t *testing.T, test breachTest) {
// Notify the breach arbiter about the breach. // Notify the breach arbiter about the breach.
retribution, err := lnwallet.NewBreachRetribution( retribution, err := lnwallet.NewBreachRetribution(
alice.State(), height, 1, forceCloseTx, alice.State(), height, 1, forceCloseTx,
fn.Some[lnwallet.AuxLeafStore](&lnwallet.MockAuxLeafStore{}),
) )
require.NoError(t, err, "unable to create breach retribution") require.NoError(t, err, "unable to create breach retribution")
@@ -1799,6 +1801,7 @@ func TestBreachDelayedJusticeConfirmation(t *testing.T) {
// Notify the breach arbiter about the breach. // Notify the breach arbiter about the breach.
retribution, err := lnwallet.NewBreachRetribution( retribution, err := lnwallet.NewBreachRetribution(
alice.State(), height, uint32(blockHeight), forceCloseTx, alice.State(), height, uint32(blockHeight), forceCloseTx,
fn.Some[lnwallet.AuxLeafStore](&lnwallet.MockAuxLeafStore{}),
) )
require.NoError(t, err, "unable to create breach retribution") require.NoError(t, err, "unable to create breach retribution")
+16 -2
View File
@@ -217,6 +217,10 @@ type ChainArbitratorConfig struct {
// meanwhile, turn `PaymentCircuit` into an interface or bring it to a // meanwhile, turn `PaymentCircuit` into an interface or bring it to a
// lower package. // lower package.
QueryIncomingCircuit func(circuit models.CircuitKey) *models.CircuitKey QueryIncomingCircuit func(circuit models.CircuitKey) *models.CircuitKey
// AuxLeafStore is an optional store that can be used to store auxiliary
// leaves for certain custom channel types.
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
} }
// ChainArbitrator is a sub-system that oversees the on-chain resolution of all // ChainArbitrator is a sub-system that oversees the on-chain resolution of all
@@ -299,8 +303,13 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
return nil, err return nil, err
} }
var chanOpts []lnwallet.ChannelOpt
a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
})
chanMachine, err := lnwallet.NewLightningChannel( chanMachine, err := lnwallet.NewLightningChannel(
a.c.cfg.Signer, channel, nil, a.c.cfg.Signer, channel, nil, chanOpts...,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -344,10 +353,15 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error)
return nil, err return nil, err
} }
var chanOpts []lnwallet.ChannelOpt
a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
})
// Finally, we'll force close the channel completing // Finally, we'll force close the channel completing
// the force close workflow. // the force close workflow.
chanMachine, err := lnwallet.NewLightningChannel( chanMachine, err := lnwallet.NewLightningChannel(
a.c.cfg.Signer, channel, nil, a.c.cfg.Signer, channel, nil, chanOpts...,
) )
if err != nil { if err != nil {
return nil, err return nil, err
+35 -5
View File
@@ -193,6 +193,9 @@ type chainWatcherConfig struct {
// obfuscater. This is used by the chain watcher to identify which // obfuscater. This is used by the chain watcher to identify which
// state was broadcast and confirmed on-chain. // state was broadcast and confirmed on-chain.
extractStateNumHint func(*wire.MsgTx, [lnwallet.StateHintSize]byte) uint64 extractStateNumHint func(*wire.MsgTx, [lnwallet.StateHintSize]byte) uint64
// auxLeafStore can be used to fetch information for custom channels.
auxLeafStore fn.Option[lnwallet.AuxLeafStore]
} }
// chainWatcher is a system that's assigned to every active channel. The duty // chainWatcher is a system that's assigned to every active channel. The duty
@@ -308,7 +311,7 @@ func (c *chainWatcher) Start() error {
) )
if chanState.ChanType.IsTaproot() { if chanState.ChanType.IsTaproot() {
c.fundingPkScript, _, err = input.GenTaprootFundingScript( c.fundingPkScript, _, err = input.GenTaprootFundingScript(
localKey, remoteKey, 0, localKey, remoteKey, 0, chanState.TapscriptRoot,
) )
if err != nil { if err != nil {
return err return err
@@ -423,15 +426,36 @@ func (c *chainWatcher) handleUnknownLocalState(
&c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg, &c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg,
) )
auxResult, err := fn.MapOptionZ(
c.cfg.auxLeafStore,
//nolint:lll
func(s lnwallet.AuxLeafStore) fn.Result[lnwallet.CommitDiffAuxResult] {
return s.FetchLeavesFromCommit(
lnwallet.NewAuxChanState(c.cfg.chanState),
c.cfg.chanState.LocalCommitment, *commitKeyRing,
)
},
).Unpack()
if err != nil {
return false, fmt.Errorf("unable to fetch aux leaves: %w", err)
}
// With the keys derived, we'll construct the remote script that'll be // With the keys derived, we'll construct the remote script that'll be
// present if they have a non-dust balance on the commitment. // present if they have a non-dust balance on the commitment.
var leaseExpiry uint32 var leaseExpiry uint32
if c.cfg.chanState.ChanType.HasLeaseExpiration() { if c.cfg.chanState.ChanType.HasLeaseExpiration() {
leaseExpiry = c.cfg.chanState.ThawHeight leaseExpiry = c.cfg.chanState.ThawHeight
} }
remoteAuxLeaf := fn.ChainOption(
func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
return l.RemoteAuxLeaf
},
)(auxResult.AuxLeaves)
remoteScript, _, err := lnwallet.CommitScriptToRemote( remoteScript, _, err := lnwallet.CommitScriptToRemote(
c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
commitKeyRing.ToRemoteKey, leaseExpiry, commitKeyRing.ToRemoteKey, leaseExpiry,
remoteAuxLeaf,
) )
if err != nil { if err != nil {
return false, err return false, err
@@ -440,10 +464,16 @@ func (c *chainWatcher) handleUnknownLocalState(
// Next, we'll derive our script that includes the revocation base for // Next, we'll derive our script that includes the revocation base for
// the remote party allowing them to claim this output before the CSV // the remote party allowing them to claim this output before the CSV
// delay if we breach. // delay if we breach.
localAuxLeaf := fn.ChainOption(
func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
return l.LocalAuxLeaf
},
)(auxResult.AuxLeaves)
localScript, err := lnwallet.CommitScriptToSelf( localScript, err := lnwallet.CommitScriptToSelf(
c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator, c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey, commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey,
uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry, uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry,
localAuxLeaf,
) )
if err != nil { if err != nil {
return false, err return false, err
@@ -866,7 +896,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail,
spendHeight := uint32(commitSpend.SpendingHeight) spendHeight := uint32(commitSpend.SpendingHeight)
retribution, err := lnwallet.NewBreachRetribution( retribution, err := lnwallet.NewBreachRetribution(
c.cfg.chanState, broadcastStateNum, spendHeight, c.cfg.chanState, broadcastStateNum, spendHeight,
commitSpend.SpendingTx, commitSpend.SpendingTx, c.cfg.auxLeafStore,
) )
switch { switch {
@@ -1116,8 +1146,8 @@ func (c *chainWatcher) dispatchLocalForceClose(
"detected", c.cfg.chanState.FundingOutpoint) "detected", c.cfg.chanState.FundingOutpoint)
forceClose, err := lnwallet.NewLocalForceCloseSummary( forceClose, err := lnwallet.NewLocalForceCloseSummary(
c.cfg.chanState, c.cfg.signer, c.cfg.chanState, c.cfg.signer, commitSpend.SpendingTx, stateNum,
commitSpend.SpendingTx, stateNum, c.cfg.auxLeafStore,
) )
if err != nil { if err != nil {
return err return err
@@ -1210,7 +1240,7 @@ func (c *chainWatcher) dispatchRemoteForceClose(
// channel on-chain. // channel on-chain.
uniClose, err := lnwallet.NewUnilateralCloseSummary( uniClose, err := lnwallet.NewUnilateralCloseSummary(
c.cfg.chanState, c.cfg.signer, commitSpend, c.cfg.chanState, c.cfg.signer, commitSpend,
remoteCommit, commitPoint, remoteCommit, commitPoint, c.cfg.auxLeafStore,
) )
if err != nil { if err != nil {
return err return err
+12 -1
View File
@@ -24,6 +24,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/discovery" "github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/graph" "github.com/lightningnetwork/lnd/graph"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@@ -544,6 +545,10 @@ type Config struct {
// backed funding flow to not use utxos still being swept by the sweeper // backed funding flow to not use utxos still being swept by the sweeper
// subsystem. // subsystem.
IsSweeperOutpoint func(wire.OutPoint) bool IsSweeperOutpoint func(wire.OutPoint) bool
// AuxLeafStore is an optional store that can be used to store auxiliary
// leaves for certain custom channel types.
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
} }
// Manager acts as an orchestrator/bridge between the wallet's // Manager acts as an orchestrator/bridge between the wallet's
@@ -1069,9 +1074,14 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
} }
} }
var chanOpts []lnwallet.ChannelOpt
f.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
})
// We create the state-machine object which wraps the database state. // We create the state-machine object which wraps the database state.
lnChannel, err := lnwallet.NewLightningChannel( lnChannel, err := lnwallet.NewLightningChannel(
nil, channel, nil, nil, channel, nil, chanOpts...,
) )
if err != nil { if err != nil {
log.Errorf("Unable to create LightningChannel(%v): %v", log.Errorf("Unable to create LightningChannel(%v): %v",
@@ -2899,6 +2909,7 @@ func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) {
if channel.ChanType.IsTaproot() { if channel.ChanType.IsTaproot() {
pkScript, _, err := input.GenTaprootFundingScript( pkScript, _, err := input.GenTaprootFundingScript(
localKey, remoteKey, int64(channel.Capacity), localKey, remoteKey, int64(channel.Capacity),
channel.TapscriptRoot,
) )
if err != nil { if err != nil {
return nil, err return nil, err
+5
View File
@@ -28,6 +28,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/channelnotifier" "github.com/lightningnetwork/lnd/channelnotifier"
"github.com/lightningnetwork/lnd/discovery" "github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg" "github.com/lightningnetwork/lnd/lncfg"
@@ -563,6 +564,9 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
IsSweeperOutpoint: func(wire.OutPoint) bool { IsSweeperOutpoint: func(wire.OutPoint) bool {
return false return false
}, },
AuxLeafStore: fn.Some[lnwallet.AuxLeafStore](
&lnwallet.MockAuxLeafStore{},
),
} }
for _, op := range options { for _, op := range options {
@@ -672,6 +676,7 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) {
OpenChannelPredicate: chainedAcceptor, OpenChannelPredicate: chainedAcceptor,
DeleteAliasEdge: oldCfg.DeleteAliasEdge, DeleteAliasEdge: oldCfg.DeleteAliasEdge,
AliasManager: oldCfg.AliasManager, AliasManager: oldCfg.AliasManager,
AuxLeafStore: oldCfg.AuxLeafStore,
}) })
require.NoError(t, err, "failed recreating aliceFundingManager") require.NoError(t, err, "failed recreating aliceFundingManager")
+5 -1
View File
@@ -11,12 +11,14 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/lightningnetwork/lnd/batch" "github.com/lightningnetwork/lnd/batch"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnutils"
@@ -1138,12 +1140,14 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte,
} }
fundingScript, _, err := input.GenTaprootFundingScript( fundingScript, _, err := input.GenTaprootFundingScript(
pubKey1, pubKey2, 0, pubKey1, pubKey2, 0, fn.None[chainhash.Hash](),
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
// TODO(roasbeef): add tapscript root to gossip v1.5
return fundingScript, nil return fundingScript, nil
} }
+94 -43
View File
@@ -11,8 +11,10 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnutils" "github.com/lightningnetwork/lnd/lnutils"
"golang.org/x/crypto/ripemd160" "golang.org/x/crypto/ripemd160"
@@ -199,26 +201,30 @@ func GenFundingPkScript(aPub, bPub []byte, amt int64) ([]byte, *wire.TxOut, erro
} }
// GenTaprootFundingScript constructs the taproot-native funding output that // GenTaprootFundingScript constructs the taproot-native funding output that
// uses musig2 to create a single aggregated key to anchor the channel. // uses MuSig2 to create a single aggregated key to anchor the channel.
func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey, func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey,
amt int64) ([]byte, *wire.TxOut, error) { amt int64, tapscriptRoot fn.Option[chainhash.Hash]) ([]byte,
*wire.TxOut, error) {
muSig2Opt := musig2.WithBIP86KeyTweak()
tapscriptRoot.WhenSome(func(scriptRoot chainhash.Hash) {
muSig2Opt = musig2.WithTaprootKeyTweak(scriptRoot[:])
})
// Similar to the existing p2wsh funding script, we'll always make sure // Similar to the existing p2wsh funding script, we'll always make sure
// we sort the keys before any major operations. In order to ensure // we sort the keys before any major operations. In order to ensure
// that there's no other way this output can be spent, we'll use a BIP // that there's no other way this output can be spent, we'll use a BIP
// 86 tweak here during aggregation. // 86 tweak here during aggregation, unless the user has explicitly
// // specified a tapscript root.
// TODO(roasbeef): revisit if BIP 86 is needed here?
combinedKey, _, _, err := musig2.AggregateKeys( combinedKey, _, _, err := musig2.AggregateKeys(
[]*btcec.PublicKey{aPub, bPub}, true, []*btcec.PublicKey{aPub, bPub}, true, muSig2Opt,
musig2.WithBIP86KeyTweak(),
) )
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("unable to combine keys: %w", err) return nil, nil, fmt.Errorf("unable to combine keys: %w", err)
} }
// Now that we have the combined key, we can create a taproot pkScript // Now that we have the combined key, we can create a taproot pkScript
// from this, and then make the txout given the amount. // from this, and then make the txOut given the amount.
pkScript, err := PayToTaprootScript(combinedKey.FinalKey) pkScript, err := PayToTaprootScript(combinedKey.FinalKey)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("unable to make taproot "+ return nil, nil, fmt.Errorf("unable to make taproot "+
@@ -228,7 +234,7 @@ func GenTaprootFundingScript(aPub, bPub *btcec.PublicKey,
txOut := wire.NewTxOut(amt, pkScript) txOut := wire.NewTxOut(amt, pkScript)
// For the "witness program" we just return the raw pkScript since the // For the "witness program" we just return the raw pkScript since the
// output we create can _only_ be spent with a musig2 signature. // output we create can _only_ be spent with a MuSig2 signature.
return pkScript, txOut, nil return pkScript, txOut, nil
} }
@@ -640,6 +646,13 @@ type HtlcScriptTree struct {
// TimeoutTapLeaf is the tapleaf for the timeout path. // TimeoutTapLeaf is the tapleaf for the timeout path.
TimeoutTapLeaf txscript.TapLeaf TimeoutTapLeaf txscript.TapLeaf
// AuxLeaf is an auxiliary leaf that can be used to extend the base
// HTLC script tree with new spend paths, or just as extra commitment
// space. When present, this leaf will always be in the right-most area
// of the tapscript tree.
AuxLeaf AuxTapLeaf
// htlcType is the type of HTLC script this is.
htlcType htlcType htlcType htlcType
} }
@@ -720,8 +733,8 @@ var _ TapscriptDescriptor = (*HtlcScriptTree)(nil)
// senderHtlcTapScriptTree builds the tapscript tree which is used to anchor // senderHtlcTapScriptTree builds the tapscript tree which is used to anchor
// the HTLC key for HTLCs on the sender's commitment. // the HTLC key for HTLCs on the sender's commitment.
func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
revokeKey *btcec.PublicKey, payHash []byte, revokeKey *btcec.PublicKey, payHash []byte, hType htlcType,
hType htlcType) (*HtlcScriptTree, error) { auxLeaf AuxTapLeaf) (*HtlcScriptTree, error) {
// First, we'll obtain the tap leaves for both the success and timeout // First, we'll obtain the tap leaves for both the success and timeout
// path. // path.
@@ -738,11 +751,14 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
return nil, err return nil, err
} }
tapLeaves := []txscript.TapLeaf{successTapLeaf, timeoutTapLeaf}
auxLeaf.WhenSome(func(l txscript.TapLeaf) {
tapLeaves = append(tapLeaves, l)
})
// With the two leaves obtained, we'll now make the tapscript tree, // With the two leaves obtained, we'll now make the tapscript tree,
// then obtain the root from that // then obtain the root from that
tapscriptTree := txscript.AssembleTaprootScriptTree( tapscriptTree := txscript.AssembleTaprootScriptTree(tapLeaves...)
successTapLeaf, timeoutTapLeaf,
)
tapScriptRoot := tapscriptTree.RootNode.TapHash() tapScriptRoot := tapscriptTree.RootNode.TapHash()
@@ -761,6 +777,7 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
}, },
SuccessTapLeaf: successTapLeaf, SuccessTapLeaf: successTapLeaf,
TimeoutTapLeaf: timeoutTapLeaf, TimeoutTapLeaf: timeoutTapLeaf,
AuxLeaf: auxLeaf,
htlcType: hType, htlcType: hType,
}, nil }, nil
} }
@@ -795,7 +812,8 @@ func senderHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
// unilaterally spend the created output. // unilaterally spend the created output.
func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey, func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey,
revokeKey *btcec.PublicKey, payHash []byte, revokeKey *btcec.PublicKey, payHash []byte,
whoseCommit lntypes.ChannelParty) (*HtlcScriptTree, error) { whoseCommit lntypes.ChannelParty, auxLeaf AuxTapLeaf) (*HtlcScriptTree,
error) {
var hType htlcType var hType htlcType
if whoseCommit.IsLocal() { if whoseCommit.IsLocal() {
@@ -808,8 +826,8 @@ func SenderHTLCScriptTaproot(senderHtlcKey, receiverHtlcKey,
// tree that includes the top level output script, as well as the two // tree that includes the top level output script, as well as the two
// tap leaf paths. // tap leaf paths.
return senderHtlcTapScriptTree( return senderHtlcTapScriptTree(
senderHtlcKey, receiverHtlcKey, revokeKey, payHash, senderHtlcKey, receiverHtlcKey, revokeKey, payHash, hType,
hType, auxLeaf,
) )
} }
@@ -1279,8 +1297,8 @@ func ReceiverHtlcTapLeafSuccess(receiverHtlcKey *btcec.PublicKey,
// receiverHtlcTapScriptTree builds the tapscript tree which is used to anchor // receiverHtlcTapScriptTree builds the tapscript tree which is used to anchor
// the HTLC key for HTLCs on the receiver's commitment. // the HTLC key for HTLCs on the receiver's commitment.
func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey, func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
revokeKey *btcec.PublicKey, payHash []byte, revokeKey *btcec.PublicKey, payHash []byte, cltvExpiry uint32,
cltvExpiry uint32, hType htlcType) (*HtlcScriptTree, error) { hType htlcType, auxLeaf AuxTapLeaf) (*HtlcScriptTree, error) {
// First, we'll obtain the tap leaves for both the success and timeout // First, we'll obtain the tap leaves for both the success and timeout
// path. // path.
@@ -1297,11 +1315,14 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
return nil, err return nil, err
} }
tapLeaves := []txscript.TapLeaf{timeoutTapLeaf, successTapLeaf}
auxLeaf.WhenSome(func(l txscript.TapLeaf) {
tapLeaves = append(tapLeaves, l)
})
// With the two leaves obtained, we'll now make the tapscript tree, // With the two leaves obtained, we'll now make the tapscript tree,
// then obtain the root from that // then obtain the root from that
tapscriptTree := txscript.AssembleTaprootScriptTree( tapscriptTree := txscript.AssembleTaprootScriptTree(tapLeaves...)
timeoutTapLeaf, successTapLeaf,
)
tapScriptRoot := tapscriptTree.RootNode.TapHash() tapScriptRoot := tapscriptTree.RootNode.TapHash()
@@ -1320,6 +1341,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
}, },
SuccessTapLeaf: successTapLeaf, SuccessTapLeaf: successTapLeaf,
TimeoutTapLeaf: timeoutTapLeaf, TimeoutTapLeaf: timeoutTapLeaf,
AuxLeaf: auxLeaf,
htlcType: hType, htlcType: hType,
}, nil }, nil
} }
@@ -1355,7 +1377,7 @@ func receiverHtlcTapScriptTree(senderHtlcKey, receiverHtlcKey,
func ReceiverHTLCScriptTaproot(cltvExpiry uint32, func ReceiverHTLCScriptTaproot(cltvExpiry uint32,
senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey, senderHtlcKey, receiverHtlcKey, revocationKey *btcec.PublicKey,
payHash []byte, whoseCommit lntypes.ChannelParty, payHash []byte, whoseCommit lntypes.ChannelParty,
) (*HtlcScriptTree, error) { auxLeaf AuxTapLeaf) (*HtlcScriptTree, error) {
var hType htlcType var hType htlcType
if whoseCommit.IsLocal() { if whoseCommit.IsLocal() {
@@ -1369,7 +1391,7 @@ func ReceiverHTLCScriptTaproot(cltvExpiry uint32,
// tap leaf paths. // tap leaf paths.
return receiverHtlcTapScriptTree( return receiverHtlcTapScriptTree(
senderHtlcKey, receiverHtlcKey, revocationKey, payHash, senderHtlcKey, receiverHtlcKey, revocationKey, payHash,
cltvExpiry, hType, cltvExpiry, hType, auxLeaf,
) )
} }
@@ -1598,9 +1620,9 @@ func TaprootSecondLevelTapLeaf(delayKey *btcec.PublicKey,
} }
// SecondLevelHtlcTapscriptTree construct the indexed tapscript tree needed to // SecondLevelHtlcTapscriptTree construct the indexed tapscript tree needed to
// generate the taptweak to create the final output and also control block. // generate the tap tweak to create the final output and also control block.
func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey, csvDelay uint32,
csvDelay uint32) (*txscript.IndexedTapScriptTree, error) { auxLeaf AuxTapLeaf) (*txscript.IndexedTapScriptTree, error) {
// First grab the second level leaf script we need to create the top // First grab the second level leaf script we need to create the top
// level output. // level output.
@@ -1609,9 +1631,14 @@ func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey,
return nil, err return nil, err
} }
tapLeaves := []txscript.TapLeaf{secondLevelTapLeaf}
auxLeaf.WhenSome(func(l txscript.TapLeaf) {
tapLeaves = append(tapLeaves, l)
})
// Now that we have the sole second level script, we can create the // Now that we have the sole second level script, we can create the
// tapscript tree that commits to both the leaves. // tapscript tree that commits to both the leaves.
return txscript.AssembleTaprootScriptTree(secondLevelTapLeaf), nil return txscript.AssembleTaprootScriptTree(tapLeaves...), nil
} }
// TaprootSecondLevelHtlcScript is the uniform script that's used as the output // TaprootSecondLevelHtlcScript is the uniform script that's used as the output
@@ -1631,12 +1658,12 @@ func SecondLevelHtlcTapscriptTree(delayKey *btcec.PublicKey,
// //
// The keyspend path require knowledge of the top level revocation private key. // The keyspend path require knowledge of the top level revocation private key.
func TaprootSecondLevelHtlcScript(revokeKey, delayKey *btcec.PublicKey, func TaprootSecondLevelHtlcScript(revokeKey, delayKey *btcec.PublicKey,
csvDelay uint32) (*btcec.PublicKey, error) { csvDelay uint32, auxLeaf AuxTapLeaf) (*btcec.PublicKey, error) {
// First, we'll make the tapscript tree that commits to the redemption // First, we'll make the tapscript tree that commits to the redemption
// path. // path.
tapScriptTree, err := SecondLevelHtlcTapscriptTree( tapScriptTree, err := SecondLevelHtlcTapscriptTree(
delayKey, csvDelay, delayKey, csvDelay, auxLeaf,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1661,17 +1688,21 @@ type SecondLevelScriptTree struct {
// SuccessTapLeaf is the tapleaf for the redemption path. // SuccessTapLeaf is the tapleaf for the redemption path.
SuccessTapLeaf txscript.TapLeaf SuccessTapLeaf txscript.TapLeaf
// AuxLeaf is an optional leaf that can be used to extend the script
// tree.
AuxLeaf AuxTapLeaf
} }
// TaprootSecondLevelScriptTree constructs the tapscript tree used to spend the // TaprootSecondLevelScriptTree constructs the tapscript tree used to spend the
// second level HTLC output. // second level HTLC output.
func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey, func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey,
csvDelay uint32) (*SecondLevelScriptTree, error) { csvDelay uint32, auxLeaf AuxTapLeaf) (*SecondLevelScriptTree, error) {
// First, we'll make the tapscript tree that commits to the redemption // First, we'll make the tapscript tree that commits to the redemption
// path. // path.
tapScriptTree, err := SecondLevelHtlcTapscriptTree( tapScriptTree, err := SecondLevelHtlcTapscriptTree(
delayKey, csvDelay, delayKey, csvDelay, auxLeaf,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1692,6 +1723,7 @@ func TaprootSecondLevelScriptTree(revokeKey, delayKey *btcec.PublicKey,
InternalKey: revokeKey, InternalKey: revokeKey,
}, },
SuccessTapLeaf: tapScriptTree.LeafMerkleProofs[0].TapLeaf, SuccessTapLeaf: tapScriptTree.LeafMerkleProofs[0].TapLeaf,
AuxLeaf: auxLeaf,
}, nil }, nil
} }
@@ -2073,6 +2105,12 @@ type CommitScriptTree struct {
// RevocationLeaf is the leaf used to spend the output with the // RevocationLeaf is the leaf used to spend the output with the
// revocation key signature. // revocation key signature.
RevocationLeaf txscript.TapLeaf RevocationLeaf txscript.TapLeaf
// AuxLeaf is an auxiliary leaf that can be used to extend the base
// commitment script tree with new spend paths, or just as extra
// commitment space. When present, this leaf will always be in the
// left-most or right-most area of the tapscript tree.
AuxLeaf AuxTapLeaf
} }
// A compile time check to ensure CommitScriptTree implements the // A compile time check to ensure CommitScriptTree implements the
@@ -2137,8 +2175,9 @@ func (c *CommitScriptTree) Tree() ScriptTree {
// NewLocalCommitScriptTree returns a new CommitScript tree that can be used to // NewLocalCommitScriptTree returns a new CommitScript tree that can be used to
// create and spend the commitment output for the local party. // create and spend the commitment output for the local party.
func NewLocalCommitScriptTree(csvTimeout uint32, func NewLocalCommitScriptTree(csvTimeout uint32, selfKey,
selfKey, revokeKey *btcec.PublicKey) (*CommitScriptTree, error) { revokeKey *btcec.PublicKey, auxLeaf AuxTapLeaf) (*CommitScriptTree,
error) {
// First, we'll need to construct the tapLeaf that'll be our delay CSV // First, we'll need to construct the tapLeaf that'll be our delay CSV
// clause. // clause.
@@ -2158,9 +2197,13 @@ func NewLocalCommitScriptTree(csvTimeout uint32,
// the two leaves, and then obtain a root from that. // the two leaves, and then obtain a root from that.
delayTapLeaf := txscript.NewBaseTapLeaf(delayScript) delayTapLeaf := txscript.NewBaseTapLeaf(delayScript)
revokeTapLeaf := txscript.NewBaseTapLeaf(revokeScript) revokeTapLeaf := txscript.NewBaseTapLeaf(revokeScript)
tapScriptTree := txscript.AssembleTaprootScriptTree(
delayTapLeaf, revokeTapLeaf, tapLeaves := []txscript.TapLeaf{delayTapLeaf, revokeTapLeaf}
) auxLeaf.WhenSome(func(l txscript.TapLeaf) {
tapLeaves = append(tapLeaves, l)
})
tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaves...)
tapScriptRoot := tapScriptTree.RootNode.TapHash() tapScriptRoot := tapScriptTree.RootNode.TapHash()
// Now that we have our root, we can arrive at the final output script // Now that we have our root, we can arrive at the final output script
@@ -2178,6 +2221,7 @@ func NewLocalCommitScriptTree(csvTimeout uint32,
}, },
SettleLeaf: delayTapLeaf, SettleLeaf: delayTapLeaf,
RevocationLeaf: revokeTapLeaf, RevocationLeaf: revokeTapLeaf,
AuxLeaf: auxLeaf,
}, nil }, nil
} }
@@ -2247,7 +2291,7 @@ func TaprootCommitScriptToSelf(csvTimeout uint32,
selfKey, revokeKey *btcec.PublicKey) (*btcec.PublicKey, error) { selfKey, revokeKey *btcec.PublicKey) (*btcec.PublicKey, error) {
commitScriptTree, err := NewLocalCommitScriptTree( commitScriptTree, err := NewLocalCommitScriptTree(
csvTimeout, selfKey, revokeKey, csvTimeout, selfKey, revokeKey, NoneTapLeaf(),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -2573,7 +2617,7 @@ func CommitScriptToRemoteConfirmed(key *btcec.PublicKey) ([]byte, error) {
// NewRemoteCommitScriptTree constructs a new script tree for the remote party // NewRemoteCommitScriptTree constructs a new script tree for the remote party
// to sweep their funds after a hard coded 1 block delay. // to sweep their funds after a hard coded 1 block delay.
func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey, func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey,
) (*CommitScriptTree, error) { auxLeaf AuxTapLeaf) (*CommitScriptTree, error) {
// First, construct the remote party's tapscript they'll use to sweep // First, construct the remote party's tapscript they'll use to sweep
// their outputs. // their outputs.
@@ -2589,10 +2633,16 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey,
return nil, err return nil, err
} }
tapLeaf := txscript.NewBaseTapLeaf(remoteScript)
tapLeaves := []txscript.TapLeaf{tapLeaf}
auxLeaf.WhenSome(func(l txscript.TapLeaf) {
tapLeaves = append(tapLeaves, l)
})
// With this script constructed, we'll map that into a tapLeaf, then // With this script constructed, we'll map that into a tapLeaf, then
// make a new tapscript root from that. // make a new tapscript root from that.
tapLeaf := txscript.NewBaseTapLeaf(remoteScript) tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaves...)
tapScriptTree := txscript.AssembleTaprootScriptTree(tapLeaf)
tapScriptRoot := tapScriptTree.RootNode.TapHash() tapScriptRoot := tapScriptTree.RootNode.TapHash()
// Now that we have our root, we can arrive at the final output script // Now that we have our root, we can arrive at the final output script
@@ -2609,6 +2659,7 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey,
InternalKey: &TaprootNUMSKey, InternalKey: &TaprootNUMSKey,
}, },
SettleLeaf: tapLeaf, SettleLeaf: tapLeaf,
AuxLeaf: auxLeaf,
}, nil }, nil
} }
@@ -2625,9 +2676,9 @@ func NewRemoteCommitScriptTree(remoteKey *btcec.PublicKey,
// <remotepubkey> OP_CHECKSIG // <remotepubkey> OP_CHECKSIG
// 1 OP_CHECKSEQUENCEVERIFY OP_DROP // 1 OP_CHECKSEQUENCEVERIFY OP_DROP
func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey, func TaprootCommitScriptToRemote(remoteKey *btcec.PublicKey,
) (*btcec.PublicKey, error) { auxLeaf AuxTapLeaf) (*btcec.PublicKey, error) {
commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey) commitScriptTree, err := NewRemoteCommitScriptTree(remoteKey, auxLeaf)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+15 -10
View File
@@ -853,7 +853,7 @@ var witnessSizeTests = []witnessSizeTest{
signer := &dummySigner{} signer := &dummySigner{}
commitScriptTree, err := input.NewLocalCommitScriptTree( commitScriptTree, err := input.NewLocalCommitScriptTree(
testCSVDelay, testKey.PubKey(), testCSVDelay, testKey.PubKey(),
testKey.PubKey(), testKey.PubKey(), input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -887,7 +887,7 @@ var witnessSizeTests = []witnessSizeTest{
signer := &dummySigner{} signer := &dummySigner{}
commitScriptTree, err := input.NewLocalCommitScriptTree( commitScriptTree, err := input.NewLocalCommitScriptTree(
testCSVDelay, testKey.PubKey(), testCSVDelay, testKey.PubKey(),
testKey.PubKey(), testKey.PubKey(), input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -921,7 +921,7 @@ var witnessSizeTests = []witnessSizeTest{
signer := &dummySigner{} signer := &dummySigner{}
//nolint:lll //nolint:lll
commitScriptTree, err := input.NewRemoteCommitScriptTree( commitScriptTree, err := input.NewRemoteCommitScriptTree(
testKey.PubKey(), testKey.PubKey(), input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -988,6 +988,7 @@ var witnessSizeTests = []witnessSizeTest{
scriptTree, err := input.SecondLevelHtlcTapscriptTree( scriptTree, err := input.SecondLevelHtlcTapscriptTree(
testKey.PubKey(), testCSVDelay, testKey.PubKey(), testCSVDelay,
input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1027,6 +1028,7 @@ var witnessSizeTests = []witnessSizeTest{
scriptTree, err := input.SecondLevelHtlcTapscriptTree( scriptTree, err := input.SecondLevelHtlcTapscriptTree(
testKey.PubKey(), testCSVDelay, testKey.PubKey(), testCSVDelay,
input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1075,6 +1077,7 @@ var witnessSizeTests = []witnessSizeTest{
htlcScriptTree, err := input.SenderHTLCScriptTaproot( htlcScriptTree, err := input.SenderHTLCScriptTaproot(
senderKey.PubKey(), receiverKey.PubKey(), senderKey.PubKey(), receiverKey.PubKey(),
revokeKey.PubKey(), payHash[:], lntypes.Remote, revokeKey.PubKey(), payHash[:], lntypes.Remote,
input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1116,7 +1119,7 @@ var witnessSizeTests = []witnessSizeTest{
htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( htlcScriptTree, err := input.ReceiverHTLCScriptTaproot(
testCLTVExpiry, senderKey.PubKey(), testCLTVExpiry, senderKey.PubKey(),
receiverKey.PubKey(), revokeKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(),
payHash[:], lntypes.Remote, payHash[:], lntypes.Remote, input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1158,7 +1161,7 @@ var witnessSizeTests = []witnessSizeTest{
htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( htlcScriptTree, err := input.ReceiverHTLCScriptTaproot(
testCLTVExpiry, senderKey.PubKey(), testCLTVExpiry, senderKey.PubKey(),
receiverKey.PubKey(), revokeKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(),
payHash[:], lntypes.Remote, payHash[:], lntypes.Remote, input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1205,6 +1208,7 @@ var witnessSizeTests = []witnessSizeTest{
htlcScriptTree, err := input.SenderHTLCScriptTaproot( htlcScriptTree, err := input.SenderHTLCScriptTaproot(
senderKey.PubKey(), receiverKey.PubKey(), senderKey.PubKey(), receiverKey.PubKey(),
revokeKey.PubKey(), payHash[:], lntypes.Remote, revokeKey.PubKey(), payHash[:], lntypes.Remote,
input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1265,6 +1269,7 @@ var witnessSizeTests = []witnessSizeTest{
htlcScriptTree, err := input.SenderHTLCScriptTaproot( htlcScriptTree, err := input.SenderHTLCScriptTaproot(
senderKey.PubKey(), receiverKey.PubKey(), senderKey.PubKey(), receiverKey.PubKey(),
revokeKey.PubKey(), payHash[:], lntypes.Remote, revokeKey.PubKey(), payHash[:], lntypes.Remote,
input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1310,7 +1315,7 @@ var witnessSizeTests = []witnessSizeTest{
htlcScriptTree, err := input.ReceiverHTLCScriptTaproot( htlcScriptTree, err := input.ReceiverHTLCScriptTaproot(
testCLTVExpiry, senderKey.PubKey(), testCLTVExpiry, senderKey.PubKey(),
receiverKey.PubKey(), revokeKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(),
payHash[:], lntypes.Remote, payHash[:], lntypes.Remote, input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1383,7 +1388,7 @@ func genTimeoutTx(t *testing.T,
// Create the unsigned timeout tx. // Create the unsigned timeout tx.
timeoutTx, err := lnwallet.CreateHtlcTimeoutTx( timeoutTx, err := lnwallet.CreateHtlcTimeoutTx(
chanType, false, testOutPoint, testAmt, testCLTVExpiry, chanType, false, testOutPoint, testAmt, testCLTVExpiry,
testCSVDelay, 0, testPubkey, testPubkey, testCSVDelay, 0, testPubkey, testPubkey, input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1396,7 +1401,7 @@ func genTimeoutTx(t *testing.T,
if chanType.IsTaproot() { if chanType.IsTaproot() {
tapscriptTree, err = input.SenderHTLCScriptTaproot( tapscriptTree, err = input.SenderHTLCScriptTaproot(
testPubkey, testPubkey, testPubkey, testHash160, testPubkey, testPubkey, testPubkey, testHash160,
lntypes.Remote, lntypes.Remote, input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1452,7 +1457,7 @@ func genSuccessTx(t *testing.T, chanType channeldb.ChannelType) *wire.MsgTx {
// Create the unsigned success tx. // Create the unsigned success tx.
successTx, err := lnwallet.CreateHtlcSuccessTx( successTx, err := lnwallet.CreateHtlcSuccessTx(
chanType, false, testOutPoint, testAmt, testCSVDelay, 0, chanType, false, testOutPoint, testAmt, testCSVDelay, 0,
testPubkey, testPubkey, testPubkey, testPubkey, input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -1465,7 +1470,7 @@ func genSuccessTx(t *testing.T, chanType channeldb.ChannelType) *wire.MsgTx {
if chanType.IsTaproot() { if chanType.IsTaproot() {
tapscriptTree, err = input.ReceiverHTLCScriptTaproot( tapscriptTree, err = input.ReceiverHTLCScriptTaproot(
testCLTVExpiry, testPubkey, testPubkey, testPubkey, testCLTVExpiry, testPubkey, testPubkey, testPubkey,
testHash160, lntypes.Remote, testHash160, lntypes.Remote, input.NoneTapLeaf(),
) )
require.NoError(t, err) require.NoError(t, err)
+28
View File
@@ -8,6 +8,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcwallet/waddrmgr" "github.com/btcsuite/btcwallet/waddrmgr"
"github.com/lightningnetwork/lnd/fn"
) )
const ( const (
@@ -21,6 +22,33 @@ const (
PubKeyFormatCompressedOdd byte = 0x03 PubKeyFormatCompressedOdd byte = 0x03
) )
// AuxTapLeaf is a type alias for an optional tapscript leaf that may be added
// to the tapscript tree of HTLC and commitment outputs.
type AuxTapLeaf = fn.Option[txscript.TapLeaf]
// NoneTapLeaf returns an empty optional tapscript leaf.
func NoneTapLeaf() AuxTapLeaf {
return fn.None[txscript.TapLeaf]()
}
// HtlcIndex represents the monotonically increasing counter that is used to
// identify HTLCs created by a peer.
type HtlcIndex = uint64
// HtlcAuxLeaf is a type that represents an auxiliary leaf for an HTLC output.
// An HTLC may have up to two aux leaves: one for the output on the commitment
// transaction, and one for the second level HTLC.
type HtlcAuxLeaf struct {
AuxTapLeaf
// SecondLevelLeaf is the auxiliary leaf for the second level HTLC
// success or timeout transaction.
SecondLevelLeaf AuxTapLeaf
}
// HtlcAuxLeaves is a type alias for a map of optional tapscript leaves.
type HtlcAuxLeaves = map[HtlcIndex]HtlcAuxLeaf
// NewTxSigHashesV0Only returns a new txscript.TxSigHashes instance that will // NewTxSigHashesV0Only returns a new txscript.TxSigHashes instance that will
// only calculate the sighash midstate values for segwit v0 inputs and can // only calculate the sighash midstate values for segwit v0 inputs and can
// therefore never be used for transactions that want to spend segwit v1 // therefore never be used for transactions that want to spend segwit v1
+137 -52
View File
@@ -1,13 +1,16 @@
package input package input
import ( import (
"bytes"
"crypto/rand" "crypto/rand"
"fmt"
"testing" "testing"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@@ -31,7 +34,9 @@ type testSenderHtlcScriptTree struct {
htlcAmt int64 htlcAmt int64
} }
func newTestSenderHtlcScriptTree(t *testing.T) *testSenderHtlcScriptTree { func newTestSenderHtlcScriptTree(t *testing.T,
auxLeaf AuxTapLeaf) *testSenderHtlcScriptTree {
var preImage lntypes.Preimage var preImage lntypes.Preimage
_, err := rand.Read(preImage[:]) _, err := rand.Read(preImage[:])
require.NoError(t, err) require.NoError(t, err)
@@ -48,7 +53,7 @@ func newTestSenderHtlcScriptTree(t *testing.T) *testSenderHtlcScriptTree {
payHash := preImage.Hash() payHash := preImage.Hash()
htlcScriptTree, err := SenderHTLCScriptTaproot( htlcScriptTree, err := SenderHTLCScriptTaproot(
senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(), senderKey.PubKey(), receiverKey.PubKey(), revokeKey.PubKey(),
payHash[:], lntypes.Remote, payHash[:], lntypes.Remote, auxLeaf,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -207,13 +212,9 @@ func htlcSenderTimeoutWitnessGen(sigHash txscript.SigHashType,
} }
} }
// TestTaprootSenderHtlcSpend tests that all the positive and negative paths func testTaprootSenderHtlcSpend(t *testing.T, auxLeaf AuxTapLeaf) {
// for the sender HTLC tapscript tree work as expected.
func TestTaprootSenderHtlcSpend(t *testing.T) {
t.Parallel()
// First, create a new test script tree. // First, create a new test script tree.
htlcScriptTree := newTestSenderHtlcScriptTree(t) htlcScriptTree := newTestSenderHtlcScriptTree(t, auxLeaf)
spendTx := wire.NewMsgTx(2) spendTx := wire.NewMsgTx(2)
spendTx.AddTxIn(&wire.TxIn{}) spendTx.AddTxIn(&wire.TxIn{})
@@ -432,6 +433,26 @@ func TestTaprootSenderHtlcSpend(t *testing.T) {
} }
} }
// TestTaprootSenderHtlcSpend tests that all the positive and negative paths
// for the sender HTLC tapscript tree work as expected.
func TestTaprootSenderHtlcSpend(t *testing.T) {
t.Parallel()
for _, hasAuxLeaf := range []bool{true, false} {
name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf)
t.Run(name, func(t *testing.T) {
var auxLeaf AuxTapLeaf
if hasAuxLeaf {
auxLeaf = fn.Some(txscript.NewBaseTapLeaf(
bytes.Repeat([]byte{0x01}, 32),
))
}
testTaprootSenderHtlcSpend(t, auxLeaf)
})
}
}
type testReceiverHtlcScriptTree struct { type testReceiverHtlcScriptTree struct {
preImage lntypes.Preimage preImage lntypes.Preimage
@@ -452,7 +473,9 @@ type testReceiverHtlcScriptTree struct {
lockTime int32 lockTime int32
} }
func newTestReceiverHtlcScriptTree(t *testing.T) *testReceiverHtlcScriptTree { func newTestReceiverHtlcScriptTree(t *testing.T,
auxLeaf AuxTapLeaf) *testReceiverHtlcScriptTree {
var preImage lntypes.Preimage var preImage lntypes.Preimage
_, err := rand.Read(preImage[:]) _, err := rand.Read(preImage[:])
require.NoError(t, err) require.NoError(t, err)
@@ -471,7 +494,7 @@ func newTestReceiverHtlcScriptTree(t *testing.T) *testReceiverHtlcScriptTree {
payHash := preImage.Hash() payHash := preImage.Hash()
htlcScriptTree, err := ReceiverHTLCScriptTaproot( htlcScriptTree, err := ReceiverHTLCScriptTaproot(
cltvExpiry, senderKey.PubKey(), receiverKey.PubKey(), cltvExpiry, senderKey.PubKey(), receiverKey.PubKey(),
revokeKey.PubKey(), payHash[:], lntypes.Remote, revokeKey.PubKey(), payHash[:], lntypes.Remote, auxLeaf,
) )
require.NoError(t, err) require.NoError(t, err)
@@ -629,15 +652,11 @@ func htlcReceiverSuccessWitnessGen(sigHash txscript.SigHashType,
} }
} }
// TestTaprootReceiverHtlcSpend tests that all possible paths for redeeming an func testTaprootReceiverHtlcSpend(t *testing.T, auxLeaf AuxTapLeaf) {
// accepted HTLC (on the commitment transaction) of the receiver work properly.
func TestTaprootReceiverHtlcSpend(t *testing.T) {
t.Parallel()
// We'll start by creating the HTLC script tree (contains all 3 valid // We'll start by creating the HTLC script tree (contains all 3 valid
// spend paths), and also a mock spend transaction that we'll be // spend paths), and also a mock spend transaction that we'll be
// signing below. // signing below.
htlcScriptTree := newTestReceiverHtlcScriptTree(t) htlcScriptTree := newTestReceiverHtlcScriptTree(t, auxLeaf)
// TODO(roasbeef): issue with revoke key??? ctrl block even/odd // TODO(roasbeef): issue with revoke key??? ctrl block even/odd
@@ -891,6 +910,28 @@ func TestTaprootReceiverHtlcSpend(t *testing.T) {
} }
} }
// TestTaprootReceiverHtlcSpend tests that all possible paths for redeeming an
// accepted HTLC (on the commitment transaction) of the receiver work properly.
func TestTaprootReceiverHtlcSpend(t *testing.T) {
t.Parallel()
for _, hasAuxLeaf := range []bool{true, false} {
name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf)
t.Run(name, func(t *testing.T) {
var auxLeaf AuxTapLeaf
if hasAuxLeaf {
auxLeaf = fn.Some(
txscript.NewBaseTapLeaf(
bytes.Repeat([]byte{0x01}, 32),
),
)
}
testTaprootReceiverHtlcSpend(t, auxLeaf)
})
}
}
type testCommitScriptTree struct { type testCommitScriptTree struct {
csvDelay uint32 csvDelay uint32
@@ -905,7 +946,9 @@ type testCommitScriptTree struct {
*CommitScriptTree *CommitScriptTree
} }
func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) { func newTestCommitScriptTree(local bool,
auxLeaf AuxTapLeaf) (*testCommitScriptTree, error) {
selfKey, err := btcec.NewPrivateKey() selfKey, err := btcec.NewPrivateKey()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -925,10 +968,11 @@ func newTestCommitScriptTree(local bool) (*testCommitScriptTree, error) {
if local { if local {
commitScriptTree, err = NewLocalCommitScriptTree( commitScriptTree, err = NewLocalCommitScriptTree(
csvDelay, selfKey.PubKey(), revokeKey.PubKey(), csvDelay, selfKey.PubKey(), revokeKey.PubKey(),
auxLeaf,
) )
} else { } else {
commitScriptTree, err = NewRemoteCommitScriptTree( commitScriptTree, err = NewRemoteCommitScriptTree(
selfKey.PubKey(), selfKey.PubKey(), auxLeaf,
) )
} }
if err != nil { if err != nil {
@@ -1020,12 +1064,8 @@ func localCommitRevokeWitGen(sigHash txscript.SigHashType,
} }
} }
// TestTaprootCommitScriptToSelf tests that the taproot script for redeeming func testTaprootCommitScriptToSelf(t *testing.T, auxLeaf AuxTapLeaf) {
// one's output after a force close behaves as expected. commitScriptTree, err := newTestCommitScriptTree(true, auxLeaf)
func TestTaprootCommitScriptToSelf(t *testing.T) {
t.Parallel()
commitScriptTree, err := newTestCommitScriptTree(true)
require.NoError(t, err) require.NoError(t, err)
spendTx := wire.NewMsgTx(2) spendTx := wire.NewMsgTx(2)
@@ -1187,6 +1227,26 @@ func TestTaprootCommitScriptToSelf(t *testing.T) {
} }
} }
// TestTaprootCommitScriptToSelf tests that the taproot script for redeeming
// one's output after a force close behaves as expected.
func TestTaprootCommitScriptToSelf(t *testing.T) {
t.Parallel()
for _, hasAuxLeaf := range []bool{true, false} {
name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf)
t.Run(name, func(t *testing.T) {
var auxLeaf AuxTapLeaf
if hasAuxLeaf {
auxLeaf = fn.Some(txscript.NewBaseTapLeaf(
bytes.Repeat([]byte{0x01}, 32),
))
}
testTaprootCommitScriptToSelf(t, auxLeaf)
})
}
}
func remoteCommitSweepWitGen(sigHash txscript.SigHashType, func remoteCommitSweepWitGen(sigHash txscript.SigHashType,
commitScriptTree *testCommitScriptTree) witnessGen { commitScriptTree *testCommitScriptTree) witnessGen {
@@ -1220,12 +1280,8 @@ func remoteCommitSweepWitGen(sigHash txscript.SigHashType,
} }
} }
// TestTaprootCommitScriptRemote tests that the remote party can properly sweep func testTaprootCommitScriptRemote(t *testing.T, auxLeaf AuxTapLeaf) {
// their output after force close. commitScriptTree, err := newTestCommitScriptTree(false, auxLeaf)
func TestTaprootCommitScriptRemote(t *testing.T) {
t.Parallel()
commitScriptTree, err := newTestCommitScriptTree(false)
require.NoError(t, err) require.NoError(t, err)
spendTx := wire.NewMsgTx(2) spendTx := wire.NewMsgTx(2)
@@ -1364,6 +1420,26 @@ func TestTaprootCommitScriptRemote(t *testing.T) {
} }
} }
// TestTaprootCommitScriptRemote tests that the remote party can properly sweep
// their output after force close.
func TestTaprootCommitScriptRemote(t *testing.T) {
t.Parallel()
for _, hasAuxLeaf := range []bool{true, false} {
name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf)
t.Run(name, func(t *testing.T) {
var auxLeaf AuxTapLeaf
if hasAuxLeaf {
auxLeaf = fn.Some(txscript.NewBaseTapLeaf(
bytes.Repeat([]byte{0x01}, 32),
))
}
testTaprootCommitScriptRemote(t, auxLeaf)
})
}
}
type testAnchorScriptTree struct { type testAnchorScriptTree struct {
sweepKey *btcec.PrivateKey sweepKey *btcec.PrivateKey
@@ -1599,25 +1675,21 @@ type testSecondLevelHtlcTree struct {
tapScriptRoot []byte tapScriptRoot []byte
} }
func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) { func newTestSecondLevelHtlcTree(t *testing.T,
auxLeaf AuxTapLeaf) *testSecondLevelHtlcTree {
delayKey, err := btcec.NewPrivateKey() delayKey, err := btcec.NewPrivateKey()
if err != nil { require.NoError(t, err)
return nil, err
}
revokeKey, err := btcec.NewPrivateKey() revokeKey, err := btcec.NewPrivateKey()
if err != nil { require.NoError(t, err)
return nil, err
}
const csvDelay = 6 const csvDelay = 6
scriptTree, err := SecondLevelHtlcTapscriptTree( scriptTree, err := SecondLevelHtlcTapscriptTree(
delayKey.PubKey(), csvDelay, delayKey.PubKey(), csvDelay, auxLeaf,
) )
if err != nil { require.NoError(t, err)
return nil, err
}
tapScriptRoot := scriptTree.RootNode.TapHash() tapScriptRoot := scriptTree.RootNode.TapHash()
@@ -1626,9 +1698,7 @@ func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) {
) )
pkScript, err := PayToTaprootScript(htlcKey) pkScript, err := PayToTaprootScript(htlcKey)
if err != nil { require.NoError(t, err)
return nil, err
}
const amt = 100 const amt = 100
@@ -1643,7 +1713,7 @@ func newTestSecondLevelHtlcTree() (*testSecondLevelHtlcTree, error) {
amt: amt, amt: amt,
scriptTree: scriptTree, scriptTree: scriptTree,
tapScriptRoot: tapScriptRoot[:], tapScriptRoot: tapScriptRoot[:],
}, nil }
} }
func secondLevelHtlcSuccessWitGen(sigHash txscript.SigHashType, func secondLevelHtlcSuccessWitGen(sigHash txscript.SigHashType,
@@ -1713,13 +1783,8 @@ func secondLevelHtlcRevokeWitnessgen(sigHash txscript.SigHashType,
} }
} }
// TestTaprootSecondLevelHtlcScript tests that a channel peer can properly func testTaprootSecondLevelHtlcScript(t *testing.T, auxLeaf AuxTapLeaf) {
// spend the second level HTLC script to resolve HTLCs. htlcScriptTree := newTestSecondLevelHtlcTree(t, auxLeaf)
func TestTaprootSecondLevelHtlcScript(t *testing.T) {
t.Parallel()
htlcScriptTree, err := newTestSecondLevelHtlcTree()
require.NoError(t, err)
spendTx := wire.NewMsgTx(2) spendTx := wire.NewMsgTx(2)
spendTx.AddTxIn(&wire.TxIn{}) spendTx.AddTxIn(&wire.TxIn{})
@@ -1879,3 +1944,23 @@ func TestTaprootSecondLevelHtlcScript(t *testing.T) {
}) })
} }
} }
// TestTaprootSecondLevelHtlcScript tests that a channel peer can properly
// spend the second level HTLC script to resolve HTLCs.
func TestTaprootSecondLevelHtlcScript(t *testing.T) {
t.Parallel()
for _, hasAuxLeaf := range []bool{true, false} {
name := fmt.Sprintf("aux_leaf=%v", hasAuxLeaf)
t.Run(name, func(t *testing.T) {
var auxLeaf AuxTapLeaf
if hasAuxLeaf {
auxLeaf = fn.Some(txscript.NewBaseTapLeaf(
bytes.Repeat([]byte{0x01}, 32),
))
}
testTaprootSecondLevelHtlcScript(t, auxLeaf)
})
}
}
+2
View File
@@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/chainreg" "github.com/lightningnetwork/lnd/chainreg"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/funding" "github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/labels" "github.com/lightningnetwork/lnd/labels"
@@ -1192,6 +1193,7 @@ func deriveFundingShim(ht *lntest.HarnessTest, carol, dave *node.HarnessNode,
_, fundingOutput, err = input.GenTaprootFundingScript( _, fundingOutput, err = input.GenTaprootFundingScript(
carolKey, daveKey, int64(chanSize), carolKey, daveKey, int64(chanSize),
fn.None[chainhash.Hash](),
) )
require.NoError(ht, err) require.NoError(ht, err)
+2 -1
View File
@@ -456,7 +456,8 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
defer cleanUp() defer cleanUp()
partialChainControl, walletConfig, cleanUp, err := implCfg.BuildWalletConfig( partialChainControl, walletConfig, cleanUp, err := implCfg.BuildWalletConfig(
ctx, dbs, interceptorChain, grpcListeners, ctx, dbs, &implCfg.AuxComponents, interceptorChain,
grpcListeners,
) )
if err != nil { if err != nil {
return mkErr("error creating wallet config: %v", err) return mkErr("error creating wallet config: %v", err)
+239
View File
@@ -0,0 +1,239 @@
package lnwallet
import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
// CommitSortFunc is a function type alias for a function that sorts the
// commitment transaction outputs. The second parameter is a list of CLTV
// timeouts that must correspond to the number of transaction outputs, with the
// value of 0 for non-HTLC outputs. The HTLC indexes are needed to have a
// deterministic sort value for HTLCs that have the identical amount, CLTV
// timeout and payment hash (e.g. multiple MPP shards of the same payment, where
// the on-chain script would be identical).
type CommitSortFunc func(tx *wire.MsgTx, cltvs []uint32,
indexes []input.HtlcIndex) error
// DefaultCommitSort is the default commitment sort function that sorts the
// commitment transaction inputs and outputs according to BIP69. The second
// parameter is a list of CLTV timeouts that must correspond to the number of
// transaction outputs, with the value of 0 for non-HTLC outputs. The third
// parameter is unused for the default sort function.
func DefaultCommitSort(tx *wire.MsgTx, cltvs []uint32,
_ []input.HtlcIndex) error {
InPlaceCommitSort(tx, cltvs)
return nil
}
// CommitAuxLeaves stores two potential auxiliary leaves for the remote and
// local output that may be used to augment the final tapscript trees of the
// commitment transaction.
type CommitAuxLeaves struct {
// LocalAuxLeaf is the local party's auxiliary leaf.
LocalAuxLeaf input.AuxTapLeaf
// RemoteAuxLeaf is the remote party's auxiliary leaf.
RemoteAuxLeaf input.AuxTapLeaf
// OutgoingHTLCLeaves is the set of aux leaves for the outgoing HTLCs
// on this commitment transaction.
OutgoingHtlcLeaves input.HtlcAuxLeaves
// IncomingHTLCLeaves is the set of aux leaves for the incoming HTLCs
// on this commitment transaction.
IncomingHtlcLeaves input.HtlcAuxLeaves
}
// AuxChanState is a struct that holds certain fields of the
// channeldb.OpenChannel struct that are used by the aux components. The data
// is copied over to prevent accidental mutation of the original channel state.
type AuxChanState struct {
// ChanType denotes which type of channel this is.
ChanType channeldb.ChannelType
// FundingOutpoint is the outpoint of the final funding transaction.
// This value uniquely and globally identifies the channel within the
// target blockchain as specified by the chain hash parameter.
FundingOutpoint wire.OutPoint
// ShortChannelID encodes the exact location in the chain in which the
// channel was initially confirmed. This includes: the block height,
// transaction index, and the output within the target transaction.
//
// If IsZeroConf(), then this will the "base" (very first) ALIAS scid
// and the confirmed SCID will be stored in ConfirmedScid.
ShortChannelID lnwire.ShortChannelID
// IsInitiator is a bool which indicates if we were the original
// initiator for the channel. This value may affect how higher levels
// negotiate fees, or close the channel.
IsInitiator bool
// Capacity is the total capacity of this channel.
Capacity btcutil.Amount
// LocalChanCfg is the channel configuration for the local node.
LocalChanCfg channeldb.ChannelConfig
// RemoteChanCfg is the channel configuration for the remote node.
RemoteChanCfg channeldb.ChannelConfig
// 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
// TapscriptRoot is an optional tapscript root used to derive the MuSig2
// funding output.
TapscriptRoot fn.Option[chainhash.Hash]
// CustomBlob is an optional blob that can be used to store information
// specific to a custom channel type. This information is only created
// at channel funding time, and after wards is to be considered
// immutable.
CustomBlob fn.Option[tlv.Blob]
}
// NewAuxChanState creates a new AuxChanState from the given channel state.
func NewAuxChanState(chanState *channeldb.OpenChannel) AuxChanState {
return AuxChanState{
ChanType: chanState.ChanType,
FundingOutpoint: chanState.FundingOutpoint,
ShortChannelID: chanState.ShortChannelID,
IsInitiator: chanState.IsInitiator,
Capacity: chanState.Capacity,
LocalChanCfg: chanState.LocalChanCfg,
RemoteChanCfg: chanState.RemoteChanCfg,
ThawHeight: chanState.ThawHeight,
TapscriptRoot: chanState.TapscriptRoot,
CustomBlob: chanState.CustomBlob,
}
}
// CommitDiffAuxInput is the input required to compute the diff of the auxiliary
// leaves for a commitment transaction.
type CommitDiffAuxInput struct {
// ChannelState is the static channel information of the channel this
// commitment transaction relates to.
ChannelState AuxChanState
// PrevBlob is the blob of the previous commitment transaction.
PrevBlob tlv.Blob
// UnfilteredView is the unfiltered, original HTLC view of the channel.
// Unfiltered in this context means that the view contains all HTLCs,
// including the canceled ones.
UnfilteredView *HtlcView
// WhoseCommit denotes whose commitment transaction we are computing the
// diff for.
WhoseCommit lntypes.ChannelParty
// OurBalance is the balance of the local party.
OurBalance lnwire.MilliSatoshi
// TheirBalance is the balance of the remote party.
TheirBalance lnwire.MilliSatoshi
// KeyRing is the key ring that can be used to derive keys for the
// commitment transaction.
KeyRing CommitmentKeyRing
}
// CommitDiffAuxResult is the result of computing the diff of the auxiliary
// leaves for a commitment transaction.
type CommitDiffAuxResult struct {
// AuxLeaves are the auxiliary leaves for the new commitment
// transaction.
AuxLeaves fn.Option[CommitAuxLeaves]
// CommitSortFunc is an optional function that sorts the commitment
// transaction inputs and outputs.
CommitSortFunc fn.Option[CommitSortFunc]
}
// AuxLeafStore is used to optionally fetch auxiliary tapscript leaves for the
// commitment transaction given an opaque blob. This is also used to implement
// a state transition function for the blobs to allow them to be refreshed with
// each state.
type AuxLeafStore interface {
// FetchLeavesFromView attempts to fetch the auxiliary leaves that
// correspond to the passed aux blob, and pending original (unfiltered)
// HTLC view.
FetchLeavesFromView(
in CommitDiffAuxInput) fn.Result[CommitDiffAuxResult]
// FetchLeavesFromCommit attempts to fetch the auxiliary leaves that
// correspond to the passed aux blob, and an existing channel
// commitment.
FetchLeavesFromCommit(chanState AuxChanState,
commit channeldb.ChannelCommitment,
keyRing CommitmentKeyRing) fn.Result[CommitDiffAuxResult]
// FetchLeavesFromRevocation attempts to fetch the auxiliary leaves
// from a channel revocation that stores balance + blob information.
FetchLeavesFromRevocation(
r *channeldb.RevocationLog) fn.Result[CommitDiffAuxResult]
// ApplyHtlcView serves as the state transition function for the custom
// channel's blob. Given the old blob, and an HTLC view, then a new
// blob should be returned that reflects the pending updates.
ApplyHtlcView(in CommitDiffAuxInput) fn.Result[fn.Option[tlv.Blob]]
}
// auxLeavesFromView is used to derive the set of commit aux leaves (if any),
// that are needed to create a new commitment transaction using the original
// (unfiltered) htlc view.
func auxLeavesFromView(leafStore AuxLeafStore, chanState *channeldb.OpenChannel,
prevBlob fn.Option[tlv.Blob], originalView *HtlcView,
whoseCommit lntypes.ChannelParty, ourBalance,
theirBalance lnwire.MilliSatoshi,
keyRing CommitmentKeyRing) fn.Result[CommitDiffAuxResult] {
return fn.MapOptionZ(
prevBlob, func(blob tlv.Blob) fn.Result[CommitDiffAuxResult] {
return leafStore.FetchLeavesFromView(CommitDiffAuxInput{
ChannelState: NewAuxChanState(chanState),
PrevBlob: blob,
UnfilteredView: originalView,
WhoseCommit: whoseCommit,
OurBalance: ourBalance,
TheirBalance: theirBalance,
KeyRing: keyRing,
})
},
)
}
// updateAuxBlob is a helper function that attempts to update the aux blob
// given the prior and current state information.
func updateAuxBlob(leafStore AuxLeafStore, chanState *channeldb.OpenChannel,
prevBlob fn.Option[tlv.Blob], nextViewUnfiltered *HtlcView,
whoseCommit lntypes.ChannelParty, ourBalance,
theirBalance lnwire.MilliSatoshi,
keyRing CommitmentKeyRing) fn.Result[fn.Option[tlv.Blob]] {
return fn.MapOptionZ(
prevBlob, func(blob tlv.Blob) fn.Result[fn.Option[tlv.Blob]] {
return leafStore.ApplyHtlcView(CommitDiffAuxInput{
ChannelState: NewAuxChanState(chanState),
PrevBlob: blob,
UnfilteredView: nextViewUnfiltered,
WhoseCommit: whoseCommit,
OurBalance: ourBalance,
TheirBalance: theirBalance,
KeyRing: keyRing,
})
},
)
}
+5 -2
View File
@@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
@@ -178,8 +179,9 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
} }
func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount, func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
localScript, remoteScript []byte, _ ...lnwallet.ChanCloseOpt, localScript, remoteScript []byte,
) (input.Signature, *chainhash.Hash, btcutil.Amount, error) { _ ...lnwallet.ChanCloseOpt) (input.Signature, *chainhash.Hash,
btcutil.Amount, error) {
if m.chanType.IsTaproot() { if m.chanType.IsTaproot() {
return lnwallet.NewMusigPartialSig( return lnwallet.NewMusigPartialSig(
@@ -188,6 +190,7 @@ func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
R: new(btcec.PublicKey), R: new(btcec.PublicKey),
}, },
lnwire.Musig2Nonce{}, lnwire.Musig2Nonce{}, nil, lnwire.Musig2Nonce{}, lnwire.Musig2Nonce{}, nil,
fn.None[chainhash.Hash](),
), nil, 0, nil ), nil, 0, nil
} }
+12 -3
View File
@@ -5,7 +5,9 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
) )
@@ -56,6 +58,14 @@ type ShimIntent struct {
// generate an aggregate key to use as the taproot-native multi-sig // generate an aggregate key to use as the taproot-native multi-sig
// output. // output.
musig2 bool musig2 bool
// tapscriptRoot is the root of the tapscript tree that will be used to
// create the funding output. This field will only be utilized if the
// MuSig2 flag above is set to true.
//
// TODO(roasbeef): fold above into new chan type? sum type like thing,
// includes the tapscript root, etc
tapscriptRoot fn.Option[chainhash.Hash]
} }
// FundingOutput returns the witness script, and the output that creates the // FundingOutput returns the witness script, and the output that creates the
@@ -76,9 +86,8 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) {
// Similar to the existing p2wsh script, we'll always ensure // Similar to the existing p2wsh script, we'll always ensure
// the keys are sorted before use. // the keys are sorted before use.
return input.GenTaprootFundingScript( return input.GenTaprootFundingScript(
s.localKey.PubKey, s.localKey.PubKey, s.remoteKey, int64(totalAmt),
s.remoteKey, s.tapscriptRoot,
int64(totalAmt),
) )
} }
+1
View File
@@ -534,6 +534,7 @@ func (p *PsbtAssembler) ProvisionChannel(req *Request) (Intent, error) {
ShimIntent: ShimIntent{ ShimIntent: ShimIntent{
localFundingAmt: p.fundingAmt, localFundingAmt: p.fundingAmt,
musig2: req.Musig2, musig2: req.Musig2,
tapscriptRoot: req.TapscriptRoot,
}, },
State: PsbtShimRegistered, State: PsbtShimRegistered,
BasePsbt: p.basePsbt, BasePsbt: p.basePsbt,
+1 -1
View File
@@ -394,7 +394,6 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
// we will call the specialized coin selection function for // we will call the specialized coin selection function for
// that. // that.
case r.FundUpToMaxAmt != 0 && r.MinFundAmt != 0: case r.FundUpToMaxAmt != 0 && r.MinFundAmt != 0:
// We need to ensure that manually selected coins, which // We need to ensure that manually selected coins, which
// are spent entirely on the channel funding, leave // are spent entirely on the channel funding, leave
// enough funds in the wallet to cover for a reserve. // enough funds in the wallet to cover for a reserve.
@@ -539,6 +538,7 @@ func (w *WalletAssembler) ProvisionChannel(r *Request) (Intent, error) {
localFundingAmt: localContributionAmt, localFundingAmt: localContributionAmt,
remoteFundingAmt: r.RemoteAmt, remoteFundingAmt: r.RemoteAmt,
musig2: r.Musig2, musig2: r.Musig2,
tapscriptRoot: r.TapscriptRoot,
}, },
InputCoins: selectedCoins, InputCoins: selectedCoins,
coinLeaser: w.cfg.CoinLeaser, coinLeaser: w.cfg.CoinLeaser,
+505 -168
View File
File diff suppressed because it is too large. Load diff
+92 -42
View File
@@ -2,6 +2,7 @@ package lnwallet
import ( import (
"bytes" "bytes"
crand "crypto/rand"
"crypto/sha256" "crypto/sha256"
"fmt" "fmt"
"math/rand" "math/rand"
@@ -386,6 +387,12 @@ func TestSimpleAddSettleWorkflow(t *testing.T) {
) )
}) })
t.Run("taproot with tapscript root", func(t *testing.T) {
flags := channeldb.SimpleTaprootFeatureBit |
channeldb.TapscriptRootBit
testAddSettleWorkflow(t, true, flags, false)
})
t.Run("storeFinalHtlcResolutions=true", func(t *testing.T) { t.Run("storeFinalHtlcResolutions=true", func(t *testing.T) {
testAddSettleWorkflow(t, false, 0, true) testAddSettleWorkflow(t, false, 0, true)
}) })
@@ -828,6 +835,16 @@ func TestForceClose(t *testing.T) {
anchorAmt: AnchorSize * 2, anchorAmt: AnchorSize * 2,
}) })
}) })
t.Run("taproot with tapscript root", func(t *testing.T) {
testForceClose(t, &forceCloseTestCase{
chanType: channeldb.SingleFunderTweaklessBit |
channeldb.AnchorOutputsBit |
channeldb.SimpleTaprootFeatureBit |
channeldb.TapscriptRootBit,
expectedCommitWeight: input.TaprootCommitWeight,
anchorAmt: AnchorSize * 2,
})
})
} }
type forceCloseTestCase struct { type forceCloseTestCase struct {
@@ -5678,6 +5695,7 @@ func TestChannelUnilateralCloseHtlcResolution(t *testing.T) {
spendDetail, spendDetail,
aliceChannel.channelState.RemoteCommitment, aliceChannel.channelState.RemoteCommitment,
aliceChannel.channelState.RemoteCurrentRevocation, aliceChannel.channelState.RemoteCurrentRevocation,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.NoError(t, err, "unable to create alice close summary") require.NoError(t, err, "unable to create alice close summary")
@@ -5827,6 +5845,7 @@ func TestChannelUnilateralClosePendingCommit(t *testing.T) {
spendDetail, spendDetail,
aliceChannel.channelState.RemoteCommitment, aliceChannel.channelState.RemoteCommitment,
aliceChannel.channelState.RemoteCurrentRevocation, aliceChannel.channelState.RemoteCurrentRevocation,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.NoError(t, err, "unable to create alice close summary") require.NoError(t, err, "unable to create alice close summary")
@@ -5844,6 +5863,7 @@ func TestChannelUnilateralClosePendingCommit(t *testing.T) {
spendDetail, spendDetail,
aliceRemoteChainTip.Commitment, aliceRemoteChainTip.Commitment,
aliceChannel.channelState.RemoteNextRevocation, aliceChannel.channelState.RemoteNextRevocation,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.NoError(t, err, "unable to create alice close summary") require.NoError(t, err, "unable to create alice close summary")
@@ -6724,6 +6744,7 @@ func TestNewBreachRetributionSkipsDustHtlcs(t *testing.T) {
breachTx := aliceChannel.channelState.RemoteCommitment.CommitTx breachTx := aliceChannel.channelState.RemoteCommitment.CommitTx
breachRet, err := NewBreachRetribution( breachRet, err := NewBreachRetribution(
aliceChannel.channelState, revokedStateNum, 100, breachTx, aliceChannel.channelState, revokedStateNum, 100, breachTx,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.NoError(t, err, "unable to create breach retribution") require.NoError(t, err, "unable to create breach retribution")
@@ -8525,10 +8546,10 @@ func TestEvaluateView(t *testing.T) {
} }
} }
view := &htlcView{ view := &HtlcView{
ourUpdates: test.ourHtlcs, OurUpdates: test.ourHtlcs,
theirUpdates: test.theirHtlcs, TheirUpdates: test.theirHtlcs,
feePerKw: feePerKw, FeePerKw: feePerKw,
} }
var ( var (
@@ -8549,17 +8570,17 @@ func TestEvaluateView(t *testing.T) {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if result.feePerKw != test.expectedFee { if result.FeePerKw != test.expectedFee {
t.Fatalf("expected fee: %v, got: %v", t.Fatalf("expected fee: %v, got: %v",
test.expectedFee, result.feePerKw) test.expectedFee, result.FeePerKw)
} }
checkExpectedHtlcs( checkExpectedHtlcs(
t, result.ourUpdates, test.ourExpectedHtlcs, t, result.OurUpdates, test.ourExpectedHtlcs,
) )
checkExpectedHtlcs( checkExpectedHtlcs(
t, result.theirUpdates, test.theirExpectedHtlcs, t, result.TheirUpdates, test.theirExpectedHtlcs,
) )
if lc.channelState.TotalMSatSent != test.expectSent { if lc.channelState.TotalMSatSent != test.expectSent {
@@ -8782,15 +8803,15 @@ func TestProcessFeeUpdate(t *testing.T) {
EntryType: FeeUpdate, EntryType: FeeUpdate,
} }
view := &htlcView{ view := &HtlcView{
feePerKw: chainfee.SatPerKWeight(feePerKw), FeePerKw: chainfee.SatPerKWeight(feePerKw),
} }
processFeeUpdate( processFeeUpdate(
update, nextHeight, test.whoseCommitChain, update, nextHeight, test.whoseCommitChain,
test.mutate, view, test.mutate, view,
) )
if view.feePerKw != test.expectedFee { if view.FeePerKw != test.expectedFee {
t.Fatalf("expected fee: %v, got: %v", t.Fatalf("expected fee: %v, got: %v",
test.expectedFee, feePerKw) test.expectedFee, feePerKw)
} }
@@ -9940,15 +9961,17 @@ func TestCreateHtlcRetribution(t *testing.T) {
aliceChannel.channelState, aliceChannel.channelState,
) )
htlc := &channeldb.HTLCEntry{ htlc := &channeldb.HTLCEntry{
Amt: testAmt, Amt: tlv.NewRecordT[tlv.TlvType4](
Incoming: true, tlv.NewBigSizeT(testAmt),
OutputIndex: 1, ),
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](true),
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16](1),
} }
// Create the htlc retribution. // Create the htlc retribution.
hr, err := createHtlcRetribution( hr, err := createHtlcRetribution(
aliceChannel.channelState, keyRing, commitHash, aliceChannel.channelState, keyRing, commitHash,
dummyPrivate, leaseExpiry, htlc, dummyPrivate, leaseExpiry, htlc, fn.None[CommitAuxLeaves](),
) )
// Expect no error. // Expect no error.
require.NoError(t, err) require.NoError(t, err)
@@ -9956,8 +9979,8 @@ func TestCreateHtlcRetribution(t *testing.T) {
// Check the fields have expected values. // Check the fields have expected values.
require.EqualValues(t, testAmt, hr.SignDesc.Output.Value) require.EqualValues(t, testAmt, hr.SignDesc.Output.Value)
require.Equal(t, commitHash, hr.OutPoint.Hash) require.Equal(t, commitHash, hr.OutPoint.Hash)
require.EqualValues(t, htlc.OutputIndex, hr.OutPoint.Index) require.EqualValues(t, htlc.OutputIndex.Val, hr.OutPoint.Index)
require.Equal(t, htlc.Incoming, hr.IsIncoming) require.Equal(t, htlc.Incoming.Val, hr.IsIncoming)
} }
// TestCreateBreachRetribution checks that `createBreachRetribution` behaves as // TestCreateBreachRetribution checks that `createBreachRetribution` behaves as
@@ -9997,30 +10020,31 @@ func TestCreateBreachRetribution(t *testing.T) {
aliceChannel.channelState, aliceChannel.channelState,
) )
htlc := &channeldb.HTLCEntry{ htlc := &channeldb.HTLCEntry{
Amt: btcutil.Amount(testAmt), Amt: tlv.NewRecordT[tlv.TlvType4](
Incoming: true, tlv.NewBigSizeT(btcutil.Amount(testAmt)),
OutputIndex: uint16(htlcIndex), ),
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](true),
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2](
uint16(htlcIndex),
),
} }
// Create a dummy revocation log. // Create a dummy revocation log.
ourAmtMsat := lnwire.MilliSatoshi(ourAmt * 1000) ourAmtMsat := lnwire.MilliSatoshi(ourAmt * 1000)
theirAmtMsat := lnwire.MilliSatoshi(theirAmt * 1000) theirAmtMsat := lnwire.MilliSatoshi(theirAmt * 1000)
revokedLog := channeldb.RevocationLog{ revokedLog := channeldb.NewRevocationLog(
CommitTxHash: commitHash, uint16(localIndex), uint16(remoteIndex), commitHash,
OurOutputIndex: uint16(localIndex), fn.Some(ourAmtMsat), fn.Some(theirAmtMsat),
TheirOutputIndex: uint16(remoteIndex), []*channeldb.HTLCEntry{htlc}, fn.None[tlv.Blob](),
HTLCEntries: []*channeldb.HTLCEntry{htlc}, )
TheirBalance: &theirAmtMsat,
OurBalance: &ourAmtMsat,
}
// Create a log with an empty local output index. // Create a log with an empty local output index.
revokedLogNoLocal := revokedLog revokedLogNoLocal := revokedLog
revokedLogNoLocal.OurOutputIndex = channeldb.OutputIndexEmpty revokedLogNoLocal.OurOutputIndex.Val = channeldb.OutputIndexEmpty
// Create a log with an empty remote output index. // Create a log with an empty remote output index.
revokedLogNoRemote := revokedLog revokedLogNoRemote := revokedLog
revokedLogNoRemote.TheirOutputIndex = channeldb.OutputIndexEmpty revokedLogNoRemote.TheirOutputIndex.Val = channeldb.OutputIndexEmpty
testCases := []struct { testCases := []struct {
name string name string
@@ -10050,14 +10074,20 @@ func TestCreateBreachRetribution(t *testing.T) {
{ {
name: "fail due to our index too big", name: "fail due to our index too big",
revocationLog: &channeldb.RevocationLog{ revocationLog: &channeldb.RevocationLog{
OurOutputIndex: uint16(htlcIndex + 1), //nolint:lll
OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0](
uint16(htlcIndex + 1),
),
}, },
expectedErr: ErrOutputIndexOutOfRange, expectedErr: ErrOutputIndexOutOfRange,
}, },
{ {
name: "fail due to their index too big", name: "fail due to their index too big",
revocationLog: &channeldb.RevocationLog{ revocationLog: &channeldb.RevocationLog{
TheirOutputIndex: uint16(htlcIndex + 1), //nolint:lll
TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1](
uint16(htlcIndex + 1),
),
}, },
expectedErr: ErrOutputIndexOutOfRange, expectedErr: ErrOutputIndexOutOfRange,
}, },
@@ -10126,11 +10156,12 @@ func TestCreateBreachRetribution(t *testing.T) {
require.Equal(t, remote, br.RemoteOutpoint) require.Equal(t, remote, br.RemoteOutpoint)
for _, hr := range br.HtlcRetributions { for _, hr := range br.HtlcRetributions {
require.EqualValues(t, testAmt, require.EqualValues(
hr.SignDesc.Output.Value) t, testAmt, hr.SignDesc.Output.Value,
)
require.Equal(t, commitHash, hr.OutPoint.Hash) require.Equal(t, commitHash, hr.OutPoint.Hash)
require.EqualValues(t, htlcIndex, hr.OutPoint.Index) require.EqualValues(t, htlcIndex, hr.OutPoint.Index)
require.Equal(t, htlc.Incoming, hr.IsIncoming) require.Equal(t, htlc.Incoming.Val, hr.IsIncoming)
} }
} }
@@ -10146,6 +10177,7 @@ func TestCreateBreachRetribution(t *testing.T) {
tc.revocationLog, tx, tc.revocationLog, tx,
aliceChannel.channelState, keyRing, aliceChannel.channelState, keyRing,
dummyPrivate, leaseExpiry, dummyPrivate, leaseExpiry,
fn.None[CommitAuxLeaves](),
) )
// Check the error if expected. // Check the error if expected.
@@ -10264,6 +10296,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) {
// error as there are no past delta state saved as revocation logs yet. // error as there are no past delta state saved as revocation logs yet.
_, err = NewBreachRetribution( _, err = NewBreachRetribution(
aliceChannel.channelState, stateNum, breachHeight, breachTx, aliceChannel.channelState, stateNum, breachHeight, breachTx,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.ErrorIs(t, err, channeldb.ErrNoPastDeltas) require.ErrorIs(t, err, channeldb.ErrNoPastDeltas)
@@ -10271,6 +10304,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) {
// provided. // provided.
_, err = NewBreachRetribution( _, err = NewBreachRetribution(
aliceChannel.channelState, stateNum, breachHeight, nil, aliceChannel.channelState, stateNum, breachHeight, nil,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.ErrorIs(t, err, channeldb.ErrNoPastDeltas) require.ErrorIs(t, err, channeldb.ErrNoPastDeltas)
@@ -10316,6 +10350,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) {
// successfully. // successfully.
br, err := NewBreachRetribution( br, err := NewBreachRetribution(
aliceChannel.channelState, stateNum, breachHeight, breachTx, aliceChannel.channelState, stateNum, breachHeight, breachTx,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -10327,6 +10362,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) {
// since the necessary info should now be found in the revocation log. // since the necessary info should now be found in the revocation log.
br, err = NewBreachRetribution( br, err = NewBreachRetribution(
aliceChannel.channelState, stateNum, breachHeight, nil, aliceChannel.channelState, stateNum, breachHeight, nil,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.NoError(t, err) require.NoError(t, err)
assertRetribution(br, 1, 0) assertRetribution(br, 1, 0)
@@ -10335,6 +10371,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) {
// error. // error.
_, err = NewBreachRetribution( _, err = NewBreachRetribution(
aliceChannel.channelState, stateNum+1, breachHeight, breachTx, aliceChannel.channelState, stateNum+1, breachHeight, breachTx,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound) require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound)
@@ -10342,6 +10379,7 @@ func testNewBreachRetribution(t *testing.T, chanType channeldb.ChannelType) {
// provided. // provided.
_, err = NewBreachRetribution( _, err = NewBreachRetribution(
aliceChannel.channelState, stateNum+1, breachHeight, nil, aliceChannel.channelState, stateNum+1, breachHeight, nil,
fn.Some[AuxLeafStore](&MockAuxLeafStore{}),
) )
require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound) require.ErrorIs(t, err, channeldb.ErrLogEntryNotFound)
} }
@@ -10379,7 +10417,8 @@ func TestExtractPayDescs(t *testing.T) {
// NOTE: we use nil commitment key rings to avoid checking the htlc // NOTE: we use nil commitment key rings to avoid checking the htlc
// scripts(`genHtlcScript`) as it should be tested independently. // scripts(`genHtlcScript`) as it should be tested independently.
incomingPDs, outgoingPDs, err := lnChan.extractPayDescs( incomingPDs, outgoingPDs, err := lnChan.extractPayDescs(
0, htlcs, nil, nil, lntypes.Local, 0, htlcs, lntypes.Dual[*CommitmentKeyRing]{}, lntypes.Local,
fn.None[CommitAuxLeaves](),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -10415,19 +10454,26 @@ func assertPayDescMatchHTLC(t *testing.T, pd PaymentDescriptor,
// the `Incoming`. // the `Incoming`.
func createRandomHTLC(t *testing.T, incoming bool) channeldb.HTLC { func createRandomHTLC(t *testing.T, incoming bool) channeldb.HTLC {
var onionBlob [lnwire.OnionPacketSize]byte var onionBlob [lnwire.OnionPacketSize]byte
_, err := rand.Read(onionBlob[:]) _, err := crand.Read(onionBlob[:])
require.NoError(t, err) require.NoError(t, err)
var rHash [lntypes.HashSize]byte var rHash [lntypes.HashSize]byte
_, err = rand.Read(rHash[:]) _, err = crand.Read(rHash[:])
require.NoError(t, err) require.NoError(t, err)
sig := make([]byte, 64) sig := make([]byte, 64)
_, err = rand.Read(sig) _, err = crand.Read(sig)
require.NoError(t, err) require.NoError(t, err)
randCustomData := make([]byte, 32)
_, err = crand.Read(randCustomData)
require.NoError(t, err)
randCustomType := rand.Intn(255) + lnwire.MinCustomRecordsTlvType
blinding, err := pubkeyFromHex( blinding, err := pubkeyFromHex(
"0228f2af0abe322403480fb3ee172f7f1601e67d1da6cad40b54c4468d48236c39", //nolint:lll "0228f2af0abe322403480fb3ee172f7f1601e67d1da6cad40b54c4468d48" +
"236c39",
) )
require.NoError(t, err) require.NoError(t, err)
@@ -10442,9 +10488,13 @@ func createRandomHTLC(t *testing.T, incoming bool) channeldb.HTLC {
HtlcIndex: rand.Uint64(), HtlcIndex: rand.Uint64(),
LogIndex: rand.Uint64(), LogIndex: rand.Uint64(),
BlindingPoint: tlv.SomeRecordT( BlindingPoint: tlv.SomeRecordT(
//nolint:lll tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](blinding), blinding,
),
), ),
CustomRecords: map[uint64][]byte{
uint64(randCustomType): randCustomData,
},
} }
} }
+153 -51
View File
@@ -11,6 +11,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -226,8 +227,7 @@ func (w *WitnessScriptDesc) WitnessScriptForPath(
// the settled funds in the channel, plus the unsettled funds. // the settled funds in the channel, plus the unsettled funds.
func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool, func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool,
selfKey, revokeKey *btcec.PublicKey, csvDelay, leaseExpiry uint32, selfKey, revokeKey *btcec.PublicKey, csvDelay, leaseExpiry uint32,
) ( auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) {
input.ScriptDescriptor, error) {
switch { switch {
// For taproot scripts, we'll need to make a slightly modified script // For taproot scripts, we'll need to make a slightly modified script
@@ -237,7 +237,7 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool,
// Our "redeem" script here is just the taproot witness program. // Our "redeem" script here is just the taproot witness program.
case chanType.IsTaproot(): case chanType.IsTaproot():
return input.NewLocalCommitScriptTree( return input.NewLocalCommitScriptTree(
csvDelay, selfKey, revokeKey, csvDelay, selfKey, revokeKey, auxLeaf,
) )
// If we are the initiator of a leased channel, then we have an // If we are the initiator of a leased channel, then we have an
@@ -291,8 +291,8 @@ func CommitScriptToSelf(chanType channeldb.ChannelType, initiator bool,
// script for. The second return value is the CSV delay of the output script, // script for. The second return value is the CSV delay of the output script,
// what must be satisfied in order to spend the output. // what must be satisfied in order to spend the output.
func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool, func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool,
remoteKey *btcec.PublicKey, remoteKey *btcec.PublicKey, leaseExpiry uint32,
leaseExpiry uint32) (input.ScriptDescriptor, uint32, error) { auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, uint32, error) {
switch { switch {
// If we are not the initiator of a leased channel, then the remote // If we are not the initiator of a leased channel, then the remote
@@ -321,7 +321,7 @@ func CommitScriptToRemote(chanType channeldb.ChannelType, initiator bool,
// with the sole tap leaf enforcing the 1 CSV delay. // with the sole tap leaf enforcing the 1 CSV delay.
case chanType.IsTaproot(): case chanType.IsTaproot():
toRemoteScriptTree, err := input.NewRemoteCommitScriptTree( toRemoteScriptTree, err := input.NewRemoteCommitScriptTree(
remoteKey, remoteKey, auxLeaf,
) )
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@@ -420,14 +420,14 @@ func sweepSigHash(chanType channeldb.ChannelType) txscript.SigHashType {
// argument should correspond to the owner of the commitment transaction which // argument should correspond to the owner of the commitment transaction which
// we are generating the to_local script for. // we are generating the to_local script for.
func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool, func SecondLevelHtlcScript(chanType channeldb.ChannelType, initiator bool,
revocationKey, delayKey *btcec.PublicKey, revocationKey, delayKey *btcec.PublicKey, csvDelay, leaseExpiry uint32,
csvDelay, leaseExpiry uint32) (input.ScriptDescriptor, error) { auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) {
switch { switch {
// For taproot channels, the pkScript is a segwit v1 p2tr output. // For taproot channels, the pkScript is a segwit v1 p2tr output.
case chanType.IsTaproot(): case chanType.IsTaproot():
return input.TaprootSecondLevelScriptTree( return input.TaprootSecondLevelScriptTree(
revocationKey, delayKey, csvDelay, revocationKey, delayKey, csvDelay, auxLeaf,
) )
// If we are the initiator of a leased channel, then we have an // If we are the initiator of a leased channel, then we have an
@@ -613,7 +613,7 @@ func CommitScriptAnchors(chanType channeldb.ChannelType,
// with, and abstracts the various ways of constructing commitment // with, and abstracts the various ways of constructing commitment
// transactions. // transactions.
type CommitmentBuilder struct { type CommitmentBuilder struct {
// chanState is the underlying channels's state struct, used to // chanState is the underlying channel's state struct, used to
// determine the type of channel we are dealing with, and relevant // determine the type of channel we are dealing with, and relevant
// parameters. // parameters.
chanState *channeldb.OpenChannel chanState *channeldb.OpenChannel
@@ -621,18 +621,25 @@ type CommitmentBuilder struct {
// obfuscator is a 48-bit state hint that's used to obfuscate the // obfuscator is a 48-bit state hint that's used to obfuscate the
// current state number on the commitment transactions. // current state number on the commitment transactions.
obfuscator [StateHintSize]byte obfuscator [StateHintSize]byte
// auxLeafStore is an interface that allows us to fetch auxiliary
// tapscript leaves for the commitment output.
auxLeafStore fn.Option[AuxLeafStore]
} }
// NewCommitmentBuilder creates a new CommitmentBuilder from chanState. // NewCommitmentBuilder creates a new CommitmentBuilder from chanState.
func NewCommitmentBuilder(chanState *channeldb.OpenChannel) *CommitmentBuilder { func NewCommitmentBuilder(chanState *channeldb.OpenChannel,
leafStore fn.Option[AuxLeafStore]) *CommitmentBuilder {
// The anchor channel type MUST be tweakless. // The anchor channel type MUST be tweakless.
if chanState.ChanType.HasAnchors() && !chanState.ChanType.IsTweakless() { if chanState.ChanType.HasAnchors() && !chanState.ChanType.IsTweakless() {
panic("invalid channel type combination") panic("invalid channel type combination")
} }
return &CommitmentBuilder{ return &CommitmentBuilder{
chanState: chanState, chanState: chanState,
obfuscator: createStateHintObfuscator(chanState), obfuscator: createStateHintObfuscator(chanState),
auxLeafStore: leafStore,
} }
} }
@@ -685,9 +692,9 @@ type unsignedCommitmentTx struct {
// fees, but after anchor outputs. // fees, but after anchor outputs.
func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance, func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
theirBalance lnwire.MilliSatoshi, whoseCommit lntypes.ChannelParty, theirBalance lnwire.MilliSatoshi, whoseCommit lntypes.ChannelParty,
feePerKw chainfee.SatPerKWeight, height uint64, feePerKw chainfee.SatPerKWeight, height uint64, originalHtlcView,
filteredHTLCView *htlcView, filteredHTLCView *HtlcView, keyRing *CommitmentKeyRing,
keyRing *CommitmentKeyRing) (*unsignedCommitmentTx, error) { prevCommit *commitment) (*unsignedCommitmentTx, error) {
dustLimit := cb.chanState.LocalChanCfg.DustLimit dustLimit := cb.chanState.LocalChanCfg.DustLimit
if whoseCommit.IsRemote() { if whoseCommit.IsRemote() {
@@ -695,7 +702,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
} }
numHTLCs := int64(0) numHTLCs := int64(0)
for _, htlc := range filteredHTLCView.ourUpdates { for _, htlc := range filteredHTLCView.OurUpdates {
if HtlcIsDust( if HtlcIsDust(
cb.chanState.ChanType, false, whoseCommit, feePerKw, cb.chanState.ChanType, false, whoseCommit, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit, htlc.Amount.ToSatoshis(), dustLimit,
@@ -706,7 +713,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
numHTLCs++ numHTLCs++
} }
for _, htlc := range filteredHTLCView.theirUpdates { for _, htlc := range filteredHTLCView.TheirUpdates {
if HtlcIsDust( if HtlcIsDust(
cb.chanState.ChanType, true, whoseCommit, feePerKw, cb.chanState.ChanType, true, whoseCommit, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit, htlc.Amount.ToSatoshis(), dustLimit,
@@ -748,10 +755,24 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
theirBalance -= commitFeeMSat theirBalance -= commitFeeMSat
} }
var ( var commitTx *wire.MsgTx
commitTx *wire.MsgTx
err error // Before we create the commitment transaction below, we'll try to see
) // if there're any aux leaves that need to be a part of the tapscript
// tree. We'll only do this if we have a custom blob defined though.
auxResult, err := fn.MapOptionZ(
cb.auxLeafStore,
func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] {
return auxLeavesFromView(
s, cb.chanState, prevCommit.customBlob,
originalHtlcView, whoseCommit, ourBalance,
theirBalance, *keyRing,
)
},
).Unpack()
if err != nil {
return nil, fmt.Errorf("unable to fetch aux leaves: %w", err)
}
// Depending on whether the transaction is ours or not, we call // Depending on whether the transaction is ours or not, we call
// CreateCommitTx with parameters matching the perspective, to generate // CreateCommitTx with parameters matching the perspective, to generate
@@ -767,6 +788,7 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
&cb.chanState.LocalChanCfg, &cb.chanState.RemoteChanCfg, &cb.chanState.LocalChanCfg, &cb.chanState.RemoteChanCfg,
ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), ourBalance.ToSatoshis(), theirBalance.ToSatoshis(),
numHTLCs, cb.chanState.IsInitiator, leaseExpiry, numHTLCs, cb.chanState.IsInitiator, leaseExpiry,
auxResult.AuxLeaves,
) )
} else { } else {
commitTx, err = CreateCommitTx( commitTx, err = CreateCommitTx(
@@ -774,12 +796,26 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
&cb.chanState.RemoteChanCfg, &cb.chanState.LocalChanCfg, &cb.chanState.RemoteChanCfg, &cb.chanState.LocalChanCfg,
theirBalance.ToSatoshis(), ourBalance.ToSatoshis(), theirBalance.ToSatoshis(), ourBalance.ToSatoshis(),
numHTLCs, !cb.chanState.IsInitiator, leaseExpiry, numHTLCs, !cb.chanState.IsInitiator, leaseExpiry,
auxResult.AuxLeaves,
) )
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Similarly, we'll now attempt to extract the set of aux leaves for
// the set of incoming and outgoing HTLCs.
incomingAuxLeaves := fn.MapOption(
func(leaves CommitAuxLeaves) input.HtlcAuxLeaves {
return leaves.IncomingHtlcLeaves
},
)(auxResult.AuxLeaves)
outgoingAuxLeaves := fn.MapOption(
func(leaves CommitAuxLeaves) input.HtlcAuxLeaves {
return leaves.OutgoingHtlcLeaves
},
)(auxResult.AuxLeaves)
// We'll now add all the HTLC outputs to the commitment transaction. // We'll now add all the HTLC outputs to the commitment transaction.
// Each output includes an off-chain 2-of-2 covenant clause, so we'll // Each output includes an off-chain 2-of-2 covenant clause, so we'll
// need the objective local/remote keys for this particular commitment // need the objective local/remote keys for this particular commitment
@@ -790,7 +826,8 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
// commitment outputs and should correspond to zero values for the // commitment outputs and should correspond to zero values for the
// purposes of sorting. // purposes of sorting.
cltvs := make([]uint32, len(commitTx.TxOut)) cltvs := make([]uint32, len(commitTx.TxOut))
for _, htlc := range filteredHTLCView.ourUpdates { htlcIndexes := make([]input.HtlcIndex, len(commitTx.TxOut))
for _, htlc := range filteredHTLCView.OurUpdates {
if HtlcIsDust( if HtlcIsDust(
cb.chanState.ChanType, false, whoseCommit, feePerKw, cb.chanState.ChanType, false, whoseCommit, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit, htlc.Amount.ToSatoshis(), dustLimit,
@@ -799,16 +836,26 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
continue continue
} }
auxLeaf := fn.ChainOption(
func(leaves input.HtlcAuxLeaves) input.AuxTapLeaf {
return leaves[htlc.HtlcIndex].AuxTapLeaf
},
)(outgoingAuxLeaves)
err := addHTLC( err := addHTLC(
commitTx, whoseCommit, false, htlc, keyRing, commitTx, whoseCommit, false, htlc, keyRing,
cb.chanState.ChanType, cb.chanState.ChanType, auxLeaf,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
cltvs = append(cltvs, htlc.Timeout) // nolint:makezero
// We want to add the CLTV and HTLC index to their respective
// slices, even if we already pre-allocated them.
cltvs = append(cltvs, htlc.Timeout) //nolint
htlcIndexes = append(htlcIndexes, htlc.HtlcIndex) //nolint
} }
for _, htlc := range filteredHTLCView.theirUpdates { for _, htlc := range filteredHTLCView.TheirUpdates {
if HtlcIsDust( if HtlcIsDust(
cb.chanState.ChanType, true, whoseCommit, feePerKw, cb.chanState.ChanType, true, whoseCommit, feePerKw,
htlc.Amount.ToSatoshis(), dustLimit, htlc.Amount.ToSatoshis(), dustLimit,
@@ -817,14 +864,24 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
continue continue
} }
auxLeaf := fn.ChainOption(
func(leaves input.HtlcAuxLeaves) input.AuxTapLeaf {
return leaves[htlc.HtlcIndex].AuxTapLeaf
},
)(incomingAuxLeaves)
err := addHTLC( err := addHTLC(
commitTx, whoseCommit, true, htlc, keyRing, commitTx, whoseCommit, true, htlc, keyRing,
cb.chanState.ChanType, cb.chanState.ChanType, auxLeaf,
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
cltvs = append(cltvs, htlc.Timeout) // nolint:makezero
// We want to add the CLTV and HTLC index to their respective
// slices, even if we already pre-allocated them.
cltvs = append(cltvs, htlc.Timeout) //nolint
htlcIndexes = append(htlcIndexes, htlc.HtlcIndex) //nolint
} }
// Set the state hint of the commitment transaction to facilitate // Set the state hint of the commitment transaction to facilitate
@@ -836,9 +893,16 @@ func (cb *CommitmentBuilder) createUnsignedCommitmentTx(ourBalance,
} }
// Sort the transactions according to the agreed upon canonical // Sort the transactions according to the agreed upon canonical
// ordering. This lets us skip sending the entire transaction over, // ordering (which might be customized for custom channel types, but
// instead we'll just send signatures. // deterministic and both parties will arrive at the same result). This
InPlaceCommitSort(commitTx, cltvs) // lets us skip sending the entire transaction over, instead we'll just
// send signatures.
commitSort := auxResult.CommitSortFunc.UnwrapOr(DefaultCommitSort)
err = commitSort(commitTx, cltvs, htlcIndexes)
if err != nil {
return nil, fmt.Errorf("unable to sort commitment "+
"transaction: %w", err)
}
// Next, we'll ensure that we don't accidentally create a commitment // Next, we'll ensure that we don't accidentally create a commitment
// transaction which would be invalid by consensus. // transaction which would be invalid by consensus.
@@ -880,24 +944,33 @@ func CreateCommitTx(chanType channeldb.ChannelType,
fundingOutput wire.TxIn, keyRing *CommitmentKeyRing, fundingOutput wire.TxIn, keyRing *CommitmentKeyRing,
localChanCfg, remoteChanCfg *channeldb.ChannelConfig, localChanCfg, remoteChanCfg *channeldb.ChannelConfig,
amountToLocal, amountToRemote btcutil.Amount, amountToLocal, amountToRemote btcutil.Amount,
numHTLCs int64, initiator bool, leaseExpiry uint32) (*wire.MsgTx, error) { numHTLCs int64, initiator bool, leaseExpiry uint32,
auxLeaves fn.Option[CommitAuxLeaves]) (*wire.MsgTx, error) {
// First, we create the script for the delayed "pay-to-self" output. // First, we create the script for the delayed "pay-to-self" output.
// This output has 2 main redemption clauses: either we can redeem the // This output has 2 main redemption clauses: either we can redeem the
// output after a relative block delay, or the remote node can claim // output after a relative block delay, or the remote node can claim
// the funds with the revocation key if we broadcast a revoked // the funds with the revocation key if we broadcast a revoked
// commitment transaction. // commitment transaction.
localAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf {
return l.LocalAuxLeaf
})(auxLeaves)
toLocalScript, err := CommitScriptToSelf( toLocalScript, err := CommitScriptToSelf(
chanType, initiator, keyRing.ToLocalKey, keyRing.RevocationKey, chanType, initiator, keyRing.ToLocalKey, keyRing.RevocationKey,
uint32(localChanCfg.CsvDelay), leaseExpiry, uint32(localChanCfg.CsvDelay), leaseExpiry,
fn.FlattenOption(localAuxLeaf),
) )
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Next, we create the script paying to the remote. // Next, we create the script paying to the remote.
remoteAuxLeaf := fn.MapOption(func(l CommitAuxLeaves) input.AuxTapLeaf {
return l.RemoteAuxLeaf
})(auxLeaves)
toRemoteScript, _, err := CommitScriptToRemote( toRemoteScript, _, err := CommitScriptToRemote(
chanType, initiator, keyRing.ToRemoteKey, leaseExpiry, chanType, initiator, keyRing.ToRemoteKey, leaseExpiry,
fn.FlattenOption(remoteAuxLeaf),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1077,7 +1150,7 @@ func genSegwitV0HtlcScript(chanType channeldb.ChannelType,
// channel. // channel.
func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty, func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing, timeout uint32, rHash [32]byte, keyRing *CommitmentKeyRing,
) (*input.HtlcScriptTree, error) { auxLeaf input.AuxTapLeaf) (*input.HtlcScriptTree, error) {
var ( var (
htlcScriptTree *input.HtlcScriptTree htlcScriptTree *input.HtlcScriptTree
@@ -1094,7 +1167,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
case isIncoming && whoseCommit.IsLocal(): case isIncoming && whoseCommit.IsLocal():
htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( htlcScriptTree, err = input.ReceiverHTLCScriptTaproot(
timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, timeout, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey,
keyRing.RevocationKey, rHash[:], whoseCommit, keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf,
) )
// We're being paid via an HTLC by the remote party, and the HTLC is // We're being paid via an HTLC by the remote party, and the HTLC is
@@ -1103,7 +1176,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
case isIncoming && whoseCommit.IsRemote(): case isIncoming && whoseCommit.IsRemote():
htlcScriptTree, err = input.SenderHTLCScriptTaproot( htlcScriptTree, err = input.SenderHTLCScriptTaproot(
keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey,
keyRing.RevocationKey, rHash[:], whoseCommit, keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf,
) )
// We're sending an HTLC which is being added to our commitment // We're sending an HTLC which is being added to our commitment
@@ -1112,7 +1185,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
case !isIncoming && whoseCommit.IsLocal(): case !isIncoming && whoseCommit.IsLocal():
htlcScriptTree, err = input.SenderHTLCScriptTaproot( htlcScriptTree, err = input.SenderHTLCScriptTaproot(
keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey,
keyRing.RevocationKey, rHash[:], whoseCommit, keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf,
) )
// Finally, we're paying the remote party via an HTLC, which is being // Finally, we're paying the remote party via an HTLC, which is being
@@ -1121,7 +1194,7 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
case !isIncoming && whoseCommit.IsRemote(): case !isIncoming && whoseCommit.IsRemote():
htlcScriptTree, err = input.ReceiverHTLCScriptTaproot( htlcScriptTree, err = input.ReceiverHTLCScriptTaproot(
timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey, timeout, keyRing.LocalHtlcKey, keyRing.RemoteHtlcKey,
keyRing.RevocationKey, rHash[:], whoseCommit, keyRing.RevocationKey, rHash[:], whoseCommit, auxLeaf,
) )
} }
@@ -1136,7 +1209,8 @@ func GenTaprootHtlcScript(isIncoming bool, whoseCommit lntypes.ChannelParty,
// along side the multiplexer. // along side the multiplexer.
func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool, func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
whoseCommit lntypes.ChannelParty, timeout uint32, rHash [32]byte, whoseCommit lntypes.ChannelParty, timeout uint32, rHash [32]byte,
keyRing *CommitmentKeyRing) (input.ScriptDescriptor, error) { keyRing *CommitmentKeyRing,
auxLeaf input.AuxTapLeaf) (input.ScriptDescriptor, error) {
if !chanType.IsTaproot() { if !chanType.IsTaproot() {
return genSegwitV0HtlcScript( return genSegwitV0HtlcScript(
@@ -1146,7 +1220,7 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
} }
return GenTaprootHtlcScript( return GenTaprootHtlcScript(
isIncoming, whoseCommit, timeout, rHash, keyRing, isIncoming, whoseCommit, timeout, rHash, keyRing, auxLeaf,
) )
} }
@@ -1159,13 +1233,15 @@ func genHtlcScript(chanType channeldb.ChannelType, isIncoming bool,
// the descriptor itself. // the descriptor itself.
func addHTLC(commitTx *wire.MsgTx, whoseCommit lntypes.ChannelParty, func addHTLC(commitTx *wire.MsgTx, whoseCommit lntypes.ChannelParty,
isIncoming bool, paymentDesc *PaymentDescriptor, isIncoming bool, paymentDesc *PaymentDescriptor,
keyRing *CommitmentKeyRing, chanType channeldb.ChannelType) error { keyRing *CommitmentKeyRing, chanType channeldb.ChannelType,
auxLeaf input.AuxTapLeaf) error {
timeout := paymentDesc.Timeout timeout := paymentDesc.Timeout
rHash := paymentDesc.RHash rHash := paymentDesc.RHash
scriptInfo, err := genHtlcScript( scriptInfo, err := genHtlcScript(
chanType, isIncoming, whoseCommit, timeout, rHash, keyRing, chanType, isIncoming, whoseCommit, timeout, rHash, keyRing,
auxLeaf,
) )
if err != nil { if err != nil {
return err return err
@@ -1198,7 +1274,8 @@ func addHTLC(commitTx *wire.MsgTx, whoseCommit lntypes.ChannelParty,
// output scripts and compares them against the outputs inside the commitment // output scripts and compares them against the outputs inside the commitment
// to find the match. // to find the match.
func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash, func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash,
chanState *channeldb.OpenChannel) (uint32, uint32, error) { chanState *channeldb.OpenChannel,
leafStore fn.Option[AuxLeafStore]) (uint32, uint32, error) {
// Init the output indexes as empty. // Init the output indexes as empty.
ourIndex := uint32(channeldb.OutputIndexEmpty) ourIndex := uint32(channeldb.OutputIndexEmpty)
@@ -1228,26 +1305,51 @@ func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash,
leaseExpiry = chanState.ThawHeight leaseExpiry = chanState.ThawHeight
} }
// Map the scripts from our PoV. When facing a local commitment, the to // If we have a custom blob, then we'll attempt to fetch the aux leaves
// local output belongs to us and the to remote output belongs to them. // for this state.
// When facing a remote commitment, the to local output belongs to them auxResult, err := fn.MapOptionZ(
// and the to remote output belongs to us. leafStore, func(a AuxLeafStore) fn.Result[CommitDiffAuxResult] {
return a.FetchLeavesFromCommit(
NewAuxChanState(chanState), chanCommit,
*keyRing,
)
},
).Unpack()
if err != nil {
return ourIndex, theirIndex, fmt.Errorf("unable to fetch aux "+
"leaves: %w", err)
}
// Compute the to local script. From our PoV, when facing a remote // Map the scripts from our PoV. When facing a local commitment, the
// commitment, the to local output belongs to them. // 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.
localAuxLeaf := fn.ChainOption(
func(l CommitAuxLeaves) input.AuxTapLeaf {
return l.LocalAuxLeaf
},
)(auxResult.AuxLeaves)
theirScript, err := CommitScriptToSelf( theirScript, err := CommitScriptToSelf(
chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey, chanState.ChanType, isRemoteInitiator, keyRing.ToLocalKey,
keyRing.RevocationKey, theirDelay, leaseExpiry, keyRing.RevocationKey, theirDelay, leaseExpiry, localAuxLeaf,
) )
if err != nil { if err != nil {
return ourIndex, theirIndex, err return ourIndex, theirIndex, err
} }
// Compute the to remote script. From our PoV, when facing a remote // Compute the to_remote script. From our PoV, when facing a remote
// commitment, the to remote output belongs to us. // commitment, the to_remote output belongs to us.
remoteAuxLeaf := fn.ChainOption(
func(l CommitAuxLeaves) input.AuxTapLeaf {
return l.RemoteAuxLeaf
},
)(auxResult.AuxLeaves)
ourScript, _, err := CommitScriptToRemote( ourScript, _, err := CommitScriptToRemote(
chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey, chanState.ChanType, isRemoteInitiator, keyRing.ToRemoteKey,
leaseExpiry, leaseExpiry, remoteAuxLeaf,
) )
if err != nil { if err != nil {
return ourIndex, theirIndex, err return ourIndex, theirIndex, err
+5
View File
@@ -5,6 +5,7 @@ import (
"github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
@@ -62,4 +63,8 @@ type Config struct {
// CoinSelectionStrategy is the strategy that is used for selecting // CoinSelectionStrategy is the strategy that is used for selecting
// coins when funding a transaction. // coins when funding a transaction.
CoinSelectionStrategy wallet.CoinSelectionStrategy CoinSelectionStrategy wallet.CoinSelectionStrategy
// AuxLeafStore is an optional store that can be used to store auxiliary
// leaves for certain custom channel types.
AuxLeafStore fn.Option[AuxLeafStore]
} }
+44
View File
@@ -17,8 +17,10 @@ import (
"github.com/btcsuite/btcwallet/wallet/txauthor" "github.com/btcsuite/btcwallet/wallet/txauthor"
"github.com/btcsuite/btcwallet/wtxmgr" "github.com/btcsuite/btcwallet/wtxmgr"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn" "github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/tlv"
) )
var ( var (
@@ -397,3 +399,45 @@ func (*mockChainIO) GetBlockHeader(
return nil, nil return nil, nil
} }
type MockAuxLeafStore struct{}
// A compile time check to ensure that MockAuxLeafStore implements the
// AuxLeafStore interface.
var _ AuxLeafStore = (*MockAuxLeafStore)(nil)
// FetchLeavesFromView attempts to fetch the auxiliary leaves that
// correspond to the passed aux blob, and pending original (unfiltered)
// HTLC view.
func (*MockAuxLeafStore) FetchLeavesFromView(
_ CommitDiffAuxInput) fn.Result[CommitDiffAuxResult] {
return fn.Ok(CommitDiffAuxResult{})
}
// FetchLeavesFromCommit attempts to fetch the auxiliary leaves that
// correspond to the passed aux blob, and an existing channel
// commitment.
func (*MockAuxLeafStore) FetchLeavesFromCommit(_ AuxChanState,
_ channeldb.ChannelCommitment,
_ CommitmentKeyRing) fn.Result[CommitDiffAuxResult] {
return fn.Ok(CommitDiffAuxResult{})
}
// FetchLeavesFromRevocation attempts to fetch the auxiliary leaves
// from a channel revocation that stores balance + blob information.
func (*MockAuxLeafStore) FetchLeavesFromRevocation(
_ *channeldb.RevocationLog) fn.Result[CommitDiffAuxResult] {
return fn.Ok(CommitDiffAuxResult{})
}
// ApplyHtlcView serves as the state transition function for the custom
// channel's blob. Given the old blob, and an HTLC view, then a new
// blob should be returned that reflects the pending updates.
func (*MockAuxLeafStore) ApplyHtlcView(
_ CommitDiffAuxInput) fn.Result[fn.Option[tlv.Blob]] {
return fn.Ok(fn.None[tlv.Blob]())
}
+79 -28
View File
@@ -8,8 +8,10 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -37,6 +39,20 @@ var (
ErrSessionNotFinalized = fmt.Errorf("musig2 session not finalized") ErrSessionNotFinalized = fmt.Errorf("musig2 session not finalized")
) )
// tapscriptRootToSignOpt is a function that takes a tapscript root and returns
// a MuSig2 sign opt that'll apply the tweak when signing+verifying.
func tapscriptRootToSignOpt(root chainhash.Hash) musig2.SignOption {
return musig2.WithTaprootSignTweak(root[:])
}
// TapscriptRootToTweak is a helper function that converts a tapscript root
// into a tweak that can be used with the MuSig2 API.
func TapscriptRootToTweak(root chainhash.Hash) input.MuSig2Tweaks {
return input.MuSig2Tweaks{
TaprootTweak: root[:],
}
}
// MusigPartialSig is a wrapper around the base musig2.PartialSignature type // MusigPartialSig is a wrapper around the base musig2.PartialSignature type
// that also includes information about the set of nonces used, and also the // that also includes information about the set of nonces used, and also the
// signer. This allows us to implement the input.Signature interface, as that // signer. This allows us to implement the input.Signature interface, as that
@@ -54,25 +70,30 @@ type MusigPartialSig struct {
// signerKeys is the set of public keys of all signers. // signerKeys is the set of public keys of all signers.
signerKeys []*btcec.PublicKey signerKeys []*btcec.PublicKey
// tapscriptTweak is an optional tweak, that if specified, will be used
// instead of the normal BIP 86 tweak when validating the signature.
tapscriptTweak fn.Option[chainhash.Hash]
} }
// NewMusigPartialSig creates a new musig partial signature. // NewMusigPartialSig creates a new MuSig2 partial signature.
func NewMusigPartialSig(sig *musig2.PartialSignature, func NewMusigPartialSig(sig *musig2.PartialSignature, signerNonce,
signerNonce, combinedNonce lnwire.Musig2Nonce, combinedNonce lnwire.Musig2Nonce, signerKeys []*btcec.PublicKey,
signerKeys []*btcec.PublicKey) *MusigPartialSig { tapscriptTweak fn.Option[chainhash.Hash]) *MusigPartialSig {
return &MusigPartialSig{ return &MusigPartialSig{
sig: sig, sig: sig,
signerNonce: signerNonce, signerNonce: signerNonce,
combinedNonce: combinedNonce, combinedNonce: combinedNonce,
signerKeys: signerKeys, signerKeys: signerKeys,
tapscriptTweak: tapscriptTweak,
} }
} }
// FromWireSig maps a wire partial sig to this internal type that we'll use to // FromWireSig maps a wire partial sig to this internal type that we'll use to
// perform signature validation. // perform signature validation.
func (p *MusigPartialSig) FromWireSig(sig *lnwire.PartialSigWithNonce, func (p *MusigPartialSig) FromWireSig(
) *MusigPartialSig { sig *lnwire.PartialSigWithNonce) *MusigPartialSig {
p.sig = &musig2.PartialSignature{ p.sig = &musig2.PartialSignature{
S: &sig.Sig, S: &sig.Sig,
@@ -135,9 +156,15 @@ func (p *MusigPartialSig) Verify(msg []byte, pub *btcec.PublicKey) bool {
var m [32]byte var m [32]byte
copy(m[:], msg) copy(m[:], msg)
// If we have a tapscript tweak, then we'll use that as a tweak
// otherwise, we'll fall back to the normal BIP 86 sign tweak.
signOpts := fn.MapOption(tapscriptRootToSignOpt)(
p.tapscriptTweak,
).UnwrapOr(musig2.WithBip86SignTweak())
return p.sig.Verify( return p.sig.Verify(
p.signerNonce, p.combinedNonce, p.signerKeys, pub, m, p.signerNonce, p.combinedNonce, p.signerKeys, pub, m,
musig2.WithSortedKeys(), musig2.WithBip86SignTweak(), musig2.WithSortedKeys(), signOpts,
) )
} }
@@ -160,6 +187,14 @@ func (n *MusigNoncePair) String() string {
n.SigningNonce.PubNonce[:]) n.SigningNonce.PubNonce[:])
} }
// TapscriptRootToTweak is a function that takes a MuSig2 taproot tweak and
// returns the root hash of the tapscript tree.
func muSig2TweakToRoot(tweak input.MuSig2Tweaks) chainhash.Hash {
var root chainhash.Hash
copy(root[:], tweak.TaprootTweak)
return root
}
// MusigSession abstracts over the details of a logical musig session. A single // MusigSession abstracts over the details of a logical musig session. A single
// session is used for each commitment transactions. The sessions use a JIT // session is used for each commitment transactions. The sessions use a JIT
// nonce style, wherein part of the session can be created using only the // nonce style, wherein part of the session can be created using only the
@@ -197,15 +232,20 @@ type MusigSession struct {
// commitType tracks if this is the session for the local or remote // commitType tracks if this is the session for the local or remote
// commitment. // commitment.
commitType MusigCommitType commitType MusigCommitType
// tapscriptTweak is an optional tweak, that if specified, will be used
// instead of the normal BIP 86 tweak when creating the MuSig2
// aggregate key and session.
tapscriptTweak fn.Option[input.MuSig2Tweaks]
} }
// NewPartialMusigSession creates a new musig2 session given only the // NewPartialMusigSession creates a new musig2 session given only the
// verification nonce (local nonce), and the other information that has already // verification nonce (local nonce), and the other information that has already
// been bound to the session. // been bound to the session.
func NewPartialMusigSession(verificationNonce musig2.Nonces, func NewPartialMusigSession(verificationNonce musig2.Nonces,
localKey, remoteKey keychain.KeyDescriptor, localKey, remoteKey keychain.KeyDescriptor, signer input.MuSig2Signer,
signer input.MuSig2Signer, inputTxOut *wire.TxOut, inputTxOut *wire.TxOut, commitType MusigCommitType,
commitType MusigCommitType) *MusigSession { tapscriptTweak fn.Option[input.MuSig2Tweaks]) *MusigSession {
signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey} signerKeys := []*btcec.PublicKey{localKey.PubKey, remoteKey.PubKey}
@@ -214,13 +254,14 @@ func NewPartialMusigSession(verificationNonce musig2.Nonces,
} }
return &MusigSession{ return &MusigSession{
nonces: nonces, nonces: nonces,
remoteKey: remoteKey, remoteKey: remoteKey,
localKey: localKey, localKey: localKey,
inputTxOut: inputTxOut, inputTxOut: inputTxOut,
signerKeys: signerKeys, signerKeys: signerKeys,
signer: signer, signer: signer,
commitType: commitType, commitType: commitType,
tapscriptTweak: tapscriptTweak,
} }
} }
@@ -254,9 +295,9 @@ func (m *MusigSession) FinalizeSession(signingNonce musig2.Nonces) error {
remoteNonce = m.nonces.SigningNonce remoteNonce = m.nonces.SigningNonce
} }
tweakDesc := input.MuSig2Tweaks{ tweakDesc := m.tapscriptTweak.UnwrapOr(input.MuSig2Tweaks{
TaprootBIP0086Tweak: true, TaprootBIP0086Tweak: true,
} })
m.session, err = m.signer.MuSig2CreateSession( m.session, err = m.signer.MuSig2CreateSession(
input.MuSig2Version100RC2, m.localKey.KeyLocator, m.signerKeys, input.MuSig2Version100RC2, m.localKey.KeyLocator, m.signerKeys,
&tweakDesc, [][musig2.PubNonceSize]byte{remoteNonce.PubNonce}, &tweakDesc, [][musig2.PubNonceSize]byte{remoteNonce.PubNonce},
@@ -351,8 +392,11 @@ func (m *MusigSession) SignCommit(tx *wire.MsgTx) (*MusigPartialSig, error) {
return nil, err return nil, err
} }
tapscriptRoot := fn.MapOption(muSig2TweakToRoot)(m.tapscriptTweak)
return NewMusigPartialSig( return NewMusigPartialSig(
sig, m.session.PublicNonce, m.combinedNonce, m.signerKeys, sig, m.session.PublicNonce, m.combinedNonce, m.signerKeys,
tapscriptRoot,
), nil ), nil
} }
@@ -364,7 +408,7 @@ func (m *MusigSession) Refresh(verificationNonce *musig2.Nonces,
return NewPartialMusigSession( return NewPartialMusigSession(
*verificationNonce, m.localKey, m.remoteKey, m.signer, *verificationNonce, m.localKey, m.remoteKey, m.signer,
m.inputTxOut, m.commitType, m.inputTxOut, m.commitType, m.tapscriptTweak,
), nil ), nil
} }
@@ -451,9 +495,11 @@ func (m *MusigSession) VerifyCommitSig(commitTx *wire.MsgTx,
// When we verify a commitment signature, we always assume that we're // When we verify a commitment signature, we always assume that we're
// verifying a signature on our local commitment. Therefore, we'll use: // verifying a signature on our local commitment. Therefore, we'll use:
// their remote nonce, and also public key. // their remote nonce, and also public key.
tapscriptRoot := fn.MapOption(muSig2TweakToRoot)(m.tapscriptTweak)
partialSig := NewMusigPartialSig( partialSig := NewMusigPartialSig(
&musig2.PartialSignature{S: &sig.Sig}, &musig2.PartialSignature{S: &sig.Sig},
m.nonces.SigningNonce.PubNonce, m.combinedNonce, m.signerKeys, m.nonces.SigningNonce.PubNonce, m.combinedNonce, m.signerKeys,
tapscriptRoot,
) )
// With the partial sig loaded with the proper context, we'll now // With the partial sig loaded with the proper context, we'll now
@@ -537,6 +583,10 @@ type MusigSessionCfg struct {
// InputTxOut is the output that we're signing for. This will be the // InputTxOut is the output that we're signing for. This will be the
// funding input. // funding input.
InputTxOut *wire.TxOut InputTxOut *wire.TxOut
// TapscriptRoot is an optional tweak that can be used to modify the
// MuSig2 public key used in the session.
TapscriptTweak fn.Option[chainhash.Hash]
} }
// MusigPairSession houses the two musig2 sessions needed to do funding and // MusigPairSession houses the two musig2 sessions needed to do funding and
@@ -561,13 +611,14 @@ func NewMusigPairSession(cfg *MusigSessionCfg) *MusigPairSession {
// //
// Both sessions will be created using only the verification nonce for // Both sessions will be created using only the verification nonce for
// the local+remote party. // the local+remote party.
tapscriptTweak := fn.MapOption(TapscriptRootToTweak)(cfg.TapscriptTweak)
localSession := NewPartialMusigSession( localSession := NewPartialMusigSession(
cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey, cfg.LocalNonce, cfg.LocalKey, cfg.RemoteKey, cfg.Signer,
cfg.Signer, cfg.InputTxOut, LocalMusigCommit, cfg.InputTxOut, LocalMusigCommit, tapscriptTweak,
) )
remoteSession := NewPartialMusigSession( remoteSession := NewPartialMusigSession(
cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey, cfg.RemoteNonce, cfg.LocalKey, cfg.RemoteKey, cfg.Signer,
cfg.Signer, cfg.InputTxOut, RemoteMusigCommit, cfg.InputTxOut, RemoteMusigCommit, tapscriptTweak,
) )
return &MusigPairSession{ return &MusigPairSession{
+4
View File
@@ -221,4 +221,8 @@ type PaymentDescriptor struct {
// blinded route (ie, not the introduction node) from update_add_htlc's // blinded route (ie, not the introduction node) from update_add_htlc's
// TLVs. // TLVs.
BlindingPoint lnwire.BlindingPointRecord BlindingPoint lnwire.BlindingPointRecord
// CustomRecords also stores the set of optional custom records that
// may have been attached to a sent HTLC.
CustomRecords lnwire.CustomRecords
} }
+5
View File
@@ -415,6 +415,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
chanType |= channeldb.ScidAliasFeatureBit chanType |= channeldb.ScidAliasFeatureBit
} }
if req.TapscriptRoot.IsSome() {
chanType |= channeldb.TapscriptRootBit
}
return &ChannelReservation{ return &ChannelReservation{
ourContribution: &ChannelContribution{ ourContribution: &ChannelContribution{
FundingAmount: ourBalance.ToSatoshis(), FundingAmount: ourBalance.ToSatoshis(),
@@ -448,6 +452,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
InitialLocalBalance: ourBalance, InitialLocalBalance: ourBalance,
InitialRemoteBalance: theirBalance, InitialRemoteBalance: theirBalance,
Memo: req.Memo, Memo: req.Memo,
TapscriptRoot: req.TapscriptRoot,
}, },
pushMSat: req.PushMSat, pushMSat: req.PushMSat,
pendingChanID: req.PendingChanID, pendingChanID: req.PendingChanID,
+16
View File
@@ -14,6 +14,7 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
@@ -348,6 +349,21 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
Packager: channeldb.NewChannelPackager(shortChanID), Packager: channeldb.NewChannelPackager(shortChanID),
} }
// If the channel type has a tapscript root, then we'll also specify
// one here to apply to both the channels.
if chanType.HasTapscriptRoot() {
var tapscriptRoot chainhash.Hash
_, err := io.ReadFull(rand.Reader, tapscriptRoot[:])
if err != nil {
return nil, nil, err
}
someRoot := fn.Some(tapscriptRoot)
aliceChannelState.TapscriptRoot = someRoot
bobChannelState.TapscriptRoot = someRoot
}
aliceSigner := input.NewMockSigner(aliceKeys, nil) aliceSigner := input.NewMockSigner(aliceKeys, nil)
bobSigner := input.NewMockSigner(bobKeys, nil) bobSigner := input.NewMockSigner(bobKeys, nil)
+7 -5
View File
@@ -8,6 +8,7 @@ import (
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
) )
const ( const (
@@ -50,8 +51,8 @@ var (
// - <sender sig> <receiver sig> <preimage> <success_script> <control_block> // - <sender sig> <receiver sig> <preimage> <success_script> <control_block>
func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool, func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool,
htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay, htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, csvDelay,
leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey) ( leaseExpiry uint32, revocationKey, delayKey *btcec.PublicKey,
*wire.MsgTx, error) { auxLeaf input.AuxTapLeaf) (*wire.MsgTx, error) {
// Create a version two transaction (as the success version of this // Create a version two transaction (as the success version of this
// spends an output with a CSV timeout). // spends an output with a CSV timeout).
@@ -71,7 +72,7 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool,
// HTLC outputs. // HTLC outputs.
scriptInfo, err := SecondLevelHtlcScript( scriptInfo, err := SecondLevelHtlcScript(
chanType, initiator, revocationKey, delayKey, csvDelay, chanType, initiator, revocationKey, delayKey, csvDelay,
leaseExpiry, leaseExpiry, auxLeaf,
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -110,7 +111,8 @@ func CreateHtlcSuccessTx(chanType channeldb.ChannelType, initiator bool,
func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool, func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool,
htlcOutput wire.OutPoint, htlcAmt btcutil.Amount, htlcOutput wire.OutPoint, htlcAmt btcutil.Amount,
cltvExpiry, csvDelay, leaseExpiry uint32, cltvExpiry, csvDelay, leaseExpiry uint32,
revocationKey, delayKey *btcec.PublicKey) (*wire.MsgTx, error) { revocationKey, delayKey *btcec.PublicKey,
auxLeaf input.AuxTapLeaf) (*wire.MsgTx, error) {
// Create a version two transaction (as the success version of this // Create a version two transaction (as the success version of this
// spends an output with a CSV timeout), and set the lock-time to the // spends an output with a CSV timeout), and set the lock-time to the
@@ -134,7 +136,7 @@ func CreateHtlcTimeoutTx(chanType channeldb.ChannelType, initiator bool,
// HTLC outputs. // HTLC outputs.
scriptInfo, err := SecondLevelHtlcScript( scriptInfo, err := SecondLevelHtlcScript(
chanType, initiator, revocationKey, delayKey, csvDelay, chanType, initiator, revocationKey, delayKey, csvDelay,
leaseExpiry, leaseExpiry, auxLeaf,
) )
if err != nil { if err != nil {
return nil, err return nil, err
+2
View File
@@ -21,6 +21,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
@@ -631,6 +632,7 @@ func testSpendValidation(t *testing.T, tweakless bool) {
commitmentTx, err := CreateCommitTx( commitmentTx, err := CreateCommitTx(
channelType, *fakeFundingTxIn, keyRing, aliceChanCfg, channelType, *fakeFundingTxIn, keyRing, aliceChanCfg,
bobChanCfg, channelBalance, channelBalance, 0, true, 0, bobChanCfg, channelBalance, channelBalance, 0, true, 0,
fn.None[CommitAuxLeaves](),
) )
if err != nil { if err != nil {
t.Fatalf("unable to create commitment transaction: %v", nil) t.Fatalf("unable to create commitment transaction: %v", nil)
+45 -6
View File
@@ -23,6 +23,7 @@ import (
"github.com/btcsuite/btcwallet/wallet" "github.com/btcsuite/btcwallet/wallet"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
@@ -210,6 +211,11 @@ type InitFundingReserveMsg struct {
// channel that will be useful to our future selves. // channel that will be useful to our future selves.
Memo []byte Memo []byte
// TapscriptRoot is the root of the tapscript tree that will be used to
// create the funding output. This is an optional field that should
// only be set for taproot channels.
TapscriptRoot fn.Option[chainhash.Hash]
// err is a channel in which all errors will be sent across. Will be // err is a channel in which all errors will be sent across. Will be
// nil if this initial set is successful. // nil if this initial set is successful.
// //
@@ -1464,6 +1470,21 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
req.err <- nil req.err <- nil
} }
// createCommitOpts is a struct that holds the options for creating a new
// commitment transaction.
type createCommitOpts struct {
auxLeaves fn.Option[CommitAuxLeaves]
}
// defaultCommitOpts returns a new createCommitOpts with default values.
func defaultCommitOpts() createCommitOpts {
return createCommitOpts{}
}
// CreateCommitOpt is a functional option that can be used to modify the way a
// new commitment transaction is created.
type CreateCommitOpt func(*createCommitOpts)
// CreateCommitmentTxns is a helper function that creates the initial // CreateCommitmentTxns is a helper function that creates the initial
// commitment transaction for both parties. This function is used during the // commitment transaction for both parties. This function is used during the
// initial funding workflow as both sides must generate a signature for the // initial funding workflow as both sides must generate a signature for the
@@ -1473,7 +1494,13 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount,
ourChanCfg, theirChanCfg *channeldb.ChannelConfig, ourChanCfg, theirChanCfg *channeldb.ChannelConfig,
localCommitPoint, remoteCommitPoint *btcec.PublicKey, localCommitPoint, remoteCommitPoint *btcec.PublicKey,
fundingTxIn wire.TxIn, chanType channeldb.ChannelType, initiator bool, fundingTxIn wire.TxIn, chanType channeldb.ChannelType, initiator bool,
leaseExpiry uint32) (*wire.MsgTx, *wire.MsgTx, error) { leaseExpiry uint32, opts ...CreateCommitOpt) (*wire.MsgTx, *wire.MsgTx,
error) {
options := defaultCommitOpts()
for _, optFunc := range opts {
optFunc(&options)
}
localCommitmentKeys := DeriveCommitmentKeys( localCommitmentKeys := DeriveCommitmentKeys(
localCommitPoint, lntypes.Local, chanType, ourChanCfg, localCommitPoint, lntypes.Local, chanType, ourChanCfg,
@@ -1487,7 +1514,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount,
ourCommitTx, err := CreateCommitTx( ourCommitTx, err := CreateCommitTx(
chanType, fundingTxIn, localCommitmentKeys, ourChanCfg, chanType, fundingTxIn, localCommitmentKeys, ourChanCfg,
theirChanCfg, localBalance, remoteBalance, 0, initiator, theirChanCfg, localBalance, remoteBalance, 0, initiator,
leaseExpiry, leaseExpiry, options.auxLeaves,
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -1501,7 +1528,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount,
theirCommitTx, err := CreateCommitTx( theirCommitTx, err := CreateCommitTx(
chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg, chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg,
ourChanCfg, remoteBalance, localBalance, 0, !initiator, ourChanCfg, remoteBalance, localBalance, 0, !initiator,
leaseExpiry, leaseExpiry, options.auxLeaves,
) )
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
@@ -2102,6 +2129,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation,
if res.musigSessions == nil { if res.musigSessions == nil {
_, fundingOutput, err := input.GenTaprootFundingScript( _, fundingOutput, err := input.GenTaprootFundingScript(
localKey, remoteKey, channelValue, localKey, remoteKey, channelValue,
res.partialState.TapscriptRoot,
) )
if err != nil { if err != nil {
return err return err
@@ -2341,11 +2369,14 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
fundingTxOut *wire.TxOut fundingTxOut *wire.TxOut
) )
if chanType.IsTaproot() { if chanType.IsTaproot() {
fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript( //nolint:lll //nolint:lll
fundingWitnessScript, fundingTxOut, err = input.GenTaprootFundingScript(
ourKey.PubKey, theirKey.PubKey, channelValue, ourKey.PubKey, theirKey.PubKey, channelValue,
pendingReservation.partialState.TapscriptRoot,
) )
} else { } else {
fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript( //nolint:lll //nolint:lll
fundingWitnessScript, fundingTxOut, err = input.GenFundingPkScript(
ourKey.PubKey.SerializeCompressed(), ourKey.PubKey.SerializeCompressed(),
theirKey.PubKey.SerializeCompressed(), channelValue, theirKey.PubKey.SerializeCompressed(), channelValue,
) )
@@ -2465,9 +2496,16 @@ func initStateHints(commit1, commit2 *wire.MsgTx,
func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel, func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
fundingTx *wire.MsgTx) error { fundingTx *wire.MsgTx) error {
var chanOpts []ChannelOpt
l.Cfg.AuxLeafStore.WhenSome(func(s AuxLeafStore) {
chanOpts = append(chanOpts, WithLeafStore(s))
})
// First, we'll obtain a fully signed commitment transaction so we can // First, we'll obtain a fully signed commitment transaction so we can
// pass into it on the chanvalidate package for verification. // pass into it on the chanvalidate package for verification.
channel, err := NewLightningChannel(l.Cfg.Signer, channelState, nil) channel, err := NewLightningChannel(
l.Cfg.Signer, channelState, nil, chanOpts...,
)
if err != nil { if err != nil {
return err return err
} }
@@ -2482,6 +2520,7 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
if channelState.ChanType.IsTaproot() { if channelState.ChanType.IsTaproot() {
fundingScript, _, err = input.GenTaprootFundingScript( fundingScript, _, err = input.GenTaprootFundingScript(
localKey, remoteKey, int64(channel.Capacity), localKey, remoteKey, int64(channel.Capacity),
channelState.TapscriptRoot,
) )
if err != nil { if err != nil {
return err return err
+13 -1
View File
@@ -372,6 +372,10 @@ type Config struct {
AddLocalAlias func(alias, base lnwire.ShortChannelID, AddLocalAlias func(alias, base lnwire.ShortChannelID,
gossip bool) error gossip bool) error
// AuxLeafStore is an optional store that can be used to store auxiliary
// leaves for certain custom channel types.
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
// PongBuf is a slice we'll reuse instead of allocating memory on the // PongBuf is a slice we'll reuse instead of allocating memory on the
// heap. Since only reads will occur and no writes, there is no need // heap. Since only reads will occur and no writes, there is no need
// for any synchronization primitives. As a result, it's safe to share // for any synchronization primitives. As a result, it's safe to share
@@ -943,8 +947,12 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
} }
} }
var chanOpts []lnwallet.ChannelOpt
p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
})
lnChan, err := lnwallet.NewLightningChannel( lnChan, err := lnwallet.NewLightningChannel(
p.cfg.Signer, dbChan, p.cfg.SigPool, p.cfg.Signer, dbChan, p.cfg.SigPool, chanOpts...,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to create channel "+ return nil, fmt.Errorf("unable to create channel "+
@@ -4151,6 +4159,10 @@ func (p *Brontide) addActiveChannel(c *lnpeer.NewChannel) error {
chanOpts = append(chanOpts, lnwallet.WithSkipNonceInit()) chanOpts = append(chanOpts, lnwallet.WithSkipNonceInit())
} }
p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
})
// If not already active, we'll add this channel to the set of active // If not already active, we'll add this channel to the set of active
// channels, so we can look it up later easily according to its channel // channels, so we can look it up later easily according to its channel
// ID. // ID.
+7 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwallet/chancloser" "github.com/lightningnetwork/lnd/lnwallet/chancloser"
@@ -43,10 +44,15 @@ func (m *MusigChanCloser) ProposalClosingOpts() (
} }
localKey, remoteKey := m.channel.MultiSigKeys() localKey, remoteKey := m.channel.MultiSigKeys()
tapscriptTweak := fn.MapOption(lnwallet.TapscriptRootToTweak)(
m.channel.State().TapscriptRoot,
)
m.musigSession = lnwallet.NewPartialMusigSession( m.musigSession = lnwallet.NewPartialMusigSession(
*m.remoteNonce, localKey, remoteKey, *m.remoteNonce, localKey, remoteKey,
m.channel.Signer, m.channel.FundingTxOut(), m.channel.Signer, m.channel.FundingTxOut(),
lnwallet.RemoteMusigCommit, lnwallet.RemoteMusigCommit, tapscriptTweak,
) )
err := m.musigSession.FinalizeSession(*m.localNonce) err := m.musigSession.FinalizeSession(*m.localNonce)
+3
View File
@@ -1273,6 +1273,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
return &pc.Incoming return &pc.Incoming
}, },
AuxLeafStore: implCfg.AuxLeafStore,
}, dbs.ChanStateDB) }, dbs.ChanStateDB)
// Select the configuration and funding parameters for Bitcoin. // Select the configuration and funding parameters for Bitcoin.
@@ -1607,6 +1608,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
br, err := lnwallet.NewBreachRetribution( br, err := lnwallet.NewBreachRetribution(
channel, commitHeight, 0, nil, channel, commitHeight, 0, nil,
implCfg.AuxLeafStore,
) )
if err != nil { if err != nil {
return nil, 0, err return nil, 0, err
@@ -4073,6 +4075,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
DisallowRouteBlinding: s.cfg.ProtocolOptions.NoRouteBlinding(), DisallowRouteBlinding: s.cfg.ProtocolOptions.NoRouteBlinding(),
MaxFeeExposure: thresholdMSats, MaxFeeExposure: thresholdMSats,
Quit: s.quit, Quit: s.quit,
AuxLeafStore: s.implCfg.AuxLeafStore,
MsgRouter: s.implCfg.MsgRouter, MsgRouter: s.implCfg.MsgRouter,
} }
+7 -2
View File
@@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
secp "github.com/decred/dcrd/dcrec/secp256k1/v4" secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -307,9 +308,11 @@ func newTaprootJusticeKit(sweepScript []byte,
keyRing := breachInfo.KeyRing keyRing := breachInfo.KeyRing
// TODO(roasbeef): aux leaf tower updates needed
tree, err := input.NewLocalCommitScriptTree( tree, err := input.NewLocalCommitScriptTree(
breachInfo.RemoteDelay, keyRing.ToLocalKey, breachInfo.RemoteDelay, keyRing.ToLocalKey,
keyRing.RevocationKey, keyRing.RevocationKey, fn.None[txscript.TapLeaf](),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@@ -416,7 +419,9 @@ func (t *taprootJusticeKit) ToRemoteOutputSpendInfo() (*txscript.PkScript,
return nil, nil, 0, err return nil, nil, 0, err
} }
scriptTree, err := input.NewRemoteCommitScriptTree(toRemotePk) scriptTree, err := input.NewRemoteCommitScriptTree(
toRemotePk, fn.None[txscript.TapLeaf](),
)
if err != nil { if err != nil {
return nil, nil, 0, err return nil, nil, 0, err
} }
+5 -1
View File
@@ -12,6 +12,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
@@ -304,7 +305,9 @@ func TestJusticeKitRemoteWitnessConstruction(t *testing.T) {
name: "taproot commitment", name: "taproot commitment",
blobType: TypeAltruistTaprootCommit, blobType: TypeAltruistTaprootCommit,
expWitnessScript: func(pk *btcec.PublicKey) []byte { expWitnessScript: func(pk *btcec.PublicKey) []byte {
tree, _ := input.NewRemoteCommitScriptTree(pk) tree, _ := input.NewRemoteCommitScriptTree(
pk, fn.None[txscript.TapLeaf](),
)
return tree.SettleLeaf.Script return tree.SettleLeaf.Script
}, },
@@ -461,6 +464,7 @@ func TestJusticeKitToLocalWitnessConstruction(t *testing.T) {
script, _ := input.NewLocalCommitScriptTree( script, _ := input.NewLocalCommitScriptTree(
csvDelay, delay, rev, csvDelay, delay, rev,
fn.None[txscript.TapLeaf](),
) )
return script.RevocationLeaf.Script return script.RevocationLeaf.Script
@@ -11,6 +11,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
secp "github.com/decred/dcrd/dcrec/secp256k1/v4" secp "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@@ -123,7 +124,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
if isTaprootChannel { if isTaprootChannel {
toLocalCommitTree, err = input.NewLocalCommitScriptTree( toLocalCommitTree, err = input.NewLocalCommitScriptTree(
csvDelay, toLocalPK, revPK, csvDelay, toLocalPK, revPK, fn.None[txscript.TapLeaf](),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -174,7 +175,7 @@ func testJusticeDescriptor(t *testing.T, blobType blob.Type) {
toRemoteSequence = 1 toRemoteSequence = 1
commitScriptTree, err := input.NewRemoteCommitScriptTree( commitScriptTree, err := input.NewRemoteCommitScriptTree(
toRemotePK, toRemotePK, fn.None[txscript.TapLeaf](),
) )
require.NoError(t, err) require.NoError(t, err)
@@ -10,6 +10,7 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwallet"
@@ -136,6 +137,7 @@ func genTaskTest(
if chanType.IsTaproot() { if chanType.IsTaproot() {
scriptTree, _ := input.NewLocalCommitScriptTree( scriptTree, _ := input.NewLocalCommitScriptTree(
csvDelay, toLocalPK, revPK, csvDelay, toLocalPK, revPK,
fn.None[txscript.TapLeaf](),
) )
pkScript, _ := input.PayToTaprootScript( pkScript, _ := input.PayToTaprootScript(
@@ -189,7 +191,7 @@ func genTaskTest(
if chanType.IsTaproot() { if chanType.IsTaproot() {
scriptTree, _ := input.NewRemoteCommitScriptTree( scriptTree, _ := input.NewRemoteCommitScriptTree(
toRemotePK, toRemotePK, fn.None[txscript.TapLeaf](),
) )
pkScript, _ := input.PayToTaprootScript( pkScript, _ := input.PayToTaprootScript(
+5 -2
View File
@@ -19,6 +19,7 @@ import (
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channelnotifier" "github.com/lightningnetwork/lnd/channelnotifier"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb" "github.com/lightningnetwork/lnd/kvdb"
@@ -230,12 +231,14 @@ func (c *mockChannel) createRemoteCommitTx(t *testing.T) {
// Construct the to-local witness script. // Construct the to-local witness script.
toLocalScriptTree, err := input.NewLocalCommitScriptTree( toLocalScriptTree, err := input.NewLocalCommitScriptTree(
c.csvDelay, c.toLocalPK, c.revPK, c.csvDelay, c.toLocalPK, c.revPK, fn.None[txscript.TapLeaf](),
) )
require.NoError(t, err, "unable to create to-local script") require.NoError(t, err, "unable to create to-local script")
// Construct the to-remote witness script. // Construct the to-remote witness script.
toRemoteScriptTree, err := input.NewRemoteCommitScriptTree(c.toRemotePK) toRemoteScriptTree, err := input.NewRemoteCommitScriptTree(
c.toRemotePK, fn.None[txscript.TapLeaf](),
)
require.NoError(t, err, "unable to create to-remote script") require.NoError(t, err, "unable to create to-remote script")
// Compute the to-local witness script hash. // Compute the to-local witness script hash.