diff --git a/config_builder.go b/config_builder.go index ca32ac4cd..ee4064035 100644 --- a/config_builder.go +++ b/config_builder.go @@ -178,6 +178,10 @@ type AuxComponents struct { // AuxSigner is an optional signer that can be used to sign auxiliary // leaves for certain custom channel types. AuxSigner fn.Option[lnwallet.AuxSigner] + + // AuxDataParser is an optional data parser that can be used to parse + // auxiliary data for certain custom channel types. + AuxDataParser fn.Option[AuxDataParser] } // DefaultWalletImpl is the default implementation of our normal, btcwallet diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go index 6694e8b4f..4ef60a71d 100644 --- a/lnrpc/routerrpc/router_backend.go +++ b/lnrpc/routerrpc/router_backend.go @@ -25,6 +25,7 @@ import ( "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/subscribe" "github.com/lightningnetwork/lnd/zpay32" + "google.golang.org/protobuf/proto" ) const ( @@ -104,6 +105,10 @@ type RouterBackend struct { // TODO(yy): remove this config after the new status code is fully // deployed to the network(v0.20.0). UseStatusInitiated bool + + // ParseCustomChannelData is a function that can be used to parse custom + // channel data from the first hop of a route. + ParseCustomChannelData func(message proto.Message) error } // MissionControl defines the mission control dependencies of routerrpc. @@ -596,8 +601,14 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error) resp.CustomChannelData = customData - // TODO(guggero): Feed the route into the custom data parser - // (part 3 of the mega PR series). + // Allow the aux data parser to parse the custom records into + // a human-readable JSON (if available). + if r.ParseCustomChannelData != nil { + err := r.ParseCustomChannelData(resp) + if err != nil { + return nil, err + } + } } incomingAmt := route.TotalAmount diff --git a/rpcserver.go b/rpcserver.go index 6b9be6a91..841dffe8f 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -87,6 +87,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "gopkg.in/macaroon-bakery.v2/bakery" ) @@ -579,6 +580,17 @@ func MainRPCServerPermissions() map[string][]bakery.Op { } } +// AuxDataParser is an interface that is used to parse auxiliary custom data +// within RPC messages. This is used to transform binary blobs to human-readable +// JSON representations. +type AuxDataParser interface { + // InlineParseCustomData replaces any custom data binary blob in the + // given RPC message with its corresponding JSON formatted data. This + // transforms the binary (likely TLV encoded) data to a human-readable + // JSON representation (still as byte slice). + InlineParseCustomData(msg proto.Message) error +} + // rpcServer is a gRPC, RPC front end to the lnd daemon. // TODO(roasbeef): pagination support for the list-style calls type rpcServer struct { @@ -731,6 +743,20 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service, }, SetChannelAuto: s.chanStatusMgr.RequestAuto, UseStatusInitiated: subServerCgs.RouterRPC.UseStatusInitiated, + ParseCustomChannelData: func(msg proto.Message) error { + err = fn.MapOptionZ( + r.server.implCfg.AuxDataParser, + func(parser AuxDataParser) error { + return parser.InlineParseCustomData(msg) + }, + ) + if err != nil { + return fmt.Errorf("error parsing custom data: "+ + "%w", err) + } + + return nil + }, } genInvoiceFeatures := func() *lnwire.FeatureVector { @@ -3603,7 +3629,7 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, unsettledRemoteBalance, pendingOpenLocalBalance, pendingOpenRemoteBalance) - return &lnrpc.ChannelBalanceResponse{ + resp := &lnrpc.ChannelBalanceResponse{ LocalBalance: &lnrpc.Amount{ Sat: uint64(localBalance.ToSatoshis()), Msat: uint64(localBalance), @@ -3633,7 +3659,19 @@ func (r *rpcServer) ChannelBalance(ctx context.Context, // Deprecated fields. Balance: int64(localBalance.ToSatoshis()), PendingOpenBalance: int64(pendingOpenLocalBalance.ToSatoshis()), - }, nil + } + + err = fn.MapOptionZ( + r.server.implCfg.AuxDataParser, + func(parser AuxDataParser) error { + return parser.InlineParseCustomData(resp) + }, + ) + if err != nil { + return nil, fmt.Errorf("error parsing custom data: %w", err) + } + + return resp, nil } type ( @@ -4075,6 +4113,16 @@ func (r *rpcServer) PendingChannels(ctx context.Context, resp.WaitingCloseChannels = waitingCloseChannels resp.TotalLimboBalance += limbo + err = fn.MapOptionZ( + r.server.implCfg.AuxDataParser, + func(parser AuxDataParser) error { + return parser.InlineParseCustomData(resp) + }, + ) + if err != nil { + return nil, fmt.Errorf("error parsing custom data: %w", err) + } + return resp, nil } @@ -4389,6 +4437,16 @@ func (r *rpcServer) ListChannels(ctx context.Context, resp.Channels = append(resp.Channels, channel) } + err = fn.MapOptionZ( + r.server.implCfg.AuxDataParser, + func(parser AuxDataParser) error { + return parser.InlineParseCustomData(resp) + }, + ) + if err != nil { + return nil, fmt.Errorf("error parsing custom data: %w", err) + } + return resp, nil } diff --git a/rpcserver_test.go b/rpcserver_test.go index ca70ad9df..9dd3c3f86 100644 --- a/rpcserver_test.go +++ b/rpcserver_test.go @@ -1,14 +1,79 @@ package lnd import ( + "fmt" "testing" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/fn" + "github.com/lightningnetwork/lnd/lnrpc" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) func TestGetAllPermissions(t *testing.T) { perms := GetAllPermissions() - // Currently there are there are 16 entity:action pairs in use. + // Currently there are 16 entity:action pairs in use. assert.Equal(t, len(perms), 16) } + +// mockDataParser is a mock implementation of the AuxDataParser interface. +type mockDataParser struct { +} + +// InlineParseCustomData replaces any custom data binary blob in the given RPC +// message with its corresponding JSON formatted data. This transforms the +// binary (likely TLV encoded) data to a human-readable JSON representation +// (still as byte slice). +func (m *mockDataParser) InlineParseCustomData(msg proto.Message) error { + switch m := msg.(type) { + case *lnrpc.ChannelBalanceResponse: + m.CustomChannelData = []byte(`{"foo": "bar"}`) + + return nil + + default: + return fmt.Errorf("mock only supports ChannelBalanceResponse") + } +} + +func TestAuxDataParser(t *testing.T) { + // We create an empty channeldb, so we can fetch some channels. + cdb, err := channeldb.Open(t.TempDir()) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, cdb.Close()) + }) + + r := &rpcServer{ + server: &server{ + chanStateDB: cdb.ChannelStateDB(), + implCfg: &ImplementationCfg{ + AuxComponents: AuxComponents{ + AuxDataParser: fn.Some[AuxDataParser]( + &mockDataParser{}, + ), + }, + }, + }, + } + + // With the aux data parser in place, we should get a formatted JSON + // in the custom channel data field. + resp, err := r.ChannelBalance(nil, &lnrpc.ChannelBalanceRequest{}) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, []byte(`{"foo": "bar"}`), resp.CustomChannelData) + + // If we don't supply the aux data parser, we should get the raw binary + // data. Which in this case is just two VarInt fields (1 byte each) that + // represent the value of 0 (zero active and zero pending channels). + r.server.implCfg.AuxComponents.AuxDataParser = fn.None[AuxDataParser]() + + resp, err = r.ChannelBalance(nil, &lnrpc.ChannelBalanceRequest{}) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, []byte{0x00, 0x00}, resp.CustomChannelData) +}