Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to filter reported channels #19

Merged
merged 9 commits into from
Dec 20, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 39 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,27 @@ USAGE:
balance-agent [global options] command [command options] [arguments...]

VERSION:
v0.0.22
v0.0.38
fiksn marked this conversation as resolved.
Show resolved Hide resolved

COMMANDS:
help, h Shows a list of commands or help for one command

GLOBAL OPTIONS:
--apikey value api key
--rpcserver value host:port of ln daemon (default: "localhost:10009")
--lnddir value path to lnd's base directory (default: "/home/user/lnd")
--tlscertpath value path to TLS certificate (default: "/home/user/lnd/tls.cert")
--chain value, -c value the chain lnd is running on e.g. bitcoin (default: "bitcoin")
--network value, -n value the network lnd is running on e.g. mainnet, testnet, etc. (default: "mainnet")
--macaroonpath value path to macaroon file
--allowedentropy value allowed entropy in bits for channel balances (default: 64)
--interval value interval to poll - 10s, 1m, 10m or 1h (default: "10s")
--private report private data as well (default: false)
--preferipv4 If you have the choice between IPv6 and IPv4 prefer IPv4 (default: false)
--verbosity value log level for V logs (default: 0)
--help, -h show help
--version, -v print the version
--apikey value api key
--rpcserver value host:port of ln daemon (default: "localhost:10009")
--lnddir value path to lnd's base directory (default: "/home/user/.lnd")
--tlscertpath value path to TLS certificate (default: "/home/user/.lnd/tls.cert")
--chain value, -c value the chain lnd is running on e.g. bitcoin (default: "bitcoin")
--network value, -n value the network lnd is running on e.g. mainnet, testnet, etc. (default: "mainnet")
--macaroonpath value path to macaroon file
--allowedentropy value allowed entropy in bits for channel balances (default: 64)
--interval value interval to poll - 10s, 1m, 10m or 1h (default: "10s")
--private report private data as well (default: false)
--preferipv4 If you have the choice between IPv6 and IPv4 prefer IPv4 (default: false)
--channels-whitelist value Path to file containing a whitelist of channels
--verbosity value log level for V logs (default: 0)
--help, -h show help
--version, -v print the version
```

It tries the best to have sane defaults so you can just start it up on your node without further hassle.
Expand Down Expand Up @@ -129,11 +130,34 @@ Usage:
docker run -v /tmp:/tmp -e API_KEY=changeme ghcr.io/bolt-observer/agent:v0.0.35
```

## Filtering on agent side

You can limit what channnels are reported using `--channels-whitelist` option. It specifies a path of a local file to be used as a whitelist of what channels to report.
`--channels-whitelist` implies `--private` (and specifying both is a mistake). The format of the file is simple:

```
# Comments start with a # character
# You can list pubkeys for example:
0288037d3f0bdcfb240402b43b80cdc32e41528b3e2ebe05884aff507d71fca71a # bolt.observer
# which means any channel where peer pubkey is this
# or you can specify a specific short channel id e.g.,
759930760125546497
# too, invalid lines like
whatever
# will be ignored (and logged as a warning, aliases also don't work!)
# Validity for channel id is not checked (it just has to be numeric), thus:
1337
# is perfectly valid (altho it won't match and thus allow the reporting of
# any additional channel).
# Empty files means nothing - in whitelist context: do not report anything.
```

## Components

Internally we use:
* [channelchecker](./channelchecker): an abstraction for checking all channels
* [nodeinfo](./nodeinfo): this can basically report `lncli getnodeinfo` for your node - it is used by the agent so we have a full view of node info & channels
* [filter](./filter): this is used to filter specific channels on the agent side
* [checkermonitoring](./checkermonitoring): is used for reporting metrics via Graphite (not used directly in balance-agent here)
* [lightning_api](./lightning_api): an abstraction around lightning node API (that furthermore heavily depends on common code from [lnd](https://github.com/lightningnetwork/lnd))

Expand Down
24 changes: 22 additions & 2 deletions channelchecker/channelchecker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

checkermonitoring "github.com/bolt-observer/agent/checkermonitoring"
entities "github.com/bolt-observer/agent/entities"
"github.com/bolt-observer/agent/filter"
api "github.com/bolt-observer/agent/lightning_api"
common_entities "github.com/bolt-observer/go_common/entities"
utils "github.com/bolt-observer/go_common/utils"
Expand Down Expand Up @@ -109,6 +110,12 @@ func (c *ChannelChecker) Subscribe(
settings.NoopInterval = c.keepAliveInterval
}

if settings.Filter == nil {
glog.V(3).Infof("Filter was nil, allowing everything")
f, _ := filter.NewAllowAllFilter()
settings.Filter = f
}

c.globalSettings.Set(info.IdentityPubkey+uniqueId, Settings{
identifier: entities.NodeIdentifier{Identifier: pubKey, UniqueId: uniqueId},
settings: settings,
Expand Down Expand Up @@ -140,6 +147,11 @@ func (c *ChannelChecker) GetState(
return nil, errors.New("invalid pubkey")
}

if settings.Filter == nil {
f, _ := filter.NewAllowAllFilter()
settings.Filter = f
}

resp, err := c.checkOne(entities.NodeIdentifier{Identifier: pubKey, UniqueId: uniqueId}, getApi, settings, true, false)
if err != nil {
return nil, err
Expand All @@ -156,7 +168,9 @@ func (c *ChannelChecker) getChannelList(
api api.LightingApiCalls,
info *api.InfoApi,
precisionBits int,
allowPrivateChans bool) ([]entities.ChannelBalance, SetOfChanIds, error) {
allowPrivateChans bool,
filter filter.FilterInterface,
) ([]entities.ChannelBalance, SetOfChanIds, error) {

defer c.monitoring.MetricsTimer("channellist", map[string]string{"pubkey": info.IdentityPubkey})()

Expand All @@ -180,6 +194,12 @@ func (c *ChannelChecker) getChannelList(

for _, channel := range channels.Channels {
if channel.Private && !allowPrivateChans {
glog.V(3).Infof("Skipping private channel %v", channel.ChanId)
continue
}

if !filter.AllowChanId(channel.ChanId) && !filter.AllowPubKey(channel.RemotePubkey) {
glog.V(3).Infof("Filtering channel %v", channel.ChanId)
continue
}

Expand Down Expand Up @@ -474,7 +494,7 @@ func (c *ChannelChecker) checkOne(
identifier.Identifier = info.IdentityPubkey
}

channelList, set, err := c.getChannelList(api, info, settings.AllowedEntropy, settings.AllowPrivateChannels)
channelList, set, err := c.getChannelList(api, info, settings.AllowedEntropy, settings.AllowPrivateChannels, settings.Filter)
if err != nil {
c.monitoring.MetricsReport("checkone", "failure", map[string]string{"pubkey": pubkey})
return nil, err
Expand Down
124 changes: 124 additions & 0 deletions channelchecker/channelchecker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (

miniredis "github.com/alicebob/miniredis/v2"
agent_entities "github.com/bolt-observer/agent/entities"
"github.com/bolt-observer/agent/filter"
lightning_api "github.com/bolt-observer/agent/lightning_api"
entities "github.com/bolt-observer/go_common/entities"
utils "github.com/bolt-observer/go_common/utils"
Expand Down Expand Up @@ -216,6 +217,129 @@ func TestBasicFlow(t *testing.T) {
}
}

func TestBasicFlowFilterOne(t *testing.T) {
pubKey, api, d := initTest(t)

d.HttpApi.DoFunc = func(req *http.Request) (*http.Response, error) {
contents := ""
if strings.Contains(req.URL.Path, "v1/getinfo") {
contents = getInfoJson("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256")
} else if strings.Contains(req.URL.Path, "v1/channels") {
contents = getChannelJson(1337, false, true)
}

r := ioutil.NopCloser(bytes.NewReader([]byte(contents)))

return &http.Response{
StatusCode: 200,
Body: r,
}, nil
}

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(15*time.Second))

c := NewDefaultChannelChecker(ctx, time.Duration(0), true, false, nil)
// Make everything a bit faster
c.OverrideLoopInterval(1 * time.Second)
was_called := false

f, _ := filter.NewUnitTestFilter()
fd := f.(*filter.UnitTestFilter)
fd.AddAllowChanId(1)
fd.AddAllowChanId(1337)

c.Subscribe(
pubKey, "random_id",
func() lightning_api.LightingApiCalls { return api },
agent_entities.ReportingSettings{
AllowedEntropy: 64,
PollInterval: agent_entities.SECOND,
AllowPrivateChannels: true,
Filter: f,
},
func(ctx context.Context, report *agent_entities.ChannelBalanceReport) bool {
if len(report.ChangedChannels) == 1 && report.UniqueId == "random_id" {
was_called = true
}

cancel()
return true
},
)

c.EventLoop()

select {
case <-time.After(5 * time.Second):
t.Fatal("Took too long")
case <-ctx.Done():
if !was_called {
t.Fatalf("Callback was not correctly invoked")
}
}
}

func TestBasicFlowFilterTwo(t *testing.T) {
pubKey, api, d := initTest(t)

d.HttpApi.DoFunc = func(req *http.Request) (*http.Response, error) {
contents := ""
if strings.Contains(req.URL.Path, "v1/getinfo") {
contents = getInfoJson("02b67e55fb850d7f7d77eb71038362bc0ed0abd5b7ee72cc4f90b16786c69b9256")
} else if strings.Contains(req.URL.Path, "v1/channels") {
contents = getChannelJson(1337, false, true)
}

r := ioutil.NopCloser(bytes.NewReader([]byte(contents)))

return &http.Response{
StatusCode: 200,
Body: r,
}, nil
}

ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(15*time.Second))

c := NewDefaultChannelChecker(ctx, time.Duration(0), true, false, nil)
// Make everything a bit faster
c.OverrideLoopInterval(1 * time.Second)
was_called := false

f, _ := filter.NewUnitTestFilter()
fd := f.(*filter.UnitTestFilter)
fd.AddAllowPubKey("02004c625d622245606a1ea2c1c69cfb4516b703b47945a3647713c05fe4aaeb1c")

c.Subscribe(
pubKey, "random_id",
func() lightning_api.LightingApiCalls { return api },
agent_entities.ReportingSettings{
AllowedEntropy: 64,
PollInterval: agent_entities.SECOND,
AllowPrivateChannels: true,
Filter: f,
},
func(ctx context.Context, report *agent_entities.ChannelBalanceReport) bool {
if len(report.ChangedChannels) == 2 && report.UniqueId == "random_id" {
was_called = true
}

cancel()
return true
},
)

c.EventLoop()

select {
case <-time.After(5 * time.Second):
t.Fatal("Took too long")
case <-ctx.Done():
if !was_called {
t.Fatalf("Callback was not correctly invoked")
}
}
}

func TestContextCanBeNil(t *testing.T) {
pubKey, api, d := initTest(t)

Expand Down
Loading