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