diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 1d644a357..730416d4d 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -21,7 +21,7 @@ env: GO_VERSION: '1.23.6' - LITD_ITEST_BRANCH: 'lnd-19' + LITD_ITEST_BRANCH: 'closedchannel-data' jobs: ####################### @@ -266,6 +266,9 @@ jobs: name: run itests runs-on: ubuntu-latest steps: + - name: cleanup space + run: rm -rf /opt/hostedtoolcache + - name: git checkout uses: actions/checkout@v4 @@ -306,6 +309,9 @@ jobs: name: run itests postgres runs-on: ubuntu-latest steps: + - name: cleanup space + run: rm -rf /opt/hostedtoolcache + - name: git checkout uses: actions/checkout@v4 @@ -347,6 +353,9 @@ jobs: runs-on: ubuntu-latest steps: + - name: cleanup space + run: rm -rf /opt/hostedtoolcache + - name: git checkout uses: actions/checkout@v4 @@ -391,7 +400,7 @@ jobs: # Notify about the completion of all coverage collecting jobs. finish: if: ${{ always() }} - needs: [unit-test] + needs: [ unit-test ] runs-on: ubuntu-latest steps: - uses: coverallsapp/github-action@v2 diff --git a/docs/examples/basic-price-oracle/go.mod b/docs/examples/basic-price-oracle/go.mod index eb766ff11..14ab51bfe 100644 --- a/docs/examples/basic-price-oracle/go.mod +++ b/docs/examples/basic-price-oracle/go.mod @@ -98,7 +98,7 @@ require ( github.com/lightninglabs/neutrino v0.16.1 // indirect github.com/lightninglabs/neutrino/cache v1.1.2 // indirect github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb // indirect - github.com/lightningnetwork/lnd v0.19.0-beta.rc1 // indirect + github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f // indirect github.com/lightningnetwork/lnd/cert v1.2.2 // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect github.com/lightningnetwork/lnd/fn/v2 v2.0.8 // indirect diff --git a/docs/examples/basic-price-oracle/go.sum b/docs/examples/basic-price-oracle/go.sum index a362e4abf..4393ec096 100644 --- a/docs/examples/basic-price-oracle/go.sum +++ b/docs/examples/basic-price-oracle/go.sum @@ -448,8 +448,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.19.0-beta.rc1 h1:FJvsdw4PZ41ykrHi7vNGit9IIohE+IlKpVwL5/1+L+0= -github.com/lightningnetwork/lnd v0.19.0-beta.rc1/go.mod h1:BP+neeFpmeAA7o5hu3zp3FwOEl26idSyPV9zBOavp6E= +github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f h1:+Bejv2Ij/ryUjLacBd5au0acMH0AYs0lhb7ki5rx9ms= +github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f/go.mod h1:BP+neeFpmeAA7o5hu3zp3FwOEl26idSyPV9zBOavp6E= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= diff --git a/go.mod b/go.mod index 5f6cee363..cab203dee 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.2 github.com/lightninglabs/lndclient v0.19.0-3 github.com/lightninglabs/neutrino/cache v1.1.2 - github.com/lightningnetwork/lnd v0.19.0-beta.rc1 + github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 github.com/lightningnetwork/lnd/fn/v2 v2.0.8 diff --git a/go.sum b/go.sum index 4743913e6..183c2f3ed 100644 --- a/go.sum +++ b/go.sum @@ -502,8 +502,8 @@ github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display h1:Y2WiPkBS github.com/lightninglabs/protobuf-go-hex-display v1.33.0-hex-display/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb h1:yfM05S8DXKhuCBp5qSMZdtSwvJ+GFzl94KbXMNB1JDY= github.com/lightningnetwork/lightning-onion v1.2.1-0.20240712235311-98bd56499dfb/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI= -github.com/lightningnetwork/lnd v0.19.0-beta.rc1 h1:FJvsdw4PZ41ykrHi7vNGit9IIohE+IlKpVwL5/1+L+0= -github.com/lightningnetwork/lnd v0.19.0-beta.rc1/go.mod h1:BP+neeFpmeAA7o5hu3zp3FwOEl26idSyPV9zBOavp6E= +github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f h1:+Bejv2Ij/ryUjLacBd5au0acMH0AYs0lhb7ki5rx9ms= +github.com/lightningnetwork/lnd v0.19.0-beta.rc1.0.20250327183348-eb822a5e117f/go.mod h1:BP+neeFpmeAA7o5hu3zp3FwOEl26idSyPV9zBOavp6E= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= diff --git a/rfq/manager.go b/rfq/manager.go index f93dcd5fb..96da8e781 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -575,7 +575,7 @@ func (m *Manager) addScidAlias(scidAlias uint64, assetSpecifier asset.Specifier, } match, err := m.ChannelCompatible( - ctxb, assetData.Assets, assetSpecifier, + ctxb, assetData, assetSpecifier, ) if err != nil { return err @@ -999,14 +999,13 @@ func (m *Manager) AssetMatchesSpecifier(ctx context.Context, // if the specifier is a group key, then all assets in the channel must belong // to that group. func (m *Manager) ChannelCompatible(ctx context.Context, - jsonAssets []rfqmsg.JsonAssetChanInfo, specifier asset.Specifier) (bool, + jsonChannel rfqmsg.JsonAssetChannel, specifier asset.Specifier) (bool, error) { - for _, chanAsset := range jsonAssets { - gen := chanAsset.AssetInfo.AssetGenesis - assetIDBytes, err := hex.DecodeString( - gen.AssetID, - ) + fundingAssets := jsonChannel.FundingAssets + for _, chanAsset := range fundingAssets { + gen := chanAsset.AssetGenesis + assetIDBytes, err := hex.DecodeString(gen.AssetID) if err != nil { return false, fmt.Errorf("error decoding asset ID: %w", err) diff --git a/rfqmsg/custom_channel_data.go b/rfqmsg/custom_channel_data.go index f252f378c..cd4deb3e8 100644 --- a/rfqmsg/custom_channel_data.go +++ b/rfqmsg/custom_channel_data.go @@ -28,19 +28,19 @@ type JsonAssetUtxo struct { DecimalDisplay uint8 `json:"decimal_display"` } -// JsonAssetChanInfo is a struct that represents the channel information of a -// single asset within a channel. -type JsonAssetChanInfo struct { - AssetInfo JsonAssetUtxo `json:"asset_utxo"` - Capacity uint64 `json:"capacity"` - LocalBalance uint64 `json:"local_balance"` - RemoteBalance uint64 `json:"remote_balance"` -} - // JsonAssetChannel is a struct that represents the channel information of all // assets within a channel. type JsonAssetChannel struct { - Assets []JsonAssetChanInfo `json:"assets"` + FundingAssets []JsonAssetUtxo `json:"funding_assets"` + LocalAssets []JsonAssetTranche `json:"local_assets"` + RemoteAssets []JsonAssetTranche `json:"remote_assets"` + OutgoingHtlcs []JsonAssetTranche `json:"outgoing_htlcs"` + IncomingHtlcs []JsonAssetTranche `json:"incoming_htlcs"` + Capacity uint64 `json:"capacity"` + LocalBalance uint64 `json:"local_balance"` + RemoteBalance uint64 `json:"remote_balance"` + OutgoingHtlcBalance uint64 `json:"outgoing_htlc_balance"` + IncomingHtlcBalance uint64 `json:"incoming_htlc_balance"` } // JsonAssetChannelBalances is a struct that represents the balance information @@ -58,9 +58,9 @@ type JsonCloseOutput struct { ScriptKeys map[string]string `json:"script_keys"` } -// JsonHtlcBalance is a struct that represents the balance of a single asset -// HTLC. -type JsonHtlcBalance struct { +// JsonAssetTranche is a struct that represents the balance of a single asset +// tranche. +type JsonAssetTranche struct { AssetID string `json:"asset_id"` Amount uint64 `json:"amount"` } @@ -68,6 +68,6 @@ type JsonHtlcBalance struct { // JsonHtlc is a struct that represents the asset information that can be // transferred via an HTLC. type JsonHtlc struct { - Balances []*JsonHtlcBalance `json:"balances"` - RfqID string `json:"rfq_id"` + Balances []*JsonAssetTranche `json:"balances"` + RfqID string `json:"rfq_id"` } diff --git a/rfqmsg/records.go b/rfqmsg/records.go index c10f8b07e..596f3659b 100644 --- a/rfqmsg/records.go +++ b/rfqmsg/records.go @@ -177,7 +177,7 @@ func (h *Htlc) ToCustomRecords() (lnwire.CustomRecords, error) { // AsJson returns the Htlc record as a JSON blob. func (h *Htlc) AsJson() ([]byte, error) { j := &JsonHtlc{ - Balances: make([]*JsonHtlcBalance, len(h.Balances())), + Balances: make([]*JsonAssetTranche, len(h.Balances())), } h.RfqID.ValOpt().WhenSome(func(id ID) { @@ -185,7 +185,7 @@ func (h *Htlc) AsJson() ([]byte, error) { }) for idx, balance := range h.Balances() { - j.Balances[idx] = &JsonHtlcBalance{ + j.Balances[idx] = &JsonAssetTranche{ AssetID: hex.EncodeToString(balance.AssetID.Val[:]), Amount: balance.Amount.Val, } diff --git a/rpcserver.go b/rpcserver.go index 2b765fbe0..f81ff85c6 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -8036,7 +8036,7 @@ type channelWithSpecifier struct { channelInfo lndclient.ChannelInfo // assetInfo contains the asset related info of the channel. - assetInfo rfqmsg.JsonAssetChanInfo + assetInfo rfqmsg.JsonAssetChannel } // computeChannelAssetBalance computes the total local and remote balance for @@ -8066,29 +8066,17 @@ func (r *rpcServer) computeChannelAssetBalance(ctx context.Context, // Check if the assets of this channel match the provided // specifier. pass, err := r.cfg.RfqManager.ChannelCompatible( - ctx, assetData.Assets, specifier, + ctx, assetData, specifier, ) if err != nil { return nil, err } if pass { - // Since the assets of the channel passed the above - // filter, we're safe to aggregate their info to be - // represented as a single entity. - var aggrInfo rfqmsg.JsonAssetChanInfo - - // TODO(george): refactor when JSON gets fixed - for _, info := range assetData.Assets { - aggrInfo.Capacity += info.Capacity - aggrInfo.LocalBalance += info.LocalBalance - aggrInfo.RemoteBalance += info.RemoteBalance - } - channels = append(channels, channelWithSpecifier{ specifier: specifier, channelInfo: openChan, - assetInfo: aggrInfo, + assetInfo: assetData, }) } } diff --git a/tapchannelmsg/custom_channel_data.go b/tapchannelmsg/custom_channel_data.go index 8a930ca50..5943db677 100644 --- a/tapchannelmsg/custom_channel_data.go +++ b/tapchannelmsg/custom_channel_data.go @@ -13,6 +13,20 @@ import ( "google.golang.org/protobuf/proto" ) +// outputsToJsonTranches converts a list of asset outputs to a list of JSON +// asset tranches. +func outputsToJsonTranches(outputs []*AssetOutput) []rfqmsg.JsonAssetTranche { + tranches := make([]rfqmsg.JsonAssetTranche, len(outputs)) + for idx, output := range outputs { + tranches[idx] = rfqmsg.JsonAssetTranche{ + Amount: output.Amount.Val, + AssetID: hex.EncodeToString(output.AssetID.Val[:]), + } + } + + return tranches +} + // ReadOpenChannel reads the content of an OpenChannel struct from a reader. func ReadOpenChannel(r io.Reader, maxReadSize uint32) (*OpenChannel, error) { openChanData, err := wire.ReadVarBytes(r, 0, maxReadSize, "chan data") @@ -62,35 +76,54 @@ func (c *ChannelCustomData) AsJson() ([]byte, error) { return []byte{}, nil } - resp := &rfqmsg.JsonAssetChannel{} + resp := &rfqmsg.JsonAssetChannel{ + Capacity: OutputSum(c.OpenChan.Assets()), + LocalBalance: c.LocalCommit.LocalAssets.Val.Sum(), + RemoteBalance: c.LocalCommit.RemoteAssets.Val.Sum(), + OutgoingHtlcBalance: c.LocalCommit.OutgoingHtlcAssets.Val.Sum(), + IncomingHtlcBalance: c.LocalCommit.IncomingHtlcAssets.Val.Sum(), + } + + // First, we encode the funding state, which lists all assets committed + // to the channel at the time of channel opening. for _, output := range c.OpenChan.Assets() { a := output.Proof.Val.Asset assetID := a.ID() - utxo := rfqmsg.JsonAssetUtxo{ - Version: int64(a.Version), - AssetGenesis: rfqmsg.JsonAssetGenesis{ - GenesisPoint: a.FirstPrevOut.String(), - Name: a.Tag, - MetaHash: hex.EncodeToString( - a.MetaHash[:], + groupPubKey := a.ScriptKey.PubKey + resp.FundingAssets = append( + resp.FundingAssets, rfqmsg.JsonAssetUtxo{ + Version: int64(a.Version), + AssetGenesis: rfqmsg.JsonAssetGenesis{ + GenesisPoint: a.FirstPrevOut.String(), + Name: a.Tag, + MetaHash: hex.EncodeToString( + a.MetaHash[:], + ), + AssetID: hex.EncodeToString(assetID[:]), + }, + Amount: a.Amount, + ScriptKey: hex.EncodeToString( + groupPubKey.SerializeCompressed(), ), - AssetID: hex.EncodeToString(assetID[:]), + DecimalDisplay: c.OpenChan.DecimalDisplay.Val, }, - Amount: a.Amount, - ScriptKey: hex.EncodeToString( - a.ScriptKey.PubKey.SerializeCompressed(), - ), - DecimalDisplay: c.OpenChan.DecimalDisplay.Val, - } - resp.Assets = append(resp.Assets, rfqmsg.JsonAssetChanInfo{ - AssetInfo: utxo, - Capacity: output.Amount.Val, - LocalBalance: c.LocalCommit.LocalAssets.Val.Sum(), - RemoteBalance: c.LocalCommit.RemoteAssets.Val.Sum(), - }) + ) } + resp.LocalAssets = outputsToJsonTranches( + c.LocalCommit.LocalAssets.Val.Outputs, + ) + resp.RemoteAssets = outputsToJsonTranches( + c.LocalCommit.RemoteAssets.Val.Outputs, + ) + resp.OutgoingHtlcs = outputsToJsonTranches( + c.LocalCommit.OutgoingHtlcAssets.Val.Outputs(), + ) + resp.IncomingHtlcs = outputsToJsonTranches( + c.LocalCommit.IncomingHtlcAssets.Val.Outputs(), + ) + return json.Marshal(resp) } @@ -123,6 +156,28 @@ func ReadChannelCustomData(chanData []byte) (*ChannelCustomData, error) { }, nil } +// jsonFormatChannelCustomData converts the custom channel data in the given +// byte slice to JSON format. +func jsonFormatChannelCustomData(customData []byte) ([]byte, error) { + if len(customData) == 0 { + return customData, nil + } + + channelData, err := ReadChannelCustomData(customData) + if err != nil { + return nil, fmt.Errorf("error reading custom channel data: %w", + err) + } + + jsonBytes, err := channelData.AsJson() + if err != nil { + return nil, fmt.Errorf("error converting custom channel data "+ + "to JSON: %w", err) + } + + return jsonBytes, nil +} + // BalanceCustomData represents the data that is returned in the // CustomChannelData field of a lnrpc.ChannelBalanceResponse object. type BalanceCustomData struct { @@ -142,7 +197,7 @@ func (b *BalanceCustomData) AsJson() ([]byte, error) { } for _, openChan := range b.OpenChannels { for _, assetOutput := range openChan.LocalOutputs() { - assetID := assetOutput.Proof.Val.Asset.ID() + assetID := assetOutput.AssetID.Val assetIDStr := hex.EncodeToString(assetID[:]) assetName := assetOutput.Proof.Val.Asset.Tag @@ -160,7 +215,7 @@ func (b *BalanceCustomData) AsJson() ([]byte, error) { } for _, assetOutput := range openChan.RemoteOutputs() { - assetID := assetOutput.Proof.Val.Asset.ID() + assetID := assetOutput.AssetID.Val assetIDStr := hex.EncodeToString(assetID[:]) assetName := assetOutput.Proof.Val.Asset.Tag @@ -180,7 +235,7 @@ func (b *BalanceCustomData) AsJson() ([]byte, error) { for _, pendingChan := range b.PendingChannels { for _, assetOutput := range pendingChan.LocalOutputs() { - assetID := assetOutput.Proof.Val.Asset.ID() + assetID := assetOutput.AssetID.Val assetIDStr := hex.EncodeToString(assetID[:]) assetName := assetOutput.Proof.Val.Asset.Tag @@ -198,7 +253,7 @@ func (b *BalanceCustomData) AsJson() ([]byte, error) { } for _, assetOutput := range pendingChan.RemoteOutputs() { - assetID := assetOutput.Proof.Val.Asset.ID() + assetID := assetOutput.AssetID.Val assetIDStr := hex.EncodeToString(assetID[:]) assetName := assetOutput.Proof.Val.Asset.Tag @@ -265,16 +320,39 @@ func ReadBalanceCustomData(balanceData []byte) (*BalanceCustomData, error) { return result, nil } -// replaceCloseOutCustomChannelData replaces the custom channel data in the -// given close output with the JSON representation of the custom data. -func replaceCloseOutCustomChannelData(localOut *lnrpc.CloseOutput) error { - if len(localOut.CustomChannelData) == 0 { - return nil +// jsonFormatBalanceCustomData converts the custom channel data in the given +// byte slice to JSON format. +func jsonFormatBalanceCustomData(customData []byte) ([]byte, error) { + if len(customData) == 0 { + return customData, nil + } + + channelData, err := ReadBalanceCustomData(customData) + if err != nil { + return nil, fmt.Errorf("error reading custom balance data: %w", + err) + } + + jsonBytes, err := channelData.AsJson() + if err != nil { + return nil, fmt.Errorf("error converting custom balance data "+ + "to JSON: %w", err) + } + + return jsonBytes, nil +} + +// jsonFormatCloseOutCustomData converts the custom channel data in the given +// close output with the JSON representation. +func jsonFormatCloseOutCustomData(customData []byte) ([]byte, error) { + if len(customData) == 0 { + return customData, nil } - closeData, err := DecodeAuxShutdownMsg(localOut.CustomChannelData) + closeData, err := DecodeAuxShutdownMsg(customData) if err != nil { - return fmt.Errorf("error reading custom close data: %w", err) + return nil, fmt.Errorf("error reading custom close data: %w", + err) } jsonCloseData := rfqmsg.JsonCloseOutput{ @@ -295,13 +373,35 @@ func replaceCloseOutCustomChannelData(localOut *lnrpc.CloseOutput) error { ) } - localOut.CustomChannelData, err = json.Marshal(jsonCloseData) + jsonBytes, err := json.Marshal(jsonCloseData) if err != nil { - return fmt.Errorf("error converting custom close data to "+ + return nil, fmt.Errorf("error converting custom close data to "+ "JSON: %w", err) } - return nil + return jsonBytes, nil +} + +// jsonFormatHtlcCustomData converts the custom channel data in the given HTLC +// with the JSON representation. +func jsonFormatHtlcCustomData(customData []byte) ([]byte, error) { + if len(customData) == 0 { + return customData, nil + } + + parsedHtlc, err := rfqmsg.DecodeHtlc(customData) + if err != nil { + return nil, fmt.Errorf("error parsing custom channel data: %w", + err) + } + + jsonBytes, err := parsedHtlc.AsJson() + if err != nil { + return nil, fmt.Errorf("error converting custom channel data "+ + "to JSON: %w", err) + } + + return jsonBytes, nil } // ParseCustomChannelData parses the custom channel data in the given lnd RPC @@ -312,41 +412,25 @@ func ParseCustomChannelData(msg proto.Message) error { for idx := range m.Channels { rpcChannel := m.Channels[idx] - if len(rpcChannel.CustomChannelData) == 0 { - continue - } - - channelData, err := ReadChannelCustomData( + customData, err := jsonFormatChannelCustomData( rpcChannel.CustomChannelData, ) if err != nil { - return fmt.Errorf("error reading custom "+ - "channel data: %w", err) + return err } - rpcChannel.CustomChannelData, err = channelData.AsJson() - if err != nil { - return fmt.Errorf("error converting custom "+ - "channel data to JSON: %w", err) - } + rpcChannel.CustomChannelData = customData } case *lnrpc.ChannelBalanceResponse: - if len(m.CustomChannelData) == 0 { - return nil - } - - balanceData, err := ReadBalanceCustomData(m.CustomChannelData) + customData, err := jsonFormatBalanceCustomData( + m.CustomChannelData, + ) if err != nil { - return fmt.Errorf("error reading custom balance "+ - "data: %w", err) + return err } - m.CustomChannelData, err = balanceData.AsJson() - if err != nil { - return fmt.Errorf("error converting custom balance "+ - "data to JSON: %w", err) - } + m.CustomChannelData = customData case *lnrpc.PendingChannelsResponse: for idx := range m.PendingOpenChannels { @@ -357,23 +441,68 @@ func ParseCustomChannelData(msg proto.Message) error { continue } - if len(rpcChannel.CustomChannelData) == 0 { + customData, err := jsonFormatChannelCustomData( + rpcChannel.CustomChannelData, + ) + if err != nil { + return err + } + + rpcChannel.CustomChannelData = customData + } + + for idx := range m.PendingForceClosingChannels { + pendingForceClose := m.PendingForceClosingChannels[idx] + rpcChannel := pendingForceClose.Channel + + if rpcChannel == nil { + continue + } + + customData, err := jsonFormatChannelCustomData( + rpcChannel.CustomChannelData, + ) + if err != nil { + return err + } + + rpcChannel.CustomChannelData = customData + } + + for idx := range m.WaitingCloseChannels { + waitingClose := m.WaitingCloseChannels[idx] + rpcChannel := waitingClose.Channel + + if rpcChannel == nil { continue } - channelData, err := ReadChannelCustomData( + customData, err := jsonFormatChannelCustomData( rpcChannel.CustomChannelData, ) if err != nil { - return fmt.Errorf("error reading custom "+ - "channel data: %w", err) + return err } - rpcChannel.CustomChannelData, err = channelData.AsJson() + rpcChannel.CustomChannelData = customData + } + + case *lnrpc.ClosedChannelsResponse: + for idx := range m.Channels { + rpcChannel := m.Channels[idx] + + if rpcChannel == nil { + continue + } + + customData, err := jsonFormatChannelCustomData( + rpcChannel.CustomChannelData, + ) if err != nil { - return fmt.Errorf("error converting custom "+ - "channel data to JSON: %w", err) + return err } + + rpcChannel.CustomChannelData = customData } case *lnrpc.CloseStatusUpdate: @@ -384,60 +513,50 @@ func ParseCustomChannelData(msg proto.Message) error { localOut := closeUpd.ChanClose.LocalCloseOutput if localOut != nil { - err := replaceCloseOutCustomChannelData(localOut) + customData, err := jsonFormatCloseOutCustomData( + localOut.CustomChannelData, + ) if err != nil { - return fmt.Errorf("error replacing local "+ - "custom close data: %w", err) + return err } + + localOut.CustomChannelData = customData } remoteOut := closeUpd.ChanClose.RemoteCloseOutput if remoteOut != nil { - err := replaceCloseOutCustomChannelData(remoteOut) + customData, err := jsonFormatCloseOutCustomData( + remoteOut.CustomChannelData, + ) if err != nil { - return fmt.Errorf("error replacing remote "+ - "custom close data: %w", err) + return err } - } - case *lnrpc.Route: - if len(m.CustomChannelData) == 0 { - return nil + remoteOut.CustomChannelData = customData } - parsedHtlc, err := rfqmsg.DecodeHtlc(m.CustomChannelData) + case *lnrpc.Route: + customData, err := jsonFormatHtlcCustomData( + m.CustomChannelData, + ) if err != nil { - return fmt.Errorf("error parsing custom "+ - "channel data: %w", err) + return err } - m.CustomChannelData, err = parsedHtlc.AsJson() - if err != nil { - return fmt.Errorf("error converting custom "+ - "channel data to JSON: %w", err) - } + m.CustomChannelData = customData case *lnrpc.Invoice: for idx := range m.Htlcs { htlc := m.Htlcs[idx] - if len(htlc.CustomChannelData) == 0 { - continue - } - - parsedHtlc, err := rfqmsg.DecodeHtlc( + customData, err := jsonFormatHtlcCustomData( htlc.CustomChannelData, ) if err != nil { - return fmt.Errorf("error parsing custom "+ - "channel data: %w", err) + return err } - htlc.CustomChannelData, err = parsedHtlc.AsJson() - if err != nil { - return fmt.Errorf("error converting custom "+ - "channel data to JSON: %w", err) - } + htlc.CustomChannelData = customData } } diff --git a/tapchannelmsg/custom_channel_data_test.go b/tapchannelmsg/custom_channel_data_test.go new file mode 100644 index 000000000..bd23d189e --- /dev/null +++ b/tapchannelmsg/custom_channel_data_test.go @@ -0,0 +1,313 @@ +package tapchannelmsg + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/internal/test" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/require" +) + +var ( + hexStr = hex.EncodeToString + pubKeyHexStr = func(pubKey *btcec.PublicKey) string { + return hex.EncodeToString(pubKey.SerializeCompressed()) + } +) + +// TestReadChannelCustomData tests that we can read the custom data from a +// channel state response and format it as JSON. +func TestReadChannelCustomData(t *testing.T) { + proof1 := randProof(t) + proof2 := randProof(t) + proof3 := randProof(t) + proof4 := randProof(t) + assetID1 := proof1.Asset.ID() + assetID2 := proof2.Asset.ID() + assetID3 := proof3.Asset.ID() + assetID4 := proof4.Asset.ID() + output1 := NewAssetOutput(assetID1, 1000, proof1) + output2 := NewAssetOutput(assetID2, 2000, proof2) + output3 := NewAssetOutput(assetID3, 3000, proof3) + output4 := NewAssetOutput(assetID4, 4000, proof4) + + fundingState := NewOpenChannel([]*AssetOutput{output1, output2}, 11) + commitState := NewCommitment( + []*AssetOutput{output1}, []*AssetOutput{output2}, + map[input.HtlcIndex][]*AssetOutput{ + 1: {output3}, + }, map[input.HtlcIndex][]*AssetOutput{ + 2: {output4}, + }, lnwallet.CommitAuxLeaves{}, + ) + + fundingBlob := fundingState.Bytes() + commitBlob := commitState.Bytes() + + var customChannelData bytes.Buffer + require.NoError(t, wire.WriteVarBytes( + &customChannelData, 0, fundingBlob, + )) + require.NoError(t, wire.WriteVarBytes( + &customChannelData, 0, commitBlob, + )) + + channelJSON, err := jsonFormatChannelCustomData( + customChannelData.Bytes(), + ) + require.NoError(t, err) + + var formattedJSON bytes.Buffer + err = json.Indent(&formattedJSON, channelJSON, "", " ") + require.NoError(t, err) + + expected := `{ + "funding_assets": [ + { + "version": 0, + "asset_genesis": { + "genesis_point": "` + proof1.Asset.FirstPrevOut.String() + `", + "name": "` + proof1.Asset.Tag + `", + "meta_hash": "` + hexStr(proof1.Asset.MetaHash[:]) + `", + "asset_id": "` + hexStr(assetID1[:]) + `" + }, + "amount": 1, + "script_key": "` + pubKeyHexStr(proof1.Asset.ScriptKey.PubKey) + `", + "decimal_display": 11 + }, + { + "version": 0, + "asset_genesis": { + "genesis_point": "` + proof2.Asset.FirstPrevOut.String() + `", + "name": "` + proof2.Asset.Tag + `", + "meta_hash": "` + hexStr(proof2.Asset.MetaHash[:]) + `", + "asset_id": "` + hexStr(assetID2[:]) + `" + }, + "amount": 1, + "script_key": "` + pubKeyHexStr(proof2.Asset.ScriptKey.PubKey) + `", + "decimal_display": 11 + } + ], + "local_assets": [ + { + "asset_id": "` + hexStr(assetID1[:]) + `", + "amount": 1000 + } + ], + "remote_assets": [ + { + "asset_id": "` + hexStr(assetID2[:]) + `", + "amount": 2000 + } + ], + "outgoing_htlcs": [ + { + "asset_id": "` + hexStr(assetID3[:]) + `", + "amount": 3000 + } + ], + "incoming_htlcs": [ + { + "asset_id": "` + hexStr(assetID4[:]) + `", + "amount": 4000 + } + ], + "capacity": 3000, + "local_balance": 1000, + "remote_balance": 2000, + "outgoing_htlc_balance": 3000, + "incoming_htlc_balance": 4000 +}` + require.Equal(t, expected, formattedJSON.String()) +} + +// TestReadBalanceCustomData tests that we can read the custom data from a +// channel balance response and format it as JSON. +func TestReadBalanceCustomData(t *testing.T) { + proof1 := randProof(t) + proof2 := randProof(t) + proof3 := randProof(t) + assetID1 := proof1.Asset.ID() + assetID2 := proof2.Asset.ID() + assetID3 := proof3.Asset.ID() + output1 := NewAssetOutput(assetID1, 1000, proof1) + output2 := NewAssetOutput(assetID2, 2000, proof2) + output3 := NewAssetOutput(assetID3, 3000, proof3) + + openChannel1 := NewCommitment( + []*AssetOutput{output1}, []*AssetOutput{output2}, nil, nil, + lnwallet.CommitAuxLeaves{}, + ) + openChannel2 := NewCommitment( + []*AssetOutput{output2}, []*AssetOutput{output3}, nil, nil, + lnwallet.CommitAuxLeaves{}, + ) + pendingChannel1 := NewCommitment( + []*AssetOutput{output3}, nil, nil, nil, + lnwallet.CommitAuxLeaves{}, + ) + pendingChannel2 := NewCommitment( + nil, []*AssetOutput{output1}, nil, nil, + lnwallet.CommitAuxLeaves{}, + ) + + var customChannelData bytes.Buffer + require.NoError(t, wire.WriteVarInt(&customChannelData, 0, 2)) + require.NoError(t, wire.WriteVarBytes( + &customChannelData, 0, openChannel1.Bytes(), + )) + require.NoError(t, wire.WriteVarBytes( + &customChannelData, 0, openChannel2.Bytes(), + )) + require.NoError(t, wire.WriteVarInt(&customChannelData, 0, 2)) + require.NoError(t, wire.WriteVarBytes( + &customChannelData, 0, pendingChannel1.Bytes(), + )) + require.NoError(t, wire.WriteVarBytes( + &customChannelData, 0, pendingChannel2.Bytes(), + )) + + channelJSON, err := jsonFormatBalanceCustomData( + customChannelData.Bytes(), + ) + require.NoError(t, err) + + var formattedJSON bytes.Buffer + err = json.Indent(&formattedJSON, channelJSON, "", " ") + require.NoError(t, err) + + // The results are in a map, so the order can't be predicted. But we + // have distinct balances, so we can just make sure the expected values + // appear in the JSON. + expectedOpen1 := `"` + hexStr(assetID1[:]) + `": { + "asset_id": "` + hexStr(assetID1[:]) + `", + "name": "` + proof1.Asset.Tag + `", + "local_balance": 1000, + "remote_balance": 0 + }` + expectedOpen2 := `"` + hexStr(assetID2[:]) + `": { + "asset_id": "` + hexStr(assetID2[:]) + `", + "name": "` + proof2.Asset.Tag + `", + "local_balance": 2000, + "remote_balance": 2000 + }` + expectedOpen3 := `"` + hexStr(assetID3[:]) + `": { + "asset_id": "` + hexStr(assetID3[:]) + `", + "name": "` + proof3.Asset.Tag + `", + "local_balance": 0, + "remote_balance": 3000 + }` + + expectedPending1 := `"` + hexStr(assetID1[:]) + `": { + "asset_id": "` + hexStr(assetID1[:]) + `", + "name": "` + proof1.Asset.Tag + `", + "local_balance": 0, + "remote_balance": 1000 + }` + expectedPending2 := `"` + hexStr(assetID3[:]) + `": { + "asset_id": "` + hexStr(assetID3[:]) + `", + "name": "` + proof3.Asset.Tag + `", + "local_balance": 3000, + "remote_balance": 0 + }` + + require.Contains(t, formattedJSON.String(), expectedOpen1) + require.Contains(t, formattedJSON.String(), expectedOpen2) + require.Contains(t, formattedJSON.String(), expectedOpen3) + require.Contains(t, formattedJSON.String(), expectedPending1) + require.Contains(t, formattedJSON.String(), expectedPending2) +} + +// TestCloseOutCustomData tests that we can read the custom data from a channel +// close response and format it as JSON. +func TestCloseOutCustomData(t *testing.T) { + testAssetInternalKey := test.RandPubKey(t) + testBtcInternalKey := test.RandPubKey(t) + + testScriptKeys := make(ScriptKeyMap) + + const numScriptKeys = 10 + for i := 0; i < numScriptKeys; i++ { + testScriptKeys[[32]byte{byte(i)}] = *test.RandPubKey(t) + } + + shutdownMsg := NewAuxShutdownMsg( + testBtcInternalKey, testAssetInternalKey, + testScriptKeys, nil, + ) + + var customChannelData bytes.Buffer + require.NoError(t, shutdownMsg.Encode(&customChannelData)) + + channelJSON, err := jsonFormatCloseOutCustomData( + customChannelData.Bytes(), + ) + require.NoError(t, err) + + var formattedJSON bytes.Buffer + err = json.Indent(&formattedJSON, channelJSON, "", " ") + require.NoError(t, err) + + expectedStart := `{ + "btc_internal_key": "` + pubKeyHexStr(testBtcInternalKey) + `", + "asset_internal_key": "` + pubKeyHexStr(testAssetInternalKey) + `", + "script_keys": {` + require.Contains(t, formattedJSON.String(), expectedStart) + + for id, key := range testScriptKeys { + expected := `"` + hexStr(id[:]) + `": "` + + pubKeyHexStr(&key) + `"` + require.Contains(t, formattedJSON.String(), expected) + } +} + +// TestHtlcCustomData tests that we can read the custom data from a HTLC +// response and format it as JSON. +func TestHtlcCustomData(t *testing.T) { + assetID1 := [32]byte{1} + assetID2 := [32]byte{2} + assetID3 := [32]byte{3} + rfqID := rfqmsg.ID{0, 1, 2, 3, 4, 5, 6, 7} + htlc := rfqmsg.NewHtlc([]*rfqmsg.AssetBalance{ + rfqmsg.NewAssetBalance(assetID1, 1000), + rfqmsg.NewAssetBalance(assetID2, 2000), + rfqmsg.NewAssetBalance(assetID3, 5000), + }, fn.Some(rfqID)) + + var customChannelData bytes.Buffer + require.NoError(t, htlc.Encode(&customChannelData)) + + channelJSON, err := jsonFormatHtlcCustomData(customChannelData.Bytes()) + require.NoError(t, err) + + var formattedJSON bytes.Buffer + err = json.Indent(&formattedJSON, channelJSON, "", " ") + require.NoError(t, err) + + expected := `{ + "balances": [ + { + "asset_id": "` + hexStr(assetID1[:]) + `", + "amount": 1000 + }, + { + "asset_id": "` + hexStr(assetID2[:]) + `", + "amount": 2000 + }, + { + "asset_id": "` + hexStr(assetID3[:]) + `", + "amount": 5000 + } + ], + "rfq_id": "` + hexStr(rfqID[:]) + `" +}` + require.Equal(t, expected, formattedJSON.String()) +} diff --git a/tapchannelmsg/records.go b/tapchannelmsg/records.go index 1c84be4b3..959649f62 100644 --- a/tapchannelmsg/records.go +++ b/tapchannelmsg/records.go @@ -19,6 +19,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" + "golang.org/x/exp/maps" ) const ( @@ -1325,6 +1326,21 @@ type HtlcAssetOutput struct { HtlcOutputs map[input.HtlcIndex]AssetOutputListRecord } +// Sum returns the sum of the amounts of all the asset outputs in the list. +func (h *HtlcAssetOutput) Sum() uint64 { + return OutputSum(h.Outputs()) +} + +// Outputs returns a flat list of all the asset outputs in the list. +func (h *HtlcAssetOutput) Outputs() []*AssetOutput { + return fn.FlatMap( + maps.Values(h.HtlcOutputs), + func(r AssetOutputListRecord) []*AssetOutput { + return r.Outputs + }, + ) +} + // NewHtlcAssetOutput creates a new HtlcAssetOutput record with the given HTLC // outputs. func NewHtlcAssetOutput( diff --git a/tapchannelmsg/records_test.go b/tapchannelmsg/records_test.go index 9b16ccab7..ada63de15 100644 --- a/tapchannelmsg/records_test.go +++ b/tapchannelmsg/records_test.go @@ -35,10 +35,8 @@ var ( ) ) -// TestOpenChannel tests encoding and decoding of the OpenChannel struct. -func TestOpenChannel(t *testing.T) { - t.Parallel() - +// randProof creates a new, random proof. +func randProof(t *testing.T) proof.Proof { oddTxBlockHex, err := os.ReadFile(oddTxBlockHexFileName) require.NoError(t, err) @@ -53,12 +51,16 @@ func TestOpenChannel(t *testing.T) { randGen := asset.RandGenesis(t, asset.Normal) scriptKey1 := test.RandPubKey(t) - originalRandProof := proof.RandProof( - t, randGen, scriptKey1, oddTxBlock, 0, 1, - ) + return proof.RandProof(t, randGen, scriptKey1, oddTxBlock, 0, 1) +} + +// TestOpenChannel tests encoding and decoding of the OpenChannel struct. +func TestOpenChannel(t *testing.T) { + t.Parallel() // Proofs don't Encode everything, so we need to do a quick Encode/ // Decode cycle to make sure we can compare it afterward. + originalRandProof := randProof(t) proofBytes, err := proof.Encode(&originalRandProof) require.NoError(t, err) randProof, err := proof.Decode(proofBytes) @@ -183,26 +185,9 @@ func TestAuxLeaves(t *testing.T) { func TestCommitment(t *testing.T) { t.Parallel() - oddTxBlockHex, err := os.ReadFile(oddTxBlockHexFileName) - require.NoError(t, err) - - oddTxBlockBytes, err := hex.DecodeString( - strings.Trim(string(oddTxBlockHex), "\n"), - ) - require.NoError(t, err) - - var oddTxBlock wire.MsgBlock - err = oddTxBlock.Deserialize(bytes.NewReader(oddTxBlockBytes)) - require.NoError(t, err) - - randGen := asset.RandGenesis(t, asset.Normal) - scriptKey1 := test.RandPubKey(t) - originalRandProof := proof.RandProof( - t, randGen, scriptKey1, oddTxBlock, 0, 1, - ) - // Proofs don't Encode everything, so we need to do a quick Encode/ // Decode cycle to make sure we can compare it afterward. + originalRandProof := randProof(t) proofBytes, err := proof.Encode(&originalRandProof) require.NoError(t, err) randProof, err := proof.Decode(proofBytes)