lnd: add aux data parser

This commit adds an optional data parser that can inspect and in-place
format custom data of certain RPC messages.
We don't add an implementation of the interface itself, as that will be
provided by external components when packaging up lnd as a bundle with
other software.
This commit is contained in:
Oliver Gugger 2024-05-17 15:19:28 +02:00
parent 1b3f386979
commit a780c81340
No known key found for this signature in database
GPG key ID: 8E4256593F177720
3 changed files with 107 additions and 3 deletions

View file

@ -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

View file

@ -46,6 +46,7 @@ import (
"github.com/lightningnetwork/lnd/contractcourt"
"github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/feature"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/funding"
"github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
@ -82,6 +83,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"
)
@ -567,6 +569,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 {
@ -3544,7 +3557,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),
@ -3574,7 +3587,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 (
@ -4330,6 +4355,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
}

View file

@ -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)
}