diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
new file mode 100644
index 0000000..31bc119
--- /dev/null
+++ b/.github/workflows/lint.yml
@@ -0,0 +1,41 @@
+name: Lint
+on:
+ push:
+ branches:
+ - "master"
+ pull_request:
+ paths:
+ - ".github/workflows/server-unit-tests.yml"
+ - "**.go"
+ - "go.mod"
+ - "go.sum"
+
+jobs:
+ server:
+ name: Lint
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Run revive linter
+ uses: docker://morphy/revive-action:v2
+
+ go-mod-tidy:
+ name: Go mod tidy
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version-file: go.mod
+
+ - name: Run go mod tidy
+ run: go mod tidy
+
+ - name: Ensure clean git state
+ run: git diff-index --quiet HEAD -- || (echo "Please run 'go mod tidy' and commit changes." && exit 1)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..3f22705
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,33 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "*"
+
+permissions:
+ contents: write
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup Go
+ uses: actions/setup-go@v4
+ with:
+ go-version-file: go.mod
+
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v5
+ with:
+ distribution: goreleaser
+ version: latest
+ args: release --clean
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..c77e57e
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,32 @@
+name: Test
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - "master"
+ pull_request:
+ paths:
+ - ".github/workflows/test.yml"
+ - "**.go"
+ - "go.mod"
+ - "go.sum"
+
+jobs:
+ tests:
+ name: Tests
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up Go
+ uses: actions/setup-go@v4
+ with:
+ go-version-file: go.mod
+
+ - name: Run tests
+ run: go test ./...
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e0d0587
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+acceptlnd.yaml
+acceptlnd.yml
+*.cert
+*.macaroon
\ No newline at end of file
diff --git a/.goreleaser.yml b/.goreleaser.yml
new file mode 100644
index 0000000..97870b2
--- /dev/null
+++ b/.goreleaser.yml
@@ -0,0 +1,37 @@
+project_name: acceptlnd
+before:
+ hooks:
+ - go mod download
+ - go mod tidy
+builds:
+ -
+ ldflags: -s -w -X main.version={{.Version}}
+ env:
+ - CGO_ENABLED=0
+ goos:
+ - darwin
+ - linux
+ - windows
+ - freebsd
+ - openbsd
+ goarch:
+ - 386
+ - amd64
+ - arm
+ - arm64
+archives:
+ -
+ name_template: '{{ .ProjectName }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}-{{ .Tag }}'
+ format: tar.gz
+ format_overrides:
+ - goos: windows
+ format: zip
+checksum:
+ name_template: checksums.txt
+changelog:
+ sort: asc
+ filters:
+ exclude:
+ - '^docs:'
+ - '^test:'
+ - 'typo'
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..f85a74b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,19 @@
+FROM golang:1.21-alpine3.18 as builder
+
+WORKDIR /app
+
+COPY go.mod .
+
+RUN go mod download && go mod verify
+
+COPY . .
+
+RUN CGO_ENABLED=0 go build -o acceptlnd -ldflags="-s -w" .
+
+# ---
+
+FROM scratch
+
+COPY --from=builder /app/acceptlnd /acceptlnd
+
+ENTRYPOINT ["/acceptlnd"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a32d749
--- /dev/null
+++ b/README.md
@@ -0,0 +1,208 @@
+## AcceptLND
+
+AcceptLND is a channel requests management tool based on policies for LND.
+
+## Usage
+
+```bash
+acceptlnd [-config CONFIG] [-debug] [-version]
+
+Parameters:
+ -config Path to the configuration file (default: "acceptlnd.yml")
+ -debug Enable debug level logging
+ -version Print the current version
+```
+
+## Installation
+
+Download the binary from the [Releases](https://github.com/aftermath2/acceptlnd/releases) page, use docker or compile it yourself.
+
+Docker
+
+```console
+docker build -t acceptlnd .
+# The configuration, certificate and macaroon must be mounted into the container.
+# The paths specified in the configuration file can be absolute or relative to the mount path.
+docker run --network=host -v acceptlnd
+```
+
+
+
+Build from source
+
+> Requires Go 1.18+ installed
+
+```console
+git clone https://github.com/aftermath2/acceptlnd
+cd acceptlnd
+go build -o acceptlnd -ldflags="-s -w" .
+```
+
+
+
+## Configuration
+
+The configuration file can be passed as a flag (`-config=""`) when executing the binary, the default value is `acceptlnd.yml`.
+
+Configuration schema:
+
+| Key | Type | Required | Description |
+| -- | -- | -- | -- |
+| **rpc_address** | string | ✔ | LND GRPC address (`host:port`) |
+| **certificate_path** | string | ✔ | Path to LND's TLS certificate |
+| **macaroon_path** | string | ✔ | Path to the macaroon file. See [macaroon](#macaroon) |
+| **policies** | [][Policy](#policy) | ✖ | Set of policies to enforce |
+
+### Macaroon
+
+AcceptLND needs a macaroon to communicate with the LND instance to manage channel requests.
+
+Although `admin.macaroon` can be used, it is recommended baking a fine-grained macaroon that gives AcceptLND access just to the RPC methods it uses. To bake it, execute:
+
+```
+lncli bakemacaroon uri:/lnrpc.Lightning/ChannelAcceptor uri:/lnrpc.Lightning/GetInfo uri:/lnrpc.Lightning/GetNodeInfo --save_to acceptlnd.macaroon
+```
+
+Once created, specify its path in the `macaroon_path` field of the configuration file, it can be relative or absolute.
+
+## Policy
+
+Policies define a set of requirements that must be met for a request to be accepted. A configuration may have an unlimited number of policies, they are evaluated from top to bottom.
+
+A policy would only be enforced if its conditions are satisfied, or if it has no conditions.
+
+| Name | Type | Description |
+| -- | -- | -- |
+| **conditions** | [Conditions](#conditions) | Set of conditions that must be met to enforce the policies |
+| **reject_all** | boolean | Reject all channel requests |
+| **whitelist** | []string | List of nodes public keys whose requests will be accepted |
+| **blacklist** | []string | List of nodes public keys whose requests will be rejected |
+| **reject_private_channels** | boolean | Whether private channels should be rejected |
+| **reject_zero_conf_channels** | boolean | Whether to reject zero confirmation channels |
+| **request** | [Request](#request) | Parameters related to the channel opening request |
+| **node** | [Node](#node) | Parameters related to the channel initiator |
+
+Here's a simple example:
+
+```yml
+policies:
+ -
+ conditions:
+ is_private: true
+ request:
+ channel_capacity:
+ min: 2_000_000
+```
+
+This policy only applies to private channels and will reject those that have a capacity lower than 2 million sats.
+
+> [!Note]
+> The denomination used in all the numbers is **satoshis**.
+>
+> More examples can be found at [/examples](./examples/).
+
+### Conditions
+
+Conditions are used to evaluate policies conditionally. When used, all of them must resolve to true or the policy is skipped.
+
+They are defined in the configuration exactly the same way policies are, only a few fields change.
+
+| Name | Type | Description |
+| -- | -- | -- |
+| **whitelist** | []string | List of nodes public keys to which policies should be applied |
+| **blacklist** | []string | List of nodes public keys to which policies should not be applied |
+| **is_private** | boolean | Match private channels |
+| **wants_zero_conf** | boolean | Match zero confirmation channels |
+| **request** | [Request](#request) | Parameters related to the channel opening request |
+| **node** | [Node](#node) | Parameters related to the initiator node |
+
+### Request
+
+Parameters related to the channel opening request.
+
+| Name | Type | Description |
+| -- | -- | -- |
+| **channel_capacity** | range | Requested channel size |
+| **channel_reserve** | range | Requested channel reserve |
+| **push_amount** | range | Pushed amount of sats |
+| **csv_delay** | range | Requested CSV delay |
+| **max_accepted_htlcs** | range | The total number of incoming HTLC's that the initiator will accept |
+| **min_htlc** | range | The smallest HTLC in millisatoshis that the initiator will accept |
+| **max_value_in_flight** | range | The maximum amount of coins in millisatoshis that can be pending in the channel |
+| **dust_limit** | range | The dust limit of the initiator's commitment transaction |
+| **commitment_types** | []int | Accepted channel commitment types. See [lnrpc.CommitmentTypes](https://lightning.engineering/api-docs/api/lnd/lightning/channel-acceptor/index.html#lnrpccommitmenttype) |
+
+### Node
+
+Parameters related to the node that is initiating the channel.
+
+| Name | Type | Description |
+| -- | -- | -- |
+| **capacity** | range | Peer's node capacity |
+| **hybrid** | boolean | Whether the peer will be required to be hybrid |
+| **feature_flags** | []int | Feature flags the peer node must know. Check out [lnrpc.FeatureBit](https://lightning.engineering/api-docs/api/lnd/lightning/query-routes#lnrpcfeaturebit) |
+| **Channels** | [Channels](#Channels) | Initiator node channels |
+
+### Channels
+
+Parameters related to the initiator node's channels.
+
+| Name | Type | Description |
+| -- | -- | -- |
+| **number** | range | Peer's number of channels |
+| **zero_base_fees** | boolean | Whether the peer's channels must all have zero base fees |
+| **capacity** | stat_range | Channels size |
+| **block_height** | stat_range | Channels block height |
+| **time_lock_delta** | stat_range | Channels time lock delta |
+| **min_htlc** | stat_range | Channels minimum HTLC |
+| **max_htlc** | stat_range | Channels maximum HTLC |
+| **last_update_diff** | stat_range | Channels last update difference to the time of the request (seconds) |
+| **together** | range | Number of channels that the host node and initiator node have together |
+| **incoming_fee_rates** | stat_range | Channels incoming fee rates |
+| **outgoing_fee_rates** | stat_range | Channels outgoing fee rates |
+| **incoming_base_fees** | stat_range | Channels incoming base fees |
+| **outgoing_base_fees** | stat_range | Channels outgoing base fees |
+| **outgoing_disabled** | stat_range | Number of outgoing disabled channels. The value type is float and should be between 0 and 1 |
+| **incoming_disabled** | stat_range | Number of incoming disabled channels. The value type is float and should be between 0 and 1 |
+
+> [!Note]
+> **Outgoing** refers to the channel value from the initiator's node side, **incoming** to the value corresponding to the initiator node peer side.
+>
+> For instance, let's say Bob wants to open a channel with us and he already has one with Charlie. Bob has a base fee of 0 sats and Charlie has a base fee of 1 sat. In this case, the outgoing base fee is 0 sats (Bob's side) and the incoming base fee is 1 sat (Charlie's side).
+
+#### Range
+
+A range may have a minimum value, a maximum value or both defined. All values are in **satoshis**.
+
+> `Min` and `Max` are inclusive, they include the value assigned: `[Min, Max]`.
+
+##### Example
+
+```yml
+request:
+ channel_capacity:
+ min: 2_000_000
+ max: 50_000_000
+```
+
+#### Statistic range (stat_range)
+
+Statistic ranges work just like ranges but they compare values against the node's data set after being aggregated using an operation.
+
+##### Example
+
+```yml
+node:
+ channels:
+ outgoing_fee_rates:
+ operation: median
+ min: 0
+ max: 100
+```
+
+#### Operations
+
+- **mean** (default): average of a list of numbers.
+- **median**: middle value in a list ordered from smallest to largest.
+- **mode**: most frequently occurring value on a list.
+- **range**: difference between the biggest and the smallest number.
diff --git a/config/config.go b/config/config.go
new file mode 100644
index 0000000..c34cdba
--- /dev/null
+++ b/config/config.go
@@ -0,0 +1,63 @@
+package config
+
+import (
+ "log/slog"
+ "net"
+ "os"
+
+ "github.com/aftermath2/acceptlnd/policy"
+
+ "github.com/pkg/errors"
+ "gopkg.in/yaml.v2"
+)
+
+// Config is acceptLND's configuration schema.
+type Config struct {
+ RPCAddress string `yaml:"rpc_address"`
+ CertificatePath string `yaml:"certificate_path"`
+ MacaroonPath string `yaml:"macaroon_path"`
+ Policies []*policy.Policy `yaml:"policies"`
+}
+
+// Load reads the configuration file and returns a new object.
+func Load(path string) (Config, error) {
+ if path == "" {
+ path = "acceptlnd.yml"
+ }
+
+ slog.Info("Configuration file: " + path)
+
+ f, err := os.OpenFile(path, os.O_RDONLY, 0o600)
+ if err != nil {
+ return Config{}, errors.Wrap(err, "opening file")
+ }
+ defer f.Close()
+
+ var config Config
+ if err := yaml.NewDecoder(f).Decode(&config); err != nil {
+ return Config{}, errors.Wrap(err, "decoding configuration")
+ }
+
+ if err := validate(config); err != nil {
+ return Config{}, errors.Wrap(err, "validating configuration")
+ }
+
+ return config, nil
+}
+
+func validate(config Config) error {
+ _, _, err := net.SplitHostPort(config.RPCAddress)
+ if err != nil {
+ return errors.Wrap(err, "invalid RPC address")
+ }
+
+ if _, err := os.Stat(config.CertificatePath); os.IsNotExist(err) {
+ return errors.New("the certificate file specified does not exist")
+ }
+
+ if _, err := os.Stat(config.MacaroonPath); os.IsNotExist(err) {
+ return errors.New("the macaroon file specified does not exist")
+ }
+
+ return nil
+}
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 0000000..8e81ca5
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,117 @@
+package config
+
+import (
+ "testing"
+
+ "github.com/aftermath2/acceptlnd/policy"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestLoad(t *testing.T) {
+ cases := []struct {
+ desc string
+ path string
+ fail bool
+ }{
+ {
+ desc: "Valid",
+ path: "./testdata/config.yml",
+ },
+ {
+ desc: "Invalid value type",
+ path: "./testdata/invalid_config.yml",
+ fail: true,
+ },
+ {
+ desc: "Invalid value",
+ path: "./testdata/invalid_config2.yml",
+ fail: true,
+ },
+ {
+ desc: "Non existent",
+ path: "",
+ fail: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ _, err := Load(tc.path)
+ if tc.fail {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
+
+func TestValidate(t *testing.T) {
+ tru := true
+
+ cases := []struct {
+ desc string
+ config Config
+ fail bool
+ }{
+ {
+ desc: "Valid",
+ config: Config{
+ RPCAddress: "127.0.0.1:10001",
+ CertificatePath: "./testdata/tls.mock",
+ MacaroonPath: "./testdata/acceptlnd.mock",
+ Policies: []*policy.Policy{
+ {
+ RejectPrivateChannels: &tru,
+ Node: &policy.Node{
+ Hybrid: &tru,
+ },
+ Request: &policy.Request{
+ CommitmentTypes: &[]lnrpc.CommitmentType{
+ lnrpc.CommitmentType_ANCHORS,
+ },
+ },
+ },
+ },
+ },
+ fail: false,
+ },
+ {
+ desc: "Invalid RPC address",
+ config: Config{
+ RPCAddress: "localhost",
+ },
+ fail: true,
+ },
+ {
+ desc: "Invalid certificate path",
+ config: Config{
+ RPCAddress: "127.0.0.1:10001",
+ CertificatePath: "tls.cert",
+ },
+ fail: true,
+ },
+ {
+ desc: "Invalid macaroon path",
+ config: Config{
+ RPCAddress: "127.0.0.1:10001",
+ CertificatePath: "./testdata/tls.mock",
+ MacaroonPath: "admin.macaroon",
+ },
+ fail: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ err := validate(tc.config)
+ if tc.fail {
+ assert.Error(t, err)
+ } else {
+ assert.NoError(t, err)
+ }
+ })
+ }
+}
diff --git a/config/testdata/acceptlnd.mock b/config/testdata/acceptlnd.mock
new file mode 100644
index 0000000..932a895
--- /dev/null
+++ b/config/testdata/acceptlnd.mock
@@ -0,0 +1 @@
+mock
diff --git a/config/testdata/config.yml b/config/testdata/config.yml
new file mode 100644
index 0000000..5895b67
--- /dev/null
+++ b/config/testdata/config.yml
@@ -0,0 +1,18 @@
+rpc_address: 127.0.0.1:10001
+certificate_path: ./testdata/tls.mock
+macaroon_path: ./testdata/acceptlnd.mock
+policies:
+ -
+ conditions:
+ node:
+ capacity:
+ min: 20_000_000
+ request:
+ channel_capacity:
+ min: 3_000_000
+ node:
+ hybrid: true
+ channels:
+ capacity:
+ operation: median
+ min: 1_000_000
diff --git a/config/testdata/invalid_config.yml b/config/testdata/invalid_config.yml
new file mode 100644
index 0000000..2246f01
--- /dev/null
+++ b/config/testdata/invalid_config.yml
@@ -0,0 +1,12 @@
+rpc_address: 127.0.0.1:10001
+certificate_path: ./testdata/tls.mock
+macaroon_path: ./testdata/acceptlnd.mock
+policies:
+ -
+ conditions:
+ node:
+ capacity:
+ min: minimum
+ request:
+ channel_capacity:
+ min: 3_000_000
diff --git a/config/testdata/invalid_config2.yml b/config/testdata/invalid_config2.yml
new file mode 100644
index 0000000..6eda43a
--- /dev/null
+++ b/config/testdata/invalid_config2.yml
@@ -0,0 +1,3 @@
+rpc_address: localhost
+certificate_path: ./testdata/tls.mock
+macaroon_path: ./testdata/acceptlnd.mock
diff --git a/config/testdata/tls.mock b/config/testdata/tls.mock
new file mode 100644
index 0000000..932a895
--- /dev/null
+++ b/config/testdata/tls.mock
@@ -0,0 +1 @@
+mock
diff --git a/examples/advanced.yml b/examples/advanced.yml
new file mode 100644
index 0000000..bb33ede
--- /dev/null
+++ b/examples/advanced.yml
@@ -0,0 +1,23 @@
+policies:
+ -
+ request:
+ channel_capacity:
+ min: 5_000_000
+ channel_reserve:
+ max: 50_000
+ node:
+ channels:
+ outgoing_fee_rate:
+ operation: median
+ min: 0
+ max: 200
+ outgoing_base_fees:
+ operation: range
+ max: 1
+ block_height:
+ operation: mean
+ max: 770_000
+ time_lock_delta:
+ operation: mode
+ min: 40
+ max: 80
diff --git a/examples/conditional.yml b/examples/conditional.yml
new file mode 100644
index 0000000..80b6e49
--- /dev/null
+++ b/examples/conditional.yml
@@ -0,0 +1,19 @@
+policies:
+ - # Enforce policies on nodes with a capacity of 1 BTC or less
+ conditions:
+ node:
+ capacity:
+ max: 100_000_000
+ node: # This will only be enforced if the condition above is satisfied
+ channels:
+ zero_base_fees: true
+ - # If the policy above is not enforced, the next one is evaluated
+ conditions:
+ node:
+ capacity:
+ min: 100_000_000
+ node:
+ channels:
+ outgoing_fee_rates:
+ operation: median
+ max: 1000
diff --git a/examples/disabled_channels.yml b/examples/disabled_channels.yml
new file mode 100644
index 0000000..84245fc
--- /dev/null
+++ b/examples/disabled_channels.yml
@@ -0,0 +1,10 @@
+policies:
+ -
+ node:
+ channels:
+ incoming_disabled:
+ operation: mean
+ max: 0.1
+ outgoing_disabled:
+ operation: mean
+ max: 0.05
diff --git a/examples/node_channels.yml b/examples/node_channels.yml
new file mode 100644
index 0000000..ef4a443
--- /dev/null
+++ b/examples/node_channels.yml
@@ -0,0 +1,35 @@
+policies:
+ -
+ conditions:
+ node:
+ channels:
+ number:
+ min: 10
+ max: 50
+ policies:
+ request:
+ channel_capacity:
+ min: 1_000_000
+ max: 3_000_000
+ -
+ conditions:
+ node:
+ channels:
+ number:
+ min: 50
+ max: 200
+ policies:
+ request:
+ channel_capacity:
+ min: 3_000_000
+ max: 10_000_000
+ -
+ conditions:
+ node:
+ channels:
+ number:
+ min: 200
+ policies:
+ request:
+ channel_capacity:
+ min: 10_000_000
diff --git a/examples/private_channels.yml b/examples/private_channels.yml
new file mode 100644
index 0000000..a44ddc8
--- /dev/null
+++ b/examples/private_channels.yml
@@ -0,0 +1,20 @@
+policies:
+ -
+ reject_private_channels: true
+
+--
+
+policies:
+ - # Set a range for the private channels size
+ conditions:
+ is_private: true
+ request:
+ channel_capacity:
+ min: 100_000
+ max: 1_000_000
+ - # Set a minimum size for public channels
+ conditions:
+ is_private: false
+ request:
+ channel_capacity:
+ min: 1_000_000
diff --git a/examples/range.yml b/examples/range.yml
new file mode 100644
index 0000000..156e1a5
--- /dev/null
+++ b/examples/range.yml
@@ -0,0 +1,23 @@
+policies:
+ - # Accept channels from medium-sized nodes only
+ node:
+ channels:
+ number:
+ min: 30
+ max: 150
+
+--
+
+policies:
+ - # Accept channels bigger than 1M sats
+ request:
+ channel_capacity:
+ min: 1_000_000
+
+--
+
+policies:
+ - # Accept channels from nodes with a capacity lower than 1 BTC
+ node:
+ capacity:
+ max: 100_000_000
diff --git a/examples/reject.yml b/examples/reject.yml
new file mode 100644
index 0000000..c6bef44
--- /dev/null
+++ b/examples/reject.yml
@@ -0,0 +1,18 @@
+policies:
+ -
+ reject_all: true
+
+--
+
+policies:
+ -
+ reject_zero_conf_channels: true
+
+--
+
+policies:
+ -
+ blacklist:
+ - public_key_1
+ - public_key_2
+ - public_key_3
diff --git a/examples/simple.yml b/examples/simple.yml
new file mode 100644
index 0000000..f8cc775
--- /dev/null
+++ b/examples/simple.yml
@@ -0,0 +1,9 @@
+policies:
+ -
+ request:
+ channel_capacity:
+ min: 3_000_000
+ node:
+ hybrid: true
+ channels:
+ zero_base_fees: true
diff --git a/examples/stat_range.yml b/examples/stat_range.yml
new file mode 100644
index 0000000..c4cfae3
--- /dev/null
+++ b/examples/stat_range.yml
@@ -0,0 +1,8 @@
+policies:
+ -
+ node:
+ channels:
+ outgoing_fee_rates:
+ operation: median
+ min: 0
+ max: 100
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..57620fd
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,166 @@
+module github.com/aftermath2/acceptlnd
+
+go 1.21
+
+require (
+ github.com/lightningnetwork/lnd v0.17.3-beta
+ github.com/pkg/errors v0.9.1
+ github.com/stretchr/testify v1.8.4
+ google.golang.org/grpc v1.60.0
+ gopkg.in/macaroon.v2 v2.1.0
+ gopkg.in/yaml.v2 v2.4.0
+)
+
+require (
+ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
+ github.com/aead/siphash v1.0.1 // indirect
+ github.com/beorn7/perks v1.0.1 // indirect
+ github.com/btcsuite/btcd v0.23.5-0.20230905170901-80f5a0ffdf36 // indirect
+ github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
+ github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05 // indirect
+ github.com/btcsuite/btcd/btcutil/psbt v1.1.8 // indirect
+ github.com/btcsuite/btcd/chaincfg/chainhash v1.0.3 // indirect
+ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
+ github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358 // indirect
+ github.com/btcsuite/btcwallet/wallet/txauthor v1.3.3 // indirect
+ github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 // indirect
+ github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
+ github.com/btcsuite/btcwallet/walletdb v1.4.0 // indirect
+ github.com/btcsuite/btcwallet/wtxmgr v1.5.0 // indirect
+ github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
+ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
+ github.com/btcsuite/winsvc v1.0.0 // indirect
+ github.com/cenkalti/backoff/v4 v4.2.1 // indirect
+ github.com/cespare/xxhash/v2 v2.2.0 // indirect
+ github.com/coreos/go-semver v0.3.1 // indirect
+ github.com/coreos/go-systemd/v22 v22.5.0 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect
+ github.com/decred/dcrd/lru v1.1.2 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/fergusstrange/embedded-postgres v1.25.0 // indirect
+ github.com/go-errors/errors v1.5.1 // indirect
+ github.com/go-logr/logr v1.3.0 // indirect
+ github.com/go-logr/stdr v1.2.2 // indirect
+ github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect
+ github.com/gofrs/uuid v4.4.0+incompatible // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/golang/snappy v0.0.4 // indirect
+ github.com/google/btree v1.1.2 // indirect
+ github.com/google/uuid v1.4.0 // indirect
+ github.com/gorilla/websocket v1.5.1 // indirect
+ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
+ github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 // indirect
+ github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+ github.com/jackc/pgconn v1.14.1 // indirect
+ github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect
+ github.com/jackc/pgio v1.0.0 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgproto3/v2 v2.3.2 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
+ github.com/jackc/pgtype v1.14.0 // indirect
+ github.com/jackc/pgx/v4 v4.18.1 // indirect
+ github.com/jessevdk/go-flags v1.5.0 // indirect
+ github.com/jonboulle/clockwork v0.4.0 // indirect
+ github.com/jrick/logrotate v1.0.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/kkdai/bstream v1.0.0 // indirect
+ github.com/lib/pq v1.10.9 // indirect
+ github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
+ github.com/lightninglabs/neutrino v0.16.0 // indirect
+ github.com/lightninglabs/neutrino/cache v1.1.2 // indirect
+ github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f // indirect
+ github.com/lightningnetwork/lnd/clock v1.1.1 // indirect
+ github.com/lightningnetwork/lnd/healthcheck v1.2.3 // indirect
+ github.com/lightningnetwork/lnd/kvdb v1.4.4 // indirect
+ github.com/lightningnetwork/lnd/queue v1.1.1 // indirect
+ github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect
+ github.com/lightningnetwork/lnd/tlv v1.1.2 // indirect
+ github.com/lightningnetwork/lnd/tor v1.1.3 // indirect
+ github.com/ltcsuite/ltcd v0.22.1-beta // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
+ github.com/miekg/dns v1.1.57 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/prometheus/client_golang v1.17.0 // indirect
+ github.com/prometheus/client_model v0.5.0 // indirect
+ github.com/prometheus/common v0.45.0 // indirect
+ github.com/prometheus/procfs v0.12.0 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/rogpeppe/fastuuid v1.2.0 // indirect
+ github.com/sirupsen/logrus v1.9.3 // indirect
+ github.com/soheilhy/cmux v0.1.5 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/stretchr/objx v0.5.1 // indirect
+ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
+ github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 // indirect
+ github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
+ github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 // indirect
+ go.etcd.io/bbolt v1.3.8 // indirect
+ go.etcd.io/etcd/api/v3 v3.5.11 // indirect
+ go.etcd.io/etcd/client/pkg/v3 v3.5.11 // indirect
+ go.etcd.io/etcd/client/v2 v2.305.11 // indirect
+ go.etcd.io/etcd/client/v3 v3.5.11 // indirect
+ go.etcd.io/etcd/pkg/v3 v3.5.11 // indirect
+ go.etcd.io/etcd/raft/v3 v3.5.11 // indirect
+ go.etcd.io/etcd/server/v3 v3.5.11 // indirect
+ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect
+ go.opentelemetry.io/otel v1.21.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 // indirect
+ go.opentelemetry.io/otel/metric v1.21.0 // indirect
+ go.opentelemetry.io/otel/sdk v1.21.0 // indirect
+ go.opentelemetry.io/otel/trace v1.21.0 // indirect
+ go.opentelemetry.io/proto/otlp v1.0.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ go.uber.org/zap v1.26.0 // indirect
+ golang.org/x/crypto v0.16.0 // indirect
+ golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb
+ golang.org/x/mod v0.14.0 // indirect
+ golang.org/x/net v0.19.0 // indirect
+ golang.org/x/sys v0.15.0 // indirect
+ golang.org/x/term v0.15.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/time v0.5.0 // indirect
+ golang.org/x/tools v0.16.0 // indirect
+ google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect
+ google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
+ gopkg.in/errgo.v1 v1.0.1 // indirect
+ gopkg.in/macaroon-bakery.v2 v2.3.0 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ lukechampine.com/uint128 v1.3.0 // indirect
+ modernc.org/cc/v3 v3.41.0 // indirect
+ modernc.org/ccgo/v3 v3.16.15 // indirect
+ modernc.org/libc v1.37.0 // indirect
+ modernc.org/mathutil v1.6.0 // indirect
+ modernc.org/memory v1.7.2 // indirect
+ modernc.org/opt v0.1.3 // indirect
+ modernc.org/sqlite v1.27.0 // indirect
+ modernc.org/strutil v1.2.0 // indirect
+ modernc.org/token v1.1.0 // indirect
+ sigs.k8s.io/yaml v1.4.0 // indirect
+)
+
+// Replaces from lightningnetwork/lnd
+
+// This replace is for https://github.com/advisories/GHSA-25xm-hr59-7c27
+replace github.com/ulikunitz/xz => github.com/ulikunitz/xz v0.5.11
+
+// This replace is for
+// https://deps.dev/advisory/OSV/GO-2021-0053?from=%2Fgo%2Fgithub.com%252Fgogo%252Fprotobuf%2Fv1.3.1
+replace github.com/gogo/protobuf => github.com/gogo/protobuf v1.3.2
+
+// We want to format raw bytes as hex instead of base64. The forked version
+// allows us to specify that as an option.
+replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..3c8135d
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,692 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y=
+cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
+cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
+cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
+cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
+github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
+github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
+github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
+github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg=
+github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
+github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
+github.com/btcsuite/btcd v0.22.0-beta.0.20220204213055-eaf0459ff879/go.mod h1:osu7EoKiL36UThEgzYPqdRaxeo0NU8VoXqgcnwpey0g=
+github.com/btcsuite/btcd v0.23.0/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
+github.com/btcsuite/btcd v0.23.1/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
+github.com/btcsuite/btcd v0.23.4/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
+github.com/btcsuite/btcd v0.23.5-0.20230905170901-80f5a0ffdf36 h1:g/UbZ6iSzcUH9kEvC+rB8UBCqahmt69e8y6nCegczbg=
+github.com/btcsuite/btcd v0.23.5-0.20230905170901-80f5a0ffdf36/go.mod h1:0QJIIN1wwIXF/3G/m87gIwGniDMDQqjVn4SZgnFpsYY=
+github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
+github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
+github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
+github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
+github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
+github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
+github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
+github.com/btcsuite/btcd/btcutil v1.1.1/go.mod h1:nbKlBMNm9FGsdvKvu0essceubPiAcI57pYBNnsLAa34=
+github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05 h1:aemxF+69pT9sYC5E6Qj71zQVHcF72m0BNcVhCl3/thU=
+github.com/btcsuite/btcd/btcutil v1.1.4-0.20230904040416-d4f519f5dc05/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0=
+github.com/btcsuite/btcd/btcutil/psbt v1.1.8 h1:4voqtT8UppT7nmKQkXV+T9K8UyQjKOn2z/ycpmJK8wg=
+github.com/btcsuite/btcd/btcutil/psbt v1.1.8/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U=
+github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
+github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
+github.com/btcsuite/btcd/chaincfg/chainhash v1.0.3 h1:SDlJ7bAm4ewvrmZtR0DaiYbQGdKPeaaIm7bM+qRhFeU=
+github.com/btcsuite/btcd/chaincfg/chainhash v1.0.3/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
+github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f h1:bAs4lUbRJpnnkd9VhRV3jjAVU7DJVjMaK+IsvSeZvFo=
+github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
+github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
+github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358 h1:lZUSo6TISHUJQxpn/AniW5gqaN1iRNS87SDWvV3AHfg=
+github.com/btcsuite/btcwallet v0.16.10-0.20231129183218-5df09dd43358/go.mod h1:WSKhOJWUmUOHKCKEzdt+jWAHFAE/t4RqVbCwL2pEdiU=
+github.com/btcsuite/btcwallet/wallet/txauthor v1.3.3 h1:A4vz5wCONhxawdWs5y41gluZUVARhK/Pty1Uu1c5zkw=
+github.com/btcsuite/btcwallet/wallet/txauthor v1.3.3/go.mod h1:k8f2FcgUQUdfpkY98vWvDYpXLb83g17EupcLv1KcNJw=
+github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 h1:BtEN5Empw62/RVnZ0VcJaVtVlBijnLlJY+dwjAye2Bg=
+github.com/btcsuite/btcwallet/wallet/txrules v1.2.0/go.mod h1:AtkqiL7ccKWxuLYtZm8Bu8G6q82w4yIZdgq6riy60z0=
+github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 h1:PszOub7iXVYbtGybym5TGCp9Dv1h1iX4rIC3HICZGLg=
+github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3/go.mod h1:q08Rms52VyWyXcp5zDc4tdFRKkFgNsMQrv3/LvE1448=
+github.com/btcsuite/btcwallet/walletdb v1.3.5/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
+github.com/btcsuite/btcwallet/walletdb v1.4.0 h1:/C5JRF+dTuE2CNMCO/or5N8epsrhmSM4710uBQoYPTQ=
+github.com/btcsuite/btcwallet/walletdb v1.4.0/go.mod h1:oJDxAEUHVtnmIIBaa22wSBPTVcs6hUp5NKWmI8xDwwU=
+github.com/btcsuite/btcwallet/wtxmgr v1.5.0 h1:WO0KyN4l6H3JWnlFxfGR7r3gDnlGT7W2cL8vl6av4SU=
+github.com/btcsuite/btcwallet/wtxmgr v1.5.0/go.mod h1:TQVDhFxseiGtZwEPvLgtfyxuNUDsIdaJdshvWzR0HJ4=
+github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JGkIguV40SvRY1uliPX8ifOvi6ICsFCw=
+github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
+github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
+github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
+github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
+github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
+github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc=
+github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
+github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk=
+github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
+github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM=
+github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
+github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k=
+github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
+github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
+github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA=
+github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU=
+github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
+github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
+github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
+github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y=
+github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs=
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0=
+github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
+github.com/decred/dcrd/lru v1.1.2 h1:KdCzlkxppuoIDGEvCGah1fZRicrDH36IipvlB1ROkFY=
+github.com/decred/dcrd/lru v1.1.2/go.mod h1:gEdCVgXs1/YoBvFWt7Scgknbhwik3FgVSzlnCcXL2N8=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
+github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
+github.com/fergusstrange/embedded-postgres v1.25.0 h1:sa+k2Ycrtz40eCRPOzI7Ry7TtkWXXJ+YRsxpKMDhxK0=
+github.com/fergusstrange/embedded-postgres v1.25.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw=
+github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
+github.com/frankban/quicktest v1.1.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k=
+github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20=
+github.com/frankban/quicktest v1.11.3 h1:8sXhOn0uLys67V8EsXLc6eszDs8VXWxL3iRvebPhedY=
+github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
+github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=
+github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY=
+github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE=
+github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=
+github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
+github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
+github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
+github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
+github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
+github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1 h1:6UKoz5ujsI55KNpsJH3UwCq3T8kKbZwNZBNPuTTje8U=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.18.1/go.mod h1:YvJ2f6MplWDhfxiUC3KpyTy76kYUZA4W3pTv/wdKQ9Y=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
+github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
+github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
+github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
+github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
+github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
+github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
+github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
+github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
+github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
+github.com/jackc/pgconn v1.14.1 h1:smbxIaZA08n6YuxEX1sDyjV/qkbtUtkH20qLkR9MUR4=
+github.com/jackc/pgconn v1.14.1/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
+github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw=
+github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds=
+github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
+github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
+github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
+github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
+github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
+github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
+github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
+github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
+github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
+github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
+github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
+github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
+github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
+github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
+github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw=
+github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
+github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
+github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
+github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
+github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
+github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
+github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
+github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
+github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
+github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc=
+github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
+github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST4RZ4=
+github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc=
+github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI=
+github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/juju/mgotest v1.0.1/go.mod h1:vTaDufYul+Ps8D7bgseHjq87X8eu0ivlKLp9mVc/Bfc=
+github.com/juju/postgrestest v1.1.0/go.mod h1:/n17Y2T6iFozzXwSCO0JYJ5gSiz2caEtSwAjh/uLXDM=
+github.com/juju/qthttptest v0.0.1/go.mod h1://LCf/Ls22/rPw2u1yWukUJvYtfPY4nYpWUl2uZhryo=
+github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0abI=
+github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
+github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8=
+github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
+github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc=
+github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk=
+github.com/lightninglabs/neutrino v0.16.0 h1:YNTQG32fPR/Zg0vvJVI65OBH8l3U18LSXXtX91hx0q0=
+github.com/lightninglabs/neutrino v0.16.0/go.mod h1:x3OmY2wsA18+Kc3TSV2QpSUewOCiscw2mKpXgZv2kZk=
+github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g=
+github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo=
+github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display h1:pRdza2wleRN1L2fJXd6ZoQ9ZegVFTAb2bOQfruJPKcY=
+github.com/lightninglabs/protobuf-go-hex-display v1.30.0-hex-display/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f h1:Pua7+5TcFEJXIIZ1I2YAUapmbcttmLj4TTi786bIi3s=
+github.com/lightningnetwork/lightning-onion v1.2.1-0.20230823005744-06182b1d7d2f/go.mod h1:c0kvRShutpj3l6B9WtTsNTBUtjSmjZXbJd9ZBRQOSKI=
+github.com/lightningnetwork/lnd v0.17.3-beta h1:RB0bHq55SV1K5/tQxVCvkR+cwfIt38va5922qXTZ8KA=
+github.com/lightningnetwork/lnd v0.17.3-beta/go.mod h1:zKLs8Rb+jhXML7R9juPPNm6rjnINJ7Ry3haoM6jQeWo=
+github.com/lightningnetwork/lnd/clock v1.0.1/go.mod h1:KnQudQ6w0IAMZi1SgvecLZQZ43ra2vpDNj7H/aasemg=
+github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0=
+github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ=
+github.com/lightningnetwork/lnd/healthcheck v1.2.3 h1:oqhOOy8WmIEa6RBkYKC0mmYZkhl8T2kGD97n9jpML8o=
+github.com/lightningnetwork/lnd/healthcheck v1.2.3/go.mod h1:eDxH3dEwV9DeBW/6inrmlVh1qBOFV0AI14EEPnGt9gc=
+github.com/lightningnetwork/lnd/kvdb v1.4.4 h1:bCv63rVCvzqj1BkagN/EWTov6NDDgYEG/t0z2HepRMk=
+github.com/lightningnetwork/lnd/kvdb v1.4.4/go.mod h1:9SuaIqMA9ugrVkdvgQkYXa8CAKYNYd4vsEYORP4V698=
+github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI=
+github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4=
+github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
+github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA=
+github.com/lightningnetwork/lnd/tlv v1.1.2 h1:oOUKWoX4JGkeXlEkGPHk2V1LyqjPndOCNy6ma7sSa2s=
+github.com/lightningnetwork/lnd/tlv v1.1.2/go.mod h1:292dSXpZ+BNnSJFjS1qvHden9LEbulmECglSgfg+4lw=
+github.com/lightningnetwork/lnd/tor v1.1.3 h1:hPIxSpT0UUJmt7iCbF4n4nsmkYe++fvQ/zRadeFfprY=
+github.com/lightningnetwork/lnd/tor v1.1.3/go.mod h1:/LwOzgL6c+bVW0Aegoj1pGlxx9wSvbulBe876knJetc=
+github.com/ltcsuite/ltcd v0.22.1-beta h1:aXeIMuzwPss4VABDyc7Zbx+NMLYFaG3YkNTPNkKL9XA=
+github.com/ltcsuite/ltcd v0.22.1-beta/go.mod h1:O9R9U/mbZwRgr3So8TlNmW7CPc2ZQVhWyVlhXrqu/vo=
+github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
+github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
+github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
+github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
+github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
+github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
+github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
+github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4=
+github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
+github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q=
+github.com/onsi/gomega v1.26.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM=
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
+github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
+github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
+github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
+github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
+github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
+github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
+github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
+github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
+github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
+github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
+github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
+github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
+github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
+github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
+github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
+github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0=
+github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
+github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
+github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
+github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
+github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
+go.etcd.io/bbolt v1.3.5-0.20200615073812-232d8fc87f50/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
+go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
+go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
+go.etcd.io/etcd/api/v3 v3.5.11 h1:B54KwXbWDHyD3XYAwprxNzTe7vlhR69LuBgZnMVvS7E=
+go.etcd.io/etcd/api/v3 v3.5.11/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4=
+go.etcd.io/etcd/client/pkg/v3 v3.5.11 h1:bT2xVspdiCj2910T0V+/KHcVKjkUrCZVtk8J2JF2z1A=
+go.etcd.io/etcd/client/pkg/v3 v3.5.11/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4=
+go.etcd.io/etcd/client/v2 v2.305.11 h1:ZqdKLNJnWpE3bUaaj3XZ5xWyCi+7Vspgk9E0hlIBguE=
+go.etcd.io/etcd/client/v2 v2.305.11/go.mod h1:vX2j5tMynwOateY6BfVmLol3gYOIkbhqjs/BqRsdIOw=
+go.etcd.io/etcd/client/v3 v3.5.11 h1:ajWtgoNSZJ1gmS8k+icvPtqsqEav+iUorF7b0qozgUU=
+go.etcd.io/etcd/client/v3 v3.5.11/go.mod h1:a6xQUEqFJ8vztO1agJh/KQKOMfFI8og52ZconzcDJwE=
+go.etcd.io/etcd/pkg/v3 v3.5.11 h1:U5+/mZh+jps8VRWv7+xPiK1tC1hRBOBYdn7zCqtWyOY=
+go.etcd.io/etcd/pkg/v3 v3.5.11/go.mod h1:bLfwo6YEgpOAMBZJsZg5AiSS+mxNTRJi15Dvp9kKW68=
+go.etcd.io/etcd/raft/v3 v3.5.11 h1:eeimaNIT9DjV4bdLSy4FjLQ/KGSAiG1L5T1nTf5VoZg=
+go.etcd.io/etcd/raft/v3 v3.5.11/go.mod h1:Tp7kZJVtWJWLiMCPrgkimiOB5ZYi8YM93onQihpG724=
+go.etcd.io/etcd/server/v3 v3.5.11 h1:FEa0ImvoXdIPa81/vZUKpnJ74fpQ5ZivseoIKMPzfpg=
+go.etcd.io/etcd/server/v3 v3.5.11/go.mod h1:CS0+TwcuRlhg1I5CpA3YlisOcoqJB1h1GMRgje75uDs=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE=
+go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE=
+go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc=
+go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 h1:cl5P5/GIfFh4t6xyruOgJP5QiA1pw4fYYdv6nc6CBWw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0/go.mod h1:zgBdWWAu7oEEMC06MMKc5NLbA/1YDXV1sMpSqEeLQLg=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0 h1:tIqheXEFWAZ7O8A7m+J0aPTmpJN3YQ7qetUAdkkkKpk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.21.0/go.mod h1:nUeKExfxAQVbiVFn32YXpXZZHZ61Cc3s3Rn1pDBGAb0=
+go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4=
+go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM=
+go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
+go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
+go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc=
+go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ=
+go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I=
+go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM=
+go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
+go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
+go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
+go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8=
+golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
+golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
+golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
+golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
+golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
+golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.16.0 h1:GO788SKMRunPIBCXiQyo2AaexLstOrVhuAL5YwsckQM=
+golang.org/x/tools v0.16.0/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
+golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg=
+google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic=
+google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA=
+google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3 h1:kzJAXnzZoFbe5bhZd4zjUuHos/I31yH4thfMb/13oVY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20231211222908-989df2bf70f3/go.mod h1:eJVxU6o+4G1PSczBr85xmyvSNYAKvAYgkub40YGomFM=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.60.0 h1:6FQAR0kM31P6MRdeluor2w2gPaS4SVNrD/DNTxrQ15k=
+google.golang.org/grpc v1.60.0/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk=
+gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso=
+gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg=
+gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
+gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA=
+gopkg.in/macaroon-bakery.v2 v2.3.0 h1:b40knPgPTke1QLTE8BSYeH7+R/hiIozB1A8CTLYN0Ic=
+gopkg.in/macaroon-bakery.v2 v2.3.0/go.mod h1:/8YhtPARXeRzbpEPLmRB66+gQE8/pzBBkWwg7Vz/guc=
+gopkg.in/macaroon.v2 v2.1.0 h1:HZcsjBCzq9t0eBPMKqTN/uSN6JOm78ZJ2INbqcBQOUI=
+gopkg.in/macaroon.v2 v2.1.0/go.mod h1:OUb+TQP/OP0WOerC2Jp/3CwhIKyIa9kQjuc7H24e6/o=
+gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
+lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
+modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
+modernc.org/ccgo/v3 v3.16.15 h1:KbDR3ZAVU+wiLyMESPtbtE/Add4elztFyfsWoNTgxS0=
+modernc.org/ccgo/v3 v3.16.15/go.mod h1:yT7B+/E2m43tmMOT51GMoM98/MtHIcQQSleGnddkUNI=
+modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
+modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
+modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
+modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
+modernc.org/libc v1.37.0 h1:WerjebcsP6A7Jy+f2lCnHAkiSTLf7IaSftBYUtoswak=
+modernc.org/libc v1.37.0/go.mod h1:YAXkAZ8ktnkCKaN9sw/UDeUVkGYJ/YquGO4FTi5nmHE=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.27.0 h1:MpKAHoyYB7xqcwnUwkuD+npwEa0fojF0B5QRbN+auJ8=
+modernc.org/sqlite v1.27.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
+modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
+modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
+sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
+sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
diff --git a/lightning/lightning.go b/lightning/lightning.go
new file mode 100644
index 0000000..1bfb788
--- /dev/null
+++ b/lightning/lightning.go
@@ -0,0 +1,71 @@
+// Package lightning connects to the lightning network daemon and exposes an interface with the
+// methods available to use.
+package lightning
+
+import (
+ "context"
+ "os"
+ "time"
+
+ "github.com/aftermath2/acceptlnd/config"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/lightningnetwork/lnd/macaroons"
+ "github.com/pkg/errors"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials"
+ "gopkg.in/macaroon.v2"
+)
+
+// Client represents a lightning node client.
+type Client interface {
+ ChannelAcceptor(ctx context.Context, opts ...grpc.CallOption) (lnrpc.Lightning_ChannelAcceptorClient, error)
+ GetInfo(ctx context.Context, in *lnrpc.GetInfoRequest, opts ...grpc.CallOption) (*lnrpc.GetInfoResponse, error)
+ GetNodeInfo(ctx context.Context, in *lnrpc.NodeInfoRequest, opts ...grpc.CallOption) (*lnrpc.NodeInfo, error)
+}
+
+// NewClient returns a new lightning client.
+func NewClient(config config.Config) (Client, error) {
+ opts, err := loadGRPCOpts(config)
+ if err != nil {
+ return nil, errors.Wrap(err, "loading grpc options")
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ conn, err := grpc.DialContext(ctx, config.RPCAddress, opts...)
+ if err != nil {
+ return nil, err
+ }
+
+ return lnrpc.NewLightningClient(conn), nil
+}
+
+func loadGRPCOpts(config config.Config) ([]grpc.DialOption, error) {
+ tlsCert, err := credentials.NewClientTLSFromFile(config.CertificatePath, "")
+ if err != nil {
+ return nil, errors.Wrap(err, "unable to read TLS certificate")
+ }
+
+ macBytes, err := os.ReadFile(config.MacaroonPath)
+ if err != nil {
+ return nil, errors.Wrap(err, "reading macaroon file")
+ }
+
+ mac := &macaroon.Macaroon{}
+ if err := mac.UnmarshalBinary(macBytes); err != nil {
+ return nil, errors.Wrap(err, "unmarshaling macaroon")
+ }
+
+ macaroon, err := macaroons.NewMacaroonCredential(mac)
+ if err != nil {
+ return nil, errors.Wrap(err, "creating macaroon credential")
+ }
+
+ return []grpc.DialOption{
+ grpc.WithBlock(),
+ grpc.WithTransportCredentials(tlsCert),
+ grpc.WithPerRPCCredentials(macaroon),
+ }, nil
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..ec06fed
--- /dev/null
+++ b/main.go
@@ -0,0 +1,160 @@
+package main
+
+import (
+ "context"
+ "encoding/hex"
+ "flag"
+ "fmt"
+ "log/slog"
+ "os"
+ "runtime/debug"
+
+ "github.com/aftermath2/acceptlnd/config"
+ "github.com/aftermath2/acceptlnd/lightning"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/pkg/errors"
+)
+
+func main() {
+ configPath := flag.String("config", "acceptlnd.yml", "Path to the configuration file")
+ debug := flag.Bool("debug", false, "Enable debug level logging")
+ version := flag.Bool("version", false, "Show version")
+ flag.Parse()
+
+ if *version {
+ printVersion()
+ os.Exit(0)
+ }
+
+ level := &slog.LevelVar{}
+ if *debug {
+ level.Set(slog.LevelDebug)
+ }
+ loggerOpts := &slog.HandlerOptions{
+ AddSource: *debug,
+ Level: level,
+ }
+ logger := slog.New(slog.NewTextHandler(os.Stdout, loggerOpts))
+ slog.SetDefault(logger)
+
+ config, err := config.Load(*configPath)
+ if err != nil {
+ fatal(err)
+ }
+
+ client, err := lightning.NewClient(config)
+ if err != nil {
+ fatal(err)
+ }
+
+ if err := handleChannelRequests(config, client); err != nil {
+ fatal(err)
+ }
+}
+
+func fatal(err error) {
+ slog.Error(err.Error())
+ os.Exit(1)
+}
+
+// handleChannelRequests listens to the ChannnelAcceptor RPC stream and accepts/rejects requests.
+func handleChannelRequests(config config.Config, client lightning.Client) error {
+ ctx := context.Background()
+
+ stream, err := client.ChannelAcceptor(ctx)
+ if err != nil {
+ return errors.Wrap(err, "subscribing to the channel acceptor stream")
+ }
+
+ node, err := client.GetInfo(ctx, &lnrpc.GetInfoRequest{})
+ if err != nil {
+ return errors.Wrap(err, "getting node information")
+ }
+
+ slog.Info("Listening for channel requests")
+ for {
+ req, err := stream.Recv()
+ if err != nil {
+ return errors.Wrap(err, "receiving channel request")
+ }
+ slog.Debug("Channel opening request", slog.Any("request", req))
+
+ resp := &lnrpc.ChannelAcceptResponse{Accept: false, PendingChanId: req.PendingChanId}
+ if err := processRequest(config, client, req, node.IdentityPubkey); err != nil {
+ resp.Error = err.Error()
+ } else {
+ resp.Accept = true
+ }
+
+ if err := stream.Send(resp); err != nil {
+ return errors.Wrap(err, "sending channel response")
+ }
+
+ logResponse(response{
+ accepted: resp.Accept,
+ id: hex.EncodeToString(req.PendingChanId),
+ publicKey: hex.EncodeToString(req.NodePubkey),
+ err: resp.Error,
+ })
+ }
+}
+
+func processRequest(
+ config config.Config,
+ client lightning.Client,
+ req *lnrpc.ChannelAcceptRequest,
+ nodePubKey string,
+) error {
+ getPeerInfoReq := &lnrpc.NodeInfoRequest{
+ PubKey: hex.EncodeToString(req.NodePubkey),
+ IncludeChannels: true,
+ }
+ peerNode, err := client.GetNodeInfo(context.Background(), getPeerInfoReq)
+ if err != nil {
+ return errors.New("Internal server error")
+ }
+ slog.Debug("Peer node information", slog.Any("node", peerNode))
+
+ for _, policy := range config.Policies {
+ if err := policy.Evaluate(req, nodePubKey, peerNode); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+type response struct {
+ id string
+ publicKey string
+ err string
+ accepted bool
+}
+
+func logResponse(res response) {
+ args := []any{
+ slog.Bool("accepted", res.accepted),
+ slog.String("id", res.id),
+ slog.String("public_key", res.publicKey),
+ }
+ if !res.accepted {
+ args = append(args, slog.String("error", res.err))
+ }
+
+ slog.Info("New request received", args...)
+}
+
+func printVersion() {
+ bi, _ := debug.ReadBuildInfo()
+
+ var commit string
+ for _, s := range bi.Settings {
+ if s.Key == "vcs.revision" {
+ commit = s.Value
+ break
+ }
+ }
+
+ fmt.Println("AcceptLND", bi.Main.Version, commit)
+}
diff --git a/policy/channels.go b/policy/channels.go
new file mode 100644
index 0000000..fb3c699
--- /dev/null
+++ b/policy/channels.go
@@ -0,0 +1,225 @@
+package policy
+
+import (
+ "errors"
+ "time"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+)
+
+// Channels represents a set of requirements that the initiator's node channels must satisfy.
+type Channels struct {
+ Number *Range[uint32] `yaml:"number,omitempty"`
+ Capacity *StatRange[int64] `yaml:"capacity,omitempty"`
+ ZeroBaseFees *bool `yaml:"zero_base_fees,omitempty"`
+ BlockHeight *StatRange[uint32] `yaml:"block_height,omitempty"`
+ TimeLockDelta *StatRange[uint32] `yaml:"time_lock_delta,omitempty"`
+ MinHTLC *StatRange[int64] `yaml:"min_htlc,omitempty"`
+ MaxHTLC *StatRange[uint64] `yaml:"max_htlc,omitempty"`
+ LastUpdateDiff *StatRange[uint32] `yaml:"last_update_diff,omitempty"`
+ Together *Range[int] `yaml:"together,omitempty"`
+ IncomingFeeRates *StatRange[int64] `yaml:"incoming_fee_rates,omitempty"`
+ OutgoingFeeRates *StatRange[int64] `yaml:"outgoing_fee_rates,omitempty"`
+ IncomingBaseFees *StatRange[int64] `yaml:"incoming_base_fees,omitempty"`
+ OutgoingBaseFees *StatRange[int64] `yaml:"outgoing_base_fees,omitempty"`
+ IncomingDisabled *StatRange[float64] `yaml:"incoming_disabled,omitempty"`
+ OutgoingDisabled *StatRange[float64] `yaml:"outgoing_disabled,omitempty"`
+}
+
+func (c *Channels) evaluate(nodePubKey string, peer *lnrpc.NodeInfo) error {
+ if c == nil {
+ return nil
+ }
+
+ if !check(c.Number, peer.NumChannels) {
+ return errors.New("Node number of channels " + c.Number.Reason())
+ }
+
+ if !checkStat(c.Capacity, peer, capacityFunc) {
+ return errors.New("Capacity " + c.Capacity.Reason())
+ }
+
+ if !c.checkZeroBaseFees(peer) {
+ return errors.New("Node has channels with base fees higher than zero")
+ }
+
+ if !checkStat(c.BlockHeight, peer, blockHeightFunc) {
+ return errors.New("Block height " + c.BlockHeight.Reason())
+ }
+
+ if !checkStat(c.TimeLockDelta, peer, timeLockDeltaFunc(peer)) {
+ return errors.New("Time lock delta " + c.TimeLockDelta.Reason())
+ }
+
+ if !checkStat(c.MinHTLC, peer, minHTLCFunc(peer)) {
+ return errors.New("Channels minimum HTLC " + c.MinHTLC.Reason())
+ }
+
+ if !checkStat(c.MaxHTLC, peer, maxHTLCFunc(peer)) {
+ return errors.New("Channels maximum HTLC " + c.MaxHTLC.Reason())
+ }
+
+ if !checkStat(c.LastUpdateDiff, peer, lastUpdateFunc(peer, time.Now().Unix())) {
+ return errors.New("Channels last update " + c.LastUpdateDiff.Reason())
+ }
+
+ if !c.checkTogether(nodePubKey, peer) {
+ return errors.New("Channels together " + c.Together.Reason())
+ }
+
+ if !checkStat(c.IncomingFeeRates, peer, feeRatesFunc(peer, false)) {
+ return errors.New("Incoming fee rates " + c.IncomingFeeRates.Reason())
+ }
+
+ if !checkStat(c.OutgoingFeeRates, peer, feeRatesFunc(peer, true)) {
+ return errors.New("Outgoing fee rates " + c.OutgoingFeeRates.Reason())
+ }
+
+ if !checkStat(c.IncomingBaseFees, peer, baseFeesFunc(peer, false)) {
+ return errors.New("Incoming base fees " + c.IncomingBaseFees.Reason())
+ }
+
+ if !checkStat(c.OutgoingBaseFees, peer, baseFeesFunc(peer, true)) {
+ return errors.New("Outgoing base fees " + c.OutgoingBaseFees.Reason())
+ }
+
+ if !c.checkIncomingDisabled(peer) {
+ return errors.New("Incoming disabled channels " + c.IncomingDisabled.Reason())
+ }
+
+ if !c.checkOutgoingDisabled(peer) {
+ return errors.New("Outgoing disabled channels " + c.OutgoingDisabled.Reason())
+ }
+
+ return nil
+}
+
+func (c *Channels) checkZeroBaseFees(peer *lnrpc.NodeInfo) bool {
+ if c.ZeroBaseFees == nil {
+ return true
+ }
+
+ for _, channel := range peer.Channels {
+ policy := getNodePolicy(peer.Node.PubKey, channel, true)
+ if policy.FeeBaseMsat != 0 {
+ return false
+ }
+ }
+ return true
+}
+
+func (c *Channels) checkTogether(nodePublicKey string, peer *lnrpc.NodeInfo) bool {
+ if c.Together == nil {
+ return true
+ }
+
+ count := 0
+ for _, channel := range peer.Channels {
+ if (nodePublicKey == channel.Node1Pub && peer.Node.PubKey == channel.Node2Pub) ||
+ (nodePublicKey == channel.Node2Pub && peer.Node.PubKey == channel.Node1Pub) {
+ count++
+ }
+ }
+
+ return c.Together.Contains(count)
+}
+
+func (c *Channels) checkIncomingDisabled(peer *lnrpc.NodeInfo) bool {
+ if c.IncomingDisabled == nil {
+ return true
+ }
+
+ disabledChannels := make([]float64, len(peer.Channels))
+ for i, channel := range peer.Channels {
+ policy := getNodePolicy(peer.Node.PubKey, channel, false)
+
+ if policy.Disabled {
+ disabledChannels[i] = 1
+ }
+ }
+
+ return c.IncomingDisabled.Contains(disabledChannels)
+}
+
+func (c *Channels) checkOutgoingDisabled(peer *lnrpc.NodeInfo) bool {
+ if c.OutgoingDisabled == nil {
+ return true
+ }
+
+ disabledChannels := make([]float64, len(peer.Channels))
+ for i, channel := range peer.Channels {
+ policy := getNodePolicy(peer.Node.PubKey, channel, true)
+
+ if policy.Disabled {
+ disabledChannels[i] = 1
+ }
+ }
+
+ return c.OutgoingDisabled.Contains(disabledChannels)
+}
+
+func getNodePolicy(peerPublicKey string, channel *lnrpc.ChannelEdge, outgoing bool) *lnrpc.RoutingPolicy {
+ if outgoing {
+ if peerPublicKey == channel.Node1Pub {
+ return channel.Node1Policy
+ }
+
+ return channel.Node2Policy
+ }
+
+ if peerPublicKey == channel.Node2Pub {
+ return channel.Node1Policy
+ }
+
+ return channel.Node2Policy
+}
+
+func capacityFunc(channel *lnrpc.ChannelEdge) int64 {
+ return channel.Capacity
+}
+
+func blockHeightFunc(channel *lnrpc.ChannelEdge) uint32 {
+ return uint32(channel.ChannelId >> 40)
+}
+
+func timeLockDeltaFunc(peer *lnrpc.NodeInfo) channelFunc[uint32] {
+ return func(channel *lnrpc.ChannelEdge) uint32 {
+ policy := getNodePolicy(peer.Node.PubKey, channel, true)
+ return policy.TimeLockDelta
+ }
+}
+
+func minHTLCFunc(peer *lnrpc.NodeInfo) channelFunc[int64] {
+ return func(channel *lnrpc.ChannelEdge) int64 {
+ policy := getNodePolicy(peer.Node.PubKey, channel, true)
+ return policy.MinHtlc
+ }
+}
+
+func maxHTLCFunc(peer *lnrpc.NodeInfo) channelFunc[uint64] {
+ return func(channel *lnrpc.ChannelEdge) uint64 {
+ policy := getNodePolicy(peer.Node.PubKey, channel, true)
+ return policy.MaxHtlcMsat / 1000
+ }
+}
+
+func lastUpdateFunc(peer *lnrpc.NodeInfo, now int64) channelFunc[uint32] {
+ return func(channel *lnrpc.ChannelEdge) uint32 {
+ policy := getNodePolicy(peer.Node.PubKey, channel, true)
+ return uint32(now) - policy.LastUpdate
+ }
+}
+
+func feeRatesFunc(peer *lnrpc.NodeInfo, outgoing bool) channelFunc[int64] {
+ return func(channel *lnrpc.ChannelEdge) int64 {
+ policy := getNodePolicy(peer.Node.PubKey, channel, outgoing)
+ return policy.FeeRateMilliMsat
+ }
+}
+
+func baseFeesFunc(peer *lnrpc.NodeInfo, outgoing bool) channelFunc[int64] {
+ return func(channel *lnrpc.ChannelEdge) int64 {
+ policy := getNodePolicy(peer.Node.PubKey, channel, outgoing)
+ return policy.FeeBaseMsat / 1000
+ }
+}
diff --git a/policy/channels_test.go b/policy/channels_test.go
new file mode 100644
index 0000000..4460e45
--- /dev/null
+++ b/policy/channels_test.go
@@ -0,0 +1,1034 @@
+package policy
+
+import (
+ "testing"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEvaluateChannels(t *testing.T) {
+ nodePublicKey := "node_public_key"
+ peerPublicKey := "peer_public_key"
+ maxu32 := uint32(1)
+ max64 := int64(1)
+ maxu64 := uint64(1)
+ max := 1
+ maxFloat := float64(0.5)
+ tru := true
+
+ cases := []struct {
+ channels *Channels
+ peer *lnrpc.NodeInfo
+ desc string
+ fail bool
+ }{
+ {
+ desc: "Nil channels",
+ channels: nil,
+ },
+ {
+ desc: "Empty channels",
+ channels: &Channels{},
+ peer: &lnrpc.NodeInfo{},
+ },
+ {
+ desc: "Number of channels",
+ channels: &Channels{
+ Number: &Range[uint32]{
+ Max: &maxu32,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ NumChannels: 2,
+ },
+ fail: true,
+ },
+ {
+ desc: "Capacity",
+ channels: &Channels{
+ Capacity: &StatRange[int64]{
+ Max: &max64,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Capacity: 1_000_000,
+ Node1Pub: peerPublicKey,
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Zero base fees",
+ channels: &Channels{
+ ZeroBaseFees: &tru,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ FeeBaseMsat: 1000,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Block height",
+ channels: &Channels{
+ BlockHeight: &StatRange[uint32]{
+ Max: &maxu32,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ ChannelId: 623702369048395776,
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Time lock delta",
+ channels: &Channels{
+ TimeLockDelta: &StatRange[uint32]{
+ Max: &maxu32,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ TimeLockDelta: 90,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Minimum HTLC",
+ channels: &Channels{
+ MinHTLC: &StatRange[int64]{
+ Max: &max64,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ MinHtlc: 2,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Maximum HTLC",
+ channels: &Channels{
+ MaxHTLC: &StatRange[uint64]{
+ Max: &maxu64,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ MaxHtlcMsat: 2000,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Last update difference",
+ channels: &Channels{
+ LastUpdateDiff: &StatRange[uint32]{
+ Max: &maxu32,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ LastUpdate: 14_230_110,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Together",
+ channels: &Channels{
+ Together: &Range[int]{
+ Max: &max,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node2Pub: nodePublicKey,
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node2Pub: nodePublicKey,
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Incoming fee rates",
+ channels: &Channels{
+ IncomingFeeRates: &StatRange[int64]{
+ Max: &max64,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node2Policy: &lnrpc.RoutingPolicy{
+ FeeRateMilliMsat: 10,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Outgoing fee rates",
+ channels: &Channels{
+ OutgoingFeeRates: &StatRange[int64]{
+ Max: &max64,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ FeeRateMilliMsat: 10,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Incoming base fees",
+ channels: &Channels{
+ IncomingBaseFees: &StatRange[int64]{
+ Max: &max64,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node2Policy: &lnrpc.RoutingPolicy{
+ FeeBaseMsat: 2000,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Outgoing base fees",
+ channels: &Channels{
+ OutgoingBaseFees: &StatRange[int64]{
+ Max: &max64,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ FeeBaseMsat: 2000,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Incoming disabled",
+ channels: &Channels{
+ IncomingDisabled: &StatRange[float64]{
+ Max: &maxFloat,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node2Policy: &lnrpc.RoutingPolicy{
+ Disabled: true,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Outgoing disabled",
+ channels: &Channels{
+ OutgoingDisabled: &StatRange[float64]{
+ Max: &maxFloat,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ Disabled: true,
+ },
+ },
+ },
+ },
+ fail: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ err := tc.channels.evaluate(nodePublicKey, tc.peer)
+ if tc.fail {
+ assert.NotNil(t, err)
+ } else {
+ assert.Nil(t, err)
+ }
+ })
+ }
+}
+
+func TestCheckCapacity(t *testing.T) {
+ min := int64(100_000)
+ max := int64(1_000_000)
+
+ cases := []struct {
+ capacity *StatRange[int64]
+ desc string
+ channels []*lnrpc.ChannelEdge
+ expected bool
+ }{
+ {
+ desc: "Contains",
+ capacity: &StatRange[int64]{
+ Operation: Mean,
+ Min: &min,
+ Max: &max,
+ },
+ channels: []*lnrpc.ChannelEdge{
+ {Capacity: 10_000},
+ {Capacity: 250_000},
+ },
+ expected: true,
+ },
+ {
+ desc: "Does not contain",
+ capacity: &StatRange[int64]{
+ Operation: Mean,
+ Min: &min,
+ Max: &max,
+ },
+ channels: []*lnrpc.ChannelEdge{
+ {Capacity: 50_000},
+ {Capacity: 25_000},
+ },
+ expected: false,
+ },
+ {
+ desc: "Nil",
+ expected: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ channels := Channels{
+ Capacity: tc.capacity,
+ }
+
+ actual := checkStat(
+ channels.Capacity,
+ &lnrpc.NodeInfo{Channels: tc.channels},
+ capacityFunc,
+ )
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestCheckZeroBaseFees(t *testing.T) {
+ publicKey := "public_key"
+
+ cases := []struct {
+ peer *lnrpc.NodeInfo
+ desc string
+ zeroBaseFees bool
+ expected bool
+ }{
+ {
+ desc: "Zero base fee channels",
+ zeroBaseFees: true,
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{PubKey: publicKey},
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ FeeBaseMsat: 0,
+ },
+ },
+ {
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{
+ FeeBaseMsat: 0,
+ },
+ },
+ },
+ },
+ expected: true,
+ },
+ {
+ desc: "Non zero base fee channels",
+ zeroBaseFees: true,
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{PubKey: publicKey},
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{FeeBaseMsat: 0},
+ },
+ {
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{FeeBaseMsat: 1},
+ },
+ },
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ channels := Channels{
+ ZeroBaseFees: &tc.zeroBaseFees,
+ }
+
+ actual := channels.checkZeroBaseFees(tc.peer)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ channels := Channels{}
+ actual := channels.checkZeroBaseFees(nil)
+ assert.True(t, actual)
+ })
+}
+
+func TestCheckTogether(t *testing.T) {
+ nodePublicKey := "node_public_key"
+ peerPublicKey := "peer_public_key"
+ min, max := 1, 3
+
+ cases := []struct {
+ peer *lnrpc.NodeInfo
+ together *Range[int]
+ desc string
+ nodePublicKey string
+ expected bool
+ }{
+ {
+ desc: "Channels together",
+ nodePublicKey: nodePublicKey,
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: nodePublicKey,
+ Node2Pub: peerPublicKey,
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node2Pub: nodePublicKey,
+ },
+ },
+ },
+ together: &Range[int]{
+ Min: &min,
+ Max: &max,
+ },
+ expected: true,
+ },
+ {
+ desc: "Not enough channels together",
+ nodePublicKey: nodePublicKey,
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: nodePublicKey,
+ Node2Pub: peerPublicKey,
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node2Pub: nodePublicKey,
+ },
+ },
+ },
+ together: &Range[int]{
+ Min: &max,
+ },
+ expected: false,
+ },
+ {
+ desc: "No channels together",
+ nodePublicKey: nodePublicKey,
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: nodePublicKey + "d21u",
+ Node2Pub: peerPublicKey,
+ },
+ {
+ Node1Pub: peerPublicKey + "d21u",
+ Node2Pub: nodePublicKey,
+ },
+ },
+ },
+ together: &Range[int]{
+ Min: &min,
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ channels := Channels{
+ Together: tc.together,
+ }
+
+ actual := channels.checkTogether(tc.nodePublicKey, tc.peer)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ channels := Channels{}
+ assert.True(t, channels.checkTogether("", nil))
+ })
+}
+
+func TestCheckIncomingDisabled(t *testing.T) {
+ peerPublicKey := "peer_public_key"
+ value := 0.6
+
+ cases := []struct {
+ peer *lnrpc.NodeInfo
+ incomingDisabled *StatRange[float64]
+ desc string
+ expected bool
+ }{
+ {
+ desc: "Maximum disabled channels rate met",
+ incomingDisabled: &StatRange[float64]{
+ Operation: Mean,
+ Max: &value,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: false}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: false}},
+ },
+ },
+ expected: true,
+ },
+ {
+ desc: "Maximum disabled channels rate not met",
+ incomingDisabled: &StatRange[float64]{
+ Operation: Mean,
+ Max: &value,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: false}},
+ },
+ },
+ expected: false,
+ },
+ {
+ desc: "Minimum disabled channels rate met",
+ incomingDisabled: &StatRange[float64]{
+ Operation: Mean,
+ Min: &value,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: false}},
+ },
+ },
+ expected: true,
+ },
+ {
+ desc: "Minimum disabled channels rate not met",
+ incomingDisabled: &StatRange[float64]{
+ Operation: Mean,
+ Min: &value,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: true}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: false}},
+ {Node1Pub: peerPublicKey, Node2Policy: &lnrpc.RoutingPolicy{Disabled: false}},
+ },
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ channels := Channels{
+ IncomingDisabled: tc.incomingDisabled,
+ }
+
+ actual := channels.checkIncomingDisabled(tc.peer)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ channels := Channels{}
+ assert.True(t, channels.checkIncomingDisabled(nil))
+ })
+}
+
+func TestCheckOutgoingDisabled(t *testing.T) {
+ value := 0.6
+ peerPublicKey := "peer_public_key"
+
+ cases := []struct {
+ peer *lnrpc.NodeInfo
+ outgoingDisabled *StatRange[float64]
+ desc string
+ expected bool
+ }{
+ {
+ desc: "Maximum disabled channels rate met",
+ outgoingDisabled: &StatRange[float64]{
+ Operation: Mean,
+ Max: &value,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: false},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: false},
+ },
+ },
+ },
+ expected: true,
+ },
+ {
+ desc: "Maximum disabled channels rate not met",
+ outgoingDisabled: &StatRange[float64]{
+ Operation: Mean,
+ Max: &value,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: false},
+ },
+ },
+ },
+ expected: false,
+ },
+ {
+ desc: "Minimum disabled channels rate met",
+ outgoingDisabled: &StatRange[float64]{
+ Operation: Mean,
+ Min: &value,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: false},
+ },
+ },
+ },
+ expected: true,
+ },
+ {
+ desc: "Minimum disabled channels rate not met",
+ outgoingDisabled: &StatRange[float64]{
+ Operation: Mean,
+ Min: &value,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ Channels: []*lnrpc.ChannelEdge{
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: true},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: false},
+ },
+ {
+ Node1Pub: peerPublicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{Disabled: false},
+ },
+ },
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ channels := Channels{
+ OutgoingDisabled: tc.outgoingDisabled,
+ }
+
+ actual := channels.checkOutgoingDisabled(tc.peer)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ channels := Channels{}
+ assert.True(t, channels.checkOutgoingDisabled(nil))
+ })
+}
+
+func TestGetNodePolicy(t *testing.T) {
+ publicKey := "public_key"
+ expectedPolicy := &lnrpc.RoutingPolicy{
+ TimeLockDelta: 1,
+ }
+ otherPolicy := &lnrpc.RoutingPolicy{
+ TimeLockDelta: 5,
+ }
+
+ cases := []struct {
+ peerPublicKey string
+ channel *lnrpc.ChannelEdge
+ expected *lnrpc.RoutingPolicy
+ desc string
+ outgoing bool
+ }{
+ {
+ desc: "Get incoming node policy",
+ peerPublicKey: publicKey,
+ channel: &lnrpc.ChannelEdge{
+ Node1Policy: expectedPolicy,
+ Node2Pub: publicKey,
+ Node2Policy: otherPolicy,
+ },
+ outgoing: false,
+ },
+ {
+ desc: "Get incoming node policy 2",
+ peerPublicKey: publicKey,
+ channel: &lnrpc.ChannelEdge{
+ Node1Pub: publicKey,
+ Node1Policy: otherPolicy,
+ Node2Policy: expectedPolicy,
+ },
+ outgoing: false,
+ },
+ {
+ desc: "Get outgoing node policy",
+ peerPublicKey: publicKey,
+ channel: &lnrpc.ChannelEdge{
+ Node1Pub: publicKey,
+ Node1Policy: expectedPolicy,
+ Node2Policy: otherPolicy,
+ },
+ outgoing: true,
+ },
+ {
+ desc: "Get outgoing node policy 2",
+ peerPublicKey: publicKey,
+ channel: &lnrpc.ChannelEdge{
+ Node1Policy: otherPolicy,
+ Node2Pub: publicKey,
+ Node2Policy: expectedPolicy,
+ },
+ outgoing: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ actual := getNodePolicy(tc.peerPublicKey, tc.channel, tc.outgoing)
+ assert.Equal(t, expectedPolicy, actual)
+ })
+ }
+}
+
+func TestBlockHeightFunc(t *testing.T) {
+ channel := &lnrpc.ChannelEdge{
+ ChannelId: 623702369048395776,
+ }
+ expected := uint32(567254)
+ actual := blockHeightFunc(channel)
+ assert.Equal(t, expected, actual)
+}
+
+func TestTimeLockDeltaFunc(t *testing.T) {
+ publicKey := "public_key"
+ expected := uint32(5)
+ peer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{PubKey: publicKey},
+ }
+ channel := &lnrpc.ChannelEdge{
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{TimeLockDelta: expected},
+ }
+ actual := timeLockDeltaFunc(peer)(channel)
+ assert.Equal(t, expected, actual)
+}
+
+func TestMinHTLCFunc(t *testing.T) {
+ publicKey := "public_key"
+ expected := int64(1)
+ peer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{PubKey: publicKey},
+ }
+ channel := &lnrpc.ChannelEdge{
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{MinHtlc: expected},
+ }
+ actual := minHTLCFunc(peer)(channel)
+ assert.Equal(t, expected, actual)
+}
+
+func TestMaxHTLCFunc(t *testing.T) {
+ publicKey := "public_key"
+ expected := uint64(90000000)
+ peer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{PubKey: publicKey},
+ }
+ channel := &lnrpc.ChannelEdge{
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{MaxHtlcMsat: expected * 1000},
+ }
+ actual := maxHTLCFunc(peer)(channel)
+ assert.Equal(t, expected, actual)
+}
+
+func TestLastUpdateFunc(t *testing.T) {
+ publicKey := "public_key"
+ expected := uint32(500)
+ peer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{PubKey: publicKey},
+ }
+ channel := &lnrpc.ChannelEdge{
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{LastUpdate: expected},
+ }
+ actual := lastUpdateFunc(peer, 1000)(channel)
+ assert.Equal(t, expected, actual)
+}
+
+func TestFeeRatesFunc(t *testing.T) {
+ t.Run("Incoming", func(t *testing.T) {
+ expected := int64(100)
+ peer := &lnrpc.NodeInfo{Node: &lnrpc.LightningNode{}}
+ channel := &lnrpc.ChannelEdge{
+ Node2Pub: "pub",
+ Node2Policy: &lnrpc.RoutingPolicy{FeeRateMilliMsat: expected},
+ }
+ actual := feeRatesFunc(peer, false)(channel)
+ assert.Equal(t, expected, actual)
+ })
+
+ t.Run("Outgoing", func(t *testing.T) {
+ publicKey := "public_key"
+ expected := int64(50)
+ peer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{PubKey: publicKey},
+ }
+ channel := &lnrpc.ChannelEdge{
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{FeeRateMilliMsat: expected},
+ }
+ actual := feeRatesFunc(peer, true)(channel)
+ assert.Equal(t, expected, actual)
+ })
+}
+
+func TestBaseFeesFunc(t *testing.T) {
+ t.Run("Incoming", func(t *testing.T) {
+ expected := int64(0)
+ peer := &lnrpc.NodeInfo{Node: &lnrpc.LightningNode{}}
+ channel := &lnrpc.ChannelEdge{
+ Node2Pub: "pub",
+ Node2Policy: &lnrpc.RoutingPolicy{FeeBaseMsat: expected * 1000},
+ }
+ actual := baseFeesFunc(peer, false)(channel)
+ assert.Equal(t, expected, actual)
+ })
+
+ t.Run("Outgoing", func(t *testing.T) {
+ publicKey := "public_key"
+ expected := int64(1)
+ peer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{PubKey: publicKey},
+ }
+ channel := &lnrpc.ChannelEdge{
+ Node1Pub: publicKey,
+ Node1Policy: &lnrpc.RoutingPolicy{FeeBaseMsat: expected * 1000},
+ }
+ actual := baseFeesFunc(peer, true)(channel)
+ assert.Equal(t, expected, actual)
+ })
+}
diff --git a/policy/condition.go b/policy/condition.go
new file mode 100644
index 0000000..3b2aced
--- /dev/null
+++ b/policy/condition.go
@@ -0,0 +1,93 @@
+package policy
+
+import (
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/lightningnetwork/lnd/lnwire"
+)
+
+// Conditions represents a set of requirements that must be met to apply a policy.
+type Conditions struct {
+ IsPrivate *bool `yaml:"is_private,omitempty"`
+ WantsZeroConf *bool `yaml:"wants_zero_conf,omitempty"`
+ Whitelist *[]string `yaml:"whitelist,omitempty"`
+ Blacklist *[]string `yaml:"blacklist,omitempty"`
+ Request *Request `yaml:"request,omitempty"`
+ Node *Node `yaml:"node,omitempty"`
+}
+
+// Match returns true if all the conditions Match.
+func (c *Conditions) Match(
+ req *lnrpc.ChannelAcceptRequest,
+ nodePubKey string,
+ peerNode *lnrpc.NodeInfo,
+) bool {
+ if c == nil {
+ return true
+ }
+
+ if c.checkWhitelist(peerNode.Node.PubKey) {
+ return true
+ }
+
+ if !c.checkBlacklist(peerNode.Node.PubKey) {
+ return false
+ }
+
+ if !c.checkIsPrivate(req.ChannelFlags != uint32(lnwire.FFAnnounceChannel)) {
+ return false
+ }
+
+ if !c.checkWantsZeroConf(req.WantsZeroConf) {
+ return false
+ }
+
+ if err := c.Request.evaluate(req); err != nil {
+ return false
+ }
+
+ if err := c.Node.evaluate(nodePubKey, peerNode); err != nil {
+ return false
+ }
+
+ return true
+}
+
+func (c *Conditions) checkWhitelist(publicKey string) bool {
+ if c.Whitelist == nil {
+ return false
+ }
+
+ for _, pubKey := range *c.Whitelist {
+ if publicKey == pubKey {
+ return true
+ }
+ }
+ return false
+}
+
+func (c *Conditions) checkBlacklist(publicKey string) bool {
+ if c.Blacklist == nil {
+ return true
+ }
+
+ for _, pubKey := range *c.Blacklist {
+ if publicKey == pubKey {
+ return false
+ }
+ }
+ return true
+}
+
+func (c *Conditions) checkIsPrivate(private bool) bool {
+ if c.IsPrivate == nil {
+ return true
+ }
+ return private == *c.IsPrivate
+}
+
+func (c *Conditions) checkWantsZeroConf(wantsZeroConf bool) bool {
+ if c.WantsZeroConf == nil {
+ return true
+ }
+ return wantsZeroConf == *c.WantsZeroConf
+}
diff --git a/policy/condition_test.go b/policy/condition_test.go
new file mode 100644
index 0000000..8b15bab
--- /dev/null
+++ b/policy/condition_test.go
@@ -0,0 +1,305 @@
+package policy
+
+import (
+ "testing"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/lightningnetwork/lnd/lnwire"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMatch(t *testing.T) {
+ nodePublicKey := "node_public_key"
+ peerPublicKey := "peer_public_key"
+ defaultReq := &lnrpc.ChannelAcceptRequest{}
+ defaultPeer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ }
+ tru := true
+ max := uint64(1)
+
+ cases := []struct {
+ conditions *Conditions
+ req *lnrpc.ChannelAcceptRequest
+ peer *lnrpc.NodeInfo
+ desc string
+ expected bool
+ }{
+ {
+ desc: "Nil conditions",
+ req: defaultReq,
+ peer: defaultPeer,
+ expected: true,
+ },
+ {
+ desc: "Empty conditions",
+ conditions: &Conditions{},
+ req: defaultReq,
+ peer: defaultPeer,
+ expected: true,
+ },
+ {
+ desc: "Whitelist",
+ conditions: &Conditions{
+ Whitelist: &[]string{peerPublicKey},
+ },
+ req: defaultReq,
+ peer: defaultPeer,
+ expected: true,
+ },
+ {
+ desc: "Blacklist",
+ conditions: &Conditions{
+ Blacklist: &[]string{peerPublicKey},
+ },
+ req: defaultReq,
+ peer: defaultPeer,
+ expected: false,
+ },
+ {
+ desc: "Is private",
+ conditions: &Conditions{
+ IsPrivate: &tru,
+ },
+ req: &lnrpc.ChannelAcceptRequest{
+ ChannelFlags: uint32(lnwire.FFAnnounceChannel),
+ },
+ peer: defaultPeer,
+ expected: false,
+ },
+ {
+ desc: "Wants zero conf",
+ conditions: &Conditions{
+ WantsZeroConf: &tru,
+ },
+ req: &lnrpc.ChannelAcceptRequest{
+ WantsZeroConf: false,
+ },
+ peer: defaultPeer,
+ expected: false,
+ },
+ {
+ desc: "Request",
+ conditions: &Conditions{
+ Request: &Request{
+ ChannelCapacity: &Range[uint64]{
+ Max: &max,
+ },
+ },
+ },
+ req: &lnrpc.ChannelAcceptRequest{
+ FundingAmt: 10_000,
+ },
+ peer: defaultPeer,
+ expected: false,
+ },
+
+ {
+ desc: "Node",
+ conditions: &Conditions{
+ Node: &Node{
+ Hybrid: &tru,
+ },
+ },
+ req: defaultReq,
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ Addresses: []*lnrpc.NodeAddress{
+ {Network: "tcp", Addr: "127.0.0.1:9735"},
+ },
+ },
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ actual := tc.conditions.Match(tc.req, nodePublicKey, tc.peer)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestConditionsCheckWhitelist(t *testing.T) {
+ publicKey := "key"
+
+ cases := []struct {
+ desc string
+ publicKey string
+ whitelist []string
+ expected bool
+ }{
+ {
+ desc: "Whitelisted",
+ publicKey: publicKey,
+ whitelist: []string{publicKey},
+ expected: true,
+ },
+ {
+ desc: "Not whitelisted",
+ publicKey: "not key",
+ whitelist: []string{publicKey},
+ expected: false,
+ },
+ {
+ desc: "Empty whitelist",
+ whitelist: []string{},
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ conditions := Conditions{
+ Whitelist: &tc.whitelist,
+ }
+
+ actual := conditions.checkWhitelist(tc.publicKey)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ conditions := Conditions{}
+ assert.False(t, conditions.checkWhitelist(""))
+ })
+}
+
+func TestConditionsCheckBlacklist(t *testing.T) {
+ publicKey := "key"
+
+ cases := []struct {
+ desc string
+ publicKey string
+ blacklist []string
+ expected bool
+ }{
+ {
+ desc: "Blacklisted",
+ publicKey: publicKey,
+ blacklist: []string{publicKey},
+ expected: false,
+ },
+ {
+ desc: "Not blacklisted",
+ publicKey: "not key",
+ blacklist: []string{publicKey},
+ expected: true,
+ },
+ {
+ desc: "Empty blacklist",
+ blacklist: []string{},
+ expected: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ conditions := Conditions{
+ Blacklist: &tc.blacklist,
+ }
+
+ actual := conditions.checkBlacklist(tc.publicKey)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ conditions := Conditions{}
+ assert.True(t, conditions.checkBlacklist(""))
+ })
+}
+
+func TestConditionsCheckIsPrivate(t *testing.T) {
+ cases := []struct {
+ desc string
+ isPrivate bool
+ private bool
+ expected bool
+ }{
+ {
+ desc: "Match",
+ isPrivate: true,
+ private: true,
+ expected: true,
+ },
+ {
+ desc: "No match",
+ isPrivate: false,
+ private: true,
+ expected: false,
+ },
+ {
+ desc: "No match 2",
+ isPrivate: true,
+ private: false,
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ conditions := Conditions{
+ IsPrivate: &tc.isPrivate,
+ }
+
+ actual := conditions.checkIsPrivate(tc.private)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ conditions := Conditions{}
+ actual := conditions.checkIsPrivate(true)
+ assert.True(t, actual)
+ })
+}
+
+func TestConditionsCheckWantsZeroConf(t *testing.T) {
+ cases := []struct {
+ desc string
+ wantsZeroConf bool
+ wantZeroConf bool
+ expected bool
+ }{
+ {
+ desc: "Match",
+ wantsZeroConf: true,
+ wantZeroConf: true,
+ expected: true,
+ },
+ {
+ desc: "No match",
+ wantsZeroConf: false,
+ wantZeroConf: true,
+ expected: false,
+ },
+ {
+ desc: "No match 2",
+ wantsZeroConf: true,
+ wantZeroConf: false,
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ conditions := Conditions{
+ WantsZeroConf: &tc.wantsZeroConf,
+ }
+
+ actual := conditions.checkWantsZeroConf(tc.wantZeroConf)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ conditions := Conditions{}
+ actual := conditions.checkWantsZeroConf(true)
+ assert.True(t, actual)
+ })
+}
diff --git a/policy/node.go b/policy/node.go
new file mode 100644
index 0000000..6c30ea4
--- /dev/null
+++ b/policy/node.go
@@ -0,0 +1,73 @@
+package policy
+
+import (
+ "errors"
+ "strings"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+)
+
+// Node represents a set of requirements the node requesting to open a channel must satisfy.
+type Node struct {
+ Capacity *Range[int64] `yaml:"capacity,omitempty"`
+ Hybrid *bool `yaml:"hybrid,omitempty"`
+ FeatureFlags *[]lnrpc.FeatureBit `yaml:"feature_flags,omitempty"`
+ Channels *Channels `yaml:"channels,omitempty"`
+}
+
+func (n *Node) evaluate(nodePubKey string, peerNode *lnrpc.NodeInfo) error {
+ if n == nil {
+ return nil
+ }
+
+ if !check(n.Capacity, peerNode.TotalCapacity) {
+ return errors.New("Node capacity " + n.Capacity.Reason())
+ }
+
+ if !n.checkHybrid(peerNode.Node.Addresses) {
+ return errors.New("Node doesn't have both clearnet and tor addresses")
+ }
+
+ if !n.checkFeatureFlags(peerNode.Node.Features) {
+ return errors.New("Node doesn't have the desired feature flags")
+ }
+
+ return n.Channels.evaluate(nodePubKey, peerNode)
+}
+
+func (n *Node) checkHybrid(addresses []*lnrpc.NodeAddress) bool {
+ if n.Hybrid == nil {
+ return true
+ }
+ hasClearnet := false
+ hasTor := false
+
+ for _, address := range addresses {
+ host, _, _ := strings.Cut(address.Addr, ":")
+ if strings.HasSuffix(host, ".onion") {
+ hasTor = true
+ continue
+ }
+ hasClearnet = true
+ }
+
+ if hasClearnet && hasTor {
+ return *n.Hybrid
+ }
+
+ return !*n.Hybrid
+}
+
+func (n *Node) checkFeatureFlags(features map[uint32]*lnrpc.Feature) bool {
+ if n.FeatureFlags == nil {
+ return true
+ }
+
+ for _, flag := range *n.FeatureFlags {
+ if feature, ok := features[uint32(flag)]; !ok || !feature.IsKnown {
+ return false
+ }
+ }
+
+ return true
+}
diff --git a/policy/node_test.go b/policy/node_test.go
new file mode 100644
index 0000000..6b4316b
--- /dev/null
+++ b/policy/node_test.go
@@ -0,0 +1,237 @@
+package policy
+
+import (
+ "testing"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEvaluateNode(t *testing.T) {
+ nodePublicKey := "node_public_key"
+ peerPublicKey := "peer_public_key"
+ defaultPeer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ }
+ tru := true
+ max := int64(1)
+
+ cases := []struct {
+ node *Node
+ peer *lnrpc.NodeInfo
+ desc string
+ fail bool
+ }{
+ {
+ desc: "Nil node",
+ peer: defaultPeer,
+ fail: false,
+ },
+ {
+ desc: "Empty node",
+ node: &Node{},
+ peer: defaultPeer,
+ fail: false,
+ },
+ {
+ desc: "Capacity",
+ node: &Node{
+ Capacity: &Range[int64]{
+ Max: &max,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ TotalCapacity: 100_000_000,
+ Node: defaultPeer.Node,
+ },
+ fail: true,
+ },
+ {
+ desc: "Hybrid",
+ node: &Node{
+ Hybrid: &tru,
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ Addresses: []*lnrpc.NodeAddress{
+ {Network: "tcp", Addr: "127.0.0.1:9735"},
+ },
+ },
+ },
+ fail: true,
+ },
+ {
+ desc: "Feature flags",
+ node: &Node{
+ FeatureFlags: &[]lnrpc.FeatureBit{
+ lnrpc.FeatureBit_AMP_REQ,
+ lnrpc.FeatureBit_AMP_OPT,
+ },
+ },
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ Features: map[uint32]*lnrpc.Feature{
+ uint32(lnrpc.FeatureBit_AMP_REQ): {IsKnown: true},
+ },
+ },
+ },
+ fail: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ err := tc.node.evaluate(nodePublicKey, tc.peer)
+ if tc.fail {
+ assert.NotNil(t, err)
+ } else {
+ assert.Nil(t, err)
+ }
+ })
+ }
+}
+
+func TestCheckHybrid(t *testing.T) {
+ cases := []struct {
+ desc string
+ addresses []*lnrpc.NodeAddress
+ hybrid bool
+ expected bool
+ }{
+ {
+ desc: "Hybrid",
+ addresses: []*lnrpc.NodeAddress{
+ {Addr: "url.onion:9735"},
+ {Addr: "0.0.0.0:9735"},
+ },
+ hybrid: true,
+ expected: true,
+ },
+ {
+ desc: "Hybrid (no tor)",
+ addresses: []*lnrpc.NodeAddress{
+ {Addr: "0.0.0.0:9735"},
+ },
+ hybrid: true,
+ expected: false,
+ },
+ {
+ desc: "Hybrid (no clearnet)",
+ addresses: []*lnrpc.NodeAddress{
+ {Addr: "url.onion:9735"},
+ },
+ hybrid: true,
+ expected: false,
+ },
+ {
+ desc: "Not hybrid",
+ addresses: []*lnrpc.NodeAddress{
+ {Addr: "url.onion:9735"},
+ {Addr: "0.0.0.0:9735"},
+ },
+ hybrid: false,
+ expected: false,
+ },
+ {
+ desc: "Clearnet",
+ addresses: []*lnrpc.NodeAddress{
+ {Addr: "0.0.0.0:9735"},
+ },
+ hybrid: false,
+ expected: true,
+ },
+ {
+ desc: "Tor",
+ addresses: []*lnrpc.NodeAddress{
+ {Addr: "url.onion:9735"},
+ },
+ hybrid: false,
+ expected: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ node := Node{
+ Hybrid: &tc.hybrid,
+ }
+
+ actual := node.checkHybrid(tc.addresses)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("None", func(t *testing.T) {
+ node := Node{
+ Hybrid: nil,
+ }
+
+ actual := node.checkHybrid(nil)
+ assert.True(t, actual)
+ })
+}
+
+func TestCheckFeatureFlags(t *testing.T) {
+ cases := []struct {
+ featureFlags *[]lnrpc.FeatureBit
+ features map[uint32]*lnrpc.Feature
+ desc string
+ expected bool
+ }{
+ {
+ desc: "Knows features",
+ featureFlags: &[]lnrpc.FeatureBit{
+ lnrpc.FeatureBit_AMP_REQ,
+ lnrpc.FeatureBit_AMP_OPT,
+ },
+ features: map[uint32]*lnrpc.Feature{
+ uint32(lnrpc.FeatureBit_AMP_REQ): {IsKnown: true},
+ uint32(lnrpc.FeatureBit_AMP_OPT): {IsKnown: true},
+ },
+ expected: true,
+ },
+ {
+ desc: "Knows only one",
+ featureFlags: &[]lnrpc.FeatureBit{
+ lnrpc.FeatureBit_AMP_REQ,
+ lnrpc.FeatureBit_AMP_OPT,
+ },
+ features: map[uint32]*lnrpc.Feature{
+ uint32(lnrpc.FeatureBit_AMP_OPT): {IsKnown: true},
+ },
+ expected: false,
+ },
+ {
+ desc: "Unknown flags",
+ featureFlags: &[]lnrpc.FeatureBit{
+ lnrpc.FeatureBit_AMP_REQ,
+ lnrpc.FeatureBit_AMP_OPT,
+ },
+ features: map[uint32]*lnrpc.Feature{
+ uint32(lnrpc.FeatureBit_AMP_REQ): {IsKnown: false},
+ uint32(lnrpc.FeatureBit_AMP_OPT): {IsKnown: false},
+ },
+ expected: false,
+ },
+ {
+ desc: "Empty flags",
+ featureFlags: nil,
+ expected: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ node := Node{
+ FeatureFlags: tc.featureFlags,
+ }
+
+ actual := node.checkFeatureFlags(tc.features)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+}
diff --git a/policy/policy.go b/policy/policy.go
new file mode 100644
index 0000000..59ffcdd
--- /dev/null
+++ b/policy/policy.go
@@ -0,0 +1,107 @@
+// Package policy evaluates the set of conditions and requirements set by the node operator that a
+// channel opening request must satisfy.
+package policy
+
+import (
+ "errors"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/lightningnetwork/lnd/lnwire"
+)
+
+// Policy represents a set of requirements that a channel opening request must satisfy. They are
+// enforced only if the conditions are met or do not exist.
+type Policy struct {
+ Conditions *Conditions `yaml:"conditions,omitempty"`
+ Request *Request `yaml:"request,omitempty"`
+ Node *Node `yaml:"node,omitempty"`
+ Whitelist *[]string `yaml:"whitelist,omitempty"`
+ Blacklist *[]string `yaml:"blacklist,omitempty"`
+ RejectAll *bool `yaml:"reject_all,omitempty"`
+ RejectPrivateChannels *bool `yaml:"reject_private_channels,omitempty"`
+ RejectZeroConfChannels *bool `yaml:"reject_zero_conf_channels,omitempty"`
+}
+
+// Evaluate set of policies.
+func (p *Policy) Evaluate(
+ req *lnrpc.ChannelAcceptRequest,
+ nodePublicKey string,
+ peerNode *lnrpc.NodeInfo,
+) error {
+ if p.Conditions != nil && !p.Conditions.Match(req, nodePublicKey, peerNode) {
+ return nil
+ }
+
+ if !p.checkRejectAll() {
+ return errors.New("No new channels are accepted")
+ }
+
+ if p.checkWhitelist(peerNode.Node.PubKey) {
+ return nil
+ }
+
+ if !p.checkBlacklist(peerNode.Node.PubKey) {
+ return errors.New("Node is blacklisted")
+ }
+
+ if !p.checkPrivate(req.ChannelFlags != uint32(lnwire.FFAnnounceChannel)) {
+ return errors.New("Private channels are not accepted")
+ }
+
+ if !p.checkZeroConf(req.WantsZeroConf) {
+ return errors.New("Zero conf channels are not accepted")
+ }
+
+ if err := p.Request.evaluate(req); err != nil {
+ return err
+ }
+
+ return p.Node.evaluate(nodePublicKey, peerNode)
+}
+
+func (p *Policy) checkRejectAll() bool {
+ if p.RejectAll == nil {
+ return true
+ }
+ return !*p.RejectAll
+}
+
+func (p *Policy) checkWhitelist(publicKey string) bool {
+ if p.Whitelist == nil {
+ return false
+ }
+
+ for _, pubKey := range *p.Whitelist {
+ if publicKey == pubKey {
+ return true
+ }
+ }
+ return false
+}
+
+func (p *Policy) checkBlacklist(publicKey string) bool {
+ if p.Blacklist == nil {
+ return true
+ }
+
+ for _, pubKey := range *p.Blacklist {
+ if publicKey == pubKey {
+ return false
+ }
+ }
+ return true
+}
+
+func (p *Policy) checkPrivate(private bool) bool {
+ if p.RejectPrivateChannels == nil || !private {
+ return true
+ }
+ return private && !*p.RejectPrivateChannels
+}
+
+func (p *Policy) checkZeroConf(wantsZeroConf bool) bool {
+ if p.RejectZeroConfChannels == nil || !wantsZeroConf {
+ return true
+ }
+ return wantsZeroConf && !*p.RejectZeroConfChannels
+}
diff --git a/policy/policy_test.go b/policy/policy_test.go
new file mode 100644
index 0000000..c22acd2
--- /dev/null
+++ b/policy/policy_test.go
@@ -0,0 +1,357 @@
+package policy
+
+import (
+ "testing"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEvaluatePolicy(t *testing.T) {
+ nodePublicKey := "node_public_key"
+ peerPublicKey := "peer_public_key"
+ defaultReq := &lnrpc.ChannelAcceptRequest{}
+ defaultPeer := &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ },
+ }
+ tru := true
+ max := uint64(1)
+
+ cases := []struct {
+ policy Policy
+ req *lnrpc.ChannelAcceptRequest
+ peer *lnrpc.NodeInfo
+ desc string
+ fail bool
+ }{
+ {
+ desc: "No policy",
+ policy: Policy{},
+ req: defaultReq,
+ peer: defaultPeer,
+ fail: false,
+ },
+ {
+ desc: "Conditions match",
+ policy: Policy{
+ Conditions: &Conditions{
+ Whitelist: &[]string{peerPublicKey},
+ },
+ },
+ req: defaultReq,
+ peer: defaultPeer,
+ fail: false,
+ },
+ {
+ desc: "No conditions match",
+ policy: Policy{
+ Conditions: &Conditions{
+ Blacklist: &[]string{peerPublicKey},
+ },
+ },
+ req: defaultReq,
+ peer: defaultPeer,
+ fail: false,
+ },
+ {
+ desc: "Whitelist",
+ policy: Policy{
+ Whitelist: &[]string{peerPublicKey},
+ },
+ req: defaultReq,
+ peer: defaultPeer,
+ fail: false,
+ },
+ {
+ desc: "Blacklist",
+ policy: Policy{
+ Blacklist: &[]string{peerPublicKey},
+ },
+ req: defaultReq,
+ peer: defaultPeer,
+ fail: true,
+ },
+ {
+ desc: "Reject all",
+ policy: Policy{
+ RejectAll: &tru,
+ },
+ req: defaultReq,
+ peer: defaultPeer,
+ fail: true,
+ },
+ {
+ desc: "Reject private channels",
+ policy: Policy{
+ RejectPrivateChannels: &tru,
+ },
+ req: &lnrpc.ChannelAcceptRequest{
+ ChannelFlags: 0,
+ },
+ peer: defaultPeer,
+ fail: true,
+ },
+ {
+ desc: "Reject wants zero conf",
+ policy: Policy{
+ RejectZeroConfChannels: &tru,
+ },
+ req: &lnrpc.ChannelAcceptRequest{
+ WantsZeroConf: true,
+ },
+ peer: defaultPeer,
+ fail: true,
+ },
+ {
+ desc: "Request",
+ policy: Policy{
+ Request: &Request{
+ ChannelCapacity: &Range[uint64]{
+ Max: &max,
+ },
+ },
+ },
+ req: &lnrpc.ChannelAcceptRequest{
+ FundingAmt: 10_000,
+ },
+ peer: defaultPeer,
+ fail: true,
+ },
+ {
+ desc: "Node",
+ policy: Policy{
+ Node: &Node{
+ Hybrid: &tru,
+ },
+ },
+ req: defaultReq,
+ peer: &lnrpc.NodeInfo{
+ Node: &lnrpc.LightningNode{
+ PubKey: peerPublicKey,
+ Addresses: []*lnrpc.NodeAddress{
+ {Network: "tcp", Addr: "127.0.0.1:9735"},
+ },
+ },
+ },
+ fail: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ err := tc.policy.Evaluate(tc.req, nodePublicKey, tc.peer)
+ if tc.fail {
+ assert.NotNil(t, err)
+ } else {
+ assert.Nil(t, err)
+ }
+ })
+ }
+}
+
+func TestCheckRejectAll(t *testing.T) {
+ cases := []struct {
+ desc string
+ rejectAll bool
+ expected bool
+ }{
+ {
+ desc: "Reject all",
+ rejectAll: true,
+ expected: false,
+ },
+ {
+ desc: "Do not reject all",
+ rejectAll: false,
+ expected: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ policy := Policy{
+ RejectAll: &tc.rejectAll,
+ }
+
+ actual := policy.checkRejectAll()
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ policy := Policy{}
+ assert.True(t, policy.checkRejectAll())
+ })
+}
+
+func TestCheckWhitelist(t *testing.T) {
+ publicKey := "key"
+
+ cases := []struct {
+ desc string
+ publicKey string
+ whitelist []string
+ expected bool
+ }{
+ {
+ desc: "Whitelisted",
+ publicKey: publicKey,
+ whitelist: []string{publicKey},
+ expected: true,
+ },
+ {
+ desc: "Not whitelisted",
+ publicKey: "not key",
+ whitelist: []string{publicKey},
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ policy := Policy{
+ Whitelist: &tc.whitelist,
+ }
+
+ actual := policy.checkWhitelist(tc.publicKey)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ policy := Policy{}
+ assert.False(t, policy.checkWhitelist(""))
+ })
+}
+
+func TestCheckBlacklist(t *testing.T) {
+ publicKey := "key"
+
+ cases := []struct {
+ desc string
+ publicKey string
+ blacklist []string
+ expected bool
+ }{
+ {
+ desc: "Blacklisted",
+ publicKey: publicKey,
+ blacklist: []string{publicKey},
+ expected: false,
+ },
+ {
+ desc: "Not blacklisted",
+ publicKey: "not key",
+ blacklist: []string{publicKey},
+ expected: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ policy := Policy{
+ Blacklist: &tc.blacklist,
+ }
+
+ actual := policy.checkBlacklist(tc.publicKey)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ policy := Policy{}
+ assert.True(t, policy.checkBlacklist(""))
+ })
+}
+
+func TestCheckPrivate(t *testing.T) {
+ cases := []struct {
+ desc string
+ rejectPrivate bool
+ private bool
+ expected bool
+ }{
+ {
+ desc: "Reject",
+ rejectPrivate: true,
+ private: true,
+ expected: false,
+ },
+ {
+ desc: "Reject 2",
+ rejectPrivate: true,
+ private: false,
+ expected: true,
+ },
+ {
+ desc: "Accept",
+ rejectPrivate: false,
+ private: true,
+ expected: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ policy := Policy{
+ RejectPrivateChannels: &tc.rejectPrivate,
+ }
+
+ actual := policy.checkPrivate(tc.private)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Empty", func(t *testing.T) {
+ policy := Policy{}
+ actual := policy.checkPrivate(true)
+ assert.True(t, actual)
+ })
+}
+
+func TestCheckZeroConf(t *testing.T) {
+ cases := []struct {
+ desc string
+ rejectZeroConf bool
+ wantsZeroConf bool
+ expected bool
+ }{
+ {
+ desc: "Reject",
+ rejectZeroConf: true,
+ wantsZeroConf: true,
+ expected: false,
+ },
+ {
+ desc: "Accept",
+ rejectZeroConf: true,
+ wantsZeroConf: false,
+ expected: true,
+ },
+ {
+ desc: "Accept 2",
+ rejectZeroConf: false,
+ wantsZeroConf: true,
+ expected: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ policy := Policy{
+ RejectZeroConfChannels: &tc.rejectZeroConf,
+ }
+
+ actual := policy.checkZeroConf(tc.wantsZeroConf)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Empty", func(t *testing.T) {
+ policy := Policy{}
+ actual := policy.checkZeroConf(true)
+ assert.True(t, actual)
+ })
+}
diff --git a/policy/range.go b/policy/range.go
new file mode 100644
index 0000000..afb27f1
--- /dev/null
+++ b/policy/range.go
@@ -0,0 +1,194 @@
+package policy
+
+import (
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "golang.org/x/exp/constraints"
+)
+
+// Operations to measure the central tendency of a data set.
+const (
+ // Middle value in a list ordered from smallest to largest.
+ Median Operation = "median"
+ // Average of a list of numbers.
+ Mean Operation = "mean"
+ // Most frequently occurring value on a list.
+ Mode Operation = "mode"
+ // Difference between the biggest and the smallest number.
+ RangeOp Operation = "range"
+)
+
+// Operation is a mathematical operation applied to a set of values.
+type Operation string
+
+// Number is an integer or float.
+type Number interface {
+ constraints.Integer | constraints.Float
+}
+
+// Range represents the limits of a series.
+type Range[T Number] struct {
+ Min *T `yaml:"min,omitempty"`
+ Max *T `yaml:"max,omitempty"`
+}
+
+// Contains returns whether the received value is within the range.
+func (r Range[T]) Contains(v T) bool {
+ if r.Min != nil && v < *r.Min {
+ return false
+ }
+ if r.Max != nil && v > *r.Max {
+ return false
+ }
+ return true
+}
+
+// Reason returns the reason why a number was not in the range.
+func (r Range[T]) Reason() string {
+ if r.Min != nil && r.Max != nil {
+ return fmt.Sprintf("is not between %v and %v", *r.Min, *r.Max)
+ }
+ if r.Min != nil {
+ return fmt.Sprintf("is lower than %v", *r.Min)
+ }
+ if r.Max != nil {
+ return fmt.Sprintf("is higher than %v", *r.Max)
+ }
+
+ return ""
+}
+
+func check[T Number](r *Range[T], v T) bool {
+ if r == nil {
+ return true
+ }
+
+ return r.Contains(v)
+}
+
+// StatRange is like a range but received multiple values and applies an operation to them.
+type StatRange[T Number] struct {
+ Min *T `yaml:"min,omitempty"`
+ Max *T `yaml:"max,omitempty"`
+ Operation Operation `yaml:"operation,omitempty"`
+}
+
+// Contains returns whether the aggregated value is within the range.
+func (a StatRange[T]) Contains(values []T) bool {
+ var v T
+ switch a.Operation {
+ case Median:
+ v = median(values)
+ case Mode:
+ v = mode(values)
+ case RangeOp:
+ v = rangeOp(values)
+ default:
+ v = mean(values)
+ }
+
+ // Range is not used as a property to have a cleaner configuration and avoid declaring min
+ // and max inside "range"
+ r := &Range[T]{
+ Min: a.Min,
+ Max: a.Max,
+ }
+ return r.Contains(v)
+}
+
+// Reason returns the reason why a number was not in the range.
+func (a StatRange[T]) Reason() string {
+ r := &Range[T]{
+ Min: a.Min,
+ Max: a.Max,
+ }
+
+ var sb strings.Builder
+ if a.Operation == "" {
+ a.Operation = Mean
+ }
+ sb.WriteString(string(a.Operation))
+ sb.WriteString(" value ")
+ sb.WriteString(r.Reason())
+ return sb.String()
+}
+
+type channelFunc[T Number] func(channel *lnrpc.ChannelEdge) T
+
+func checkStat[T Number](
+ sr *StatRange[T],
+ peer *lnrpc.NodeInfo,
+ f channelFunc[T],
+) bool {
+ if sr == nil {
+ return true
+ }
+
+ values := make([]T, 0, len(peer.Channels))
+ for _, channel := range peer.Channels {
+ value := f(channel)
+ values = append(values, value)
+ }
+
+ return sr.Contains(values)
+}
+
+func median[T Number](values []T) T {
+ if len(values) == 0 {
+ return 0
+ }
+ slices.Sort(values)
+
+ l := len(values)
+ if l%2 == 0 {
+ return (values[l/2-1] + values[l/2]) / 2.0
+ }
+
+ return values[l/2]
+}
+
+func mean[T Number](values []T) T {
+ if len(values) == 0 {
+ return 0
+ }
+
+ var sum T
+ for _, v := range values {
+ sum += v
+ }
+
+ return sum / T(len(values))
+}
+
+func mode[T Number](values []T) T {
+ if len(values) == 0 {
+ return 0
+ }
+
+ occurences := make(map[T]T)
+ for _, v := range values {
+ occurences[v]++
+ }
+
+ var highest T
+ for value, count := range occurences {
+ if count > occurences[highest] {
+ highest = value
+ }
+ }
+
+ return highest
+}
+
+func rangeOp[T Number](values []T) T {
+ if len(values) < 2 {
+ return 0
+ }
+
+ slices.Sort(values)
+
+ return values[len(values)-1] - values[0]
+}
diff --git a/policy/range_test.go b/policy/range_test.go
new file mode 100644
index 0000000..a339ae5
--- /dev/null
+++ b/policy/range_test.go
@@ -0,0 +1,534 @@
+package policy
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestRangeContains(t *testing.T) {
+ cases := []struct {
+ desc string
+ min int
+ max int
+ value int
+ expected bool
+ }{
+ {
+ desc: "Above min",
+ min: 10,
+ value: 20,
+ expected: true,
+ },
+ {
+ desc: "Below min",
+ min: 10,
+ value: 2,
+ expected: false,
+ },
+ {
+ desc: "Below max",
+ max: 10,
+ value: 5,
+ expected: true,
+ },
+ {
+ desc: "Above max",
+ max: 10,
+ value: 20,
+ expected: false,
+ },
+ {
+ desc: "Between min and max",
+ min: 10,
+ max: 20,
+ value: 15,
+ expected: true,
+ },
+ {
+ desc: "Outside min and max",
+ min: 10,
+ max: 20,
+ value: 25,
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ rng := Range[int]{
+ Min: &tc.min,
+ Max: &tc.max,
+ }
+ if tc.min == 0 {
+ rng.Min = nil
+ }
+ if tc.max == 0 {
+ rng.Max = nil
+ }
+
+ actual := rng.Contains(tc.value)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestRangeReason(t *testing.T) {
+ cases := []struct {
+ desc string
+ expected string
+ min int
+ max int
+ }{
+ {
+ desc: "Min",
+ min: 10,
+ expected: "is lower than 10",
+ },
+ {
+ desc: "Max",
+ max: 10,
+ expected: "is higher than 10",
+ },
+ {
+ desc: "Min and max",
+ min: 10,
+ max: 20,
+ expected: "is not between 10 and 20",
+ },
+ {
+ desc: "Empty",
+ expected: "",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ rng := Range[int]{
+ Min: &tc.min,
+ Max: &tc.max,
+ }
+ if tc.min == 0 {
+ rng.Min = nil
+ }
+ if tc.max == 0 {
+ rng.Max = nil
+ }
+
+ actual := rng.Reason()
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestCheck(t *testing.T) {
+ cases := []struct {
+ desc string
+ min int
+ max int
+ value int
+ expected bool
+ }{
+ {
+ desc: "Contains",
+ min: 1,
+ max: 5,
+ value: 3,
+ expected: true,
+ },
+ {
+ desc: "Equal to min",
+ min: 1,
+ max: 5,
+ value: 1,
+ expected: true,
+ },
+ {
+ desc: "Equal to max",
+ min: 1,
+ max: 5,
+ value: 5,
+ expected: true,
+ },
+ {
+ desc: "Lower than min",
+ min: 1,
+ max: 5,
+ value: 0,
+ expected: false,
+ },
+ {
+ desc: "Higher than max",
+ min: 1,
+ max: 5,
+ value: 6,
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ rng := &Range[int]{
+ Min: &tc.min,
+ Max: &tc.max,
+ }
+
+ actual := check[int](rng, tc.value)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ actual := check[int](nil, 0)
+ assert.True(t, actual)
+ })
+}
+
+func TestStatRangeContains(t *testing.T) {
+ cases := []struct {
+ desc string
+ operation Operation
+ values []int
+ min int
+ max int
+ expected bool
+ }{
+ {
+ desc: "Median",
+ operation: Median,
+ min: 1,
+ max: 9,
+ values: []int{0, 4, 5, 6, 8},
+ expected: true,
+ },
+ {
+ desc: "Median min",
+ operation: Median,
+ min: 2,
+ values: []int{0, 4, 5, 6, 8},
+ expected: true,
+ },
+ {
+ desc: "Median min out",
+ operation: Median,
+ min: 10,
+ values: []int{0, 4, 5, 6, 8},
+ expected: false,
+ },
+ {
+ desc: "Median max",
+ operation: Median,
+ max: 9,
+ values: []int{0, 4, 5, 6, 8},
+ expected: true,
+ },
+ {
+ desc: "Median max out",
+ operation: Median,
+ max: 4,
+ values: []int{0, 4, 5, 6, 8},
+ expected: false,
+ },
+ {
+ desc: "Mean",
+ operation: Mean,
+ min: 1,
+ max: 8,
+ values: []int{0, 4, 5, 6, 8},
+ expected: true,
+ },
+ {
+ desc: "Mean min",
+ operation: Mean,
+ min: 1,
+ values: []int{0, 4, 5, 6, 8},
+ expected: true,
+ },
+ {
+ desc: "Mean min out",
+ operation: Mean,
+ min: 10,
+ values: []int{0, 4, 5, 6, 8},
+ expected: false,
+ },
+ {
+ desc: "Mean max",
+ operation: Mean,
+ max: 9,
+ values: []int{0, 4, 5, 6, 8},
+ expected: true,
+ },
+ {
+ desc: "Mean max out",
+ operation: Mean,
+ max: 3,
+ values: []int{0, 4, 5, 6, 8},
+ expected: false,
+ },
+ {
+ desc: "Mode",
+ operation: Mode,
+ min: 3,
+ max: 6,
+ values: []int{2, 4, 5, 5, 25, 26},
+ expected: true,
+ },
+ {
+ desc: "Mode min",
+ operation: Mode,
+ min: 11,
+ values: []int{11, 11, 13},
+ expected: true,
+ },
+ {
+ desc: "Mode min out",
+ operation: Mode,
+ min: 12,
+ values: []int{11, 11, 13},
+ expected: false,
+ },
+ {
+ desc: "Mode max",
+ operation: Mode,
+ max: 6,
+ values: []int{0, 4, 6, 6, 8},
+ expected: true,
+ },
+ {
+ desc: "Mode max out",
+ operation: Mode,
+ max: 3,
+ values: []int{0, 7, 8, 8},
+ expected: false,
+ },
+ {
+ desc: "Range",
+ operation: RangeOp,
+ min: 1,
+ max: 10,
+ values: []int{0, 4, 5, 6, 8},
+ expected: true,
+ },
+ {
+ desc: "Range min",
+ operation: RangeOp,
+ min: 5,
+ values: []int{0, 11, 15},
+ expected: true,
+ },
+ {
+ desc: "Range min out",
+ operation: RangeOp,
+ min: 12,
+ values: []int{6, 11, 13},
+ expected: false,
+ },
+ {
+ desc: "Range max",
+ operation: RangeOp,
+ max: 6,
+ values: []int{1, 4, 5},
+ expected: true,
+ },
+ {
+ desc: "Range max out",
+ operation: RangeOp,
+ max: 3,
+ values: []int{0, 4},
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ statRange := StatRange[int]{
+ Min: &tc.min,
+ Max: &tc.max,
+ Operation: tc.operation,
+ }
+ if tc.min == 0 {
+ statRange.Min = nil
+ }
+ if tc.max == 0 {
+ statRange.Max = nil
+ }
+
+ actual := statRange.Contains(tc.values)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestStatRangeReason(t *testing.T) {
+ cases := []struct {
+ desc string
+ expected string
+ operation Operation
+ min int
+ max int
+ }{
+ {
+ desc: "Min",
+ operation: Mean,
+ min: 10,
+ expected: "mean value is lower than 10",
+ },
+ {
+ desc: "Max",
+ operation: Median,
+ max: 10,
+ expected: "median value is higher than 10",
+ },
+ {
+ desc: "Min and max (mode)",
+ operation: Mode,
+ min: 10,
+ max: 20,
+ expected: "mode value is not between 10 and 20",
+ },
+ {
+ desc: "Min and max (range)",
+ operation: RangeOp,
+ min: 5,
+ max: 8,
+ expected: "range value is not between 5 and 8",
+ },
+ {
+ desc: "Default operation",
+ expected: "mean value ",
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ rng := StatRange[int]{
+ Operation: tc.operation,
+ Min: &tc.min,
+ Max: &tc.max,
+ }
+ if tc.min == 0 {
+ rng.Min = nil
+ }
+ if tc.max == 0 {
+ rng.Max = nil
+ }
+
+ actual := rng.Reason()
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestMedian(t *testing.T) {
+ cases := []struct {
+ desc string
+ values []int
+ expected int
+ }{
+ {
+ desc: "Even number of values",
+ values: []int{1, 4, 5, 7, 8, 12},
+ expected: 6,
+ },
+ {
+ desc: "Odd number of values",
+ values: []int{1, 4, 5, 7, 8, 12, 13},
+ expected: 7,
+ },
+ {
+ desc: "No values",
+ values: []int{},
+ expected: 0,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ actual := median(tc.values)
+ assert.Exactly(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestMean(t *testing.T) {
+ cases := []struct {
+ desc string
+ values []int
+ expected int
+ }{
+ {
+ desc: "Round result",
+ values: []int{4, 6, 11},
+ expected: 7,
+ },
+ {
+ desc: "Approximate result",
+ values: []int{4, 6, 10},
+ expected: 6,
+ },
+ {
+ desc: "No values",
+ values: []int{},
+ expected: 0,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ actual := mean(tc.values)
+ assert.Exactly(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestMode(t *testing.T) {
+ cases := []struct {
+ desc string
+ values []int
+ expected int
+ }{
+ {
+ desc: "Mode",
+ values: []int{1, 1, 2, 5, 7, 4, 6, 1},
+ expected: 1,
+ },
+ {
+ desc: "No values",
+ values: []int{},
+ expected: 0,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ actual := mode(tc.values)
+ assert.Exactly(t, tc.expected, actual)
+ })
+ }
+}
+
+func TestRangeOp(t *testing.T) {
+ cases := []struct {
+ desc string
+ values []int
+ expected int
+ }{
+ {
+ desc: "Range",
+ values: []int{2, 23},
+ expected: 21,
+ },
+ {
+ desc: "No values",
+ values: []int{},
+ expected: 0,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ actual := rangeOp(tc.values)
+ assert.Exactly(t, tc.expected, actual)
+ })
+ }
+}
diff --git a/policy/request.go b/policy/request.go
new file mode 100644
index 0000000..c7b2460
--- /dev/null
+++ b/policy/request.go
@@ -0,0 +1,77 @@
+package policy
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+)
+
+// Request represents the desired values in a channel request.
+type Request struct {
+ ChannelCapacity *Range[uint64] `yaml:"channel_capacity,omitempty"`
+ ChannelReserve *Range[uint64] `yaml:"channel_reserve,omitempty"`
+ CSVDelay *Range[uint32] `yaml:"csv_delay,omitempty"`
+ PushAmount *Range[uint64] `yaml:"push_amount,omitempty"`
+ MaxAcceptedHTLCs *Range[uint32] `yaml:"max_accepted_htlcs,omitempty"`
+ MinHTLC *Range[uint64] `yaml:"min_htlc,omitempty"`
+ MaxValueInFlight *Range[uint64] `yaml:"max_value_in_flight,omitempty"`
+ DustLimit *Range[uint64] `yaml:"dust_limit,omitempty"`
+ CommitmentTypes *[]lnrpc.CommitmentType `yaml:"commitment_types,omitempty"`
+}
+
+func (r *Request) evaluate(req *lnrpc.ChannelAcceptRequest) error {
+ if r == nil {
+ return nil
+ }
+
+ if !check(r.ChannelCapacity, req.FundingAmt) {
+ return errors.New("Channel capacity " + r.ChannelCapacity.Reason())
+ }
+
+ if !check(r.PushAmount, req.PushAmt) {
+ return errors.New("Pushed amount lower than expected")
+ }
+
+ if !check(r.ChannelReserve, req.ChannelReserve) {
+ return errors.New("Channel reserve " + r.ChannelReserve.Reason())
+ }
+
+ if !check(r.CSVDelay, req.CsvDelay) {
+ return errors.New("Check sequence verify delay " + r.CSVDelay.Reason())
+ }
+
+ if !check(r.MaxAcceptedHTLCs, req.MaxAcceptedHtlcs) {
+ return errors.New("Maximum accepted HTLCs " + r.MaxAcceptedHTLCs.Reason())
+ }
+
+ if !check(r.MinHTLC, req.MinHtlc) {
+ return errors.New("Minimum HTLCs " + r.MinHTLC.Reason())
+ }
+
+ if !check(r.MaxValueInFlight, req.MaxValueInFlight) {
+ return errors.New("Maximum value in flight " + r.MaxValueInFlight.Reason())
+ }
+
+ if !check(r.DustLimit, req.DustLimit) {
+ return errors.New("Commitment transaction dust limit " + r.DustLimit.Reason())
+ }
+
+ if !r.checkCommitmentType(req.CommitmentType) {
+ return fmt.Errorf("Commitment type is not in %s", *r.CommitmentTypes)
+ }
+
+ return nil
+}
+
+func (r *Request) checkCommitmentType(commitmentType lnrpc.CommitmentType) bool {
+ if r.CommitmentTypes == nil {
+ return true
+ }
+ for _, ct := range *r.CommitmentTypes {
+ if ct == commitmentType {
+ return true
+ }
+ }
+ return false
+}
diff --git a/policy/request_test.go b/policy/request_test.go
new file mode 100644
index 0000000..c711d4f
--- /dev/null
+++ b/policy/request_test.go
@@ -0,0 +1,195 @@
+package policy
+
+import (
+ "testing"
+
+ "github.com/lightningnetwork/lnd/lnrpc"
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEvaluateRequest(t *testing.T) {
+ max64 := uint64(1)
+ max32 := uint32(1)
+
+ cases := []struct {
+ chanReq *lnrpc.ChannelAcceptRequest
+ req *Request
+ desc string
+ fail bool
+ }{
+ {
+ desc: "Nil request",
+ req: nil,
+ fail: false,
+ },
+ {
+ desc: "Empty request",
+ req: &Request{},
+ chanReq: &lnrpc.ChannelAcceptRequest{},
+ fail: false,
+ },
+ {
+ desc: "Channel capacity",
+ req: &Request{
+ ChannelCapacity: &Range[uint64]{
+ Max: &max64,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ FundingAmt: 1000,
+ },
+ fail: true,
+ },
+ {
+ desc: "Push amount",
+ req: &Request{
+ PushAmount: &Range[uint64]{
+ Max: &max64,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ PushAmt: 1000,
+ },
+ fail: true,
+ },
+ {
+ desc: "Channel reserve",
+ req: &Request{
+ ChannelReserve: &Range[uint64]{
+ Max: &max64,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ ChannelReserve: 1000,
+ },
+ fail: true,
+ },
+ {
+ desc: "CSV delay",
+ req: &Request{
+ CSVDelay: &Range[uint32]{
+ Max: &max32,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ CsvDelay: 144,
+ },
+ fail: true,
+ },
+ {
+ desc: "Max accepted HTLCs",
+ req: &Request{
+ MaxAcceptedHTLCs: &Range[uint32]{
+ Max: &max32,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ MaxAcceptedHtlcs: 300,
+ },
+ fail: true,
+ },
+ {
+ desc: "Min HTLC",
+ req: &Request{
+ MinHTLC: &Range[uint64]{
+ Max: &max64,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ MinHtlc: 5,
+ },
+ fail: true,
+ },
+ {
+ desc: "Max value in flight",
+ req: &Request{
+ MaxValueInFlight: &Range[uint64]{
+ Max: &max64,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ MaxValueInFlight: 1000,
+ },
+ fail: true,
+ },
+ {
+ desc: "Dust limit",
+ req: &Request{
+ DustLimit: &Range[uint64]{
+ Max: &max64,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ DustLimit: 1000,
+ },
+ fail: true,
+ },
+ {
+ desc: "Commitment type",
+ req: &Request{
+ CommitmentTypes: &[]lnrpc.CommitmentType{
+ lnrpc.CommitmentType_ANCHORS,
+ },
+ },
+ chanReq: &lnrpc.ChannelAcceptRequest{
+ CommitmentType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE,
+ },
+ fail: true,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ err := tc.req.evaluate(tc.chanReq)
+ if tc.fail {
+ assert.NotNil(t, err)
+ } else {
+ assert.Nil(t, err)
+ }
+ })
+ }
+}
+
+func TestCheckCommitmentType(t *testing.T) {
+ cases := []struct {
+ desc string
+ commitmentTypes []lnrpc.CommitmentType
+ commitmentType lnrpc.CommitmentType
+ expected bool
+ }{
+ {
+ desc: "Accept",
+ commitmentType: lnrpc.CommitmentType_ANCHORS,
+ commitmentTypes: []lnrpc.CommitmentType{
+ lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE,
+ lnrpc.CommitmentType_ANCHORS,
+ },
+ expected: true,
+ },
+ {
+ desc: "Reject",
+ commitmentType: lnrpc.CommitmentType_LEGACY,
+ commitmentTypes: []lnrpc.CommitmentType{
+ lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE,
+ lnrpc.CommitmentType_ANCHORS,
+ },
+ expected: false,
+ },
+ }
+
+ for _, tc := range cases {
+ t.Run(tc.desc, func(t *testing.T) {
+ r := Request{
+ CommitmentTypes: &tc.commitmentTypes,
+ }
+
+ actual := r.checkCommitmentType(tc.commitmentType)
+ assert.Equal(t, tc.expected, actual)
+ })
+ }
+
+ t.Run("Nil", func(t *testing.T) {
+ r := Request{}
+ assert.True(t, r.checkCommitmentType(lnrpc.CommitmentType_ANCHORS))
+ })
+}