Merge pull request #9884 from ellemouton/relaxFeatureBitCheck
What changed, and why it matters
This commit is a massive repository import or initial commit adding the entire LND codebase plus CI, docs, and tooling. The PR title mentions relaxing a feature-bit check for peer features, but the supplied diff does not show any code changes related to that title—it only shows newly added files. There is no visible security patch or vulnerability fix in the provided materials.
Obtain the actual code diff for PR #9884 (the non-boilerplate changes). Focus review on lnwire/features.go, feature/set.go, and any peer feature-bit handling paths. Until the real code changes are available, do not treat this commit as a security patch.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit hash d1d3a82010ae393ad205ab9e249737e0bfef1416 is titled ‘Merge pull request #9884 from ellemouton/relaxFeatureBitCheck’ with message ‘multi: use relaxed feature bit Set method for peer features’. However, the diff presented is an enormous addition of 1,944 files (stats +771807 -0) and only the very beginning of those additions is shown. The visible portion contains repository boilerplate: .editorconfig, .gemini config, GitHub Actions, .gitignore, .golangci.yml, Makefile, README, SECURITY.md, and the start of accessman.go. No changes to lnwire/features.go, feature/set.go, peer handling, or any ‘relaxed feature bit Set method’ are visible. Without the actual code diff for the claimed change, no security-relevant code modification can be evaluated.
Changed components
Inspect captured patch +771807 / −0
diff --git a/.custom-gcl.yml b/.custom-gcl.yml
new file mode 100644
index 0000000..6ca1009
--- /dev/null
+++ b/.custom-gcl.yml
@@ -0,0 +1,4 @@
+version: v1.57.0
+plugins:
+ - module: 'github.com/lightningnetwork/lnd/tools/linters'
+ path: ./tools/linters
\ No newline at end of file
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..110b5c2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,16 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# Top-most EditorConfig file.
+root = true
+
+# Unix-style newlines with a newline ending every file.
+[*.md]
+end_of_line = lf
+insert_final_newline = true
+max_line_length = 80
+
+# 8 space indentation for Golang code.
+[*.go]
+indent_style = tab
+indent_size = 8
+max_line_length = 80
diff --git a/.gemini/config.yaml b/.gemini/config.yaml
new file mode 100644
index 0000000..9b4e971
--- /dev/null
+++ b/.gemini/config.yaml
@@ -0,0 +1,12 @@
+# Config for the Gemini Pull Request Review Bot.
+# https://github.com/marketplace/gemini-code-assist
+have_fun: false
+code_review:
+ disable: false
+ comment_severity_threshold: MEDIUM
+ max_review_comments: -1
+ pull_request_opened:
+ help: false
+ summary: true
+ code_review: true
+ignore_patterns: []
diff --git a/.gemini/styleguide.md b/.gemini/styleguide.md
new file mode 100644
index 0000000..a9b498a
--- /dev/null
+++ b/.gemini/styleguide.md
@@ -0,0 +1,331 @@
+# LND Style Guide
+
+## Code Documentation and Commenting
+
+- Always use the Golang code style described below in this document.
+- Readable code is the most important requirement for any commit created.
+- Comments must not explain the code 1:1 but instead explain the _why_ behind a
+ certain block of code, in case it requires contextual knowledge.
+- Unit tests must always use the `require` library. Either table driven unit
+ tests or tests using the `rapid` library are preferred.
+- The line length MUST NOT exceed 80 characters, this is very important.
+ You must count the Golang indentation (tabulator character) as 8 spaces when
+ determining the line length. Use creative approaches or the wrapping rules
+ specified below to make sure the line length isn't exceeded. HOWEVER: during
+ code reviews, please leave this check up to the linter and do not comment on
+ it (why? because gemini is bad at counting characters).
+- Every function must be commented with its purpose and assumptions.
+- Function comments must begin with the function name.
+- Function comments should be complete sentences.
+- Exported functions require detailed comments for the caller.
+
+**WRONG**
+```go
+// generates a revocation key
+func DeriveRevocationPubkey(commitPubKey *btcec.PublicKey,
+ revokePreimage []byte) *btcec.PublicKey {
+```
+**RIGHT**
+```go
+// DeriveRevocationPubkey derives the revocation public key given the
+// counterparty's commitment key, and revocation preimage derived via a
+// pseudo-random-function. In the event that we (for some reason) broadcast a
+// revoked commitment transaction, then if the other party knows the revocation
+// preimage, then they'll be able to derive the corresponding private key to
+// this private key by exploiting the homomorphism in the elliptic curve group.
+//
+// The derivation is performed as follows:
+//
+// revokeKey := commitKey + revokePoint
+// := G*k + G*h
+// := G * (k+h)
+//
+// Therefore, once we divulge the revocation preimage, the remote peer is able
+// to compute the proper private key for the revokeKey by computing:
+// revokePriv := commitPriv + revokePreimge mod N
+//
+// Where N is the order of the sub-group.
+func DeriveRevocationPubkey(commitPubKey *btcec.PublicKey,
+ revokePreimage []byte) *btcec.PublicKey {
+```
+- In-body comments should explain the *intention* of the code.
+
+**WRONG**
+```go
+// return err if amt is less than 546
+if amt < 546 {
+ return err
+}
+```
+**RIGHT**
+```go
+// Treat transactions with amounts less than the amount which is considered dust
+// as non-standard.
+if amt < 546 {
+ return err
+}
+```
+
+## Code Spacing and formatting
+
+- Segment code into logical stanzas separated by newlines.
+
+**WRONG**
+```go
+ witness := make([][]byte, 4)
+ witness[0] = nil
+ if bytes.Compare(pubA, pubB) == -1 {
+ witness[1] = sigB
+ witness[2] = sigA
+ } else {
+ witness[1] = sigA
+ witness[2] = sigB
+ }
+ witness[3] = witnessScript
+ return witness
+```
+**RIGHT**
+```go
+ witness := make([][]byte, 4)
+
+ // When spending a p2wsh multi-sig script, rather than an OP_0, we add
+ // a nil stack element to eat the extra pop.
+ witness[0] = nil
+
+ // When initially generating the witnessScript, we sorted the serialized
+ // public keys in descending order. So we do a quick comparison in order
+ // to ensure the signatures appear on the Script Virtual Machine stack in
+ // the correct order.
+ if bytes.Compare(pubA, pubB) == -1 {
+ witness[1] = sigB
+ witness[2] = sigA
+ } else {
+ witness[1] = sigA
+ witness[2] = sigB
+ }
+
+ // Finally, add the preimage as the last witness element.
+ witness[3] = witnessScript
+
+ return witness
+```
+
+- Use spacing between `case` and `select` stanzas.
+
+**WRONG**
+```go
+ switch {
+ case a:
+ <code block>
+ case b:
+ <code block>
+ case c:
+ <code block>
+ case d:
+ <code block>
+ default:
+ <code block>
+ }
+```
+**RIGHT**
+```go
+ switch {
+ // Brief comment detailing instances of this case (repeat below).
+ case a:
+ <code block>
+
+ case b:
+ <code block>
+
+ case c:
+ <code block>
+
+ case d:
+ <code block>
+
+ default:
+ <code block>
+ }
+```
+
+## Additional Style Constraints
+
+### 80 character line length
+
+- Wrap columns at 80 characters.
+- Tabs are 8 spaces.
+
+**WRONG**
+```go
+myKey := "0214cd678a565041d00e6cf8d62ef8add33b4af4786fb2beb87b366a2e151fcee7"
+```
+
+**RIGHT**
+```go
+myKey := "0214cd678a565041d00e6cf8d62ef8add33b4af4786fb2beb87b366a2e1" +
+ "51fcee7"
+```
+
+### Wrapping long function calls
+
+- If a function call exceeds the column limit, place the closing parenthesis
+ on its own line and start all arguments on a new line after the opening
+ parenthesis.
+
+**WRONG**
+```go
+value, err := bar(a,
+ a, b, c)
+```
+
+**RIGHT**
+```go
+value, err := bar(
+ a, a, b, c,
+)
+```
+
+- Compact form is acceptable if visual symmetry of parentheses is preserved.
+
+**ACCEPTABLE**
+```go
+ response, err := node.AddInvoice(
+ ctx, &lnrpc.Invoice{
+ Memo: "invoice",
+ ValueMsat: int64(oneUnitMilliSat - 1),
+ },
+ )
+```
+
+**PREFERRED**
+```go
+ response, err := node.AddInvoice(ctx, &lnrpc.Invoice{
+ Memo: "invoice",
+ ValueMsat: int64(oneUnitMilliSat - 1),
+ })
+```
+
+### Exception for log and error message formatting
+
+- Minimize lines for log and error messages, while adhering to the
+ 80-character limit.
+
+**WRONG**
+```go
+return fmt.Errorf(
+ "this is a long error message with a couple (%d) place holders",
+ len(things),
+)
+
+log.Debugf(
+ "Something happened here that we need to log: %v",
+ longVariableNameHere,
+)
+```
+
+**RIGHT**
+```go
+return fmt.Errorf("this is a long error message with a couple (%d) place "+
+ "holders", len(things))
+
+log.Debugf("Something happened here that we need to log: %v",
+ longVariableNameHere)
+```
+
+### Exceptions and additional styling for structured logging
+
+- **Static messages:** Use key-value pairs instead of formatted strings for the
+ `msg` parameter.
+- **Key-value attributes:** Use `slog.Attr` helper functions.
+- **Line wrapping:** Structured log lines are an exception to the 80-character
+ rule. Use one line per key-value pair for multiple attributes.
+
+**WRONG**
+```go
+log.DebugS(ctx, fmt.Sprintf("User %d just spent %.8f to open a channel", userID, 0.0154))
+```
+
+**RIGHT**
+```go
+log.InfoS(ctx, "Channel open performed",
+ slog.Int("user_id", userID),
+ btclog.Fmt("amount", "%.8f", 0.00154))
+```
+
+### Wrapping long function definitions
+
+- If function arguments exceed the 80-character limit, maintain indentation
+ on following lines.
+- Do not end a line with an open parenthesis if the function definition is not
+ finished.
+
+**WRONG**
+```go
+func foo(a, b, c,
+) (d, error) {
+
+func bar(a, b, c) (
+ d, error,
+) {
+
+func baz(a, b, c) (
+ d, error) {
+```
+**RIGHT**
+```go
+func foo(a, b,
+ c) (d, error) {
+
+func baz(a, b, c) (d,
+ error) {
+
+func longFunctionName(
+ a, b, c) (d, error) {
+```
+
+- If a function declaration spans multiple lines, the body should start with an
+ empty line.
+
+**WRONG**
+```go
+func foo(a, b, c,
+ d, e) error {
+ var a int
+}
+```
+**RIGHT**
+```go
+func foo(a, b, c,
+ d, e) error {
+
+ var a int
+}
+```
+
+## Use of Log Levels
+
+- Available levels: `trace`, `debug`, `info`, `warn`, `error`, `critical`.
+- Only use `error` for internal errors not triggered by external sources.
+
+## Testing
+
+- To run all tests for a specific package:
+ `make unit pkg=$pkg`
+- To run a specific test case within a package:
+ `make unit pkg=$pkg case=$case`
+
+## Git Commit Messages
+
+- **Subject Line:**
+ - Format: `subsystem: short description of changes`
+ - `subsystem` should be the package primarily affected (e.g., `lnwallet`, `rpcserver`).
+ - For multiple packages, use `+` or `,` as a delimiter (e.g., `lnwallet+htlcswitch`).
+ - For widespread changes, use `multi:`.
+ - Keep it under 50 characters.
+ - Use the present tense (e.g., "Fix bug", not "Fixed bug").
+
+- **Message Body:**
+ - Separate from the subject with a blank line.
+ - Explain the "what" and "why" of the change.
+ - Wrap text to 72 characters.
+ - Use bullet points for lists.
diff --git a/.github/CODEOWNERS-HINT b/.github/CODEOWNERS-HINT
new file mode 100644
index 0000000..523fa8e
--- /dev/null
+++ b/.github/CODEOWNERS-HINT
@@ -0,0 +1,195 @@
+# This file lists the owners of code in different areas of the lnd codebase
+# Codeowners will own the review for the changes being merged into their
+# respective areas
+
+# aezeed
+/aezeed/ @guggero @yyforyongyu @roasbeef
+
+# alias manager
+/aliasmgr/ @Crypt-iQ
+
+# amp
+/amp/ @yyforyongyu @roasbeef
+
+# auto pilot
+/autopilot/ @bitromortac
+
+# batch
+/batch/ @bhandras
+
+# block cache
+/blockcache/ @yyforyongyu @ellemouton
+
+# brontide
+/brontide/ @Roasbeef @yyforyongyu @morehouse
+
+# buffer
+/buffer/ @yyforyongyu
+
+# build
+/build/ @Roasbeef @guggero
+
+# certificates
+/cert/ @guggero
+
+# chain notifications
+/chainntnfs/ @Roasbeef @yyforyongyu
+
+# chain registry
+/chainreg/ @ellemouton
+
+# channel acceptor
+/chanacceptor/ @Crypt-iQ
+
+# channel backup
+/chanbackup/ @guggero @ellemouton
+
+# channel fitness
+/chanfitness/ @yyforyongyu
+
+# channel db
+/channeldb/ @Roasbeef @yyforyongyu
+
+# channel notifier
+/channelnotifier/ @yyforyongyu
+
+# clock
+/clock/ @bhandras
+
+# cluster
+/cluster/ @bhandras
+
+# command line
+/cmd/ @ellemouton
+
+# contract court
+/contractcourt/ @yyforyongyu @Roasbeef @Crypt-iQ
+
+# contrib
+/contrib/ @guggero
+
+# discovery
+/discovery/ @ellemouton @yyforyongyu
+
+# docker
+/docker/ @guggero
+
+# feature
+/feature/ @ProofOfKeags
+
+# functions/methods
+/fn/ @Roasbeef @ProofOfKeags
+
+# funding
+/funding/ @Crypt-iQ @morehouse
+
+# health check
+/healthcheck/ @guggero
+
+# htlc switch
+/htlcswitch/ @Roasbeef @yyforyongyu
+
+# musig2
+/internal/musig2v040/ @guggero
+
+# invoices
+/invoices/ @yyforyongyu @bhandras
+
+# key chain
+/keychain/ @guggero @roasbeef
+
+# kvdb
+/kvdb/ @bhandras
+
+# lncfg
+/lncfg/ @Roasbeef
+
+# lnencrypt
+/lnencrypt/ @guggero
+
+# lnpeer
+/lnpeer/ @Roasbeef
+
+#lntest
+/lntest/ @yyforyongyu
+
+# lntypes
+/lntypes/ @bitromortac
+
+# lnutils
+/lnutils/ @yyforyongyu
+
+# lnwallet
+/lnwallet/ @Roasbeef @yyforyongyu @Crypt-iQ
+
+# lnwire
+/lnwire/ @ellemouton @morehouse
+
+# macaroons
+/macaroons/ @guggero
+
+# mobile
+/mobile/ @guggero
+
+# monitoring
+/monitoring/ @guggero
+
+# multimutex
+/multimutex/ @Roasbeef
+
+# nat
+/nat/ @Roasbeef
+
+# network announcements
+/netann/ @ellemouton @yyforyongyu
+
+# peer
+/peer/ @ProofOfKeags @Crypt-iQ @morehouse
+
+# peernotifier
+/peernotifier/ @yyforyongyu
+
+# pool
+/pool/ @yyforyongyu
+
+# queue
+/queue/ @bhandras
+
+# record
+/record/ @guggero
+
+# routing
+/routing/ @bitromortac @ellemouton @yyforyongyu
+
+# rpcperms
+/rpcperms/ @guggero
+
+# shachain
+/shachain/ @Roasbeef
+
+# signal
+/signal/ @Roasbeef
+
+# sqldb
+/sqldb/ @bhandras
+
+# sweep
+/sweep/ @yyforyongyu @ziggie1984
+
+# ticker
+/ticker/ @guggero
+
+# tlv
+/tlv/ @Roasbeef
+
+# tor
+/tor/ @yyforyongyu
+
+# walletunlocker
+/walletunlocker/ @guggero
+
+# watchtower
+/watchtower/ @ellemouton
+
+# zpay32
+/zpay32/ @Roasbeef @morehouse
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..b3eba1d
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,30 @@
+---
+name: Bug report
+about: Create a bug report. Please use the discussions section for general or troubleshooting questions.
+title: '[bug]: '
+labels: ["bug", "needs triage"]
+assignees: ''
+---
+
+### Background
+
+Describe your issue here.
+
+### Your environment
+
+* version of `lnd`
+* which operating system (`uname -a` on *Nix)
+* version of `btcd`, `bitcoind`, or other backend
+* any other relevant environment details
+
+### Steps to reproduce
+
+Tell us how to reproduce this issue. Please provide stacktraces and links to code in question.
+
+### Expected behaviour
+
+Tell us what should happen
+
+### Actual behaviour
+
+Tell us what happens instead
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..a04ad5a
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,14 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Discussions
+ url: https://github.com/lightningnetwork/lnd/discussions
+ about: For general or troubleshooting questions or if you're not sure what issue type to pick.
+ - name: Documentation for lnd and lightning-terminal
+ url: https://docs.lightning.engineering/
+ about: Please make sure the documentation cannot answer your question first.
+ - name: Lightning Community Slack
+ url: https://lightning.engineering/slack.html
+ about: Please ask and answer questions here.
+ - name: Security issue disclosure policy
+ url: https://github.com/lightningnetwork/lnd#security
+ about: Please refer to this document when reporting security related issues.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..ed8bfb3
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,19 @@
+---
+name: Feature request
+about: Suggest a new feature for `lnd`.
+title: '[feature]: '
+labels: enhancement
+assignees: ''
+---
+
+**Is your feature request related to a problem? Please describe.**
+<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
+
+**Describe the solution you'd like**
+<!-- A clear and concise description of what you want to happen. -->
+
+**Describe alternatives you've considered**
+<!-- A clear and concise description of any alternative solutions or features you've considered. -->
+
+**Additional context**
+<!-- Add any other context or screenshots about the feature request here. -->
diff --git a/.github/actions/check-label/action.yml b/.github/actions/check-label/action.yml
new file mode 100644
index 0000000..00c344a
--- /dev/null
+++ b/.github/actions/check-label/action.yml
@@ -0,0 +1,30 @@
+name: "Check PR labels"
+description: "Checks if specific labels are present on a PR and sets outputs accordingly."
+
+inputs:
+ label:
+ description: "The label to check for"
+ required: true
+ skip-message:
+ description: "The message to display when skipping due to label"
+ required: true
+
+outputs:
+ skip:
+ description: "Whether to skip the tests (true/false)"
+ value: ${{ steps.check.outputs.skip }}
+
+runs:
+ using: "composite"
+ steps:
+ - name: Check for label
+ id: check
+ shell: bash
+ run: |
+ if [[ "${{ contains(github.event.pull_request.labels.*.name, inputs.label) }}" == "true" ]]; then
+ echo "::notice::${{ inputs.skip-message }}"
+ echo "${{ inputs.skip-message }}" >> $GITHUB_STEP_SUMMARY
+ echo "skip=true" >> $GITHUB_OUTPUT
+ else
+ echo "skip=false" >> $GITHUB_OUTPUT
+ fi
\ No newline at end of file
diff --git a/.github/actions/cleanup-space/action.yml b/.github/actions/cleanup-space/action.yml
new file mode 100644
index 0000000..037435f
--- /dev/null
+++ b/.github/actions/cleanup-space/action.yml
@@ -0,0 +1,16 @@
+name: "Clean up runner disk space"
+description: "Removes large, non-essential toolsets to free up disk space on the runner."
+
+runs:
+ using: "composite"
+ steps:
+ - name: Free up disk space
+ shell: bash
+ run: |
+ echo "Removing large toolsets to free up disk space..."
+ # Remove dotnet to save disk space.
+ sudo rm -rf /usr/share/dotnet
+ # Remove android to save disk space.
+ sudo rm -rf /usr/local/lib/android
+ # Remove ghc to save disk space.
+ sudo rm -rf /opt/ghc
diff --git a/.github/actions/rebase/action.yml b/.github/actions/rebase/action.yml
new file mode 100644
index 0000000..cf2e72f
--- /dev/null
+++ b/.github/actions/rebase/action.yml
@@ -0,0 +1,15 @@
+name: "Rebase on to the PR target base branch"
+description: "A reusable workflow that's used to rebase the PR code on to the target base branch."
+
+runs:
+ using: "composite"
+
+ steps:
+ - name: fetch and rebase on ${{ github.base_ref }}
+ shell: bash
+ run: |
+ git remote add upstream https://github.com/${{ github.repository }}
+ git fetch upstream ${{ github.base_ref }}:refs/remotes/upstream/${{ github.base_ref }}
+ export GIT_COMMITTER_EMAIL="lnd-ci@example.com"
+ export GIT_COMMITTER_NAME="LND CI"
+ git rebase upstream/${{ github.base_ref }}
diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml
new file mode 100644
index 0000000..3ba6348
--- /dev/null
+++ b/.github/actions/setup-go/action.yml
@@ -0,0 +1,90 @@
+name: "Setup Golang environment"
+description: "A reusable workflow that's used to set up the Go environment and cache."
+inputs:
+ go-version:
+ description: "The version of Golang to set up"
+ required: true
+ key-prefix:
+ description: "A prefix to use for the cache key, to separate cache entries from other workflows"
+ required: false
+ use-build-cache:
+ description: "Whether to use the build cache"
+ required: false
+ # Boolean values aren't supported in the workflow syntax, so we use a
+ # string. To not confuse the value with true/false, we use 'yes' and 'no'.
+ default: 'yes'
+
+runs:
+ using: "composite"
+
+ steps:
+ - name: setup go ${{ inputs.go-version }}
+ uses: actions/setup-go@v5
+ with:
+ go-version: '${{ inputs.go-version }}'
+ cache: 'false'
+
+ # When we run go build or go test, the Go compiler calculates a signature
+ # for each package based on the content of its `.go` source files, its
+ # dependencies, and the compiler flags. It then checks the restored build
+ # cache for an entry matching that exact signature to speed up the jobs.
+ # - Cache Hit: If an entry exists (meaning the source files and
+ # dependencies haven't changed), it reuses the compiled artifact directly
+ # from the cache.
+ # - Cache Miss: If no entry exists (because we changed a line of code), it
+ # recompiles that specific package and stores the new result in the cache
+ # for the next time.
+ - name: go cache
+ if: ${{ inputs.use-build-cache == 'yes' }}
+ uses: actions/cache@v4
+ with:
+ # In order:
+ # * Module download cache
+ # * Build cache (Linux)
+ # * Build cache (Mac)
+ # * Build cache (Windows)
+ path: |
+ ~/go/pkg/mod
+ ~/.cache/go-build
+ ~/Library/Caches/go-build
+ ~\AppData\Local\go-build
+
+ # The key is used to create and later look up the cache. It's made of
+ # four parts:
+ # - The base part is made from the OS name, Go version and a
+ # job-specified key prefix. Example: `linux-go-1.24.6-unit-test-`.
+ # It ensures that a job running on Linux with Go 1.24 only looks for
+ # caches from the same environment.
+ # - The unique part is the `hashFiles('**/go.sum')`, which calculates a
+ # hash (a fingerprint) of the go.sum file.
+ key: ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-${{ hashFiles('**/go.sum') }}
+
+ # The restore-keys provides a list of fallback keys. If no cache
+ # matches the key exactly, the action will look for a cache where the
+ # key starts with one of the restore-keys. The action searches the
+ # restore-keys list in order and restores the most recently created
+ # cache that matches the prefix. Once the job is done, a new cache is
+ # created and saved using the new key.
+ restore-keys: |
+ ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-
+
+ # The complete, downloaded source code of all our dependencies (the
+ # libraries lnd project imports). This prevents the go command from having
+ # to re-download every dependency from the internet on every single job
+ # run. It's like having a local library of all the third-party code lnd
+ # needs.
+ - name: go module cache
+ if: ${{ inputs.use-build-cache == 'no' }}
+ uses: actions/cache@v4
+ with:
+ # Just the module download cache.
+ path: |
+ ~/go/pkg/mod
+ key: ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-no-build-cache-${{ hashFiles('**/go.sum') }}
+ restore-keys: |
+ ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-no-build-cache-
+
+ - name: set GOPATH
+ shell: bash
+ run: |
+ echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..22b0f99
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,21 @@
+## Change Description
+Description of change / link to associated issue.
+
+## Steps to Test
+Steps for reviewers to follow to test the change.
+
+## Pull Request Checklist
+### Testing
+- [ ] Your PR passes all CI checks.
+- [ ] Tests covering the positive and negative (error paths) are included.
+- [ ] Bug fixes contain tests triggering the bug to prevent regressions.
+
+### Code Style and Documentation
+- [ ] The change is not [insubstantial](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#substantial-contributions-only). Typo fixes are not accepted to fight bot spam.
+- [ ] The change obeys the [Code Documentation and Commenting](https://github.com/lightningnetwork/lnd/blob/master/docs/development_guidelines.md#code-documentation-and-commenting) guidelines, and lines wrap at 80.
+- [ ] Commits follow the [Ideal Git Commit Structure](https://github.com/lightningnetwork/lnd/blob/master/docs/development_guidelines.md#ideal-git-commit-structure).
+- [ ] Any new logging statements use an appropriate subsystem and logging level.
+- [ ] Any new lncli commands have appropriate tags in the comments for the rpc in the proto file.
+- [ ] [There is a change description in the release notes](https://github.com/lightningnetwork/lnd/tree/master/docs/release-notes), or `[skip ci]` in the commit message for small changes.
+
+📝 Please see our [Contribution Guidelines](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md) for further guidance.
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 0000000..a9bb6cb
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,76 @@
+name: Docker image build
+
+on:
+ push:
+ tags:
+ - 'v*'
+ schedule:
+ # Every day at midnight (UTC).
+ - cron: '0 0 * * *'
+
+defaults:
+ run:
+ shell: bash
+
+env:
+ DOCKER_REPO: lightninglabs
+ DOCKER_IMAGE: lnd
+
+jobs:
+ ########################
+ # Check release signing keys
+ ########################
+ pgp-key-check:
+ name: Check PGP key expirations
+ runs-on: ubuntu-latest
+ # We don't want to fail the build because of PGP key expirations because
+ # they are only used for release builds and this job failing should catch
+ # the attention of the maintainers.
+ continue-on-error: true
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+
+ - name: Check PGP key expirations
+ run: scripts/check-pgp-expiry.sh
+
+ ########################
+ # Build and push the daily docker image
+ ########################
+ main:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up QEMU
+ uses: lightninglabs/gh-actions/setup-qemu-action@2021.01.25.00
+
+ - name: Set up Docker Buildx
+ uses: lightninglabs/gh-actions/setup-buildx-action@2021.01.25.00
+
+ - name: Login to DockerHub
+ uses: lightninglabs/gh-actions/login-action@2021.01.25.00
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_API_KEY }}
+
+ - name: Set env
+ run: |
+ echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
+ echo "IMAGE_TAG=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
+
+ - name: Set daily tag
+ if: github.event.schedule == '0 0 * * *'
+ run: |
+ echo "RELEASE_VERSION=master" >> $GITHUB_ENV
+ echo "IMAGE_TAG=daily-testing-$(date -u +%Y%m%d),${DOCKER_REPO}/${DOCKER_IMAGE}:daily-testing-only" >> $GITHUB_ENV
+
+ - name: Build and push
+ id: docker_build
+ uses: lightninglabs/gh-actions/build-push-action@2021.01.25.00
+ with:
+ push: true
+ platforms: linux/amd64,linux/arm64
+ tags: "${{ env.DOCKER_REPO }}/${{ env.DOCKER_IMAGE }}:${{ env.IMAGE_TAG }}"
+ build-args: checkout=${{ env.RELEASE_VERSION }}
+
+ - name: Image digest
+ run: echo ${{ steps.docker_build.outputs.digest }}
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..b89985e
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,655 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - "master"
+ pull_request:
+ branches:
+ - "*"
+ merge_group:
+ branches:
+ - "master"
+
+permissions:
+ # Required to manage and delete caches.
+ actions: write
+ # Default permission for checking out code.
+ contents: read
+
+concurrency:
+ # Cancel any previous workflows if they are from a PR or push.
+ group: ${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+defaults:
+ run:
+ shell: bash
+
+env:
+ BITCOIN_VERSION: "29"
+
+ # TRANCHES defines the number of tranches used in the itests.
+ TRANCHES: 16
+
+ # SMALL_TRANCHES defines the number of tranches used in the less stable itest
+ # builds
+ #
+ # TODO(yy): remove this value and use TRANCHES.
+ SMALL_TRANCHES: 8
+
+ # If you change this please also update GO_VERSION in Makefile (then run
+ # `make lint` to see where else it needs to be updated as well).
+ GO_VERSION: 1.24.6
+
+jobs:
+ static-checks:
+ name: Static Checks
+ runs-on: ubuntu-latest
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ # Needed for some checks.
+ fetch-depth: 0
+
+ - name: Clean up runner space
+ uses: ./.github/actions/cleanup-space
+
+ - name: Setup Go ${{ env.GO_VERSION }}
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ use-build-cache: 'no'
+
+ ########################
+ # sample configuration check
+ ########################
+ - name: Check default values in sample-lnd.conf file
+ run: make sample-conf-check
+
+ ########################
+ # Check code and RPC format
+ ########################
+ - name: Check code format
+ run: make fmt-check
+
+ - name: Check go modules tidiness
+ run: make tidy-module-check
+
+ - name: Lint proto files
+ run: make protolint
+
+ ########################
+ # SQLC code gen check
+ ########################
+ - name: Docker image cache
+ uses: satackey/action-docker-layer-caching@v0.0.11
+ # Ignore the failure of a step and avoid terminating the job.
+ continue-on-error: true
+
+ - name: Check SQL models
+ run: make sqlc-check
+
+ ########################
+ # RPC and mobile compilation check
+ ########################
+ - name: Check RPC format
+ run: make rpc-check
+
+ - name: Check JSON/WASM stub compilation
+ run: make rpc-js-compile
+
+ - name: Check mobile RPC bindings
+ run: make mobile-rpc
+
+ - name: Check mobile specific code
+ run: go build --tags="mobile" ./mobile
+
+ ########################
+ # check commits
+ ########################
+ check-commits:
+ if: github.event_name == 'pull_request'
+ name: Check commits
+ runs-on: ubuntu-latest
+ steps:
+ - name: git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Clean up runner space
+ uses: ./.github/actions/cleanup-space
+
+ - name: setup go ${{ env.GO_VERSION }}
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ # Use the same cache from unit test job to save time.
+ key-prefix: unit-test
+
+ - name: fetch and rebase on ${{ github.base_ref }}
+ uses: ./.github/actions/rebase
+
+ - name: check commits
+ run: scripts/check-each-commit.sh upstream/${{ github.base_ref }}
+
+ ########################
+ # lint code
+ ########################
+ lint:
+ name: Lint code
+ runs-on: ubuntu-latest
+ steps:
+ - name: git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Clean up runner space
+ uses: ./.github/actions/cleanup-space
+
+ - name: setup go ${{ env.GO_VERSION }}
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ # Use the same cache from unit test job to save time.
+ key-prefix: unit-test
+
+ - name: lint
+ run: GOGC=50 make lint
+
+ ########################
+ # cross compilation
+ ########################
+ cross-compile:
+ name: Cross compilation
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+ matrix:
+ # Please keep this list in sync with make/release_flags.mk!
+ include:
+ - name: i386
+ sys: freebsd-386 linux-386 windows-386
+ - name: amd64
+ sys: darwin-amd64 freebsd-amd64 linux-amd64 netbsd-amd64 openbsd-amd64 windows-amd64
+ - name: arm
+ sys: darwin-arm64 freebsd-arm linux-armv6 linux-armv7 linux-arm64 windows-arm
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+
+ - name: Clean up runner space
+ uses: ./.github/actions/cleanup-space
+
+ - name: Setup go ${{ env.GO_VERSION }}
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ key-prefix: cross-compile
+ use-build-cache: 'no'
+
+ - name: Build release for all architectures
+ run: make release sys="${{ matrix.sys }}"
+
+ ########################
+ # run unit tests
+ ########################
+ unit-test:
+ name: Run unit tests
+ runs-on: ubuntu-latest
+ strategy:
+ # Allow other tests in the matrix to continue if one fails.
+ fail-fast: false
+ matrix:
+ unit_type:
+ - unit-cover
+ - unit tags="kvdb_etcd"
+ - unit tags="kvdb_postgres"
+ - unit tags="kvdb_sqlite"
+ - unit tags="test_db_sqlite"
+ - unit tags="test_db_postgres"
+ - unit-race
+ - unit-module
+
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Clean up runner space
+ uses: ./.github/actions/cleanup-space
+
+ - name: Fetch and rebase on ${{ github.base_ref }}
+ if: github.event_name == 'pull_request'
+ uses: ./.github/actions/rebase
+
+ - name: Git checkout fuzzing seeds
+ uses: actions/checkout@v4
+ with:
+ repository: lightninglabs/lnd-fuzz
+ path: lnd-fuzz
+
+ - name: Rsync fuzzing seeds
+ run: rsync -a --ignore-existing lnd-fuzz/ ./
+
+ - name: Setup go ${{ env.GO_VERSION }}
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ key-prefix: unit-test
+
+ - name: Install bitcoind
+ run: ./scripts/install_bitcoind.sh $BITCOIN_VERSION
+
+ - name: Run ${{ matrix.unit_type }}
+ run: make ${{ matrix.unit_type }}
+
+ - name: Clean coverage
+ run: grep -Ev '(\.pb\.go|\.pb\.json\.go|\.pb\.gw\.go)' coverage.txt > coverage-norpc.txt
+ if: matrix.unit_type == 'unit-cover'
+
+ - name: Send coverage
+ uses: coverallsapp/github-action@v2
+ if: matrix.unit_type == 'unit-cover'
+ continue-on-error: true
+ with:
+ file: coverage-norpc.txt
+ flag-name: 'unit'
+ format: 'golang'
+ parallel: true
+
+
+ ########################
+ # run integration tests with TRANCHES
+ ########################
+ basic-integration-test:
+ name: Run basic itests
+ runs-on: ubuntu-latest
+ strategy:
+ # Allow other tests in the matrix to continue if one fails.
+ fail-fast: false
+ matrix:
+ include:
+ - name: btcd
+ args: backend=btcd cover=1
+ - name: bitcoind
+ args: backend=bitcoind cover=1
+ - name: bitcoind-notxindex
+ args: backend="bitcoind notxindex"
+ - name: neutrino
+ args: backend=neutrino cover=1
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check for no-itest label
+ id: check-label
+ uses: ./.github/actions/check-label
+ with:
+ label: 'no-itest'
+ skip-message: "Tests auto-passed due to 'no-itest' label"
+
+ - name: Clean up runner space
+ if: steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/cleanup-space
+
+ - name: Fetch and rebase on ${{ github.base_ref }}
+ if: github.event_name == 'pull_request' && steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/rebase
+
+ - name: Setup go ${{ env.GO_VERSION }}
+ if: steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ key-prefix: integration-test
+
+ - name: Install bitcoind
+ if: steps.check-label.outputs.skip != 'true'
+ run: ./scripts/install_bitcoind.sh $BITCOIN_VERSION
+
+ - name: Run ${{ matrix.name }}
+ if: steps.check-label.outputs.skip != 'true'
+ run: make itest-parallel tranches=${{ env.TRANCHES }} ${{ matrix.args }} shuffleseed=${{ github.run_id }}${{ strategy.job-index }}
+
+ - name: Clean coverage
+ run: grep -Ev '(\.pb\.go|\.pb\.json\.go|\.pb\.gw\.go)' coverage.txt > coverage-norpc.txt
+ if: ${{ contains(matrix.args, 'cover=1') && steps.check-label.outputs.skip != 'true' }}
+
+ - name: Send coverage
+ if: ${{ contains(matrix.args, 'cover=1') && steps.check-label.outputs.skip != 'true' }}
+ continue-on-error: true
+ uses: coverallsapp/github-action@v2
+ with:
+ file: coverage-norpc.txt
+ flag-name: 'itest-${{ matrix.name }}'
+ format: 'golang'
+ parallel: true
+
+ - name: Zip log files on failure
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ timeout-minutes: 5 # timeout after 5 minute
+ run: 7z a logs-itest-${{ matrix.name }}.zip itest/**/*.log itest/postgres.log
+
+ - name: Upload log files on failure
+ uses: actions/upload-artifact@v4
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ with:
+ name: logs-itest-${{ matrix.name }}
+ path: logs-itest-${{ matrix.name }}.zip
+ retention-days: 5
+
+ ########################
+ # run integration tests with SMALL_TRANCHES
+ ########################
+ integration-test:
+ name: Run itests
+ runs-on: ubuntu-latest
+ strategy:
+ # Allow other tests in the matrix to continue if one fails.
+ fail-fast: false
+ matrix:
+ include:
+ - name: bitcoind-rpcpolling
+ args: backend="bitcoind rpcpolling"
+ - name: bitcoind-etcd
+ args: backend=bitcoind dbbackend=etcd
+ - name: bitcoind-sqlite
+ args: backend=bitcoind dbbackend=sqlite
+ - name: bitcoind-sqlite-nativesql
+ args: backend=bitcoind dbbackend=sqlite nativesql=true
+ - name: bitcoind-sqlite=nativesql-experiment
+ args: backend=bitcoind dbbackend=sqlite nativesql=true tags=test_native_sql
+ - name: bitcoind-postgres
+ args: backend=bitcoind dbbackend=postgres
+ - name: bitcoind-postgres-nativesql
+ args: backend=bitcoind dbbackend=postgres nativesql=true
+ - name: bitcoind-postgres-nativesql-experiment
+ args: backend=bitcoind dbbackend=postgres nativesql=true tags=test_native_sql
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check for no-itest label
+ id: check-label
+ uses: ./.github/actions/check-label
+ with:
+ label: 'no-itest'
+ skip-message: "Tests auto-passed due to 'no-itest' label"
+
+ - name: Clean up runner space
+ if: steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/cleanup-space
+
+ - name: Fetch and rebase on ${{ github.base_ref }}
+ if: github.event_name == 'pull_request' && steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/rebase
+
+ - name: Setup go ${{ env.GO_VERSION }}
+ if: steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ key-prefix: integration-test
+
+ - name: Install bitcoind
+ if: steps.check-label.outputs.skip != 'true'
+ run: ./scripts/install_bitcoind.sh $BITCOIN_VERSION
+
+ - name: Run ${{ matrix.name }}
+ if: steps.check-label.outputs.skip != 'true'
+ run: make itest-parallel tranches=${{ env.SMALL_TRANCHES }} ${{ matrix.args }} shuffleseed=${{ github.run_id }}${{ strategy.job-index }}
+
+ - name: Clean coverage
+ run: grep -Ev '(\.pb\.go|\.pb\.json\.go|\.pb\.gw\.go)' coverage.txt > coverage-norpc.txt
+ if: ${{ contains(matrix.args, 'cover=1') && steps.check-label.outputs.skip != 'true' }}
+
+ - name: Send coverage
+ if: ${{ contains(matrix.args, 'cover=1') && steps.check-label.outputs.skip != 'true' }}
+ continue-on-error: true
+ uses: coverallsapp/github-action@v2
+ with:
+ file: coverage-norpc.txt
+ flag-name: 'itest-${{ matrix.name }}'
+ format: 'golang'
+ parallel: true
+
+ - name: Zip log files on failure
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ timeout-minutes: 5 # timeout after 5 minute
+ run: 7z a logs-itest-${{ matrix.name }}.zip itest/**/*.log
+
+ - name: Upload log files on failure
+ uses: actions/upload-artifact@v4
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ with:
+ name: logs-itest-${{ matrix.name }}
+ path: logs-itest-${{ matrix.name }}.zip
+ retention-days: 5
+
+
+ ########################
+ # run windows integration test
+ ########################
+ windows-integration-test:
+ name: Run windows itest
+ runs-on: windows-latest
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check for no-itest label
+ id: check-label
+ uses: ./.github/actions/check-label
+ with:
+ label: 'no-itest'
+ skip-message: "Tests auto-passed due to 'no-itest' label"
+
+ - name: Fetch and rebase on ${{ github.base_ref }}
+ if: github.event_name == 'pull_request' && steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/rebase
+
+ - name: Setup go ${{ env.GO_VERSION }}
+ if: steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ key-prefix: integration-test
+
+ - name: Run itest
+ if: steps.check-label.outputs.skip != 'true'
+ run: make itest-parallel tranches=${{ env.SMALL_TRANCHES }} windows=1 shuffleseed=${{ github.run_id }}
+
+ - name: Kill any remaining lnd processes
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ shell: powershell
+ run: taskkill /IM lnd-itest.exe /T /F
+
+ - name: Zip log files on failure
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ timeout-minutes: 5 # timeout after 5 minute
+ run: 7z a logs-itest-windows.zip itest/**/*.log
+
+ - name: Upload log files on failure
+ uses: actions/upload-artifact@v4
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ with:
+ name: logs-itest-windows
+ path: logs-itest-windows.zip
+ retention-days: 5
+
+ ########################
+ # run macOS integration test
+ ########################
+ macos-integration-test:
+ name: Run macOS itest
+ runs-on: macos-14
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Check for no-itest label
+ id: check-label
+ uses: ./.github/actions/check-label
+ with:
+ label: 'no-itest'
+ skip-message: "Tests auto-passed due to 'no-itest' label"
+
+ - name: Fetch and rebase on ${{ github.base_ref }}
+ if: github.event_name == 'pull_request' && steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/rebase
+
+ - name: Setup go ${{ env.GO_VERSION }}
+ if: steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/setup-go
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ key-prefix: integration-test
+
+ - name: Run itest
+ if: steps.check-label.outputs.skip != 'true'
+ run: make itest-parallel tranches=${{ env.SMALL_TRANCHES }} shuffleseed=${{ github.run_id }}
+
+ - name: Zip log files on failure
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ timeout-minutes: 5 # timeout after 5 minute
+ run: 7z a logs-itest-macos.zip itest/**/*.log
+
+ - name: Upload log files on failure
+ uses: actions/upload-artifact@v4
+ if: ${{ failure() && steps.check-label.outputs.skip != 'true' }}
+ with:
+ name: logs-itest-macos
+ path: logs-itest-macos.zip
+ retention-days: 5
+
+ ########################
+ # check pinned dependencies
+ ########################
+ dep-pin:
+ name: Check pinned dependencies
+ runs-on: ubuntu-latest
+ strategy:
+ # Allow other tests in the matrix to continue if one fails.
+ fail-fast: false
+ matrix:
+ pinned_dep:
+ - google.golang.org/grpc v1.59.0
+ - github.com/golang/protobuf v1.5.4
+
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+
+ - name: Clean up runner space
+ uses: ./.github/actions/cleanup-space
+
+ - name: Ensure dependencies at correct version
+ run: if ! grep -q "${{ matrix.pinned_dep }}" go.mod; then echo dependency ${{ matrix.pinned_dep }} should not be altered ; exit 1 ; fi
+
+ ########################
+ # check PR updates release notes
+ ########################
+ milestone-check:
+ name: Check release notes updated
+ runs-on: ubuntu-latest
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+
+ - name: Check for no-changelog label
+ id: check-label
+ uses: ./.github/actions/check-label
+ with:
+ label: 'no-changelog'
+ skip-message: "Changelog check auto-passed due to 'no-changelog' label"
+
+ - name: Clean up runner space
+ if: steps.check-label.outputs.skip != 'true'
+ uses: ./.github/actions/cleanup-space
+
+ - name: Release notes check
+ if: steps.check-label.outputs.skip != 'true'
+ run: scripts/check-release-notes.sh
+
+ ########################
+ # Backwards Compatibility Test
+ ########################
+ backwards-compatibility-test:
+ name: Backwards compatibility test
+ runs-on: ubuntu-latest
+ steps:
+ - name: Git checkout
+ uses: actions/checkout@v4
+
+ - name: 🐳 Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: 🛡️ Backwards compatibility test
+ run: make backwards-compat-test
+
+ #########################################
+ # Auto Cache Cleanup on Pull Requests
+ #########################################
+ auto-cleanup-cache:
+ name: Cache Cleanup
+ runs-on: ubuntu-latest
+
+ # This condition checks for pull requests from authors with write access.
+ if: >-
+ contains('OWNER, MEMBER, COLLABORATOR', github.event.pull_request.author_association)
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Delete caches older than 12 hours
+ continue-on-error: true
+ env:
+ # GITHUB_TOKEN is required for the gh CLI.
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ echo "Finding caches not used in the last 12 hours..."
+
+ # Get the current time and the cutoff time (12 hours ago) in Unix
+ # timestamp format.
+ cutoff_timestamp=$(date -d "12 hours ago" +%s)
+
+ # Use gh and jq to parse caches. Delete any cache last accessed
+ # before the cutoff time.
+ gh cache list --json key,lastAccessedAt | jq -r '.[] |
+ select(.lastAccessedAt != null) | "\(.lastAccessedAt) \(.key)"' |
+ while read -r last_accessed_at key; do
+ last_accessed_timestamp=$(date -d "$last_accessed_at" +%s)
+
+ if (( last_accessed_timestamp < cutoff_timestamp )); then
+ echo "Deleting old cache. Key: $key, Last Used: $last_accessed_at"
+ gh cache delete "$key"
+ fi
+ done
+
+ # Notify about the completion of all coverage collecting jobs.
+ finish:
+ name: Send coverage report
+ if: ${{ !cancelled() }}
+ needs: [unit-test, basic-integration-test]
+ runs-on: ubuntu-latest
+ timeout-minutes: 5
+ steps:
+ - name: Send coverage
+ uses: coverallsapp/github-action@v2
+ continue-on-error: true
+ with:
+ parallel-finished: true
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
new file mode 100644
index 0000000..7c37e43
--- /dev/null
+++ b/.github/workflows/release.yaml
@@ -0,0 +1,153 @@
+name: Release build
+
+on:
+ push:
+ tags:
+ - 'v*'
+
+defaults:
+ run:
+ shell: bash
+
+env:
+ # If you change this please also update GO_VERSION in Makefile (then run
+ # `make lint` to see where else it needs to be updated as well).
+ GO_VERSION: 1.24.6
+
+jobs:
+ ########################
+ # Create release
+ ########################
+ main:
+ name: Release build
+ runs-on: ubuntu-latest
+ steps:
+ - name: git checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: cleanup space
+ run: rm -rf /opt/hostedtoolcache && mkdir -p /opt/hostedtoolcache/go
+
+ - name: setup go ${{ env.GO_VERSION }}
+ uses: actions/setup-go@v5
+ with:
+ go-version: '${{ env.GO_VERSION }}'
+ cache: 'false'
+
+ - name: Set env
+ run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
+
+ - name: build release for all architectures
+ run: SKIP_VERSION_CHECK=1 make release tag=${{ env.RELEASE_VERSION }}
+
+ - name: Create Release
+ uses: lightninglabs/gh-actions/action-gh-release@c7149b6a7818d1c39b36b69e727569897b6f2c5a
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ name: lnd ${{ env.RELEASE_VERSION }}
+ draft: true
+ prerelease: false
+ files: lnd-${{ env.RELEASE_VERSION }}/*
+ body: |
+ # Database Migrations
+ TODO
+
+ # Verifying the Release
+
+ In order to verify the release, you'll need to have `gpg` or `gpg2` installed on your system. Once you've obtained a copy (and hopefully verified that as well), you'll first need to import the keys that have signed this release if you haven't done so already:
+
+ ```
+ curl https://raw.githubusercontent.com/lightningnetwork/lnd/master/scripts/keys/roasbeef.asc | gpg --import
+ ```
+
+ Once you have the required PGP keys, you can verify the release (assuming `manifest-roasbeef-${{ env.RELEASE_VERSION }}.sig` and `manifest-${{ env.RELEASE_VERSION }}.txt` are in the current directory) with:
+
+ ```
+ gpg --verify manifest-roasbeef-${{ env.RELEASE_VERSION }}.sig manifest-${{ env.RELEASE_VERSION }}.txt
+ ```
+
+ You should see the following if the verification was successful:
+
+ ```
+ gpg: Signature made Wed Sep 30 17:35:20 2020 PDT
+ gpg: using RSA key 60A1FA7DA5BFF08BDCBBE7903BBD59E99B280306
+ gpg: Good signature from "Olaoluwa Osuntokun <laolu32@gmail.com>" [ultimate]
+ ```
+
+ That will verify the signature of the manifest file, which ensures integrity and authenticity of the archive you've downloaded locally containing the binaries. Next, depending on your operating system, you should then re-compute the `sha256` hash of the archive with `shasum -a 256 <filename>`, compare it with the corresponding one in the manifest file, and ensure they match *exactly*.
+
+ ## Verifying the Release Timestamp
+
+ From this new version onwards, in addition time-stamping the _git tag_ with [OpenTimestamps](https://opentimestamps.org/), we'll also now timestamp the manifest file along with its signature. Two new files are now included along with the rest of our release artifacts: ` manifest-roasbeef-${{ env.RELEASE_VERSION }}.txt.asc.ots`.
+
+ Assuming you have the opentimestamps client installed locally, the timestamps can be verified with the following commands:
+ ```
+ ots verify manifest-roasbeef-${{ env.RELEASE_VERSION }}.sig.ots -f manifest-roasbeef-${{ env.RELEASE_VERSION }}.sig
+ ```
+
+ Alternatively, [the OpenTimestamps website](https://opentimestamps.org/) can be used to verify timestamps if one doesn't have a `bitcoind` instance accessible locally.
+
+ These timestamps should give users confidence in the integrity of this release even after the key that signed the release expires.
+
+ ## Verifying the Release Binaries
+
+ Our release binaries are fully reproducible. Third parties are able to verify that the release binaries were produced properly without having to trust the release manager(s). See our [reproducible builds guide](https://github.com/lightningnetwork/lnd/blob/master/docs/release.md) for how this can be achieved.
+ The release binaries are compiled with `go${{ env.GO_VERSION }}`, which is required by verifiers to arrive at the same ones.
+ They include the following build tags: `autopilotrpc`, `signrpc`, `walletrpc`, `chainrpc`, `invoicesrpc`, `neutrinorpc`, `routerrpc`, `watchtowerrpc`, `monitoring`, `peersrpc`, `kvdb_postrgres`, `kvdb_etcd` and `kvdb_sqlite`. Note that these are already included in the release script, so they do not need to be provided.
+
+ The `make release` command can be used to ensure one rebuilds with all the same flags used for the release. If one wishes to build for only a single platform, then `make release sys=<OS-ARCH> tag=<tag>` can be used.
+
+ Finally, you can also verify the _tag_ itself with the following command:
+
+ ```
+ $ git verify-tag ${{ env.RELEASE_VERSION }}
+ gpg: Signature made Tue Sep 15 18:55:00 2020 PDT
+ gpg: using RSA key 60A1FA7DA5BFF08BDCBBE7903BBD59E99B280306
+ gpg: Good signature from "Olaoluwa Osuntokun <laolu32@gmail.com>" [ultimate]
+ ```
+
+ ## Verifying the Docker Images
+
+ To verify the `lnd` and `lncli` binaries inside the docker images against the signed, reproducible release binaries, there is a verification script in the image that can be called (before starting the container for example):
+
+ ```shell
+ $ docker run --rm --entrypoint="" lightninglabs/lnd:${{ env.RELEASE_VERSION }} /verify-install.sh ${{ env.RELEASE_VERSION }}
+ $ OK=$?
+ $ if [ "$OK" -ne "0" ]; then echo "Verification failed!"; exit 1; done
+ $ docker run lightninglabs/lnd [command-line options]
+ ```
+
+ # Building the Contained Release
+
+ Users are able to rebuild the target release themselves without having to fetch any of the dependencies. In order to do so, assuming
+ that `vendor.tar.gz` and `lnd-source-${{ env.RELEASE_VERSION }}.tar.gz` are in the current directory, follow these steps:
+
+ ```
+ tar -xvzf lnd-source-${{ env.RELEASE_VERSION }}.tar.gz
+ mv vendor.tar.gz lnd-source/
+ cd lnd-source
+ tar -xvzf vendor.tar.gz
+ go install -v -mod=vendor -ldflags "-X github.com/lightningnetwork/lnd/build.Commit=${{ env.RELEASE_VERSION }}" ./cmd/lnd
+ go install -v -mod=vendor -ldflags "-X github.com/lightningnetwork/lnd/build.Commit=${{ env.RELEASE_VERSION }}" ./cmd/lncli
+ ```
+
+ The `-mod=vendor` flag tells the `go build` command that it doesn't need to fetch the dependencies, and instead, they're all enclosed in the local vendor directory.
+
+ Additionally, it's now possible to use the [enclosed `release.sh` script to bundle a release for a _specific_ system like so](https://github.com/lightningnetwork/lnd/pull/2191):
+
+ ```
+ make release sys="linux-arm64 darwin-amd64"
+ ```
+
+ ⚡️⚡️⚡️ OK, now to the rest of the release notes! ⚡️⚡️⚡️
+
+ # Release Notes
+
+ TODO
+
+ # Contributors (Alphabetical Order)
+
+ TODO
diff --git a/.github/workflows/stats.yml b/.github/workflows/stats.yml
new file mode 100644
index 0000000..62b79f9
--- /dev/null
+++ b/.github/workflows/stats.yml
@@ -0,0 +1,27 @@
+name: Pull Request Stats
+
+on:
+ pull_request:
+ types: [opened]
+
+permissions:
+ # Required to post stats as comments.
+ actions: write
+ # Default permission for checking out code.
+ contents: read
+
+jobs:
+ stats:
+ runs-on: ubuntu-latest
+
+ # Check if the PR is from the base repo (not a fork). Only the
+ # collaborators have the permission to create a side branch from the base
+ # repo, so this implicitly restricts who can run this job.
+ if: github.event.pull_request.head.repo.fork == false
+
+ steps:
+ - name: Run pull request stats
+ uses: flowwer-dev/pull-request-stats@v2.11.0
+ with:
+ period: 30 # 30 days of review stats
+ charts: true
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..11c67fe
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,87 @@
+# ---> Go
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
+
+/lnd
+/lnd-debug
+/lncli
+/lncli-debug
+/lnd-itest
+/lncli-itest
+
+# Integration test log files
+itest/*.log
+itest/.backendlogs
+itest/.minerlogs
+itest/lnd-itest
+itest/btcd-itest
+itest/.logs-*
+itest/cover
+
+cmd/cmd
+*.key
+*.hex
+
+# Ignore the custom linter binary if it is built.
+custom-gcl
+
+cmd/lncli/lncli
+
+# Files from mobile build.
+mobile/build
+mobile/*_generated.go
+
+# vim
+*.swp
+
+*.hex
+*.db
+*.bin
+
+vendor
+*.idea
+*.iml
+profile.cov
+profile.tmp
+
+.DS_Store
+
+.vscode
+*.code-workspace
+
+# Coverage test
+coverage.txt
+
+# Visual Studio cache/options directory
+.vs/
+
+# Release build directory (to avoid build.vcs.modified Golang build tag to be
+# set to true by having untracked files in the working directory).
+/lnd-*/
+
+.aider*
+
+# All test data generated from rapid.
+*/testdata
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..73d7ad4
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,298 @@
+run:
+ # If you change this please also update GO_VERSION in Makefile (then run
+ # `make lint` to see where else it needs to be updated as well).
+ go: "1.24.6"
+
+ # Abort after 10 minutes.
+ timeout: 10m
+
+ build-tags:
+ - autopilotrpc
+ - chainrpc
+ - dev
+ - invoicesrpc
+ - neutrinorpc
+ - peersrpc
+ - signrpc
+ - walletrpc
+ - watchtowerrpc
+ - kvdb_etcd
+ - kvdb_postgres
+ - kvdb_sqlite
+ - integration
+
+linters-settings:
+ custom:
+ ll:
+ type: "module"
+ description: "Custom lll linter with 'S' log line exclusion."
+ settings:
+ # Max line length, lines longer will be reported.
+ line-length: 80
+ # Tab width in spaces.
+ tab-width: 8
+ # The regex that we will use to detect the start of an `S` log line.
+ log-regex: "^\\s*.*(L|l)og\\.(Info|Debug|Trace|Warn|Error|Critical)S\\("
+
+ errorlint:
+ # Check for incorrect fmt.Errorf error wrapping.
+ errorf: true
+
+ gofmt:
+ # simplify code: gofmt with `-s` option, true by default
+ simplify: true
+
+ tagliatelle:
+ case:
+ rules:
+ json: snake
+
+ whitespace:
+ multi-func: true
+ multi-if: true
+
+ gosec:
+ excludes:
+ - G402 # Look for bad TLS connection settings.
+ - G306 # Poor file permissions used when writing to a new file.
+ - G601 # Implicit memory aliasing in for loop.
+ - G115 # Integer overflow in conversion.
+
+ staticcheck:
+ checks: ["-SA1019"]
+
+ funlen:
+ # Checks the number of lines in a function.
+ # If lower than 0, disable the check.
+ lines: 200
+ # Checks the number of statements in a function.
+ statements: 80
+
+ dupl:
+ # Tokens count to trigger issue.
+ threshold: 200
+
+ nestif:
+ # Minimal complexity of if statements to report.
+ min-complexity: 10
+
+ nlreturn:
+ # Size of the block (including return statement that is still "OK")
+ # so no return split required.
+ block-size: 3
+
+ gomnd:
+ # List of numbers to exclude from analysis.
+ # The numbers should be written as string.
+ # Values always ignored: "1", "1.0", "0" and "0.0"
+ # Default: []
+ ignored-numbers:
+ - '0666'
+ - '0755'
+
+ # List of function patterns to exclude from analysis.
+ # Values always ignored: `time.Date`
+ # Default: []
+ ignored-functions:
+ - 'math.*'
+ - 'strconv.ParseInt'
+ - 'errors.Wrap'
+
+ gomoddirectives:
+ replace-local: true
+ replace-allow-list:
+ # See go.mod for the explanation why these are needed.
+ - github.com/ulikunitz/xz
+ - github.com/gogo/protobuf
+ - google.golang.org/protobuf
+ - github.com/lightningnetwork/lnd/sqldb
+
+
+linters:
+ enable-all: true
+ disable:
+ # We instead use our own custom line length linter called `ll` since
+ # then we can ignore log lines.
+ - lll
+
+ # Global variables are used in many places throughout the code base.
+ - gochecknoglobals
+
+ # We want to allow short variable names.
+ - varnamelen
+
+ # We want to allow TODOs.
+ - godox
+
+ # Instances of table driven tests that don't pre-allocate shouldn't trigger
+ # the linter.
+ - prealloc
+
+ # Init functions are used by loggers throughout the codebase.
+ - gochecknoinits
+
+ # Deprecated linters. See https://golangci-lint.run/usage/linters/.
+ - bodyclose
+ - contextcheck
+ - nilerr
+ - noctx
+ - rowserrcheck
+ - sqlclosecheck
+ - tparallel
+ - unparam
+ - wastedassign
+
+ # Disable gofumpt as it has weird behavior regarding formatting multiple
+ # lines for a function which is in conflict with our contribution
+ # guidelines. See https://github.com/mvdan/gofumpt/issues/235.
+ - gofumpt
+
+ # Disable whitespace linter as it has conflict rules against our
+ # contribution guidelines.
+ - wsl
+
+ # Allow using default empty values.
+ - exhaustruct
+
+ # Allow exiting case select faster by putting everything in default.
+ - exhaustive
+
+ # Allow tests to be put in the same package.
+ - testpackage
+
+ # Don't run the cognitive related linters.
+ - gocognit
+ - gocyclo
+ - maintidx
+ - cyclop
+
+ # Allow customized interfaces to be returned from functions.
+ - ireturn
+
+ # Disable too many blank identifiers check. We won't be able to run this
+ # unless a large refactor has been applied to old code.
+ - dogsled
+
+ # We don't wrap errors.
+ - wrapcheck
+
+ # Allow dynamic errors.
+ - err113
+
+ # We use ErrXXX instead.
+ - errname
+
+ # Disable nil check to allow returning multiple nil values.
+ - nilnil
+
+ # We often split tests into separate test functions. If we are forced to
+ # call t.Helper() within those functions, we lose the information where
+ # exactly a test failed in the generated failure stack trace.
+ - thelper
+
+ # The linter is too aggressive and doesn't add much value since reviewers
+ # will also catch magic numbers that make sense to extract.
+ - mnd
+
+ # Some of the tests cannot be parallelized. On the other hand, we don't
+ # gain much performance with this check so we disable it for now until
+ # unit tests become our CI bottleneck.
+ - paralleltest
+
+ # New linters that we haven't had time to address yet.
+ - testifylint
+ - perfsprint
+ - inamedparam
+ - copyloopvar
+ - tagalign
+ - protogetter
+ - revive
+ - depguard
+ - gosmopolitan
+ - intrange
+ - goconst
+
+ # Deprecated linters that have been replaced by newer ones.
+ - tenv
+
+issues:
+ # Only show newly introduced problems.
+ new-from-rev: 03eab4db64540aa5f789c617793e4459f4ba9e78
+
+ # Skip autogenerated files for mobile and gRPC as well as copied code for
+ # internal use.
+ skip-files:
+ - "mobile\\/.*generated\\.go"
+ - "\\.pb\\.go$"
+ - "\\.pb\\.gw\\.go$"
+ - "internal\\/musig2v040"
+
+ skip-dirs:
+ - channeldb/migration_01_to_11
+ - channeldb/migration/lnwire21
+
+ exclude-rules:
+ # Exclude gosec from running for tests so that tests with weak randomness
+ # (math/rand) will pass the linter.
+ - path: _test\.go
+ linters:
+ - gosec
+ - funlen
+ - revive
+ # Allow duplications in tests so it's easier to follow a single unit
+ # test.
+ - dupl
+
+ - path: mock*
+ linters:
+ - revive
+ # forcetypeassert is skipped for the mock because the test would fail
+ # if the returned value doesn't match the type, so there's no need to
+ # check the convert.
+ - forcetypeassert
+
+ - path: test*
+ linters:
+ - gosec
+ - funlen
+
+ # Allow duplicated code and fmt.Printf() in DB migrations.
+ - path: channeldb/migration*
+ linters:
+ - dupl
+ - forbidigo
+ - godot
+
+ # Allow duplicated code and fmt.Printf() in DB migration tests.
+ - path: channeldb/migtest
+ linters:
+ - dupl
+ - forbidigo
+ - godot
+
+ # Allow fmt.Printf() in commands.
+ - path: cmd/commands/*
+ linters:
+ - forbidigo
+
+ # Allow fmt.Printf() in config parsing.
+ - path: config\.go
+ linters:
+ - forbidigo
+ - path: lnd\.go
+ linters:
+ - forbidigo
+
+ - path: lnmock/*
+ linters:
+ # forcetypeassert is skipped for the mock because the test would fail
+ # if the returned value doesn't match the type, so there's no need to
+ # check the convert.
+ - forcetypeassert
+
+ - path: mock*
+ linters:
+ # forcetypeassert is skipped for the mock because the test would fail
+ # if the returned value doesn't match the type, so there's no need to
+ # check the convert.
+ - forcetypeassert
diff --git a/.protolint.yaml b/.protolint.yaml
new file mode 100644
index 0000000..269d74a
--- /dev/null
+++ b/.protolint.yaml
@@ -0,0 +1,70 @@
+# The example configuration file for the protolint is located here:
+# https://github.com/yoheimuta/protolint/blob/master/_example/config/.protolint.yaml
+---
+# Lint directives.
+lint:
+ # Linter rules.
+ # Run `protolint list` to see all available rules.
+ rules:
+ # Determines whether or not to include the default set of linters.
+ no_default: true
+
+ # Set the default to all linters. This option works the other way around as no_default does.
+ # If you want to enable this option, delete the comment out below and no_default.
+ # all_default: true.
+
+ # The specific linters to add.
+ add:
+ - MESSAGE_NAMES_UPPER_CAMEL_CASE
+ - MAX_LINE_LENGTH
+ - INDENT
+ - FILE_NAMES_LOWER_SNAKE_CASE
+ - IMPORTS_SORTED
+ - PACKAGE_NAME_LOWER_CASE
+ - ORDER
+ - SERVICES_HAVE_COMMENT
+ - RPCS_HAVE_COMMENT
+ - PROTO3_FIELDS_AVOID_REQUIRED
+ - PROTO3_GROUPS_AVOID
+ - SYNTAX_CONSISTENT
+ - RPC_NAMES_CASE
+ - QUOTE_CONSISTENT
+
+ # Linter rules option.
+ rules_option:
+ # MAX_LINE_LENGTH rule option.
+ max_line_length:
+ # Enforces a maximum line length.
+ max_chars: 80
+ # Specifies the character count for tab characters.
+ tab_chars: 2
+
+ # INDENT rule option.
+ indent:
+ # Available styles are 4(4-spaces), 2(2-spaces) or tab.
+ style: 4
+ # Specifies if it should stop considering and inserting new lines at the appropriate positions.
+ # when the inner elements are on the same line. Default is false.
+ not_insert_newline: true
+
+ # QUOTE_CONSISTENT rule option.
+ quote_consistent:
+ # Available quote are "double" or "single".
+ quote: double
+
+ # ENUM_FIELD_NAMES_ZERO_VALUE_END_WITH rule option.
+ enum_field_names_zero_value_end_with:
+ suffix: INVALID
+
+ # SERVICE_NAMES_END_WITH rule option.
+ service_names_end_with:
+ text: Service
+
+ # REPEATED_FIELD_NAMES_PLURALIZED rule option.
+ ## The spec for each rules follows the implementation of https://github.com/gertd/go-pluralize.
+ ## Plus, you can refer to this rule's test code.
+ repeated_field_names_pluralized:
+ uncountable_rules:
+ - paper
+ irregular_rules:
+ Irregular: Regular
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..84e81e0
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "editor.tabSize": 8,
+ "editor.rulers": [
+ 80
+ ],
+ "go.buildTags": "autopilotrpc chainrpc dev invoicesrpc neutrinorpc peersrpc signrpc walletrpc watchtowerrpc"
+}
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..5540ddb
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,55 @@
+# If you change this please also update GO_VERSION in Makefile (then run
+# `make lint` to see where else it needs to be updated as well).
+FROM golang:1.24.6-alpine as builder
+
+# Force Go to use the cgo based DNS resolver. This is required to ensure DNS
+# queries required to connect to linked containers succeed.
+ENV GODEBUG netdns=cgo
+
+# Pass a tag, branch or a commit using build-arg. This allows a docker
+# image to be built from a specified Git state. The default image
+# will use the Git tip of master by default.
+ARG checkout="master"
+ARG git_url="https://github.com/lightningnetwork/lnd"
+
+# Install dependencies and build the binaries.
+RUN apk add --no-cache --update alpine-sdk \
+ git \
+ make \
+ gcc \
+&& git clone $git_url /go/src/github.com/lightningnetwork/lnd \
+&& cd /go/src/github.com/lightningnetwork/lnd \
+&& git checkout $checkout \
+&& make release-install
+
+# Start a new, final image.
+FROM alpine as final
+
+# Define a root volume for data persistence.
+VOLUME /root/.lnd
+
+# Add utilities for quality of life and SSL-related reasons. We also require
+# curl and gpg for the signature verification script.
+RUN apk --no-cache add \
+ bash \
+ jq \
+ ca-certificates \
+ gnupg \
+ curl
+
+# Copy the binaries from the builder image.
+COPY --from=builder /go/bin/lncli /bin/
+COPY --from=builder /go/bin/lnd /bin/
+COPY --from=builder /go/src/github.com/lightningnetwork/lnd/scripts/verify-install.sh /
+COPY --from=builder /go/src/github.com/lightningnetwork/lnd/scripts/keys/* /keys/
+
+# Store the SHA256 hash of the binaries that were just produced for later
+# verification.
+RUN sha256sum /bin/lnd /bin/lncli > /shasums.txt \
+ && cat /shasums.txt
+
+# Expose lnd ports (p2p, rpc).
+EXPOSE 9735 10009
+
+# Specify the start command and entrypoint as the lnd daemon.
+ENTRYPOINT ["lnd"]
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..cfab3da
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (C) 2015-2022 Lightning Labs and The Lightning Network Developers
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..6a883aa
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,496 @@
+PKG := github.com/lightningnetwork/lnd
+MOBILE_PKG := $(PKG)/mobile
+TOOLS_DIR := tools
+
+GOCC ?= go
+PREFIX ?= /usr/local
+
+BTCD_PKG := github.com/btcsuite/btcd
+GOIMPORTS_PKG := github.com/rinchsan/gosimports/cmd/gosimports
+
+GO_BIN := ${GOPATH}/bin
+BTCD_BIN := $(GO_BIN)/btcd
+GOIMPORTS_BIN := $(GO_BIN)/gosimports
+GOMOBILE_BIN := $(GO_BIN)/gomobile
+
+MOBILE_BUILD_DIR :=${GOPATH}/src/$(MOBILE_PKG)/build
+IOS_BUILD_DIR := $(MOBILE_BUILD_DIR)/ios
+IOS_BUILD := $(IOS_BUILD_DIR)/Lndmobile.xcframework
+ANDROID_BUILD_DIR := $(MOBILE_BUILD_DIR)/android
+ANDROID_BUILD := $(ANDROID_BUILD_DIR)/Lndmobile.aar
+
+COMMIT := $(shell git describe --tags --dirty)
+
+# Determine the minor version of the active Go installation.
+ACTIVE_GO_VERSION := $(shell $(GOCC) version | sed -nre 's/^[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p')
+ACTIVE_GO_VERSION_MINOR := $(shell echo $(ACTIVE_GO_VERSION) | cut -d. -f2)
+
+# GO_VERSION is the Go version used for the release build, docker files, and
+# GitHub Actions. This is the reference version for the project. All other Go
+# versions are checked against this version.
+GO_VERSION = 1.24.6
+
+GOBUILD := $(GOCC) build -v
+GOINSTALL := $(GOCC) install -v
+GOTEST := $(GOCC) test
+
+GOFILES_NOVENDOR = $(shell find . -type f -name '*.go' -not -path "./vendor/*" -not -name "*pb.go" -not -name "*pb.gw.go" -not -name "*.pb.json.go")
+
+RM := rm -f
+CP := cp
+MAKE := make
+XARGS := xargs -L 1
+
+include make/testing_flags.mk
+include make/release_flags.mk
+include make/fuzz_flags.mk
+
+DEV_TAGS := $(if ${tags},$(DEV_TAGS) ${tags},$(DEV_TAGS))
+
+# We only return the part inside the double quote here to avoid escape issues
+# when calling the external release script. The second parameter can be used to
+# add additional ldflags if needed (currently only used for the release).
+make_ldflags = $(1) -X $(PKG)/build.Commit=$(COMMIT)
+
+DEV_GCFLAGS := -gcflags "all=-N -l"
+DEV_LDFLAGS := -ldflags "$(call make_ldflags)"
+# For the release, we want to remove the symbol table and debug information (-s)
+# and omit the DWARF symbol table (-w). Also we clear the build ID.
+RELEASE_LDFLAGS := $(call make_ldflags, -s -w -buildid=)
+
+# Linting uses a lot of memory, so keep it under control by limiting the number
+# of workers if requested.
+ifneq ($(workers),)
+LINT_WORKERS = --concurrency=$(workers)
+endif
+
+DOCKER_TOOLS = docker run \
+ --rm \
+ -v $(shell bash -c "$(GOCC) env GOCACHE || (mkdir -p /tmp/go-cache; echo /tmp/go-cache)"):/tmp/build/.cache \
+ -v $(shell bash -c "$(GOCC) env GOMODCACHE || (mkdir -p /tmp/go-modcache; echo /tmp/go-modcache)"):/tmp/build/.modcache \
+ -v $(shell bash -c "mkdir -p /tmp/go-lint-cache; echo /tmp/go-lint-cache"):/root/.cache/golangci-lint \
+ -v $$(pwd):/build lnd-tools
+
+GREEN := "\\033[0;32m"
+NC := "\\033[0m"
+define print
+ echo $(GREEN)$1$(NC)
+endef
+
+default: scratch
+
+all: scratch check install
+
+# ============
+# DEPENDENCIES
+# ============
+$(BTCD_BIN):
+ @$(call print, "Installing btcd.")
+ cd $(TOOLS_DIR); $(GOCC) install -trimpath $(BTCD_PKG)
+
+$(GOIMPORTS_BIN):
+ @$(call print, "Installing goimports.")
+ cd $(TOOLS_DIR); $(GOCC) install -trimpath $(GOIMPORTS_PKG)
+
+# ============
+# INSTALLATION
+# ============
+
+#? build: Build lnd and lncli binaries, place them in project directory
+build:
+ @$(call print, "Building debug lnd and lncli.")
+ $(GOBUILD) -tags="$(DEV_TAGS)" -o lnd-debug $(DEV_GCFLAGS) $(DEV_LDFLAGS) $(PKG)/cmd/lnd
+ $(GOBUILD) -tags="$(DEV_TAGS)" -o lncli-debug $(DEV_GCFLAGS) $(DEV_LDFLAGS) $(PKG)/cmd/lncli
+
+#? build-itest: Build integration test binaries, place them in itest directory
+build-itest:
+ @$(call print, "Building itest btcd and lnd.")
+ CGO_ENABLED=0 $(GOBUILD) -tags="integration" -o itest/btcd-itest$(EXEC_SUFFIX) $(DEV_LDFLAGS) $(BTCD_PKG)
+ CGO_ENABLED=0 $(GOBUILD) -tags="$(ITEST_TAGS)" $(ITEST_COVERAGE) -o itest/lnd-itest$(EXEC_SUFFIX) $(DEV_LDFLAGS) $(PKG)/cmd/lnd
+
+ @$(call print, "Building itest binary for ${backend} backend.")
+ CGO_ENABLED=0 $(GOTEST) -v ./itest -tags="$(DEV_TAGS) $(RPC_TAGS) integration $(backend)" -c -o itest/itest.test$(EXEC_SUFFIX)
+
+#? build-itest-race: Build integration test binaries in race detector mode, place them in itest directory
+build-itest-race:
+ @$(call print, "Building itest btcd and lnd with race detector.")
+ CGO_ENABLED=0 $(GOBUILD) -tags="integration" -o itest/btcd-itest$(EXEC_SUFFIX) $(DEV_LDFLAGS) $(BTCD_PKG)
+ CGO_ENABLED=1 $(GOBUILD) -race -tags="$(ITEST_TAGS)" -o itest/lnd-itest$(EXEC_SUFFIX) $(DEV_LDFLAGS) $(PKG)/cmd/lnd
+
+ @$(call print, "Building itest binary for ${backend} backend.")
+ CGO_ENABLED=0 $(GOTEST) -v ./itest -tags="$(DEV_TAGS) $(RPC_TAGS) integration $(backend)" -c -o itest/itest.test$(EXEC_SUFFIX)
+
+#? install-binaries: Build and install lnd and lncli binaries, place them in $GOPATH/bin
+install-binaries:
+ @$(call print, "Installing lnd and lncli.")
+ $(GOINSTALL) -tags="${tags}" -ldflags="$(RELEASE_LDFLAGS)" $(PKG)/cmd/lnd
+ $(GOINSTALL) -tags="${tags}" -ldflags="$(RELEASE_LDFLAGS)" $(PKG)/cmd/lncli
+
+#? manpages: generate and install man pages
+manpages:
+ @$(call print, "Generating man pages lncli.1 and lnd.1.")
+ ./scripts/gen_man_pages.sh $(DESTDIR) $(PREFIX)
+
+#? install: Build and install lnd and lncli binaries and place them in $GOPATH/bin.
+install: install-binaries
+
+#? install-all: Performs all the same tasks as the install command along with generating and installing the man pages for the lnd and lncli binaries. This command is useful in an environment where a user has root access and so has write access to the man page directory.
+install-all: install manpages
+
+#? release-install: Build and install lnd and lncli release binaries, place them in $GOPATH/bin
+release-install:
+ @$(call print, "Installing release lnd and lncli.")
+ env CGO_ENABLED=0 $(GOINSTALL) -v -trimpath -ldflags="$(RELEASE_LDFLAGS)" -tags="$(RELEASE_TAGS)" $(PKG)/cmd/lnd
+ env CGO_ENABLED=0 $(GOINSTALL) -v -trimpath -ldflags="$(RELEASE_LDFLAGS)" -tags="$(RELEASE_TAGS)" $(PKG)/cmd/lncli
+
+#? cross-release-install: Build lnd and lncli release binaries for single/all supported platforms to /tmp (useful for checking cross compilation or priming release build cache).
+cross-release-install:
+ @$(call print, "Cross compiling release lnd and lncli.")
+ for sys in $(BUILD_SYSTEM); do \
+ echo "Building lnd and lncli for $$sys"; \
+ export CGO_ENABLED=0 GOOS=$$(echo $$sys | cut -d- -f1) GOARCH=$$(echo $$sys | cut -d- -f2); \
+ if [ "$$GOARCH" = "armv6" ]; then \
+ export GOARCH=arm; GOARM=6; \
+ elif [ "$$GOARCH" = "armv7" ]; then \
+ export GOARCH=arm; GOARM=7; \
+ fi; \
+ $(GOBUILD) -trimpath -ldflags="$(RELEASE_LDFLAGS)" -tags="$(RELEASE_TAGS)" -o /tmp/lnd-$$sys $(PKG)/cmd/lnd; \
+ $(GOBUILD) -trimpath -ldflags="$(RELEASE_LDFLAGS)" -tags="$(RELEASE_TAGS)" -o /tmp/lncli-$$sys $(PKG)/cmd/lncli; \
+ echo; \
+ done
+
+#? release: Build the full set of reproducible release binaries for all supported platforms. Make sure the generated mobile RPC stubs don't influence our vendor package by removing them first in the clean-mobile target.
+release: clean-mobile
+ @$(call print, "Releasing lnd and lncli binaries.")
+ $(VERSION_CHECK)
+ ./scripts/release.sh build-release "$(VERSION_TAG)" "$(BUILD_SYSTEM)" "$(RELEASE_TAGS)" "$(RELEASE_LDFLAGS)" "$(GO_VERSION)"
+
+#? docker-release: Same as release but within a docker container to support reproducible builds on BSD/MacOS platforms
+docker-release:
+ @$(call print, "Building release helper docker image.")
+ if [ "$(tag)" = "" ]; then echo "Must specify tag=<commit_or_tag>!"; exit 1; fi
+
+ docker build -t lnd-release-helper -f make/builder.Dockerfile make/
+
+ # Run the actual compilation inside the docker image. We pass in all flags
+ # that we might want to overwrite in manual tests.
+ $(DOCKER_RELEASE_HELPER) make release tag="$(tag)" sys="$(sys)" COMMIT="$(COMMIT)"
+
+docker-tools:
+ @$(call print, "Building tools docker image.")
+ docker build -q -t lnd-tools $(TOOLS_DIR)
+
+scratch: build
+
+
+# =======
+# TESTING
+# =======
+
+#? check: Run unit and integration tests
+check: unit itest
+
+db-instance:
+ifeq ($(dbbackend),postgres)
+ # Remove a previous postgres instance if it exists.
+ docker rm lnd-postgres --force || echo "Starting new postgres container"
+
+ # Start a fresh postgres instance. Allow a maximum of 500 connections so
+ # that multiple lnd instances with a maximum number of connections of 20
+ # each can run concurrently. Note that many of the settings here are
+ # specifically for integration testing and are not fit for running
+ # production nodes. The increase in max connections ensures that there
+ # are enough entries allocated for the RWConflictPool to allow multiple
+ # conflicting transactions to track serialization conflicts. The
+ # increase in predicate locks and locks per transaction is to allow the
+ # queries to lock individual rows instead of entire tables, helping
+ # reduce serialization conflicts. Disabling sequential scan for small
+ # tables also helps prevent serialization conflicts by ensuring lookups
+ # lock only relevant rows in the index rather than the entire table.
+ docker run --name lnd-postgres -e POSTGRES_PASSWORD=postgres -p 6432:5432 -d postgres:13-alpine -N 1500 -c max_pred_locks_per_transaction=1024 -c max_locks_per_transaction=128 -c enable_seqscan=off
+ docker logs -f lnd-postgres >itest/postgres.log 2>&1 &
+
+ # Wait for the instance to be started.
+ sleep $(POSTGRES_START_DELAY)
+endif
+
+clean-itest-logs:
+ rm -rf itest/*.log itest/.logs-*
+
+#? itest-only: Only run integration tests without re-building binaries
+itest-only: clean-itest-logs db-instance
+ @$(call print, "Running integration tests with ${backend} backend.")
+ date
+ EXEC_SUFFIX=$(EXEC_SUFFIX) scripts/itest_part.sh 0 1 $(SHUFFLE_SEED) $(TEST_FLAGS) $(ITEST_FLAGS) -test.v
+ $(COLLECT_ITEST_COVERAGE)
+
+#? itest: Build and run integration tests
+itest: build-itest itest-only
+
+#? itest-race: Build and run integration tests in race detector mode
+itest-race: build-itest-race itest-only
+
+#? itest-parallel: Build and run integration tests in parallel mode, running up to ITEST_PARALLELISM test tranches in parallel (default 4)
+itest-parallel: clean-itest-logs build-itest db-instance
+ @$(call print, "Running tests")
+ date
+ EXEC_SUFFIX=$(EXEC_SUFFIX) scripts/itest_parallel.sh $(ITEST_PARALLELISM) $(NUM_ITEST_TRANCHES) $(SHUFFLE_SEED) $(TEST_FLAGS) $(ITEST_FLAGS)
+ $(COLLECT_ITEST_COVERAGE)
+
+#? itest-clean: Kill all running itest processes
+itest-clean:
+ @$(call print, "Cleaning old itest processes")
+ killall lnd-itest || echo "no running lnd-itest process found";
+
+#? unit: Run unit tests
+unit: $(BTCD_BIN)
+ @$(call print, "Running unit tests.")
+ $(UNIT)
+
+#? unit-module: Run unit tests of all submodules
+unit-module:
+ @$(call print, "Running submodule unit tests.")
+ scripts/unit_test_modules.sh
+
+#? unit-debug: Run unit tests with debug log output enabled
+unit-debug: $(BTCD_BIN)
+ @$(call print, "Running debug unit tests.")
+ $(UNIT_DEBUG)
+
+#? unit-cover: Run unit tests in coverage mode
+unit-cover: $(BTCD_BIN)
+ @$(call print, "Running unit coverage tests.")
+ $(UNIT_COVER)
+
+#? unit-race: Run unit tests in race detector mode
+unit-race: $(BTCD_BIN)
+ @$(call print, "Running unit race tests.")
+ env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(UNIT_RACE)
+
+#? unit-bench: Run benchmark tests
+unit-bench: $(BTCD_BIN)
+ @$(call print, "Running benchmark tests.")
+ $(UNIT_BENCH)
+
+# =============
+# FLAKE HUNTING
+# =============
+
+#? flakehunter-itest: Run the integration tests continuously until one fails
+flakehunter-itest: build-itest
+ @$(call print, "Flake hunting ${backend} integration tests.")
+ while [ $$? -eq 0 ]; do make itest-only icase='${icase}' backend='${backend}'; done
+
+#? flakehunter-unit: Run the unit tests continuously until one fails
+flakehunter-unit:
+ @$(call print, "Flake hunting unit test.")
+ scripts/unit-test-flake-hunter.sh ${pkg} ${case}
+
+#? flakehunter-unit-all: Run all unit tests continuously until one fails
+flakehunter-unit-all: $(BTCD_BIN)
+ @$(call print, "Flake hunting unit tests.")
+ while [ $$? -eq 0 ]; do make unit; done
+
+#? flakehunter-unit-race: Run all unit tests in race detector mode continuously until one fails
+flakehunter-unit-race: $(BTCD_BIN)
+ @$(call print, "Flake hunting unit tests in race detector mode.")
+ while [ $$? -eq 0 ]; do make unit-race; done
+
+#? flakehunter-itest-parallel: Run the integration tests continuously until one fails, running up to ITEST_PARALLELISM test tranches in parallel (default 4)
+flakehunter-itest-parallel:
+ @$(call print, "Flake hunting ${backend} integration tests in parallel.")
+ while [ $$? -eq 0 ]; do make itest-parallel tranches=1 parallel=${ITEST_PARALLELISM} icase='${icase}' backend='${backend}'; done
+
+# =============
+# FUZZING
+# =============
+
+#? fuzz: Run the fuzzing tests
+fuzz:
+ @$(call print, "Fuzzing packages '$(FUZZPKG)'.")
+ scripts/fuzz.sh run "$(FUZZPKG)" "$(FUZZ_TEST_RUN_TIME)" "$(FUZZ_NUM_PROCESSES)"
+
+# =========
+# UTILITIES
+# =========
+
+#? fmt: Format source code and fix imports
+fmt: $(GOIMPORTS_BIN)
+ @$(call print, "Fixing imports.")
+ gosimports -w $(GOFILES_NOVENDOR)
+ @$(call print, "Formatting source.")
+ gofmt -l -w -s $(GOFILES_NOVENDOR)
+
+#? fmt-check: Make sure source code is formatted and imports are correct
+fmt-check: fmt
+ @$(call print, "Checking fmt results.")
+ if test -n "$$(git status --porcelain)"; then echo "code not formatted correctly, please run `make fmt` again!"; git status; git diff; exit 1; fi
+
+#? check-go-version-yaml: Verify that the Go version is correct in all YAML files
+check-go-version-yaml:
+ @$(call print, "Checking for target Go version (v$(GO_VERSION)) in YAML files (*.yaml, *.yml)")
+ ./scripts/check-go-version-yaml.sh $(GO_VERSION)
+
+#? check-go-version-dockerfile: Verify that the Go version is correct in all Dockerfile files
+check-go-version-dockerfile:
+ @$(call print, "Checking for target Go version (v$(GO_VERSION)) in Dockerfile files (*Dockerfile)")
+ ./scripts/check-go-version-dockerfile.sh $(GO_VERSION)
+
+#? check-go-version: Verify that the Go version is correct in all project files
+check-go-version: check-go-version-dockerfile check-go-version-yaml
+
+#? lint-source: Run static code analysis
+lint-source: docker-tools
+ @$(call print, "Linting source.")
+ $(DOCKER_TOOLS) custom-gcl run -v $(LINT_WORKERS)
+
+#? lint: Run static code analysis
+lint: check-go-version lint-source
+
+#? protolint: Lint proto files using protolint
+protolint:
+ @$(call print, "Linting proto files.")
+ docker run --rm --volume "$$(pwd):/workspace" --workdir /workspace yoheimuta/protolint lint lnrpc/
+
+#? tidy-module: Run `go mod` tidy for all modules
+tidy-module:
+ echo "Running 'go mod tidy' for all modules"
+ scripts/tidy_modules.sh
+
+#? tidy-module-check: Make sure all modules are up to date
+tidy-module-check: tidy-module
+ if test -n "$$(git status --porcelain)"; then echo "modules not updated, please run `make tidy-module` again!"; git status; exit 1; fi
+
+#? list: List all available make targets
+list:
+ @$(call print, "Listing commands:")
+ @$(MAKE) -qp | \
+ awk -F':' '/^[a-zA-Z0-9][^$$#\/\t=]*:([^=]|$$)/ {split($$1,A,/ /);for(i in A)print A[i]}' | \
+ grep -v Makefile | \
+ sort
+
+#? help: List all available make targets with their descriptions
+help: Makefile
+ @$(call print, "Listing commands:")
+ @sed -n 's/^#?//p' $< | column -t -s ':' | sort | sed -e 's/^/ /'
+
+#? backwards-compat-test: Run basic backwards compatibility test
+backwards-compat-test:
+ @$(call print, "Running backwards compatability test")
+ ./scripts/bw-compatibility-test/test.sh
+
+#? sqlc: Generate sql models and queries in Go
+sqlc:
+ @$(call print, "Generating sql models and queries in Go")
+ ./scripts/gen_sqlc_docker.sh
+
+#? sqlc-check: Make sure sql models and queries are up to date
+sqlc-check: sqlc
+ @$(call print, "Verifying sql code generation.")
+ if test -n "$$(git status --porcelain '*.go')"; then echo "SQL models not properly generated!"; git status --porcelain '*.go'; exit 1; fi
+
+#? rpc: Compile protobuf definitions and generate REST proxy stubs
+rpc:
+ @$(call print, "Compiling protos.")
+ cd ./lnrpc; ./gen_protos_docker.sh
+
+#? rpc-format: Format protobuf definition files
+rpc-format:
+ @$(call print, "Formatting protos.")
+ cd ./lnrpc; find . -name "*.proto" | xargs clang-format --style=file -i
+
+#? rpc-check: Make sure protobuf definitions are up to date
+rpc-check: rpc
+ @$(call print, "Verifying protos.")
+ cd ./lnrpc; ../scripts/check-rest-annotations.sh
+ if test -n "$$(git status --porcelain)"; then echo "Protos not properly formatted or not compiled with v3.4.0"; git status; git diff; exit 1; fi
+
+#? rpc-js-compile: Compile protobuf definitions and generate JSON/WASM stubs
+rpc-js-compile:
+ @$(call print, "Compiling JSON/WASM stubs.")
+ GOOS=js GOARCH=wasm $(GOBUILD) -tags="$(WASM_RELEASE_TAGS)" $(PKG)/lnrpc/...
+
+#? sample-conf-check: Make sure default values in the sample-lnd.conf file are set correctly
+sample-conf-check:
+ @$(call print, "Checking that default values in the sample-lnd.conf file are set correctly")
+ scripts/check-sample-lnd-conf.sh "$(RELEASE_TAGS)"
+
+#? mobile-rpc: Compile mobile RPC stubs from the protobuf definitions
+mobile-rpc:
+ @$(call print, "Creating mobile RPC from protos.")
+ cd ./lnrpc; COMPILE_MOBILE=1 SUBSERVER_PREFIX=1 ./gen_protos_docker.sh
+
+#? vendor: Create a vendor directory with all dependencies
+vendor:
+ @$(call print, "Re-creating vendor directory.")
+ rm -r vendor/; $(GOCC) mod vendor
+
+#? apple: Build mobile RPC stubs and project template for iOS and macOS
+apple: mobile-rpc
+ @$(call print, "Building iOS and macOS cxframework ($(IOS_BUILD)).")
+ mkdir -p $(IOS_BUILD_DIR)
+ $(GOMOBILE_BIN) bind -target=ios,iossimulator,macos -tags="mobile $(DEV_TAGS) $(RPC_TAGS)" -ldflags "$(RELEASE_LDFLAGS)" -v -o $(IOS_BUILD) $(MOBILE_PKG)
+
+#? ios: Build mobile RPC stubs and project template for iOS
+ios: mobile-rpc
+ @$(call print, "Building iOS cxframework ($(IOS_BUILD)).")
+ mkdir -p $(IOS_BUILD_DIR)
+ $(GOMOBILE_BIN) bind -target=ios,iossimulator -tags="mobile $(DEV_TAGS) $(RPC_TAGS)" -ldflags "$(RELEASE_LDFLAGS)" -v -o $(IOS_BUILD) $(MOBILE_PKG)
+
+#? macos: Build mobile RPC stubs and project template for macOS
+macos: mobile-rpc
+ @$(call print, "Building macOS cxframework ($(IOS_BUILD)).")
+ mkdir -p $(IOS_BUILD_DIR)
+ $(GOMOBILE_BIN) bind -target=macos -tags="mobile $(DEV_TAGS) $(RPC_TAGS)" -ldflags "$(RELEASE_LDFLAGS)" -v -o $(IOS_BUILD) $(MOBILE_PKG)
+
+#? android: Build mobile RPC stubs and project template for Android
+android: mobile-rpc
+ @$(call print, "Building Android library ($(ANDROID_BUILD)).")
+ mkdir -p $(ANDROID_BUILD_DIR)
+ $(GOMOBILE_BIN) bind -target=android -androidapi 21 -tags="mobile $(DEV_TAGS) $(RPC_TAGS)" -ldflags "$(RELEASE_LDFLAGS)" -v -o $(ANDROID_BUILD) $(MOBILE_PKG)
+
+#? mobile: Build mobile RPC stubs and project templates for iOS and Android
+mobile: ios android
+
+#? clean: Remove all generated files
+clean:
+ @$(call print, "Cleaning source.$(NC)")
+ $(RM) ./lnd-debug ./lncli-debug
+ $(RM) ./lnd-itest ./lncli-itest
+ $(RM) -r ./vendor .vendor-new
+
+#? clean-mobile: Remove all generated mobile files
+clean-mobile:
+ @$(call print, "Cleaning autogenerated mobile RPC stubs.")
+ $(RM) -r mobile/build
+ $(RM) mobile/*_generated.go
+
+.PHONY: all \
+ btcd \
+ default \
+ build \
+ install \
+ scratch \
+ check \
+ help \
+ itest-only \
+ itest \
+ unit \
+ unit-debug \
+ unit-cover \
+ unit-race \
+ flakehunter \
+ flake-unit \
+ fmt \
+ lint \
+ list \
+ rpc \
+ rpc-format \
+ rpc-check \
+ rpc-js-compile \
+ mobile-rpc \
+ vendor \
+ ios \
+ android \
+ mobile \
+ clean
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..091bb82
--- /dev/null
+++ b/README.md
@@ -0,0 +1,101 @@
+## Lightning Network Daemon
+
+[](https://github.com/lightningnetwork/lnd/actions/workflows/release.yaml)
+[](https://github.com/lightningnetwork/lnd/blob/master/LICENSE)
+[](https://web.libera.chat/#lnd)
+[](https://godoc.org/github.com/lightningnetwork/lnd)
+[](https://goreportcard.com/report/github.com/lightningnetwork/lnd)
+
+<img src="logo.png">
+
+The Lightning Network Daemon (`lnd`) - is a complete implementation of a
+[Lightning Network](https://lightning.network) node. `lnd` has several pluggable back-end
+chain services including [`btcd`](https://github.com/btcsuite/btcd) (a
+full-node), [`bitcoind`](https://github.com/bitcoin/bitcoin), and
+[`neutrino`](https://github.com/lightninglabs/neutrino) (a new experimental light client). The project's codebase uses the
+[btcsuite](https://github.com/btcsuite/) set of Bitcoin libraries, and also
+exports a large set of isolated re-usable Lightning Network related libraries
+within it. In the current state `lnd` is capable of:
+* Creating channels.
+* Closing channels.
+* Completely managing all channel states (including the exceptional ones!).
+* Maintaining a fully authenticated+validated channel graph.
+* Performing path finding within the network, passively forwarding incoming payments.
+* Sending outgoing [onion-encrypted payments](https://github.com/lightningnetwork/lightning-onion)
+through the network.
+* Updating advertised fee schedules.
+* Automatic channel management ([`autopilot`](https://github.com/lightningnetwork/lnd/tree/master/autopilot)).
+
+## Lightning Network Specification Compliance
+`lnd` _fully_ conforms to the [Lightning Network specification
+(BOLTs)](https://github.com/lightningnetwork/lightning-rfc). BOLT stands for:
+Basis of Lightning Technology. The specifications are currently being drafted
+by several groups of implementers based around the world including the
+developers of `lnd`. The set of specification documents as well as our
+implementation of the specification are still a work-in-progress. With that
+said, the current status of `lnd`'s BOLT compliance is:
+
+ - [X] BOLT 1: Base Protocol
+ - [X] BOLT 2: Peer Protocol for Channel Management
+ - [X] BOLT 3: Bitcoin Transaction and Script Formats
+ - [X] BOLT 4: Onion Routing Protocol
+ - [X] BOLT 5: Recommendations for On-chain Transaction Handling
+ - [X] BOLT 7: P2P Node and Channel Discovery
+ - [X] BOLT 8: Encrypted and Authenticated Transport
+ - [X] BOLT 9: Assigned Feature Flags
+ - [X] BOLT 10: DNS Bootstrap and Assisted Node Location
+ - [X] BOLT 11: Invoice Protocol for Lightning Payments
+
+## Developer Resources
+
+The daemon has been designed to be as developer friendly as possible in order
+to facilitate application development on top of `lnd`. Two primary RPC
+interfaces are exported: an HTTP REST API, and a [gRPC](https://grpc.io/)
+service. The exported APIs are not yet stable, so be warned: they may change
+drastically in the near future.
+
+An automatically generated set of documentation for the RPC APIs can be found
+at [api.lightning.community](https://api.lightning.community). A set of developer
+resources including guides, articles, example applications and community resources can be found at:
+[docs.lightning.engineering](https://docs.lightning.engineering).
+
+Finally, we also have an active
+[Slack](https://lightning.engineering/slack.html) where protocol developers, application developers, testers and users gather to
+discuss various aspects of `lnd` and also Lightning in general.
+
+First-time contributors are [highly encouraged to start with code review
+first](docs/review.md), before creating their own Pull Requests.
+
+## Installation
+ In order to build from source, please see [the installation
+ instructions](docs/INSTALL.md).
+
+## Docker
+ To run lnd from Docker, please see the main [Docker instructions](docs/DOCKER.md)
+
+## IRC
+ * irc.libera.chat
+ * channel #lnd
+ * [webchat](https://web.libera.chat/#lnd)
+
+## Safety
+
+When operating a mainnet `lnd` node, please refer to our [operational safety
+guidelines](docs/safety.md). It is important to note that `lnd` is still
+**beta** software and that ignoring these operational guidelines can lead to
+loss of funds.
+
+## Security
+
+The developers of `lnd` take security _very_ seriously. The disclosure of
+security vulnerabilities helps us secure the health of `lnd`, privacy of our
+users, and also the health of the Lightning Network as a whole. If you find
+any issues regarding security or privacy, please disclose the information
+responsibly by sending an email to security at lightning dot engineering,
+preferably encrypted using our designated PGP key
+(`91FE464CD75101DA6B6BAB60555C6465E5BCB3AF`) which can be found
+[here](https://gist.githubusercontent.com/Roasbeef/6fb5b52886183239e4aa558f83d085d3/raw/5fa96010af201628bcfa61e9309d9b13d23d220f/security@lightning.engineering).
+
+## Further reading
+* [Step-by-step send payment guide with docker](https://github.com/lightningnetwork/lnd/tree/master/docker)
+* [Contribution guide](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md)
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..ea945bd
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,11 @@
+# Security Policy
+
+## Supported Versions
+
+The last major lnd release is to be considered the current support version. Given an issue severe enough, a backport will be issued either to the prior major release or the set of releases considered utilized enough.
+
+## Reporting a Vulnerability
+
+To report security issues, send an email to security@lightning.engineering (this list isn't to be used for support).
+
+The following key can be used to communicate sensitive information: `91FE 464C D751 01DA 6B6B AB60 555C 6465 E5BC B3AF`.
diff --git a/accessman.go b/accessman.go
new file mode 100644
index 0000000..971132f
--- /dev/null
+++ b/accessman.go
@@ -0,0 +1,664 @@
+package lnd
+
+import (
+ "context"
+ "fmt"
+ "maps"
+ "sync"
+
+ "github.com/btcsuite/btcd/btcec/v2"
+ "github.com/btcsuite/btclog/v2"
+ "github.com/lightningnetwork/lnd/channeldb"
+ "github.com/lightningnetwork/lnd/lnutils"
+)
+
+// accessMan is responsible for managing the server's access permissions.
+type accessMan struct {
+ cfg *accessManConfig
+
+ // banScoreMtx is used for the server's ban tracking. If the server
+ // mutex is also going to be locked, ensure that this is locked after
+ // the server mutex.
+ banScoreMtx sync.RWMutex
+
+ // peerChanInfo is a mapping from remote public key to {bool, uint64}
+ // where the bool indicates that we have an open/closed channel with the
+ // peer and where the uint64 indicates the number of pending-open
+ // channels we currently have with them. This mapping will be used to
+ // determine access permissions for the peer. The map key is the
+ // string-version of the serialized public key.
+ //
+ // NOTE: This MUST be accessed with the banScoreMtx held.
+ peerChanInfo map[string]channeldb.ChanCount
+
+ // peerScores stores each connected peer's access status. The map key
+ // is the string-version of the serialized public key.
+ //
+ // NOTE: This MUST be accessed with the banScoreMtx held.
+ //
+ // TODO(yy): unify `peerScores` and `peerChanInfo` - there's no need to
+ // create two maps tracking essentially the same info. `numRestricted`
+ // can also be derived from `peerChanInfo`.
+ peerScores map[string]peerSlotStatus
+
+ // numRestricted tracks the number of peers with restricted access in
+ // peerScores. This MUST be accessed with the banScoreMtx held.
+ numRestricted int64
+}
+
+type accessManConfig struct {
+ // initAccessPerms checks the channeldb for initial access permissions
+ // and then populates the peerChanInfo and peerScores maps.
+ initAccessPerms func() (map[string]channeldb.ChanCount, error)
+
+ // shouldDisconnect determines whether we should disconnect a peer or
+ // not.
+ shouldDisconnect func(*btcec.PublicKey) (bool, error)
+
+ // maxRestrictedSlots is the number of restricted slots we'll allocate.
+ maxRestrictedSlots int64
+}
+
+func newAccessMan(cfg *accessManConfig) (*accessMan, error) {
+ a := &accessMan{
+ cfg: cfg,
+ peerChanInfo: make(map[string]channeldb.ChanCount),
+ peerScores: make(map[string]peerSlotStatus),
+ }
+
+ counts, err := a.cfg.initAccessPerms()
+ if err != nil {
+ return nil, err
+ }
+
+ // We'll populate the server's peerChanInfo map with the counts fetched
+ // via initAccessPerms. Also note that we haven't yet connected to the
+ // peers.
+ maps.Copy(a.peerChanInfo, counts)
+
+ acsmLog.Info("Access Manager initialized")
+
+ return a, nil
+}
+
+// hasPeer checks whether a given peer already exists in the internal maps.
+func (a *accessMan) hasPeer(ctx context.Context,
+ pub string) (peerAccessStatus, bool) {
+
+ // Lock banScoreMtx for reading so that we can read the banning maps
+ // below.
+ a.banScoreMtx.RLock()
+ defer a.banScoreMtx.RUnlock()
+
+ count, found := a.peerChanInfo[pub]
+ if found {
+ if count.HasOpenOrClosedChan {
+ acsmLog.DebugS(ctx, "Peer has open/closed channel, "+
+ "assigning protected access")
+
+ // Exit early if the peer is no longer restricted.
+ return peerStatusProtected, true
+ }
+
+ if count.PendingOpenCount != 0 {
+ acsmLog.DebugS(ctx, "Peer has pending channel(s), "+
+ "assigning temporary access")
+
+ // Exit early if the peer is no longer restricted.
+ return peerStatusTemporary, true
+ }
+
+ return peerStatusRestricted, true
+ }
+
+ // Check if the peer is found in the scores map.
+ status, found := a.peerScores[pub]
+ if found {
+ acsmLog.DebugS(ctx, "Peer already has access", "access",
+ status.state)
+
+ return status.state, true
+ }
+
+ return peerStatusRestricted, false
+}
+
+// assignPeerPerms assigns a new peer its permissions. This does not track the
+// access in the maps. This is intentional.
+func (a *accessMan) assignPeerPerms(remotePub *btcec.PublicKey) (
+ peerAccessStatus, error) {
+
+ ctx := btclog.WithCtx(
+ context.TODO(), lnutils.LogPubKey("peer", remotePub),
+ )
+
+ peerMapKey := string(remotePub.SerializeCompressed())
+
+ acsmLog.DebugS(ctx, "Assigning permissions")
+
+ // Default is restricted unless the below filters say otherwise.
+ access, peerExist := a.hasPeer(ctx, peerMapKey)
+
+ // Exit early if the peer is not restricted.
+ if access != peerStatusRestricted {
+ return access, nil
+ }
+
+ // If we are here, it means the peer has peerStatusRestricted.
+ //
+ // Check whether this peer is banned.
+ shouldDisconnect, err := a.cfg.shouldDisconnect(remotePub)
+ if err != nil {
+ acsmLog.ErrorS(ctx, "Error checking disconnect status", err)
+
+ // Access is restricted here.
+ return access, err
+ }
+
+ if shouldDisconnect {
+ acsmLog.WarnS(ctx, "Peer is banned, assigning restricted access",
+ ErrGossiperBan)
+
+ // Access is restricted here.
+ return access, ErrGossiperBan
+ }
+
+ // If we've reached this point and access hasn't changed from
+ // restricted, then we need to check if we even have a slot for this
+ // peer.
+ acsmLog.DebugS(ctx, "Peer has no channels, assigning restricted access")
+
+ // If this is an existing peer, there's no need to check for slot limit.
+ if peerExist {
+ acsmLog.DebugS(ctx, "Skipped slot check for existing peer")
+ return access, nil
+ }
+
+ a.banScoreMtx.RLock()
+ defer a.banScoreMtx.RUnlock()
+
+ if a.numRestricted >= a.cfg.maxRestrictedSlots {
+ acsmLog.WarnS(ctx, "No more restricted slots available, "+
+ "denying peer", ErrNoMoreRestrictedAccessSlots,
+ "num_restricted", a.numRestricted, "max_restricted",
+ a.cfg.maxRestrictedSlots)
+
+ return access, ErrNoMoreRestrictedAccessSlots
+ }
+
+ return access, nil
+}
+
+// newPendingOpenChan is called after the pending-open channel has been
+// committed to the database. This may transition a restricted-access peer to a
+// temporary-access peer.
+func (a *accessMan) newPendingOpenChan(remotePub *btcec.PublicKey) error {
+ a.banScoreMtx.Lock()
+ defer a.banScoreMtx.Unlock()
+
+ ctx := btclog.WithCtx(
+ context.TODO(), lnutils.LogPubKey("peer", remotePub),
+ )
+
+ acsmLog.DebugS(ctx, "Processing new pending open channel")
+
+ peerMapKey := string(remotePub.SerializeCompressed())
+
+ // Fetch the peer's access status from peerScores.
+ status, found := a.peerScores[peerMapKey]
+ if !found {
+ acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
+
+ // If we didn't find the peer, we'll return an error.
+ return ErrNoPeerScore
+ }
+
+ switch status.state {
+ case peerStatusProtected:
+ acsmLog.DebugS(ctx, "Peer already protected, no change")
+
+ // If this peer's access status is protected, we don't need to
+ // do anything.
+ return nil
+
+ case peerStatusTemporary:
+ // If this peer's access status is temporary, we'll need to
+ // update the peerChanInfo map. The peer's access status will
+ // stay temporary.
+ peerCount, found := a.peerChanInfo[peerMapKey]
+ if !found {
+ // Error if we did not find any info in peerChanInfo.
+ acsmLog.ErrorS(ctx, "Pending peer info not found",
+ ErrNoPendingPeerInfo)
+
+ return ErrNoPendingPeerInfo
+ }
+
+ // Increment the pending channel amount.
+ peerCount.PendingOpenCount += 1
+ a.peerChanInfo[peerMapKey] = peerCount
+
+ acsmLog.DebugS(ctx, "Peer is temporary, incremented "+
+ "pending count",
+ "pending_count", peerCount.PendingOpenCount)
+
+ case peerStatusRestricted:
+ // If the peer's access status is restricted, then we can
+ // transition it to a temporary-access peer. We'll need to
+ // update numRestricted and also peerScores. We'll also need to
+ // update peerChanInfo.
+ peerCount := channeldb.ChanCount{
+ HasOpenOrClosedChan: false,
+ PendingOpenCount: 1,
+ }
+
+ a.peerChanInfo[peerMapKey] = peerCount
+
+ // A restricted-access slot has opened up.
+ oldRestricted := a.numRestricted
+ a.numRestricted -= 1
+
+ a.peerScores[peerMapKey] = peerSlotStatus{
+ state: peerStatusTemporary,
+ }
+
+ acsmLog.InfoS(ctx, "Peer transitioned restricted -> "+
+ "temporary (pending open)",
+ "old_restricted", oldRestricted,
+ "new_restricted", a.numRestricted)
+
+ default:
+ // This should not be possible.
+ err := fmt.Errorf("invalid peer access status %v for %x",
+ status.state, peerMapKey)
+ acsmLog.ErrorS(ctx, "Invalid peer access status", err)
+
+ return err
+ }
+
+ return nil
+}
+
+// newPendingCloseChan is called when a pending-open channel prematurely closes
+// before the funding transaction has confirmed. This potentially demotes a
+// temporary-access peer to a restricted-access peer. If no restricted-access
+// slots are available, the peer will be disconnected.
+func (a *accessMan) newPendingCloseChan(remotePub *btcec.PublicKey) error {
+ a.banScoreMtx.Lock()
+ defer a.banScoreMtx.Unlock()
+
+ ctx := btclog.WithCtx(
+ context.TODO(), lnutils.LogPubKey("peer", remotePub),
+ )
+
+ acsmLog.DebugS(ctx, "Processing pending channel close")
+
+ peerMapKey := string(remotePub.SerializeCompressed())
+
+ // Fetch the peer's access status from peerScores.
+ status, found := a.peerScores[peerMapKey]
+ if !found {
+ acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
+
+ return ErrNoPeerScore
+ }
+
+ switch status.state {
+ case peerStatusProtected:
+ // If this peer is protected, we don't do anything.
+ acsmLog.DebugS(ctx, "Peer is protected, no change")
+
+ return nil
+
+ case peerStatusTemporary:
+ // If this peer is temporary, we need to check if it will
+ // revert to a restricted-access peer.
+ peerCount, found := a.peerChanInfo[peerMapKey]
+ if !found {
+ acsmLog.ErrorS(ctx, "Pending peer info not found",
+ ErrNoPendingPeerInfo)
+
+ // Error if we did not find any info in peerChanInfo.
+ return ErrNoPendingPeerInfo
+ }
+
+ currentNumPending := peerCount.PendingOpenCount - 1
+
+ acsmLog.DebugS(ctx, "Peer is temporary, decrementing "+
+ "pending count",
+ "pending_count", currentNumPending)
+
+ if currentNumPending == 0 {
+ // Remove the entry from peerChanInfo.
+ delete(a.peerChanInfo, peerMapKey)
+
+ // If this is the only pending-open channel for this
+ // peer and it's getting removed, attempt to demote
+ // this peer to a restricted peer.
+ if a.numRestricted == a.cfg.maxRestrictedSlots {
+ // There are no available restricted slots, so
+ // we need to disconnect this peer. We leave
+ // this up to the caller.
+ acsmLog.WarnS(ctx, "Peer last pending "+
+ "channel closed: ",
+ ErrNoMoreRestrictedAccessSlots,
+ "num_restricted", a.numRestricted,
+ "max_restricted", a.cfg.maxRestrictedSlots)
+
+ return ErrNoMoreRestrictedAccessSlots
+ }
+
+ // Otherwise, there is an available restricted-access
+ // slot, so we can demote this peer.
+ a.peerScores[peerMapKey] = peerSlotStatus{
+ state: peerStatusRestricted,
+ }
+
+ // Update numRestricted.
+ oldRestricted := a.numRestricted
+ a.numRestricted++
+
+ acsmLog.InfoS(ctx, "Peer transitioned "+
+ "temporary -> restricted "+
+ "(last pending closed)",
+ "old_restricted", oldRestricted,
+ "new_restricted", a.numRestricted)
+
+ return nil
+ }
+
+ // Else, we don't need to demote this peer since it has other
+ // pending-open channels with us.
+ peerCount.PendingOpenCount = currentNumPending
+ a.peerChanInfo[peerMapKey] = peerCount
+
+ acsmLog.DebugS(ctx, "Peer still has other pending channels",
+ "pending_count", currentNumPending)
+
+ return nil
+
+ case peerStatusRestricted:
+ // This should not be possible. This indicates an error.
+ err := fmt.Errorf("invalid peer access state transition: "+
+ "pending close for restricted peer %x", peerMapKey)
+ acsmLog.ErrorS(ctx, "Invalid peer access state transition", err)
+
+ return err
+
+ default:
+ // This should not be possible.
+ err := fmt.Errorf("invalid peer access status %v for %x",
+ status.state, peerMapKey)
+ acsmLog.ErrorS(ctx, "Invalid peer access status", err)
+
+ return err
+ }
+}
+
+// newOpenChan is called when a pending-open channel becomes an open channel
+// (i.e. the funding transaction has confirmed). If the remote peer is a
+// temporary-access peer, it will be promoted to a protected-access peer.
+func (a *accessMan) newOpenChan(remotePub *btcec.PublicKey) error {
+ a.banScoreMtx.Lock()
+ defer a.banScoreMtx.Unlock()
+
+ ctx := btclog.WithCtx(
+ context.TODO(), lnutils.LogPubKey("peer", remotePub),
+ )
+
+ acsmLog.DebugS(ctx, "Processing new open channel")
+
+ peerMapKey := string(remotePub.SerializeCompressed())
+
+ // Fetch the peer's access status from peerScores.
+ status, found := a.peerScores[peerMapKey]
+ if !found {
+ // If we didn't find the peer, we'll return an error.
+ acsmLog.ErrorS(ctx, "Peer score not found", ErrNoPeerScore)
+
+ return ErrNoPeerScore
+ }
+
+ switch status.state {
+ case peerStatusProtected:
+ acsmLog.DebugS(ctx, "Peer already protected, no change")
+
+ // If the peer's state is already protected, we don't need to do
+ // anything more.
+ return nil
+
+ case peerStatusTemporary:
+ // If the peer's state is temporary, we'll upgrade the peer to
+ // a protected peer.
+ peerCount, found := a.peerChanInfo[peerMapKey]
+ if !found {
+ // Error if we did not find any info in peerChanInfo.
+ acsmLog.ErrorS(ctx, "Pending peer info not found",
+ ErrNoPendingPeerInfo)
+
+ return ErrNoPendingPeerInfo
+ }
+
+ peerCount.HasOpenOrClosedChan = true
+ peerCount.PendingOpenCount -= 1
+
+ a.peerChanInfo[peerMapKey] = peerCount
+
+ newStatus := peerSlotStatus{
+ state: peerStatusProtected,
+ }
+ a.peerScores[peerMapKey] = newStatus
+
+ acsmLog.InfoS(ctx, "Peer transitioned temporary -> "+
+ "protected (channel opened)")
+
+ return nil
+
+ case peerStatusRestricted:
+ // This should not be possible. For the server to receive a
+ // state-transition event via NewOpenChan, the server must have
+ // previously granted this peer "temporary" access. This
+ // temporary access would not have been revoked or downgraded
+ // without `CloseChannel` being called with the pending
+ // argument set to true. This means that an open-channel state
+ // transition would be impossible. Therefore, we can return an
+ // error.
+ err := fmt.Errorf("invalid peer access status: new open "+
+ "channel for restricted peer %x", peerMapKey)
+
+ acsmLog.ErrorS(ctx, "Invalid peer access status", err)
+
+ return err
+
+ default:
+ // This should not be possible.
+ err := fmt.Errorf("invalid peer access status %v for %x",
+ status.state, peerMapKey)
+
+ acsmLog.ErrorS(ctx, "Invalid peer access status", err)
+
+ return err
+ }
+}
+
+// checkAcceptIncomingConn checks whether, given the remote's public hex-
+// encoded key, we should not accept this incoming connection or immediately
+// disconnect. This does not assign to the server's peerScores maps. This is
+// just an inbound filter that the brontide listeners use.
+//
+// TODO(yy): We should also consider removing this `checkAcceptIncomingConn`
+// check as a) it doesn't check for ban score; and b) we should, and already
+// have this check when we handle incoming connection in `InboundPeerConnected`.
+func (a *accessMan) checkAcceptIncomingConn(remotePub *btcec.PublicKey) (
+ bool, error) {
+
+ ctx := btclog.WithCtx(
+ context.TODO(), lnutils.LogPubKey("peer", remotePub),
+ )
+
+ peerMapKey := string(remotePub.SerializeCompressed())
+
+ acsmLog.TraceS(ctx, "Checking incoming connection ban score")
+
+ a.banScoreMtx.RLock()
+ defer a.banScoreMtx.RUnlock()
+
+ _, found := a.peerChanInfo[peerMapKey]
+
+ // Exit early if found.
+ if found {
+ acsmLog.DebugS(ctx, "Peer found (protected/temporary), "+
+ "accepting")
+
+ return true, nil
+ }
+
+ _, found = a.peerScores[peerMapKey]
+
+ // Exit early if found.
+ if found {
+ acsmLog.DebugS(ctx, "Found existing peer, accepting")
+
+ return true, nil
+ }
+
+ acsmLog.DebugS(ctx, "Peer not found in counts, checking restricted "+
+ "slots")
+
+ // Check numRestricted to see if there is an available slot. In
+ // the future, it's possible to add better heuristics.
+ if a.numRestricted < a.cfg.maxRestrictedSlots {
+ // There is an available slot.
+ acsmLog.DebugS(ctx, "Restricted slot available, accepting ",
+ "num_restricted", a.numRestricted, "max_restricted",
+ a.cfg.maxRestrictedSlots)
+
+ return true, nil
+ }
+
+ // If there are no slots left, then we reject this connection.
+ acsmLog.WarnS(ctx, "No restricted slots available, rejecting ",
+ ErrNoMoreRestrictedAccessSlots, "num_restricted",
+ a.numRestricted, "max_restricted", a.cfg.maxRestrictedSlots)
+
+ return false, ErrNoMoreRestrictedAccessSlots
+}
+
+// addPeerAccess tracks a peer's access in the maps. This should be called when
+// the peer has fully connected.
+func (a *accessMan) addPeerAccess(remotePub *btcec.PublicKey,
+ access peerAccessStatus, inbound bool) {
+
+ ctx := btclog.WithCtx(
+ context.TODO(), lnutils.LogPubKey("peer", remotePub),
+ )
+
+ acsmLog.DebugS(ctx, "Adding peer access", "access", access)
+
+ // Add the remote public key to peerScores.
+ a.banScoreMtx.Lock()
+ defer a.banScoreMtx.Unlock()
+
+ peerMapKey := string(remotePub.SerializeCompressed())
+
+ // Exit early if this is an existing peer, which means it won't take
+ // another slot.
+ _, found := a.peerScores[peerMapKey]
+ if found {
+ acsmLog.DebugS(ctx, "Skipped taking restricted slot for "+
+ "existing peer")
+
+ return
+ }
+
+ a.peerScores[peerMapKey] = peerSlotStatus{state: access}
+
+ // Exit early if this is not a restricted peer.
+ if access != peerStatusRestricted {
+ acsmLog.DebugS(ctx, "Skipped taking restricted slot as peer "+
+ "already has access", "access", access)
+
+ return
+ }
+
+ // Increment numRestricted if this is an inbound connection.
+ if inbound {
+ oldRestricted := a.numRestricted
+ a.numRestricted++
+
+ acsmLog.DebugS(ctx, "Incremented restricted slots",
+ "old_restricted", oldRestricted,
+ "new_restricted", a.numRestricted)
+
+ return
+ }
+
+ // Otherwise, this is a newly created outbound connection. We won't
+ // place any restriction on it, instead, we will do a hot upgrade here
+ // to move it from restricted to temporary.
+ peerCount := channeldb.ChanCount{
+ HasOpenOrClosedChan: false,
+ PendingOpenCount: 0,
+ }
+
+ a.peerChanInfo[peerMapKey] = peerCount
+ a.peerScores[peerMapKey] = peerSlotStatus{
+ state: peerStatusTemporary,
+ }
+
+ acsmLog.InfoS(ctx, "Upgraded outbound peer: restricted -> temporary")
+}
+
+// removePeerAccess removes the peer's access from the maps. This should be
+// called when the peer has been disconnected.
+func (a *accessMan) removePeerAccess(ctx context.Context, peerPubKey string) {
+ acsmLog.DebugS(ctx, "Removing access:")
+
+ a.banScoreMtx.Lock()
+ defer a.banScoreMtx.Unlock()
+
+ status, found := a.peerScores[peerPubKey]
+ if !found {
+ acsmLog.InfoS(ctx, "Peer score not found during removal")
+ return
+ }
+
+ if status.state == peerStatusRestricted {
+ // If the status is restricted, then we decrement from
+ // numRestrictedSlots.
+ oldRestricted := a.numRestricted
+ a.numRestricted--
+
+ acsmLog.DebugS(ctx, "Decremented restricted slots",
+ "old_restricted", oldRestricted,
+ "new_restricted", a.numRestricted)
+ }
+
+ acsmLog.TraceS(ctx, "Deleting from peerScores:")
+
+ delete(a.peerScores, peerPubKey)
+
+ // We now check whether this peer has channels with us or not.
+ info, found := a.peerChanInfo[peerPubKey]
+ if !found {
+ acsmLog.DebugS(ctx, "Chan info not found during removal:")
+ return
+ }
+
+ // Exit early if the peer has channel(s) with us.
+ if info.HasOpenOrClosedChan {
+ acsmLog.DebugS(ctx, "Skipped removing peer with channels:")
+ return
+ }
+
+ // Skip removing the peer if it has pending open/close with us.
+ if info.PendingOpenCount != 0 {
+ acsmLog.DebugS(ctx, "Skipped removing peer with pending "+
+ "channels:")
+ return
+ }
+
+ // Given this peer has no channels with us, we can now remove it.
+ delete(a.peerChanInfo, peerPubKey)
+ acsmLog.TraceS(ctx, "Removed peer from peerChanInfo:")
+}
diff --git a/accessman_test.go b/accessman_test.go
new file mode 100644
index 0000000..4f0c492
--- /dev/null
+++ b/accessman_test.go
@@ -0,0 +1,813 @@
+package lnd
+
+import (
+ "testing"
+
+ "github.com/btcsuite/btcd/btcec/v2"
+ "github.com/lightningnetwork/lnd/channeldb"
+ "github.com/stretchr/testify/require"
+)
+
+// assertInboundConnection asserts that we're able to accept an inbound
+// connection successfully without any access permissions being violated.
+func assertInboundConnection(t *testing.T, a *accessMan,
+ remotePub *btcec.PublicKey, status peerAccessStatus) {
+
+ remotePubSer := string(remotePub.SerializeCompressed())
+
+ isSlotAvailable, err := a.checkAcceptIncomingConn(remotePub)
+ require.NoError(t, err)
+ require.True(t, isSlotAvailable)
+
+ peerAccess, err := a.assignPeerPerms(remotePub)
+ require.NoError(t, err)
+ require.Equal(t, status, peerAccess)
+
+ a.addPeerAccess(remotePub, peerAccess, true)
+ peerScore, ok := a.peerScores[remotePubSer]
+ require.True(t, ok)
+ require.Equal(t, status, peerScore.state)
+}
+
+func assertAccessState(t *testing.T, a *accessMan, remotePub *btcec.PublicKey,
+ expectedStatus peerAccessStatus) {
+
+ remotePubSer := string(remotePub.SerializeCompressed())
+ peerScore, ok := a.peerScores[remotePubSer]
+ require.True(t, ok)
+ require.Equal(t, expectedStatus, peerScore.state)
+}
+
+// TestAccessManRestrictedSlots tests that the configurable number of
+// restricted slots are properly allocated. It also tests that certain peers
+// with access permissions are allowed to bypass the slot mechanism.
+func TestAccessManRestrictedSlots(t *testing.T) {
+ t.Parallel()
+
+ // We'll pre-populate the map to mock the database fetch. We'll make
+ // three peers. One has an open/closed channel. One has both an open
+ // / closed channel and a pending channel. The last one has only a
+ // pending channel.
+ peerPriv1, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ peerKey1 := peerPriv1.PubKey()
+ peerKeySer1 := string(peerKey1.SerializeCompressed())
+
+ peerPriv2, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ peerKey2 := peerPriv2.PubKey()
+ peerKeySer2 := string(peerKey2.SerializeCompressed())
+
+ peerPriv3, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ peerKey3 := peerPriv3.PubKey()
+ peerKeySer3 := string(peerKey3.SerializeCompressed())
+
+ var (
+ peer1PendingCount = 0
+ peer2PendingCount = 1
+ peer3PendingCount = 1
+ )
+
+ initPerms := func() (map[string]channeldb.ChanCount, error) {
+ return map[string]channeldb.ChanCount{
+ peerKeySer1: {
+ HasOpenOrClosedChan: true,
+ PendingOpenCount: uint64(peer1PendingCount),
+ },
+ peerKeySer2: {
+ HasOpenOrClosedChan: true,
+ PendingOpenCount: uint64(peer2PendingCount),
+ },
+ peerKeySer3: {
+ HasOpenOrClosedChan: false,
+ PendingOpenCount: uint64(peer3PendingCount),
+ },
+ }, nil
+ }
+
+ disconnect := func(*btcec.PublicKey) (bool, error) {
+ return false, nil
+ }
+
+ cfg := &accessManConfig{
+ initAccessPerms: initPerms,
+ shouldDisconnect: disconnect,
+ maxRestrictedSlots: 1,
+ }
+
+ a, err := newAccessMan(cfg)
+ require.NoError(t, err)
+
+ // Check that the peerChanInfo map is correctly populated with three
+ // peers.
+ require.Equal(t, 0, int(a.numRestricted))
+ require.Equal(t, 3, len(a.peerChanInfo))
+
+ peerCount1, ok := a.peerChanInfo[peerKeySer1]
+ require.True(t, ok)
+ require.True(t, peerCount1.HasOpenOrClosedChan)
+ require.Equal(t, peer1PendingCount, int(peerCount1.PendingOpenCount))
+
+ peerCount2, ok := a.peerChanInfo[peerKeySer2]
+ require.True(t, ok)
+ require.True(t, peerCount2.HasOpenOrClosedChan)
+ require.Equal(t, peer2PendingCount, int(peerCount2.PendingOpenCount))
+
+ peerCount3, ok := a.peerChanInfo[peerKeySer3]
+ require.True(t, ok)
+ require.False(t, peerCount3.HasOpenOrClosedChan)
+ require.Equal(t, peer3PendingCount, int(peerCount3.PendingOpenCount))
+
+ // We'll now start to connect the peers. We'll add a new fourth peer
+ // that will take up the restricted slot. The first three peers should
+ // be able to bypass this restricted slot mechanism.
+ peerPriv4, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ peerKey4 := peerPriv4.PubKey()
+
+ // Follow the normal process of an incoming connection. We check if we
+ // can accommodate this peer in checkAcceptIncomingConn and then we
+ // assign its access permissions and then insert into the map.
+ assertInboundConnection(t, a, peerKey4, peerStatusRestricted)
+
+ // Connect the three peers. This should happen without any issue.
+ assertInboundConnection(t, a, peerKey1, peerStatusProtected)
+ assertInboundConnection(t, a, peerKey2, peerStatusProtected)
+ assertInboundConnection(t, a, peerKey3, peerStatusTemporary)
+
+ // Check that a pending-open channel promotes the restricted peer.
+ err = a.newPendingOpenChan(peerKey4)
+ require.NoError(t, err)
+ assertAccessState(t, a, peerKey4, peerStatusTemporary)
+
+ // Assert that accessman's internal state is updated with peer4. We
+ // expect this new peer to have 1 pending open count.
+ peerCount4, ok := a.peerChanInfo[string(peerKey4.SerializeCompressed())]
+ require.True(t, ok)
+ require.False(t, peerCount4.HasOpenOrClosedChan)
+ require.Equal(t, 1, int(peerCount4.PendingOpenCount))
+
+ // Check that an open channel promotes the temporary peer.
+ err = a.newOpenChan(peerKey3)
+ require.NoError(t, err)
+ assertAccessState(t, a, peerKey3, peerStatusProtected)
+
+ // Assert that accessman's internal state is updated with peer3. We
+ // expect this existing peer to decrement its pending open count and the
+ // flag `HasOpenOrClosedChan` should be true.
+ peerCount3, ok = a.peerChanInfo[peerKeySer3]
+ require.True(t, ok)
+ require.True(t, peerCount3.HasOpenOrClosedChan)
+ require.Equal(t, peer3PendingCount-1, int(peerCount3.PendingOpenCount))
+
+ // We should be able to accommodate a new peer.
+ peerPriv5, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ peerKey5 := peerPriv5.PubKey()
+
+ assertInboundConnection(t, a, peerKey5, peerStatusRestricted)
+
+ // Check that a pending-close channel event for peer 4 demotes the
+ // peer.
+ err = a.newPendingCloseChan(peerKey4)
+ require.ErrorIs(t, err, ErrNoMoreRestrictedAccessSlots)
+
+ // Assert that peer4 is removed.
+ _, ok = a.peerChanInfo[string(peerKey4.SerializeCompressed())]
+ require.False(t, ok)
+}
+
+// TestAssignPeerPerms asserts that the peer's access status is correctly
+// assigned.
+func TestAssignPeerPerms(t *testing.T) {
+ t.Parallel()
+
+ // genPeerPub is a helper closure that generates a random public key.
+ genPeerPub := func() *btcec.PublicKey {
+ peerPriv, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+
+ return peerPriv.PubKey()
+ }
+
+ disconnect := func(_ *btcec.PublicKey) (bool, error) {
+ return true, nil
+ }
+
+ noDisconnect := func(_ *btcec.PublicKey) (bool, error) {
+ return false, nil
+ }
+
+ var testCases = []struct {
+ name string
+ peerPub *btcec.PublicKey
+ chanCount channeldb.ChanCount
+ shouldDisconnect func(*btcec.PublicKey) (bool, error)
+ numRestricted int
+
+ expectedStatus peerAccessStatus
+ expectedErr error
+ }{
+ // peer1 has a channel with us, and we expect it to have a
+ // protected status.
+ {
+ name: "peer with channels",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: true,
+ },
+ shouldDisconnect: noDisconnect,
+ expectedStatus: peerStatusProtected,
+ expectedErr: nil,
+ },
+ // peer2 has a channel open and a pending channel with us, we
+ // expect it to have a protected status.
+ {
+ name: "peer with channels and pending channels",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: true,
+ PendingOpenCount: 1,
+ },
+ shouldDisconnect: noDisconnect,
+ expectedStatus: peerStatusProtected,
+ expectedErr: nil,
+ },
+ // peer3 has a pending channel with us, and we expect it to have
+ // a temporary status.
+ {
+ name: "peer with pending channels",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: false,
+ PendingOpenCount: 1,
+ },
+ shouldDisconnect: noDisconnect,
+ expectedStatus: peerStatusTemporary,
+ expectedErr: nil,
+ },
+ // peer4 has no channel with us, and we expect it to have a
+ // restricted status.
+ {
+ name: "peer with no channels",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: false,
+ PendingOpenCount: 0,
+ },
+ shouldDisconnect: noDisconnect,
+ expectedStatus: peerStatusRestricted,
+ expectedErr: nil,
+ },
+ // peer5 has no channel with us, and we expect it to have a
+ // restricted status. We also expect the error `ErrGossiperBan`
+ // to be returned given we will use a mocked `shouldDisconnect`
+ // in this test to disconnect on peer5 only.
+ {
+ name: "peer with no channels and banned",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: false,
+ PendingOpenCount: 0,
+ },
+ shouldDisconnect: disconnect,
+ expectedStatus: peerStatusRestricted,
+ expectedErr: ErrGossiperBan,
+ },
+ // peer6 has no channel with us, and we expect it to have a
+ // restricted status. Since this peer is seen, we don't expect
+ // the error `ErrNoMoreRestrictedAccessSlots` to be returned.
+ {
+ name: "peer with no channels and restricted",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: false,
+ PendingOpenCount: 0,
+ },
+ shouldDisconnect: noDisconnect,
+ numRestricted: 1,
+
+ expectedStatus: peerStatusRestricted,
+ expectedErr: nil,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ peerStr := string(tc.peerPub.SerializeCompressed())
+
+ initPerms := func() (map[string]channeldb.ChanCount,
+ error) {
+
+ return map[string]channeldb.ChanCount{
+ peerStr: tc.chanCount,
+ }, nil
+ }
+
+ cfg := &accessManConfig{
+ initAccessPerms: initPerms,
+ shouldDisconnect: tc.shouldDisconnect,
+ maxRestrictedSlots: 1,
+ }
+
+ a, err := newAccessMan(cfg)
+ require.NoError(t, err)
+
+ // Initialize the internal state of the accessman.
+ a.numRestricted = int64(tc.numRestricted)
+
+ status, err := a.assignPeerPerms(tc.peerPub)
+ require.Equal(t, tc.expectedStatus, status)
+ require.ErrorIs(t, tc.expectedErr, err)
+ })
+ }
+}
+
+// TestAssignPeerPermsBypassRestriction asserts that when a peer has a channel
+// with us, either it being open, pending, or closed, no restriction is placed
+// on this peer.
+func TestAssignPeerPermsBypassRestriction(t *testing.T) {
+ t.Parallel()
+
+ // genPeerPub is a helper closure that generates a random public key.
+ genPeerPub := func() *btcec.PublicKey {
+ peerPriv, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+
+ return peerPriv.PubKey()
+ }
+
+ // Mock shouldDisconnect to always return true and assert that it has no
+ // effect on the peer.
+ disconnect := func(_ *btcec.PublicKey) (bool, error) {
+ return true, nil
+ }
+
+ var testCases = []struct {
+ name string
+ peerPub *btcec.PublicKey
+ chanCount channeldb.ChanCount
+ expectedStatus peerAccessStatus
+ }{
+ // peer1 has a channel with us, and we expect it to have a
+ // protected status.
+ {
+ name: "peer with channels",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: true,
+ },
+ expectedStatus: peerStatusProtected,
+ },
+ // peer2 has a channel open and a pending channel with us, we
+ // expect it to have a protected status.
+ {
+ name: "peer with channels and pending channels",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: true,
+ PendingOpenCount: 1,
+ },
+ expectedStatus: peerStatusProtected,
+ },
+ // peer3 has a pending channel with us, and we expect it to have
+ // a temporary status.
+ {
+ name: "peer with pending channels",
+ peerPub: genPeerPub(),
+ chanCount: channeldb.ChanCount{
+ HasOpenOrClosedChan: false,
+ PendingOpenCount: 1,
+ },
+ expectedStatus: peerStatusTemporary,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ peerStr := string(tc.peerPub.SerializeCompressed())
+
+ initPerms := func() (map[string]channeldb.ChanCount,
+ error) {
+
+ return map[string]channeldb.ChanCount{
+ peerStr: tc.chanCount,
+ }, nil
+ }
+
+ // Config the accessman such that it has zero max slots
+ // and always return true on `shouldDisconnect`. We
+ // should see the peers in this test are not affected by
+ // these checks.
+ cfg := &accessManConfig{
+ initAccessPerms: initPerms,
+ shouldDisconnect: disconnect,
+ maxRestrictedSlots: 0,
+ }
+
+ a, err := newAccessMan(cfg)
+ require.NoError(t, err)
+
+ status, err := a.assignPeerPerms(tc.peerPub)
+ require.NoError(t, err)
+ require.Equal(t, tc.expectedStatus, status)
+ })
+ }
+}
+
+// TestAssignPeerPermsBypassExisting asserts that when the peer is a
+// pre-existing peer, it won't be restricted.
+func TestAssignPeerPermsBypassExisting(t *testing.T) {
+ t.Parallel()
+
+ // genPeerPub is a helper closure that generates a random public key.
+ genPeerPub := func() *btcec.PublicKey {
+ peerPriv, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+
+ return peerPriv.PubKey()
+ }
+
+ // peer1 exists in `peerChanInfo` map.
+ peer1 := genPeerPub()
+ peer1Str := string(peer1.SerializeCompressed())
+
+ // peer2 exists in `peerScores` map.
+ peer2 := genPeerPub()
+ peer2Str := string(peer2.SerializeCompressed())
+
+ // peer3 is a new peer.
+ peer3 := genPeerPub()
+
+ // Create params to init the accessman.
+ initPerms := func() (map[string]channeldb.ChanCount, error) {
+ return map[string]channeldb.ChanCount{
+ peer1Str: {},
+ }, nil
+ }
+
+ disconnect := func(*btcec.PublicKey) (bool, error) {
+ return false, nil
+ }
+
+ cfg := &accessManConfig{
+ initAccessPerms: initPerms,
+ shouldDisconnect: disconnect,
+ maxRestrictedSlots: 0,
+ }
+
+ a, err := newAccessMan(cfg)
+ require.NoError(t, err)
+
+ // Add peer2 to the `peerScores`.
+ a.peerScores[peer2Str] = peerSlotStatus{
+ state: peerStatusTemporary,
+ }
+
+ // Assigning to peer1 should not return an error.
+ status, err := a.assignPeerPerms(peer1)
+ require.NoError(t, err)
+ require.Equal(t, peerStatusRestricted, status)
+
+ // Assigning to peer2 should not return an error.
+ status, err = a.assignPeerPerms(peer2)
+ require.NoError(t, err)
+ require.Equal(t, peerStatusTemporary, status)
+
+ // Assigning to peer3 should return an error.
+ status, err = a.assignPeerPerms(peer3)
+ require.ErrorIs(t, err, ErrNoMoreRestrictedAccessSlots)
+ require.Equal(t, peerStatusRestricted, status)
+}
+
+// TestHasPeer asserts `hasPeer` returns the correct results.
+func TestHasPeer(t *testing.T) {
+ t.Parallel()
+
+ ctx := t.Context()
+
+ // Create a testing accessMan.
+ a := &accessMan{
+ peerChanInfo: make(map[string]channeldb.ChanCount),
+ peerScores: make(map[string]peerSlotStatus),
+ }
+
+ // peer1 exists with an open channel.
+ peer1 := "peer1"
+ a.peerChanInfo[peer1] = channeldb.ChanCount{
+ HasOpenOrClosedChan: true,
+ }
+ peer1Access := peerStatusProtected
+
+ // peer2 exists with a pending channel.
+ peer2 := "peer2"
+ a.peerChanInfo[peer2] = channeldb.ChanCount{
+ PendingOpenCount: 1,
+ }
+ peer2Access := peerStatusTemporary
+
+ // peer3 exists without any channels.
+ peer3 := "peer3"
+ a.peerChanInfo[peer3] = channeldb.ChanCount{}
+ peer3Access := peerStatusRestricted
+
+ // peer4 exists with a score.
+ peer4 := "peer4"
+ peer4Access := peerStatusTemporary
+ a.peerScores[peer4] = peerSlotStatus{state: peer4Access}
+
+ // peer5 doesn't exist.
+ peer5 := "peer5"
+
+ // We now assert `hasPeer` returns the correct results.
+ //
+ // peer1 should be found with peerStatusProtected.
+ access, found := a.hasPeer(ctx, peer1)
+ require.True(t, found)
+ require.Equal(t, peer1Access, access)
+
+ // peer2 should be found with peerStatusTemporary.
+ access, found = a.hasPeer(ctx, peer2)
+ require.True(t, found)
+ require.Equal(t, peer2Access, access)
+
+ // peer3 should be found with peerStatusRestricted.
+ access, found = a.hasPeer(ctx, peer3)
+ require.True(t, found)
+ require.Equal(t, peer3Access, access)
+
+ // peer4 should be found with peerStatusTemporary.
+ access, found = a.hasPeer(ctx, peer4)
+ require.True(t, found)
+ require.Equal(t, peer4Access, access)
+
+ // peer5 should NOT be found.
+ access, found = a.hasPeer(ctx, peer5)
+ require.False(t, found)
+ require.Equal(t, peerStatusRestricted, access)
+}
+
+// TestAddPeerAccessInbound asserts the num of slots is correctly incremented
+// only for a new inbound peer with restricted access.
+func TestAddPeerAccessInbound(t *testing.T) {
+ t.Parallel()
+
+ // Create a testing accessMan.
+ a := &accessMan{
+ peerChanInfo: make(map[string]channeldb.ChanCount),
+ peerScores: make(map[string]peerSlotStatus),
+ }
+
+ // Create a testing key.
+ priv, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ pub := priv.PubKey()
+ pubStr := string(pub.SerializeCompressed())
+
+ // Add this peer as an inbound peer with peerStatusRestricted.
+ a.addPeerAccess(pub, peerStatusRestricted, true)
+
+ // Assert the accessMan's internal state.
+ //
+ // We expect to see one peer found in the score map, and one slot is
+ // taken, and this peer is not found in the counts map.
+ require.Len(t, a.peerScores, 1)
+ require.Equal(t, int64(1), a.numRestricted)
+ require.NotContains(t, a.peerChanInfo, pubStr)
+
+ // The peer should be found in the score map.
+ score, ok := a.peerScores[pubStr]
+ require.True(t, ok)
+
+ expecedScore := peerSlotStatus{state: peerStatusRestricted}
+ require.Equal(t, expecedScore, score)
+
+ // Add this peer again, we expect the available slots to stay unchanged.
+ a.addPeerAccess(pub, peerStatusRestricted, true)
+
+ // Assert the internal state is not changed.
+ require.Len(t, a.peerScores, 1)
+ require.Equal(t, int64(1), a.numRestricted)
+ require.NotContains(t, a.peerChanInfo, pubStr)
+
+ // Reset the accessMan.
+ a = &accessMan{
+ peerChanInfo: make(map[string]channeldb.ChanCount),
+ peerScores: make(map[string]peerSlotStatus),
+ }
+
+ // Add this peer as an inbound peer with peerStatusTemporary.
+ a.addPeerAccess(pub, peerStatusTemporary, true)
+
+ // Assert the accessMan's internal state.
+ //
+ // We expect to see one peer found in the score map, and no slot is
+ // taken since this peer is not restricted.
+ require.Len(t, a.peerScores, 1)
+ require.Equal(t, int64(0), a.numRestricted)
+
+ // NOTE: in reality this is not possible as the peer must have been put
+ // into the map `peerChanInfo` before its perm can be upgraded.
+ require.NotContains(t, a.peerChanInfo, pubStr)
+
+ // The peer should be found in the score map.
+ score, ok = a.peerScores[pubStr]
+ require.True(t, ok)
+
+ expecedScore = peerSlotStatus{state: peerStatusTemporary}
+ require.Equal(t, expecedScore, score)
+}
+
+// TestAddPeerAccessOutbound asserts that outbound peer is not restricted and
+// its perm is upgraded when it has peerStatusRestricted.
+func TestAddPeerAccessOutbound(t *testing.T) {
+ t.Parallel()
+
+ // Create a testing accessMan.
+ a := &accessMan{
+ peerChanInfo: make(map[string]channeldb.ChanCount),
+ peerScores: make(map[string]peerSlotStatus),
+ }
+
+ // Create a testing key.
+ priv, err := btcec.NewPrivateKey()
+ require.NoError(t, err)
+ pub := priv.PubKey()
+ pubStr := string(pub.SerializeCompressed())
+
+ // Add this peer as an outbound peer with peerStatusRestricted.
+ a.addPeerAccess(pub, peerStatusRestricted, false)
+
+ // Assert the accessMan's internal state.
+ //
+ // We expect to see one peer found in the score map, and no slot is
+ // taken, and this peer is found in the counts map.
+ require.Len(t, a.peerScores, 1)
+ require.Equal(t, int64(0), a.numRestricted)
+ require.Contains(t, a.peerChanInfo, pubStr)
+
+ // The peer should be found in the score map.
+ score, ok := a.peerScores[pubStr]
+ require.True(t, ok)
+
+ // Its perm should be upgraded to temporary.
+ expecedScore := peerSlotStatus{state: peerStatusTemporary}
+ require.Equal(t, expecedScore, score)
+
+ // The peer should be found in the peer counts map.
+ count, ok := a.peerChanInfo[pubStr]
+ require.True(t, ok)
+
+ // The peer's count should be initialized correctly.
+ require.Zero(t, count.PendingOpenCount)
+ require.False(t, count.HasOpenOrClosedChan)
+
+ // Add this peer again, we expect the available slots to stay unchanged.
+ a.addPeerAccess(pub, peerStatusRestricted, true)
+
+ // Assert the internal state is not changed.
+ require.Len(t, a.peerScores, 1)
+ require.Equal(t, int64(0), a.numRestricted)
+ require.Contains(t, a.peerChanInfo, pubStr)
+
+ // Reset the accessMan.
+ a = &accessMan{
+ peerChanInfo: make(map[string]channeldb.ChanCount),
+ peerScores: make(map[string]peerSlotStatus),
+ }
+
+ // Add this peer as an inbound peer with peerStatusTemporary.
+ a.addPeerAccess(pub, peerStatusTemporary, true)
+
+ // Assert the accessMan's internal state.
+ //
+ // We expect to see one peer found in the score map, and no slot is
+ // taken since this peer is not restricted.
+ require.Len(t, a.peerScores, 1)
+ require.Equal(t, int64(0), a.numRestricted)
+
+ // NOTE: in reality this is not possible as the peer must have been put
+ // into the map `peerChanInfo` before its perm can be upgraded.
+ require.NotContains(t, a.peerChanInfo, pubStr)
+
+ // The peer should be found in the score map.
+ score, ok = a.peerScores[pubStr]
+ require.True(t, ok)
+
+ expecedScore = peerSlotStatus{state: peerStatusTemporary}
+ require.Equal(t, expecedScore, score)
+}
+
+// TestRemovePeerAccess asserts `removePeerAccess` correctly update the
+// accessman's internal state based on the peer's status.
+func TestRemovePeerAccess(t *testing.T) {
+ t.Parallel()
+ ctx := t.Context()
+
+ // Create a testing accessMan.
+ a := &accessMan{
+ peerChanInfo: make(map[string]channeldb.ChanCount),
+ peerScores: make(map[string]peerSlotStatus),
+ }
+
+ // numRestrictedExpected specifies the final value to expect once the
+ // test finishes.
+ var numRestrictedExpected int
+
+ // peer1 exists with an open channel, which should not be removed. Since
+ // it has protected status, the numRestricted should stay unchanged.
+ peer1 := "peer1"
+ a.peerChanInfo[peer1] = channeldb.ChanCount{
+ HasOpenOrClosedChan: true,
+ }
+ peer1Access := peerStatusProtected
+ a.peerScores[peer1] = peerSlotStatus{state: peer1Access}
+
+ // peer2 exists with a pending channel, which should not be removed.
+ // Since it has temporary status, the numRestricted should stay
+ // unchanged.
+ peer2 := "peer2"
+ a.peerChanInfo[peer2] = channeldb.ChanCount{
+ PendingOpenCount: 1,
+ }
+ peer2Access := peerStatusTemporary
+ a.peerScores[peer2] = peerSlotStatus{state: peer2Access}
+
+ // peer3 exists without any channels, which will be removed. Since it
+ // has restricted status, the numRestricted should be decremented.
+ peer3 := "peer3"
+ a.peerChanInfo[peer3] = channeldb.ChanCount{}
+ peer3Access := peerStatusRestricted
+ a.peerScores[peer3] = peerSlotStatus{state: peer3Access}
+ numRestrictedExpected--
+
+ // peer4 exists with a score and a temporary status, which will be
+ // removed.
+ peer4 := "peer4"
+ peer4Access := peerStatusTemporary
+ a.peerScores[peer4] = peerSlotStatus{state: peer4Access}
+
+ // peer5 doesn't exist, removing it will be a NOOP.
+ peer5 := "peer5"
+
+ // We now assert `removePeerAccess` behaves as expected.
+ //
+ // Remove peer1 should change nothing.
+ a.removePeerAccess(ctx, peer1)
+
+ // peer1 should be removed from peerScores but not peerChanInfo.
+ _, found := a.peerScores[peer1]
+ require.False(t, found)
+ _, found = a.peerChanInfo[peer1]
+ require.True(t, found)
+
+ // Remove peer2 should change nothing.
+ a.removePeerAccess(ctx, peer2)
+
+ // peer2 should be removed from peerScores but not peerChanInfo.
+ _, found = a.peerScores[peer2]
+ require.False(t, found)
+ _, found = a.peerChanInfo[peer2]
+ require.True(t, found)
+
+ // Remove peer3 should remove it from the maps.
+ a.removePeerAccess(ctx, peer3)
+
+ // peer3 should be removed from peerScores and peerChanInfo.
+ _, found = a.peerScores[peer3]
+ require.False(t, found)
+ _, found = a.peerChanInfo[peer3]
+ require.False(t, found)
+
+ // Remove peer4 should remove it from the maps.
+ a.removePeerAccess(ctx, peer4)
+
+ // peer4 should be removed from peerScores and NOT be found in
+ // peerChanInfo.
+ _, found = a.peerScores[peer4]
+ require.False(t, found)
+ _, found = a.peerChanInfo[peer4]
+ require.False(t, found)
+
+ // Remove peer5 should be NOOP.
+ a.removePeerAccess(ctx, peer5)
+
+ // peer5 should NOT be found.
+ _, found = a.peerScores[peer5]
+ require.False(t, found)
+ _, found = a.peerChanInfo[peer5]
+ require.False(t, found)
+
+ // Finally, assert the numRestricted is decremented as expected. Given
+ // we only have peer3 which has restricted access, it should decrement
+ // once.
+ //
+ // NOTE: The value is actually negative here, which is allowed in
+ // accessman.
+ require.EqualValues(t, numRestrictedExpected, a.numRestricted)
+}
diff --git a/aezeed/README.md b/aezeed/README.md
new file mode 100644
index 0000000..a1fc217
--- /dev/null
+++ b/aezeed/README.md
@@ -0,0 +1,102 @@
+# aezeed
+
+[In this PR](https://github.com/lightningnetwork/lnd/pull/773) we added a new package implementing the aezeed cipher
+seed scheme (based on [aez](http://web.cs.ucdavis.edu/~rogaway/aez/)).
+
+This new scheme aims to address
+two major features lacking in [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki): versioning, and a
+wallet birthday. The lack a version means that wallets may not
+necessarily know how to re-derive addresses during the recovery
+process. A lack of a birthday means that wallets don’t know how far
+back to look in the chain to ensure that they derive all the proper
+user addresses. Additionally, BIP39 use a very weak [KDF](https://en.wikipedia.org/wiki/Key_derivation_function). We use
+scrypt with modern parameters (n=32768, r=8, p=1). A set of benchmarks has
+been added, on my laptop I get about 100ms per attempt:
+
+```shell
+$ go test -run=XXX -bench=.
+
+goos: linux
+goarch: amd64
+pkg: github.com/lightningnetwork/lnd/aezeed
+BenchmarkTomnemonic-4 20 93280730 ns/op 33559670 B/op 36 allocs/op
+BenchmarkToCipherSeed-4 10 102323892 ns/op 36915684 B/op 41 allocs/op
+PASS
+ok github.com/lightningnetwork/lnd/aezeed 4.168s
+```
+
+Aside from addressing the shortcomings of BIP 39, an aezeed cipher seed
+can both be upgraded, and have its password changed.
+
+Sample seed:
+
+```text
+ability dance scatter raw fly dentist bar nominee exhaust wine snap super cost case coconut ticket spread funny grain chimney aspect business quiz ginger
+```
+
+## Plaintext aezeed encoding
+
+The aezeed scheme addresses these two drawbacks and adds a number of
+desirable features. First, we start with the following plaintext seed:
+
+```text
+1 byte internal version || 2 byte timestamp || 16 bytes of entropy
+```
+
+The version field is for wallets to be able to know how to re-derive
+the keys of the wallet.
+
+The 2 byte timestamp is expressed in Bitcoin Days Genesis, meaning that
+the number of days since the timestamp in Bitcoin’s genesis block. This
+allows us to save space, and also avoid using a wasteful level of
+granularity. This can currently express time up until 2188.
+
+Finally, the entropy is raw entropy that should be used to derive the
+wallet’s HD root.
+
+## aezeed enciphering/deciphering
+
+Next, we’ll take the plaintext seed described above and encipher it to
+procure a final cipher text. We’ll then take this cipher text (the
+_CipherSeed_) and encode that using a 24-word mnemonic. The enciphering
+process takes a user-defined passphrase. If no passphrase is provided,
+then the string “aezeed” will be used.
+
+To encipher a plaintext seed (19 bytes) to arrive at an enciphered
+cipher seed (33 bytes), we apply the following operations:
+
+* First we take the external version and append it to our buffer. The
+external version describes how we encipher. For the first version
+(version 0), we’ll use scrypt(n=32768, r=8, p=1) and aezeed.
+* Next, we’ll use scrypt (with the version 9 params) to generate a
+strong key for encryption. We’ll generate a 32-byte key using 5 bytes
+as a salt. The usage of the salt is meant to make the creation of
+rainbow tables infeasible.
+* Next, the enciphering process. We use aez, modern AEAD with
+nonce-misuse resistance properties. The important trait we exploit is
+that it’s an arbitrary input length block cipher. Additionally, it
+has what’s essentially a configurable MAC size. In our scheme we’ll use
+a value of 8, which acts as a 64-bit checksum. We’ll encrypt with our
+generated seed, and use an AD of (version || salt).
+* Finally, we’ll encode this 33-byte cipher text using the default
+word list of BIP 39 to produce 24 English words.
+
+## Properties of the aezeed cipher seed
+
+The aezeed cipher seed scheme has a few cool properties, notably:
+
+* The mnemonic itself is a cipher text, meaning leaving it in
+plaintext is advisable if the user also sets a passphrase. This is in
+contrast to BIP 39 where the mnemonic alone (without a passphrase) may
+be sufficient to steal funds.
+* A cipherseed can be modified to change the passphrase. This
+means that if the users wants a stronger passphrase, they can decipher
+(with the old passphrase), then encipher (with a new passphrase).
+Compared to BIP 39, where if the users used a passphrase, since the
+mapping is one way, they can’t change the passphrase of their existing
+HD key chain.
+* A cipher seed can be upgraded. Since we have an external version,
+offline tools can be provided to decipher using the old params, and
+encipher using the new params. In the future if we change ciphers,
+change scrypt, or just the parameters of scrypt, then users can easily
+upgrade their seed with an offline tool.
diff --git a/aezeed/bench_test.go b/aezeed/bench_test.go
new file mode 100644
index 0000000..3fa0edb
--- /dev/null
+++ b/aezeed/bench_test.go
@@ -0,0 +1,69 @@
+package aezeed
+
+import (
+ "testing"
+ "time"
+)
+
+var (
+ mnemonic Mnemonic
+
+ seed *CipherSeed
+)
+
+// BenchmarkTomnemonic benchmarks the process of converting a cipher seed
+// (given the salt), to an enciphered mnemonic.
+func BenchmarkTomnemonic(b *testing.B) {
+ scryptN = 32768
+ scryptR = 8
+ scryptP = 1
+
+ pass := []byte("1234567890abcedfgh")
+ cipherSeed, err := New(0, nil, time.Now())
+ if err != nil {
+ b.Fatalf("unable to create seed: %v", err)
+ }
+
+ var r Mnemonic
+ for i := 0; i < b.N; i++ {
+ r, err = cipherSeed.ToMnemonic(pass)
+ if err != nil {
+ b.Fatalf("unable to encipher: %v", err)
+ }
+ }
+
+ b.ReportAllocs()
+
+ mnemonic = r
+}
+
+// BenchmarkToCipherSeed benchmarks the process of deciphering an existing
+// enciphered mnemonic.
+func BenchmarkToCipherSeed(b *testing.B) {
+ scryptN = 32768
+ scryptR = 8
+ scryptP = 1
+
+ pass := []byte("1234567890abcedfgh")
+ cipherSeed, err := New(0, nil, time.Now())
+ if err != nil {
+ b.Fatalf("unable to create seed: %v", err)
+ }
+
+ mnemonic, err := cipherSeed.ToMnemonic(pass)
+ if err != nil {
+ b.Fatalf("unable to create mnemonic: %v", err)
+ }
+
+ var s *CipherSeed
+ for i := 0; i < b.N; i++ {
+ s, err = mnemonic.ToCipherSeed(pass)
+ if err != nil {
+ b.Fatalf("unable to decipher: %v", err)
+ }
+ }
+
+ b.ReportAllocs()
+
+ seed = s
+}
diff --git a/aezeed/cipherseed.go b/aezeed/cipherseed.go
new file mode 100644
index 0000000..4e05b25
--- /dev/null
+++ b/aezeed/cipherseed.go
@@ -0,0 +1,603 @@
+package aezeed
+
+import (
+ "bytes"
+ "crypto/rand"
+ "encoding/binary"
+ "hash/crc32"
+ "io"
+ "time"
+
+ "github.com/Yawning/aez"
+ "github.com/kkdai/bstream"
+ "golang.org/x/crypto/scrypt"
+)
+
+const (
+ // CipherSeedVersion is the current version of the aezeed scheme as
+ // defined in this package. This version indicates the following
+ // parameters for the deciphered cipher seed: a 1 byte version, 2 bytes
+ // for the Bitcoin Days Genesis timestamp, and 16 bytes for entropy. It
+ // also governs how the cipher seed should be enciphered. In this
+ // version we take the deciphered seed, create a 5 byte salt, use that
+ // with an optional passphrase to generate a 32-byte key (via scrypt),
+ // then encipher with aez (using the salt and version as AD). The final
+ // enciphered seed is: version || ciphertext || salt.
+ CipherSeedVersion uint8 = 0
+
+ // DecipheredCipherSeedSize is the size of the plaintext seed resulting
+ // from deciphering the cipher seed. The size consists of the
+ // following:
+ //
+ // * 1 byte version || 2 bytes timestamp || 16 bytes of entropy.
+ //
+ // The version is used by wallets to know how to re-derive relevant
+ // addresses, the 2 byte timestamp a BDG (Bitcoin Days Genesis) offset,
+ // and finally, the 16 bytes to be used to generate the HD wallet seed.
+ DecipheredCipherSeedSize = 19
+
+ // EncipheredCipherSeedSize is the size of the fully encoded+enciphered
+ // cipher seed. We first obtain the enciphered plaintext seed by
+ // carrying out the enciphering as governed in the current version. We
+ // then take that enciphered seed (now 19+4=23 bytes due to ciphertext
+ // expansion, essentially a checksum) and prepend a version, then
+ // append the salt, and then take a checksum of everything. The
+ // checksum allows us to verify that the user input the correct set of
+ // words, then we can verify the passphrase due to the internal MAC
+ // equiv. The final breakdown is:
+ //
+ // * 1 byte version || 23 byte enciphered seed || 5 byte salt || 4 byte checksum
+ //
+ // With CipherSeedVersion we encipher as follows: we use
+ // scrypt(n=32768, r=8, p=1) to derive a 32-byte key from an optional
+ // user passphrase. We then encipher the plaintext seed using a value
+ // of tau (with aez) of 8-bytes (so essentially a 32-bit MAC). When
+ // enciphering, we include the version and scrypt salt as the AD. This
+ // gives us a total of 33 bytes. These 33 bytes fit cleanly into 24
+ // mnemonic words.
+ EncipheredCipherSeedSize = 33
+
+ // CipherTextExpansion is the number of bytes that will be added as
+ // redundancy for the enciphering scheme implemented by aez. This can
+ // be seen as the size of the equivalent MAC.
+ CipherTextExpansion = 4
+
+ // EntropySize is the number of bytes of entropy we'll use to generate
+ // the seed.
+ EntropySize = 16
+
+ // NumMnemonicWords is the number of words that an encoded cipher seed
+ // will result in.
+ NumMnemonicWords = 24
+
+ // SaltSize is the size of the salt we'll generate to use with scrypt
+ // to generate a key for use within aez from the user's passphrase. The
+ // role of the salt is to make the creation of rainbow tables
+ // infeasible.
+ SaltSize = 5
+
+ // adSize is the size of the encoded associated data that will be
+ // passed into aez when enciphering and deciphering the seed. The AD
+ // itself (associated data) is just the cipher seed version and salt.
+ adSize = 6
+
+ // checkSumSize is the size of the checksum applied to the final
+ // encoded ciphertext.
+ checkSumSize = 4
+
+ // keyLen is the size of the key that we'll use for encryption with
+ // aez.
+ keyLen = 32
+
+ // BitsPerWord is the number of bits each word in the wordlist encodes.
+ // We encode our mnemonic using 24 words, so 264 bits (33 bytes).
+ BitsPerWord = 11
+
+ // saltOffset is the index within an enciphered cipher seed that marks
+ // the start of the salt.
+ saltOffset = EncipheredCipherSeedSize - checkSumSize - SaltSize
+
+ // checkSumSize is the index within an enciphered cipher seed that
+ // marks the start of the checksum.
+ checkSumOffset = EncipheredCipherSeedSize - checkSumSize
+)
+
+var (
+ // Below at the default scrypt parameters that are tied to cipher seed
+ // version zero.
+ scryptN = 32768
+ scryptR = 8
+ scryptP = 1
+
+ // crcTable is a table that presents the polynomial we'll use for
+ // computing our checksum.
+ crcTable = crc32.MakeTable(crc32.Castagnoli)
+
+ // defaultPassphrase is the default passphrase that will be used for
+ // encryption in the case that the user chooses not to specify their
+ // own passphrase.
+ defaultPassphrase = []byte("aezeed")
+)
+
+var (
+ // BitcoinGenesisDate is the timestamp of Bitcoin's genesis block.
+ // We'll use this value in order to create a compact birthday for the
+ // seed. The birthday will be interested as the number of days since
+ // the genesis date. We refer to this time period as ABE (after Bitcoin
+ // era).
+ BitcoinGenesisDate = time.Unix(1231006505, 0)
+)
+
+// SeedOptions is a type that holds options that configure the generation of a
+// new cipher seed.
+type SeedOptions struct {
+ // randomnessSource is the source of randomness that is used to generate
+ // the salt that is used for encrypting the seed.
+ randomnessSource io.Reader
+}
+
+// DefaultOptions returns the default seed options.
+func DefaultOptions() *SeedOptions {
+ return &SeedOptions{
+ randomnessSource: rand.Reader,
+ }
+}
+
+// SeedOptionModifier is a function signature for modifying the default
+// SeedOptions.
+type SeedOptionModifier func(*SeedOptions)
+
+// WithRandomnessSource returns an option modifier that replaces the default
+// randomness source with the given reader.
+func WithRandomnessSource(src io.Reader) SeedOptionModifier {
+ return func(opts *SeedOptions) {
+ opts.randomnessSource = src
+ }
+}
+
+// CipherSeed is a fully decoded instance of the aezeed scheme. At a high
+// level, the encoded cipher seed is the enciphering of: a version byte, a set
+// of bytes for a timestamp, the entropy which will be used to directly
+// construct the HD seed, and finally a checksum over the rest. This scheme was
+// created as the widely used schemes in the space lack two critical traits: a
+// version byte, and a birthday timestamp. The version allows us to modify the
+// details of the scheme in the future, and the birthday gives wallets a limit
+// of how far back in the chain they'll need to start scanning. We also add an
+// external version to the enciphering plaintext seed. With this addition,
+// seeds are able to be "upgraded" (to diff params, or entirely diff crypt),
+// while maintaining the semantics of the plaintext seed.
+//
+// The core of the scheme is the usage of aez to carefully control the size of
+// the final encrypted seed. With the current parameters, this scheme can be
+// encoded using a 24 word mnemonic. We use 4 bytes of ciphertext expansion
+// when enciphering the raw seed, giving us the equivalent of 40-bit MAC (as we
+// check for a particular seed version). Using the external 4 byte checksum,
+// we're able to ensure that the user input the correct set of words. Finally,
+// the password in the scheme is optional. If not specified, "aezeed" will be
+// used as the password. Otherwise, the addition of the password means that
+// users can encrypt the raw "plaintext" seed under distinct passwords to
+// produce unique mnemonic phrases.
+type CipherSeed struct {
+ // InternalVersion is the version of the plaintext cipher seed. This is
+ // to be used by wallets to determine if the seed version is compatible
+ // with the derivation schemes they know.
+ InternalVersion uint8
+
+ // Birthday is the time that the seed was created. This is expressed as
+ // the number of days since the timestamp in the Bitcoin genesis block.
+ // We use days as seconds gives us wasted granularity. The oldest seed
+ // that we can encode using this format is through the date 2188.
+ Birthday uint16
+
+ // Entropy is a set of bytes generated via a CSPRNG. This is the value
+ // that should be used to directly generate the HD root, as defined
+ // within BIP0032.
+ Entropy [EntropySize]byte
+
+ // salt is the salt that was used to generate the key from the user's
+ // specified passphrase.
+ salt [SaltSize]byte
+}
+
+// New generates a new CipherSeed instance from an optional source of entropy.
+// If the entropy isn't provided, then a set of random bytes will be used in
+// place. The final fixed argument should be the time at which the seed was
+// created, followed by optional seed option modifiers.
+func New(internalVersion uint8, entropy *[EntropySize]byte,
+ now time.Time, modifiers ...SeedOptionModifier) (*CipherSeed, error) {
+
+ opts := DefaultOptions()
+ for _, modifier := range modifiers {
+ modifier(opts)
+ }
+
+ // If a set of entropy wasn't provided, then we'll read a set of bytes
+ // from the randomness source provided (which by default is the system's
+ // CSPRNG).
+ var seed [EntropySize]byte
+ if entropy == nil {
+ if _, err := opts.randomnessSource.Read(seed[:]); err != nil {
+ return nil, err
+ }
+ } else {
+ // Otherwise, we'll copy the set of bytes.
+ copy(seed[:], entropy[:])
+ }
+
+ // To compute our "birthday", we'll first use the current time, then
+ // subtract that from the Bitcoin Genesis Date. We'll then convert that
+ // value to days.
+ birthday := uint16(now.Sub(BitcoinGenesisDate) / (time.Hour * 24))
+
+ c := &CipherSeed{
+ InternalVersion: internalVersion,
+ Birthday: birthday,
+ Entropy: seed,
+ }
+
+ // Next, we'll read a random salt that will be used with scrypt to
+ // eventually derive our key.
+ if _, err := opts.randomnessSource.Read(c.salt[:]); err != nil {
+ return nil, err
+ }
+
+ return c, nil
+}
+
+// encode attempts to encode the target cipherSeed into the passed io.Writer
+// instance.
+func (c *CipherSeed) encode(w io.Writer) error {
+ err := binary.Write(w, binary.BigEndian, c.InternalVersion)
+ if err != nil {
+ return err
+ }
+
+ if err := binary.Write(w, binary.BigEndian, c.Birthday); err != nil {
+ return err
+ }
+
+ if _, err := w.Write(c.Entropy[:]); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// decode attempts to decode an encoded cipher seed instance into the target
+// CipherSeed struct.
+func (c *CipherSeed) decode(r io.Reader) error {
+ err := binary.Read(r, binary.BigEndian, &c.InternalVersion)
+ if err != nil {
+ return err
+ }
+
+ if err := binary.Read(r, binary.BigEndian, &c.Birthday); err != nil {
+ return err
+ }
+
+ if _, err := io.ReadFull(r, c.Entropy[:]); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// encodeAD returns the fully encoded associated data for use when performing
+// our current enciphering operation. The AD is: version || salt.
+func encodeAD(version uint8, salt [SaltSize]byte) [adSize]byte {
+ var ad [adSize]byte
+ ad[0] = version
+ copy(ad[1:], salt[:])
+
+ return ad
+}
+
+// extractAD extracts an associated data from a fully encoded and enciphered
+// cipher seed. This is to be used when attempting to decrypt an enciphered
+// cipher seed.
+func extractAD(encipheredSeed [EncipheredCipherSeedSize]byte) [adSize]byte {
+ var ad [adSize]byte
+ ad[0] = encipheredSeed[0]
+
+ copy(ad[1:], encipheredSeed[saltOffset:checkSumOffset])
+
+ return ad
+}
+
+// encipher takes a fully populated cipher seed instance, and enciphers the
+// encoded seed, then appends a randomly generated seed used to stretch the
+// passphrase out into an appropriate key, then computes a checksum over the
+// preceding.
+func (c *CipherSeed) encipher(pass []byte) ([EncipheredCipherSeedSize]byte,
+ error) {
+
+ var cipherSeedBytes [EncipheredCipherSeedSize]byte
+
+ // If the passphrase wasn't provided, then we'll use the string
+ // "aezeed" in place.
+ passphrase := pass
+ if len(passphrase) == 0 {
+ passphrase = defaultPassphrase
+ }
+
+ // With our salt pre-generated, we'll now run the password through a
+ // KDF to obtain the key we'll use for encryption.
+ key, err := scrypt.Key(
+ passphrase, c.salt[:], scryptN, scryptR, scryptP, keyLen,
+ )
+ if err != nil {
+ return cipherSeedBytes, err
+ }
+
+ // Next, we'll encode the serialized plaintext cipher seed into a buffer
+ // that we'll use for encryption.
+ var seedBytes bytes.Buffer
+ if err := c.encode(&seedBytes); err != nil {
+ return cipherSeedBytes, err
+ }
+
+ // With our plaintext seed encoded, we'll now construct the AD that
+ // will be passed to the encryption operation. This ensures to
+ // authenticate both the salt and the external version.
+ ad := encodeAD(CipherSeedVersion, c.salt)
+
+ // With all items assembled, we'll now encipher the plaintext seed
+ // with our AD, key, and MAC size.
+ cipherSeed := seedBytes.Bytes()
+ cipherText := aez.Encrypt(
+ key, nil, [][]byte{ad[:]}, CipherTextExpansion, cipherSeed, nil,
+ )
+
+ // Finally, we'll pack the {version || ciphertext || salt || checksum}
+ // seed into a byte slice for encoding as a mnemonic.
+ cipherSeedBytes[0] = CipherSeedVersion
+ copy(cipherSeedBytes[1:saltOffset], cipherText)
+ copy(cipherSeedBytes[saltOffset:], c.salt[:])
+
+ // With the seed mostly assembled, we'll now compute a checksum all the
+ // contents.
+ checkSum := crc32.Checksum(cipherSeedBytes[:checkSumOffset], crcTable)
+
+ // With our checksum computed, we can finish encoding the full cipher
+ // seed.
+ var checkSumBytes [4]byte
+ binary.BigEndian.PutUint32(checkSumBytes[:], checkSum)
+ copy(cipherSeedBytes[checkSumOffset:], checkSumBytes[:])
+
+ return cipherSeedBytes, nil
+}
+
+// cipherTextToMnemonic converts the aez ciphertext appended with the salt to a
+// 24-word mnemonic pass phrase.
+func cipherTextToMnemonic(cipherText [EncipheredCipherSeedSize]byte) (Mnemonic,
+ error) {
+
+ var words [NumMnemonicWords]string
+
+ // First, we'll convert the ciphertext itself into a bitstream for easy
+ // manipulation.
+ cipherBits := bstream.NewBStreamReader(cipherText[:])
+
+ // With our bitstream obtained, we'll read 11 bits at a time, then use
+ // that to index into our word list to obtain the next word.
+ for i := 0; i < NumMnemonicWords; i++ {
+ index, err := cipherBits.ReadBits(BitsPerWord)
+ if err != nil {
+ return Mnemonic{}, err
+ }
+
+ words[i] = DefaultWordList[index]
+ }
+
+ return words, nil
+}
+
+// ToMnemonic maps the final enciphered cipher seed to a human-readable 24-word
+// mnemonic phrase. The password is optional, as if it isn't specified aezeed
+// will be used in its place.
+func (c *CipherSeed) ToMnemonic(pass []byte) (Mnemonic, error) {
+ // First, we'll convert the valid seed triple into an aez cipher text
+ // with our KDF salt appended to it.
+ cipherText, err := c.encipher(pass)
+ if err != nil {
+ return Mnemonic{}, err
+ }
+
+ // Now that we have our cipher text, we'll convert it into a mnemonic
+ // phrase.
+ return cipherTextToMnemonic(cipherText)
+}
+
+// Encipher maps the cipher seed to an aez ciphertext using an optional
+// passphrase.
+func (c *CipherSeed) Encipher(pass []byte) ([EncipheredCipherSeedSize]byte,
+ error) {
+
+ return c.encipher(pass)
+}
+
+// BirthdayTime returns the cipher seed's internal birthday format as a native
+// golang Time struct.
+func (c *CipherSeed) BirthdayTime() time.Time {
+ offset := time.Duration(c.Birthday) * 24 * time.Hour
+ return BitcoinGenesisDate.Add(offset)
+}
+
+// Mnemonic is a 24-word passphrase as of cipher seed version zero. This
+// passphrase encodes an encrypted seed triple (version, birthday, entropy).
+// Additionally, we also encode the salt used with scrypt to derive the key
+// that the cipher text is encrypted with, and the version which tells us how
+// to decipher the seed.
+type Mnemonic [NumMnemonicWords]string
+
+// mnemonicToCipherText converts a 24-word mnemonic phrase into a 33 byte
+// cipher text.
+//
+// NOTE: This assumes that all words have already been checked to be amongst
+// our word list.
+func mnemonicToCipherText(mnemonic *Mnemonic) [EncipheredCipherSeedSize]byte {
+ var cipherText [EncipheredCipherSeedSize]byte
+
+ // We'll now perform the reverse mapping to that of
+ // cipherTextToMnemonic: we'll get the index of the word, then write
+ // out that index to the bit stream.
+ cipherBits := bstream.NewBStreamWriter(EncipheredCipherSeedSize)
+ for _, word := range mnemonic {
+ // Using the reverse word map, we'll locate the index of this
+ // word within the word list.
+ index := uint64(ReverseWordMap[word])
+
+ // With the index located, we'll now write this out to the
+ // bitstream, appending to what's already there.
+ cipherBits.WriteBits(index, BitsPerWord)
+ }
+
+ copy(cipherText[:], cipherBits.Bytes())
+
+ return cipherText
+}
+
+// ToCipherSeed attempts to map the mnemonic to the original cipher text byte
+// slice. Then we'll attempt to decrypt the ciphertext using aez with the
+// passed passphrase, using the last 5 bytes of the ciphertext as a salt for
+// the KDF.
+func (m *Mnemonic) ToCipherSeed(pass []byte) (*CipherSeed, error) {
+ // First, we'll attempt to decipher the mnemonic by mapping back into
+ // our byte slice and applying our deciphering scheme.
+ plainSeed, salt, err := m.Decipher(pass)
+ if err != nil {
+ return nil, err
+ }
+
+ // If decryption was successful, then we'll decode into a fresh
+ // CipherSeed struct.
+ c := CipherSeed{
+ salt: salt,
+ }
+ if err := c.decode(bytes.NewReader(plainSeed[:])); err != nil {
+ return nil, err
+ }
+
+ return &c, nil
+}
+
+// decipherCipherSeed attempts to decipher the passed cipher seed ciphertext
+// using the passed passphrase. This function is the opposite of
+// the encipher method.
+func decipherCipherSeed(cipherSeedBytes [EncipheredCipherSeedSize]byte,
+ pass []byte) ([DecipheredCipherSeedSize]byte, [SaltSize]byte, error) {
+
+ var (
+ plainSeed [DecipheredCipherSeedSize]byte
+ salt [SaltSize]byte
+ )
+
+ // Before we do anything, we'll ensure that the version is one that we
+ // understand. Otherwise, we won't be able to decrypt, or even parse
+ // the cipher seed.
+ if cipherSeedBytes[0] != CipherSeedVersion {
+ return plainSeed, salt, ErrIncorrectVersion
+ }
+
+ // Next, we'll slice off the salt from the pass cipher seed, then
+ // snip off the end of the cipher seed, ignoring the version, and
+ // finally the checksum.
+ copy(salt[:], cipherSeedBytes[saltOffset:saltOffset+SaltSize])
+ cipherSeed := cipherSeedBytes[1:saltOffset]
+ checksum := cipherSeedBytes[checkSumOffset:]
+
+ // Before we perform any crypto operations, we'll re-create and verify
+ // the checksum to ensure that the user input the proper set of words.
+ freshChecksum := crc32.Checksum(
+ cipherSeedBytes[:checkSumOffset], crcTable,
+ )
+ if freshChecksum != binary.BigEndian.Uint32(checksum) {
+ return plainSeed, salt, ErrIncorrectMnemonic
+ }
+
+ // With the salt separated from the cipher text, we'll now obtain the
+ // key used for encryption.
+ key, err := scrypt.Key(pass, salt[:], scryptN, scryptR, scryptP, keyLen)
+ if err != nil {
+ return plainSeed, salt, err
+ }
+
+ // We'll also extract the AD that will be required to properly pass the
+ // MAC check.
+ ad := extractAD(cipherSeedBytes)
+
+ // With the key, we'll attempt to decrypt the plaintext. If the
+ // ciphertext was altered, or the passphrase is incorrect, then we'll
+ // error out.
+ plainSeedBytes, ok := aez.Decrypt(
+ key, nil, [][]byte{ad[:]}, CipherTextExpansion, cipherSeed, nil,
+ )
+ if !ok {
+ return plainSeed, salt, ErrInvalidPass
+ }
+ copy(plainSeed[:], plainSeedBytes)
+
+ return plainSeed, salt, nil
+
+}
+
+// Decipher attempts to decipher the encoded mnemonic by first mapping to the
+// original ciphertext, then applying our deciphering scheme. ErrInvalidPass
+// will be returned if the passphrase is incorrect.
+func (m *Mnemonic) Decipher(pass []byte) ([DecipheredCipherSeedSize]byte,
+ [SaltSize]byte, error) {
+
+ // Before we attempt to map the mnemonic back to the original
+ // ciphertext, we'll ensure that all the word are actually a part of
+ // the current default word list.
+ wordDict := make(map[string]struct{}, len(DefaultWordList))
+ for _, word := range DefaultWordList {
+ wordDict[word] = struct{}{}
+ }
+
+ for i, word := range m {
+ if _, ok := wordDict[word]; !ok {
+ emptySeed := [DecipheredCipherSeedSize]byte{}
+ return emptySeed, [SaltSize]byte{},
+ ErrUnknownMnemonicWord{
+ Word: word,
+ Index: uint8(i),
+ }
+ }
+ }
+
+ // If the passphrase wasn't provided, then we'll use the string
+ // "aezeed" in place.
+ passphrase := pass
+ if len(passphrase) == 0 {
+ passphrase = defaultPassphrase
+ }
+
+ // Next, we'll map the mnemonic phrase back into the original cipher
+ // text.
+ cipherText := mnemonicToCipherText(m)
+
+ // Finally, we'll attempt to decipher the enciphered seed. The result
+ // will be the raw seed minus the ciphertext expansion, external
+ // version, and salt.
+ return decipherCipherSeed(cipherText, passphrase)
+}
+
+// ChangePass takes an existing mnemonic, and passphrase for said mnemonic and
+// re-enciphers the plaintext cipher seed into a brand-new mnemonic. This can
+// be used to allow users to re-encrypt the same seed with multiple pass
+// phrases, or just change the passphrase on an existing seed.
+func (m *Mnemonic) ChangePass(oldPass, newPass []byte) (Mnemonic, error) {
+ var newMnemonic Mnemonic
+
+ // First, we'll try to decrypt the current mnemonic using the existing
+ // passphrase. If this fails, then we can't proceed any further.
+ cipherSeed, err := m.ToCipherSeed(oldPass)
+ if err != nil {
+ return newMnemonic, err
+ }
+
+ // If the deciphering was successful, then we'll now re-encipher using
+ // the new user provided passphrase.
+ return cipherSeed.ToMnemonic(newPass)
+}
diff --git a/aezeed/cipherseed_integration.go b/aezeed/cipherseed_integration.go
new file mode 100644
index 0000000..50d29cb
--- /dev/null
+++ b/aezeed/cipherseed_integration.go
@@ -0,0 +1,13 @@
+//go:build integration
+
+package aezeed
+
+import "github.com/btcsuite/btcwallet/waddrmgr"
+
+func init() {
+ // For the purposes of our itest, we'll crank down the scrypt params a
+ // bit.
+ scryptN = waddrmgr.FastScryptOptions.N
+ scryptR = waddrmgr.FastScryptOptions.R
+ scryptP = waddrmgr.FastScryptOptions.P
+}
diff --git a/aezeed/cipherseed_test.go b/aezeed/cipherseed_test.go
new file mode 100644
index 0000000..158b69b
--- /dev/null
+++ b/aezeed/cipherseed_test.go
@@ -0,0 +1,563 @@
+package aezeed
+
+import (
+ "bytes"
+ "math/rand"
+ "testing"
+ "testing/quick"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+// TestVector defines the values that are used to create a fully initialized
+// aezeed mnemonic seed and the expected values that should be calculated.
+type TestVector struct {
+ version uint8
+ time time.Time
+ entropy [EntropySize]byte
+ salt [SaltSize]byte
+ password []byte
+ expectedMnemonic [NumMnemonicWords]string
+ expectedBirthday uint16
+}
+
+var (
+ testEntropy = [EntropySize]byte{
+ 0x81, 0xb6, 0x37, 0xd8,
+ 0x63, 0x59, 0xe6, 0x96,
+ 0x0d, 0xe7, 0x95, 0xe4,
+ 0x1e, 0x0b, 0x4c, 0xfd,
+ }
+ testSalt = [SaltSize]byte{
+ 0x73, 0x61, 0x6c, 0x74, 0x31, // equal to "salt1"
+ }
+ version0TestVectors = []TestVector{{
+ version: 0,
+ time: BitcoinGenesisDate,
+ entropy: testEntropy,
+ salt: testSalt,
+ password: []byte{},
+ expectedMnemonic: [NumMnemonicWords]string{
+ "ability", "liquid", "travel", "stem", "barely", "drastic",
+ "pact", "cupboard", "apple", "thrive", "morning", "oak",
+ "feature", "tissue", "couch", "old", "math", "inform",
+ "success", "suggest", "drink", "motion", "know", "royal",
+ },
+ expectedBirthday: 0,
+ }, {
+ version: 0,
+ time: time.Unix(1521799345, 0), // 03/23/2018 @ 10:02am (UTC)
+ entropy: testEntropy,
+ salt: testSalt,
+ password: []byte("!very_safe_55345_password*"),
+ expectedMnemonic: [NumMnemonicWords]string{
+ "able", "tree", "stool", "crush", "transfer", "cloud",
+ "cross", "three", "profit", "outside", "hen", "citizen",
+ "plate", "ride", "require", "leg", "siren", "drum",
+ "success", "suggest", "drink", "require", "fiscal", "upgrade",
+ },
+ expectedBirthday: 3365,
+ }}
+)
+
+func assertCipherSeedEqual(t *testing.T, cipherSeed *CipherSeed,
+ cipherSeed2 *CipherSeed) {
+
+ require.Equal(
+ t, cipherSeed.InternalVersion, cipherSeed2.InternalVersion,
+ "internal version",
+ )
+ require.Equal(t, cipherSeed.Birthday, cipherSeed2.Birthday, "birthday")
+ require.Equal(t, cipherSeed.Entropy, cipherSeed2.Entropy, "entropy")
+ require.Equal(t, cipherSeed.salt, cipherSeed2.salt, "salt")
+}
+
+// TestAezeedVersion0TestVectors tests some fixed test vector values against
+// the expected mnemonic words.
+func TestAezeedVersion0TestVectors(t *testing.T) {
+ t.Parallel()
+
+ // To minimize the number of tests that need to be run, go through all
+ // test vectors in the same test and also check the birthday calculation
+ // while we're at it.
+ for _, v := range version0TestVectors {
+ // First, we create new cipher seed with the given values
+ // from the test vector.
+ cipherSeed, err := New(v.version, &v.entropy, v.time)
+ require.NoError(t, err)
+
+ // Then we need to set the salt to the pre-defined value,
+ // otherwise we'll end up with randomness in our mnemonics.
+ cipherSeed.salt = v.salt
+
+ // Now that the seed has been created, we'll attempt to convert
+ // it to a valid mnemonic.
+ mnemonic, err := cipherSeed.ToMnemonic(v.password)
+ require.NoError(t, err)
+
+ // Finally we compare the generated mnemonic and birthday to the
+ // expected value.
+ require.Equal(t, v.expectedMnemonic[:], mnemonic[:])
+ require.Equal(t, v.expectedBirthday, cipherSeed.Birthday)
+ }
+}
+
+// TestWithRandomnessSource tests that seed generation is fully deterministic
+// when a custom static randomness source is provided.
+func TestWithRandomnessSource(t *testing.T) {
+ sourceData := append([]byte{}, testEntropy[:]...)
+ sourceData = append(sourceData, testSalt[:]...)
+ src := bytes.NewReader(sourceData)
+
+ // First, we create new cipher seed with the given values from the test
+ // vector but with no entropy.
+ v := version0TestVectors[0]
+ cipherSeed, err := New(
+ v.version, nil, v.time, WithRandomnessSource(src),
+ )
+ require.NoError(t, err)
+
+ // The salt should be set to our test salt.
+ require.Equal(t, testSalt, cipherSeed.salt)
+
+ // Now that the seed has been created, we'll attempt to convert it to a
+ // valid mnemonic.
+ mnemonic, err := cipherSeed.ToMnemonic(v.password)
+ require.NoError(t, err)
+
+ // Finally, we compare the generated mnemonic and birthday to the
+ // expected value.
+ require.Equal(t, v.expectedMnemonic[:], mnemonic[:])
+ require.Equal(t, v.expectedBirthday, cipherSeed.Birthday)
+}
+
+// TestEmptyPassphraseDerivation tests that the aezeed scheme is able to derive
+// a proper mnemonic, and decipher that mnemonic when the user uses an empty
+// passphrase.
+func TestEmptyPassphraseDerivation(t *testing.T) {
+ t.Parallel()
+
+ // Our empty passphrase...
+ pass := []byte{}
+
+ // We'll now create a new cipher seed with an internal version of zero
+ // to simulate a wallet that just adopted the scheme.
+ cipherSeed, err := New(0, &testEntropy, time.Now())
+ require.NoError(t, err)
+
+ // Now that the seed has been created, we'll attempt to convert it to a
+ // valid mnemonic.
+ mnemonic, err := cipherSeed.ToMnemonic(pass)
+ require.NoError(t, err)
+
+ // Next, we'll try to decrypt the mnemonic with the passphrase that we
+ // used.
+ cipherSeed2, err := mnemonic.ToCipherSeed(pass)
+ require.NoError(t, err)
+
+ // Finally, we'll ensure that the uncovered cipher seed matches
+ // precisely.
+ assertCipherSeedEqual(t, cipherSeed, cipherSeed2)
+}
+
+// TestManualEntropyGeneration tests that if the user doesn't provide a source
+// of entropy, then we do so ourselves.
+func TestManualEntropyGeneration(t *testing.T) {
+ t.Parallel()
+
+ // Our empty passphrase...
+ pass := []byte{}
+
+ // We'll now create a new cipher seed with an internal version of zero
+ // to simulate a wallet that just adopted the scheme.
+ cipherSeed, err := New(0, nil, time.Now())
+ require.NoError(t, err)
+
+ // Now that the seed has been created, we'll attempt to convert it to a
+ // valid mnemonic.
+ mnemonic, err := cipherSeed.ToMnemonic(pass)
+ require.NoError(t, err)
+
+ // Next, we'll try to decrypt the mnemonic with the passphrase that we
+ // used.
+ cipherSeed2, err := mnemonic.ToCipherSeed(pass)
+ require.NoError(t, err)
+
+ // Finally, we'll ensure that the uncovered cipher seed matches
+ // precisely.
+ assertCipherSeedEqual(t, cipherSeed, cipherSeed2)
+}
+
+// TestInvalidPassphraseRejection tests if a caller attempts to use the
+// incorrect passphrase for an enciphered seed, then the proper error is
+// returned.
+func TestInvalidPassphraseRejection(t *testing.T) {
+ t.Parallel()
+
+ // First, we'll generate a new cipher seed with a test passphrase.
+ pass := []byte("test")
+ cipherSeed, err := New(0, &testEntropy, time.Now())
+ require.NoError(t, err)
+
+ // Now that we have our cipher seed, we'll encipher it and request a
+ // mnemonic that we can use to recover later.
+ mnemonic, err := cipherSeed.ToMnemonic(pass)
+ require.NoError(t, err)
+
+ // If we try to decipher with the wrong passphrase, we should get the
+ // proper error.
+ wrongPass := []byte("kek")
+ _, err = mnemonic.ToCipherSeed(wrongPass)
+ require.Equal(t, ErrInvalidPass, err)
+}
+
+// TestRawEncipherDecipher tests that callers are able to use the raw methods
+// to map between ciphertext and the raw plaintext deciphered seed.
+func TestRawEncipherDecipher(t *testing.T) {
+ t.Parallel()
+
+ // First, we'll generate a new cipher seed with a test passphrase.
+ pass := []byte("test")
+ cipherSeed, err := New(0, &testEntropy, time.Now())
+ require.NoError(t, err)
+
+ // With the cipher seed obtained, we'll now use the raw encipher method
+ // to obtain our final cipher text.
+ cipherText, err := cipherSeed.Encipher(pass)
+ require.NoError(t, err)
+
+ mnemonic, err := cipherTextToMnemonic(cipherText)
+ require.NoError(t, err)
+
+ // Now that we have the ciphertext (mapped to the mnemonic), we'll
+ // attempt to decipher it raw using the user's passphrase.
+ plainSeedBytes, salt, err := mnemonic.Decipher(pass)
+ require.NoError(t, err)
+ require.Equal(t, cipherSeed.salt, salt)
+
+ // If we deserialize the plaintext seed bytes, it should exactly match
+ // the original cipher seed.
+ newSeed := CipherSeed{
+ salt: salt,
+ }
+ err = newSeed.decode(bytes.NewReader(plainSeedBytes[:]))
+ require.NoError(t, err)
+
+ assertCipherSeedEqual(t, cipherSeed, &newSeed)
+}
+
+// TestInvalidExternalVersion tests that if we present a ciphertext with the
+// incorrect version to decipherCipherSeed, then it fails with the expected
+// error.
+func TestInvalidExternalVersion(t *testing.T) {
+ t.Parallel()
+
+ // First, we'll generate a new cipher seed.
+ cipherSeed, err := New(0, &testEntropy, time.Now())
+ require.NoError(t, err)
+
+ // With the cipher seed obtained, we'll now use the raw encipher method
+ // to obtain our final cipher text.
+ pass := []byte("newpasswhodis")
+ cipherText, err := cipherSeed.Encipher(pass)
+ require.NoError(t, err)
+
+ // Now that we have the cipher text, we'll modify the first byte to be
+ // an invalid version.
+ cipherText[0] = 44
+
+ // With the version swapped, if we try to decipher it, (no matter the
+ // passphrase), it should fail.
+ _, _, err = decipherCipherSeed(cipherText, []byte("kek"))
+ require.Equal(t, ErrIncorrectVersion, err)
+}
+
+// TestChangePassphrase tests that we're able to generate a cipher seed, then
+// change the password. If we attempt to decipher the new enciphered seed, then
+// we should get the exact same seed back.
+func TestChangePassphrase(t *testing.T) {
+ t.Parallel()
+
+ // First, we'll generate a new cipher seed with a test passphrase.
+ pass := []byte("test")
+ cipherSeed, err := New(0, &testEntropy, time.Now())
+ require.NoError(t, err)
+
+ // Now that we have our cipher seed, we'll encipher it and request a
+ // mnemonic that we can use to recover later.
+ mnemonic, err := cipherSeed.ToMnemonic(pass)
+ require.NoError(t, err)
+
+ // Now that have the mnemonic, we'll attempt to re-encipher the
+ // passphrase in order to get a brand-new mnemonic.
+ newPass := []byte("strongerpassyeh!")
+ newMnemonic, err := mnemonic.ChangePass(pass, newPass)
+ require.NoError(t, err)
+
+ // We'll now attempt to decipher the new mnemonic using the new
+ // passphrase to arrive at (what should be) the original cipher seed.
+ newCipherSeed, err := newMnemonic.ToCipherSeed(newPass)
+ require.NoError(t, err)
+
+ // Now that we have the cipher seed, we'll verify that the plaintext
+ // seed matches *identically*.
+ assertCipherSeedEqual(t, cipherSeed, newCipherSeed)
+}
+
+// TestChangePassphraseWrongPass tests that if we have a valid enciphered
+// cipher seed, but then try to change the password with the *wrong* password,
+// then we get an error.
+func TestChangePassphraseWrongPass(t *testing.T) {
+ t.Parallel()
+
+ // First, we'll generate a new cipher seed with a test passphrase.
+ pass := []byte("test")
+ cipherSeed, err := New(0, &testEntropy, time.Now())
+ require.NoError(t, err)
+
+ // Now that we have our cipher seed, we'll encipher it and request a
+ // mnemonic that we can use to recover later.
+ mnemonic, err := cipherSeed.ToMnemonic(pass)
+ require.NoError(t, err)
+
+ // Now that have the mnemonic, we'll attempt to re-encipher the
+ // passphrase in order to get a brand-new mnemonic. However, we'll be
+ // using the *wrong* passphrase. This should result in an
+ // ErrInvalidPass error.
+ wrongPass := []byte("kek")
+ newPass := []byte("strongerpassyeh!")
+ _, err = mnemonic.ChangePass(wrongPass, newPass)
+ require.Equal(t, ErrInvalidPass, err)
+}
+
+// TestMnemonicEncoding uses quickcheck like property based testing to ensure
+// that we're always able to fully recover the original byte stream encoded
+// into the mnemonic phrase.
+func TestMnemonicEncoding(t *testing.T) {
+ t.Parallel()
+
+ // mainScenario is the main driver of our property based test. We'll
+ // ensure that given a random byte string of length 33 bytes, if we
+ // convert that to the mnemonic, then we should be able to reverse the
+ // conversion.
+ mainScenario := func(cipherSeedBytes [EncipheredCipherSeedSize]byte) bool {
+ mnemonic, err := cipherTextToMnemonic(cipherSeedBytes)
+ if err != nil {
+ t.Fatalf("unable to map cipher text: %v", err)
+ return false
+ }
+
+ newCipher := mnemonicToCipherText(&mnemonic)
+
+ if newCipher != cipherSeedBytes {
+ t.Fatalf("cipherseed doesn't match: expected %v, got %v",
+ cipherSeedBytes, newCipher)
+ return false
+ }
+
+ return true
+ }
+
+ if err := quick.Check(mainScenario, nil); err != nil {
+ t.Fatalf("fuzz check failed: %v", err)
+ }
+}
+
+// TestEncipherDecipher is a property-based test that ensures that given a
+// version, entropy, and birthday, then we're able to map that to a cipher seed
+// mnemonic, then back to the original plaintext cipher seed.
+func TestEncipherDecipher(t *testing.T) {
+ t.Parallel()
+
+ // mainScenario is the main driver of our property based test. We'll
+ // ensure that given a random seed tuple (internal version, entropy,
+ // and birthday) we're able to convert that to a valid cipher seed.
+ // Additionally, we should be able to decipher the final mnemonic, and
+ // recover the original cipher seed.
+ mainScenario := func(version uint8, entropy [EntropySize]byte,
+ nowInt int64, pass [20]byte) bool {
+
+ now := time.Unix(nowInt, 0)
+
+ cipherSeed, err := New(version, &entropy, now)
+ if err != nil {
+ t.Fatalf("unable to map cipher text: %v", err)
+ return false
+ }
+
+ mnemonic, err := cipherSeed.ToMnemonic(pass[:])
+ if err != nil {
+ t.Fatalf("unable to generate mnemonic: %v", err)
+ return false
+ }
+
+ cipherSeed2, err := mnemonic.ToCipherSeed(pass[:])
+ if err != nil {
+ t.Fatalf("unable to decrypt cipher seed: %v", err)
+ return false
+ }
+
+ if cipherSeed.InternalVersion != cipherSeed2.InternalVersion {
+ t.Fatalf("mismatched versions: expected %v, got %v",
+ cipherSeed.InternalVersion, cipherSeed2.InternalVersion)
+ return false
+ }
+ if cipherSeed.Birthday != cipherSeed2.Birthday {
+ t.Fatalf("mismatched birthday: expected %v, got %v",
+ cipherSeed.Birthday, cipherSeed2.Birthday)
+ return false
+ }
+ if cipherSeed.Entropy != cipherSeed2.Entropy {
+ t.Fatalf("mismatched versions: expected %x, got %x",
+ cipherSeed.Entropy[:], cipherSeed2.Entropy[:])
+ return false
+ }
+
+ return true
+ }
+
+ if err := quick.Check(mainScenario, nil); err != nil {
+ t.Fatalf("fuzz check failed: %v", err)
+ }
+}
+
+// TestSeedEncodeDecode tests that we're able to reverse the encoding of an
+// arbitrary raw seed.
+func TestSeedEncodeDecode(t *testing.T) {
+ // mainScenario is the primary driver of our property-based test. We'll
+ // ensure that given a random cipher seed, we can encode it and decode
+ // it precisely.
+ mainScenario := func(version uint8, nowInt int64,
+ entropy [EntropySize]byte) bool {
+
+ now := time.Unix(nowInt, 0)
+ day := time.Hour * 24
+ numDaysSinceGenesis := now.Sub(BitcoinGenesisDate) / day
+ seed := CipherSeed{
+ InternalVersion: version,
+ Birthday: uint16(numDaysSinceGenesis),
+ Entropy: entropy,
+ }
+
+ var b bytes.Buffer
+ if err := seed.encode(&b); err != nil {
+ t.Fatalf("unable to encode: %v", err)
+ return false
+ }
+
+ var newSeed CipherSeed
+ if err := newSeed.decode(&b); err != nil {
+ t.Fatalf("unable to decode: %v", err)
+ return false
+ }
+
+ if seed.InternalVersion != newSeed.InternalVersion {
+ t.Fatalf("mismatched versions: expected %v, got %v",
+ seed.InternalVersion, newSeed.InternalVersion)
+ return false
+ }
+ if seed.Birthday != newSeed.Birthday {
+ t.Fatalf("mismatched birthday: expected %v, got %v",
+ seed.Birthday, newSeed.Birthday)
+ return false
+ }
+ if seed.Entropy != newSeed.Entropy {
+ t.Fatalf("mismatched versions: expected %x, got %x",
+ seed.Entropy[:], newSeed.Entropy[:])
+ return false
+ }
+
+ return true
+ }
+
+ if err := quick.Check(mainScenario, nil); err != nil {
+ t.Fatalf("fuzz check failed: %v", err)
+ }
+}
+
+// TestDecipherUnknownMnemonicWord tests that if we obtain a mnemonic, then
+// modify one of the words to not be within the word list, then it's detected
+// when we attempt to map it back to the original cipher seed.
+func TestDecipherUnknownMnemonicWord(t *testing.T) {
+ t.Parallel()
+
+ // First, we'll create a new cipher seed with "test" ass a password.
+ pass := []byte("test")
+ cipherSeed, err := New(0, &testEntropy, time.Now())
+ require.NoError(t, err)
+
+ // Now that we have our cipher seed, we'll encipher it and request a
+ // mnemonic that we can use to recover later.
+ mnemonic, err := cipherSeed.ToMnemonic(pass)
+ require.NoError(t, err)
+
+ // Before we attempt to decrypt the cipher seed, we'll mutate one of
+ // the word so it isn't actually in our final word list.
+ randIndex := rand.Int31n(int32(len(mnemonic)))
+ mnemonic[randIndex] = "kek"
+
+ // If we attempt to map back to the original cipher seed now, then we
+ // should get ErrUnknownMnemonicWord.
+ _, err = mnemonic.ToCipherSeed(pass)
+ wordErr := &ErrUnknownMnemonicWord{}
+ require.ErrorAs(t, err, wordErr)
+ require.Equal(t, "kek", wordErr.Word)
+ require.Equal(t, uint8(randIndex), wordErr.Index)
+
+ // If the mnemonic includes a word that is not in the englishList it
+ // fails, even when it is a substring of a valid word Example: `heart`
+ // is in the list, `hear` is not.
+ mnemonic[randIndex] = "hear"
+
+ // If we attempt to map back to the original cipher seed now, then we
+ // should get ErrUnknownMnemonicWord.
+ _, err = mnemonic.ToCipherSeed(pass)
+ require.ErrorAs(t, err, wordErr)
+}
+
+// TestDecipherIncorrectMnemonic tests that if we obtain a cipher seed, but then
+// swap out words, then checksum fails.
+func TestDecipherIncorrectMnemonic(t *testing.T) {
+ // First, we'll create a new cipher seed with "test" ass a password.
+ pass := []byte("test")
+ cipherSeed, err := New(0, &testEntropy, time.Now())
+ require.NoError(t, err)
+
+ // Now that we have our cipher seed, we'll encipher it and request a
+ // mnemonic that we can use to recover later.
+ mnemonic, err := cipherSeed.ToMnemonic(pass)
+ require.NoError(t, err)
+
+ // We'll now swap out two words from the mnemonic, which should trigger
+ // a checksum failure.
+ swapIndex1 := 9
+ swapIndex2 := 13
+
+ mnemonic[swapIndex1], mnemonic[swapIndex2] =
+ mnemonic[swapIndex2], mnemonic[swapIndex1]
+
+ // If the words happen to be the same by pure chance, we'll try again
+ // with different indexes.
+ if mnemonic[swapIndex1] == mnemonic[swapIndex2] {
+ swapIndex1 = 3
+ mnemonic[swapIndex1], mnemonic[swapIndex2] =
+ mnemonic[swapIndex2], mnemonic[swapIndex1]
+ }
+
+ // If we attempt to decrypt now, we should get a checksum failure.
+ // If we attempt to map back to the original cipher seed now, then we
+ // should get ErrIncorrectMnemonic.
+ _, err = mnemonic.ToCipherSeed(pass)
+ require.Equal(t, ErrIncorrectMnemonic, err)
+}
+
+// TODO(roasbeef): add test failure checksum fail is modified, new error
+
+func init() {
+ // For the purposes of our test, we'll crank down the scrypt params a
+ // bit.
+ scryptN = 16
+ scryptR = 8
+ scryptP = 1
+}
diff --git a/aezeed/errors.go b/aezeed/errors.go
new file mode 100644
index 0000000..8f213c0
--- /dev/null
+++ b/aezeed/errors.go
@@ -0,0 +1,36 @@
+package aezeed
+
+import "fmt"
+
+var (
+ // ErrIncorrectVersion is returned if a seed bares a mismatched
+ // external version to that of the package executing the aezeed scheme.
+ ErrIncorrectVersion = fmt.Errorf("wrong seed version")
+
+ // ErrInvalidPass is returned if the user enters an invalid passphrase
+ // for a particular enciphered mnemonic.
+ ErrInvalidPass = fmt.Errorf("invalid passphrase")
+
+ // ErrIncorrectMnemonic is returned if we detect that the checksum of
+ // the specified mnemonic doesn't match. This indicates the user input
+ // the wrong mnemonic.
+ ErrIncorrectMnemonic = fmt.Errorf("mnemonic phrase checksum doesn't " +
+ "match")
+)
+
+// ErrUnknownMnemonicWord is returned when attempting to decipher and
+// enciphered mnemonic, but a word encountered isn't a member of our word list.
+type ErrUnknownMnemonicWord struct {
+ // Word is the unknown word in the mnemonic phrase.
+ Word string
+
+ // Index is the index (starting from zero) within the slice of strings
+ // that makes up the mnemonic that points to the incorrect word.
+ Index uint8
+}
+
+// Error returns a human-readable string describing the error.
+func (e ErrUnknownMnemonicWord) Error() string {
+ return fmt.Sprintf("word %v isn't a part of default word list "+
+ "(index=%v)", e.Word, e.Index)
+}
diff --git a/aezeed/wordlist.go b/aezeed/wordlist.go
new file mode 100644
index 0000000..1540c0b
--- /dev/null
+++ b/aezeed/wordlist.go
@@ -0,0 +1,2073 @@
+package aezeed
+
+import (
+ "strings"
+)
+
+var (
+ // ReverseWordMap maps a word to its position within the default word list.
+ ReverseWordMap map[string]int
+)
+
+func init() {
+ ReverseWordMap = make(map[string]int)
+ for i, v := range DefaultWordList {
+ ReverseWordMap[v] = i
+ }
+}
+
+// DefaultWordList is a slice of the current default word list that's used to
+// encode the enciphered seed into a human readable set of words.
+var DefaultWordList = strings.Split(englishWordList, "\n")
+
+// englishWordList is an English wordlist that's used as part of version 0 of
+// the cipherseed scheme. This is the *same* word list that's recommend for use
+// with BIP0039.
+var englishWordList = `abandon
+ability
+able
+about
+above
+absent
+absorb
+abstract
+absurd
+abuse
+access
+accident
+account
+accuse
+achieve
+acid
+acoustic
+acquire
+across
+act
+action
+actor
+actress
+actual
+adapt
+add
+addict
+address
+adjust
+admit
+adult
+advance
+advice
+aerobic
+affair
+afford
+afraid
+again
+age
+agent
+agree
+ahead
+aim
+air
+airport
+aisle
+alarm
+album
+alcohol
+alert
+alien
+all
+alley
+allow
+almost
+alone
+alpha
+already
+also
+alter
+always
+amateur
+amazing
+among
+amount
+amused
+analyst
+anchor
+ancient
+anger
+angle
+angry
+animal
+ankle
+announce
+annual
+another
+answer
+antenna
+antique
+anxiety
+any
+apart
+apology
+appear
+apple
+approve
+april
+arch
+arctic
+area
+arena
+argue
+arm
+armed
+armor
+army
+around
+arrange
+arrest
+arrive
+arrow
+art
+artefact
+artist
+artwork
+ask
+aspect
+assault
+asset
+assist
+assume
+asthma
+athlete
+atom
+attack
+attend
+attitude
+attract
+auction
+audit
+august
+aunt
+author
+auto
+autumn
+average
+avocado
+avoid
+awake
+aware
+away
+awesome
+awful
+awkward
+axis
+baby
+bachelor
+bacon
+badge
+bag
+balance
+balcony
+ball
+bamboo
+banana
+banner
+bar
+barely
+bargain
+barrel
+base
+basic
+basket
+battle
+beach
+bean
+beauty
+because
+become
+beef
+before
+begin
+behave
+behind
+believe
+below
+belt
+bench
+benefit
+best
+betray
+better
+between
+beyond
+bicycle
+bid
+bike
+bind
+biology
+bird
+birth
+bitter
+black
+blade
+blame
+blanket
+blast
+bleak
+bless
+blind
+blood
+blossom
+blouse
+blue
+blur
+blush
+board
+boat
+body
+boil
+bomb
+bone
+bonus
+book
+boost
+border
+boring
+borrow
+boss
+bottom
+bounce
+box
+boy
+bracket
+brain
+brand
+brass
+brave
+bread
+breeze
+brick
+bridge
+brief
+bright
+bring
+brisk
+broccoli
+broken
+bronze
+broom
+brother
+brown
+brush
+bubble
+buddy
+budget
+buffalo
+build
+bulb
+bulk
+bullet
+bundle
+bunker
+burden
+burger
+burst
+bus
+business
+busy
+butter
+buyer
+buzz
+cabbage
+cabin
+cable
+cactus
+cage
+cake
+call
+calm
+camera
+camp
+can
+canal
+cancel
+candy
+cannon
+canoe
+canvas
+canyon
+capable
+capital
+captain
+car
+carbon
+card
+cargo
+carpet
+carry
+cart
+case
+cash
+casino
+castle
+casual
+cat
+catalog
+catch
+category
+cattle
+caught
+cause
+caution
+cave
+ceiling
+celery
+cement
+census
+century
+cereal
+certain
+chair
+chalk
+champion
+change
+chaos
+chapter
+charge
+chase
+chat
+cheap
+check
+cheese
+chef
+cherry
+chest
+chicken
+chief
+child
+chimney
+choice
+choose
+chronic
+chuckle
+chunk
+churn
+cigar
+cinnamon
+circle
+citizen
+city
+civil
+claim
+clap
+clarify
+claw
+clay
+clean
+clerk
+clever
+click
+client
+cliff
+climb
+clinic
+clip
+clock
+clog
+close
+cloth
+cloud
+clown
+club
+clump
+cluster
+clutch
+coach
+coast
+coconut
+code
+coffee
+coil
+coin
+collect
+color
+column
+combine
+come
+comfort
+comic
+common
+company
+concert
+conduct
+confirm
+congress
+connect
+consider
+control
+convince
+cook
+cool
+copper
+copy
+coral
+core
+corn
+correct
+cost
+cotton
+couch
+country
+couple
+course
+cousin
+cover
+coyote
+crack
+cradle
+craft
+cram
+crane
+crash
+crater
+crawl
+crazy
+cream
+credit
+creek
+crew
+cricket
+crime
+crisp
+critic
+crop
+cross
+crouch
+crowd
+crucial
+cruel
+cruise
+crumble
+crunch
+crush
+cry
+crystal
+cube
+culture
+cup
+cupboard
+curious
+current
+curtain
+curve
+cushion
+custom
+cute
+cycle
+dad
+damage
+damp
+dance
+danger
+daring
+dash
+daughter
+dawn
+day
+deal
+debate
+debris
+decade
+december
+decide
+decline
+decorate
+decrease
+deer
+defense
+define
+defy
+degree
+delay
+deliver
+demand
+demise
+denial
+dentist
+deny
+depart
+depend
+deposit
+depth
+deputy
+derive
+describe
+desert
+design
+desk
+despair
+destroy
+detail
+detect
+develop
+device
+devote
+diagram
+dial
+diamond
+diary
+dice
+diesel
+diet
+differ
+digital
+dignity
+dilemma
+dinner
+dinosaur
+direct
+dirt
+disagree
+discover
+disease
+dish
+dismiss
+disorder
+display
+distance
+divert
+divide
+divorce
+dizzy
+doctor
+document
+dog
+doll
+dolphin
+domain
+donate
+donkey
+donor
+door
+dose
+double
+dove
+draft
+dragon
+drama
+drastic
+draw
+dream
+dress
+drift
+drill
+drink
+drip
+drive
+drop
+drum
+dry
+duck
+dumb
+dune
+during
+dust
+dutch
+duty
+dwarf
+dynamic
+eager
+eagle
+early
+earn
+earth
+easily
+east
+easy
+echo
+ecology
+economy
+edge
+edit
+educate
+effort
+egg
+eight
+either
+elbow
+elder
+electric
+elegant
+element
+elephant
+elevator
+elite
+else
+embark
+embody
+embrace
+emerge
+emotion
+employ
+empower
+empty
+enable
+enact
+end
+endless
+endorse
+enemy
+energy
+enforce
+engage
+engine
+enhance
+enjoy
+enlist
+enough
+enrich
+enroll
+ensure
+enter
+entire
+entry
+envelope
+episode
+equal
+equip
+era
+erase
+erode
+erosion
+error
+erupt
+escape
+essay
+essence
+estate
+eternal
+ethics
+evidence
+evil
+evoke
+evolve
+exact
+example
+excess
+exchange
+excite
+exclude
+excuse
+execute
+exercise
+exhaust
+exhibit
+exile
+exist
+exit
+exotic
+expand
+expect
+expire
+explain
+expose
+express
+extend
+extra
+eye
+eyebrow
+fabric
+face
+faculty
+fade
+faint
+faith
+fall
+false
+fame
+family
+famous
+fan
+fancy
+fantasy
+farm
+fashion
+fat
+fatal
+father
+fatigue
+fault
+favorite
+feature
+february
+federal
+fee
+feed
+feel
+female
+fence
+festival
+fetch
+fever
+few
+fiber
+fiction
+field
+figure
+file
+film
+filter
+final
+find
+fine
+finger
+finish
+fire
+firm
+first
+fiscal
+fish
+fit
+fitness
+fix
+flag
+flame
+flash
+flat
+flavor
+flee
+flight
+flip
+float
+flock
+floor
+flower
+fluid
+flush
+fly
+foam
+focus
+fog
+foil
+fold
+follow
+food
+foot
+force
+forest
+forget
+fork
+fortune
+forum
+forward
+fossil
+foster
+found
+fox
+fragile
+frame
+frequent
+fresh
+friend
+fringe
+frog
+front
+frost
+frown
+frozen
+fruit
+fuel
+fun
+funny
+furnace
+fury
+future
+gadget
+gain
+galaxy
+gallery
+game
+gap
+garage
+garbage
+garden
+garlic
+garment
+gas
+gasp
+gate
+gather
+gauge
+gaze
+general
+genius
+genre
+gentle
+genuine
+gesture
+ghost
+giant
+gift
+giggle
+ginger
+giraffe
+girl
+give
+glad
+glance
+glare
+glass
+glide
+glimpse
+globe
+gloom
+glory
+glove
+glow
+glue
+goat
+goddess
+gold
+good
+goose
+gorilla
+gospel
+gossip
+govern
+gown
+grab
+grace
+grain
+grant
+grape
+grass
+gravity
+great
+green
+grid
+grief
+grit
+grocery
+group
+grow
+grunt
+guard
+guess
+guide
+guilt
+guitar
+gun
+gym
+habit
+hair
+half
+hammer
+hamster
+hand
+happy
+harbor
+hard
+harsh
+harvest
+hat
+have
+hawk
+hazard
+head
+health
+heart
+heavy
+hedgehog
+height
+hello
+helmet
+help
+hen
+hero
+hidden
+high
+hill
+hint
+hip
+hire
+history
+hobby
+hockey
+hold
+hole
+holiday
+hollow
+home
+honey
+hood
+hope
+horn
+horror
+horse
+hospital
+host
+hotel
+hour
+hover
+hub
+huge
+human
+humble
+humor
+hundred
+hungry
+hunt
+hurdle
+hurry
+hurt
+husband
+hybrid
+ice
+icon
+idea
+identify
+idle
+ignore
+ill
+illegal
+illness
+image
+imitate
+immense
+immune
+impact
+impose
+improve
+impulse
+inch
+include
+income
+increase
+index
+indicate
+indoor
+industry
+infant
+inflict
+inform
+inhale
+inherit
+initial
+inject
+injury
+inmate
+inner
+innocent
+input
+inquiry
+insane
+insect
+inside
+inspire
+install
+intact
+interest
+into
+invest
+invite
+involve
+iron
+island
+isolate
+issue
+item
+ivory
+jacket
+jaguar
+jar
+jazz
+jealous
+jeans
+jelly
+jewel
+job
+join
+joke
+journey
+joy
+judge
+juice
+jump
+jungle
+junior
+junk
+just
+kangaroo
+keen
+keep
+ketchup
+key
+kick
+kid
+kidney
+kind
+kingdom
+kiss
+kit
+kitchen
+kite
+kitten
+kiwi
+knee
+knife
+knock
+know
+lab
+label
+labor
+ladder
+lady
+lake
+lamp
+language
+laptop
+large
+later
+latin
+laugh
+laundry
+lava
+law
+lawn
+lawsuit
+layer
+lazy
+leader
+leaf
+learn
+leave
+lecture
+left
+leg
+legal
+legend
+leisure
+lemon
+lend
+length
+lens
+leopard
+lesson
+letter
+level
+liar
+liberty
+library
+license
+life
+lift
+light
+like
+limb
+limit
+link
+lion
+liquid
+list
+little
+live
+lizard
+load
+loan
+lobster
+local
+lock
+logic
+lonely
+long
+loop
+lottery
+loud
+lounge
+love
+loyal
+lucky
+luggage
+lumber
+lunar
+lunch
+luxury
+lyrics
+machine
+mad
+magic
+magnet
+maid
+mail
+main
+major
+make
+mammal
+man
+manage
+mandate
+mango
+mansion
+manual
+maple
+marble
+march
+margin
+marine
+market
+marriage
+mask
+mass
+master
+match
+material
+math
+matrix
+matter
+maximum
+maze
+meadow
+mean
+measure
+meat
+mechanic
+medal
+media
+melody
+melt
+member
+memory
+mention
+menu
+mercy
+merge
+merit
+merry
+mesh
+message
+metal
+method
+middle
+midnight
+milk
+million
+mimic
+mind
+minimum
+minor
+minute
+miracle
+mirror
+misery
+miss
+mistake
+mix
+mixed
+mixture
+mobile
+model
+modify
+mom
+moment
+monitor
+monkey
+monster
+month
+moon
+moral
+more
+morning
+mosquito
+mother
+motion
+motor
+mountain
+mouse
+move
+movie
+much
+muffin
+mule
+multiply
+muscle
+museum
+mushroom
+music
+must
+mutual
+myself
+mystery
+myth
+naive
+name
+napkin
+narrow
+nasty
+nation
+nature
+near
+neck
+need
+negative
+neglect
+neither
+nephew
+nerve
+nest
+net
+network
+neutral
+never
+news
+next
+nice
+night
+noble
+noise
+nominee
+noodle
+normal
+north
+nose
+notable
+note
+nothing
+notice
+novel
+now
+nuclear
+number
+nurse
+nut
+oak
+obey
+object
+oblige
+obscure
+observe
+obtain
+obvious
+occur
+ocean
+october
+odor
+off
+offer
+office
+often
+oil
+okay
+old
+olive
+olympic
+omit
+once
+one
+onion
+online
+only
+open
+opera
+opinion
+oppose
+option
+orange
+orbit
+orchard
+order
+ordinary
+organ
+orient
+original
+orphan
+ostrich
+other
+outdoor
+outer
+output
+outside
+oval
+oven
+over
+own
+owner
+oxygen
+oyster
+ozone
+pact
+paddle
+page
+pair
+palace
+palm
+panda
+panel
+panic
+panther
+paper
+parade
+parent
+park
+parrot
+party
+pass
+patch
+path
+patient
+patrol
+pattern
+pause
+pave
+payment
+peace
+peanut
+pear
+peasant
+pelican
+pen
+penalty
+pencil
+people
+pepper
+perfect
+permit
+person
+pet
+phone
+photo
+phrase
+physical
+piano
+picnic
+picture
+piece
+pig
+pigeon
+pill
+pilot
+pink
+pioneer
+pipe
+pistol
+pitch
+pizza
+place
+planet
+plastic
+plate
+play
+please
+pledge
+pluck
+plug
+plunge
+poem
+poet
+point
+polar
+pole
+police
+pond
+pony
+pool
+popular
+portion
+position
+possible
+post
+potato
+pottery
+poverty
+powder
+power
+practice
+praise
+predict
+prefer
+prepare
+present
+pretty
+prevent
+price
+pride
+primary
+print
+priority
+prison
+private
+prize
+problem
+process
+produce
+profit
+program
+project
+promote
+proof
+property
+prosper
+protect
+proud
+provide
+public
+pudding
+pull
+pulp
+pulse
+pumpkin
+punch
+pupil
+puppy
+purchase
+purity
+purpose
+purse
+push
+put
+puzzle
+pyramid
+quality
+quantum
+quarter
+question
+quick
+quit
+quiz
+quote
+rabbit
+raccoon
+race
+rack
+radar
+radio
+rail
+rain
+raise
+rally
+ramp
+ranch
+random
+range
+rapid
+rare
+rate
+rather
+raven
+raw
+razor
+ready
+real
+reason
+rebel
+rebuild
+recall
+receive
+recipe
+record
+recycle
+reduce
+reflect
+reform
+refuse
+region
+regret
+regular
+reject
+relax
+release
+relief
+rely
+remain
+remember
+remind
+remove
+render
+renew
+rent
+reopen
+repair
+repeat
+replace
+report
+require
+rescue
+resemble
+resist
+resource
+response
+result
+retire
+retreat
+return
+reunion
+reveal
+review
+reward
+rhythm
+rib
+ribbon
+rice
+rich
+ride
+ridge
+rifle
+right
+rigid
+ring
+riot
+ripple
+risk
+ritual
+rival
+river
+road
+roast
+robot
+robust
+rocket
+romance
+roof
+rookie
+room
+rose
+rotate
+rough
+round
+route
+royal
+rubber
+rude
+rug
+rule
+run
+runway
+rural
+sad
+saddle
+sadness
+safe
+sail
+salad
+salmon
+salon
+salt
+salute
+same
+sample
+sand
+satisfy
+satoshi
+sauce
+sausage
+save
+say
+scale
+scan
+scare
+scatter
+scene
+scheme
+school
+science
+scissors
+scorpion
+scout
+scrap
+screen
+script
+scrub
+sea
+search
+season
+seat
+second
+secret
+section
+security
+seed
+seek
+segment
+select
+sell
+seminar
+senior
+sense
+sentence
+series
+service
+session
+settle
+setup
+seven
+shadow
+shaft
+shallow
+share
+shed
+shell
+sheriff
+shield
+shift
+shine
+ship
+shiver
+shock
+shoe
+shoot
+shop
+short
+shoulder
+shove
+shrimp
+shrug
+shuffle
+shy
+sibling
+sick
+side
+siege
+sight
+sign
+silent
+silk
+silly
+silver
+similar
+simple
+since
+sing
+siren
+sister
+situate
+six
+size
+skate
+sketch
+ski
+skill
+skin
+skirt
+skull
+slab
+slam
+sleep
+slender
+slice
+slide
+slight
+slim
+slogan
+slot
+slow
+slush
+small
+smart
+smile
+smoke
+smooth
+snack
+snake
+snap
+sniff
+snow
+soap
+soccer
+social
+sock
+soda
+soft
+solar
+soldier
+solid
+solution
+solve
+someone
+song
+soon
+sorry
+sort
+soul
+sound
+soup
+source
+south
+space
+spare
+spatial
+spawn
+speak
+special
+speed
+spell
+spend
+sphere
+spice
+spider
+spike
+spin
+spirit
+split
+spoil
+sponsor
+spoon
+sport
+spot
+spray
+spread
+spring
+spy
+square
+squeeze
+squirrel
+stable
+stadium
+staff
+stage
+stairs
+stamp
+stand
+start
+state
+stay
+steak
+steel
+stem
+step
+stereo
+stick
+still
+sting
+stock
+stomach
+stone
+stool
+story
+stove
+strategy
+street
+strike
+strong
+struggle
+student
+stuff
+stumble
+style
+subject
+submit
+subway
+success
+such
+sudden
+suffer
+sugar
+suggest
+suit
+summer
+sun
+sunny
+sunset
+super
+supply
+supreme
+sure
+surface
+surge
+surprise
+surround
+survey
+suspect
+sustain
+swallow
+swamp
+swap
+swarm
+swear
+sweet
+swift
+swim
+swing
+switch
+sword
+symbol
+symptom
+syrup
+system
+table
+tackle
+tag
+tail
+talent
+talk
+tank
+tape
+target
+task
+taste
+tattoo
+taxi
+teach
+team
+tell
+ten
+tenant
+tennis
+tent
+term
+test
+text
+thank
+that
+theme
+then
+theory
+there
+they
+thing
+this
+thought
+three
+thrive
+throw
+thumb
+thunder
+ticket
+tide
+tiger
+tilt
+timber
+time
+tiny
+tip
+tired
+tissue
+title
+toast
+tobacco
+today
+toddler
+toe
+together
+toilet
+token
+tomato
+tomorrow
+tone
+tongue
+tonight
+tool
+tooth
+top
+topic
+topple
+torch
+tornado
+tortoise
+toss
+total
+tourist
+toward
+tower
+town
+toy
+track
+trade
+traffic
+tragic
+train
+transfer
+trap
+trash
+travel
+tray
+treat
+tree
+trend
+trial
+tribe
+trick
+trigger
+trim
+trip
+trophy
+trouble
+truck
+true
+truly
+trumpet
+trust
+truth
+try
+tube
+tuition
+tumble
+tuna
+tunnel
+turkey
+turn
+turtle
+twelve
+twenty
+twice
+twin
+twist
+two
+type
+typical
+ugly
+umbrella
+unable
+unaware
+uncle
+uncover
+under
+undo
+unfair
+unfold
+unhappy
+uniform
+unique
+unit
+universe
+unknown
+unlock
+until
+unusual
+unveil
+update
+upgrade
+uphold
+upon
+upper
+upset
+urban
+urge
+usage
+use
+used
+useful
+useless
+usual
+utility
+vacant
+vacuum
+vague
+valid
+valley
+valve
+van
+vanish
+vapor
+various
+vast
+vault
+vehicle
+velvet
+vendor
+venture
+venue
+verb
+verify
+version
+very
+vessel
+veteran
+viable
+vibrant
+vicious
+victory
+video
+view
+village
+vintage
+violin
+virtual
+virus
+visa
+visit
+visual
+vital
+vivid
+vocal
+voice
+void
+volcano
+volume
+vote
+voyage
+wage
+wagon
+wait
+walk
+wall
+walnut
+want
+warfare
+warm
+warrior
+wash
+wasp
+waste
+water
+wave
+way
+wealth
+weapon
+wear
+weasel
+weather
+web
+wedding
+weekend
+weird
+welcome
+west
+wet
+whale
+what
+wheat
+wheel
+when
+where
+whip
+whisper
+wide
+width
+wife
+wild
+will
+win
+window
+wine
+wing
+wink
+winner
+winter
+wire
+wisdom
+wise
+wish
+witness
+wolf
+woman
+wonder
+wood
+wool
+word
+work
+world
+worry
+worth
+wrap
+wreck
+wrestle
+wrist
+write
+wrong
+yard
+year
+yellow
+you
+young
+youth
+zebra
+zero
+zone
+zoo`
diff --git a/aliasmgr/aliasmgr.go b/aliasmgr/aliasmgr.go
new file mode 100644
index 0000000..258d205
--- /dev/null
+++ b/aliasmgr/aliasmgr.go
@@ -0,0 +1,684 @@
+package aliasmgr
+
+import (
+ "encoding/binary"
+ "fmt"
+ "maps"
+ "slices"
+ "sync"
+
+ "github.com/lightningnetwork/lnd/fn/v2"
+ "github.com/lightningnetwork/lnd/htlcswitch/hop"
+ "github.com/lightningnetwork/lnd/kvdb"
+ "github.com/lightningnetwork/lnd/lnwire"
+)
+
+// UpdateLinkAliases is a function type for a function that locates the active
+// link that matches the given shortID and triggers an update based on the
+// latest values of the alias manager.
+type UpdateLinkAliases func(shortID lnwire.ShortChannelID) error
+
+// ScidAliasMap is a map from a base short channel ID to a set of alias short
+// channel IDs.
+type ScidAliasMap map[lnwire.ShortChannelID][]lnwire.ShortChannelID
+
+var (
+ // aliasBucket stores aliases as keys and their base SCIDs as values.
+ // This is used to populate the maps that the Manager uses. The keys
+ // are alias SCIDs and the values are their respective base SCIDs. This
+ // is used instead of the other way around (base -> alias...) because
+ // updating an alias would require fetching all the existing aliases,
+ // adding another one, and then flushing the write to disk. This is
+ // inefficient compared to N 1:1 mappings at the cost of marginally
+ // more disk space.
+ aliasBucket = []byte("alias-bucket")
+
+ // confirmedBucket stores whether or not a given base SCID should no
+ // longer have entries in the ToBase maps. The key is the SCID that is
+ // confirmed with 6 confirmations and is public, and the value is
+ // empty.
+ confirmedBucket = []byte("base-bucket")
+
+ // aliasAllocBucket is a root-level bucket that stores the last alias
+ // that was allocated. It is used to allocate a new alias when
+ // requested.
+ aliasAllocBucket = []byte("alias-alloc-bucket")
+
+ // lastAliasKey is a key in the aliasAllocBucket whose value is the
+ // last allocated alias ShortChannelID. This will be updated upon calls
+ // to RequestAlias.
+ lastAliasKey = []byte("last-alias-key")
+
+ // invoiceAliasBucket is a root-level bucket that stores the alias
+ // SCIDs that our peers send us in the channel_ready TLV. The keys are
+ // the ChannelID generated from the FundingOutpoint and the values are
+ // the remote peer's alias SCID.
+ invoiceAliasBucket = []byte("invoice-alias-bucket")
+
+ // byteOrder denotes the byte order of database (de)-serialization
+ // operations.
+ byteOrder = binary.BigEndian
+
+ // AliasStartBlockHeight is the starting block height of the alias
+ // range.
+ AliasStartBlockHeight uint32 = 16_000_000
+
+ // AliasEndBlockHeight is the ending block height of the alias range.
+ AliasEndBlockHeight uint32 = 16_250_000
+
+ // StartingAlias is the first alias ShortChannelID that will get
+ // assigned by RequestAlias. The starting BlockHeight is chosen so that
+ // legitimate SCIDs in integration tests aren't mistaken for an alias.
+ StartingAlias = lnwire.ShortChannelID{
+ BlockHeight: AliasStartBlockHeight,
+ TxIndex: 0,
+ TxPosition: 0,
+ }
+
+ // errNoBase is returned when a base SCID isn't found.
+ errNoBase = fmt.Errorf("no base found")
+
+ // errNoPeerAlias is returned when the peer's alias for a given
+ // channel is not found.
+ errNoPeerAlias = fmt.Errorf("no peer alias found")
+
+ // ErrAliasNotFound is returned when the alias is not found and can't
+ // be mapped to a base SCID.
+ ErrAliasNotFound = fmt.Errorf("alias not found")
+)
+
+// Manager is a struct that handles aliases for LND. It has an underlying
+// database that can allocate aliases for channels, stores the peer's last
+// alias for use in our hop hints, and contains mappings that both the Switch
+// and Gossiper use.
+type Manager struct {
+ backend kvdb.Backend
+
+ // linkAliasUpdater is a function used by the alias manager to
+ // facilitate live update of aliases in other subsystems.
+ linkAliasUpdater UpdateLinkAliases
+
+ // baseToSet is a mapping from the "base" SCID to the set of aliases
+ // for this channel. This mapping includes all channels that
+ // negotiated the option-scid-alias feature bit.
+ baseToSet ScidAliasMap
+
+ // aliasToBase is a mapping that maps all aliases for a given channel
+ // to its base SCID. This is only used for channels that have
+ // negotiated option-scid-alias feature bit.
+ aliasToBase map[lnwire.ShortChannelID]lnwire.ShortChannelID
+
+ // peerAlias is a cache for the alias SCIDs that our peers send us in
+ // the channel_ready TLV. The keys are the ChannelID generated from
+ // the FundingOutpoint and the values are the remote peer's alias SCID.
+ // The values should match the ones stored in the "invoice-alias-bucket"
+ // bucket.
+ peerAlias map[lnwire.ChannelID]lnwire.ShortChannelID
+
+ sync.RWMutex
+}
+
+// NewManager initializes an alias Manager from the passed database backend.
+func NewManager(db kvdb.Backend, linkAliasUpdater UpdateLinkAliases) (*Manager,
+ error) {
+
+ m := &Manager{
+ backend: db,
+ baseToSet: make(ScidAliasMap),
+ linkAliasUpdater: linkAliasUpdater,
+ }
+
+ m.aliasToBase = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
+ m.peerAlias = make(map[lnwire.ChannelID]lnwire.ShortChannelID)
+
+ err := m.populateMaps()
+ return m, err
+}
+
+// populateMaps reads the database state and populates the maps.
+func (m *Manager) populateMaps() error {
+ // This map tracks the base SCIDs that are confirmed and don't need to
+ // have entries in the *ToBase mappings as they won't be used in the
+ // gossiper.
+ baseConfMap := make(map[lnwire.ShortChannelID]struct{})
+
+ // This map caches what is found in the database and is used to
+ // populate the Manager's actual maps.
+ aliasMap := make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
+
+ // This map caches the ChannelID/alias SCIDs stored in the database and
+ // is used to populate the Manager's cache.
+ peerAliasMap := make(map[lnwire.ChannelID]lnwire.ShortChannelID)
+
+ err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
+ baseConfBucket, err := tx.CreateTopLevelBucket(confirmedBucket)
+ if err != nil {
+ return err
+ }
+
+ err = baseConfBucket.ForEach(func(k, v []byte) error {
+ // The key will the base SCID and the value will be
+ // empty. Existence in the bucket means the SCID is
+ // confirmed.
+ baseScid := lnwire.NewShortChanIDFromInt(
+ byteOrder.Uint64(k),
+ )
+ baseConfMap[baseScid] = struct{}{}
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ aliasToBaseBucket, err := tx.CreateTopLevelBucket(aliasBucket)
+ if err != nil {
+ return err
+ }
+
+ err = aliasToBaseBucket.ForEach(func(k, v []byte) error {
+ // The key will be the alias SCID and the value will be
+ // the base SCID.
+ aliasScid := lnwire.NewShortChanIDFromInt(
+ byteOrder.Uint64(k),
+ )
+ baseScid := lnwire.NewShortChanIDFromInt(
+ byteOrder.Uint64(v),
+ )
+ aliasMap[aliasScid] = baseScid
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+
+ invAliasBucket, err := tx.CreateTopLevelBucket(
+ invoiceAliasBucket,
+ )
+ if err != nil {
+ return err
+ }
+
+ err = invAliasBucket.ForEach(func(k, v []byte) error {
+ var chanID lnwire.ChannelID
+ copy(chanID[:], k)
+ alias := lnwire.NewShortChanIDFromInt(
+ byteOrder.Uint64(v),
+ )
+
+ peerAliasMap[chanID] = alias
+
+ return nil
+ })
+
+ return err
+ }, func() {
+ baseConfMap = make(map[lnwire.ShortChannelID]struct{})
+ aliasMap = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
+ peerAliasMap = make(map[lnwire.ChannelID]lnwire.ShortChannelID)
+ })
+ if err != nil {
+ return err
+ }
+
+ // Populate the baseToSet map regardless if the baseSCID is marked as
+ // public with 6 confirmations.
+ for aliasSCID, baseSCID := range aliasMap {
+ m.baseToSet[baseSCID] = append(m.baseToSet[baseSCID], aliasSCID)
+
+ // Skip if baseSCID is in the baseConfMap.
+ if _, ok := baseConfMap[baseSCID]; ok {
+ continue
+ }
+
+ m.aliasToBase[aliasSCID] = baseSCID
+ }
+
+ // Populate the peer alias cache.
+ m.peerAlias = peerAliasMap
+
+ return nil
+}
+
+// AddLocalAlias adds a database mapping from the passed alias to the passed
+// base SCID. The gossip boolean marks whether or not to create a mapping
+// that the gossiper will use. It is set to false for the upgrade path where
+// the feature-bit is toggled on and there are existing channels. The linkUpdate
+// flag is used to signal whether this function should also trigger an update
+// on the htlcswitch scid alias maps.
+func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID,
+ gossip, linkUpdate bool) error {
+
+ // We need to lock the manager for the whole duration of this method,
+ // except for the very last part where we call the link updater. In
+ // order for us to safely use a defer _and_ still be able to manually
+ // unlock, we use a sync.Once.
+ m.Lock()
+ unlockOnce := sync.Once{}
+ unlock := func() {
+ unlockOnce.Do(m.Unlock)
+ }
+ defer unlock()
+
+ err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
+ // If the caller does not want to allow the alias to be used
+ // for a channel update, we'll mark it in the baseConfBucket.
+ if !gossip {
+ var baseGossipBytes [8]byte
+ byteOrder.PutUint64(
+ baseGossipBytes[:], baseScid.ToUint64(),
+ )
+
+ confBucket, err := tx.CreateTopLevelBucket(
+ confirmedBucket,
+ )
+ if err != nil {
+ return err
+ }
+
+ err = confBucket.Put(baseGossipBytes[:], []byte{})
+ if err != nil {
+ return err
+ }
+ }
+
+ aliasToBaseBucket, err := tx.CreateTopLevelBucket(aliasBucket)
+ if err != nil {
+ return err
+ }
+
+ var (
+ aliasBytes [8]byte
+ baseBytes [8]byte
+ )
+
+ byteOrder.PutUint64(aliasBytes[:], alias.ToUint64())
+ byteOrder.PutUint64(baseBytes[:], baseScid.ToUint64())
+ return aliasToBaseBucket.Put(aliasBytes[:], baseBytes[:])
+ }, func() {})
+ if err != nil {
+ return err
+ }
+
+ // Update the aliasToBase and baseToSet maps.
+ m.baseToSet[baseScid] = append(m.baseToSet[baseScid], alias)
+
+ // Only store the gossiper map if gossip is true.
+ if gossip {
+ m.aliasToBase[alias] = baseScid
+ }
+
+ // We definitely need to unlock the Manager before calling the link
+ // updater. If we don't, we'll deadlock. We use a sync.Once to ensure
+ // that we only unlock once.
+ unlock()
+
+ // Finally, we trigger a htlcswitch update if the flag is set, in order
+ // for any future htlc that references the added alias to be properly
+ // routed.
+ if linkUpdate {
+ return m.linkAliasUpdater(baseScid)
+ }
+
+ return nil
+}
+
+// GetAliases fetches the set of aliases stored under a given base SCID from
+// write-through caches.
+func (m *Manager) GetAliases(
+ base lnwire.ShortChannelID) []lnwire.ShortChannelID {
+
+ m.RLock()
+ defer m.RUnlock()
+
+ aliasSet, ok := m.baseToSet[base]
+ if ok {
+ // Copy the found alias slice.
+ setCopy := make([]lnwire.ShortChannelID, len(aliasSet))
+ copy(setCopy, aliasSet)
+ return setCopy
+ }
+
+ return nil
+}
+
+// FindBaseSCID finds the base SCID for a given alias. This is used in the
+// gossiper to find the correct SCID to lookup in the graph database.
+func (m *Manager) FindBaseSCID(
+ alias lnwire.ShortChannelID) (lnwire.ShortChannelID, error) {
+
+ m.RLock()
+ defer m.RUnlock()
+
+ base, ok := m.aliasToBase[alias]
+ if ok {
+ return base, nil
+ }
+
+ return lnwire.ShortChannelID{}, errNoBase
+}
+
+// DeleteSixConfs removes a mapping for the gossiper once six confirmations
+// have been reached and the channel is public. At this point, only the
+// confirmed SCID should be used.
+func (m *Manager) DeleteSixConfs(baseScid lnwire.ShortChannelID) error {
+ m.Lock()
+ defer m.Unlock()
+
+ err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
+ baseConfBucket, err := tx.CreateTopLevelBucket(confirmedBucket)
+ if err != nil {
+ return err
+ }
+
+ var baseBytes [8]byte
+ byteOrder.PutUint64(baseBytes[:], baseScid.ToUint64())
+ return baseConfBucket.Put(baseBytes[:], []byte{})
+ }, func() {})
+ if err != nil {
+ return err
+ }
+
+ // Now that the database state has been updated, we'll delete all of
+ // the aliasToBase mappings for this SCID.
+ for alias, base := range m.aliasToBase {
+ if base.ToUint64() == baseScid.ToUint64() {
+ delete(m.aliasToBase, alias)
+ }
+ }
+
+ return nil
+}
+
+// DeleteLocalAlias removes a mapping from the database and the Manager's maps.
+func (m *Manager) DeleteLocalAlias(alias,
+ baseScid lnwire.ShortChannelID) error {
+
+ // We need to lock the manager for the whole duration of this method,
+ // except for the very last part where we call the link updater. In
+ // order for us to safely use a defer _and_ still be able to manually
+ // unlock, we use a sync.Once.
+ m.Lock()
+ unlockOnce := sync.Once{}
+ unlock := func() {
+ unlockOnce.Do(m.Unlock)
+ }
+ defer unlock()
+
+ err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
+ aliasToBaseBucket, err := tx.CreateTopLevelBucket(aliasBucket)
+ if err != nil {
+ return err
+ }
+
+ var aliasBytes [8]byte
+ byteOrder.PutUint64(aliasBytes[:], alias.ToUint64())
+
+ // If the user attempts to delete an alias that doesn't exist,
+ // we'll want to inform them about it and not just do nothing.
+ if aliasToBaseBucket.Get(aliasBytes[:]) == nil {
+ return ErrAliasNotFound
+ }
+
+ return aliasToBaseBucket.Delete(aliasBytes[:])
+ }, func() {})
+ if err != nil {
+ return err
+ }
+
+ // Now that the database state has been updated, we'll delete the
+ // mapping from the Manager's maps.
+ aliasSet, ok := m.baseToSet[baseScid]
+ if !ok {
+ return ErrAliasNotFound
+ }
+
+ // We'll filter the alias set and remove the alias from it.
+ aliasSet = fn.Filter(aliasSet, func(a lnwire.ShortChannelID) bool {
+ return a.ToUint64() != alias.ToUint64()
+ })
+
+ // If the alias set is empty, we'll delete the base SCID from the
+ // baseToSet map.
+ if len(aliasSet) == 0 {
+ delete(m.baseToSet, baseScid)
+ } else {
+ m.baseToSet[baseScid] = aliasSet
+ }
+
+ // Finally, we'll delete the aliasToBase mapping from the Manager's
+ // cache (but this is only set if we gossip the alias).
+ delete(m.aliasToBase, alias)
+
+ // We definitely need to unlock the Manager before calling the link
+ // updater. If we don't, we'll deadlock. We use a sync.Once to ensure
+ // that we only unlock once.
+ unlock()
+
+ return m.linkAliasUpdater(baseScid)
+}
+
+// PutPeerAlias stores the peer's alias SCID once we learn of it in the
+// channel_ready message.
+func (m *Manager) PutPeerAlias(chanID lnwire.ChannelID,
+ alias lnwire.ShortChannelID) error {
+
+ m.Lock()
+ defer m.Unlock()
+
+ err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
+ bucket, err := tx.CreateTopLevelBucket(invoiceAliasBucket)
+ if err != nil {
+ return err
+ }
+
+ var scratch [8]byte
+ byteOrder.PutUint64(scratch[:], alias.ToUint64())
+ return bucket.Put(chanID[:], scratch[:])
+ }, func() {})
+ if err != nil {
+ return err
+ }
+
+ // Now that the database state has been updated, we can update it in
+ // our cache.
+ m.peerAlias[chanID] = alias
+
+ return nil
+}
+
+// GetPeerAlias retrieves a peer's alias SCID by the channel's ChanID.
+func (m *Manager) GetPeerAlias(chanID lnwire.ChannelID) (lnwire.ShortChannelID,
+ error) {
+
+ m.RLock()
+ defer m.RUnlock()
+
+ alias, ok := m.peerAlias[chanID]
+ if !ok || alias == hop.Source {
+ return lnwire.ShortChannelID{}, errNoPeerAlias
+ }
+
+ return alias, nil
+}
+
+// RequestAlias returns a new ALIAS ShortChannelID to the caller by allocating
+// the next un-allocated ShortChannelID. The starting ShortChannelID is
+// 16000000:0:0 and the ending ShortChannelID is 16250000:16777215:65535. This
+// gives roughly 2^58 possible ALIAS ShortChannelIDs which ensures this space
+// won't get exhausted.
+func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
+ var nextAlias lnwire.ShortChannelID
+
+ m.RLock()
+ defer m.RUnlock()
+
+ // haveAlias returns true if the passed alias is already assigned to a
+ // channel in the baseToSet map.
+ haveAlias := func(maybeNextAlias lnwire.ShortChannelID) bool {
+ return fn.Any(
+ slices.Collect(maps.Values(m.baseToSet)),
+ func(aliasList []lnwire.ShortChannelID) bool {
+ return fn.Any(
+ aliasList,
+ func(alias lnwire.ShortChannelID) bool {
+ return alias == maybeNextAlias
+ },
+ )
+ },
+ )
+ }
+
+ err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
+ bucket, err := tx.CreateTopLevelBucket(aliasAllocBucket)
+ if err != nil {
+ return err
+ }
+
+ lastBytes := bucket.Get(lastAliasKey)
+ if lastBytes == nil {
+ // If the key does not exist, then we can write the
+ // StartingAlias to it.
+ nextAlias = StartingAlias
+
+ // If the very first alias is already assigned, we'll
+ // keep incrementing until we find an unassigned alias.
+ // This is to avoid collision with custom added SCID
+ // aliases that fall into the same range as the ones we
+ // generate here monotonically. Those custom SCIDs are
+ // stored in a different bucket, but we can just check
+ // the in-memory map for simplicity.
+ for {
+ if !haveAlias(nextAlias) {
+ break
+ }
+
+ nextAlias = getNextScid(nextAlias)
+
+ // Abort if we've reached the end of the range.
+ if nextAlias.BlockHeight >=
+ AliasEndBlockHeight {
+
+ return fmt.Errorf("range for custom " +
+ "aliases exhausted")
+ }
+ }
+
+ var scratch [8]byte
+ byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
+ return bucket.Put(lastAliasKey, scratch[:])
+ }
+
+ // Otherwise the key does exist so we can convert the retrieved
+ // lastAlias to a ShortChannelID and use it to assign the next
+ // ShortChannelID. This next ShortChannelID will then be
+ // persisted in the database.
+ lastScid := lnwire.NewShortChanIDFromInt(
+ byteOrder.Uint64(lastBytes),
+ )
+ nextAlias = getNextScid(lastScid)
+
+ // If the next alias is already assigned, we'll keep
+ // incrementing until we find an unassigned alias. This is to
+ // avoid collision with custom added SCID aliases that fall into
+ // the same range as the ones we generate here monotonically.
+ // Those custom SCIDs are stored in a different bucket, but we
+ // can just check the in-memory map for simplicity.
+ for {
+ if !haveAlias(nextAlias) {
+ break
+ }
+
+ nextAlias = getNextScid(nextAlias)
+
+ // Abort if we've reached the end of the range.
+ if nextAlias.BlockHeight >= AliasEndBlockHeight {
+ return fmt.Errorf("range for custom " +
+ "aliases exhausted")
+ }
+ }
+
+ var scratch [8]byte
+ byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
+ return bucket.Put(lastAliasKey, scratch[:])
+ }, func() {
+ nextAlias = lnwire.ShortChannelID{}
+ })
+ if err != nil {
+ return nextAlias, err
+ }
+
+ return nextAlias, nil
+}
+
+// ListAliases returns a carbon copy of baseToSet. This is used by the rpc
+// layer.
+func (m *Manager) ListAliases() ScidAliasMap {
+ m.RLock()
+ defer m.RUnlock()
+
+ baseCopy := make(ScidAliasMap)
+
+ for k, v := range m.baseToSet {
+ setCopy := make([]lnwire.ShortChannelID, len(v))
+ copy(setCopy, v)
+ baseCopy[k] = setCopy
+ }
+
+ return baseCopy
+}
+
+// getNextScid is a utility function that returns the next SCID for a given
+// alias SCID. The BlockHeight ranges from [16000000, 16250000], the TxIndex
+// ranges from [1, 16777215], and the TxPosition ranges from [1, 65535].
+func getNextScid(last lnwire.ShortChannelID) lnwire.ShortChannelID {
+ var (
+ next lnwire.ShortChannelID
+ incrementIdx bool
+ incrementHeight bool
+ )
+
+ // If the TxPosition is 65535, then it goes to 0 and we need to
+ // increment the TxIndex.
+ if last.TxPosition == 65535 {
+ incrementIdx = true
+ }
+
+ // If the TxIndex is 16777215 and we need to increment it, then it goes
+ // to 0 and we need to increment the BlockHeight.
+ if last.TxIndex == 16777215 && incrementIdx {
+ incrementIdx = false
+ incrementHeight = true
+ }
+
+ switch {
+ // If we increment the TxIndex, then TxPosition goes to 0.
+ case incrementIdx:
+ next.BlockHeight = last.BlockHeight
+ next.TxIndex = last.TxIndex + 1
+ next.TxPosition = 0
+
+ // If we increment the BlockHeight, then the Tx fields go to 0.
+ case incrementHeight:
+ next.BlockHeight = last.BlockHeight + 1
+ next.TxIndex = 0
+ next.TxPosition = 0
+
+ // Otherwise, we only need to increment the TxPosition.
+ default:
+ next.BlockHeight = last.BlockHeight
+ next.TxIndex = last.TxIndex
+ next.TxPosition = last.TxPosition + 1
+ }
+
+ return next
+}
+
+// IsAlias returns true if the passed SCID is an alias. The function determines
+// this by looking at the BlockHeight. If the BlockHeight is greater than
+// AliasStartBlockHeight and less than AliasEndBlockHeight, then it is an alias
+// assigned by RequestAlias. These bounds only apply to aliases we generate.
+// Our peers are free to use any range they choose.
+func IsAlias(scid lnwire.ShortChannelID) bool {
+ return scid.BlockHeight >= AliasStartBlockHeight &&
+ scid.BlockHeight < AliasEndBlockHeight
+}
diff --git a/aliasmgr/aliasmgr_test.go b/aliasmgr/aliasmgr_test.go
new file mode 100644
index 0000000..b288ade
--- /dev/null
+++ b/aliasmgr/aliasmgr_test.go
@@ -0,0 +1,259 @@
+package aliasmgr
+
+import (
+ "math/rand"
+ "path/filepath"
+ "testing"
+
+ "github.com/lightningnetwork/lnd/kvdb"
+ "github.com/lightningnetwork/lnd/lnwire"
+ "github.com/stretchr/testify/require"
+)
+
+// TestAliasStorePeerAlias tests that putting and retrieving a peer's alias
+// works properly.
+func TestAliasStorePeerAlias(t *testing.T) {
+ t.Parallel()
+
+ // Create the backend database and use this to create the aliasStore.
+ dbPath := filepath.Join(t.TempDir(), "testdb")
+ db, err := kvdb.Create(
+ kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
+ false,
+ )
+ require.NoError(t, err)
+ defer db.Close()
+
+ linkUpdater := func(shortID lnwire.ShortChannelID) error {
+ return nil
+ }
+
+ aliasStore, err := NewManager(db, linkUpdater)
+ require.NoError(t, err)
+
+ var chanID1 [32]byte
+ _, err = rand.Read(chanID1[:])
+ require.NoError(t, err)
+
+ // Test that we can put the (chanID, alias) mapping in the database.
+ // Also check that we retrieve exactly what we put in.
+ err = aliasStore.PutPeerAlias(chanID1, StartingAlias)
+ require.NoError(t, err)
+
+ storedAlias, err := aliasStore.GetPeerAlias(chanID1)
+ require.NoError(t, err)
+ require.Equal(t, StartingAlias, storedAlias)
+}
+
+// TestAliasStoreRequest tests that the aliasStore delivers the expected SCID.
+func TestAliasStoreRequest(t *testing.T) {
+ t.Parallel()
+
+ // Create the backend database and use this to create the aliasStore.
+ dbPath := filepath.Join(t.TempDir(), "testdb")
+ db, err := kvdb.Create(
+ kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
+ false,
+ )
+ require.NoError(t, err)
+ defer db.Close()
+
+ linkUpdater := func(shortID lnwire.ShortChannelID) error {
+ return nil
+ }
+
+ aliasStore, err := NewManager(db, linkUpdater)
+ require.NoError(t, err)
+
+ // We'll assert that the very first alias we receive is StartingAlias.
+ alias1, err := aliasStore.RequestAlias()
+ require.NoError(t, err)
+ require.Equal(t, StartingAlias, alias1)
+
+ // The next alias should be the result of passing in StartingAlias to
+ // getNextScid.
+ nextAlias := getNextScid(alias1)
+ alias2, err := aliasStore.RequestAlias()
+ require.NoError(t, err)
+ require.Equal(t, nextAlias, alias2)
+}
+
+// TestAliasLifecycle tests that the aliases can be created and deleted.
+func TestAliasLifecycle(t *testing.T) {
+ t.Parallel()
+
+ // Create the backend database and use this to create the aliasStore.
+ dbPath := filepath.Join(t.TempDir(), "testdb")
+ db, err := kvdb.Create(
+ kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
+ false,
+ )
+ require.NoError(t, err)
+ defer db.Close()
+
+ updateChan := make(chan struct{}, 1)
+
+ linkUpdater := func(shortID lnwire.ShortChannelID) error {
+ updateChan <- struct{}{}
+ return nil
+ }
+
+ aliasStore, err := NewManager(db, linkUpdater)
+ require.NoError(t, err)
+
+ const (
+ base = uint64(123123123)
+ alias = uint64(456456456)
+ )
+
+ // Parse the aliases and base to short channel ID format.
+ baseScid := lnwire.NewShortChanIDFromInt(base)
+ aliasScid := lnwire.NewShortChanIDFromInt(alias)
+ aliasScid2 := lnwire.NewShortChanIDFromInt(alias + 1)
+
+ // Add the first alias.
+ err = aliasStore.AddLocalAlias(aliasScid, baseScid, false, true)
+ require.NoError(t, err)
+
+ // The link updater should be called.
+ <-updateChan
+
+ // Query the aliases and verify the results.
+ aliasList := aliasStore.GetAliases(baseScid)
+ require.Len(t, aliasList, 1)
+ require.Contains(t, aliasList, aliasScid)
+
+ // Add the second alias.
+ err = aliasStore.AddLocalAlias(aliasScid2, baseScid, false, true)
+ require.NoError(t, err)
+
+ // The link updater should be called.
+ <-updateChan
+
+ // Query the aliases and verify the results.
+ aliasList = aliasStore.GetAliases(baseScid)
+ require.Len(t, aliasList, 2)
+ require.Contains(t, aliasList, aliasScid)
+ require.Contains(t, aliasList, aliasScid2)
+
+ // Delete the first alias.
+ err = aliasStore.DeleteLocalAlias(aliasScid, baseScid)
+ require.NoError(t, err)
+
+ // The link updater should be called.
+ <-updateChan
+
+ // We expect to get an error if we attempt to delete the same alias
+ // again.
+ err = aliasStore.DeleteLocalAlias(aliasScid, baseScid)
+ require.ErrorIs(t, err, ErrAliasNotFound)
+
+ // The link updater should _not_ be called.
+ select {
+ case <-updateChan:
+ t.Fatal("link alias updater should not have been called")
+ default:
+ }
+
+ // Query the aliases and verify that first one doesn't exist anymore.
+ aliasList = aliasStore.GetAliases(baseScid)
+ require.Len(t, aliasList, 1)
+ require.Contains(t, aliasList, aliasScid2)
+ require.NotContains(t, aliasList, aliasScid)
+
+ // Delete the second alias.
+ err = aliasStore.DeleteLocalAlias(aliasScid2, baseScid)
+ require.NoError(t, err)
+
+ // The link updater should be called.
+ <-updateChan
+
+ // Query the aliases and verify that none exists.
+ aliasList = aliasStore.GetAliases(baseScid)
+ require.Len(t, aliasList, 0)
+
+ // We now request an alias generated by the aliasStore. This should give
+ // the first from the pre-defined list of allocated aliases.
+ firstRequested, err := aliasStore.RequestAlias()
+ require.NoError(t, err)
+ require.Equal(t, StartingAlias, firstRequested)
+
+ // We now manually add the next alias from the range as a custom alias.
+ secondAlias := getNextScid(firstRequested)
+ err = aliasStore.AddLocalAlias(secondAlias, baseScid, false, true)
+ require.NoError(t, err)
+
+ // When we now request another alias from the allocation list, we expect
+ // the third one (tx position 2) to be returned.
+ thirdRequested, err := aliasStore.RequestAlias()
+ require.NoError(t, err)
+ require.Equal(t, getNextScid(secondAlias), thirdRequested)
+ require.EqualValues(t, 2, thirdRequested.TxPosition)
+}
+
+// TestGetNextScid tests that given a current lnwire.ShortChannelID,
+// getNextScid returns the expected alias to use next.
+func TestGetNextScid(t *testing.T) {
+ tests := []struct {
+ name string
+ current lnwire.ShortChannelID
+ expected lnwire.ShortChannelID
+ }{
+ {
+ name: "starting alias",
+ current: StartingAlias,
+ expected: lnwire.ShortChannelID{
+ BlockHeight: AliasStartBlockHeight,
+ TxIndex: 0,
+ TxPosition: 1,
+ },
+ },
+ {
+ name: "txposition rollover",
+ current: lnwire.ShortChannelID{
+ BlockHeight: 16_100_000,
+ TxIndex: 15,
+ TxPosition: 65535,
+ },
+ expected: lnwire.ShortChannelID{
+ BlockHeight: 16_100_000,
+ TxIndex: 16,
+ TxPosition: 0,
+ },
+ },
+ {
+ name: "txindex max no rollover",
+ current: lnwire.ShortChannelID{
+ BlockHeight: 16_100_000,
+ TxIndex: 16777215,
+ TxPosition: 15,
+ },
+ expected: lnwire.ShortChannelID{
+ BlockHeight: 16_100_000,
+ TxIndex: 16777215,
+ TxPosition: 16,
+ },
+ },
+ {
+ name: "txindex rollover",
+ current: lnwire.ShortChannelID{
+ BlockHeight: 16_100_000,
+ TxIndex: 16777215,
+ TxPosition: 65535,
+ },
+ expected: lnwire.ShortChannelID{
+ BlockHeight: 16_100_001,
+ TxIndex: 0,
+ TxPosition: 0,
+ },
+ },
+ }
+
+ for _, test := range tests {
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ nextScid := getNextScid(test.current)
+ require.Equal(t, test.expected, nextScid)
+ })
+ }
+}
diff --git a/amp/child.go b/amp/child.go
new file mode 100644
index 0000000..5614625
--- /dev/null
+++ b/amp/child.go
@@ -0,0 +1,92 @@
+package amp
+
+import (
+ "crypto/sha256"
+ "encoding/binary"
+ "fmt"
+
+ "github.com/lightningnetwork/lnd/lntypes"
+)
+
+// Share represents an n-of-n sharing of a secret 32-byte value. The secret can
+// be recovered by XORing all n shares together.
+type Share [32]byte
+
+// Xor stores the byte-wise xor of shares x and y in z.
+func (z *Share) Xor(x, y *Share) {
+ for i := range z {
+ z[i] = x[i] ^ y[i]
+ }
+}
+
+// ChildDesc contains the information necessary to derive a child hash/preimage
+// pair that is attached to a particular HTLC. This information will be known by
+// both the sender and receiver in the process of fulfilling an AMP payment.
+type ChildDesc struct {
+ // Share is one of n shares of the root seed. Once all n shares are
+ // known to the receiver, the Share will also provide entropy to the
+ // derivation of child hash and preimage.
+ Share Share
+
+ // Index is 32-bit value that can be used to derive up to 2^32 child
+ // hashes and preimages from a single Share. This allows the payment
+ // hashes sent over the network to be refreshed without needing to
+ // modify the Share.
+ Index uint32
+}
+
+// Child is a payment hash and preimage pair derived from the root seed. In
+// addition to the derived values, a Child carries all information required in
+// the derivation apart from the root seed (unless n=1).
+type Child struct {
+ // ChildDesc contains the data required to derive the child hash and
+ // preimage below.
+ ChildDesc
+
+ // Preimage is the child payment preimage that can be used to settle the
+ // HTLC carrying Hash.
+ Preimage lntypes.Preimage
+
+ // Hash is the child payment hash that to be carried by the HTLC.
+ Hash lntypes.Hash
+}
+
+// String returns a human-readable description of a Child.
+func (c *Child) String() string {
+ return fmt.Sprintf("share=%x, index=%d -> preimage=%v, hash=%v",
+ c.Share, c.Index, c.Preimage, c.Hash)
+}
+
+// DeriveChild computes the child preimage and child hash for a given (root,
+// share, index) tuple. The derivation is defined as:
+//
+// child_preimage = SHA256(root || share || be32(index)),
+// child_hash = SHA256(child_preimage).
+func DeriveChild(root Share, desc ChildDesc) *Child {
+ var (
+ indexBytes [4]byte
+ preimage lntypes.Preimage
+ hash lntypes.Hash
+ )
+
+ // Serialize the child index in big-endian order.
+ binary.BigEndian.PutUint32(indexBytes[:], desc.Index)
+
+ // Compute child_preimage as SHA256(root || share || child_index).
+ h := sha256.New()
+ _, _ = h.Write(root[:])
+ _, _ = h.Write(desc.Share[:])
+ _, _ = h.Write(indexBytes[:])
+ copy(preimage[:], h.Sum(nil))
+
+ // Compute child_hash as SHA256(child_preimage).
+ h = sha256.New()
+ _, _ = h.Write(preimage[:])
+ copy(hash[:], h.Sum(nil))
+
+ return &Child{
+ ChildDesc: desc,
+ Preimage: preimage,
+ Hash: hash,
+ }
+}
diff --git a/amp/derivation_test.go b/amp/derivation_test.go
new file mode 100644
index 0000000..af8162d
--- /dev/null
+++ b/amp/derivation_test.go
@@ -0,0 +1,141 @@
+package amp_test
+
+import (
+ "testing"
+
+ "github.com/lightningnetwork/lnd/amp"
+ "github.com/stretchr/testify/require"
+)
+
+type sharerTest struct {
+ name string
+ numShares int
+ merge bool
+}
+
+var sharerTests = []sharerTest{
+ {
+ name: "root only",
+ numShares: 1,
+ },
+ {
+ name: "two shares",
+ numShares: 2,
+ },
+ {
+ name: "many shares",
+ numShares: 10,
+ },
+ {
+ name: "merge 4 shares",
+ numShares: 4,
+ merge: true,
+ },
+ {
+ name: "merge many shares",
+ numShares: 20,
+ merge: true,
+ },
+}
+
+// TestSharer executes the end-to-end derivation between sender and receiver,
+// asserting that shares are properly computed and, when reconstructed by the
+// receiver, produce identical child hashes and preimages as the sender.
+func TestSharer(t *testing.T) {
+ for _, test := range sharerTests {
+ test := test
+ t.Run(test.name, func(t *testing.T) {
+ t.Parallel()
+
+ testSharer(t, test)
+ })
+ }
+}
+
+func testSharer(t *testing.T, test sharerTest) {
+ // Construct a new sharer with a random seed.
+ var (
+ sharer amp.Sharer
+ err error
+ )
+ sharer, err = amp.NewSeedSharer()
+ require.NoError(t, err)
+
+ // Assert that we can instantiate an equivalent root sharer using the
+ // root share.
+ root := sharer.Root()
+ sharerFromRoot := amp.SeedSharerFromRoot(&root)
+ require.Equal(t, sharer, sharerFromRoot)
+
+ // Generate numShares-1 randomized shares.
+ children := make([]*amp.Child, 0, test.numShares)
+ for i := 0; i < test.numShares-1; i++ {
+ var left amp.Sharer
+ left, sharer, err = sharer.Split()
+ require.NoError(t, err)
+
+ child := left.Child(0)
+
+ assertChildShare(t, child, 0)
+ children = append(children, child)
+ }
+
+ // Compute the final share and finalize the sharing.
+ child := sharer.Child(0)
+ sharer = sharer.Zero()
+
+ assertChildShare(t, child, 0)
+ children = append(children, child)
+
+ // If we are testing merging, merge half of the created children back
+ // into the sharer.
+ if test.merge {
+ for i := len(children) / 2; i < len(children); i++ {
+ sharer = sharer.Merge(children[i])
+ }
+ children = children[:len(children)/2]
+
+ // We must create a new last child from what we just merged
+ // back.
+ child := sharer.Child(0)
+
+ assertChildShare(t, child, 0)
+ children = append(children, child)
+ }
+
+ assertReconstruction(t, children...)
+}
+
+// assertChildShare checks that the child has the expected child index, and that
+// the child's preimage is valid for the its hash.
+func assertChildShare(t *testing.T, child *amp.Child, expIndex int) {
+ t.Helper()
+
+ require.Equal(t, uint32(expIndex), child.Index)
+ require.True(t, child.Preimage.Matches(child.Hash))
+}
+
+// assertReconstruction takes a list of children and simulates the receiver
+// recombining the shares, and then deriving the child preimage and hash for
+// each HTLC. This asserts that the receiver can always rederive the full set of
+// children knowing only the shares and child indexes for each.
+func assertReconstruction(t *testing.T, children ...*amp.Child) {
+ t.Helper()
+
+ // Reconstruct a child descriptor for each of the provided children.
+ // In practice, the receiver will only know the share and the child
+ // index it learns for each HTLC.
+ descs := make([]amp.ChildDesc, 0, len(children))
+ for _, child := range children {
+ descs = append(descs, amp.ChildDesc{
+ Share: child.Share,
+ Index: child.Index,
+ })
+ }
+
+ // Now, recombine the shares and rederive a child for each of the
+ // descriptors above. The resulting set of children should exactly match
+ // the set provided.
+ children2 := amp.ReconstructChildren(descs...)
+ require.Equal(t, children, children2)
+}
diff --git a/amp/shard_tracker.go b/amp/shard_tracker.go
new file mode 100644
index 0000000..473447e
--- /dev/null
+++ b/amp/shard_tracker.go
@@ -0,0 +1,165 @@
+package amp
+
+import (
+ "crypto/rand"
+ "encoding/binary"
+ "fmt"
+ "sync"
+
+ "github.com/lightningnetwork/lnd/lntypes"
+ "github.com/lightningnetwork/lnd/lnwire"
+ "github.com/lightningnetwork/lnd/record"
+ "github.com/lightningnetwork/lnd/routing/shards"
+)
+
+// Shard is an implementation of the shards.PaymentShards interface specific
+// to AMP payments.
+type Shard struct {
+ child *Child
+ mpp *record.MPP
+ amp *record.AMP
+}
+
+// A compile time check to ensure Shard implements the shards.PaymentShard
+// interface.
+var _ shards.PaymentShard = (*Shard)(nil)
+
+// Hash returns the hash used for the HTLC representing this AMP shard.
+func (s *Shard) Hash() lntypes.Hash {
+ return s.child.Hash
+}
+
+// MPP returns any extra MPP records that should be set for the final hop on
+// the route used by this shard.
+func (s *Shard) MPP() *record.MPP {
+ return s.mpp
+}
+
+// AMP returns any extra AMP records that should be set for the final hop on
+// the route used by this shard.
+func (s *Shard) AMP() *record.AMP {
+ return s.amp
+}
+
+// ShardTracker is an implementation of the shards.ShardTracker interface
+// that is able to generate payment shards according to the AMP splitting
+// algorithm. It can be used to generate new hashes to use for HTLCs, and also
+// cancel shares used for failed payment shards.
+type ShardTracker struct {
+ setID [32]byte
+ paymentAddr [32]byte
+ totalAmt lnwire.MilliSatoshi
+
+ sharer Sharer
+
+ shards map[uint64]*Child
+ sync.Mutex
+}
+
+// A compile time check to ensure ShardTracker implements the
+// shards.ShardTracker interface.
+var _ shards.ShardTracker = (*ShardTracker)(nil)
+
+// NewShardTracker creates a new shard tracker to use for AMP payments. The
+// root shard, setID, payment address and total amount must be correctly set in
+// order for the TLV options to include with each shard to be created
+// correctly.
+func NewShardTracker(root, setID, payAddr [32]byte,
+ totalAmt lnwire.MilliSatoshi) *ShardTracker {
+
+ // Create a new seed sharer from this root.
+ rootShare := Share(root)
+ rootSharer := SeedSharerFromRoot(&rootShare)
+
+ return &ShardTracker{
+ setID: setID,
+ paymentAddr: payAddr,
+ totalAmt: totalAmt,
+ sharer: rootSharer,
+ shards: make(map[uint64]*Child),
+ }
+}
+
+// NewShard registers a new attempt with the ShardTracker and returns a
+// new shard representing this attempt. This attempt's shard should be canceled
+// if it ends up not being used by the overall payment, i.e. if the attempt
+// fails.
+func (s *ShardTracker) NewShard(pid uint64, last bool) (shards.PaymentShard,
+ error) {
+
+ s.Lock()
+ defer s.Unlock()
+
+ // Use a random child index.
+ var childIndex [4]byte
+ if _, err := rand.Read(childIndex[:]); err != nil {
+ return nil, err
+ }
+ idx := binary.BigEndian.Uint32(childIndex[:])
+
+ // Depending on whether we are requesting the last shard or not, either
+ // split the current share into two, or get a Child directly from the
+ // current sharer.
+ var child *Child
+ if last {
+ child = s.sharer.Child(idx)
+
+ // If this was the last shard, set the current share to the
+ // zero share to indicate we cannot split it further.
+ s.sharer = s.sharer.Zero()
+ } else {
+ left, sharer, err := s.sharer.Split()
+ if err != nil {
+ return nil, err
+ }
+
+ s.sharer = sharer
+ child = left.Child(idx)
+ }
+
+ // Track the new child and return the shard.
+ s.shards[pid] = child
+
+ mpp := record.NewMPP(s.totalAmt, s.paymentAddr)
+ amp := record.NewAMP(
+ child.ChildDesc.Share, s.setID, child.ChildDesc.Index,
+ )
+
+ return &Shard{
+ child: child,
+ mpp: mpp,
+ amp: amp,
+ }, nil
+}
+
+// CancelShard cancel's the shard corresponding to the given attempt ID.
+func (s *ShardTracker) CancelShard(pid uint64) error {
+ s.Lock()
+ defer s.Unlock()
+
+ c, ok := s.shards[pid]
+ if !ok {
+ return fmt.Errorf("pid not found")
+ }
+ delete(s.shards, pid)
+
+ // Now that we are canceling this shard, we XOR the share back into our
+ // current share.
+ s.sharer = s.sharer.Merge(c)
+ return nil
+}
+
+// GetHash retrieves the hash used by the shard of the given attempt ID. This
+// will return an error if the attempt ID is unknown.
+func (s *ShardTracker) GetHash(pid uint64) (lntypes.Hash, error) {
+ s.Lock()
+ defer s.Unlock()
+
+ c, ok := s.shards[pid]
+ if !ok {
+ return lntypes.Hash{}, fmt.Errorf("AMP shard for attempt %v "+
+ "not found", pid)
+ }
+
+ return c.Hash, nil
+}
diff --git a/amp/shard_trWhy this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.