diff --git a/channeldb/channel.go b/channeldb/channel.go index 3fbd0b55a..a31052a70 100644 --- a/channeldb/channel.go +++ b/channeldb/channel.go @@ -251,6 +251,10 @@ type chanAuxData struct { // tapscriptRoot is the optional Tapscript root the channel funding // output commits to. tapscriptRoot tlv.OptionalRecordT[tlv.TlvType5, [32]byte] + + // customBlob is an optional TLV encoded blob of data representing + // custom channel funding information. + customBlob tlv.OptionalRecordT[tlv.TlvType6, tlv.Blob] } // encode serializes the chanAuxData to the given io.Writer. @@ -269,6 +273,9 @@ func (c *chanAuxData) encode(w io.Writer) error { tlvRecords = append(tlvRecords, root.Record()) }, ) + c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType6, tlv.Blob]) { + tlvRecords = append(tlvRecords, blob.Record()) + }) // Create the tlv stream. tlvStream, err := tlv.NewStream(tlvRecords...) @@ -283,6 +290,7 @@ func (c *chanAuxData) encode(w io.Writer) error { func (c *chanAuxData) 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( @@ -292,6 +300,7 @@ func (c *chanAuxData) decode(r io.Reader) error { c.realScid.Record(), memo.Record(), tapscriptRoot.Record(), + blob.Record(), ) if err != nil { return err @@ -308,6 +317,9 @@ func (c *chanAuxData) decode(r io.Reader) error { 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 } @@ -325,6 +337,9 @@ func (c *chanAuxData) toOpenChan(o *OpenChannel) { c.tapscriptRoot.WhenSomeV(func(h [32]byte) { o.TapscriptRoot = fn.Some[chainhash.Hash](h) }) + c.customBlob.WhenSomeV(func(blob tlv.Blob) { + o.CustomBlob = fn.Some(blob) + }) } // newChanAuxDataFromChan creates a new chanAuxData from the given channel. @@ -354,6 +369,11 @@ func newChanAuxDataFromChan(openChan *OpenChannel) *chanAuxData { tlv.NewPrimitiveRecord[tlv.TlvType5, [32]byte](h), ) }) + openChan.CustomBlob.WhenSome(func(blob tlv.Blob) { + c.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType6](blob), + ) + }) return c } @@ -607,6 +627,74 @@ type ChannelConfig struct { HtlcBasePoint keychain.KeyDescriptor } +// commitAuxData stores all the optional data that may be store as a TLV stream +// at the _end_ of the normal serialized commit on disk. +type commitAuxData 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 *commitAuxData) 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 ecode the aux data from the passed io.Reader. +func (c *commitAuxData) 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 +} + +// toChanCommit extracts the optional data stored in the commitAuxData struct +// and stores it in the ChannelCommitment. +func (c *commitAuxData) toChanCommit(commit *ChannelCommitment) { + c.customBlob.WhenSomeV(func(blob tlv.Blob) { + commit.CustomBlob = fn.Some(blob) + }) +} + +// newCommitAuxData creates an aux data struct from the normal chan commitment. +func newCommitAuxData(commit *ChannelCommitment) commitAuxData { + var c commitAuxData + + commit.CustomBlob.WhenSome(func(blob tlv.Blob) { + c.customBlob = tlv.SomeRecordT( + tlv.NewPrimitiveRecord[tlv.TlvType1](blob), + ) + }) + + return c +} + // ChannelCommitment is a snapshot of the commitment state at a particular // 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 @@ -673,6 +761,11 @@ type ChannelCommitment struct { // able by us. CommitTx *wire.MsgTx + // CustomBlob is an optional blob that can be used to store information + // specific to a custom channel type. This may track soem custom + // specific state for this given commitment. + CustomBlob fn.Option[tlv.Blob] + // CommitSig is one half of the signature required to fully complete // the script for the commitment transaction above. This is the // signature signed by the remote party for our version of the @@ -682,9 +775,6 @@ type ChannelCommitment struct { // Htlcs is the set of HTLC's that are pending at this particular // commitment height. Htlcs []HTLC - - // TODO(roasbeef): pending commit pointer? - // * lets just walk through } // ChannelStatus is a bit vector used to indicate whether an OpenChannel is in @@ -982,6 +1072,12 @@ type OpenChannel struct { // 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 Db *ChannelStateDB @@ -2793,6 +2889,16 @@ 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. + // + // TODO(roasbeef): migrate it after all? + auxData := newCommitAuxData(&diff.Commitment) + if err := auxData.encode(w); err != nil { + return fmt.Errorf("unable to write aux data: %w", err) + } + return nil } @@ -2853,6 +2959,18 @@ 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 commitAuxData + if err := auxData.decode(r); err != nil { + return nil, fmt.Errorf("unable to decode aux data: %w", err) + + } + + auxData.toChanCommit(&d.Commitment) + return &d, nil } @@ -3831,6 +3949,9 @@ func (c *OpenChannel) Snapshot() *ChannelSnapshot { }, } + // TODO(roasbeef): fill in other info for the commitment above + // * also custom blob + // Copy over the current set of HTLCs to ensure the caller can't mutate // our internal state. snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs)) @@ -4222,6 +4343,12 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment, return err } + // Before we write to disk, we'll also write our aux data as well. + auxData := newCommitAuxData(c) + if err := auxData.encode(&b); err != nil { + return fmt.Errorf("unable to write aux data: %w", err) + } + return chanBucket.Put(commitKey, b.Bytes()) } @@ -4367,7 +4494,9 @@ func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) { return c, nil } -func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment, error) { +func fetchChanCommitment(chanBucket kvdb.RBucket, + local bool) (ChannelCommitment, error) { + var commitKey []byte if local { commitKey = append(chanCommitmentKey, byte(0x00)) @@ -4381,7 +4510,23 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment } 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 commitAuxData + if err := auxData.decode(r); err != nil { + return ChannelCommitment{}, fmt.Errorf("unable to decode "+ + "chan aux data: %w", err) + } + + auxData.toChanCommit(&chanCommit) + + return chanCommit, nil } func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error { diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go index 2389015cf..5cfe39284 100644 --- a/channeldb/channel_test.go +++ b/channeldb/channel_test.go @@ -337,6 +337,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { FeePerKw: btcutil.Amount(5000), CommitTx: channels.TestFundingTx, CommitSig: bytes.Repeat([]byte{1}, 71), + CustomBlob: fn.Some([]byte{1, 2, 3}), }, RemoteCommitment: ChannelCommitment{ CommitHeight: 0, @@ -346,6 +347,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { FeePerKw: btcutil.Amount(5000), CommitTx: channels.TestFundingTx, CommitSig: bytes.Repeat([]byte{1}, 71), + CustomBlob: fn.Some([]byte{4, 5, 6}), }, NumConfsRequired: 4, RemoteCurrentRevocation: privKey.PubKey(), @@ -360,6 +362,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel { InitialRemoteBalance: lnwire.MilliSatoshi(3000), Memo: []byte("test"), TapscriptRoot: fn.Some(tapscriptRoot), + CustomBlob: fn.Some([]byte{1, 2, 3}), } } @@ -649,6 +652,7 @@ func TestChannelStateTransition(t *testing.T) { CommitTx: newTx, CommitSig: newSig, Htlcs: htlcs, + CustomBlob: fn.Some([]byte{4, 5, 6}), } // First update the local node's broadcastable state and also add a @@ -686,9 +690,12 @@ func TestChannelStateTransition(t *testing.T) { // have been updated. updatedChannel, err := cdb.FetchOpenChannels(channel.IdentityPub) require.NoError(t, err, "unable to fetch updated channel") + assertCommitmentEqual(t, &commitment, &updatedChannel[0].LocalCommitment) + numDiskUpdates, err := updatedChannel[0].CommitmentHeight() require.NoError(t, err, "unable to read commitment height from disk") + if numDiskUpdates != uint64(commitment.CommitHeight) { t.Fatalf("num disk updates doesn't match: %v vs %v", numDiskUpdates, commitment.CommitHeight)