diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7591492088..a67fed64e1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,6 +30,8 @@ env: BITCOIND_VERSION: '22.0' BITCOIND_IMAGE: 'lightninglabs/bitcoin-core' + BTCD_VERSION_LATEST: v0.25.0 + jobs: ######################## # Format, compileation and lint check @@ -63,6 +65,15 @@ jobs: - name: Check RPC format run: make rpc-check + - name: Check generated SQL code is up-to-date + run: make sqlc-check + + - name: Check SQL formatting + run: make sql-format-check + + - name: Check SQL linting + run: make sql-lint-check + - name: compile code run: go install -v ./... @@ -112,10 +123,22 @@ jobs: steps: - name: extract bitcoind from docker image run: |- - docker pull ${{ env.BITCOIND_IMAGE }}:${{ env.BITCOIND_VERSION }} - CONTAINER_ID=$(docker create ${{ env.BITCOIND_IMAGE }}:${{ env.BITCOIND_VERSION }}) - sudo docker cp $CONTAINER_ID:/opt/bitcoin-${{ env.BITCOIND_VERSION }}/bin/bitcoind /usr/local/bin/bitcoind + IMAGE=${{ env.BITCOIND_IMAGE }}:${{ env.BITCOIND_VERSION }} + docker pull $IMAGE + + BIN_PATH=$(docker run --rm --entrypoint sh $IMAGE -c ' + command -v bitcoind || + find /opt /usr -type f -name bitcoind 2>/dev/null | head -n 1 + ') + if [ -z "$BIN_PATH" ]; then + echo "could not locate bitcoind in $IMAGE" >&2 + exit 1 + fi + + CONTAINER_ID=$(docker create $IMAGE) + sudo docker cp $CONTAINER_ID:$BIN_PATH /usr/local/bin/bitcoind docker rm $CONTAINER_ID + bitcoind --version - name: git checkout uses: actions/checkout@v5 @@ -123,17 +146,6 @@ jobs: - name: Clean up runner space uses: ./.github/actions/cleanup-space - - name: go cache - uses: actions/cache@v4 - with: - path: /home/runner/work/go - key: btcwallet-${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ github.job }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - btcwallet-${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ github.job }}-${{ hashFiles('**/go.sum') }} - btcwallet-${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ github.job }}- - btcwallet-${{ runner.os }}-go-${{ env.GO_VERSION }}- - btcwallet-${{ runner.os }}-go- - - name: setup go ${{ env.GO_VERSION }} uses: actions/setup-go@v5 with: @@ -143,9 +155,225 @@ jobs: run: make ${{ matrix.unit_type }} - name: Send coverage - uses: shogo82148/actions-goveralls@v1 + uses: coverallsapp/github-action@v2 if: matrix.unit_type == 'unit-cover' continue-on-error: true with: - path-to-profile: coverage.txt + file: coverage.txt + flag-name: unit + format: golang parallel: true + + ######################## + # run integration tests (SQLite + Postgres) + ######################## + itest-db: + name: ${{ matrix.db }} itest (${{ matrix.race && 'race' || 'cover' }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + db: [sqlite, postgres] + race: [false, true] + steps: + - name: git checkout + uses: actions/checkout@v5 + + - name: Clean up runner space + uses: ./.github/actions/cleanup-space + + - name: setup go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: run ${{ matrix.db }} itest-db (coverage) + if: ${{ !matrix.race }} + run: make itest-db db=${{ matrix.db }} cover=1 verbose=1 + + - name: run ${{ matrix.db }} itest-db-race + if: matrix.race + run: make itest-db-race db=${{ matrix.db }} verbose=1 + + - name: Upload coverage to Coveralls + uses: coverallsapp/github-action@v2 + if: ${{ !matrix.race }} + continue-on-error: true + with: + file: coverage-itest-${{ matrix.db }}.txt + flag-name: itest-db-${{ matrix.db }} + format: golang + parallel: true + + ######################## + # Run bwtest integration tests against each supported chain backend. + # + # These jobs currently use kvdb only. Database expansion is planned via a + # separate matrix in a future change. + ######################## + itest-btcd: + name: itest btcd + runs-on: ubuntu-latest + steps: + - name: git checkout + uses: actions/checkout@v5 + + - name: Clean up runner space + uses: ./.github/actions/cleanup-space + + - name: setup go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: add go bin to PATH + run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + + - name: install btcd ${{ env.BTCD_VERSION_LATEST }} + run: go install -v github.com/btcsuite/btcd@${{ env.BTCD_VERSION_LATEST }} + + - name: check btcd version + run: btcd --version + + # The btcd backend job runs btcd for both the shared miner and the chain + # backend under test. + - name: run itest (btcd, kvdb) + run: make itest chain=btcd db=kvdb + + - name: upload itest logs + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: itest-logs-btcd + path: itest/test-logs + retention-days: 5 + + ######################## + # Run bwtest integration tests with the neutrino backend. + # + # Job flow: + # - Install btcd `${{ env.BTCD_VERSION_LATEST }}`. + # - Use btcd as the shared miner for the suite. + # - Run `make itest chain=neutrino db=kvdb` so wallets use the in-process + # neutrino backend while blocks/peers come from btcd. + ######################## + itest-neutrino: + name: itest neutrino + runs-on: ubuntu-latest + steps: + - name: git checkout + uses: actions/checkout@v5 + + - name: Clean up runner space + uses: ./.github/actions/cleanup-space + + - name: setup go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: add go bin to PATH + run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + + - name: install btcd ${{ env.BTCD_VERSION_LATEST }} + run: go install -v github.com/btcsuite/btcd@${{ env.BTCD_VERSION_LATEST }} + + - name: check btcd version + run: btcd --version + + # Neutrino runs in-process, but it still relies on the shared btcd miner + # for chain data and peer connectivity. + - name: run itest (neutrino, kvdb) + run: make itest chain=neutrino db=kvdb + + - name: upload itest logs + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: itest-logs-neutrino + path: itest/test-logs + retention-days: 5 + + ######################## + # Run bwtest integration tests with the bitcoind backend. + # + # Job flow: + # - Install btcd `${{ env.BTCD_VERSION_LATEST }}` as the shared miner. + # - Run a matrix over bitcoind versions `30` (latest) and `28` (older). + # - Extract each matrix binary from the docker image and run + # `make itest chain=bitcoind db=kvdb`. + ######################## + itest-bitcoind: + name: itest bitcoind (v${{ matrix.bitcoind_version }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + bitcoind_version: ['30', '28'] + steps: + - name: git checkout + uses: actions/checkout@v5 + + - name: Clean up runner space + uses: ./.github/actions/cleanup-space + + - name: setup go ${{ env.GO_VERSION }} + uses: actions/setup-go@v5 + with: + go-version: '${{ env.GO_VERSION }}' + + - name: add go bin to PATH + run: echo "$(go env GOPATH)/bin" >> $GITHUB_PATH + + - name: install btcd ${{ env.BTCD_VERSION_LATEST }} + run: go install -v github.com/btcsuite/btcd@${{ env.BTCD_VERSION_LATEST }} + + - name: check btcd version + run: btcd --version + + # For bitcoind backend tests, btcd remains the shared miner while the + # matrix covers both bitcoind v30 (latest) and v28 (older). + - name: extract bitcoind from docker image (${{ matrix.bitcoind_version }}) + run: |- + IMAGE=${{ env.BITCOIND_IMAGE }}:${{ matrix.bitcoind_version }} + docker pull $IMAGE + + BIN_PATH=$(docker run --rm --entrypoint sh $IMAGE -c ' + command -v bitcoind || + find /opt /usr -type f -name bitcoind 2>/dev/null | head -n 1 + ') + if [ -z "$BIN_PATH" ]; then + echo "could not locate bitcoind in $IMAGE" >&2 + exit 1 + fi + + CONTAINER_ID=$(docker create $IMAGE) + sudo docker cp $CONTAINER_ID:$BIN_PATH /usr/local/bin/bitcoind + docker rm $CONTAINER_ID + bitcoind --version + + - name: run itest (bitcoind, kvdb) + run: make itest chain=bitcoind db=kvdb + + - name: upload itest logs + if: ${{ failure() }} + uses: actions/upload-artifact@v4 + with: + name: itest-logs-bitcoind-${{ matrix.bitcoind_version }} + path: itest/test-logs + retention-days: 5 + + ######################## + # Complete parallel coverage uploads + ######################## + finish: + name: Finish coverage upload + if: ${{ !cancelled() }} + needs: [unit-test, itest-db] + runs-on: ubuntu-latest + steps: + - name: Finish parallel Coveralls upload + uses: coverallsapp/github-action@v2 + continue-on-error: true + with: + parallel-finished: true diff --git a/.gitignore b/.gitignore index 25a01ef540..1ab06d6ed7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,11 +2,20 @@ btcwallet vendor .idea coverage.txt +coverage-itest-postgres.txt +coverage-itest-sqlite.txt *.swp .vscode .DS_Store .aider* +/.worktrees/ coverage.out *.prof *.test *cpu.out + +# Integration test logs. +itest/test-logs/ + +# Backwards compatibility for older log dir. +itest/.minerlogs/ diff --git a/.golangci.yml b/.golangci.yml index c457c439d3..ecc9665b8e 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -62,7 +62,12 @@ linters: funlen: # Checks the number of lines in a function. # If lower than 0, disable the check. - lines: 100 + # Increased from 100 to 200 to allow more verbose parameters while still + # keeping low complexity. e.g.: + # func (w *PostgresWalletDB) CreateDerivedAccount(ctx context.Context, + # params CreateDerivedAccountParams) (*AccountInfo, error) { + lines: 200 + # Checks the number of statements in a function. statements: 50 @@ -144,6 +149,13 @@ linters: # Allow returning unwrapped errors in tests. - wrapcheck + # The split backend packages intentionally forward many calls into the + # shared db helpers. Keep wrapcheck enabled elsewhere while the package + # split settles. + - path: wallet/internal/db/(pg|sqlite|kvdb)/.*\.go + linters: + - wrapcheck + - path: mock* linters: - revive @@ -152,6 +164,30 @@ linters: # the convert. - forcetypeassert + # The shared bwtest/mock and wallet/internal/bwtest/mock packages embed + # testify.Mock; the testify pattern triggers magic-number, nil-nil, and + # missing-blank-line-before-return linters that don't apply to mocked + # interface implementations. Exclude those linters for these packages. + - path: bwtest/mock/.*\.go + linters: + - mnd + - nilnil + - nlreturn + - revive + - forcetypeassert + - wrapcheck + - wsl_v5 + + - path: wallet/internal/bwtest/mock/.*\.go + linters: + - mnd + - nilnil + - nlreturn + - revive + - forcetypeassert + - wrapcheck + - wsl_v5 + # Allow fmt.Printf() in commands. - path: cmd/commands/* linters: @@ -168,6 +204,25 @@ linters: - funcorder - ireturn + # Allow deprecated entrypoints to be exercised from the kvdb adapter + # tests during the tx-writer migration. + - path: wallet/internal/db/kvdb/.*_test\.go + linters: + - staticcheck + + # E2E integration tests run a long-lived backend harness and execute test + # cases in registration order so the suite can stop on first failure. + - path: itest/.*_test\.go + linters: + - paralleltest + + # DB integration tests often share a backend fixture across subtests, and + # forcing parallel subtests makes those ordering assumptions unsafe. + - path: wallet/internal/db/itest/.*_test\.go + linters: + - paralleltest + - tparallel + issues: # Show only new issues created after git revision `REV`. # Default: "" diff --git a/.sqlfluff b/.sqlfluff new file mode 100644 index 0000000000..3ae05159ec --- /dev/null +++ b/.sqlfluff @@ -0,0 +1,41 @@ +# SQLFluff configuration +# Source: https://docs.sqlfluff.com/en/stable/configuration/setting_configuration.html + +[sqlfluff] +# Supported dialects: postgres, sqlite +# We specify dialect via CLI (-d flag) to handle both postgres and sqlite +# sql_file_exts = .sql +# exclude_rules = None + +# Standard max line length +max_line_length = 120 + +# Use all available CPU cores for better performance +processes = 0 + +[sqlfluff:indentation] +# Use spaces for indentation +indent_unit = space +tab_space_size = 4 +indented_joins = False +indented_using_on = True + +[sqlfluff:rules:capitalisation.keywords] +# Keywords should be uppercase (e.g., SELECT, FROM, WHERE) +capitalisation_policy = upper + +[sqlfluff:rules:capitalisation.identifiers] +# Table and column names should be lowercase +extended_capitalisation_policy = lower + +[sqlfluff:rules:capitalisation.functions] +# Function names should be lowercase +extended_capitalisation_policy = lower + +[sqlfluff:rules:capitalisation.types] +# Data types should be uppercase (e.g., INTEGER, BYTEA, BLOB) +extended_capitalisation_policy = upper + +[sqlfluff:rules:convention.not_equal] +# Prefer != over <> for not equal +preferred_not_equal_style = c_style diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..787e1ca040 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,132 @@ +# AGENTS.md + +Welcome to the `btcwallet` knowledge base. This file defines the global project + structure, subsystem mappings, and critical operational constraints for all + subagents. + +## SCOPE & PRIORITY +- Follow this file first for repo-specific workflow and style. +- If guidance conflicts, prefer the stricter rule and match nearby code. + +## OVERVIEW +`btcwallet` is a Bitcoin wallet daemon supporting BIP0032 (HD) and BIP0044. It + acts as a client to `btcd`, `bitcoin-core` and `neutrino`, and provides both + legacy JSON-RPC and gRPC APIs to wallet clients. + +## REPO STRUCTURE & MAP +| Path | Component | Description | Sub-Agent Config | +|------------------------|:--------------------------|:-----------------------------------------------------------|:--------------------------------| +| `docs/developer/` | **Docs** | Deep-dive design docs (ADRs) and development guides. | None | +| `wallet/` | **Wallet Core** | Business logic, HD key derivation, transaction building. | `wallet/AGENTS.md` | +| `wallet/internal/db/` | **Database Layer** | Storage drivers, migrations, SQL-based relational logic. | `wallet/internal/db/AGENTS.md` | +| `wallet/internal/sql/` | **SQL Assets** | SQL schema, query, migration, and sqlc codegen. | `wallet/internal/sql/AGENTS.md` | +| `rpc/` | **RPC** | gRPC and legacy JSON-RPC APIs and server implementations. | None | +| `chain/` | **Blockchain** | Sync clients for Neutrino, `btcd` RPC, and `bitcoind` RPC. | None | +| `itest/` | **Integration Scenarios** | Actual daemon-level test cases and E2E flows. | None | +| `bwtest/` | **Harness Layer** | Test harness definitions and backend wrappers. | None | + +## REPO & WORKTREE WORKFLOW +- Keep the main checkout at the repo root. +- Place auxiliary worktrees under `/.worktrees/`. +- Use short, task-oriented worktree names. +- Don't create sibling worktrees outside the repo root unless asked. +- Remove finished worktrees with `git worktree remove `. +- Clean stale worktree metadata with `git worktree prune`. + +## CRITICAL GOTCHAS & CONSTRAINTS +- **Go Version:** The workspace root requires Go `1.24.6`. Nested modules keep + their own `go.mod` metadata and should be tested in their own module context + when touched. +- **Docker-backed Tooling:** Mutating operations (`make fmt`, `make sql`, + `make rpc`, `make lint`) and PostgreSQL integration tests require Docker + (`btcwallet-tools` image). +- **Mutating Checks:** Targets like `make fmt-check`, `make sqlc-check`, + `make rpc-check`, and `make tidy-module-check` **mutate files** before + checking Git cleanliness. Never run them on a dirty tree unless you want + auto-fixes applied. +- **Generated File Boundaries:** Do not manually edit `*.pb.go` or any code in + directories populated by `sqlc` or protobuf generators. Modify source + definitions (SQL/Proto) and run `make sql` or `make rpc`. +- **Test Locations:** Unit tests live alongside code. DB integration tests live + in `wallet/internal/db/itest`. E2E integration tests are orchestrated from + `itest/`. +- **ADR** Before changing architecture-sensitive areas, review + `docs/developer/adr/README.md` for recorded design decisions, context, + tradeoffs, and consequences. + +## VERIFICATION STRATEGY +- Start by running the narrowest relevant test. +- Before handing off a significant change, run at least package-level tests. +- Run the matching generation or check the target for SQL, proto, module, or + config changes. +- Verify backend flow or RPC changes with `make itest`. +- Add or run a benchmark if a change claims performance improvement. +- CI covers formatting, imports, modules, proto, SQL, lint, unit, DB, and e2e. + +## COMMANDS + +### Build & Install +- `make build`: Compile the workspace. +- `make install`: Install `btcwallet`, `dropwtxmgr`, and `sweepaccount` binaries + to `$GOBIN`. + +### Linting, Formatting & Code Generation +- `make fmt`: Fix imports (`gosimports`) and format Go code (`gofmt`). +- `make lint`: Run `golangci-lint` with fix mode enabled (uses Docker). +- `make sql`: Lint, format, and regenerate SQL models/queries (uses + SQLFluff/sqlc). +- `make rpc-format`: Format protobuf definition files. +- `make rpc`: Regenerate gRPC code from protobuf definitions. +- `make tidy-module`: Tidy all Go modules in the workspace. + +### Verification & Checks +- `make fmt-check`: Check Go formatting and imports. +- `make lint-check`: Run linter in check-only mode. +- `make sqlc-check`: Ensure generated SQL code is up to date. +- `make rpc-check`: Ensure generated protobuf code is up to date. +- `make tidy-module-check`: Confirm Go modules are tidy. + +### Testing +- `make unit`: Run all package unit tests. +- `make unit pkg=wallet case=TestName`: Targeted unit test. +- `make unit-race pkg=wallet case=TestName`: Test with race detector enabled. +- `make itest-db db=[sqlite|postgres]`: Perform database layer integration + tests. +- `make itest chain=[btcd|bitcoind|neutrino]`: Start E2E daemon integration + tests. +- `make itest chain=btcd db=kvdb icase=manager`: Filter E2E tests by case name. + +## CONVENTIONS + +Use the developer docs as the source of truth for detailed conventions: + +- `docs/developer/contribution_guidelines.md`: contribution workflow, PRs, + commits, review expectations, and function comment requirements. +- `docs/developer/code_formatting_rules.md`: formatting, layout, naming, and + local style. +- `docs/developer/unit_testing_guidelines.md`: unit test structure, coverage, + and test documentation. +- `docs/developer/ENGINEERING_GUIDE.md`: package design, architecture, error + handling, logging, and concurrency. + +Keep these high-signal reminders in mind when editing code: + +- Let `make fmt` manage imports. Go files use tab indentation. Markdown files + use LF and wrap near 80 columns. +- Avoid generic package names like `utils`, `common`, or `helpers`. +- Put `context.Context` first for blocking or long-running operations. +- Wrap dependency errors with context using `%w` and check sentinels with + `errors.Is`. +- Ensure every goroutine has a clear shutdown path, don't access maps + concurrently without synchronization, and pass sync primitives by pointer. +- Preserve local logging style. Use `error` for unexpected internal failures and + keep structured log messages static. +- Comment every function and method with its intended purpose and assumptions. + Function comments must start with the declaration name; exported APIs also + need the caller-facing detail required by the contribution guide. +- Write regression tests for bug fixes and cover positive and negative paths. + Test comments should explain why the test exists and what it checks. +- Run `make rpc-format` for proto formatting and avoid ad hoc local `replace` + directives in Go submodules. +- Test case names must follow `component action` and must not use `_`. +- E2E test logs are written under `itest/test-logs/`. diff --git a/Makefile b/Makefile index def43ca079..56b7c3ac45 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,18 @@ GOINSTALL := GO111MODULE=on go install -v GOFILES = $(shell find . -type f -name '*.go' -not -name "*.pb.go") + +# SQL directories. +SQL_DIR := wallet/internal/sql + +# SQL file paths. +SQL_POSTGRES_DIR := $(SQL_DIR)/pg +SQL_POSTGRES_MIGRATIONS := $(SQL_POSTGRES_DIR)/migrations +SQL_POSTGRES_QUERIES := $(SQL_POSTGRES_DIR)/queries +SQL_SQLITE_DIR := $(SQL_DIR)/sqlite +SQL_SQLITE_MIGRATIONS := $(SQL_SQLITE_DIR)/migrations +SQL_SQLITE_QUERIES := $(SQL_SQLITE_DIR)/queries + RM := rm -f CP := cp MAKE := make @@ -14,15 +26,58 @@ XARGS := xargs -L 1 include make/testing_flags.mk # Linting uses a lot of memory, so keep it under control by limiting the number -# of workers if requested. -ifneq ($(workers),) +# of workers. Override with `workers=` when needed. +workers ?= 4 LINT_WORKERS = --concurrency=$(workers) -endif + +GOLANGCI_LINT = golangci-lint run -v --config .golangci.yml $(LINT_WORKERS) +LINT_DB_SQLITE_PKGS = ./wallet/internal/db/sqlite ./wallet/internal/db/itest +LINT_DB_POSTGRES_PKGS = ./wallet/internal/db/pg ./wallet/internal/db/itest + +define lint_targets + @$(call print, "Linting source.") + $(DOCKER_TOOLS) $(GOLANGCI_LINT) $(LINT_FIX) + @$(call print, "Linting integration tests.") + $(DOCKER_TOOLS) $(GOLANGCI_LINT) $(LINT_FIX) \ + --build-tags="itest $(DEV_TAGS) nolog" ./itest + @$(call print, "Linting SQLite DB integration tests.") + $(DOCKER_TOOLS) $(GOLANGCI_LINT) $(LINT_FIX) \ + --build-tags="itest $(DEV_TAGS) $(LOG_TAGS)" \ + $(LINT_DB_SQLITE_PKGS) + @$(call print, "Linting PostgreSQL DB integration tests.") + $(DOCKER_TOOLS) $(GOLANGCI_LINT) $(LINT_FIX) \ + --build-tags="itest $(DEV_TAGS) $(LOG_TAGS) test_db_postgres" \ + $(LINT_DB_POSTGRES_PKGS) +endef + +# Detect if we're in a git worktree. Use git rev-parse --git-common-dir to get +# the path to the main git directory for the linter's diff processor to work +# correctly with the new-from-rev setting. +GIT_COMMON_DIR := $(shell \ + common_dir="$$(git rev-parse --git-common-dir 2>/dev/null)"; \ + if [ "$$common_dir" != ".git" ] && [ -n "$$common_dir" ]; then \ + echo "$$common_dir"; \ + fi) +GIT_VOLUME := $(if $(GIT_COMMON_DIR),-v "$(GIT_COMMON_DIR):$(GIT_COMMON_DIR):ro",) DOCKER_TOOLS = docker run \ --rm \ -v $(shell bash -c "mkdir -p /tmp/go-build-cache; echo /tmp/go-build-cache"):/root/.cache/go-build \ - -v $$(pwd):/build btcwallet-tools + -v $$(pwd):/build \ + $(GIT_VOLUME) \ + btcwallet-tools + +# Pin the sqlfluff image by digest to keep linting results stable across +# environments and over time. The tag alone is mutable, so it could later point +# to a different build with changed rules or behavior. This digest currently +# corresponds to sqlfluff 4.2.1 on Docker Hub: +# https://hub.docker.com/layers/sqlfluff/sqlfluff/4.2.1 +SQLFLUFF = docker run \ + --rm \ + --user $$(id -u):$$(id -g) \ + -v $$(pwd):/sql \ + -w /sql \ + sqlfluff/sqlfluff@sha256:0b1f131e3e9b4ac10bf45f1b2cb8680e67f74e63f8e6db9f0f0207dbbe3c71d2 GREEN := "\\033[0;32m" NC := "\\033[0m" @@ -84,6 +139,44 @@ unit-bench: @$(call print, "Running benchmark tests.") $(UNIT_BENCH) +#? itest-db: Run integration tests for wallet database +itest-db: + @if [ -z "$(IT_DB_LABEL)" ]; then \ + echo "Unknown integration test database '$(db)'. Use db=sqlite or db=postgres." ; \ + exit 1 ; \ + fi + @$(call print, "Running $(IT_DB_LABEL) integration tests.") + $(ITEST_DB) + @if [ -n "$(ITEST_DB_COVERPROFILE)" ]; then \ + echo "Filtering coverage report."; \ + ./scripts/filter_coverage.sh $(IT_DB_TYPE); \ + fi + +#? itest-db-race: Run integration tests for wallet database with race detector +itest-db-race: + @if [ -z "$(IT_DB_LABEL)" ]; then \ + echo "Unknown integration test database '$(db)'. Use db=sqlite or db=postgres." ; \ + exit 1 ; \ + fi + @$(call print, "Running $(IT_DB_LABEL) integration tests (race).") + env CGO_ENABLED=1 GORACE="history_size=7 halt_on_errors=1" $(ITEST_DB_RACE) + +#? itest: Run integration tests +#? itest (vars): chain=btcd|bitcoind|neutrino backend=btcd|bitcoind|neutrino db=kvdb|sqlite|postgres +#? itest (vars): icase= (filter itest cases) +#? itest (vars): timeout= verbose=1 nocache=1 +#? itest (ex): make itest icase=manager +#? itest (ex): make itest chain=btcd db=kvdb +itest: + @$(call print, "Running integration tests.") + @$(GOTEST) -v ./itest \ + -tags="itest $(DEV_TAGS) nolog" \ + $(if $(icase),-test.run="TestBtcWallet$$|$(icase)",) \ + $(filter-out -test.run=%,$(TEST_FLAGS)) \ + -args \ + -chain="$(if $(backend),$(backend),$(if $(chain),$(chain),btcd))" \ + -db="$(if $(db),$(db),kvdb)" + # ========= # UTILITIES # ========= @@ -108,17 +201,16 @@ rpc-format: #? lint-config-check: Verify golangci-lint configuration lint-config-check: docker-tools @$(call print, "Verifying golangci-lint configuration.") - $(DOCKER_TOOLS) golangci-lint config verify -v + $(DOCKER_TOOLS) golangci-lint config verify -v --config .golangci.yml #? lint: Lint source and check errors lint-check: lint-config-check - @$(call print, "Linting source.") - $(DOCKER_TOOLS) golangci-lint run -v $(LINT_WORKERS) + $(call lint_targets) #? lint: Lint source and fix +lint: LINT_FIX = --fix lint: lint-config-check - @$(call print, "Linting source.") - $(DOCKER_TOOLS) golangci-lint run -v --fix $(LINT_WORKERS) + $(call lint_targets) #? docker-tools: Build tools docker image docker-tools: @@ -138,7 +230,7 @@ rpc-check: rpc #? protolint: Lint proto files using protolint protolint: @$(call print, "Linting proto files.") - docker run --rm --volume "$$(pwd):/workspace" --workdir /workspace yoheimuta/protolint lint rpc/ + $(DOCKER_TOOLS) protolint lint -config_dir_path=. rpc/ #? sample-conf-check: Make sure default values in the sample-btcwallet.conf file are set correctly sample-conf-check: install @@ -159,6 +251,79 @@ tidy-module: 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 +#? sql-parse: Ensures SQL files are syntactically valid +sql-parse: + @$(call print, "Validating SQL files (postgres migrations).") + $(SQLFLUFF) parse --config /sql/.sqlfluff --dialect postgres $(SQL_POSTGRES_MIGRATIONS) --format none + @$(call print, "Validating SQL files (postgres queries).") + $(SQLFLUFF) parse --config /sql/.sqlfluff --dialect postgres $(SQL_POSTGRES_QUERIES) --format none + @$(call print, "Validating SQL files (sqlite migrations).") + $(SQLFLUFF) parse --config /sql/.sqlfluff --dialect sqlite $(SQL_SQLITE_MIGRATIONS) --format none + @$(call print, "Validating SQL files (sqlite queries).") + $(SQLFLUFF) parse --config /sql/.sqlfluff --dialect sqlite $(SQL_SQLITE_QUERIES) --format none + +#? sqlc: Generate Go code from SQL queries and migrations +sqlc: sql-parse docker-tools + @$(call print, "Generating sql models and queries in Go") + $(DOCKER_TOOLS) sqlc generate -f sqlc.yaml + +#? sqlc-check: Verify generated Go SQL queries and migrations 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 + +#? sql-format: Format SQL migration and query files (like 'make fmt') +sql-format: + @$(call print, "Formatting SQL files (postgres migrations).") + $(SQLFLUFF) format --config /sql/.sqlfluff --dialect postgres $(SQL_POSTGRES_MIGRATIONS) + @$(call print, "Formatting SQL files (postgres queries).") + $(SQLFLUFF) format --config /sql/.sqlfluff --dialect postgres $(SQL_POSTGRES_QUERIES) + @$(call print, "Formatting SQL files (sqlite migrations).") + $(SQLFLUFF) format --config /sql/.sqlfluff --dialect sqlite $(SQL_SQLITE_MIGRATIONS) + @$(call print, "Formatting SQL files (sqlite queries).") + $(SQLFLUFF) format --config /sql/.sqlfluff --dialect sqlite $(SQL_SQLITE_QUERIES) + +#? sql-check: Verify SQL migration and query files are formatted correctly (like 'make fmt-check') +sql-format-check: sql-format + @$(call print, "Checking SQL formatting.") + if test -n "$$(git status --porcelain '$(SQL_DIR)/**/*.sql')"; then echo "SQL files not formatted correctly, please run 'make sql-format' again!"; git status; git diff; exit 1; fi + +#? sql-lint: Lint SQL migration and query files and fix issues (like 'make lint') +sql-lint: + @$(call print, "Linting SQL files (postgres migrations).") + $(SQLFLUFF) fix --config /sql/.sqlfluff --dialect postgres $(SQL_POSTGRES_MIGRATIONS) + @$(call print, "Linting SQL files (postgres queries).") + $(SQLFLUFF) fix --config /sql/.sqlfluff --dialect postgres $(SQL_POSTGRES_QUERIES) + @$(call print, "Linting SQL files (sqlite migrations).") + $(SQLFLUFF) fix --config /sql/.sqlfluff --dialect sqlite $(SQL_SQLITE_MIGRATIONS) + @$(call print, "Linting SQL files (sqlite queries).") + $(SQLFLUFF) fix --config /sql/.sqlfluff --dialect sqlite $(SQL_SQLITE_QUERIES) + +#? sql-lint-check: Lint SQL files and report errors (like 'make lint-check') +sql-lint-check: + @$(call print, "Linting SQL files (postgres migrations).") + $(SQLFLUFF) lint --config /sql/.sqlfluff --dialect postgres $(SQL_POSTGRES_MIGRATIONS) + @$(call print, "Linting SQL files (postgres queries).") + $(SQLFLUFF) lint --config /sql/.sqlfluff --dialect postgres $(SQL_POSTGRES_QUERIES) + @$(call print, "Linting SQL files (sqlite migrations).") + $(SQLFLUFF) lint --config /sql/.sqlfluff --dialect sqlite $(SQL_SQLITE_MIGRATIONS) + @$(call print, "Linting SQL files (sqlite queries).") + $(SQLFLUFF) lint --config /sql/.sqlfluff --dialect sqlite $(SQL_SQLITE_QUERIES) + +#? sql: Lint, verify, format, and regenerate SQL code for local development +# First try auto-fixes. If that still fails, rerun lint in check mode so the +# remaining unfixable violations are printed before aborting the workflow. +# On success, verify the final SQL is clean before formatting and regeneration. +sql: + @if ! $(MAKE) --no-print-directory --silent sql-lint; then \ + $(MAKE) --no-print-directory --silent sql-lint-check; \ + exit 1; \ + fi + @$(MAKE) --no-print-directory --silent sql-lint-check + @$(MAKE) --no-print-directory --silent sql-format + @$(MAKE) --no-print-directory --silent sqlc + + .PHONY: all \ default \ build \ @@ -168,10 +333,21 @@ tidy-module-check: tidy-module unit-race \ unit-debug \ unit-bench \ + itest \ + itest-db \ + itest-db-race \ fmt \ fmt-check \ tidy-module \ tidy-module-check \ + sql-parse \ + sql \ + sqlc \ + sqlc-check \ + sql-format \ + sql-lint \ + sql-lint-check \ + sql-format-check \ rpc-format \ lint \ lint-config-check \ diff --git a/README.md b/README.md index b7cb2c5c60..ff6dc97811 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,13 @@ disk. btcwallet uses the HD path for all derived addresses, as described by [BIP0044](https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki). -Due to the sensitive nature of public data in a BIP0032 wallet, -btcwallet provides the option of encrypting not just private keys, but -public data as well. This is intended to thwart privacy risks where a -wallet file is compromised without exposing all current and future -addresses (public keys) managed by the wallet. While access to this -information would not allow an attacker to spend or steal coins, it -does mean they could track all transactions involving your addresses -and therefore know your exact balance. In a future release, public data -encryption will extend to transactions as well. +btcwallet encrypts all private key material (private keys, HD seeds) +at rest using a single passphrase. Public data such as addresses, +transactions, and balances is stored in plaintext, consistent with +Bitcoin Core conventions. Users who require stronger privacy at rest +should use full disk encryption (e.g. LUKS). See +[ADR 0009](docs/developer/adr/0009-single-passphrase-encryption.md) +for design rationale. btcwallet is not an SPV client and requires connecting to a local or remote btcd instance for asynchronous blockchain queries and diff --git a/bwtest/README.md b/bwtest/README.md new file mode 100644 index 0000000000..880fb66083 --- /dev/null +++ b/bwtest/README.md @@ -0,0 +1,67 @@ +# bwtest + +`bwtest` contains the integration test harness used by `itest`. + +## Overview + +The harness provides: + +- A shared miner (btcd) that produces blocks for all test cases. +- A configurable chain backend under test (`btcd`, `bitcoind`, `neutrino`). +- Per-subtest resources: + - A fresh `chain.Interface` instance. + - A fresh wallet database instance. +- Cleanup that keeps tests isolated: + - Stops wallets created by the test. + - Requires the miner mempool to be empty on success. + +## Logs + +Each test run creates a per-run log directory under `itest/test-logs`. + +- Backend logs are flattened into `miner.log` and `chain_backend.log`. +- Wallet logs are written per test case as `wallet-.log`. + +## Backends + +Chain backends are implemented in separate files: + +- `bwtest/btcd.go` +- `bwtest/bitcoind.go` +- `bwtest/neutrino.go` + +The `bitcoind` backend uses ZMQ for block/tx notifications. + +## Wallet Helpers + +`bwtest` includes convenience helpers for tests that do not want to directly +exercise the wallet manager: + +- `(*HarnessTest).CreateEmptyWallet` +- `(*HarnessTest).CreateFundedWallet` + +Example usage: + +```go +func testFoo(t *bwtest.HarnessTest) { + t.CreateEmptyWallet() + + // Now add tests that need a started wallet instance. +} + +func testBar(t *bwtest.HarnessTest) { + t.CreateFundedWallet() + + // Now add tests that need a wallet with spendable funds. +} +``` + +Manager-focused tests should continue to create wallets through the manager API +directly. + +## Fast Scrypt + +`bwtest` sets `waddrmgr.DefaultScryptOptions` to `waddrmgr.FastScryptOptions` via +an `init()` function. Any package that imports `bwtest` (including `itest`) +automatically benefits from faster key derivation, avoiding CPU exhaustion and +timeouts — especially when running with `-race`. diff --git a/bwtest/backend.go b/bwtest/backend.go new file mode 100644 index 0000000000..bc6667f5a4 --- /dev/null +++ b/bwtest/backend.go @@ -0,0 +1,69 @@ +package bwtest + +import ( + "context" + "errors" + "testing" + + "github.com/btcsuite/btcwallet/chain" +) + +const ( + backendBtcd = "btcd" + backendBitcoind = "bitcoind" + backendNeutrino = "neutrino" +) + +var ( + errMissingMinerAddr = errors.New("missing miner address") +) + +// ChainBackend defines the interface that all chain backends must implement. +// +// A ChainBackend instance is shared across the whole itest suite run. +// Implementations must be safe to reuse across subtests that run serially. +type ChainBackend interface { + // Name returns the name of the backend ("btcd", "bitcoind", "neutrino"). + Name() string + + // Start launches the chain backend. + Start() error + + // Stop shuts down the chain backend. + Stop() error + + // ConnectMiner connects this backend to the miner. + ConnectMiner(minerAddr string) error + + // NewChainClient creates a new chain.Interface instance backed by this + // backend. + // + // This is expected to be called once for each subtest. The returned cleanup + // function must stop and release all resources created by the client. + NewChainClient(ctx context.Context) (chain.Interface, func(), error) + + // LogDir returns the directory where the backend writes its logs (if any). + LogDir() string +} + +// NewBackend creates a ChainBackend based on the type string. +func NewBackend(t *testing.T, backendType, logDir string) ChainBackend { + t.Helper() + + switch backendType { + case backendBtcd: + return NewBtcdBackend(t, logDir) + + case backendBitcoind: + return NewBitcoindBackend(t, logDir) + + case backendNeutrino: + // Neutrino is an in-process backend and does not require a backend log + // directory. + return NewNeutrinoBackend(t, logDir) + + default: + t.Fatalf("unknown chain backend %q", backendType) + return nil + } +} diff --git a/bwtest/bitcoind.go b/bwtest/bitcoind.go new file mode 100644 index 0000000000..b8c62c2c48 --- /dev/null +++ b/bwtest/bitcoind.go @@ -0,0 +1,394 @@ +package bwtest + +import ( + "context" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcwallet/bwtest/wait" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/chain/port" + "github.com/stretchr/testify/require" +) + +const ( + // bitcoindRPCUser/bitcoindRPCPass are test-only credentials used by the + // chain client. They match the static rpcauth entry below. + bitcoindRPCUser = "weks" + bitcoindRPCPass = "weks" + + // bitcoindLogFilePerm protects daemon stdout/stderr logs written by the + // harness. + bitcoindLogFilePerm = 0o600 + + // bitcoindZMQReadDeadline bounds how long we wait on each ZMQ read. + bitcoindZMQReadDeadline = 5 * time.Second + // bitcoindMempoolPollingInterval controls fallback mempool polling cadence. + bitcoindMempoolPollingInterval = 100 * time.Millisecond + // bitcoindMaxConnections keeps descriptor requirements low in CI. + bitcoindMaxConnections = 16 + // bitcoindMaxMempoolMB reduces memory usage for short-lived test runs. + bitcoindMaxMempoolMB = 50 + + // bitcoindRPCAuthorization enables RPC access with user/pass without + // storing cleartext credentials in the datadir. + // + // Generated with: bitcoind -rpcauth=weks:weks. + bitcoindRPCAuthorization = "weks:469e9bb14ab2360f8e226efed5ca6f" + + "d$507c670e800a95284294edb5773b05544b" + + "220110063096c221be9933c82d38e1" +) + +var ( + errBitcoindNotSynced = errors.New("bitcoind not synced") +) + +// BitcoindBackend is a ChainBackend backed by a bitcoind process. +type BitcoindBackend struct { + // binary is the resolved bitcoind executable path. + binary string + + // cmd is the running bitcoind process. + cmd *exec.Cmd + + // logDir is the bitcoind data directory used by this backend instance. + logDir string + + // rpcPort is the HTTP-RPC port used by bitcoind. + rpcPort int + // p2pPort is the inbound/outbound p2p port used by bitcoind. + p2pPort int + + // zmqBlockHost publishes raw block notifications for chain clients. + zmqBlockHost string + // zmqTxHost publishes raw transaction notifications for chain clients. + zmqTxHost string + + // minerAddr is the shared miner peer address that bitcoind connects to. + minerAddr string + + // stdoutPath/stderrPath are harness-managed daemon log files. + stdoutPath string + stderrPath string + + // stdoutFile/stderrFile stay open for the lifetime of the daemon process. + stdoutFile *os.File + stderrFile *os.File + + // cmdCancel cancels the process context to unblock shutdown paths. + cmdCancel context.CancelFunc +} + +// NewBitcoindBackend creates a new BitcoindBackend. +// +// The backend writes its stdout/stderr into the passed logDir and uses ZMQ for +// block and transaction notifications. +func NewBitcoindBackend(t *testing.T, logDir string) *BitcoindBackend { + t.Helper() + + bitcoindBinary, err := GetBitcoindBinary() + require.NoError(t, err, "unable to find bitcoind binary") + + absLogDir, err := filepath.Abs(logDir) + require.NoError(t, err, "unable to get absolute bitcoind log dir") + + err = ensureLogDir(absLogDir) + require.NoError(t, err, "unable to create bitcoind log dir") + + // Reserve ports in a stable order so diagnostics are easier to read when a + // setup step fails and reports one of these endpoints. + zmqBlockPort := port.NextAvailablePort() + zmqTxPort := port.NextAvailablePort() + rpcPort := port.NextAvailablePort() + p2pPort := port.NextAvailablePort() + + zmqBlockHost := fmt.Sprintf("tcp://127.0.0.1:%d", zmqBlockPort) + zmqTxHost := fmt.Sprintf("tcp://127.0.0.1:%d", zmqTxPort) + + return &BitcoindBackend{ + binary: bitcoindBinary, + logDir: absLogDir, + rpcPort: rpcPort, + p2pPort: p2pPort, + zmqBlockHost: zmqBlockHost, + zmqTxHost: zmqTxHost, + stdoutPath: filepath.Join(absLogDir, "bitcoind.stdout.log"), + stderrPath: filepath.Join(absLogDir, "bitcoind.stderr.log"), + } +} + +// Name returns the identifier of the backend. +func (b *BitcoindBackend) Name() string { + return backendBitcoind +} + +// Start launches the backend daemon. +func (b *BitcoindBackend) Start() error { + // Startup sequence overview: + // 1. Validate harness wiring (miner address + process limits). + // 2. Build daemon arguments (regtest, rpc, zmq, resource limits). + // 3. Redirect stdout/stderr to harness-managed log files. + // 4. Start the daemon process and retain file handles. + // 5. Probe RPC until the node is responsive and chain-synced. + // + // If any step fails we return an error that points to the collected logs so + // CI failures can be diagnosed from artifacts. + if b.minerAddr == "" { + return fmt.Errorf("bitcoind: %w", errMissingMinerAddr) + } + + // Best-effort attempt to increase the file descriptor limit before starting + // bitcoind. This helps avoid startup failures on systems with a low default + // RLIMIT_NOFILE. + _ = raiseNoFileLimit() + + args := []string{ + // Core regtest + connectivity setup. + "-datadir=" + b.logDir, + "-regtest", + "-connect=" + b.minerAddr, + + // bitcoind enables P2P v2 by default, but the shared btcd miner keeps + // v2 transport disabled by default. Pin bitcoind to v1 here so harness + // startup does not rely on downgrade timing during the first peer + // handshake. + "-v2transport=0", + + // Enable wallet-required indexing and RPC auth. + "-txindex", + "-disablewallet", + "-rpcauth=" + bitcoindRPCAuthorization, + fmt.Sprintf("-rpcport=%d", b.rpcPort), + fmt.Sprintf("-port=%d", b.p2pPort), + + // Use ZMQ notifications (blocks + txs) for low-latency chain updates. + "-zmqpubrawblock=" + b.zmqBlockHost, + "-zmqpubrawtx=" + b.zmqTxHost, + "-blockfilterindex=1", + + // Reduce resource usage for test environments. + // + // NOTE: bitcoind performs a file descriptor sanity check on startup. + // Keeping the connection count low reduces the required number of file + // descriptors. + fmt.Sprintf("-maxconnections=%d", bitcoindMaxConnections), + fmt.Sprintf("-maxmempool=%d", bitcoindMaxMempoolMB), + } + + cmdCtx, cmdCancel := context.WithCancel(context.Background()) + b.cmdCancel = cmdCancel + + // #nosec G204 -- b.binary is looked up from PATH and args are controlled. + cmd := exec.CommandContext(cmdCtx, b.binary, args...) + + // #nosec G304 -- b.stdoutPath is created by the test harness. + stdout, err := os.OpenFile( + b.stdoutPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, bitcoindLogFilePerm, + ) + if err != nil { + return fmt.Errorf("open bitcoind stdout log: %w", err) + } + + // #nosec G304 -- b.stderrPath is created by the test harness. + stderr, err := os.OpenFile( + b.stderrPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, bitcoindLogFilePerm, + ) + if err != nil { + _ = stdout.Close() + return fmt.Errorf("open bitcoind stderr log: %w", err) + } + + cmd.Stdout = stdout + cmd.Stderr = stderr + + err = cmd.Start() + if err != nil { + _ = stdout.Close() + _ = stderr.Close() + + return fmt.Errorf("start bitcoind: %w", err) + } + + // Keep handles alive for the duration of the process. + b.cmd = cmd + b.stdoutFile = stdout + b.stderrFile = stderr + + // Wait until bitcoind is ready to serve RPC calls. + // + // This readiness check also verifies that bitcoind has synced to the + // pre-mined harness chain height. + host := fmt.Sprintf("127.0.0.1:%d", b.rpcPort) + clientCfg := &rpcclient.ConnConfig{ + Host: host, + User: bitcoindRPCUser, + Pass: bitcoindRPCPass, + DisableAutoReconnect: false, + DisableConnectOnNew: true, + DisableTLS: true, + HTTPPostMode: true, + } + + err = wait.NoError(func() error { + // Construct a short-lived RPC client for readiness probing. + client, err := rpcclient.New(clientCfg, nil) + if err != nil { + return fmt.Errorf("create bitcoind rpc client: %w", err) + } + + defer func() { + client.Shutdown() + client.WaitForShutdown() + }() + + _, err = client.GetBlockChainInfo() + if err != nil { + return fmt.Errorf("get blockchain info: %w", err) + } + + count, err := client.GetBlockCount() + if err != nil { + return fmt.Errorf("get block count: %w", err) + } + + if count < int64(minMatureBlocks) { + return fmt.Errorf("%w (height=%d)", errBitcoindNotSynced, + count) + } + + return nil + }, defaultTestTimeout) + if err != nil { + _ = b.Stop() + + const errFmt = "bitcoind not ready: %w; logs: %s %s" + + return fmt.Errorf(errFmt, err, b.stdoutPath, b.stderrPath) + } + + return nil +} + +// Stop shuts down the backend daemon. +func (b *BitcoindBackend) Stop() error { + if b.cmdCancel != nil { + b.cmdCancel() + b.cmdCancel = nil + } + + if b.cmd != nil && b.cmd.Process != nil { + _ = b.cmd.Process.Kill() + _ = b.cmd.Wait() + } + + // Mark the process handle as stopped so repeated Stop calls are no-ops. + b.cmd = nil + + if b.stdoutFile != nil { + _ = b.stdoutFile.Close() + b.stdoutFile = nil + } + + if b.stderrFile != nil { + _ = b.stderrFile.Close() + b.stderrFile = nil + } + + return nil +} + +// ConnectMiner records the miner address for later use. +func (b *BitcoindBackend) ConnectMiner(minerAddr string) error { + b.minerAddr = minerAddr + + return nil +} + +// NewChainClient creates a new bitcoind-backed chain.Interface connected to +// this backend. +// +// For each subtest, we create a fresh BitcoindConn and client pair so test +// teardown can fully dispose chain resources without affecting other subtests. +// Startup order matters: +// 1. Construct BitcoindConn. +// 2. Start the connection so RPC + ZMQ subscriptions become active. +// 3. Construct and start the chain client that wallets will use. +// +// Cleanup runs in reverse order to avoid races between client shutdown and +// connection teardown. +func (b *BitcoindBackend) NewChainClient(ctx context.Context) (chain.Interface, + func(), error) { + + // Create a fresh chain connection for each subtest. + host := fmt.Sprintf("127.0.0.1:%d", b.rpcPort) + cfg := &chain.BitcoindConfig{ + ChainParams: harnessNetParams, + Host: host, + User: bitcoindRPCUser, + Pass: bitcoindRPCPass, + Dialer: nil, + PrunedModeMaxPeers: 0, + // ZMQ endpoints are passed in the same block/tx order used by the + // daemon startup flags above. + ZMQConfig: &chain.ZMQConfig{ + ZMQBlockHost: b.zmqBlockHost, + ZMQTxHost: b.zmqTxHost, + ZMQReadDeadline: bitcoindZMQReadDeadline, + MempoolPollingInterval: bitcoindMempoolPollingInterval, + }, + } + + var ( + conn *chain.BitcoindConn + err error + ) + + err = wait.NoError(func() error { + conn, err = chain.NewBitcoindConn(cfg) + if err != nil { + return fmt.Errorf("create bitcoind conn: %w", err) + } + + return nil + }, defaultTestTimeout) + if err != nil { + return nil, nil, fmt.Errorf("create bitcoind conn: %w", err) + } + + err = conn.Start() + if err != nil { + return nil, nil, fmt.Errorf("start bitcoind conn: %w", err) + } + + client, err := conn.NewBitcoindClient() + if err != nil { + conn.Stop() + return nil, nil, fmt.Errorf("create bitcoind client: %w", err) + } + + err = client.Start(ctx) + if err != nil { + conn.Stop() + return nil, nil, fmt.Errorf("start bitcoind client: %w", err) + } + + cleanup := func() { + client.Stop() + conn.Stop() + } + + return client, cleanup, nil +} + +// LogDir returns the directory where bitcoind wrote its logs for this run. +func (b *BitcoindBackend) LogDir() string { + return b.logDir +} + +var _ ChainBackend = (*BitcoindBackend)(nil) diff --git a/bwtest/btcd.go b/bwtest/btcd.go new file mode 100644 index 0000000000..205def545e --- /dev/null +++ b/bwtest/btcd.go @@ -0,0 +1,138 @@ +package bwtest + +import ( + "context" + "fmt" + "testing" + + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btcwallet/chain" + "github.com/stretchr/testify/require" +) + +// BtcdBackend is a ChainBackend backed by a btcd node (via rpctest). +type BtcdBackend struct { + // harness is the underlying rpctest harness that manages the btcd process. + harness *rpctest.Harness + + // logDir is the directory where btcd writes its logs. + logDir string + + // minerAddr is the P2P address of the shared miner. + minerAddr string +} + +// NewBtcdBackend creates a new BtcdBackend. +func NewBtcdBackend(t *testing.T, logDir string) *BtcdBackend { + t.Helper() + + btcdBinary, err := GetBtcdBinary() + require.NoError(t, err, "unable to find btcd binary") + + err = ensureLogDir(logDir) + require.NoError(t, err, "unable to create btcd backend log dir") + + // Create a separate harness for the chain backend. + args := []string{ + "--rejectnonstd", // Reject non-standard txs in tests. + "--txindex", // Required for some RPC queries. + "--nowinservice", // Avoid Windows service integration. + "--nobanning", // Avoid peer banning in local tests. + "--debuglevel=debug", // Provide detailed logs for debugging. + "--logdir=" + logDir, // Write logs into our per-run dir. + "--trickleinterval=100ms", // Speed up inv relay in regtest. + "--nostalldetect", // Avoid stall detection flakiness. + } + + handlers := &rpcclient.NotificationHandlers{} + harness, err := rpctest.New(harnessNetParams, handlers, args, btcdBinary) + require.NoError(t, err, "unable to create btcd backend harness") + + return &BtcdBackend{ + harness: harness, + logDir: logDir, + } +} + +// Name returns the identifier of the backend. +func (b *BtcdBackend) Name() string { + return backendBtcd +} + +// Start launches the backend daemon. +func (b *BtcdBackend) Start() error { + // SetUp(false, 0) means we don't treat it as a miner and don't cache block + // templates. + err := b.harness.SetUp(false, 0) + if err != nil { + return fmt.Errorf("setup btcd harness: %w", err) + } + + if b.minerAddr == "" { + return fmt.Errorf("btcd: %w", errMissingMinerAddr) + } + + // Connect the backend to the miner after the node is up. + err = b.harness.Client.AddNode(b.minerAddr, "add") + if err != nil { + return fmt.Errorf("add miner node %s: %w", b.minerAddr, err) + } + + return nil +} + +// Stop shuts down the backend daemon. +func (b *BtcdBackend) Stop() error { + err := b.harness.TearDown() + if err != nil { + return fmt.Errorf("teardown btcd harness: %w", err) + } + + return nil +} + +// ConnectMiner records the miner address for later use. +func (b *BtcdBackend) ConnectMiner(minerAddr string) error { + b.minerAddr = minerAddr + + return nil +} + +// NewChainClient creates a new RPC-backed chain.Interface connected to this +// backend. +func (b *BtcdBackend) NewChainClient(ctx context.Context) (chain.Interface, + func(), error) { + + backendCfg := b.harness.RPCConfig() + rpcCfg := backendCfg + + chainConfig := &chain.RPCClientConfig{ + Conn: &rpcCfg, + Chain: harnessNetParams, + ReconnectAttempts: defaultChainReconnectAttempts, + } + + chainClient, err := chain.NewRPCClientWithConfig(chainConfig) + if err != nil { + return nil, nil, fmt.Errorf("create chain client: %w", err) + } + + err = chainClient.Start(ctx) + if err != nil { + return nil, nil, fmt.Errorf("start chain client: %w", err) + } + + cleanup := func() { + chainClient.Stop() + } + + return chainClient, cleanup, nil +} + +// LogDir returns the directory where btcd wrote its logs for this run. +func (b *BtcdBackend) LogDir() string { + return b.logDir +} + +var _ ChainBackend = (*BtcdBackend)(nil) diff --git a/bwtest/database.go b/bwtest/database.go new file mode 100644 index 0000000000..5fc84f17fe --- /dev/null +++ b/bwtest/database.go @@ -0,0 +1,53 @@ +package bwtest + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" // Register bdb driver. +) + +var ( + // ErrUnknownDBBackend is returned when an unknown db backend is requested. + ErrUnknownDBBackend = errors.New("unknown db backend") +) + +const ( + // dbNameKvdb is the identifier used for the kvdb wallet backend. + dbNameKvdb = "kvdb" + + // kvdbDriver is the walletdb driver name used for kvdb. + kvdbDriver = "bdb" + + // walletDBFilename is the default wallet database filename. + walletDBFilename = "wallet.db" +) + +// OpenWalletDB opens a wallet database instance rooted at baseDir. +// +// The returned cleanup function should be called to close the database. +func OpenWalletDB(dbType, baseDir string) (walletdb.DB, func() error, error) { + switch dbType { + case dbNameKvdb: + dbPath := filepath.Join(baseDir, walletDBFilename) + + db, err := walletdb.Create(kvdbDriver, dbPath, true, + defaultTestTimeout, false) + if err != nil { + return nil, nil, fmt.Errorf("unable to create bdb instance: %w", + err) + } + + cleanup := func() error { + return db.Close() + } + + return db, cleanup, nil + + // TODO: Add sqlite and postgres support. + default: + return nil, nil, fmt.Errorf("%w: %s", ErrUnknownDBBackend, dbType) + } +} diff --git a/bwtest/doc.go b/bwtest/doc.go new file mode 100644 index 0000000000..bf125c105d --- /dev/null +++ b/bwtest/doc.go @@ -0,0 +1,3 @@ +// Package bwtest contains the integration test harness used by the itest +// package. +package bwtest diff --git a/bwtest/harness.go b/bwtest/harness.go new file mode 100644 index 0000000000..7114cee090 --- /dev/null +++ b/bwtest/harness.go @@ -0,0 +1,312 @@ +package bwtest + +import ( + "context" + "fmt" + "path/filepath" + "runtime/debug" + "sync" + "testing" + + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/require" +) + +const ( + // defaultChainReconnectAttempts is the number of times the chain RPC client + // will attempt to reconnect before failing. + defaultChainReconnectAttempts = 5 +) + +// HarnessTest is the integration test harness. +type HarnessTest struct { + *testing.T + + // logDir is the per-run root log directory. + logDir string + + // miner is the shared mining node used to generate blocks. + miner *minerHarness + + // Backend is the chain backend under test. + Backend ChainBackend + + // ChainClient is an RPC chain client connected to the active chain backend. + // + // This client is created for each subtest harness and is intended to be + // passed to wallets under test. + ChainClient chain.Interface + + // WalletDB is a wallet database instance created for the current subtest. + WalletDB walletdb.DB + + // dbType is the configured wallet database backend. + dbType string + + // mu protects harness state that can be accessed across the main test and + // subtests. This includes the wallet registry and idempotent shutdown. + mu sync.Mutex + + // wallets is the set of wallets created by a test case. + wallets []*wallet.Wallet + + // stopped prevents stopping shared infrastructure more than once. + stopped bool + + // cleaned indicates the subtest cleanup has already run. + cleaned bool +} + +// SetupHarness creates a new HarnessTest. +func SetupHarness(t *testing.T, chainBackendType, dbType string) *HarnessTest { + t.Helper() + + logDir := createTestLogDir(t, chainBackendType, dbType) + + // 1. Start Miner (always btcd). + minerLogDir := createOrEnsureLogSubDir(t, logDir, "miner") + miner := newMiner(t, minerLogDir) + miner.SetUp() + require.NoError(t, waitForTCPListener(miner.P2PAddress(), + defaultTestTimeout), "miner p2p listener not ready") + + // 2. Start Chain Backend. + backendLogDir := "" + + // Neutrino runs in-process and has no separate daemon log directory. The + // external daemon backends (btcd/bitcoind) each get a dedicated backend log + // sub-directory. + if chainBackendType != backendNeutrino { + backendLogDir = createOrEnsureLogSubDir(t, logDir, "chain-backend") + } + + backend := NewBackend(t, chainBackendType, backendLogDir) + require.NoError(t, backend.ConnectMiner(miner.P2PAddress())) + require.NoError(t, backend.Start(), "failed to start chain backend") + + ht := &HarnessTest{ + T: t, + logDir: logDir, + miner: miner, + Backend: backend, + dbType: dbType, + } + + // Ensure the harness is cleaned up when the test finishes. + t.Cleanup(ht.Stop) + + return ht +} + +// Subtest creates a child harness that shares the miner and chain backend. +// +// The returned harness has its own wallet registry and per-test resources. +// Callers should not call Stop on the returned harness as it would stop shared +// infrastructure. +func (h *HarnessTest) Subtest(t *testing.T) *HarnessTest { + h.Helper() + + st := &HarnessTest{ + T: t, + logDir: h.logDir, + miner: h.miner, + Backend: h.Backend, + dbType: h.dbType, + } + + // Use the subtest's testing context for miner assertions. + // + // The miner is shared across test cases, but we want failures to be + // attributed to the active subtest. + // + // NOTE: The miner is shared across the whole suite and this assignment + // mutates that shared state. + // + // This is safe because the integration test suite runs subtests serially + // (no t.Parallel()). Do not enable parallel integration cases unless this + // is refactored. + st.miner.T = st.T + + walletLogCleanup := setUpWalletLogging( + t, filepath.Join(st.logDir, walletLogFileName(t)), + ) + st.Cleanup(walletLogCleanup) + + st.setUpChainClient() + st.setUpWalletDB() + + st.Cleanup(func() { + // If a test fails, we still try to stop wallets to avoid leaking + // goroutines into the next test, but we skip assertions. + if st.Failed() { + err := st.stopActiveWallets(context.Background()) + if err != nil { + st.Logf("failed to stop wallets during failed-test cleanup: %v", + err) + } + + return + } + + if st.cleaned { + return + } + + err := st.stopActiveWallets(context.Background()) + require.NoError(st, err, "failed to stop wallets") + + mempool, err := st.getRawMempool() + require.NoError(st, err, "failed to query miner mempool") + require.Empty(st, mempool, "mempool not cleaned") + + st.cleaned = true + h.cleaned = true + }) + + return st +} + +// RegisterWallet registers a wallet with the harness. +// +// Registered wallets are automatically included in harness-level assertions, +// such as MineBlocks. +func (h *HarnessTest) RegisterWallet(w *wallet.Wallet) { + h.Helper() + + if w == nil { + h.Fatalf("cannot register nil wallet") + } + + h.mu.Lock() + h.wallets = append(h.wallets, w) + h.mu.Unlock() +} + +// ActiveWallets returns a snapshot of wallets registered with this harness. +func (h *HarnessTest) ActiveWallets() []*wallet.Wallet { + h.Helper() + + h.mu.Lock() + wallets := append([]*wallet.Wallet(nil), h.wallets...) + h.mu.Unlock() + + return wallets +} + +// RunTestCase executes a harness test case. +// +// Any panic from the test function is converted into a fatal test failure with +// a stack trace. +func (h *HarnessTest) RunTestCase(name string, + testFunc func(t *HarnessTest)) { + + h.Helper() + + defer func() { + r := recover() + if r == nil { + return + } + + stack := debug.Stack() + h.Fatalf("failed (%s): panic=%v\n%s", name, r, stack) + }() + + if testFunc == nil { + h.Fatalf("nil test func for %s", name) + } + + // Execute the case. + testFunc(h) +} + +// NetParams returns the chain parameters used by the harness. +func (h *HarnessTest) NetParams() *chaincfg.Params { + h.Helper() + + return harnessNetParams +} + +// Stop shuts down all resources owned by the harness. +func (h *HarnessTest) Stop() { + h.Helper() + + h.mu.Lock() + + if h.stopped { + h.mu.Unlock() + return + } + + h.stopped = true + h.mu.Unlock() + + // Stop the chain backend first to avoid it attempting to reconnect while + // the miner is being torn down. + require.NoError(h, h.Backend.Stop(), "failed to stop chain backend") + + // Finally, stop the miner. + h.miner.Stop() + + // Flatten logs into the per-run log dir. + h.finalizeLogs() +} + +// stopActiveWallets stops all wallets registered with the harness. +// +// This is used as part of the per-subtest cleanup to avoid leaking background +// goroutines into the next test. +func (h *HarnessTest) stopActiveWallets(ctx context.Context) error { + h.Helper() + + for _, w := range h.ActiveWallets() { + if w == nil { + // Keep cleanup robust against partially initialized test state. A + // caller could register a wallet reference and fail before the + // assignment completes. + continue + } + + // The modern Wallet controller's Stop method is idempotent. + // + // NOTE: We intentionally don't call the deprecated WaitForShutdown/ + // ShuttingDown methods here, as modern wallets might not have the + // legacy fields initialized. + err := w.Stop(ctx) + if err != nil { + return fmt.Errorf("stop wallet: %w", err) + } + } + + return nil +} + +// setUpChainClient creates and starts a chain client for the active harness +// backend. +func (h *HarnessTest) setUpChainClient() { + h.Helper() + + chainClient, cleanup, err := h.Backend.NewChainClient(h.Context()) + require.NoError(h, err, "unable to create chain client") + + h.Cleanup(cleanup) + h.ChainClient = chainClient +} + +// setUpWalletDB opens a wallet database for the configured test backend. +func (h *HarnessTest) setUpWalletDB() { + h.Helper() + + dbDir := h.TempDir() + db, cleanup, err := OpenWalletDB(h.dbType, dbDir) + require.NoError(h, err, "unable to create wallet db") + + h.Cleanup(func() { + require.NoError(h, cleanup(), "failed to close database") + }) + + h.WalletDB = db +} diff --git a/bwtest/harness_assertions.go b/bwtest/harness_assertions.go new file mode 100644 index 0000000000..ac62a554df --- /dev/null +++ b/bwtest/harness_assertions.go @@ -0,0 +1,44 @@ +package bwtest + +import ( + "errors" + "fmt" + + "github.com/btcsuite/btcwallet/bwtest/wait" + "github.com/btcsuite/btcwallet/wallet" +) + +var ( + // ErrWalletNotSynced is returned when a wallet has not reached the chain + // tip. + ErrWalletNotSynced = errors.New("wallet not synced") +) + +// AssertWalletSynced polls until the wallet reports it is synced to the +// miner's best known height. +func (h *HarnessTest) AssertWalletSynced(w *wallet.Wallet) { + h.Helper() + + if w == nil { + h.Fatalf("nil wallet") + } + + err := wait.NoError(func() error { + syncedTo := w.SyncedTo() + + _, bestHeight, err := h.miner.Client.GetBestBlock() + if err != nil { + return fmt.Errorf("get best block: %w", err) + } + + if syncedTo.Height != bestHeight { + return fmt.Errorf("%w: wallet=%d chain=%d", ErrWalletNotSynced, + syncedTo.Height, bestHeight) + } + + return nil + }, defaultTestTimeout) + if err != nil { + h.Fatalf("wallet sync timeout: %v", err) + } +} diff --git a/bwtest/harness_miner.go b/bwtest/harness_miner.go new file mode 100644 index 0000000000..8691050b99 --- /dev/null +++ b/bwtest/harness_miner.go @@ -0,0 +1,506 @@ +package bwtest + +import ( + "errors" + "fmt" + "strings" + + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/btcsuite/btcwallet/bwtest/wait" + "github.com/stretchr/testify/require" +) + +var ( + // ErrMempoolTxNotFound is returned when a transaction is not found in the + // miner's mempool. + ErrMempoolTxNotFound = errors.New("transaction not found in mempool") + + // ErrMempoolTxFound is returned when a transaction is found in the miner's + // mempool. + ErrMempoolTxFound = errors.New("transaction found in mempool") + + // ErrMempoolNumTxnsMismatch is returned when the number of transactions in + // the mempool doesn't match the expected value. + ErrMempoolNumTxnsMismatch = errors.New("mempool txn count mismatch") + + // ErrBlockMissingTx is returned when a transaction is not found in a block. + ErrBlockMissingTx = errors.New("transaction not found in block") + + // ErrBlockUnexpectedTxns is returned when a block contains unexpected + // transactions. + ErrBlockUnexpectedTxns = errors.New("block contains unexpected txns") + + // ErrOutpointNotFound is returned when an outpoint is not found in the + // miner's mempool. + ErrOutpointNotFound = errors.New("outpoint not found in mempool") + + // ErrNilBlock is returned when a nil block is provided. + ErrNilBlock = errors.New("nil block") + + // ErrMinerNotSynced is returned when a temporary miner is not synced to the + // harness miner. + ErrMinerNotSynced = errors.New("miner not synced") +) + +const ( + // coinbaseAndOneTxn is the number of transactions expected in a block that + // contains only a coinbase transaction and a single non-coinbase + // transaction. + coinbaseAndOneTxn = 2 +) + +// GenerateBlocks generates the specified number of blocks. +func (h *HarnessTest) GenerateBlocks(num uint32) []*chainhash.Hash { + h.Helper() + + hashes, err := h.miner.Client.Generate(num) + require.NoError(h, err, "unable to generate blocks") + + return hashes +} + +// AssertTxInMempool asserts a transaction can be found in the miner's mempool. +func (h *HarnessTest) AssertTxInMempool(txid chainhash.Hash) *wire.MsgTx { + h.Helper() + + var foundTx *wire.MsgTx + + err := wait.NoError(func() error { + txids, err := h.getRawMempool() + if err != nil { + return fmt.Errorf("get raw mempool: %w", err) + } + + for _, memTxid := range txids { + if memTxid == txid { + tx, err := h.miner.Client.GetRawTransaction(&txid) + if err != nil { + return fmt.Errorf("get raw transaction: %w", err) + } + + foundTx = tx.MsgTx() + + return nil + } + } + + return fmt.Errorf("%w: txid=%s", ErrMempoolTxNotFound, txid) + }, defaultTestTimeout) + require.NoError(h, err, "timeout waiting for txn in mempool") + require.NotNil(h, foundTx, "found tx is nil") + + return foundTx +} + +// AssertTxNotInMempool asserts a transaction cannot be found in the miner's +// mempool. +func (h *HarnessTest) AssertTxNotInMempool(txid chainhash.Hash) { + h.Helper() + + err := wait.NoError(func() error { + txids, err := h.getRawMempool() + if err != nil { + return fmt.Errorf("get raw mempool: %w", err) + } + + for _, memTxid := range txids { + if memTxid == txid { + return fmt.Errorf("%w: txid=%s", ErrMempoolTxFound, + txid) + } + } + + return nil + }, defaultTestTimeout) + require.NoError(h, err, "timeout waiting for txn to leave mempool") +} + +// AssertNumTxnsInMempool polls until finding the expected number of +// transactions in the miner's mempool. +func (h *HarnessTest) AssertNumTxnsInMempool(n int) []chainhash.Hash { + h.Helper() + + if n < 0 { + h.Fatalf("invalid mempool size: %d", n) + } + + var txids []chainhash.Hash + + err := wait.NoError(func() error { + mempoolTxids, err := h.getRawMempool() + if err != nil { + return fmt.Errorf("get raw mempool: %w", err) + } + + if len(mempoolTxids) != n { + return fmt.Errorf("%w: want=%d got=%d", ErrMempoolNumTxnsMismatch, + n, len(mempoolTxids)) + } + + txids = mempoolTxids + + return nil + }, defaultTestTimeout) + require.NoError(h, err, "timeout waiting for mempool size") + + return txids +} + +// AssertOutpointInMempool asserts an outpoint is spent by a transaction in the +// miner's mempool. +func (h *HarnessTest) AssertOutpointInMempool(op wire.OutPoint) *wire.MsgTx { + h.Helper() + + var foundTx *wire.MsgTx + + err := wait.NoError(func() error { + txids, err := h.getRawMempool() + if err != nil { + return fmt.Errorf("get raw mempool: %w", err) + } + + for _, txid := range txids { + tx, err := h.miner.Client.GetRawTransaction(&txid) + if err != nil { + return fmt.Errorf("get raw transaction: %w", err) + } + + msgTx := tx.MsgTx() + for _, txIn := range msgTx.TxIn { + if txIn.PreviousOutPoint == op { + foundTx = msgTx + return nil + } + } + } + + return fmt.Errorf("%w: outpoint=%v", ErrOutpointNotFound, op) + }, defaultTestTimeout) + require.NoError(h, err, "timeout waiting for outpoint in mempool") + require.NotNil(h, foundTx, "found tx is nil") + + return foundTx +} + +// AssertTxInBlock asserts a transaction can be found in a block. +func (h *HarnessTest) AssertTxInBlock(block *wire.MsgBlock, + txid chainhash.Hash) { + + h.Helper() + + if block == nil { + h.Fatalf("nil block") + } + + for _, tx := range block.Transactions { + if tx == nil { + continue + } + + if tx.TxHash() == txid { + return + } + } + + h.Fatalf("%v: block=%v", fmt.Errorf("%w: txid=%s", ErrBlockMissingTx, + txid), block.BlockHash()) +} + +// MineBlocks mines blocks and asserts no transactions are found in the mined +// blocks. +// +// After each block is mined, all registered wallets are required to be synced. +func (h *HarnessTest) MineBlocks(num int) { + h.Helper() + + err := h.MineBlocksNoTxns(num) + if err != nil { + require.Fail(h, "MineBlocks", err.Error()) + } +} + +// MineBlocksNoTxns mines blocks and returns an error if any mined block +// contains non-coinbase transactions. +func (h *HarnessTest) MineBlocksNoTxns(num int) error { + h.Helper() + + blocks := h.generateBlocks(num) + for _, b := range blocks { + err := h.blockHasNoTxns(b) + if err != nil { + return err + } + } + + return nil +} + +// MineEmptyBlocks mines blocks and asserts the mempool remains empty. +// +// This differs from MineBlocks in that it explicitly requires the miner's +// mempool to have no transactions before mining begins. +func (h *HarnessTest) MineEmptyBlocks(num int) []*wire.MsgBlock { + h.Helper() + + // Require the mempool is empty before mining, otherwise these blocks might + // confirm pending transactions. + h.AssertNumTxnsInMempool(0) + + blocks := h.generateBlocks(num) + for _, b := range blocks { + err := h.blockHasNoTxns(b) + if err != nil { + require.Fail(h, "MineEmptyBlocks", err.Error()) + } + } + + return blocks +} + +// MineBlocksAndAssertNumTxns mines blocks and asserts that numTxns +// transactions are included in the first mined block. +func (h *HarnessTest) MineBlocksAndAssertNumTxns(num uint32, + numTxns int) []*wire.MsgBlock { + + h.Helper() + + if num == 0 { + h.Fatalf("invalid block count: %d", num) + } + + txids := h.AssertNumTxnsInMempool(numTxns) + blocks := h.generateBlocks(int(num)) + + for _, txid := range txids { + h.AssertTxInBlock(blocks[0], txid) + h.AssertTxNotInMempool(txid) + } + + return blocks +} + +// MineBlockWithTx mines a single block and asserts it contains the given +// transaction. +func (h *HarnessTest) MineBlockWithTx(tx *wire.MsgTx) *wire.MsgBlock { + h.Helper() + + if tx == nil { + h.Fatalf("nil tx") + } + + txid := tx.TxHash() + h.AssertTxInMempool(txid) + + // Ensure the mempool only contains our transaction so the mined block + // contains only the coinbase and this transaction. + mempoolTxids := h.AssertNumTxnsInMempool(1) + require.Equal(h, txid, mempoolTxids[0], "unexpected txn in mempool") + + blocks := h.MineBlocksAndAssertNumTxns(1, 1) + require.Len(h, blocks, 1, "expected exactly 1 block") + + block := blocks[0] + require.NotNil(h, block, "mined block is nil") + require.Len(h, block.Transactions, coinbaseAndOneTxn, + "expected coinbase and one txn") + require.Equal(h, txid, block.Transactions[1].TxHash(), + "unexpected txn in mined block") + + return block +} + +// SpawnTempMiner creates a temporary miner that is synced with the current +// miner. +// +// This is useful for reorg tests where an alternative chain needs to be mined +// in isolation. +func (h *HarnessTest) SpawnTempMiner() *HarnessTest { + h.Helper() + + minerLogDir := createUniqueLogSubDir(h.T, h.logDir, "miner-temp") + tempMiner := newMiner(h.T, minerLogDir) + tempMiner.SetUpNoChain() + + th := &HarnessTest{T: h.T, logDir: h.logDir, miner: tempMiner} + h.Cleanup(tempMiner.Stop) + + // Connect the miners and wait for the temp miner to sync. + h.ConnectToMiner(th) + + _, mainHeight := h.GetBestBlock() + err := wait.NoError(func() error { + _, tempHeight, err := tempMiner.Client.GetBestBlock() + if err != nil { + return fmt.Errorf("get best block: %w", err) + } + + if tempHeight != mainHeight { + return fmt.Errorf("%w: main=%d temp=%d", ErrMinerNotSynced, + mainHeight, tempHeight) + } + + return nil + }, defaultTestTimeout) + require.NoError(h, err, "timeout waiting for temp miner to sync") + + // Disconnect the temp miner so it can mine an alternative chain. + h.DisconnectFromMiner(th) + + return th +} + +// ConnectToMiner connects the harness miner to tempMiner. +func (h *HarnessTest) ConnectToMiner(tempMiner *HarnessTest) { + h.Helper() + + if tempMiner == nil { + h.Fatalf("nil temp miner") + } + + if tempMiner.miner == nil { + h.Fatalf("nil temp miner harness") + } + + err := h.miner.Client.AddNode(tempMiner.miner.P2PAddress(), "add") + require.NoError(h, err, "failed to connect to temp miner") + + err = tempMiner.miner.Client.AddNode(h.miner.P2PAddress(), "add") + require.NoError(h, err, "failed to connect temp miner") +} + +// DisconnectFromMiner disconnects the harness miner from tempMiner. +func (h *HarnessTest) DisconnectFromMiner(tempMiner *HarnessTest) { + h.Helper() + + if tempMiner == nil { + h.Fatalf("nil temp miner") + } + + if tempMiner.miner == nil { + h.Fatalf("nil temp miner harness") + } + + err := h.miner.Client.AddNode(tempMiner.miner.P2PAddress(), "remove") + require.NoError(h, err, "failed to disconnect from temp miner") + + err = tempMiner.miner.Client.AddNode(h.miner.P2PAddress(), "remove") + require.NoError(h, err, "failed to disconnect temp miner") +} + +// generateBlocks mines num blocks and returns the full blocks. +// +// After each block is mined, all registered wallets are required to be synced. +func (h *HarnessTest) generateBlocks(num int) []*wire.MsgBlock { + h.Helper() + + // Mining 0 blocks is a no-op. + if num == 0 { + return nil + } + + if num < 0 { + h.Fatalf("invalid block count: %d", num) + } + + if num > int(^uint32(0)) { + h.Fatalf("too many blocks requested: %d", num) + } + + blocks := make([]*wire.MsgBlock, 0, num) + for range num { + hashes := h.GenerateBlocks(1) + require.Len(h, hashes, 1, "expected 1 block hash") + + block, err := h.miner.Client.GetBlock(hashes[0]) + require.NoError(h, err, "failed to get mined block") + + blocks = append(blocks, block) + + // Ensure all wallets we created in this test have caught up. + for _, w := range h.ActiveWallets() { + h.AssertWalletSynced(w) + } + } + + return blocks +} + +// getRawMempool returns the miner's mempool transaction ids. +func (h *HarnessTest) getRawMempool() ([]chainhash.Hash, error) { + h.Helper() + + txids, err := h.miner.Client.GetRawMempool() + if err != nil { + return nil, fmt.Errorf("get raw mempool: %w", err) + } + + result := make([]chainhash.Hash, 0, len(txids)) + for _, txid := range txids { + if txid == nil { + continue + } + + result = append(result, *txid) + } + + return result, nil +} + +// blockHasNoTxns returns an error if block contains non-coinbase transactions. +func (h *HarnessTest) blockHasNoTxns(block *wire.MsgBlock) error { + h.Helper() + + if block == nil { + return ErrNilBlock + } + + if len(block.Transactions) <= 1 { + return nil + } + + var desc strings.Builder + desc.WriteString(fmt.Sprintf( + "block %v has %d txns:\n", + block.BlockHash(), len(block.Transactions)-1, + )) + + for _, tx := range block.Transactions[1:] { + if tx == nil { + continue + } + + desc.WriteString(fmt.Sprintf("%v\n", tx.TxHash())) + } + + desc.WriteString( + "Consider using `MineBlocksAndAssertNumTxns` if you expect " + + "txns, or `MineEmptyBlocks` if you want to keep txns " + + "unconfirmed.", + ) + + return fmt.Errorf("%w: %s", ErrBlockUnexpectedTxns, desc.String()) +} + +// SendOutput sends funds from the miner. +func (h *HarnessTest) SendOutput(output *wire.TxOut, + feeRate btcutil.Amount) *chainhash.Hash { + + h.Helper() + + txid, err := h.miner.SendOutputs([]*wire.TxOut{output}, feeRate) + require.NoError(h, err, "failed to send output") + + return txid +} + +// GetBestBlock returns the hash and height of the best block. +func (h *HarnessTest) GetBestBlock() (*chainhash.Hash, int32) { + h.Helper() + + hash, height, err := h.miner.Client.GetBestBlock() + require.NoError(h, err, "failed to get best block") + + return hash, height +} diff --git a/bwtest/harness_wallet.go b/bwtest/harness_wallet.go new file mode 100644 index 0000000000..472404ab60 --- /dev/null +++ b/bwtest/harness_wallet.go @@ -0,0 +1,126 @@ +package bwtest + +import ( + "context" + "strings" + "time" + + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/txscript/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/stretchr/testify/require" +) + +const ( + // defaultPubPass is the standard public passphrase used by test wallets. + defaultPubPass = "public" + + // defaultPrivPass is the standard private passphrase used by test wallets. + defaultPrivPass = "private" + + // defaultWalletRecoveryWindow keeps enough look-ahead addresses for test + // cases that derive multiple addresses while scanning historical blocks. + defaultWalletRecoveryWindow = 20 + + // defaultWalletSyncRetryInterval controls how often wallet sync retries + // when the chain backend is temporarily unavailable during startup. + defaultWalletSyncRetryInterval = 500 * time.Millisecond +) + +// CreateEmptyWallet creates, starts, and registers a new wallet instance. +// +// This is intended for non-manager integration tests that want a ready-to-use +// wallet without repeating boilerplate. +func (h *HarnessTest) CreateEmptyWallet() *wallet.Wallet { + h.Helper() + + name := "itest-" + strings.ReplaceAll(h.Name(), "/", "_") + + cfg := wallet.Config{ + // Use the subtest-scoped DB and chain client prepared by the harness. + DB: h.WalletDB, + Chain: h.ChainClient, + + // Keep network and startup behavior deterministic across tests. + ChainParams: h.NetParams(), + RecoveryWindow: defaultWalletRecoveryWindow, + WalletSyncRetryInterval: defaultWalletSyncRetryInterval, + + // Use a unique wallet name per test to avoid collisions in logs. + Name: name, + PubPassphrase: []byte(defaultPubPass), + } + + params := wallet.CreateWalletParams{ + // Generate a fresh seed for each test wallet. + Mode: wallet.ModeGenSeed, + PubPassphrase: []byte(defaultPubPass), + PrivatePassphrase: []byte(defaultPrivPass), + + // Use an old birthday to ensure the wallet can discover historical + // blocks when used in tests that pre-mine chain state. + Birthday: time.Now().Add(-1 * time.Hour), + } + + manager := wallet.NewManager() + w, err := manager.Create(cfg, params) + require.NoError(h, err, "failed to create wallet") + + err = w.Start(h.Context()) + require.NoError(h, err, "failed to start wallet") + + h.Cleanup(func() { + // We use a background context here because the test context might be + // canceled by the time cleanup runs. + _ = w.Stop(context.Background()) + }) + + // Register the wallet so harness helpers can assert global invariants. + h.RegisterWallet(w) + + return w +} + +// CreateFundedWallet creates an empty wallet and funds it with 10 BTC. +// +// This is intended for future integration tests that need spendable funds. +func (h *HarnessTest) CreateFundedWallet() *wallet.Wallet { + h.Helper() + + w := h.CreateEmptyWallet() + + err := w.Unlock(h.Context(), wallet.UnlockRequest{ + Passphrase: []byte(defaultPrivPass), + }) + require.NoError(h, err, "failed to unlock wallet") + + addr, err := w.NewAddress( + h.Context(), waddrmgr.DefaultAccountName, + waddrmgr.WitnessPubKey, false, + ) + require.NoError(h, err, "failed to create address") + + pkScript, err := txscript.PayToAddrScript(addr) + require.NoError(h, err, "failed to create pkscript") + + const tenBTC = 10 * btcutil.SatoshiPerBitcoin + + output := &wire.TxOut{Value: int64(tenBTC), PkScript: pkScript} + + // Use a minimal fee rate for regtest. + h.SendOutput(output, btcutil.Amount(1)) + + // Confirm and wait for sync. + h.MineBlocks(1) + h.AssertWalletSynced(w) + + return w +} + +func init() { + // Use fast scrypt options for tests to avoid CPU exhaustion and + // timeouts, especially when running with -race. + waddrmgr.DefaultScryptOptions = waddrmgr.FastScryptOptions +} diff --git a/bwtest/logdir.go b/bwtest/logdir.go new file mode 100644 index 0000000000..17c7a72fdf --- /dev/null +++ b/bwtest/logdir.go @@ -0,0 +1,158 @@ +package bwtest + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const ( + // testLogsRootDir is the directory under the itest package where all + // per-run logs are stored. + // + // Note: When running the integration tests with `go test ./itest`, the + // working directory is `itest`, so logs are written under + // `itest/test-logs`. + testLogsRootDir = "test-logs" + + maxLogDirAttempts = 1000 +) + +// createTestLogDir creates a per-run log directory under `test-logs`. +// +// The directory is named using the format: +// +// log---YYYYMMDD-HHMMSS +// +// If the directory already exists, a numeric suffix is appended. +func createTestLogDir(t *testing.T, chainBackend, dbBackend string) string { + t.Helper() + + err := os.MkdirAll(testLogsRootDir, logDirPerm) + require.NoError(t, err, "unable to create test log root") + + chainBackend = sanitizeLogToken(chainBackend) + dbBackend = sanitizeLogToken(dbBackend) + + base := fmt.Sprintf( + "log-%s-%s-%s", chainBackend, dbBackend, + time.Now().Format("20060102-150405"), + ) + + // Use Mkdir instead of MkdirAll so we can detect collisions and retry + // with a deterministic numeric suffix. + for i := range maxLogDirAttempts { + dir := base + if i > 0 { + dir = fmt.Sprintf("%s-%d", base, i) + } + + fullPath := filepath.Join(testLogsRootDir, dir) + + err := os.Mkdir(fullPath, logDirPerm) + if err == nil { + _, _ = fmt.Fprintf(os.Stdout, "itest logs dir: %s\n", fullPath) + return fullPath + } + + if os.IsExist(err) { + continue + } + + require.NoError(t, err, "unable to create test log dir") + } + + t.Fatalf( + "unable to create test log dir: too many collisions (%d)", + maxLogDirAttempts, + ) + + return "" +} + +// sanitizeLogToken converts a string into a safe filename token. +func sanitizeLogToken(token string) string { + if token == "" { + return "unknown" + } + + var b strings.Builder + for _, r := range token { + if isSafeLogRune(r) { + b.WriteRune(r) + continue + } + + b.WriteByte('_') + } + + return b.String() +} + +// isSafeLogRune reports whether r can be used in log directory/file names +// without additional escaping. +func isSafeLogRune(r rune) bool { + switch { + case r >= 'a' && r <= 'z': + return true + case r >= 'A' && r <= 'Z': + return true + case r >= '0' && r <= '9': + return true + case r == '-' || r == '_': + return true + default: + return false + } +} + +// createOrEnsureLogSubDir creates a named sub-directory under a per-run log +// directory. +func createOrEnsureLogSubDir(t *testing.T, parent, name string) string { + t.Helper() + + full := filepath.Join(parent, name) + err := os.MkdirAll(full, logDirPerm) + require.NoError(t, err, "unable to create log subdir") + + return full +} + +// createUniqueLogSubDir creates a uniquely named sub-directory under a per-run +// log directory. +func createUniqueLogSubDir(t *testing.T, parent, prefix string) string { + t.Helper() + + // Retry with a numeric suffix when a directory name collision occurs. + for i := range maxLogDirAttempts { + dir := prefix + if i > 0 { + dir = fmt.Sprintf("%s-%d", prefix, i) + } + + full := filepath.Join(parent, dir) + + err := os.Mkdir(full, logDirPerm) + if err == nil { + return full + } + + if os.IsExist(err) { + continue + } + + require.NoError(t, err, "unable to create log subdir") + } + + t.Fatalf( + "unable to create log subdir: too many collisions (%d)", + maxLogDirAttempts, + ) + + return "" +} diff --git a/bwtest/logs_finalize.go b/bwtest/logs_finalize.go new file mode 100644 index 0000000000..6cb6d455fe --- /dev/null +++ b/bwtest/logs_finalize.go @@ -0,0 +1,263 @@ +package bwtest + +import ( + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" +) + +const ( + minerLogFilename = "miner.log" + chainBackendLogFilename = "chain_backend.log" + + logFilePerm = 0o600 +) + +// finalizeLogs flattens component logs into the per-run log directory. +func (h *HarnessTest) finalizeLogs() { + h.Helper() + + // Flatten miner logs. + minerDst := filepath.Join(h.logDir, minerLogFilename) + + err := flattenBtcdLogs(h.T, h.miner.logPath, minerDst) + if err != nil { + h.Logf("failed to flatten miner logs: %v", err) + } + + chainLogDir := h.Backend.LogDir() + + chainDst := filepath.Join(h.logDir, chainBackendLogFilename) + if chainLogDir == "" { + // Some backends (eg. neutrino) do not have an external process log + // directory. Still create the file for consistent log collection. + // #nosec G304 -- chainDst is created by the test harness. + f, err := os.OpenFile( + chainDst, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, logFilePerm, + ) + if err != nil { + h.Logf("failed to create chain backend log file: %v", err) + return + } + + _ = f.Close() + + return + } + + switch h.Backend.Name() { + case backendBtcd: + err = flattenBtcdLogs(h.T, chainLogDir, chainDst) + if err != nil { + h.Logf("failed to flatten btcd backend logs: %v", err) + } + + case backendBitcoind: + err = flattenBitcoindLogs(h.T, chainLogDir, chainDst) + if err != nil { + h.Logf("failed to flatten bitcoind backend logs: %v", err) + } + + default: + // No backend logs to flatten. + } +} + +// flattenBitcoindLogs concatenates bitcoind logs under srcDir into dstFile. +func flattenBitcoindLogs(t *testing.T, srcDir, dstFile string) error { + t.Helper() + + // Capture process stdout/stderr first, as fatal startup errors might not be + // present in debug.log. + prelude := []string{ + filepath.Join(srcDir, "bitcoind.stderr.log"), + filepath.Join(srcDir, "bitcoind.stdout.log"), + } + + pattern := filepath.Join(srcDir, "*", "debug.log*") + + matches, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("glob bitcoind logs: %w", err) + } + + files := make([]string, 0, len(prelude)+len(matches)) + files = append(files, prelude...) + files = append(files, matches...) + + files = filterRegularFiles(files) + if len(files) == 0 { + return nil + } + + // bitcoind rotates debug.log.1, debug.log.2 etc but we don't try too hard + // ordering here. + sort.Strings(files) + + // #nosec G304 -- dstFile is created by the test harness. + f, err := os.OpenFile(dstFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, + logFilePerm) + if err != nil { + return fmt.Errorf("open dst log: %w", err) + } + + defer func() { + _ = f.Close() + }() + + for i, p := range files { + // Keep a blank line between concatenated source files to make the + // merged output easier to scan when debugging CI failures. + if i > 0 { + _, _ = f.WriteString("\n") + } + + base := filepath.Base(p) + _, _ = f.WriteString("--- " + base + " ---\n") + + // #nosec G304 -- p is discovered under the harness-controlled log dir. + src, err := os.Open(p) + if err != nil { + return fmt.Errorf("open src log: %w", err) + } + + _, cpErr := io.Copy(f, src) + _ = src.Close() + + if cpErr != nil { + return fmt.Errorf("copy src log: %w", cpErr) + } + } + + _ = os.RemoveAll(srcDir) + + return nil +} + +// flattenBtcdLogs concatenates btcd logs under srcDir into dstFile. +func flattenBtcdLogs(t *testing.T, srcDir, dstFile string) error { + t.Helper() + + pattern := filepath.Join(srcDir, "*", "btcd.log*") + + matches, err := filepath.Glob(pattern) + if err != nil { + return fmt.Errorf("glob btcd logs: %w", err) + } + + if len(matches) == 0 { + return nil + } + + files := filterRegularFiles(matches) + if len(files) == 0 { + return nil + } + + sortBtcdLogs(files) + + // #nosec G304 -- dstFile is created by the test harness. + f, err := os.OpenFile(dstFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, + logFilePerm) + if err != nil { + return fmt.Errorf("open dst log: %w", err) + } + + defer func() { + _ = f.Close() + }() + + for i, p := range files { + // Add a small delimiter between rotated files. + if i > 0 { + _, _ = f.WriteString("\n") + } + + base := filepath.Base(p) + _, _ = f.WriteString("--- " + base + " ---\n") + + // #nosec G304 -- p is discovered under the harness-controlled log dir. + src, err := os.Open(p) + if err != nil { + return fmt.Errorf("open src log: %w", err) + } + + _, cpErr := io.Copy(f, src) + _ = src.Close() + + if cpErr != nil { + return fmt.Errorf("copy src log: %w", cpErr) + } + } + + // Best effort cleanup to keep the log dir shallow. + _ = os.RemoveAll(srcDir) + + return nil +} + +// filterRegularFiles filters to existing regular files. +func filterRegularFiles(paths []string) []string { + files := make([]string, 0, len(paths)) + for _, p := range paths { + info, err := os.Stat(p) + if err != nil { + continue + } + + if info.Mode().IsRegular() { + files = append(files, p) + } + } + + return files +} + +// sortBtcdLogs sorts btcd logs by rotation index so older logs appear first. +func sortBtcdLogs(paths []string) { + sort.Slice(paths, func(i, j int) bool { + iBase := filepath.Base(paths[i]) + jBase := filepath.Base(paths[j]) + + // Prefer older rotated logs first: btcd.log.N ... btcd.log.1 then + // btcd.log. + iN, iOk := btcdLogRotationIndex(iBase) + + jN, jOk := btcdLogRotationIndex(jBase) + if iOk && jOk { + // Larger rotation index means an older file. For example, + // btcd.log.3 is older than btcd.log.1 and should be concatenated + // first. + return iN > jN + } + + if iOk != jOk { + // Rotated logs before the active log. + return iOk + } + + // Fallback to lexicographic ordering. + return iBase < jBase + }) +} + +// btcdLogRotationIndex parses the rotation suffix of btcd log filenames. +func btcdLogRotationIndex(base string) (int, bool) { + // btcd rotates logs like btcd.log.1, btcd.log.2, ... + const prefix = "btcd.log." + if !strings.HasPrefix(base, prefix) { + return 0, false + } + + n, err := strconv.Atoi(strings.TrimPrefix(base, prefix)) + if err != nil { + return 0, false + } + + return n, true +} diff --git a/bwtest/miner.go b/bwtest/miner.go new file mode 100644 index 0000000000..4f50ec425e --- /dev/null +++ b/bwtest/miner.go @@ -0,0 +1,134 @@ +package bwtest + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcd/rpcclient" + "github.com/stretchr/testify/require" +) + +const ( + // minerSetupOutputs is the number of outputs to generate during miner + // setup. + minerSetupOutputs = 50 + + // minMatureBlocks is the minimum number of blocks to mine to ensure + // coinbase maturity. + minMatureBlocks = 100 + + // retryMultiplier is the multiplier for connection retries to make tests + // more robust. + retryMultiplier = 2 + + // minerWindowMultiplier is the multiplier for the miner confirmation + // window to ensure we mine enough blocks for activation. + minerWindowMultiplier = 2 +) + +var ( + // harnessNetParams is the network parameters used for the harness. + harnessNetParams = &chaincfg.RegressionNetParams +) + +// minerHarness is a wrapper around rpctest.Harness that provides a mining node +// for integration tests. +type minerHarness struct { + *testing.T + + *rpctest.Harness + + // logPath is the directory path of the miner's logs. + logPath string +} + +// newMiner creates a new minerHarness instance. +func newMiner(t *testing.T, logDir string) *minerHarness { + t.Helper() + + btcdBinary, err := GetBtcdBinary() + require.NoError(t, err, "unable to find btcd binary") + + err = ensureLogDir(logDir) + require.NoError(t, err, "unable to create miner log dir") + + args := []string{ + "--rejectnonstd", // Reject non-standard txs in tests. + "--txindex", // Required for some RPC queries. + "--nowinservice", // Avoid Windows service integration. + "--nobanning", // Avoid peer banning in local tests. + "--debuglevel=debug", // Provide detailed logs for debugging. + "--logdir=" + logDir, // Write logs into our per-run dir. + "--trickleinterval=100ms", // Speed up inv relay in regtest. + "--nostalldetect", // Avoid stall detection flakiness. + } + + // We use an empty handlers struct as we don't need to handle notifications + // directly in the miner wrapper for now. + handlers := &rpcclient.NotificationHandlers{} + + harness, err := rpctest.New(harnessNetParams, handlers, args, btcdBinary) + require.NoError(t, err, "unable to create rpctest harness") + + m := &minerHarness{ + T: t, + Harness: harness, + logPath: logDir, + } + + return m +} + +// SetUp starts the miner node and generates initial blocks to activate SegWit. +func (m *minerHarness) SetUp() { + m.Helper() + + // Increase connection retries to make tests more robust. + m.MaxConnRetries = rpctest.DefaultMaxConnectionRetries * retryMultiplier + m.ConnectionRetryTimeout = rpctest.DefaultConnectionRetryTimeout * + retryMultiplier + + require.NoError( + m, m.Harness.SetUp(true, minerSetupOutputs), + "unable to setup miner", + ) + + // Mine enough blocks to activate SegWit. + // MinerConfirmationWindow is usually 144 for mainnet, but likely smaller + // for regtest. For rpctest, standard is often to mine ~200 blocks + // total to ensure maturity and activation. Assuming harness params are + // standard regtest. + numBlocks := max( + harnessNetParams.MinerConfirmationWindow*minerWindowMultiplier, + minMatureBlocks, + ) + + _, err := m.Client.Generate(numBlocks) + require.NoError(m, err, "unable to generate initial blocks") +} + +// SetUpNoChain starts the miner node without generating a test chain. +// +// This is intended for scenarios where the miner will sync to an existing +// chain (for example, when spawning a temporary miner for reorg tests). +func (m *minerHarness) SetUpNoChain() { + m.Helper() + + // Increase connection retries to make tests more robust. + m.MaxConnRetries = rpctest.DefaultMaxConnectionRetries * retryMultiplier + m.ConnectionRetryTimeout = rpctest.DefaultConnectionRetryTimeout * + retryMultiplier + + // SetUp(true, 0) starts the node, sets up the in-memory wallet, and + // registers notifications, but does not mine any blocks. + require.NoError( + m, m.Harness.SetUp(true, 0), + "unable to setup miner", + ) +} + +// Stop shuts down the miner. +func (m *minerHarness) Stop() { + require.NoError(m, m.TearDown(), "tear down miner failed") +} diff --git a/bwtest/mock/account_store.go b/bwtest/mock/account_store.go new file mode 100644 index 0000000000..eaecfa0d18 --- /dev/null +++ b/bwtest/mock/account_store.go @@ -0,0 +1,370 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/btcutil/v2/hdkeychain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/mock" +) + +// AccountStore is a mock implementation of the waddrmgr.AccountStore +// interface. +type AccountStore struct { + mock.Mock +} + +// A compile-time assertion to ensure that AccountStore implements the +// AccountStore interface. +var _ waddrmgr.AccountStore = (*AccountStore)(nil) + +// Scope implements the waddrmgr.AccountStore interface. +func (m *AccountStore) Scope() waddrmgr.KeyScope { + args := m.Called() + return args.Get(0).(waddrmgr.KeyScope) +} + +// AddrSchema implements the waddrmgr.AccountStore interface. +func (m *AccountStore) AddrSchema() waddrmgr.ScopeAddrSchema { + args := m.Called() + return args.Get(0).(waddrmgr.ScopeAddrSchema) +} + +// AccountProperties implements the waddrmgr.AccountStore interface. +func (m *AccountStore) AccountProperties(ns walletdb.ReadBucket, + account uint32) (*waddrmgr.AccountProperties, error) { + + args := m.Called(ns, account) + return args.Get(0).(*waddrmgr.AccountProperties), args.Error(1) +} + +// LastExternalAddress implements the waddrmgr.AccountStore interface. +func (m *AccountStore) LastExternalAddress(ns walletdb.ReadBucket, + account uint32) (waddrmgr.ManagedAddress, error) { + + args := m.Called(ns, account) + return args.Get(0).(waddrmgr.ManagedAddress), args.Error(1) +} + +// LastInternalAddress implements the waddrmgr.AccountStore interface. +func (m *AccountStore) LastInternalAddress(ns walletdb.ReadBucket, + account uint32) (waddrmgr.ManagedAddress, error) { + + args := m.Called(ns, account) + return args.Get(0).(waddrmgr.ManagedAddress), args.Error(1) +} + +// ForEachAccountAddress implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ForEachAccountAddress(ns walletdb.ReadBucket, + account uint32, fn func(maddr waddrmgr.ManagedAddress) error) error { + + args := m.Called(ns, account, fn) + return args.Error(0) +} + +// LookupAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) LookupAccount(ns walletdb.ReadBucket, + name string) (uint32, error) { + + args := m.Called(ns, name) + return args.Get(0).(uint32), args.Error(1) +} + +// AccountName implements the waddrmgr.AccountStore interface. +func (m *AccountStore) AccountName(ns walletdb.ReadBucket, + account uint32) (string, error) { + + args := m.Called(ns, account) + return args.String(0), args.Error(1) +} + +// ExtendExternalAddresses implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ExtendExternalAddresses(ns walletdb.ReadWriteBucket, + account uint32, count uint32) error { + + args := m.Called(ns, account, count) + return args.Error(0) +} + +// ExtendInternalAddresses implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ExtendInternalAddresses(ns walletdb.ReadWriteBucket, + account uint32, count uint32) error { + + args := m.Called(ns, account, count) + return args.Error(0) +} + +// MarkUsed implements the waddrmgr.AccountStore interface. +func (m *AccountStore) MarkUsed(ns walletdb.ReadWriteBucket, + address address.Address) error { + + args := m.Called(ns, address) + return args.Error(0) +} + +// DeriveFromKeyPath implements the waddrmgr.AccountStore interface. +func (m *AccountStore) DeriveFromKeyPath(ns walletdb.ReadBucket, + path waddrmgr.DerivationPath) (waddrmgr.ManagedAddress, error) { + + args := m.Called(ns, path) + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(waddrmgr.ManagedAddress), args.Error(1) +} + +// CanAddAccountDeprecated implements the waddrmgr.AccountStore interface. +func (m *AccountStore) CanAddAccountDeprecated() error { + args := m.Called() + return args.Error(0) +} + +// NewAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) NewAccount(ns walletdb.ReadWriteBucket, + name string) (uint32, error) { + + args := m.Called(ns, name) + return args.Get(0).(uint32), args.Error(1) +} + +// AllocateDerivedAccountNumber implements waddrmgr.AccountStore. +func (m *AccountStore) AllocateDerivedAccountNumber( + ns walletdb.ReadWriteBucket) (uint32, error) { + + args := m.Called(ns) + return args.Get(0).(uint32), args.Error(1) +} + +// PutDerivedAccountWithKeys implements waddrmgr.AccountStore. +func (m *AccountStore) PutDerivedAccountWithKeys( + ns walletdb.ReadWriteBucket, account uint32, name string, + plaintextPubKey []byte, encryptedPrivKey []byte) error { + + args := m.Called( + ns, account, name, plaintextPubKey, encryptedPrivKey, + ) + + return args.Error(0) +} + +// AllocateImportedAccountNumber implements waddrmgr.AccountStore. +func (m *AccountStore) AllocateImportedAccountNumber( + ns walletdb.ReadWriteBucket) (uint32, error) { + + args := m.Called(ns) + return args.Get(0).(uint32), args.Error(1) +} + +// PutWatchOnlyAccountWithKeys implements waddrmgr.AccountStore. +func (m *AccountStore) PutWatchOnlyAccountWithKeys( + ns walletdb.ReadWriteBucket, account uint32, name string, + pubKey *hdkeychain.ExtendedKey, masterKeyFingerprint uint32, + addrSchema *waddrmgr.ScopeAddrSchema) error { + + args := m.Called( + ns, account, name, pubKey, masterKeyFingerprint, addrSchema, + ) + + return args.Error(0) +} + +// LastAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) LastAccount(ns walletdb.ReadBucket) (uint32, error) { + args := m.Called(ns) + return args.Get(0).(uint32), args.Error(1) +} + +// RenameAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) RenameAccount(ns walletdb.ReadWriteBucket, + account uint32, name string) error { + + args := m.Called(ns, account, name) + return args.Error(0) +} + +// NextExternalAddresses implements the waddrmgr.AccountStore interface. +func (m *AccountStore) NextExternalAddresses(ns walletdb.ReadWriteBucket, + account uint32, count uint32) ([]waddrmgr.ManagedAddress, error) { + + args := m.Called(ns, account, count) + return args.Get(0).([]waddrmgr.ManagedAddress), args.Error(1) +} + +// NextInternalAddresses implements the waddrmgr.AccountStore interface. +func (m *AccountStore) NextInternalAddresses(ns walletdb.ReadWriteBucket, + account uint32, count uint32) ([]waddrmgr.ManagedAddress, error) { + + args := m.Called(ns, account, count) + return args.Get(0).([]waddrmgr.ManagedAddress), args.Error(1) +} + +// NewAddress implements the waddrmgr.AccountStore interface. +func (m *AccountStore) NewAddress(ns walletdb.ReadWriteBucket, + account string, internal bool) (address.Address, error) { + + args := m.Called(ns, account, internal) + return args.Get(0).(address.Address), args.Error(1) +} + +// ImportPublicKey implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ImportPublicKey(ns walletdb.ReadWriteBucket, + pubKey *btcec.PublicKey, + bs *waddrmgr.BlockStamp) (waddrmgr.ManagedAddress, error) { + + args := m.Called(ns, pubKey, bs) + return args.Get(0).(waddrmgr.ManagedAddress), args.Error(1) +} + +// ImportTaprootScript implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ImportTaprootScript(ns walletdb.ReadWriteBucket, + script *waddrmgr.Tapscript, bs *waddrmgr.BlockStamp, privKeyType byte, + isInternal bool) (waddrmgr.ManagedTaprootScriptAddress, error) { + + args := m.Called(ns, script, bs, privKeyType, isInternal) + return args.Get(0).(waddrmgr.ManagedTaprootScriptAddress), args.Error(1) +} + +// ForEachAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ForEachAccount(ns walletdb.ReadBucket, + fn func(account uint32) error) error { + + args := m.Called(ns, fn) + return args.Error(0) +} + +// IsWatchOnlyAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) IsWatchOnlyAccount(ns walletdb.ReadBucket, + account uint32) (bool, error) { + + args := m.Called(ns, account) + return args.Bool(0), args.Error(1) +} + +// IsImportedAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) IsImportedAccount(ns walletdb.ReadBucket, + account uint32) (bool, error) { + + args := m.Called(ns, account) + return args.Bool(0), args.Error(1) +} + +// NewAccountWatchingOnly implements the waddrmgr.AccountStore interface. +func (m *AccountStore) NewAccountWatchingOnly(ns walletdb.ReadWriteBucket, + name string, pubKey *hdkeychain.ExtendedKey, + masterKeyFingerprint uint32, + addrSchema *waddrmgr.ScopeAddrSchema) (uint32, error) { + + args := m.Called(ns, name, pubKey, masterKeyFingerprint, addrSchema) + return args.Get(0).(uint32), args.Error(1) +} + +// InvalidateAccountCache implements the waddrmgr.AccountStore interface. +func (m *AccountStore) InvalidateAccountCache(account uint32) { + m.Called(account) +} + +// ImportPrivateKey implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ImportPrivateKey(ns walletdb.ReadWriteBucket, + wif *btcutil.WIF, + bs *waddrmgr.BlockStamp) (waddrmgr.ManagedPubKeyAddress, error) { + + args := m.Called(ns, wif, bs) + return args.Get(0).(waddrmgr.ManagedPubKeyAddress), args.Error(1) +} + +// ActiveAccounts implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ActiveAccounts() []uint32 { + args := m.Called() + return args.Get(0).([]uint32) +} + +// ExtendAddresses implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ExtendAddresses(ns walletdb.ReadWriteBucket, + account uint32, lastIndex uint32, branch uint32) error { + + args := m.Called(ns, account, lastIndex, branch) + return args.Error(0) +} + +// DeriveAddr implements the waddrmgr.AccountStore interface. +func (m *AccountStore) DeriveAddr(account, branch, index uint32) ( + address.Address, []byte, error) { + + args := m.Called(account, branch, index) + + var addr address.Address + if args.Get(0) != nil { + addr = args.Get(0).(address.Address) + } + + var script []byte + if args.Get(1) != nil { + script = args.Get(1).([]byte) + } + + return addr, script, args.Error(2) +} + +// AddrAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) AddrAccount(ns walletdb.ReadBucket, + address address.Address) (uint32, error) { + + args := m.Called(ns, address) + return args.Get(0).(uint32), args.Error(1) +} + +// DeriveFromKeyPathCache implements the waddrmgr.AccountStore interface. +func (m *AccountStore) DeriveFromKeyPathCache( + kp waddrmgr.DerivationPath) (*btcec.PrivateKey, error) { + + args := m.Called(kp) + return args.Get(0).(*btcec.PrivateKey), args.Error(1) +} + +// NewRawAccount implements the waddrmgr.AccountStore interface. +func (m *AccountStore) NewRawAccount(ns walletdb.ReadWriteBucket, + number uint32) error { + + args := m.Called(ns, number) + return args.Error(0) +} + +// NewRawAccountWatchingOnly implements the waddrmgr.AccountStore interface. +func (m *AccountStore) NewRawAccountWatchingOnly( + ns walletdb.ReadWriteBucket, + number uint32, pubKey *hdkeychain.ExtendedKey, + masterKeyFingerprint uint32, + addrSchema *waddrmgr.ScopeAddrSchema) error { + + args := m.Called(ns, number, pubKey, masterKeyFingerprint, addrSchema) + return args.Error(0) +} + +// ImportScript implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ImportScript( + ns walletdb.ReadWriteBucket, script []byte, + bs *waddrmgr.BlockStamp) (waddrmgr.ManagedScriptAddress, error) { + + args := m.Called(ns, script, bs) + return args.Get(0).(waddrmgr.ManagedScriptAddress), args.Error(1) +} + +// ImportWitnessScript implements the waddrmgr.AccountStore interface. +func (m *AccountStore) ImportWitnessScript(ns walletdb.ReadWriteBucket, + script []byte, bs *waddrmgr.BlockStamp, witnessVersion byte, + isSecretScript bool) (waddrmgr.ManagedScriptAddress, error) { + + args := m.Called(ns, script, bs, witnessVersion, isSecretScript) + if v := args.Get(0); v != nil { + return v.(waddrmgr.ManagedScriptAddress), args.Error(1) + } + + return nil, args.Error(1) +} diff --git a/bwtest/mock/addr_store.go b/bwtest/mock/addr_store.go new file mode 100644 index 0000000000..fe1e39d878 --- /dev/null +++ b/bwtest/mock/addr_store.go @@ -0,0 +1,292 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "time" + + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/mock" +) + +// AddrStore is a mock implementation of the waddrmgr.AddrStore interface. +type AddrStore struct { + mock.Mock +} + +// Birthday returns the birthday of the address store. +func (m *AddrStore) Birthday() time.Time { + args := m.Called() + return args.Get(0).(time.Time) +} + +// SetSyncedTo marks the address manager to be in sync with the +// recently-seen block described by the blockstamp. +func (m *AddrStore) SetSyncedTo(ns walletdb.ReadWriteBucket, + bs *waddrmgr.BlockStamp) error { + + args := m.Called(ns, bs) + return args.Error(0) +} + +// SetBirthdayBlock sets the birthday block, or earliest time a key could +// have been used, for the manager. +func (m *AddrStore) SetBirthdayBlock(ns walletdb.ReadWriteBucket, + block waddrmgr.BlockStamp, verified bool) error { + + args := m.Called(ns, block, verified) + return args.Error(0) +} + +// SyncedTo returns details about the block height and hash that the +// address manager is synced through at the very least. +func (m *AddrStore) SyncedTo() waddrmgr.BlockStamp { + args := m.Called() + return args.Get(0).(waddrmgr.BlockStamp) +} + +// BlockHash returns the block hash at a particular block height. +func (m *AddrStore) BlockHash(ns walletdb.ReadBucket, + height int32) (*chainhash.Hash, error) { + + args := m.Called(ns, height) + return args.Get(0).(*chainhash.Hash), args.Error(1) +} + +// ActiveScopedKeyManagers returns a slice of all the active scoped key +// managers currently known by the root key manager. +func (m *AddrStore) ActiveScopedKeyManagers() []waddrmgr.AccountStore { + args := m.Called() + return args.Get(0).([]waddrmgr.AccountStore) +} + +// FetchScopedKeyManager attempts to fetch an active scoped manager +// according to its registered scope. +func (m *AddrStore) FetchScopedKeyManager( + scope waddrmgr.KeyScope) (waddrmgr.AccountStore, error) { + + args := m.Called(scope) + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(waddrmgr.AccountStore), args.Error(1) +} + +// Address returns a managed address given the passed address if it is +// known to the address manager. +func (m *AddrStore) Address(ns walletdb.ReadBucket, + address address.Address) (waddrmgr.ManagedAddress, error) { + + args := m.Called(ns, address) + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(waddrmgr.ManagedAddress), args.Error(1) +} + +// AddrAccount returns the account to which the given address belongs. +func (m *AddrStore) AddrAccount(ns walletdb.ReadBucket, + address address.Address) (waddrmgr.AccountStore, uint32, error) { + + args := m.Called(ns, address) + + if args.Get(0) == nil { + return nil, args.Get(1).(uint32), args.Error(2) + } + + return args.Get(0).(waddrmgr.AccountStore), + args.Get(1).(uint32), args.Error(2) +} + +// AddressDetails determines whether the wallet has access to the private +// keys required to sign for a given address, and returns other address +// details. +func (m *AddrStore) AddressDetails(ns walletdb.ReadBucket, + addr address.Address) (bool, string, waddrmgr.AddressType) { + + args := m.Called(ns, addr) + return args.Bool(0), args.String(1), args.Get(2).(waddrmgr.AddressType) +} + +// ForEachRelevantActiveAddress invokes the given closure on each active +// address relevant to the wallet. +func (m *AddrStore) ForEachRelevantActiveAddress(ns walletdb.ReadBucket, + fn func(addr address.Address) error) error { + + args := m.Called(ns, fn) + return args.Error(0) +} + +// Unlock derives the master private key from the specified passphrase. +func (m *AddrStore) Unlock(ns walletdb.ReadBucket, + passphrase []byte) error { + + args := m.Called(ns, passphrase) + return args.Error(0) +} + +// Lock performs a best try effort to remove and zero all secret keys +// associated with the address manager. +func (m *AddrStore) Lock() error { + args := m.Called() + return args.Error(0) +} + +// IsLocked returns whether or not the address managed is locked. +func (m *AddrStore) IsLocked() bool { + args := m.Called() + return args.Bool(0) +} + +// ChangePassphrase changes either the public or private passphrase to +// the provided value depending on the private flag. +func (m *AddrStore) ChangePassphrase(ns walletdb.ReadWriteBucket, + oldPass, newPass []byte, private bool, + scryptOptions *waddrmgr.ScryptOptions) error { + + args := m.Called(ns, oldPass, newPass, private, scryptOptions) + return args.Error(0) +} + +// WatchOnly returns true if the root manager is in watch only mode, and +// false otherwise. +func (m *AddrStore) WatchOnly() bool { + args := m.Called() + return args.Bool(0) +} + +// MarkUsed updates the used flag for the provided address. +func (m *AddrStore) MarkUsed(ns walletdb.ReadWriteBucket, + address address.Address) error { + + args := m.Called(ns, address) + return args.Error(0) +} + +// BirthdayBlock returns the birthday block of the address store. +func (m *AddrStore) BirthdayBlock( + ns walletdb.ReadBucket) (waddrmgr.BlockStamp, bool, error) { + + args := m.Called(ns) + return args.Get(0).(waddrmgr.BlockStamp), args.Bool(1), args.Error(2) +} + +// IsWatchOnlyAccount determines if the account with the given key scope +// is set up as watch-only. +func (m *AddrStore) IsWatchOnlyAccount(ns walletdb.ReadBucket, + keyScope waddrmgr.KeyScope, account uint32) (bool, error) { + + args := m.Called(ns, keyScope, account) + return args.Bool(0), args.Error(1) +} + +// NewScopedKeyManager creates a new scoped key manager from the root +// manager. +func (m *AddrStore) NewScopedKeyManager(ns walletdb.ReadWriteBucket, + scope waddrmgr.KeyScope, + addrSchema waddrmgr.ScopeAddrSchema) (waddrmgr.AccountStore, error) { + + args := m.Called(ns, scope, addrSchema) + return args.Get(0).(waddrmgr.AccountStore), args.Error(1) +} + +// SetBirthday sets the birthday of the address store. +func (m *AddrStore) SetBirthday(ns walletdb.ReadWriteBucket, + birthday time.Time) error { + + args := m.Called(ns, birthday) + return args.Error(0) +} + +// ForEachAccountAddress calls the given function with each address of +// the given account stored in the manager, breaking early on error. +func (m *AddrStore) ForEachAccountAddress(ns walletdb.ReadBucket, + account uint32, fn func(maddr waddrmgr.ManagedAddress) error) error { + + args := m.Called(ns, account, fn) + return args.Error(0) +} + +// LookupAccount returns the corresponding key scope and account number +// for the account with the given name. +func (m *AddrStore) LookupAccount(ns walletdb.ReadBucket, + name string) (waddrmgr.KeyScope, uint32, error) { + + args := m.Called(ns, name) + + return args.Get(0).(waddrmgr.KeyScope), + args.Get(1).(uint32), args.Error(2) +} + +// ForEachActiveAddress calls the given function with each active address +// stored in the manager, breaking early on error. +func (m *AddrStore) ForEachActiveAddress(ns walletdb.ReadBucket, + fn func(addr address.Address) error) error { + + args := m.Called(ns, fn) + return args.Error(0) +} + +// ConvertToWatchingOnly converts the current address manager to a locked +// watching-only address manager. +func (m *AddrStore) ConvertToWatchingOnly( + ns walletdb.ReadWriteBucket) error { + + args := m.Called(ns) + return args.Error(0) +} + +// ChainParams returns the chain parameters for this address manager. +func (m *AddrStore) ChainParams() *chaincfg.Params { + args := m.Called() + return args.Get(0).(*chaincfg.Params) +} + +// Close cleanly shuts down the manager. +func (m *AddrStore) Close() { + m.Called() +} + +// EncryptedMasterHDPriv implements the waddrmgr.AddrStore interface. +func (m *AddrStore) EncryptedMasterHDPriv( + ns walletdb.ReadBucket) ([]byte, error) { + + args := m.Called(ns) + if raw, ok := args.Get(0).([]byte); ok { + return raw, args.Error(1) + } + + return nil, args.Error(1) +} + +// Encrypt implements keyvault.Vault. +func (m *AddrStore) Encrypt(keyType waddrmgr.CryptoKeyType, + plaintext []byte) ([]byte, error) { + + args := m.Called(keyType, plaintext) + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).([]byte), args.Error(1) +} + +// Decrypt implements keyvault.Vault. +func (m *AddrStore) Decrypt(keyType waddrmgr.CryptoKeyType, + ciphertext []byte) ([]byte, error) { + + args := m.Called(keyType, ciphertext) + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).([]byte), args.Error(1) +} diff --git a/bwtest/mock/address.go b/bwtest/mock/address.go new file mode 100644 index 0000000000..b612b25140 --- /dev/null +++ b/bwtest/mock/address.go @@ -0,0 +1,45 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/stretchr/testify/mock" +) + +// Address is a mock implementation of the address.Address interface. +// It embeds mock.Mock to allow for flexible stubbing of its methods, +// enabling granular control over address behavior in tests. +type Address struct { + mock.Mock +} + +// EncodeAddress mocks the EncodeAddress method. +// It returns a predefined string based on mock expectations. +func (m *Address) EncodeAddress() string { + args := m.Called() + return args.String(0) +} + +// ScriptAddress mocks the ScriptAddress method. +// It returns a predefined byte slice based on mock expectations. +func (m *Address) ScriptAddress() []byte { + args := m.Called() + return args.Get(0).([]byte) +} + +// IsForNet mocks the IsForNet method. +// It returns a predefined boolean based on mock expectations. +func (m *Address) IsForNet(params *chaincfg.Params) bool { + args := m.Called(params) + return args.Bool(0) +} + +// String mocks the String method. +// It returns a predefined string based on mock expectations. +func (m *Address) String() string { + args := m.Called() + return args.String(0) +} diff --git a/bwtest/mock/chain.go b/bwtest/mock/chain.go new file mode 100644 index 0000000000..2cc594565e --- /dev/null +++ b/bwtest/mock/chain.go @@ -0,0 +1,207 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "context" + + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil/v2/gcs" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/stretchr/testify/mock" +) + +// Chain is a testify mock implementation of chain.Interface. Every +// method that runs through wallet code paths under test must have an +// expectation configured via .On("Method", args...).Return(...) — calls +// without a matching expectation panic, by design. Use .Maybe() for +// methods whose specific behavior is not under test. +type Chain struct { + mock.Mock +} + +// A compile-time assertion to ensure that Chain implements the +// chain.Interface. +var _ chain.Interface = (*Chain)(nil) + +// Start implements the chain.Interface interface. +func (m *Chain) Start(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +// Stop implements the chain.Interface interface. +func (m *Chain) Stop() { + m.Called() +} + +// WaitForShutdown implements the chain.Interface interface. +func (m *Chain) WaitForShutdown() { + m.Called() +} + +// GetBestBlock implements the chain.Interface interface. +func (m *Chain) GetBestBlock() (*chainhash.Hash, int32, error) { + args := m.Called() + hash, _ := args.Get(0).(*chainhash.Hash) + + return hash, args.Get(1).(int32), args.Error(2) +} + +// GetBlock implements the chain.Interface interface. +func (m *Chain) GetBlock(hash *chainhash.Hash) (*wire.MsgBlock, error) { + args := m.Called(hash) + block, _ := args.Get(0).(*wire.MsgBlock) + + return block, args.Error(1) +} + +// GetBlockHash implements the chain.Interface interface. +func (m *Chain) GetBlockHash(height int64) (*chainhash.Hash, error) { + args := m.Called(height) + hash, _ := args.Get(0).(*chainhash.Hash) + + return hash, args.Error(1) +} + +// GetBlockHeader implements the chain.Interface interface. +func (m *Chain) GetBlockHeader( + hash *chainhash.Hash) (*wire.BlockHeader, error) { + + args := m.Called(hash) + header, _ := args.Get(0).(*wire.BlockHeader) + + return header, args.Error(1) +} + +// GetBlockHashes implements the chain.Interface interface. +func (m *Chain) GetBlockHashes(start, end int64) ([]chainhash.Hash, error) { + args := m.Called(start, end) + return args.Get(0).([]chainhash.Hash), args.Error(1) +} + +// GetBlockHeaders implements the chain.Interface interface. +func (m *Chain) GetBlockHeaders( + hashes []chainhash.Hash) ([]*wire.BlockHeader, error) { + + args := m.Called(hashes) + return args.Get(0).([]*wire.BlockHeader), args.Error(1) +} + +// GetCFilters implements the chain.Interface interface. +func (m *Chain) GetCFilters(hashes []chainhash.Hash, + filterType wire.FilterType) ([]*gcs.Filter, error) { + + args := m.Called(hashes, filterType) + return args.Get(0).([]*gcs.Filter), args.Error(1) +} + +// GetBlocks implements the chain.Interface interface. +func (m *Chain) GetBlocks( + hashes []chainhash.Hash) ([]*wire.MsgBlock, error) { + + args := m.Called(hashes) + return args.Get(0).([]*wire.MsgBlock), args.Error(1) +} + +// IsCurrent implements the chain.Interface interface. +func (m *Chain) IsCurrent() bool { + args := m.Called() + return args.Bool(0) +} + +// GetCFilter implements the chain.Interface interface. +func (m *Chain) GetCFilter(hash *chainhash.Hash, + filterType wire.FilterType) (*gcs.Filter, error) { + + args := m.Called(hash, filterType) + return args.Get(0).(*gcs.Filter), args.Error(1) +} + +// FilterBlocks implements the chain.Interface interface. +func (m *Chain) FilterBlocks(req *chain.FilterBlocksRequest) ( + *chain.FilterBlocksResponse, error) { + + args := m.Called(req) + return args.Get(0).(*chain.FilterBlocksResponse), args.Error(1) +} + +// BlockStamp implements the chain.Interface interface. +func (m *Chain) BlockStamp() (*waddrmgr.BlockStamp, error) { + args := m.Called() + return args.Get(0).(*waddrmgr.BlockStamp), args.Error(1) +} + +// SendRawTransaction implements the chain.Interface interface. +func (m *Chain) SendRawTransaction(tx *wire.MsgTx, + allowHighFees bool) (*chainhash.Hash, error) { + + args := m.Called(tx, allowHighFees) + hash, _ := args.Get(0).(*chainhash.Hash) + + return hash, args.Error(1) +} + +// Rescan implements the chain.Interface interface. +func (m *Chain) Rescan(hash *chainhash.Hash, addrs []address.Address, + outpoints map[wire.OutPoint]address.Address) error { + + args := m.Called(hash, addrs, outpoints) + return args.Error(0) +} + +// NotifyReceived implements the chain.Interface interface. +func (m *Chain) NotifyReceived(addrs []address.Address) error { + args := m.Called(addrs) + return args.Error(0) +} + +// NotifyBlocks implements the chain.Interface interface. +func (m *Chain) NotifyBlocks() error { + args := m.Called() + return args.Error(0) +} + +// Notifications implements the chain.Interface interface. +func (m *Chain) Notifications() <-chan any { + args := m.Called() + return args.Get(0).(<-chan any) +} + +// BackEnd implements the chain.Interface interface. +func (m *Chain) BackEnd() string { + args := m.Called() + return args.String(0) +} + +// TestMempoolAccept implements the chain.Interface interface. +func (m *Chain) TestMempoolAccept(txns []*wire.MsgTx, + maxFeeRate float64) ([]*btcjson.TestMempoolAcceptResult, error) { + + args := m.Called(txns, maxFeeRate) + results, _ := args.Get(0).([]*btcjson.TestMempoolAcceptResult) + + return results, args.Error(1) +} + +// SubmitPackage implements the chain.Interface interface. +func (m *Chain) SubmitPackage(txns []*wire.MsgTx, + maxFeeRate *float64) (*btcjson.SubmitPackageResult, error) { + + args := m.Called(txns, maxFeeRate) + result, _ := args.Get(0).(*btcjson.SubmitPackageResult) + + return result, args.Error(1) +} + +// MapRPCErr implements the chain.Interface interface. +func (m *Chain) MapRPCErr(err error) error { + args := m.Called(err) + return args.Error(0) +} diff --git a/bwtest/mock/doc.go b/bwtest/mock/doc.go new file mode 100644 index 0000000000..ebf217cec4 --- /dev/null +++ b/bwtest/mock/doc.go @@ -0,0 +1,6 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +// Package mock contains shared testify-based test doubles. +package mock diff --git a/bwtest/mock/managed_address.go b/bwtest/mock/managed_address.go new file mode 100644 index 0000000000..006612e459 --- /dev/null +++ b/bwtest/mock/managed_address.go @@ -0,0 +1,80 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/mock" +) + +// ManagedAddress is a mock implementation of the waddrmgr.ManagedAddress +// interface. +type ManagedAddress struct { + mock.Mock +} + +// A compile-time assertion to ensure that ManagedAddress implements the +// ManagedAddress interface. +var _ waddrmgr.ManagedAddress = (*ManagedAddress)(nil) + +// Address implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) Address() address.Address { + args := m.Called() + return args.Get(0).(address.Address) +} + +// AddrHash implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) AddrHash() []byte { + args := m.Called() + return args.Get(0).([]byte) +} + +// Imported implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) Imported() bool { + args := m.Called() + return args.Bool(0) +} + +// Internal implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) Internal() bool { + args := m.Called() + return args.Bool(0) +} + +// Compressed implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) Compressed() bool { + args := m.Called() + return args.Bool(0) +} + +// Used implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) Used(ns walletdb.ReadBucket) bool { + args := m.Called(ns) + return args.Bool(0) +} + +// AddrType implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) AddrType() waddrmgr.AddressType { + args := m.Called() + return args.Get(0).(waddrmgr.AddressType) +} + +// InternalAccount implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) InternalAccount() uint32 { + args := m.Called() + return args.Get(0).(uint32) +} + +// DerivationInfo implements the waddrmgr.ManagedAddress interface. +func (m *ManagedAddress) DerivationInfo() ( + waddrmgr.KeyScope, waddrmgr.DerivationPath, bool) { + + args := m.Called() + + return args.Get(0).(waddrmgr.KeyScope), + args.Get(1).(waddrmgr.DerivationPath), args.Bool(2) +} diff --git a/bwtest/mock/managed_pub_key_addr.go b/bwtest/mock/managed_pub_key_addr.go new file mode 100644 index 0000000000..dece599383 --- /dev/null +++ b/bwtest/mock/managed_pub_key_addr.go @@ -0,0 +1,126 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/stretchr/testify/mock" +) + +// ManagedPubKeyAddr is a mock implementation of the +// waddrmgr.ManagedPubKeyAddress interface, used for testing. +type ManagedPubKeyAddr struct { + mock.Mock +} + +// A compile-time check to ensure that ManagedPubKeyAddr implements the +// ManagedPubKeyAddress interface. +var _ waddrmgr.ManagedPubKeyAddress = (*ManagedPubKeyAddr)(nil) + +// PubKey implements the waddrmgr.ManagedPubKeyAddress interface. +func (m *ManagedPubKeyAddr) PubKey() *btcec.PublicKey { + args := m.Called() + if args.Get(0) == nil { + return nil + } + + return args.Get(0).(*btcec.PublicKey) +} + +// ExportPrivKey implements the waddrmgr.ManagedPubKeyAddress interface. +func (m *ManagedPubKeyAddr) ExportPrivKey() (*btcutil.WIF, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*btcutil.WIF), args.Error(1) +} + +// ExportPubKey implements the waddrmgr.ManagedPubKeyAddress interface. +func (m *ManagedPubKeyAddr) ExportPubKey() string { + args := m.Called() + return args.String(0) +} + +// PrivKey implements the waddrmgr.ManagedPubKeyAddress interface. +func (m *ManagedPubKeyAddr) PrivKey() (*btcec.PrivateKey, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*btcec.PrivateKey), args.Error(1) +} + +// Address implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) Address() address.Address { + args := m.Called() + if args.Get(0) == nil { + return nil + } + + return args.Get(0).(address.Address) +} + +// AddrHash implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) AddrHash() []byte { + args := m.Called() + if args.Get(0) == nil { + return nil + } + + return args.Get(0).([]byte) +} + +// Imported implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) Imported() bool { + args := m.Called() + return args.Bool(0) +} + +// Internal implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) Internal() bool { + args := m.Called() + return args.Bool(0) +} + +// Compressed implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) Compressed() bool { + args := m.Called() + return args.Bool(0) +} + +// Used implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) Used(ns walletdb.ReadBucket) bool { + args := m.Called(ns) + return args.Bool(0) +} + +// AddrType implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) AddrType() waddrmgr.AddressType { + args := m.Called() + return args.Get(0).(waddrmgr.AddressType) +} + +// InternalAccount implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) InternalAccount() uint32 { + args := m.Called() + return args.Get(0).(uint32) +} + +// DerivationInfo implements the waddrmgr.ManagedAddress interface. +func (m *ManagedPubKeyAddr) DerivationInfo() (waddrmgr.KeyScope, + waddrmgr.DerivationPath, bool) { + + args := m.Called() + + return args.Get(0).(waddrmgr.KeyScope), + args.Get(1).(waddrmgr.DerivationPath), args.Bool(2) +} diff --git a/bwtest/mock/managed_taproot_script_addr.go b/bwtest/mock/managed_taproot_script_addr.go new file mode 100644 index 0000000000..c819229757 --- /dev/null +++ b/bwtest/mock/managed_taproot_script_addr.go @@ -0,0 +1,43 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "github.com/btcsuite/btcwallet/waddrmgr" +) + +// ManagedTaprootScriptAddress is a mock implementation of the +// waddrmgr.ManagedTaprootScriptAddress interface. +type ManagedTaprootScriptAddress struct { + ManagedAddress +} + +// A compile-time assertion to ensure that ManagedTaprootScriptAddress +// implements the waddrmgr.ManagedTaprootScriptAddress interface. +var _ waddrmgr.ManagedTaprootScriptAddress = (*ManagedTaprootScriptAddress)( + nil, +) + +// Script implements the waddrmgr.ManagedScriptAddress interface. +func (m *ManagedTaprootScriptAddress) Script() ([]byte, error) { + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).([]byte), args.Error(1) +} + +// TaprootScript implements the waddrmgr.ManagedTaprootScriptAddress interface. +func (m *ManagedTaprootScriptAddress) TaprootScript() ( + *waddrmgr.Tapscript, error) { + + args := m.Called() + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).(*waddrmgr.Tapscript), args.Error(1) +} diff --git a/bwtest/mock/mempool_chain.go b/bwtest/mock/mempool_chain.go new file mode 100644 index 0000000000..8ffcdd6302 --- /dev/null +++ b/bwtest/mock/mempool_chain.go @@ -0,0 +1,249 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "context" + "maps" + "sync" + + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcjson" + "github.com/btcsuite/btcd/btcutil/v2/gcs" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" +) + +// mempoolAcceptResultReject is the reject reason recorded for a transaction +// that the in-memory MempoolChain has already accepted. +const mempoolAcceptResultReject = "txn-already-in-mempool" + +// MempoolChain is a real-style chain.Interface fake that tracks broadcast +// transactions in an in-memory mempool. It is NOT a testify mock: methods +// return deterministic results computed from the in-memory state, so callers +// (typically benchmarks) avoid the per-call overhead of testify expectations. +// +// Methods not relevant to the broadcast path return zero values. Add real +// behavior to a method only when a benchmark or fake-driven test needs it. +type MempoolChain struct { + mu sync.RWMutex + mempool map[chainhash.Hash]*wire.MsgTx +} + +// A compile-time assertion to ensure that MempoolChain implements the +// chain.Interface. +var _ chain.Interface = (*MempoolChain)(nil) + +// Reset clears all transactions from the in-memory mempool so a benchmark +// loop can establish a clean baseline before measurement. +func (c *MempoolChain) Reset() { + c.mu.Lock() + defer c.mu.Unlock() + + c.mempool = make(map[chainhash.Hash]*wire.MsgTx) +} + +// Snapshot returns a shallow copy of the in-memory mempool keyed by tx hash. +func (c *MempoolChain) Snapshot() map[chainhash.Hash]*wire.MsgTx { + c.mu.RLock() + defer c.mu.RUnlock() + + result := make(map[chainhash.Hash]*wire.MsgTx, len(c.mempool)) + maps.Copy(result, c.mempool) + + return result +} + +// Start implements the chain.Interface interface. It initializes the +// in-memory mempool so concurrent SendRawTransaction calls observe a +// non-nil map without racing on lazy init. +func (c *MempoolChain) Start(_ context.Context) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.mempool == nil { + c.mempool = make(map[chainhash.Hash]*wire.MsgTx) + } + + return nil +} + +// Stop implements the chain.Interface interface. It clears the in-memory +// mempool so a subsequent Start observes a clean state. +func (c *MempoolChain) Stop() { + c.mu.Lock() + defer c.mu.Unlock() + + c.mempool = nil +} + +// WaitForShutdown implements the chain.Interface interface. +func (c *MempoolChain) WaitForShutdown() {} + +// GetBestBlock implements the chain.Interface interface. The fake has no +// block state, so it reports height zero with no hash. +func (c *MempoolChain) GetBestBlock() (*chainhash.Hash, int32, error) { + return nil, 0, nil +} + +// GetBlock implements the chain.Interface interface. +func (c *MempoolChain) GetBlock(*chainhash.Hash) (*wire.MsgBlock, error) { + return nil, nil +} + +// GetBlockHash implements the chain.Interface interface. +func (c *MempoolChain) GetBlockHash(int64) (*chainhash.Hash, error) { + return nil, nil +} + +// GetBlockHeader implements the chain.Interface interface. +func (c *MempoolChain) GetBlockHeader( + *chainhash.Hash) (*wire.BlockHeader, error) { + + return nil, nil +} + +// GetBlockHashes implements the chain.Interface interface. +func (c *MempoolChain) GetBlockHashes(int64, int64) ([]chainhash.Hash, error) { + return nil, nil +} + +// GetBlockHeaders implements the chain.Interface interface. +func (c *MempoolChain) GetBlockHeaders( + []chainhash.Hash) ([]*wire.BlockHeader, error) { + + return nil, nil +} + +// GetCFilters implements the chain.Interface interface. +func (c *MempoolChain) GetCFilters([]chainhash.Hash, + wire.FilterType) ([]*gcs.Filter, error) { + + return nil, nil +} + +// GetBlocks implements the chain.Interface interface. +func (c *MempoolChain) GetBlocks([]chainhash.Hash) ([]*wire.MsgBlock, error) { + return nil, nil +} + +// IsCurrent implements the chain.Interface interface. +func (c *MempoolChain) IsCurrent() bool { return false } + +// GetCFilter implements the chain.Interface interface. +func (c *MempoolChain) GetCFilter(*chainhash.Hash, + wire.FilterType) (*gcs.Filter, error) { + + return nil, nil +} + +// FilterBlocks implements the chain.Interface interface. +func (c *MempoolChain) FilterBlocks(*chain.FilterBlocksRequest) ( + *chain.FilterBlocksResponse, error) { + + return nil, nil +} + +// BlockStamp implements the chain.Interface interface. +func (c *MempoolChain) BlockStamp() (*waddrmgr.BlockStamp, error) { + return nil, nil +} + +// SendRawTransaction implements the chain.Interface interface. The +// transaction is recorded in the in-memory mempool keyed by its hash; a +// second broadcast of the same tx returns chain.ErrTxAlreadyInMempool, which +// matches the real RPC behavior tx_publisher idempotency checks rely on. +func (c *MempoolChain) SendRawTransaction(tx *wire.MsgTx, + _ bool) (*chainhash.Hash, error) { + + c.mu.Lock() + defer c.mu.Unlock() + + if c.mempool == nil { + c.mempool = make(map[chainhash.Hash]*wire.MsgTx) + } + + txHash := tx.TxHash() + if _, exists := c.mempool[txHash]; exists { + return nil, chain.ErrTxAlreadyInMempool + } + + c.mempool[txHash] = tx + + return &txHash, nil +} + +// Rescan implements the chain.Interface interface. +func (c *MempoolChain) Rescan(*chainhash.Hash, []address.Address, + map[wire.OutPoint]address.Address) error { + + return nil +} + +// NotifyReceived implements the chain.Interface interface. +func (c *MempoolChain) NotifyReceived([]address.Address) error { return nil } + +// NotifyBlocks implements the chain.Interface interface. +func (c *MempoolChain) NotifyBlocks() error { return nil } + +// Notifications implements the chain.Interface interface. +func (c *MempoolChain) Notifications() <-chan any { return nil } + +// BackEnd implements the chain.Interface interface. +func (c *MempoolChain) BackEnd() string { return "mempool-fake" } + +// TestMempoolAccept implements the chain.Interface interface. Each input +// transaction reports Allowed=true unless it is already present in the fake +// mempool, in which case it is rejected with mempoolAcceptResultReject so +// MapRPCErr translates the reject into chain.ErrTxAlreadyInMempool. +func (c *MempoolChain) TestMempoolAccept(txns []*wire.MsgTx, + _ float64) ([]*btcjson.TestMempoolAcceptResult, error) { + + c.mu.RLock() + defer c.mu.RUnlock() + + results := make([]*btcjson.TestMempoolAcceptResult, len(txns)) + for i := range txns { + txHash := txns[i].TxHash() + result := &btcjson.TestMempoolAcceptResult{ + Txid: txHash.String(), + } + if _, exists := c.mempool[txHash]; exists { + result.Allowed = false + result.RejectReason = mempoolAcceptResultReject + } else { + result.Allowed = true + } + + results[i] = result + } + + return results, nil +} + +// MapRPCErr implements the chain.Interface interface. The fake recognizes +// its own mempool-already-accepted reject reason and translates it to the +// chain package sentinel so callers can use errors.Is. +func (c *MempoolChain) MapRPCErr(err error) error { + if err == nil { + return nil + } + + if err.Error() == mempoolAcceptResultReject { + return chain.ErrTxAlreadyInMempool + } + + return err +} + +// SubmitPackage implements the chain.Interface interface. The fake performs +// no package relay and simply returns an empty result. +func (c *MempoolChain) SubmitPackage(_ []*wire.MsgTx, + _ *float64) (*btcjson.SubmitPackageResult, error) { + + return &btcjson.SubmitPackageResult{}, nil +} diff --git a/bwtest/mock/neutrino_chain.go b/bwtest/mock/neutrino_chain.go new file mode 100644 index 0000000000..368b25dfc7 --- /dev/null +++ b/bwtest/mock/neutrino_chain.go @@ -0,0 +1,184 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/btcutil/v2/gcs" + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/btcsuite/btcwallet/chain" + "github.com/lightninglabs/neutrino" + "github.com/lightninglabs/neutrino/banman" + "github.com/lightninglabs/neutrino/headerfs" +) + +// NeutrinoChain is a mock implementation of the chain.NeutrinoChainService +// interface. +type NeutrinoChain struct { + Chain +} + +// A compile-time assertion to ensure that NeutrinoChain implements the +// chain.NeutrinoChainService. +var _ chain.NeutrinoChainService = (*NeutrinoChain)(nil) + +// Stop implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) Stop() error { + args := m.Called() + return args.Error(0) +} + +// GetBlock implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) GetBlock(hash chainhash.Hash, + opts ...neutrino.QueryOption) (*btcutil.Block, error) { + + args := m.Called(hash, opts) + if args.Get(0) != nil { + if val, ok := args.Get(0).(*btcutil.Block); ok { + return val, args.Error(1) + } + } + + return nil, args.Error(1) +} + +// GetCFilter implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) GetCFilter(hash chainhash.Hash, + filterType wire.FilterType, + opts ...neutrino.QueryOption) (*gcs.Filter, error) { + + args := m.Called(hash, filterType, opts) + if args.Get(0) != nil { + if val, ok := args.Get(0).(*gcs.Filter); ok { + return val, args.Error(1) + } + } + + return nil, args.Error(1) +} + +// GetBlockHeight implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) GetBlockHeight( + hash *chainhash.Hash) (int32, error) { + + args := m.Called(hash) + return args.Get(0).(int32), args.Error(1) +} + +// BestBlock implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) BestBlock() (*headerfs.BlockStamp, error) { + args := m.Called() + if args.Get(0) != nil { + if val, ok := args.Get(0).(*headerfs.BlockStamp); ok { + return val, args.Error(1) + } + } + + return nil, args.Error(1) +} + +// SendTransaction implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) SendTransaction(tx *wire.MsgTx) error { + args := m.Called(tx) + return args.Error(0) +} + +// GetUtxo implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) GetUtxo( + opts ...neutrino.RescanOption) (*neutrino.SpendReport, error) { + + args := m.Called(opts) + if args.Get(0) != nil { + if val, ok := args.Get(0).(*neutrino.SpendReport); ok { + return val, args.Error(1) + } + } + + return nil, args.Error(1) +} + +// BanPeer implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) BanPeer(addr string, + reason banman.Reason) error { + + args := m.Called(addr, reason) + return args.Error(0) +} + +// IsBanned implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) IsBanned(addr string) bool { + args := m.Called(addr) + return args.Bool(0) +} + +// AddPeer implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) AddPeer(peer *neutrino.ServerPeer) { + m.Called(peer) +} + +// AddBytesSent implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) AddBytesSent(bytes uint64) { + m.Called(bytes) +} + +// AddBytesReceived implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) AddBytesReceived(bytes uint64) { + m.Called(bytes) +} + +// NetTotals implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) NetTotals() (uint64, uint64) { + args := m.Called() + + var a, b uint64 + if args.Get(0) != nil { + if val, ok := args.Get(0).(uint64); ok { + a = val + } + } + + if args.Get(1) != nil { + if val, ok := args.Get(1).(uint64); ok { + b = val + } + } + + return a, b +} + +// UpdatePeerHeights implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) UpdatePeerHeights(hash *chainhash.Hash, + height int32, peer *neutrino.ServerPeer) { + + m.Called(hash, height, peer) +} + +// ChainParams implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) ChainParams() chaincfg.Params { + args := m.Called() + if args.Get(0) != nil { + if val, ok := args.Get(0).(chaincfg.Params); ok { + return val + } + } + + return chaincfg.Params{} +} + +// PeerByAddr implements the chain.NeutrinoChainService interface. +func (m *NeutrinoChain) PeerByAddr( + addr string) *neutrino.ServerPeer { + + args := m.Called(addr) + if args.Get(0) != nil { + if val, ok := args.Get(0).(*neutrino.ServerPeer); ok { + return val + } + } + + return nil +} diff --git a/bwtest/mock/tx_store.go b/bwtest/mock/tx_store.go new file mode 100644 index 0000000000..ca48d9c2c0 --- /dev/null +++ b/bwtest/mock/tx_store.go @@ -0,0 +1,236 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "time" + + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcd/chainhash/v2" + "github.com/btcsuite/btcd/wire/v2" + "github.com/btcsuite/btcwallet/walletdb" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/stretchr/testify/mock" +) + +// TxStore is a mock implementation of the wtxmgr.TxStore interface. +type TxStore struct { + mock.Mock +} + +// A compile-time assertion to ensure that TxStore implements the TxStore +// interface. +var _ wtxmgr.TxStore = (*TxStore)(nil) + +// Balance implements the wtxmgr.TxStore interface. +func (m *TxStore) Balance(ns walletdb.ReadBucket, minConf int32, + syncHeight int32) (btcutil.Amount, error) { + + args := m.Called(ns, minConf, syncHeight) + if args.Get(0) == nil { + return btcutil.Amount(0), args.Error(1) + } + + return args.Get(0).(btcutil.Amount), args.Error(1) +} + +// DeleteExpiredLockedOutputs implements the wtxmgr.TxStore interface. +func (m *TxStore) DeleteExpiredLockedOutputs( + ns walletdb.ReadWriteBucket) error { + + args := m.Called(ns) + return args.Error(0) +} + +// InsertTx implements the wtxmgr.TxStore interface. +func (m *TxStore) InsertTx(ns walletdb.ReadWriteBucket, + rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) error { + + args := m.Called(ns, rec, block) + return args.Error(0) +} + +// InsertTxCheckIfExists implements the wtxmgr.TxStore interface. +func (m *TxStore) InsertTxCheckIfExists(ns walletdb.ReadWriteBucket, + rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta) (bool, error) { + + args := m.Called(ns, rec, block) + return args.Bool(0), args.Error(1) +} + +// InsertConfirmedTx implements the wtxmgr.TxStore interface. +func (m *TxStore) InsertConfirmedTx(ns walletdb.ReadWriteBucket, + rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta, + credits []wtxmgr.CreditEntry) error { + + args := m.Called(ns, rec, block, credits) + return args.Error(0) +} + +// InsertUnconfirmedTx implements the wtxmgr.TxStore interface. +func (m *TxStore) InsertUnconfirmedTx(ns walletdb.ReadWriteBucket, + rec *wtxmgr.TxRecord, credits []wtxmgr.CreditEntry) error { + + args := m.Called(ns, rec, credits) + return args.Error(0) +} + +// AddCredit implements the wtxmgr.TxStore interface. +func (m *TxStore) AddCredit(ns walletdb.ReadWriteBucket, + rec *wtxmgr.TxRecord, block *wtxmgr.BlockMeta, index uint32, + change bool) error { + + args := m.Called(ns, rec, block, index, change) + return args.Error(0) +} + +// ListLockedOutputs implements the wtxmgr.TxStore interface. +func (m *TxStore) ListLockedOutputs( + ns walletdb.ReadBucket) ([]*wtxmgr.LockedOutput, error) { + + args := m.Called(ns) + return args.Get(0).([]*wtxmgr.LockedOutput), args.Error(1) +} + +// LockOutput implements the wtxmgr.TxStore interface. +func (m *TxStore) LockOutput(ns walletdb.ReadWriteBucket, id wtxmgr.LockID, + op wire.OutPoint, duration time.Duration) (time.Time, error) { + + args := m.Called(ns, id, op, duration) + if args.Get(0) == nil { + return time.Time{}, args.Error(1) + } + + return args.Get(0).(time.Time), args.Error(1) +} + +// OutputsToWatch implements the wtxmgr.TxStore interface. +func (m *TxStore) OutputsToWatch( + ns walletdb.ReadBucket) ([]wtxmgr.Credit, error) { + + args := m.Called(ns) + return args.Get(0).([]wtxmgr.Credit), args.Error(1) +} + +// PutTxLabel implements the wtxmgr.TxStore interface. +func (m *TxStore) PutTxLabel(ns walletdb.ReadWriteBucket, + txid chainhash.Hash, label string) error { + + args := m.Called(ns, txid, label) + return args.Error(0) +} + +// RangeTransactions implements the wtxmgr.TxStore interface. +func (m *TxStore) RangeTransactions(ns walletdb.ReadBucket, begin, + end int32, f func([]wtxmgr.TxDetails) (bool, error)) error { + + args := m.Called(ns, begin, end, f) + return args.Error(0) +} + +// Rollback implements the wtxmgr.TxStore interface. +func (m *TxStore) Rollback( + ns walletdb.ReadWriteBucket, height int32) error { + + args := m.Called(ns, height) + return args.Error(0) +} + +// TxDetails implements the wtxmgr.TxStore interface. +func (m *TxStore) TxDetails(ns walletdb.ReadBucket, + txHash *chainhash.Hash) (*wtxmgr.TxDetails, error) { + + args := m.Called(ns, txHash) + details, _ := args.Get(0).(*wtxmgr.TxDetails) + + return details, args.Error(1) +} + +// UniqueTxDetails implements the wtxmgr.TxStore interface. +func (m *TxStore) UniqueTxDetails(ns walletdb.ReadBucket, + txHash *chainhash.Hash, + block *wtxmgr.Block) (*wtxmgr.TxDetails, error) { + + args := m.Called(ns, txHash, block) + details, _ := args.Get(0).(*wtxmgr.TxDetails) + + return details, args.Error(1) +} + +// UnlockOutput implements the wtxmgr.TxStore interface. +func (m *TxStore) UnlockOutput(ns walletdb.ReadWriteBucket, + id wtxmgr.LockID, op wire.OutPoint) error { + + args := m.Called(ns, id, op) + return args.Error(0) +} + +// UnspentOutputs implements the wtxmgr.TxStore interface. +func (m *TxStore) UnspentOutputs( + ns walletdb.ReadBucket) ([]wtxmgr.Credit, error) { + + args := m.Called(ns) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).([]wtxmgr.Credit), args.Error(1) +} + +// UnspentOutputsIncludingLocked implements the wtxmgr.TxStore interface. +func (m *TxStore) UnspentOutputsIncludingLocked( + ns walletdb.ReadBucket) ([]wtxmgr.Credit, error) { + + args := m.Called(ns) + + if args.Get(0) == nil { + return nil, args.Error(1) + } + + return args.Get(0).([]wtxmgr.Credit), args.Error(1) +} + +// GetUtxo implements the wtxmgr.TxStore interface. +func (m *TxStore) GetUtxo(ns walletdb.ReadBucket, + outpoint wire.OutPoint) (*wtxmgr.Credit, error) { + + args := m.Called(ns, outpoint) + credit, _ := args.Get(0).(*wtxmgr.Credit) + + return credit, args.Error(1) +} + +// FetchTxLabel implements the wtxmgr.TxStore interface. +func (m *TxStore) FetchTxLabel(ns walletdb.ReadBucket, + txid chainhash.Hash) (string, error) { + + args := m.Called(ns, txid) + return args.String(0), args.Error(1) +} + +// UnminedTxs implements the wtxmgr.TxStore interface. +func (m *TxStore) UnminedTxs( + ns walletdb.ReadBucket) ([]*wire.MsgTx, error) { + + args := m.Called(ns) + return args.Get(0).([]*wire.MsgTx), args.Error(1) +} + +// UnminedTxHashes implements the wtxmgr.TxStore interface. +func (m *TxStore) UnminedTxHashes( + ns walletdb.ReadBucket) ([]*chainhash.Hash, error) { + + args := m.Called(ns) + return args.Get(0).([]*chainhash.Hash), args.Error(1) +} + +// RemoveUnminedTx implements the wtxmgr.TxStore interface. +func (m *TxStore) RemoveUnminedTx(ns walletdb.ReadWriteBucket, + rec *wtxmgr.TxRecord) error { + + args := m.Called(ns, rec) + return args.Error(0) +} diff --git a/bwtest/mock/unsupported_managed_pub_key_addr.go b/bwtest/mock/unsupported_managed_pub_key_addr.go new file mode 100644 index 0000000000..d8a2bc9d62 --- /dev/null +++ b/bwtest/mock/unsupported_managed_pub_key_addr.go @@ -0,0 +1,74 @@ +// Copyright (c) 2026 The btcsuite developers +// Use of this source code is governed by an ISC +// license that can be found in the LICENSE file. + +package mock + +import ( + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil/v2" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/stretchr/testify/mock" +) + +// UnsupportedManagedPubKeyAddr is a mock.Mock-based test double for +// waddrmgr.ManagedPubKeyAddress implementations whose concrete type is not +// supported by wallet metadata adapters. +type UnsupportedManagedPubKeyAddr struct { + mock.Mock + waddrmgr.ManagedAddress +} + +// Imported reports whether the managed pubkey address is imported. +func (m *UnsupportedManagedPubKeyAddr) Imported() bool { + args := m.Called() + return args.Bool(0) +} + +// PubKey returns the public key associated with the managed pubkey address. +func (m *UnsupportedManagedPubKeyAddr) PubKey() *btcec.PublicKey { + args := m.Called() + + pubKey, _ := args.Get(0).(*btcec.PublicKey) + + return pubKey +} + +// ExportPubKey returns the hex-encoded public key. +func (m *UnsupportedManagedPubKeyAddr) ExportPubKey() string { + args := m.Called() + return args.String(0) +} + +// PrivKey returns the private key associated with the managed pubkey address. +func (m *UnsupportedManagedPubKeyAddr) PrivKey() (*btcec.PrivateKey, error) { + args := m.Called() + + privKey, _ := args.Get(0).(*btcec.PrivateKey) + + return privKey, args.Error(1) +} + +// ExportPrivKey returns the wallet import format encoding of the private key. +func (m *UnsupportedManagedPubKeyAddr) ExportPrivKey() (*btcutil.WIF, + error) { + + args := m.Called() + + wif, _ := args.Get(0).(*btcutil.WIF) + + return wif, args.Error(1) +} + +// DerivationInfo returns the BIP-32 derivation path for the managed pubkey +// address. +func (m *UnsupportedManagedPubKeyAddr) DerivationInfo() (waddrmgr.KeyScope, + waddrmgr.DerivationPath, bool) { + + args := m.Called() + + scope, _ := args.Get(0).(waddrmgr.KeyScope) + path, _ := args.Get(1).(waddrmgr.DerivationPath) + + return scope, path, args.Bool(2) +} diff --git a/bwtest/neutrino.go b/bwtest/neutrino.go new file mode 100644 index 0000000000..5a81e24bdc --- /dev/null +++ b/bwtest/neutrino.go @@ -0,0 +1,119 @@ +package bwtest + +import ( + "context" + "fmt" + "os" + "path/filepath" + "testing" + "time" + + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/walletdb" + _ "github.com/btcsuite/btcwallet/walletdb/bdb" // Register bdb walletdb driver. + "github.com/lightninglabs/neutrino" +) + +const neutrinoDBTimeout = 5 * time.Second + +// NeutrinoBackend is a ChainBackend that uses an in-process neutrino chain +// service connected to the shared miner. +type NeutrinoBackend struct { + minerAddr string +} + +// NewNeutrinoBackend creates a new NeutrinoBackend. +// +// Neutrino is an in-process backend and does not write process logs into the +// passed logDir. +func NewNeutrinoBackend(t *testing.T, _ string) *NeutrinoBackend { + t.Helper() + + return &NeutrinoBackend{} +} + +// Name returns the identifier of the backend. +func (n *NeutrinoBackend) Name() string { + return backendNeutrino +} + +// Start is a no-op for neutrino. +func (n *NeutrinoBackend) Start() error { + return nil +} + +// Stop is a no-op for neutrino. +func (n *NeutrinoBackend) Stop() error { + return nil +} + +// ConnectMiner records the miner address for later use. +func (n *NeutrinoBackend) ConnectMiner(minerAddr string) error { + n.minerAddr = minerAddr + return nil +} + +// NewChainClient creates a new neutrino-backed chain.Interface connected to +// the shared miner. +func (n *NeutrinoBackend) NewChainClient(ctx context.Context) (chain.Interface, + func(), error) { + + if n.minerAddr == "" { + return nil, nil, fmt.Errorf("neutrino: %w", errMissingMinerAddr) + } + + dataDir, err := os.MkdirTemp("", "btcwallet-neutrino-") + if err != nil { + return nil, nil, fmt.Errorf("create neutrino temp dir: %w", err) + } + + spvdb, err := walletdb.Create( + "bdb", filepath.Join(dataDir, "neutrino.db"), true, + neutrinoDBTimeout, false, + ) + if err != nil { + _ = os.RemoveAll(dataDir) + return nil, nil, fmt.Errorf("create neutrino db: %w", err) + } + + chainService, err := neutrino.NewChainService(neutrino.Config{ + DataDir: dataDir, + Database: spvdb, + ChainParams: *harnessNetParams, + ConnectPeers: []string{n.minerAddr}, + }) + if err != nil { + _ = spvdb.Close() + _ = os.RemoveAll(dataDir) + + return nil, nil, fmt.Errorf("create neutrino chain service: %w", err) + } + + client := chain.NewNeutrinoClient(harnessNetParams, chainService) + + err = client.Start(ctx) + if err != nil { + _ = spvdb.Close() + _ = os.RemoveAll(dataDir) + + return nil, nil, fmt.Errorf("start neutrino client: %w", err) + } + + cleanup := func() { + client.Stop() + client.WaitForShutdown() + + _ = spvdb.Close() + _ = os.RemoveAll(dataDir) + } + + return client, cleanup, nil +} + +// LogDir returns an empty string because neutrino has no backend daemon. +func (n *NeutrinoBackend) LogDir() string { + // Neutrino runs in-process, so there is no backend daemon log directory. + return "" +} + +var _ ChainBackend = (*NeutrinoBackend)(nil) diff --git a/bwtest/perms.go b/bwtest/perms.go new file mode 100644 index 0000000000..3db2c9ffc7 --- /dev/null +++ b/bwtest/perms.go @@ -0,0 +1,7 @@ +package bwtest + +// logDirPerm is the default permission for harness-managed log directories. +// +// 0o750 keeps logs accessible to the current user/group while avoiding +// world-readable test artifacts that may contain sensitive runtime details. +const logDirPerm = 0o750 diff --git a/bwtest/rlimit_other.go b/bwtest/rlimit_other.go new file mode 100644 index 0000000000..adc8986216 --- /dev/null +++ b/bwtest/rlimit_other.go @@ -0,0 +1,11 @@ +//go:build !(darwin || linux) + +package bwtest + +// raiseNoFileLimit attempts to increase the current process file descriptor +// limit. +// +// On platforms where this isn't supported, this is a no-op. +func raiseNoFileLimit() error { + return nil +} diff --git a/bwtest/rlimit_unix.go b/bwtest/rlimit_unix.go new file mode 100644 index 0000000000..ed0702ffea --- /dev/null +++ b/bwtest/rlimit_unix.go @@ -0,0 +1,83 @@ +//go:build darwin || linux + +package bwtest + +import ( + "fmt" + "syscall" +) + +const ( + // desiredNoFileLimit is the target soft descriptor limit for test runs that + // launch bitcoind. + desiredNoFileLimit = 4096 + + // assumedInfinityThreshold is the cutoff used to treat RLIMIT_NOFILE values + // as effectively infinite and normalize them to a finite limit. + assumedInfinityThreshold = 1 << 60 +) + +// raiseNoFileLimit attempts to increase the current process file descriptor +// limit. +// +// This is a best-effort helper intended for integration tests that launch +// external processes like bitcoind. Some systems have a low default soft limit, +// which can cause bitcoind to fail during startup. +// +// This helper is still needed because bitcoind validates RLIMIT_NOFILE on +// startup. Normalizing extreme/low values keeps CI environments predictable. +func raiseNoFileLimit() error { + var rlim syscall.Rlimit + + err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim) + if err != nil { + return fmt.Errorf("get rlimit: %w", err) + } + + newCur, ok := desiredNoFileCur(rlim) + if !ok { + return nil + } + + rlim.Cur = newCur + + err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim) + if err != nil { + return fmt.Errorf("set rlimit: %w", err) + } + + return nil +} + +// desiredNoFileCur computes the desired RLIMIT_NOFILE soft value and reports +// whether Setrlimit should be called. +func desiredNoFileCur(rlim syscall.Rlimit) (uint64, bool) { + // Some environments report RLIMIT_NOFILE as effectively infinite. Bitcoind + // fails fast if the value cannot be represented correctly. Normalizing this + // to a finite limit avoids startup failures. + if rlim.Cur >= assumedInfinityThreshold { + newCur := uint64(desiredNoFileLimit) + if rlim.Max > 0 && newCur > rlim.Max { + newCur = rlim.Max + } + + return newCur, true + } + + // Nothing to do if we're already above our desired limit. + if rlim.Cur >= desiredNoFileLimit { + return 0, false + } + + // Increase the soft limit, but don't exceed the hard limit. + newCur := uint64(desiredNoFileLimit) + if rlim.Max > 0 && newCur > rlim.Max { + newCur = rlim.Max + } + + if newCur <= rlim.Cur { + return 0, false + } + + return newCur, true +} diff --git a/bwtest/tcp.go b/bwtest/tcp.go new file mode 100644 index 0000000000..76d534e6be --- /dev/null +++ b/bwtest/tcp.go @@ -0,0 +1,41 @@ +package bwtest + +import ( + "context" + "fmt" + "net" + "time" + + "github.com/btcsuite/btcwallet/bwtest/wait" +) + +// waitForTCPListener polls until addr accepts TCP connections. +// +// The integration harness uses this before starting external backends that +// immediately dial the miner. Without the extra readiness check, a backend's +// first outbound peer attempt can race the rpctest miner listener on slower CI +// runners and leave the backend stuck at height 0 until its next reconnect. +func waitForTCPListener(addr string, timeout time.Duration) error { + err := wait.NoError(func() error { + ctx, cancel := context.WithTimeout( + context.Background(), wait.PollInterval, + ) + defer cancel() + + dialer := &net.Dialer{} + + conn, err := dialer.DialContext(ctx, "tcp", addr) + if err != nil { + return fmt.Errorf("dial %s: %w", addr, err) + } + + _ = conn.Close() + + return nil + }, timeout) + if err != nil { + return fmt.Errorf("wait for tcp listener %s: %w", addr, err) + } + + return nil +} diff --git a/bwtest/timeouts.go b/bwtest/timeouts.go new file mode 100644 index 0000000000..6f348a8935 --- /dev/null +++ b/bwtest/timeouts.go @@ -0,0 +1,9 @@ +package bwtest + +import "time" + +const ( + // defaultTestTimeout is a shared default timeout for polling and setup + // steps in integration tests. + defaultTestTimeout = 30 * time.Second +) diff --git a/bwtest/utils.go b/bwtest/utils.go new file mode 100644 index 0000000000..7a5e0485a3 --- /dev/null +++ b/bwtest/utils.go @@ -0,0 +1,50 @@ +package bwtest + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +// GetBtcdBinary returns the path to the btcd binary. +// It checks if "btcd" is in the PATH. +func GetBtcdBinary() (string, error) { + // If specific path is needed, we could check env vars here. + path, err := exec.LookPath("btcd") + if err != nil { + return "", fmt.Errorf("failed to find btcd binary: %w", err) + } + + path, err = filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + + return path, nil +} + +// GetBitcoindBinary returns the path to the bitcoind binary. +func GetBitcoindBinary() (string, error) { + path, err := exec.LookPath("bitcoind") + if err != nil { + return "", fmt.Errorf("failed to find bitcoind binary: %w", err) + } + + path, err = filepath.Abs(path) + if err != nil { + return "", fmt.Errorf("failed to get absolute path: %w", err) + } + + return path, nil +} + +// ensureLogDir creates the log directory if it doesn't exist. +func ensureLogDir(dir string) error { + err := os.MkdirAll(dir, logDirPerm) + if err != nil { + return fmt.Errorf("mkdir log dir: %w", err) + } + + return nil +} diff --git a/bwtest/wait/wait.go b/bwtest/wait/wait.go new file mode 100644 index 0000000000..fd41a5ebae --- /dev/null +++ b/bwtest/wait/wait.go @@ -0,0 +1,58 @@ +// Package wait provides polling helpers for integration tests. +package wait + +import ( + "errors" + "time" +) + +var ( + // ErrNoResponse is returned when f does not return within the timeout. + ErrNoResponse = errors.New("method did not return within the timeout") +) + +// PollInterval is the default polling interval used by NoError. +const PollInterval = 200 * time.Millisecond + +// NoError polls f until it returns nil or the timeout is reached. +// +// If the timeout is reached, the last error returned by f is returned. +func NoError(f func() error, timeout time.Duration) error { + // f is expected to be cheap and non-blocking. This helper is intended for + // polling state (e.g. "is the node ready?") rather than performing a long + // operation. + // + // NOTE: NoError does not interrupt f. If f blocks, NoError may block longer + // than the provided timeout. + + deadline := time.NewTimer(timeout) + defer deadline.Stop() + + ticker := time.NewTicker(PollInterval) + defer ticker.Stop() + + // Call f() immediately to avoid the initial ticker delay. + lastErr := f() + if lastErr == nil { + return nil + } + + for { + select { + case <-deadline.C: + if lastErr == nil { + return ErrNoResponse + } + + return lastErr + + case <-ticker.C: + err := f() + if err == nil { + return nil + } + + lastErr = err + } + } +} diff --git a/bwtest/wallet_logging.go b/bwtest/wallet_logging.go new file mode 100644 index 0000000000..a623644402 --- /dev/null +++ b/bwtest/wallet_logging.go @@ -0,0 +1,78 @@ +package bwtest + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/btcsuite/btcd/rpcclient" + "github.com/btcsuite/btclog" + "github.com/btcsuite/btcwallet/chain" + "github.com/btcsuite/btcwallet/waddrmgr" + "github.com/btcsuite/btcwallet/wallet" + "github.com/btcsuite/btcwallet/wtxmgr" + "github.com/stretchr/testify/require" +) + +// walletLogFilePerm is intentionally more restrictive than logDirPerm because +// wallet logs may contain addresses, txids, and operational details that should +// not be readable by other users on a shared machine. +const walletLogFilePerm = 0o600 + +// setUpWalletLogging configures the btclog-based loggers used by btcwallet to +// write into the provided log file path. +// +// NOTE: This is package-global logger configuration. It should only be used in +// serial integration tests. +func setUpWalletLogging(t *testing.T, logPath string) func() { + t.Helper() + + // #nosec G304 -- logPath is created by the test harness. + f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, + walletLogFilePerm) + require.NoError(t, err, "unable to create wallet log file") + + backend := btclog.NewBackend(f) + + btwl := backend.Logger("BTWL") + amgr := backend.Logger("AMGR") + tmgr := backend.Logger("TMGR") + chio := backend.Logger("CHIO") + rpcl := backend.Logger("RPCC") + + level, _ := btclog.LevelFromString("debug") + btwl.SetLevel(level) + amgr.SetLevel(level) + tmgr.SetLevel(level) + chio.SetLevel(level) + rpcl.SetLevel(level) + + wallet.UseLogger(btwl) + waddrmgr.UseLogger(amgr) + wtxmgr.UseLogger(tmgr) + chain.UseLogger(chio) + rpcclient.UseLogger(rpcl) + + return func() { + _ = f.Sync() + _ = f.Close() + } +} + +// walletLogFileName returns the per-test wallet log filename. +func walletLogFileName(t *testing.T) string { + t.Helper() + + // Use the leaf subtest name to keep filenames short. + name := t.Name() + + parts := strings.Split(name, "/") + if len(parts) > 0 { + name = parts[len(parts)-1] + } + + name = sanitizeLogToken(name) + + return fmt.Sprintf("wallet-%s.log", name) +} diff --git a/chain/pruned_block_dispatcher_test.go b/chain/pruned_block_dispatcher_test.go index d6782d3510..24aea12330 100644 --- a/chain/pruned_block_dispatcher_test.go +++ b/chain/pruned_block_dispatcher_test.go @@ -181,10 +181,18 @@ func (h *prunedBlockDispatcherHarness) stop() { default: } - select { - case <-h.queriedPeer: - h.t.Fatal("did not consume all queriedPeer signals") - default: + // Drain any remaining queriedPeer signals. The exact number of peer + // queries depends on the work manager's internal scheduling (e.g. + // retries, worker redistribution) and is non-deterministic. Tests + // that care about specific query counts already assert them + // explicitly via assertPeerQueried. +drainQueriedPeer: + for { + select { + case <-h.queriedPeer: + default: + break drainQueriedPeer + } } require.Empty(h.t, h.blocksQueried) @@ -271,11 +279,6 @@ func (h *prunedBlockDispatcherHarness) query(blocks []*chainhash.Hash, cancelChan := make(chan error, 1) blockChan, errChan := h.dispatcher.Query(blocks, cancelChan, opts...) - select { - case err := <-errChan: - require.NoError(h.t, err) - default: - } for _, block := range blocks { h.blocksQueried[*block]++ diff --git a/docs/developer/README.md b/docs/developer/README.md index 38ed43f839..4551496f42 100644 --- a/docs/developer/README.md +++ b/docs/developer/README.md @@ -28,6 +28,15 @@ Best practices for writing clear, effective, and maintainable unit tests. --- +## 🧾 Transaction Invalidation Flows + +A focused guide to the wallet tx-store event model for transaction invalidation, +publisher cleanup, and rollback handling. + +**[➡️ Read the Transaction Invalidation Flows Guide](./tx_invalidation_flows.md)** + +--- + ## 🏛️ Engineering Guide A deep dive into the core design philosophy, architectural patterns, and Go implementation details that guide the development of `btcwallet`. @@ -48,4 +57,4 @@ Formal documentation of significant architectural decisions, their context, and A detailed guide to creating Bitcoin transactions using the `PsbtManager` interface, covering various scenarios and best practices. -**[➡️ Read the PSBT Workflows Guide](./psbt_workflows.md)** \ No newline at end of file +**[➡️ Read the PSBT Workflows Guide](./psbt_workflows.md)** diff --git a/docs/developer/adr/0006-wtxmgr-sql-schema.md b/docs/developer/adr/0006-wtxmgr-sql-schema.md new file mode 100644 index 0000000000..f2e4516eee --- /dev/null +++ b/docs/developer/adr/0006-wtxmgr-sql-schema.md @@ -0,0 +1,255 @@ +# ADR 0006: Wallet Transaction Manager SQL Schema + +## 1. Context + +As part of the migration from a Key-Value store to a relational SQL backend, the Wallet Transaction Manager (`wtxmgr`) requires a new schema design. The `wtxmgr` is responsible for tracking: +1. **Transactions:** Immutable blockchain data and user intent. +2. **UTXOs (Credits):** Outputs owned by the wallet (the primary operational unit). +3. **Spends (Debits):** Inputs that spend those outputs. +4. **Metadata:** User labels and transient locks (leases). + +For a detailed theoretical analysis of the data model, see [UTXO Data Model and Lifecycle](../utxo_data_model.md). + +Note on txid uniqueness: Bitcoin has historical txid-duplicate edge cases (primarily coinbase). However, since BIP30, duplicates cannot create concurrently-unspent outpoints. This schema therefore treats `tx_hash` as unique per wallet for balance/coin selection purposes. + +## 2. Decision + +We will adopt a **UTXO-Centered, Soft-Deletion Schema**. + +### 2.1 Core Principles +1. **UTXO-Centered Operations:** The schema is optimized for `Balance()` and `CoinSelection()` queries, which focus on the `utxos` table. +2. **Transaction-Centered Integrity:** Validity flows from Parent to Child. A UTXO is only valid if its parent Transaction is valid (`tx_status = 1`, `published`) or is explicitly allowed for chaining (`tx_status = 0`, `pending`). +3. **Immutable History (Soft Deletion):** We **NEVER** automatically `DELETE` rows. + * Failed/RBF'd transactions are marked with a `tx_status` field (e.g., `replaced`, `failed`). + * They remain in the database for audit history but are excluded from balance queries. + * Foreign Keys use `ON DELETE RESTRICT` for creation relationships to prevent accidental data loss. +4. **Wallet-Scoped Rows:** All `wtxmgr` tables are scoped by `wallet_id` to support multiple wallets sharing a single database without row-level conflicts. + +### 2.1.1 Wallet-Scoped vs Global Transactions +Two designs were considered: + +* **Wallet-scoped tables (chosen):** + * Pros: Simple schema; no join tables; queries stay local to a wallet; avoids cross-wallet coordination. + * Cons: Duplicates transaction rows across wallets that observe the same global tx. +* **Global transactions table (alternative):** + * Pros: Storage efficiency; one canonical row per `tx_hash`. + * Cons: Requires additional mapping tables (e.g., `wallet_transactions`) for per-wallet ownership/metadata; more complex queries and constraints; more careful concurrency semantics. + +This ADR chooses wallet-scoped tables for simplicity and implementability. A future migration to a global transactions table is possible but is considered a separate architectural decision. + +### 2.1.2 Explicit Status vs. Derived Status +Two designs were considered for tracking transaction validity: + +* **Explicit `tx_status` column (chosen):** + * The `tx_status` field is a pre-computed materialization of the transaction's validity state, set atomically at write time when the invalidating event occurs (RBF, double-spend, reorg). + * It is stored as a compact numeric code (`0 = pending`, `1 = published`, `2 = replaced`, `3 = failed`, `4 = orphaned`) so hot-path predicates and indexes do not pay the storage or comparison cost of repeated status strings. + * The schema intentionally does **not** use a separate status lookup table. The status set is tiny, closed, and application-owned, so a reference table would add foreign-key and seed-data complexity without adding meaningful flexibility. Keeping the code inline on the transaction row preserves hot-path simplicity, while the Go enum layer provides the human-readable names. + * Pros: Balance and coin-selection queries filter on a single status predicate (`tx_status IN (1, 0)`) with no joins (and can be indexed if profiling justifies it); cascading invalidation is performed once at write time and never re-derived; audit states (`failed`, `replaced`, `orphaned`) are directly queryable. + * Cons: Introduces a field that could theoretically drift from the underlying facts. This is partially mitigated by `CHECK` constraints (`check_confirmed_published`, `check_coinbase_confirmation_state`) and coinbase reorg triggers; transition correctness for `failed`/`replaced` remains a write-path responsibility validated by tests. + +* **Derived status from other columns (alternative):** + * At coarse granularity (pending vs active vs invalid), some states are partly derivable: `orphaned` = `is_coinbase AND block_height IS NULL`; direct `replaced` victims can be identified from `tx_replacements.replaced_tx_id`. Direct `failed` victims are not derivable from replacement edges alone because upstream invalidation intentionally records no replacement edge. + * However, a full replacement requires preserving the distinct invalid states (`replaced`, `failed`, `orphaned`), which have different operational semantics: RBF (`replaced`) allows re-spending the same inputs with a new tx; `orphaned` coinbase can recover on reconfirmation; `failed` (double-spend) is permanent. Collapsing these into a single boolean loses information needed for recovery logic and user-facing audit. + * A boolean decomposition (e.g., `is_broadcast` + `is_invalid`) also introduces problems: `is_broadcast` does not equal `published` — a tx can be valid/published because it was received from the network, not broadcast by this wallet; boolean pairs create ambiguous combinations (e.g., `is_broadcast=false, is_invalid=true`) requiring additional constraints. + * A fully-derived alternative that preserves the same information would require at least three columns (`is_broadcast`/source marker, `invalidation_reason` enum, optional `invalidated_by_tx_id`) — strictly more schema surface than a single `tx_status` column. + * Cascading invalidation adds further complexity: downstream txs whose parent became invalid were never directly "replaced." Without a pre-computed status, every balance/coin-selection query would need a recursive CTE walking up the ancestor chain — O(depth) per tx on the hot path. + +This ADR chooses explicit status as the minimal representation that captures all lifecycle states without ambiguity. The `tx_status` column is a pre-computed materialization set once at write time; database constraints/triggers enforce key invariants (confirmation and coinbase semantics), while write-path logic is responsible for setting `failed`/`replaced` transitions correctly. + +### 2.2 Consistency & Concurrency Model +This design assumes standard SQL ACID guarantees. + +* **Atomic updates:** Reorg disconnects, status transitions (RBF/failure/orphaning), and lease acquisition MUST be performed using explicit SQL transactions. +* **Leases are the concurrency primitive:** `utxo_leases` provides an application-level lock that prevents concurrent coin selection from choosing the same UTXO. +* **Isolation:** Wallet implementations SHOULD treat coin selection + lease acquisition as a single atomic unit. If multiple processes share one database, they must rely on database-enforced constraints (primary keys/uniques) and transactional semantics rather than best-effort in-memory coordination. + +Recommended operational defaults: +* Prefer running write paths under `SERIALIZABLE` and retry on serialization failures. +* If using weaker isolation (e.g., `READ COMMITTED`), rely on unique constraints and single-statement lease acquisition (no read-then-write without conflict handling). + +### 2.3 Reorg & RBF Strategy +* **Reorgs:** The `blocks` table represents the current best chain. If a block is disconnected, any referencing transaction will have `block_height` set to `NULL` via `ON DELETE SET NULL`. + * **Effect (non-coinbase):** Regular transactions become unconfirmed (but remain `published`). + * **Coinbase special case:** Coinbase transactions from the disconnected block are marked `orphaned` (they cannot exist outside the block that created them). + * **Atomicity requirement:** The coinbase status update MUST occur atomically with the disconnect. + * PostgreSQL evaluates `CHECK` constraints immediately, including updates performed by foreign-key actions such as `ON DELETE SET NULL`. + * If you enforce the coinbase invariant at the database level, a simple `DELETE FROM blocks ...` will fail unless the coinbase row's `tx_status` is updated to `orphaned` as part of the same statement. + * Recommended: use a trigger to rewrite coinbase `tx_status` during the `block_height -> NULL` update (see 3.5). + * **Reconfirmation:** If an orphaned coinbase transaction re-enters the best chain, restoring it requires setting `block_height` and `tx_status = 1` (`published`) atomically. +* **RBF:** Handled by updating the `utxos.spent_by_tx_id` pointer to the new transaction and marking the old transaction as `replaced`. + +### 2.4 Implementation Notes + +This ADR includes a reference schema. The implementation keeps the same +invariants, but makes a few deliberate schema choices to match the existing +conventions in `wallet/internal/db/migrations/`. + +**Primary keys and wallet scoping** + +The reference schema uses composite primary keys (`(wallet_id, id)`) to make +wallet scoping enforceable with foreign keys. + +In this repository, SQLite tables follow the rowid-backed +`INTEGER PRIMARY KEY` pattern used by the existing wallet/account/address +schema. To preserve the wallet-scoping invariant while keeping that +convention, the implementation uses single-column primary keys on `id` and +adds `UNIQUE(wallet_id, id)` constraints on wallet-scoped tables. Child tables +then use composite foreign keys referencing `(wallet_id, id)`. + +**Manual pruning and `spent_by` semantics** + +The reference schema uses `ON DELETE SET NULL` for the +`(wallet_id, spent_by_tx_id)` foreign key so that physically deleting a +spending transaction can restore a UTXO to the unspent set. + +In a composite foreign key, `ON DELETE SET NULL` applies to *all* referencing +columns. With `utxos.wallet_id` being `NOT NULL`, the reference behavior cannot +be expressed directly as written. + +The implementation therefore uses `ON DELETE RESTRICT` for the spender foreign +key and defines manual pruning as an explicit, application-driven operation +that clears `utxos.spent_by_*` before deleting/pruning the spending +transaction, all within a single SQL transaction. + +**SQLite coinbase disconnect handling** + +PostgreSQL can rely on the transaction-row trigger alone when a block delete +causes `transactions.block_height` to become `NULL`. + +SQLite evaluates child-row checks before an `AFTER UPDATE` trigger on the child +table can normalize the row. To preserve the coinbase orphaning invariant, the +implementation adds a `BEFORE DELETE ON blocks` trigger that rewrites affected +transactions into their final disconnected state before the block row is +removed. + +**Transaction labels** + +User-facing labels are part of the internal store contract (`TxInfo.Label`). +The implementation stores them inline as `transactions.tx_label` instead of a +separate labels table to keep the hot read path simple. + +**Timestamps** + +Both PostgreSQL and SQLite now follow the same timestamp contract: + +- all persisted timestamps represent UTC instants +- PostgreSQL stores them as `TIMESTAMP` +- SQLite stores them as `TIMESTAMP`/`DATETIME` +- logic-sensitive comparisons (for example lease expiry) use caller-supplied UTC + values instead of relying on session-local database time semantics + +## 3. Implemented Schema Notes + +The SQL migrations under `wallet/internal/db/migrations/postgres` and +`wallet/internal/db/migrations/sqlite` are the source of truth for the concrete +schema. This section captures the important design decisions without duplicating +full reference DDL that can drift from the live migrations. + +### 3.1 Transactions + +- `transactions` remains wallet-scoped through `wallet_id` +- `tx_status` stores the wallet-relative validity state inline as a small, + closed numeric enum +- `tx_label` stores user-facing labels inline and keeps the hot read path simple +- `raw_tx` is retained because the schema does not store a fully normalized + input/output graph; callers still need to reconstruct `wire.MsgTx` for + transaction reads and dependency walks +- `UNIQUE (wallet_id, id)` remains so wallet-scoped child relations can use a + composite foreign-key target where needed + +### 3.2 UTXOs + +- `utxos.wallet_id` is intentionally not stored +- wallet ownership is derived from the creating transaction: + `utxos.tx_id -> transactions.wallet_id` +- the owning address is still recorded through `address_id` +- correctness requires the wallet derived from `tx_id` and the wallet derived + from `address_id` to match +- PostgreSQL and SQLite both enforce this invariant with triggers on `utxos` +- same-wallet spend edges are also enforced by trigger when `spent_by_tx_id` is + present + +This keeps the UTXO row normalized while preserving the important wallet-scoped +integrity checks. + +### 3.3 Replacement edges + +- `tx_replacements` remains wallet-scoped +- both endpoints reference wallet-scoped transaction rows through + `(wallet_id, id)` +- `created_at` is stored as a UTC `TIMESTAMP` + +### 3.4 UTXO leases + +- `utxo_leases` keeps `wallet_id` as a query helper for wallet-scoped lease + scans and cleanup +- the row is keyed by `utxo_id`, so one UTXO can have at most one lease row +- wallet consistency between `utxo_leases.wallet_id` and the leased UTXO is + enforced by trigger +- `expires_at` is stored as a UTC `TIMESTAMP` +- lease acquisition, renewal, and cleanup compare against explicit UTC values + supplied by the caller + +### 3.5 Triggers + +The implementation relies on triggers for the invariants that cannot be fully +expressed through ordinary foreign keys alone: + +- coinbase disconnect/orphan handling on `transactions` +- `wallet(tx_id) == wallet(address_id)` on `utxos` +- same-wallet `spent_by_tx_id` on `utxos` +- lease wallet consistency on `utxo_leases` + +This keeps the database responsible for protecting the critical wallet-scoping +rules rather than assuming the application layer is always correct. + +## 4. Consequences + +### 4.1. True Multi-Wallet Support +The transaction graph remains wallet-scoped, and every wallet-owned row is +either directly keyed by `wallet_id` or derives its wallet ownership through a +wallet-scoped parent transaction. This allows multiple wallets to share the +same database without conflicting unique constraints while still permitting the +same network outpoint or tx hash to appear in multiple wallets independently. + +Note: A separate, truly global `transactions` table shared across wallets is a different design. That approach would require a join table (e.g., `wallet_transactions`) to track per-wallet ownership and metadata. + +### 4.2. Native SQL Efficiency +Balances are calculated using `SUM(amount)` over the normalized `utxos` table +with transaction/account joins and database-side filtering, leveraging native +database optimizations. + +The schema intentionally stops short of declaring one canonical "spendable +balance" API because callers may disagree about pending chaining, lease +exclusion, confirmation thresholds, or coinbase maturity rules. + +### 4.3. Audit Trail +By using soft-deletion style transaction states (`tx_status = 2`, `replaced`), +we maintain a complete history of user attempts, even those that failed. This +is superior to previous designs that physically deleted failed transactions. + +### 4.4. Complexity Trade-off +We accept slightly more complexity in **Transaction Reconstruction** (joining inputs/outputs) in exchange for maximal performance in **Balance Calculation** and **Coin Selection**, which are the high-frequency operations. + +Additional operational consequences: +* **Pending-chaining is advanced:** Zero-latency chaining should remain an + application-level choice. Callers that opt into `pending` parents take on + the operational risk that child transactions depend on parents being + broadcast successfully. +* **Recursive invalidation is unbounded in theory:** Marking downstream transactions `failed` after an upstream double-spend can require recursive graph traversal. Implementations should assume bounded typical depth but plan for worst-case behavior (e.g., set a maximum recursion depth or iteration limit). + +### 4.5 Operational Notes (Out of Scope for This ADR) +This ADR defines the target schema and invariants. Production operations still require explicit policies and tooling: + +* **Pruning:** Define criteria for manual pruning (what can be deleted, and how to preserve audit semantics). +* **Migration:** Define how to migrate from the existing key-value store (offline conversion, incremental migration, rollback plan). +* **Backup/Restore:** Large immutable histories impact backup size and restore time; restores may require revalidation of unconfirmed transactions. + * After restore, the wallet should re-fetch the current mempool and re-evaluate RBF/double-spend status for unconfirmed transactions. +* **Schema evolution:** Future changes should be delivered via versioned migrations; preserve immutable-history guarantees when introducing new columns or constraints. + +Monitoring note: +* Track table growth, count of active leases, and lease cleanup latency. These are important for long-running nodes and multi-process deployments. + +## 5. Status + +Accepted. diff --git a/docs/developer/adr/0007-xchacha20-poly1305-encryption.md b/docs/developer/adr/0007-xchacha20-poly1305-encryption.md new file mode 100644 index 0000000000..3e64dcf051 --- /dev/null +++ b/docs/developer/adr/0007-xchacha20-poly1305-encryption.md @@ -0,0 +1,99 @@ +# ADR 0007: XChaCha20-Poly1305 Encryption + +## 1. Context + +As part of the broader effort to simplify wallet encryption to a single +passphrase model, we are revisiting the cryptographic primitives used to +protect sensitive data. Today, `btcwallet` relies on NaCl secretbox, based on +XSalsa20-Poly1305, to encrypt private key material. + +While XSalsa20-Poly1305 remains secure, XChaCha20-Poly1305 is now the +preferred successor in modern systems. It preserves the large nonce model +while offering better library support and alignment with current AEAD +standards. + +Key motivations for this change include: + +* Broad adoption of XChaCha20-Poly1305 in modern cryptographic systems +* Strong performance on systems without AES-NI, including mobile and embedded + environments +* Simpler and more uniform AEAD construction, reducing implementation risk +* Easier auditing due to reduced complexity +* Production-proven usage in the Lightning Network's encrypted transport + protocol ([BOLT + 8](https://github.com/lightning/bolts/blob/master/08-transport.md)), + demonstrating its suitability for Bitcoin-related projects + +Like XSalsa20, XChaCha20-Poly1305 uses a 192-bit nonce, which allows random +nonce generation without practical collision risk and matches our existing +nonce strategy. + +```mermaid +flowchart TD + A[User Passphrase] --> B[scrypt KDF] + B --> C[Master Key] + C --> D[XChaCha20-Poly1305] + D --> E[Encrypted Private Data] +``` + +## 2. Decision + +We will migrate the encryption primitive from NaCl secretbox, based on +XSalsa20-Poly1305, to **XChaCha20-Poly1305**. + +### Key aspects + +1. **Encryption primitive** + Use XChaCha20-Poly1305 from `golang.org/x/crypto/chacha20poly1305`, + specifically the XChaCha variant with 192-bit nonces. + +2. **Key hierarchy** + Preserve the existing structure: scrypt-based KDF, master key derivation, + and per-use encryption keys. + +3. **Data separation** + Continue storing public data in plaintext while encrypting private or + sensitive material only. + +4. **Migration path** + Encrypted data will be re-encrypted during the SQL migration process. + +## 3. Consequences + +### Pros + +* **Performance** + Strong performance in pure software environments, especially where AES-NI + is unavailable. + +* **Modern AEAD** + Aligns the codebase with current best practice for authenticated encryption. + +* **Lower implementation risk** + Fewer edge cases compared to AES-GCM or custom constructions. + +* **Auditability** + Smaller and clearer code paths improve reviewability and long-term + maintenance. + +* **Nonce safety** + The 192-bit nonce space allows safe random nonce generation and preserves + compatibility with existing designs. + +### Cons + +* **Migration cost** + All encrypted data must be re-encrypted during migration. + +* **Temporary dual support** + XSalsa20-Poly1305 must remain supported until migration is complete. + +* **Testing overhead** + Both encryption paths require validation during the transition period. + +* **Passphrase dependency** + Re-encryption is only possible when the user passphrase is available. + +## 4. Status + +Proposed. diff --git a/docs/developer/adr/0008-integration-test-framework.md b/docs/developer/adr/0008-integration-test-framework.md new file mode 100644 index 0000000000..515854d2e4 --- /dev/null +++ b/docs/developer/adr/0008-integration-test-framework.md @@ -0,0 +1,121 @@ +# ADR 0008: Integration Test Framework + +## 1. Context + +The `btcwallet` project requires a robust integration testing framework to verify the correctness of its core `wallet` package against various configurations. The current testing landscape is insufficient for verifying the complex matrix of supported backends and modes. + +There is a requirement to support: +1. **Multiple Chain Backends**: `btcd` (native), `bitcoind` (external process), and `neutrino` (SPV). +2. **Multiple Database Backends**: `kvdb` (bbolt/etcd), `sqlite`, and `postgres`. +3. **Public API Testing**: Verifying the public methods of the `wallet` package (e.g., `Create`, `Load`, `Start`, `Stop`). + +We need a standardized, reusable approach to write integration tests that can run across all these permutations without duplicating setup logic. + +## 2. Decision + +We will implement a modular integration test framework modeled after `lnd`'s `lntest`, adapted for library-mode testing. + +### 2.1. Library-Mode Testing +Tests will run `btcwallet` in **Library Mode** (in-process). +- **Why**: Faster execution, easier debugging, and direct internal state assertion. +- **How**: The test harness instantiates `wallet.Manager` directly. + +### 2.2. Architecture & Components + +The framework is split into `bwtest` (framework) and `itest` (test cases). + +#### Component Interaction + +```mermaid +graph TD + subgraph "Test Execution (itest)" + Test["Test Function (t.Run)"] + Harness["HarnessTest (Instance)"] + end + + subgraph "Infrastructure (bwtest)" + Miner["Miner (btcd)"] + ChainNode["Chain Node Process"] + DB["Database Backend"] + end + + subgraph "Application Under Test" + CB["ChainBackend (Interface Wrapper)"] + W["Wallet (In-Memory)"] + end + + Test -->|Creates & Owns| Harness + Harness -->|Starts| Miner + Harness -->|Starts| ChainNode + Harness -->|Initializes| DB + Harness -->|Configures| CB + Harness -->|Creates| W + + ChainNode -->|"P2P Sync"| Miner + CB -->|"RPC / P2P"| ChainNode + W -->|"Uses (chain.Interface)"| CB + W -->|"Stores Data"| DB +``` + +* **`Miner`**: A dedicated `btcd` instance responsible for generating blocks. It acts as the source of truth for the blockchain state. +* **`Chain Node`**: The software providing chain data (`btcd`, `bitcoind`). + * For `btcd` tests: A separate `btcd` process is started as the Chain Node and connects to the Miner. + * For `bitcoind` tests: A separate `bitcoind` process connects (P2P) to the Miner to sync. + * For `neutrino` tests: There is no separate "Chain Node" process; Neutrino connects directly to the Miner. +* **`ChainBackend`**: The interface wrapper (`rpcclient` or `neutrino.ChainService`) used by the Wallet to communicate with the Chain Node. +* **`Database Backend`**: The storage layer (`kvdb`, `postgres`, `sqlite`). +* **`HarnessTest`**: The specific test instance. It manages unique ports, temp directories, and process lifecycles. + +### 2.3. Package Structure + +- **`bwtest/`**: + - `harness.go`: `HarnessTest` orchestrator. Manages `t.Cleanup` for resource teardown. + - `chain_backend.go`: Logic to start/stop `btcd`/`bitcoind` processes and configure `neutrino`. + - `miner.go`: `Miner` implementation (controls the primary `btcd` instance). + - `database.go`: Helpers to setup/teardown DBs (create temp bolt files, init SQL schemas). +- **`itest/`**: + - `main_test.go`: Flag parsing (`-chain`, `-db`) and global test runner. + - `manager_test.go`: Test cases for `wallet.Manager`. + +### 2.4. Configuration & Isolation + +Configuration is handled via `go test` flags. + +```bash +# Default (btcd + kvdb) +make itest + +# Explicit configuration +make itest chain=bitcoind db=postgres + +# Run specific test case +make itest case=TestNewWallet +``` + +* **Sequential Execution**: Tests run sequentially to avoid resource exhaustion and port conflicts, given the heavy overhead of spinning up multiple full nodes per test. +* **Neutrino Support**: The `Miner` (btcd) will be configured with `--cfilters` to serve compact block filters, allowing Neutrino clients to connect directly to it for SPV synchronization. + +## 3. Implementation Plan + +1. **Scaffold Framework (`bwtest`)**: + - Implement `Miner` (wraps `rpctest`). + - Implement `ChainBackend` logic (start bitcoind/btcd process, setup neutrino). + - Implement `Database` setup (init postgres schema, temp sqlite files). + - Implement `HarnessTest` to orchestrate the dependency graph: + `Miner -> ChainNode -> ChainBackend -> DB -> Wallet`. +2. **Scaffold Tests (`itest`)**: + - Create `manager_test.go` as a proof-of-concept. +3. **CI Integration**: + - Update `Makefile` with `itest` targets mapping flags correctly. + +## 4. Consequences + +### Pros +- **Consistency**: Clear separation between Network, Infrastructure, and Application. +- **Isolation**: Per-test harnesses prevent state leaks. +- **Coverage**: capable of validating the entire support matrix. + +### Cons +- **Resource Intensity**: Running a separate `bitcoind`/`btcd` process per test is CPU/RAM intensive. +- **Complexity**: Dynamic port allocation and process lifecycle management are error-prone. +- **Execution Time**: Tests will take longer to run due to sequential execution and process startup costs. diff --git a/docs/developer/adr/0009-single-passphrase-encryption.md b/docs/developer/adr/0009-single-passphrase-encryption.md new file mode 100644 index 0000000000..4bc2431205 --- /dev/null +++ b/docs/developer/adr/0009-single-passphrase-encryption.md @@ -0,0 +1,75 @@ +# ADR 0009: Single-Passphrase Encryption Model + +## 1. Context + +`btcwallet` is migrating from a key-value database (kvdb) to a SQL-based +backend. This transition requires a clean, consistent encryption model that +works across SQLite and PostgreSQL. + +The legacy system used a dual-passphrase hierarchy with separate public and +private encryption keys. In practice, downstream systems often set the public +passphrase to a known constant to allow auto-start, which negated the privacy +benefits while keeping the complexity. This model also diverges from the +industry standard set by Bitcoin Core and libraries like BDK. + +The design notes that guided this change are captured in +[Single-Passphrase Encryption Design Notes](https://gist.github.com/yyforyongyu/edfeee0f84cf79851735bcc3e740a871). + +## 2. Decision + +We will adopt a **Single-Passphrase Model** that encrypts **private data only** +and leaves public data in plaintext. + +```mermaid +flowchart TD + A[User Passphrase] --> B[scrypt KDF] + B --> C[Master Key] + C --> D[Encrypt Private Data] + E[Public Data] --> F[(Database)] + D --> F +``` + +### Key points + +1. **One master encryption key** derived from the user passphrase +2. **Private data encrypted** (private keys, HD seeds, xprivs, scripts) +3. **Public data plaintext** (addresses, pubkeys, transactions, balances) +4. **Simplified hierarchy** by removing `CryptoPubKey` and `CryptoScriptKey` + +## 3. Consequences + +### Pros + +* **Simplicity**: Eliminates dual passphrase logic. +* **Performance**: Read-only operations no longer require decrypting public + data, reducing query overhead. +* **Interoperability**: Aligns with Bitcoin Core and BDK conventions. +* **UX clarity**: Users only need a single passphrase. + +### Cons + +* **Privacy at rest**: Transaction history, balances, and public keys are + exposed if the database is compromised, consistent with Bitcoin Core tradeoffs. + If stronger protection is required, users can rely on full disk encryption such + as LUKS. +* **Migration effort**: Existing wallets must be migrated away from the legacy + dual passphrase model. This work will be done along with the SQL backend migration. + +## 4. Threat Model and Security Boundaries + +This model protects private key material at rest when an attacker can read the +database but cannot guess the wallet passphrase. + +This model does not protect metadata privacy if the database contents are +exfiltrated. Addresses, balances, and transaction history remain visible by +design. + +Operational mitigations required for this model: + +* Full disk encryption for database files. +* Host hardening (least privilege, patching, endpoint protection). +* Encrypted backups with independent access controls and key management. + +## 5. Status + +Accepted. diff --git a/docs/developer/adr/0010-keyvault-encryption-layer.md b/docs/developer/adr/0010-keyvault-encryption-layer.md new file mode 100644 index 0000000000..f252575c6e --- /dev/null +++ b/docs/developer/adr/0010-keyvault-encryption-layer.md @@ -0,0 +1,187 @@ +# ADR 0010: Keyvault Encryption Layer + +## 1. Context + +The encryption model defined in ADR 0009 and the cryptographic primitive +migration proposed in ADR 0007 require a clear boundary between wallet domain +logic and database persistence. + +The legacy `waddrmgr` design couples storage, locking, key derivation, and +encryption. This makes the SQL migration harder, spreads encryption behavior +across persistence code, and makes lock state difficult to test in isolation. + +We need a dedicated component that owns lock state, key derivation, secret +material lifetime, and encryption. The database layer should remain encryption +agnostic and store encrypted key material as opaque bytes. + +The `db.Store` remains available to other wallet code for non-cryptographic +queries and updates. + +## 2. Decision + +We will introduce a dedicated `wallet/internal/keyvault` package. This package +defines the encryption boundary between wallet domain code and the store layer. + +A wallet facing `keyvault.Vault` is scoped to exactly one wallet at construction +time. It receives a `db.Store` and a wallet identifier during wiring, then keeps +persistence routing internal. Callers that hold a vault do not pass a wallet ID +to each lock, unlock, encrypt, decrypt, or derivation operation. + +Keyvault accesses persistence through `db.Store`. It does not talk directly to +SQL backends. + +```mermaid +flowchart TD + wallet[Wallet] + + subgraph support[Supporting libraries] + hd[btcutil/hdkeychain
BIP32 and BIP44 derivation] + crypto[AEAD crypto
current and migrated primitives] + end + + subgraph vault_boundary[keyvault boundary] + vault[keyvault.Vault
wallet scoped lock state
key lifecycle
secret material API] + cache[(In memory secret cache
unlocked keys
derived keys)] + end + + subgraph store_boundary[db.Store boundary] + store[db.Store
multi wallet persistence API] + + subgraph backends[Persistence backends] + sql[(SQL backend
rows scoped by wallet_id)] + kvdb[(kvdb backend
legacy storage)] + end + end + + wallet -->|owns wallet scoped vault| vault + wallet -->|uses store for non cryptographic data| store + vault -->|persists encrypted key material| store + vault -->|loads encrypted key material| store + vault -->|keeps unlocked material| cache + vault -->|derives HD keys| hd + vault -->|encrypts and decrypts secrets| crypto + store -->|routes by wallet_id| sql + store -->|adapts legacy layout| kvdb +``` + +This is a structural boundary diagram, not a runtime call sequence. + +The `Wallet` struct holds two related dependencies: + +1. A wallet scoped `keyvault.Vault` +2. A multi wallet `db.Store` + +Encrypted key material flows through the vault. Non-cryptographic wallet data +may still flow directly through the store. + +The store remains the persistence boundary for wallet data and keeps wallet ID +routing inside the store or adapter layer. The vault hides that routing from +wallet facing lock, unlock, encryption, decryption, and derivation APIs. + +### Responsibilities + +1. **Own lock state and key lifecycle** + Keyvault manages unlock state, key material lifetime, auto lock behavior, and + secure memory zeroing. + +2. **Expose typed domain interfaces** + Keyvault returns domain types such as `*btcec.PrivateKey`, + `*btcec.PublicKey`, and `btcutil.Address` instead of exposing encrypted byte + slices to wallet code. + +3. **Handle HD derivation** + Keyvault uses `btcutil/hdkeychain` for BIP32 and BIP44 derivation and returns + or persists derived key material as needed. + +4. **Maintain an in memory secret cache** + Keyvault may cache account level keys and derived keys while unlocked to + avoid repeated derivation and database reads. + +5. **Keep wallet facing APIs scoped to one wallet** + A wallet facing `keyvault.Vault` is configured for one wallet during + construction. Callers do not pass a wallet ID to every vault method. + +6. **Track current and planned cryptographic primitives** + Keyvault follows the single passphrase model accepted in ADR 0009 and adopts + the ADR 0007 primitive migration once implemented. + +7. **Coexist with `waddrmgr` during migration** + New code uses keyvault while legacy code continues to rely on `waddrmgr` + until the migration is complete. + +## 3. Consequences + +The wallet facing `keyvault.Vault` API intentionally does not expose wallet ID +parameters on methods such as `Unlock`, `Lock`, `IsLocked`, `Encrypt`, +`Decrypt`, or key derivation methods. + +Code that holds a vault already holds the vault selected for that wallet. +Requiring every method call to pass a wallet ID would push database routing into +controllers, accounts, addresses, and tests. It would also make cross wallet +mistakes possible at every call site. + +The SQL and store layers remain multi wallet aware through `wallet_id` fields +and parameters, consistent with ADR 0001. That routing is handled inside store +implementations or keyvault adapters, not repeated throughout wallet domain +code. + +### Pros + +1. **Separation of concerns** + Database code stores opaque encrypted bytes without knowing about + cryptography, lock state, or key derivation. + +2. **Type safety** + Wallet code works with typed keys and addresses instead of raw encrypted + blobs. + +3. **Centralized lock management** + Lock state, unlock timers, cache lifetime, and secret zeroing are owned by + one component. + +4. **Extensible responsibility boundary** + Keyvault centralizes secret and key responsibilities, making it easier to + add future responsibilities behind the same boundary without spreading + changes across wallet callers. + +5. **Lower call site complexity** + Wallet, account, address, and controller code can use a vault without + threading wallet IDs through every encryption or key access operation. + +6. **Better testability** + Keyvault can be tested against mock stores, and wallet domain code can be + tested against mock vault implementations. + +7. **Per wallet isolation** + Each wallet has its own vault instance, lock state, cache, timers, and secret + material lifetime. + +8. **Migration support** + Keyvault can coexist with `waddrmgr`, allowing new SQL backed paths to move + behind the new boundary before legacy paths are removed. + +### Cons + +1. **Additional abstraction** + The design introduces a new package boundary that must be maintained. + +2. **Migration cost** + Existing code paths must be refactored to use the new keyvault API. + +3. **Temporary dual systems** + `waddrmgr` and keyvault will coexist during the migration, increasing + temporary complexity. + +4. **Boundary discipline** + Implementations must keep wallet ID routing inside constructors, adapters, or + store methods. Exposing wallet ID on every vault method is rejected. + +5. **Constructor and adapter indirection** + Wiring a vault from `db.Store` plus wallet ID adds an adapter boundary + between wallet domain code and persistence. That boundary is intentional, but + future changes must keep it aligned with store methods that remain multi + wallet aware. + +## 4. Status + +Accepted. diff --git a/docs/developer/adr/0011-no-addresses-used-column.md b/docs/developer/adr/0011-no-addresses-used-column.md new file mode 100644 index 0000000000..fa02e1827b --- /dev/null +++ b/docs/developer/adr/0011-no-addresses-used-column.md @@ -0,0 +1,84 @@ +# ADR 0011: No `used` Column on the Addresses Table + +## 1. Context + +The wallet needs to answer whether an address has appeared in any transaction +the wallet has seen. This is a monotonic property used by unused-address scans +to avoid re-offering a previously published address. + +The SQL transaction schema keeps observed wallet history as the source of +truth. A transaction that is disconnected from the best chain remains in the +`transactions` table with updated status and block metadata, and `utxos` rows +reference their creating transaction with restrictive foreign keys. The wallet +does not physically remove these rows during normal reorg, replace, or orphan +handling. + +Because observed credits remain represented in SQL history, address used-ness +can be derived from transaction state: + +```sql +SELECT EXISTS(SELECT 1 FROM utxos WHERE address_id = ?) +``` + +Storing a second `addresses.used` flag would duplicate the same fact and create +drift risk between address metadata and wallet history. + +### Scope + +The SQL `is_used` projection is monotonic for non-abandoned wallet history. +Explicit abandon/delete flows intentionally remove the abandoned transaction +state, matching the rest of the wallet's abandon semantics: balances revert and +the corresponding change indices may become reusable. + +## 2. Decision + +The SQL backends (`pg` and `sqlite`) do not persist a `used` column on the +`addresses` table. + +- SQL address-read queries project `db.AddressInfo.IsUsed` from `EXISTS` over + `utxos`. +- The Store interface intentionally has no SQL `MarkAddressUsed` method: + recording an observed wallet transaction inserts the `utxos` row that future + address reads consult via the `EXISTS` projection. +- The kvdb backend continues to populate `IsUsed` from waddrmgr's sticky used + bit, because legacy rollback handling does not provide the same durable SQL + history table. + +The `db.AddressInfo.IsUsed` contract remains backend-neutral. Callers see the +same logical wallet property even though SQL derives it from wallet history and +kvdb reads it from legacy address metadata. + +## 3. Consequences + +### Pros + +- SQL has one source of truth for observed address usage. +- Address metadata avoids an extra column, migration, trigger, and write path. +- Wallet code can continue to call the Store contract without knowing how each + backend materializes `IsUsed`. + +### Cons + +- SQL address reads pay an `EXISTS` lookup against `utxos`. The + `idx_utxos_by_address` index bounds that cost for single-address reads and + address-list scans. +- The SQL and kvdb adapters implement the same contract differently. Store + method comments and schema comments should point to this ADR so the asymmetry + remains intentional and discoverable. + +### Orthogonal: the unbroadcast-tx gap + +Neither a derived SQL projection nor a stored flag covers a transaction the +user constructs but never records or broadcasts. If no wallet transaction state +exists, the wallet has no durable fact proving the address was published. That +privacy gap is independent of where used-ness is materialized. + +## 4. Implementation Notes + +- SQL migrations do not add an `addresses.used` column. +- SQL address-read queries project `is_used` with `EXISTS` over `utxos` instead + of reading address metadata. +- The Store interface has no `MarkAddressUsed` method on SQL backends; SQL + derives used-ness from wallet transaction state recorded in the `utxos` + table. +- The kvdb adapter keeps using waddrmgr's used bit. diff --git a/docs/developer/adr/0012-wallet-level-watch-only-uniformity.md b/docs/developer/adr/0012-wallet-level-watch-only-uniformity.md new file mode 100644 index 0000000000..5b14a45d23 --- /dev/null +++ b/docs/developer/adr/0012-wallet-level-watch-only-uniformity.md @@ -0,0 +1,210 @@ +# ADR 0012: Wallet-Level Watch-Only as a Uniform Invariant + +## 1. Context + +The legacy `waddrmgr` address-manager supports a richer watch-only model than +modern descriptor wallets: + +- The wallet carries a top-level `is_watch_only` flag. +- Individual accounts can additionally be watch-only (e.g. an imported + xpub-only account inside an otherwise-spendable wallet). +- Reports such as `AccountProperties().IsWatchOnly` compute an "effective" + per-account watch-only state by combining the wallet flag with the + presence of account-level private-key material. + +This per-account nuance has wide-ranging consequences for the SQL store and +the wallet API: + +- Read paths compute watch-only state via joins against the encrypted + secrets table (`account_secrets`), creating cross-cutting dependencies + between read flows and the signer surface. UTXO listing, transaction + listing, and address listing all become coupled to a table whose only + legitimate consumer is the signer. +- Balance reporting has to distinguish a wallet's "spendable" total from + its "watch-only" total, with separate accumulation paths and separate + API shapes for the two categories. +- Importing or removing a watch-only account changes a wallet's effective + signing capability at runtime, complicating audit, caching, and the + wallet's lifecycle reasoning. + +Bitcoin Core introduced descriptor wallets in v0.21.0 and made them the +default for new wallets in v23.0. In that descriptor-wallet model: + +- Is created with `disable_private_keys` set to true or false; the flag + is immutable after creation. +- Rejects descriptor imports whose mode conflicts with the wallet: + > Cannot import descriptor without private keys to a wallet with + > private keys enabled. +- Surfaces the wallet's mode through `getwalletinfo`'s + `private_keys_enabled` field and emits a single `mine` bucket from + `getbalances` — the `watchonly` sub-bucket only persists for + deprecated pre-descriptor wallets. + +## 2. Decision + +A btcwallet wallet is uniformly watch-only or uniformly spendable. The +state is recorded in `wallets.is_watch_only`, set at wallet creation, and +immutable thereafter. Imports whose mode conflicts with the wallet are +rejected at the store boundary by application-level validation that runs +uniformly on both SQL backends. + +Implications: + +- A non-watch-only wallet rejects imports that would introduce + watch-only-only material (an imported xpub account without matching + encrypted account private-key material, or an imported address + without encrypted private-key material). +- A watch-only wallet symmetrically rejects imports that would introduce + spendable material. +- Balance reporting on the wallet-internal surface returns a single + category. There is no `watchonly` sub-bucket. The wallet's watch-only + state is surfaced via `Wallet.IsWatchOnly()`. +- Users that want to track watch-only material alongside a spendable + wallet create a second wallet for that purpose, matching Core. +- Read paths (UTXO listing, transaction listing, address listing) do not + consult `account_secrets`. Watch-only state is a wallet-level + constant cached on the `Wallet` struct at startup. + +The legacy JSON-RPC contract (`getbalance`, `listaccounts`, +`includeWatchOnly`) keeps emitting and accepting its historical fields so +external consumers do not break. Wallet-internal callers migrate to the +single-bucket shape. + +### Imported addresses land in a reserved wallet-level bucket + +An imported address — with or without private-key material — is never +treated as a member of a derived (HD) account. The store holds every +imported address in a single per-scope, wallet-level **imported bucket**: +the account named `imported` (`db.DefaultImportedAccountName`), keyed by the +address's `(wallet_id, purpose, coin_type)` only. The bucket is internal +infrastructure, not an imported xpub account — it is **keyless** (no +account-level public key, no master fingerprint, and no `account_secrets` +row) and merely holds individually-imported address rows. Each imported +address row carries its own secret material when the wallet is spendable. + +The bucket is materialized lazily: the first import into a scope +auto-creates it inside the same write transaction, and later imports reuse +it. The bucket name is **reserved** — `CreateDerivedAccount` and +`CreateImportedAccount` both reject `DefaultImportedAccountName` with +`ErrReservedAccountName`, so neither a derived account nor a true imported +xpub account can occupy the slot, and an imported address can never be +retargeted into a derived account. + +Because the bucket is keyless, the spendable-wallet invariant does not apply +to the bucket account itself; it applies **per imported address**: + +- On a spendable wallet, an imported address must carry its own + `encrypted_priv_key`. A public-only or script-only address import is + rejected with `ErrSpendableWalletNeedsAddressPrivKey`. +- On a watch-only wallet, the same imported address is public-only and is + accepted; a private-key-bearing import is rejected with + `ErrWatchOnlyViolation`. + +Derived addresses are unaffected: they inherit their key material from the +account xpub/xpriv and are reached through `AccountKeyFromParams` (the +caller-supplied account name), never through the imported bucket. + +### P2A / anchor outputs are transaction state, not imports + +Pay-to-anchor (P2A) outputs are keyless by construction (`OP_1 +<0x4e73>`): there is no private key to import and none to require. Although +Bitcoin Core can encode P2A as an address/destination, a P2A is not useful as +long-lived wallet address material. Its relevance comes from the transaction +flow that created it, typically as a temporary anchor used by a child +transaction to fee-bump its parent. + +This ADR therefore treats P2A as transaction/output state, not as a standard +managed or imported address. A spendable wallet that needs to observe an anchor +tracks the concrete output through the relevant-output / watched-outpoint path, +not by importing an address row. No imported-address or address-secret row is +created for the anchor, so no symmetric-invariant exception is needed: the +invariant never triggers on a path the anchor does not travel. The child +transaction that spends and fee-bumps the parent signs with the wallet's own +key material on its other inputs/outputs; the keyless anchor input contributes +no signature, so the spend flow stays safe under the uniform wallet-level +model. + +## 3. Consequences + +- `db.UtxoInfo` does **not** carry an `IsWatchOnly` field. Wallet-level + state, cached on the `Wallet` struct, applies uniformly to every UTXO + row. +- `db.AccountInfo.IsWatchOnly` and `db.AddressInfo.IsWatchOnly` are + retained as wallet-level convenience copies. They are documented as + identical across every account or address belonging to the same + wallet. A future cleanup task may remove them; callers that want the + canonical reading use `Wallet.IsWatchOnly()`. +- `NewImportedAddressParams.IsWatchOnly()` is removed. The params struct + does not carry wallet-level state, and the per-row "watch-only because + no private key" condition cannot occur in a spendable wallet under + this invariant. +- Per-account `is_watch_only` computations in account-read queries + (`GetAccountByWalletScopeAndName` and its equivalents) drop their + `LEFT JOIN account_secrets` clause; the value is projected directly + from `wallets.is_watch_only`. +- A new symmetric invariant — enforced by application-level validation at + the store boundary, uniformly on both SQL backends — asserts that a + spendable wallet's true imported (xpub) accounts and its imported + addresses carry the matching secret material. The existing + one-directional checks (watch-only wallets cannot store secrets) are + preserved. The invariant is deliberately **not** backed by a database + trigger: a trigger cannot be expressed identically on SQLite (whose + triggers are not deferrable), would duplicate the application-level check, + and is bypassable by out-of-band writes — so one application-level check + for both backends is the single source of truth. +- The symmetric secret requirement applies to **imported** material and + exempts the keyless imported-address bucket by design. The account-level + check (`requireAccountPrivKeyOnSpendable`) runs in `CreateImportedAccount`, + the path for true imported (xpub) accounts, and requires account-level + secret material on a spendable wallet. The keyless imported bucket does not + travel that path — it is materialized by a dedicated keyless insert and + holds no account-level secret — so it is exempt; the per-address check is + what enforces spendability for the addresses it holds. +- Derived accounts are not watch-only just because they are read without + looking at `account_secrets`: the wallet-level flag is the public + watch-only signal. Spendable derived accounts may persist encrypted + account private-key material in `account_secrets`; the unlocked signing + material used at runtime is derived from wallet key material. Derived + addresses still do not store per-address private keys in `address_secrets`. + Requiring a per-address secret for a derived row would contradict the HD + design — a derived address in a spendable wallet is spendable because the + wallet holds the relevant wallet/account key material, so absence of an + address secret is not a watch-only signal. Only an *imported* row lacking + the secret material required by this invariant indicates watch-only-only + material, which is what the invariant forbids on a spendable wallet. +- This ADR does not remove `account_secrets` or `key_scope_secrets`; it only + removes account/address secret presence as a public watch-only inference + source. +- The invariant is enforced at imported-account / imported-address + **create time**, application-side and uniformly on both SQL backends (the + `ValidateWatchOnly` and spendable-secret checks at the store boundary). + Because the wallet-level `is_watch_only` flag is immutable after creation + (no `UpdateWallet` path mutates it), the new SQL store cannot reach the + legacy mixed-mode shape, so no load-boundary re-check is required. + The legacy kvdb backend is grandfathered: its data model still + permits an imported watch-only account inside a spendable wallet (the + historical `importpubkey` / `importaddress` flow). This ADR's uniform + invariant is a property of the new SQL store, not a retrofit onto + kvdb. + +## 4. References + +- [ADR 0006](0006-wtxmgr-sql-schema.md): wtxmgr SQL schema (related). +- [ADR 0011](0011-no-addresses-used-column.md): addresses table omits the + `used` column (related — also a "compute, don't persist" choice; this ADR + makes the opposite call by persisting wallet-level watch-only). +- [Bitcoin Core v23.0 release notes][btc-core-v23-release]: documents + descriptor wallets becoming the default wallet type for new wallets. +- [Bitcoin Core `createwallet` documentation][btc-core-createwallet]: + documents the `disable_private_keys` flag whose immutable wallet-level + semantics this ADR matches. +- [Bitcoin Core descriptor import implementation][btc-core-importdescriptors]: + `wallet/rpc/backup.cpp` `ProcessDescriptorImport()` is the source for + the rejection btcwallet mirrors — a descriptor without private keys + cannot be imported into a wallet with private keys enabled (and the + symmetric direction). The matching error string lives in the same + function. + +[btc-core-v23-release]: https://bitcoincore.org/en/releases/23.0/ +[btc-core-createwallet]: https://bitcoincore.org/en/doc/31.0.0/rpc/wallet/createwallet/ +[btc-core-importdescriptors]: https://github.com/bitcoin/bitcoin/blob/v31.0/src/wallet/rpc/backup.cpp diff --git a/docs/developer/adr/README.md b/docs/developer/adr/README.md index 90cbbf1959..a15041a2ea 100644 --- a/docs/developer/adr/README.md +++ b/docs/developer/adr/README.md @@ -6,4 +6,14 @@ ADRs serve as a historical log of important design choices, providing context fo ## Existing ADRs -* [ADR 0001: Multi-Wallet Architecture](./0001-multi-wallet-architecture.md) +- [ADR 0001: Multi-Wallet Architecture](./0001-multi-wallet-architecture.md) - Decides on the architecture for managing multiple distinct wallets and networks within a single daemon instance. +- [ADR 0002: Controller-Syncer-State Architecture](./0002-controller-syncer-architecture.md) - Decouples lifecycle management, synchronization logic, and state tracking from the monolithic `Wallet` struct. +- [ADR 0003: Optimistic CFilter Batch Scanning](./0003-optimistic-cfilter-batching.md) - Optimizes BIP 157/158 Compact Filter synchronization using optimistic batch scanning. +- [ADR 0004: Targeted Rescan vs. Global Rewind](./0004-targeted-rescan-vs-rewind.md) - Introduces "Targeted Rescans" to replace global "Rewinds" for more efficient transaction discovery. +- [ADR 0005: Explicit Rescan on Import](./0005-no-auto-rescan-on-import.md) - Disables automatic blockchain scanning during import operations, requiring explicit user initiation. +- [ADR 0006: Wallet Transaction Manager SQL Schema](./0006-wtxmgr-sql-schema.md) - Defines the relational SQL schema for the Wallet Transaction Manager (`wtxmgr`) migration. +- [ADR 0007: XChaCha20-Poly1305 Encryption](./0007-xchacha20-poly1305-encryption.md) - Replaces XSalsa20-Poly1305 with XChaCha20-Poly1305 for encrypting private key material. +- [ADR 0008: Integration Test Framework](./0008-integration-test-framework.md) - Defines a modular integration test framework for chain and database backend permutations. +- [ADR 0009: Single-Passphrase Encryption Model](./0009-single-passphrase-encryption.md) - Adopts a single-passphrase model that encrypts private data only while keeping public wallet metadata in plaintext. +- [ADR 0010: Keyvault Encryption Layer](./0010-keyvault-encryption-layer.md) - Defines an in-memory keyvault boundary for lock state, key lifecycle, and encryption orchestration between domain logic and SQL persistence. +- [ADR 0011: No `used` Column on the Addresses Table](./0011-no-addresses-used-column.md) - Records the decision that the SQL backend derives address used-ness from the utxos table (monotonic by ADR 0006's soft-delete schema) rather than persisting a separate column. The kvdb backend continues to use waddrmgr's legacy sticky-bit because wtxmgr deletes credit records on reorg. diff --git a/docs/developer/tx_invalidation_flows.md b/docs/developer/tx_invalidation_flows.md new file mode 100644 index 0000000000..c0efe9a015 --- /dev/null +++ b/docs/developer/tx_invalidation_flows.md @@ -0,0 +1,164 @@ +# Transaction Invalidation Flows + +This document defines how the SQL wallet store applies invalidation-related +write workflows when one wallet event changes tx history. It complements +[Wallet Data Model and Lifecycle](./utxo_data_model.md), which defines the +stored states, and +[ADR 0006: Wallet Transaction Manager SQL Schema](./adr/0006-wtxmgr-sql-schema.md), +which defines the schema and its invariants. + +It focuses on the workflow phases, the invariants each phase must preserve, +and the backend guarantees that follow from that model. + +## 1. Wallet Events and Scope + +From the wallet's point of view, a small set of events can change tx history: + +- tx ingest, e.g. when the wallet learns about a newly created or newly + confirmed tx. +- row-local metadata patching, e.g. when `UpdateTx` changes a label or + block/status fields without rewriting graph edges. +- invalidation of an unmined branch, e.g. when publisher-side cleanup fails + one local spend and its dependent descendants. +- rollback of confirmed history at a block boundary, e.g. when a reorg + disconnects blocks and rewinds formerly confirmed wallet history. + +The first two events place invalidation in the broader tx-store model. The +branch-mutating workflows described here are the ones that discover dependent +descendants, clear now-invalid spend edges, rewrite history, and commit the +whole result atomically. + +## 2. Terminology + +- **Root:** The first tx row directly affected by the wallet event. +- **Descendant:** An unmined tx that spends an output created by a root or by a + later discovered descendant. +- **Branch:** One root plus every descendant discovered from that root set. +- **Spend edge:** The wallet-owned relationship that records which tx spends a + previously created output. + +## 3. Core Invariants + +Every invalidation or rollback workflow must preserve the same wallet-visible +invariants. + +- **Atomicity:** Each wallet event executes in one database transaction. The + store must not commit a partially invalidated branch or a partially applied + rollback. +- **No dangling spend edges:** If one tx becomes invalid, the store must clear + any spend references that would otherwise keep invalid UTXO relationships + alive. +- **Retained invalid history:** Invalid, replaced, failed, or orphaned rows + remain part of the wallet's historical view. The workflow rewrites state; it + does not erase audit history. +- **Event-owned graph mutation:** Row-local patching must stay row-local. + Descendant traversal, spend-edge cleanup, replacement tracking, and rollback + orphaning belong only to the workflows that own those mutations. +- **Event-specific root states:** Different wallet events may drive the same + branch through different root-state outcomes. A direct conflict root may + become `replaced`, while a descendant invalidated by that same event becomes + `failed`. + +## 4. Workflow Phases + +When one branch-mutating event runs, the store follows the same overall phases. + +### 4.1 Discover roots + +The workflow first identifies the root rows directly affected by the wallet +event. Those roots may come from the tx being invalidated, from direct conflict +rows discovered during confirmed ingest, or from coinbase rows disconnected by +rollback. + +### 4.2 Snapshot candidate txns + +Before mutation starts, the workflow loads the current unmined tx set needed +for descendant discovery. This snapshot must happen first so later state +rewrites do not hide part of the branch that still needs cleanup. + +### 4.3 Discover descendants + +The workflow then walks the unmined candidate set to a fixed point. Each newly +discovered descendant expands the invalid parent set, which may reveal later +txns farther down the branch. + +### 4.4 Clear spend edges + +Before any affected row becomes visibly invalid, the workflow clears the +wallet-owned spend edges claimed by that root or descendant set. This prevents +the store from exposing rewritten rows that still appear to spend outputs from +an invalid branch. + +### 4.5 Rewrite roots + +After the root set is fully known and its spend edges are safe to rewrite, the +workflow applies the event-specific root outcome. The root state depends on the +wallet event, not just on the graph shape. + +### 4.6 Rewrite descendants + +Once the root outcome is fixed, the workflow rewrites dependent descendants to +their derived invalid state. Direct roots and descendants do not always land in +the same state, so they are handled as separate phases. + +### 4.7 Commit atomically + +Either every phase above commits together or none of them does. The workflow +must not leave behind half-rewritten states, partially cleared spend edges, or +only part of the affected branch updated. + +## 5. Event Outcomes + +The workflow phases stay the same across events, but the resulting root state +depends on which wallet event started the flow. + +| Wallet event | Root outcome | Descendant outcome | Example | +| --- | --- | --- | --- | +| `InvalidateUnminedTx` | `failed` | `failed` | Publisher-side cleanup rejects one local unmined branch. | +| `CreateTx` conflict handling | direct conflict roots become `replaced` | dependent descendants become `failed` | A newly confirmed winner claims wallet-owned inputs already spent by an unmined branch. | +| `RollbackToBlock` | disconnected coinbase roots become `orphaned` | dependent descendants become `failed` | A reorg disconnects the confirming block for the root branch. | + +Row-local metadata patching does not enter this branch workflow because it does +not discover descendants or rewrite spend edges. + +## 6. Worked Example + +Consider one branch with three txns: + +- `A` is the root tx. +- `B` spends an output created by `A`. +- `C` spends an output created by `B`. + +The same branch can be rewritten differently depending on the initiating event: + +- Under `InvalidateUnminedTx(A)`, `A`, `B`, and `C` all become `failed`. +- Under confirmed conflict handling against `A`, `A` becomes `replaced` while + `B` and `C` become `failed`. +- Under rollback that disconnects confirmed coinbase `A`, `A` becomes + `orphaned` while `B` and `C` become `failed`. + +In every case, descendant discovery happens before mutation and spend-edge +cleanup happens before the rewritten states become visible. + +## 7. Backend Guarantees + +Postgres and sqlite may differ internally in query bindings, row types, and +helper structure. They must still preserve the same workflow guarantees. + +- The same wallet event yields the same final tx states. +- The same descendant branch is invalidated for the same root set. +- Spend-edge cleanup happens before invalid state becomes visible. +- The whole event remains all-or-nothing at the SQL transaction boundary. + +These guarantees keep the SQL stores aligned with the legacy `kvdb` backend at +the event level, even when the SQL stores retain richer explicit invalid- +history states internally. + +## 8. Relationship to Other Docs + +- [`wallet/internal/db/interface.go`](../../wallet/internal/db/interface.go) + describes the caller-facing `TxStore` contract. +- [Wallet Data Model and Lifecycle](./utxo_data_model.md) explains the + persisted states and the "retain history" policy. +- [ADR 0006: Wallet Transaction Manager SQL Schema](./adr/0006-wtxmgr-sql-schema.md) + defines the schema-level invariants that these workflows must preserve. diff --git a/docs/developer/unit_testing_guidelines.md b/docs/developer/unit_testing_guidelines.md index 89db1ffd5d..088991f1c9 100644 --- a/docs/developer/unit_testing_guidelines.md +++ b/docs/developer/unit_testing_guidelines.md @@ -239,6 +239,69 @@ func TestProcessPaymentHandlesOrchestration(t *testing.T) { } ``` +## 6. Prefer `mock.Mock` for Interface Mocks + +When a test needs to implement an interface for expectations or call +verification, prefer embedding `mock.Mock` from `github.com/stretchr/testify/mock` +over writing a bespoke stub struct. This preference applies specifically when you +need to verify that methods were called with certain arguments or a specific +number of times. For simple stubs that only return static values without +tracking calls, a basic struct or function literal may still be appropriate. + +Using `mock.Mock` provides several advantages: +- **Explicit Expectations:** Define exactly which methods must be called, and + with what arguments, using `On(...).Return(...)`. +- **Call Verification:** Use `AssertExpectations(t)` to ensure all expected + calls were made. This is cleaner than manually tracking call counts in a stub. +- **Consistent API:** Aligns with existing project tests, making mocks easier + to read and maintain. + +### Example: Using `mock.Mock` + +First, define the mock struct by embedding the concrete `mock.Mock` type. In +the method implementation, use `m.Called` to record the call and return the +arguments. Use a defensive type assertion to safely handle unexpected return +types: + +```go +type mockStore struct { + mock.Mock +} + +func (m *mockStore) TxDetails(ctx context.Context, hash *chainhash.Hash) (*wtxmgr.TxDetails, error) { + args := m.Called(ctx, hash) + + // Safely handle the first return value with a type assertion. + details, ok := args.Get(0).(*wtxmgr.TxDetails) + if !ok && args.Get(0) != nil { + // Return an error if the mock was misconfigured with the wrong type. + return nil, fmt.Errorf("mock type error: TxDetails result") + } + + return details, args.Error(1) +} +``` + +Then, use the mock in your test by setting expectations and asserting them at +the end of the "Assert" block. + +```go +func TestGetTx(t *testing.T) { + // Arrange + store := &mockStore{} + store.On("TxDetails", mock.Anything, TstTxHash). + Return(&wtxmgr.TxDetails{...}, nil).Once() + + // Act + tx, err := GetTx(store, TstTxHash) + + // Assert + require.NoError(t, err) + require.NotNil(t, tx) + store.AssertExpectations(t) +} +``` + ## 7. Prefer `require` Over `assert` The `testify` library provides both `require` and `assert` packages. diff --git a/docs/developer/utxo_data_model.md b/docs/developer/utxo_data_model.md new file mode 100644 index 0000000000..19c43d90fa --- /dev/null +++ b/docs/developer/utxo_data_model.md @@ -0,0 +1,316 @@ +# Wallet Data Model and Lifecycle + +This document defines the core data model, state machines, and design philosophy of the `btcwallet` transaction subsystem. It serves as the definitive reference for understanding how the wallet manages money, history, and state. + +For the exact write-path rules that apply transaction ingress, invalidation, and rollback to the stored graph, see [Transaction Invalidation Flows](./tx_invalidation_flows.md). + +## 1. Design Philosophy + +The `btcwallet` architecture is built upon a specific worldview: **The wallet is a UTXO Manager.** + +While Bitcoin users often think in terms of "Balances" and "Transactions," the operational reality of the Bitcoin protocol is the **Unspent Transaction Output (UTXO)**. A wallet's primary responsibility is to discover, secure, and select these outputs for spending. + +### 1.1 Structural vs. Operational Centrality + +Our data model distinguishes between data integrity and operational efficiency: + +* **Structurally Transaction-Centered:** Data integrity flows from the parent Transaction to the child UTXO. A UTXO cannot exist without a creating transaction. If a transaction is invalid (double-spent), its outputs are invalid. +* **Operationally UTXO-Centered:** The read path is optimized for UTXOs. 99% of wallet operations (calculating balance, selecting coins for a new payment) query the **UTXO set**, not the transaction history. + +### 1.2 The "Immutable History" Policy + +We adhere to a strict **"Never Delete History"** policy. +* **Chain Data is Immutable:** Once a transaction is mined, it happened. We record it. +* **Intent is Immutable:** If a user attempts a transaction that later fails (e.g., RBF replaced), that attempt is part of the wallet's audit trail. +* **Soft Deletion:** We do not physically `DELETE` rows. Invalid or replaced transactions are marked with a status (`failed`, `replaced`) so they are ignored by balance calculations but preserved for history. + +--- + +## 2. The Data Model + +The wallet state is composed of four primary entities. + +```mermaid +classDiagram + direction LR + + class Block { + Height + Hash + Timestamp + } + + class Transaction { + TxHash + RawBytes + Status + BlockHeight (FK) + } + + class UTXO { + OutPoint (TxHash:Index) + Amount + Address + SpentBy (FK) + } + + class Lease { + LockID + Expiration + } + + Block "1" --> "0..*" Transaction : Confirms + Transaction "1" --> "0..*" UTXO : Creates + Transaction "0..1" --> "0..*" UTXO : Spends (Inputs) + UTXO "1" --> "0..1" Lease : Locks +``` + +Note: This diagram is conceptual and uses natural identifiers like `TxHash` and an outpoint (`TxHash:Index`). The SQL schema uses surrogate keys (for example `transactions.id`) and scopes all `wtxmgr` rows by `wallet_id`. + +Mapping (conceptual -> SQL): + +| Concept | SQL (example) | +| --- | --- | +| `TxHash` | `transactions.tx_hash` (unique per `wallet_id`) | +| Transaction row ID | `(transactions.wallet_id, transactions.id)` | +| OutPoint (`TxHash:Index`) | `(utxos.wallet_id, utxos.tx_id, utxos.output_index)` | +| Address (address book) | `addresses.id` (address-manager schema) | +| Confirmation | `transactions.block_height` (FK to `blocks(block_height)`) | +| Lease | `(utxo_leases.wallet_id, utxo_leases.utxo_id)` | + +### 2.1 Entity Roles + +* **Block:** The anchor for time and confirmation. It handles reorgs. If a block is disconnected, linked transactions automatically lose their confirmation status. +* **Transaction:** The provenance record. It stores the *intent* (Who sent? When?) and the *status* (Confirmed? Failed?). +* **UTXO:** The unit of value. This is the "Coin". It is the bridge between transactions. +* **Lease:** A temporary, memory-like lock on a UTXO to prevent double-spending within the wallet application (e.g., reserving a coin for a Lightning channel open). + +--- + +## 3. Transaction Lifecycle + +A Transaction represents an event that *moves* value. Its lifecycle tracks the journey from user intent to blockchain finality. + +### 3.1 State Machine + +```mermaid +stateDiagram-v2 + [*] --> Pending : User Creates + [*] --> Published : P2P/Mempool + + Pending --> Published : Broadcast + + %% Confirmation is orthogonal (BlockHeight != NULL) + + Published --> Replaced : RBF (Higher Fee Tx Wins) + Published --> Failed : Double-Spend (Other Tx Wins) + Published --> Orphaned : Reorged Coinbase + + Pending --> Failed : Inputs Spent Elsewhere + + %% Coinbase can re-enter the best chain after a reorg. + Orphaned --> Published : Reconfirmed + + Replaced --> [*] + Failed --> [*] + Orphaned --> [*] +``` + +### 3.2 Status vs. Confirmation + +We distinguish between a transaction's **Validity** (Status) and its **Confirmation** (Block Height). + +**1. Confirmation (Source of Truth: `BlockHeight`)** +* **Unconfirmed:** `BlockHeight IS NULL` +* **Confirmed:** `BlockHeight IS NOT NULL` (points to a valid block) + +**2. Validity (Source of Truth: `Status`)** + +| Status Code | Meaning | Affects Balance? | +| :--- | :--- | :--- | +| `0` (`pending`) | Created locally, not yet broadcast. | **Yes** (Factual) | +| `1` (`published`) | Active in mempool or blockchain. | **Yes** (If valid) | +| `2` (`replaced`) | Replaced by a higher-fee transaction (RBF). | **No** | +| `3` (`failed`) | Invalidated by a conflicting transaction (Double-Spend). | **No** | +| `4` (`orphaned`) | Coinbase tx that was reorged out (Invalid). | **No** | + +*Additional invariant: Coinbase transactions cannot exist in the mempool. A coinbase transaction is either confirmed (`BlockHeight IS NOT NULL` and `Status = 1`, `published`) or orphaned (`Status = 4`, `orphaned`, and `BlockHeight IS NULL`).* + +Factual-balance note: the SQL Store layer treats both `pending` and +`published` parent transactions as part of the current live UTXO set. More +conservative spendability policy (for example, excluding `pending` parents) is +applied by higher-level callers on top of that factual base. + +*Note: There is no "Abandoned" state. A broadcast transaction cannot be safely abandoned; it can only be invalidated by double-spending its inputs.* + +--- + +## 4. UTXO Lifecycle + +A UTXO represents **spendable potential**. Its state is derived entirely from its parent Transaction and its "Spent By" pointer. + +### 4.1 State Machine + +```mermaid +stateDiagram-v2 + state "Spendable (Unspent)" as Unspent + state "Immature (Coinbase)" as Immature + state "Leased (Locked)" as Leased + state "Spent (Archive)" as Spent + state "Dead (Invalid)" as Dead + + [*] --> Unspent : Created by Valid Tx + [*] --> Immature : Coinbase Confirmed + + Unspent --> Leased : Application Lock + Leased --> Unspent : Lock Expire/Release + + Unspent --> Spent : Used in New Tx + Leased --> Spent : Forced Spend + + Spent --> Unspent : Reorg (Spending Tx Invalidated) + + %% Coinbase maturity + Immature --> Unspent : Matures (100 blocks) + + %% Parent validity changes (derived, not persisted) + Unspent --> Dead : Parent Invalidated + Immature --> Dead : Parent Orphaned + Dead --> Immature : Parent Reconfirmed (Coinbase) +``` + +### 4.2 Derivation Logic + +A UTXO does not store its own status field. Its status is calculated dynamically to ensure consistency. + +| UTXO State | Parent Tx Status | `BlockHeight` | `SpentBy` Pointer | Meaning | +| :--- | :--- | :--- | :--- | :--- | +| **Confirmed** | `1` (`published`) | `NOT NULL` | `NULL` | Available, mature funds. | +| **Unconfirmed** | `1` (`published`) | `NULL` | `NULL` | Incoming funds, risk of RBF. | +| **Immature** | `1` (`published`) coinbase | `NOT NULL` | `NULL` | Mined coins, must wait 100 blocks. | +| **Spent** | `1` (`published`) | `NOT NULL` | `NOT NULL` | History. We used this money. | +| **Dead (Permanent)** | `3` / `2` (`failed` / `replaced`) | *Any* | *Any* | Permanently invalid output from a failed attempt. | +| **Dead (Recoverable)** | `4` (`orphaned`) coinbase | `NULL` | *Any* | Orphaned coinbase output. Can become `Immature` if parent is reconfirmed. | + +--- + +## 5. Operational Behaviors + +### 5.1 Reorg Handling (Auto-Healing) + +```mermaid +graph TD + Block[Block N - Orphaned] -.->|Invalidates| Tx[Transaction] + + Tx -->|State becomes Unconfirmed| Output[Created UTXO] + Output -.->|State becomes Unconfirmed| Balance[Wallet Balance] + + Tx -->|Releases| Input[Spent UTXO] + Input -.->|State becomes Unspent| Balance +``` + +When the blockchain reorganizes (disconnects blocks): +1. **Block** record is disconnected from the best chain (deleted/replaced). +2. **Transactions** in that block have their `BlockHeight` set to `NULL`. +3. **Effect (non-coinbase):** Regular transactions revert to `Unconfirmed` state (but remain `published`). + * **Coinbase special case:** Coinbase transactions from the disconnected block are marked `orphaned` (as per ADR 0006) instead of reverting to `Unconfirmed`, since they cannot exist outside of the block that created them. +4. **Cascading Effect:** + * Outputs created by these txs become `Unconfirmed` (except for outputs of `orphaned` coinbase txs, which are no longer considered part of the spendable UTXO set). + * Inputs spent by these txs revert to `Unspent` (if the spending tx is completely invalidated). + +Implementation note: The SQL `blocks` table models the current best chain. During a disconnect, the best-chain association is removed (for example by deleting the `blocks` row at that height and inserting the new one). The coinbase status update MUST be atomic with clearing `BlockHeight` to avoid an invalid "unconfirmed coinbase" state. + +Coinbase reconfirmation note: If an orphaned coinbase transaction re-enters the best chain, restoring it requires updating `BlockHeight` and `Status = 1` (`published`) atomically (one SQL statement within a transaction) to satisfy coinbase invariants. + +Reconfirmation semantics: +* The transaction keeps the same `tx_hash` and is associated with a new `block_height`. +* Outputs created by the transaction transition from `Dead (Recoverable)` back to `Immature` (and later become spendable after maturity). + +Deep reorg note: A deep reorg is handled as a batch of block disconnects. Implementations should treat this as a bounded, transactional operation (potentially chunked) rather than assuming a single-block example. + +### 5.2 RBF (Replace-By-Fee) + +```mermaid +graph TD + Input[Input UTXO] + + Input -->|Spent By| TxB[Tx B - New] + TxB -->|Status: Published| Network + + Input -.->|Was Spent By| TxA[Tx A - Old] + TxA -->|Status: Replaced| Archive +``` + +When the wallet detects a replacement transaction (Tx B) for an existing unconfirmed transaction (Tx A): +1. **Detection:** Tx B spends the same inputs as Tx A. +2. **Update:** The input UTXOs are updated to point to Tx B (`SpentBy = TxB`). +3. **Archive:** Tx A is marked as `status = 2` (`replaced`). It remains in the DB for history but is ignored by balance queries. + +### 5.3 Upstream Double-Spend (Invalidation) + +```mermaid +graph TD + Input[Input UTXO] + + Input -->|Spent By| TxD[Tx D - Competitor] + TxD -->|Status: Confirmed| Blockchain + + Input -.->|Was Spent By| TxC[Tx C - Ours] + TxC -->|Status: Failed| Archive + + TxC -->|Creates| Output[Output UTXO] + Output -.->|Status: Dead| Archive +``` + +When a transaction (Tx C) becomes invalid because *its input* was spent by a conflicting transaction (Tx D) that confirmed: +1. **Detection:** We see Tx D confirmed in a block. It spends UTXO X. +2. **Conflict:** We have Tx C (unconfirmed) which also spends UTXO X. +3. **Resolution:** + * Update UTXO X: `SpentBy = TxD`. + * Mark Tx C: `Status = 3` (`failed`). + * **Cascade:** Any UTXOs created by Tx C are now effectively "Dead" (because their parent is `failed`). + * Any transactions spending those dead UTXOs must also be marked `failed` (recursive invalidation). + +Architectural consequence: Recursive invalidation can require graph traversal across transaction chains. Typical wallet histories are shallow, but the design should acknowledge worst-case depth and define an execution strategy (batch processing, recursion limits, and/or iterative traversal) to avoid pathological performance. + +Suggested implementation strategy: +1. Start with a queue of newly-invalid transactions (double-spent, replaced, or orphaned). +2. Mark those transactions `failed`/`replaced`/`orphaned`. +3. Find UTXOs created by those transactions. +4. For each such UTXO, find transactions that spend it and enqueue them. +5. Repeat until the queue is empty. + +This can be implemented with an application-side loop (portable across databases) or a database-side recursive CTE (PostgreSQL). + +### 5.4 Coin Selection +Coin selection queries focus purely on the UTXO set: +1. Filter by `SpentBy IS NULL`. +2. Join the parent transaction and choose a caller-specific live-status + filter. + * Conservative callers can require `Status = 1` (`published`). + * Zero-latency chaining can opt into `Status IN (0, 1)` (`pending`, + `published`), but this creates a strict dependency: the parent must be + broadcast before the child. + * If confirmation is required (e.g., for safety), add + `AND BlockHeight IS NOT NULL`. +3. Filter out active `Leases`. + +Important: There is no dedicated `spendable_utxos` view in the current branch. +Coin selection should build directly on factual UTXO queries and then apply its +own policy filters for pending parents, active leases, and immature coinbase +outputs. Active leases should be excluded using the caller's UTC `now` value, +not an implicit database-local timestamp. + +Transaction reconstruction note: This model optimizes the high-frequency UTXO read path. Reconstructing a full transaction view (inputs/outputs for UI/history) is inherently more join-heavy and should be treated as a lower-frequency, separately optimized read path. + +### 5.5 Multi-Wallet Concurrency & Consistency +When multiple wallets share a single database, the transaction graph remains +wallet-scoped through `transactions.wallet_id`, and other rows either carry +`wallet_id` directly or derive wallet ownership through that transaction graph. +This avoids conflicts and makes correctness properties explicit. + +* **Consistency model:** The system assumes ACID semantics from the database. Reorg handling, status transitions, and lease acquisition must be performed as explicit SQL transactions. +* **Concurrency primitive:** UTXO leases are the wallet-level lock. Coin selection must exclude leased UTXOs, and lease acquisition should be treated as part of the same atomic unit as input selection. +* **Coinbase maturity:** Coinbase UTXOs require 100 confirmations. This rule should be enforced in a single, well-defined caller-side policy path to avoid accidental selection of immature funds. + +Suggested enforcement: a parameterized caller-side query helper or composition layer that takes `current_height` and filters out coinbase UTXOs where `current_height - block_height < 100`. diff --git a/go.mod b/go.mod index 5ed9404034..4dabb045b0 100644 --- a/go.mod +++ b/go.mod @@ -17,48 +17,107 @@ require ( github.com/btcsuite/btcwallet/walletdb v1.6.0 github.com/btcsuite/btcwallet/wtxmgr v1.6.0 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 - github.com/davecgh/go-spew v1.1.1 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 + github.com/docker/go-connections v0.7.0 + github.com/golang-migrate/migrate/v4 v4.19.1 github.com/golang/protobuf v1.5.4 + github.com/jackc/pgx/v5 v5.10.0 github.com/jessevdk/go-flags v1.6.1 github.com/jrick/logrotate v1.1.2 + github.com/lib/pq v1.12.3 github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf github.com/lightninglabs/neutrino v0.18.0 github.com/lightninglabs/neutrino/cache v1.1.4 github.com/lightningnetwork/lnd/fn/v2 v2.0.8 github.com/lightningnetwork/lnd/ticker v1.1.1 github.com/lightningnetwork/lnd/tlv v1.3.3-0.20260615022959-a067468f0f45 - github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.41.0 - golang.org/x/net v0.43.0 - golang.org/x/sync v0.16.0 - golang.org/x/term v0.34.0 - google.golang.org/grpc v1.73.0 - google.golang.org/protobuf v1.36.6 + github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 + golang.org/x/crypto v0.51.0 + golang.org/x/net v0.53.0 + golang.org/x/sync v0.20.0 + golang.org/x/term v0.43.0 + google.golang.org/grpc v1.80.0 + google.golang.org/protobuf v1.36.11 + modernc.org/sqlite v1.53.0 ) require ( + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aead/siphash v1.0.1 // indirect github.com/btcsuite/btcd/v2transport v1.0.1 // indirect github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/platforms v0.2.1 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/creack/pty v1.1.24 // indirect github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/lru v1.1.3 // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/docker v28.5.1+incompatible // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.10.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.2.6 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee // indirect github.com/kkdai/bstream v1.0.0 // indirect - github.com/kr/pretty v0.3.1 // indirect + github.com/klauspost/compress v1.18.5 // indirect github.com/lightningnetwork/lnd/clock v1.0.1 // indirect github.com/lightningnetwork/lnd/queue v1.0.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/stretchr/objx v0.5.2 // indirect + github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/patternmatcher v0.6.1 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/shirou/gopsutil/v4 v4.26.5 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect + github.com/stretchr/objx v0.5.3 // indirect + github.com/tklauser/go-sysconf v0.3.16 // indirect + github.com/tklauser/numcpus v0.11.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect go.etcd.io/bbolt v1.3.11 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.41.0 // indirect + go.opentelemetry.io/otel/metric v1.41.0 // indirect + go.opentelemetry.io/otel/trace v1.41.0 // indirect golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/libc v1.73.4 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect ) // If you change this please run `make lint` to see where else it needs to be diff --git a/go.sum b/go.sum index c2d93e042b..37de37cbb9 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,11 @@ +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/btcsuite/btcd v0.26.0 h1:yntnSshlG3+H7dTwIOR4LTFXDPojVBsFORBNN5y5c/c= @@ -32,29 +40,81 @@ github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd h1:R/opQEbFEy9JG github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.1.3 h1:w9EAbvGLyzm6jTjF83UKuqZEiUtJmvRhQDOCEIvSuE0= github.com/decred/dcrd/lru v1.1.3/go.mod h1:Tw0i0pJyiLEx/oZdHLe1Wdv/Y7EGzAX+sYftnmxBR4o= +github.com/dhui/dktest v0.4.6 h1:+DPKyScKSEp3VLtbMDHcUq6V5Lm5zfZZVb0Sk7Ahom4= +github.com/dhui/dktest v0.4.6/go.mod h1:JHTSYDtKkvFNFHJKqCzVzqXecyv+tKt8EzceOmQOgbU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM= +github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= +github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/golang-migrate/migrate/v4 v4.19.1 h1:OCyb44lFuQfYXYLx1SCxPZQGU7mcaZ7gH9yH4jSFbBA= +github.com/golang-migrate/migrate/v4 v4.19.1/go.mod h1:CTcgfjxhaUtsLipnLoQRWCrjYXycRz/g5+RWDuYgPrE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0= +github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jrick/logrotate v1.1.2 h1:6ePk462NCX7TfKtNp5JJ7MbA2YIslkpfgP03TlTYMN0= @@ -63,10 +123,14 @@ github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee h1:FPP9HDkBbPyni github.com/kcalvinalvin/anet v0.0.0-20251112173137-d8ddc1f6dbee/go.mod h1:N6sz6HwJAenJ6d+/xmSl0ikfV05ZrVGmjt1ryy/WOtE= github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= +github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= +github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= +github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/neutrino v0.18.0 h1:UsyeU3twkCSAXxUssVg8vGXcHkWKzHZ3xQCej57t5t0= @@ -84,56 +148,165 @@ github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6 github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= github.com/lightningnetwork/lnd/tlv v1.3.3-0.20260615022959-a067468f0f45 h1:krvpxGTZJiDyvwU4Jr3GwR00lT4DKITDS95s28wQKU4= github.com/lightningnetwork/lnd/tlv v1.3.3-0.20260615022959-a067468f0f45/go.mod h1:oL5WIFd3ZoEwh3oH1xzizeUl6pq3DIhx9ljDvRdvI3Q= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI= +github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/patternmatcher v0.6.1 h1:qlhtafmr6kgMIJjKJMDmMWq7WLkKIo23hsrpR3x084U= +github.com/moby/patternmatcher v0.6.1/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/shirou/gopsutil/v4 v4.26.5 h1:RPcBXkpz7kOj9PqGFQOlBPZHsyaPvPVQc098y9RmCNM= +github.com/shirou/gopsutil/v4 v4.26.5/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4= +github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk= +github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ= +github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA= +github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= +github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= +github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= -go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= -go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= -go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= +go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0 h1:dIIDULZJpgdiHz5tXrTgKIMLkus6jEFa7x5SOKcyR7E= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.29.0/go.mod h1:jlRVBe7+Z1wyxFSUs48L6OBQZ5JwH2Hg/Vbl+t9rAgI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= +go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= +go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= +go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= +go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI= +golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8= golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4= golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= -golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 h1:fc6jSaCT0vBduLYZHYrBBNY4dsWuvgyff9noRNDdBeE= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= -google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= -google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= +golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= +golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= +gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM= +google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +modernc.org/cc/v4 v4.28.4 h1:Hd/4Es+MBj+/7hSdZaisNyu6bv3V0Dp2MdllyfqaH+c= +modernc.org/cc/v4 v4.28.4/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI= +modernc.org/ccgo/v4 v4.34.4 h1:OVnSOWQjVKOYkFxoHYB+qQmSHK5gqMqARM+K9DpR/Ws= +modernc.org/ccgo/v4 v4.34.4/go.mod h1:qdKqE8FNIYyysougB1RX9MxCzp5oJOcQXSobANJ4TuE= +modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM= +modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.3 h1:6QAplYyVO+KdPW3pGnqmJDUxtkec8ooEWvks/hhU3lc= +modernc.org/gc/v3 v3.1.3/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.73.4 h1:+ra4Ui8ngyt8HDcO1FTDPWlkAh6yOdaO2yAoh8MddQA= +modernc.org/libc v1.73.4/go.mod h1:DXZ3eO8qMCNn2SnmTNCiC71nJ9Rcq3PsnpU6Vc4rWK8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg= +modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M= +modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= diff --git a/itest/README.md b/itest/README.md new file mode 100644 index 0000000000..1c1e9e4adc --- /dev/null +++ b/itest/README.md @@ -0,0 +1,57 @@ +# itest + +`itest` contains end-to-end integration tests for `btcwallet` using the harness +in `bwtest`. + +## Running Tests + +Common invocations: + +```bash +make itest + +# Select a chain backend. +make itest chain=btcd +make itest chain=bitcoind + +# Select a wallet database backend. +make itest db=kvdb + +# Filter cases by regex. +make itest icase=manager +``` + +The `chain` and `db` variables are forwarded into the test binary as flags. + +## Test Case Naming + +Integration test case names must follow: + +``` +component action +``` + +For example: + +``` +manager create wallet +``` + +This is validated by `itest/main_test.go`. + +## Logs + +Each test run creates a per-run log directory under: + +`itest/test-logs/log---YYYYMMDD-HHMMSS/` + +The harness flattens backend logs into: + +- `miner.log` +- `chain_backend.log` + +Wallet logs are created per test case: + +- `wallet-.log` + +The log directory path is printed when it is created. diff --git a/itest/list_on_test.go b/itest/list_on_test.go new file mode 100644 index 0000000000..9b5e0e2cf6 --- /dev/null +++ b/itest/list_on_test.go @@ -0,0 +1,22 @@ +//go:build itest + +package itest + +import "github.com/btcsuite/btcwallet/bwtest" + +// testCase defines a single integration test case. +type testCase struct { + // Name is the human-readable name of the test case. + Name string + + // TestFunc executes the test case. + TestFunc func(t *bwtest.HarnessTest) +} + +// allTestCases is the full set of integration test cases. +var allTestCases = []*testCase{ + { + Name: "manager create wallet", + TestFunc: testCreateWallet, + }, +} diff --git a/itest/main_test.go b/itest/main_test.go new file mode 100644 index 0000000000..3ef90ad2bb --- /dev/null +++ b/itest/main_test.go @@ -0,0 +1,124 @@ +//go:build itest + +package itest + +import ( + "flag" + "fmt" + "math/rand" + "strings" + "testing" + "time" + + "github.com/btcsuite/btcd/integration/rpctest" + "github.com/btcsuite/btcwallet/bwtest" + "github.com/btcsuite/btcwallet/chain/port" +) + +var ( + // chainBackend defines the blockchain backend to be used for the + // integration tests. + // Options: "btcd" (default), "bitcoind", "neutrino". + chainBackend = flag.String( + "chain", "btcd", + "chain backend to use (btcd, bitcoind, neutrino)", + ) + + // dbBackend defines the database backend to be used for the wallet + // storage. + // Options: "kvdb" (default), "sqlite", "postgres". + // + // This flag allows verifying that the wallet functions correctly across all + // supported database drivers. + dbBackend = flag.String( + "db", "kvdb", + "database backend to use (kvdb, sqlite, postgres)", + ) + + // shuffleSeedFlag is the source of randomness used to shuffle the test + // cases. If not specified, the test cases won't be shuffled. + shuffleSeedFlag = flag.Uint64( + "shuffleseed", 0, + "if set, shuffles the test cases using this as the source of "+ + "randomness", + ) +) + +// init configures rpctest to allocate unique listener ports. +func init() { + // Use system-unique ports for rpctest harnesses so multiple local test runs + // don't collide. + rpctest.ListenAddressGenerator = func() (string, string) { + p2p := fmt.Sprintf(rpctest.ListenerFormat, port.NextAvailablePort()) + rpc := fmt.Sprintf(rpctest.ListenerFormat, port.NextAvailablePort()) + + return p2p, rpc + } +} + +// TestBtcWallet runs the btcwallet integration test suite. +func TestBtcWallet(t *testing.T) { + if len(allTestCases) == 0 { + t.Skip("no integration test cases registered") + } + + maybeShuffleTestCases() + + harness := bwtest.SetupHarness(t, *chainBackend, *dbBackend) + + for _, tc := range allTestCases { + if tc == nil { + continue + } + + validateTestCaseName(t, tc.Name) + + name := fmt.Sprintf("%s/%s", *chainBackend, tc.Name) + + success := t.Run(name, func(st *testing.T) { + ht := harness.Subtest(st) + ht.RunTestCase(tc.Name, tc.TestFunc) + }) + if !success { + t.Logf("failure time: %v", time.Now().Format( + "2006-01-02 15:04:05.000", + )) + + break + } + } +} + +// maybeShuffleTestCases shuffles the test cases if the flag `shuffleseed` is +// set and not 0. +func maybeShuffleTestCases() { + // Exit if set to 0. + if *shuffleSeedFlag == 0 { + return + } + + r := rand.New(rand.NewSource(int64(*shuffleSeedFlag))) + r.Shuffle(len(allTestCases), func(i, j int) { + allTestCases[i], allTestCases[j] = allTestCases[j], allTestCases[i] + }) +} + +// validateTestCaseName enforces a consistent naming convention for integration +// test cases. +// +// Names must be in the format "component action" (space separated), and must +// not include underscores. +func validateTestCaseName(t *testing.T, name string) { + t.Helper() + + if strings.Contains(name, "_") { + t.Fatalf("invalid test case name %q: underscores are not allowed", + name) + } + + words := strings.Fields(name) + if len(words) < 2 { + t.Fatalf("invalid test case name %q: want 'component action'", + name) + } +} diff --git a/itest/manager_test.go b/itest/manager_test.go new file mode 100644 index 0000000000..98444596d1 --- /dev/null +++ b/itest/manager_test.go @@ -0,0 +1,55 @@ +//go:build itest + +package itest + +import ( + "context" + "time" + + "github.com/btcsuite/btcwallet/bwtest" + "github.com/btcsuite/btcwallet/wallet" + "github.com/stretchr/testify/require" +) + +// testCreateWallet verifies a wallet can be created, started, and synced. +func testCreateWallet(h *bwtest.HarnessTest) { + h.Helper() + + // Create a wallet using the Manager API. + cfg := wallet.Config{ + DB: h.WalletDB, + Chain: h.ChainClient, + ChainParams: h.NetParams(), + RecoveryWindow: 20, + WalletSyncRetryInterval: 500 * time.Millisecond, + Name: "testwallet", + PubPassphrase: []byte("public"), + } + + manager := wallet.NewManager() + params := wallet.CreateWalletParams{ + Mode: wallet.ModeGenSeed, + PubPassphrase: []byte("public"), + PrivatePassphrase: []byte("private"), + Birthday: time.Now().Add(-1 * time.Hour), + } + + w, err := manager.Create(cfg, params) + require.NoError(h, err, "failed to create wallet") + + err = w.Start(h.Context()) + require.NoError(h, err, "failed to start wallet") + h.Cleanup(func() { + // We use a background context here because h.Context() might be + // cancelled already. + require.NoError( + h, w.Stop(context.Background()), "failed to stop wallet", + ) + }) + + // Register the wallet so harness helpers can assert global invariants. + h.RegisterWallet(w) + + // Mine a few blocks and require the wallet catches up. + h.MineBlocks(5) +} diff --git a/make/testing_flags.mk b/make/testing_flags.mk index a2266c0642..6fae8ee206 100644 --- a/make/testing_flags.mk +++ b/make/testing_flags.mk @@ -1,5 +1,24 @@ DEV_TAGS = dev LOG_TAGS = +IT_TAGS ?= + +# Integration test DB selections derived from the `db` variable (sqlite, postgres). +# Defaults to sqlite if not specified. +ifeq ($(db),postgres) +IT_TAGS += test_db_postgres +IT_DB_LABEL := PostgreSQL +IT_DB_TYPE := postgres +else +IT_TAGS := +IT_DB_LABEL := SQLite +IT_DB_TYPE := sqlite +endif + +# Enable integration test coverage +ifeq ($(cover),1) +ITEST_DB_COVERPROFILE = coverage-itest-$(IT_DB_TYPE).txt +ITEST_DB_COVERAGE = -coverprofile=$(ITEST_DB_COVERPROFILE) -coverpkg=$(PKG)/wallet/internal/db/... -covermode=atomic +endif GOCC ?= go GOLIST := $(GOCC) list -tags="$(DEV_TAGS)" -deps $(PKG)/... | grep '$(PKG)' @@ -39,6 +58,10 @@ ifneq ($(nocache),) TEST_FLAGS += -test.count=1 endif +ifneq ($(benchmem),) +TEST_FLAGS += -test.benchmem +endif + # Define the log tags that will be applied only when running unit tests. If none # are provided, we default to "debug stdlog" which will be standard debug log # output. @@ -61,7 +84,7 @@ UNIT_RACE := $(GOTEST) -tags="$(DEV_TAGS) $(LOG_TAGS)" $(TEST_FLAGS) -race $(UNI # NONE is a special value which selects no other tests but only executes the # benchmark tests here. -UNIT_BENCH := $(GOTEST) -tags="$(DEV_TAGS) $(LOG_TAGS)" -test.bench=. -test.run=NONE $(UNITPKG) +UNIT_BENCH := $(GOTEST) -tags="$(DEV_TAGS) $(LOG_TAGS)" $(TEST_FLAGS) -test.bench=. -test.run=NONE $(UNITPKG) endif ifeq ($(UNIT_TARGETED), no) @@ -70,8 +93,11 @@ UNIT_DEBUG := $(GOLIST) | $(XARGS) env $(GOTEST) -v -tags="$(DEV_TAGS) $(LOG_TAG # NONE is a special value which selects no other tests but only executes the # benchmark tests here. -UNIT_BENCH := $(GOLIST) | $(XARGS) env $(GOTEST) -tags="$(DEV_TAGS) $(LOG_TAGS)" -test.bench=. -test.run=NONE +UNIT_BENCH := $(GOLIST) | $(XARGS) env $(GOTEST) -tags="$(DEV_TAGS) $(LOG_TAGS)" $(TEST_FLAGS) -test.bench=. -test.run=NONE UNIT_RACE := $(UNIT) -race endif UNIT_COVER := $(GOTEST) $(COVER_FLAGS) -tags="$(DEV_TAGS) $(LOG_TAGS)" $(TEST_FLAGS) $(COVER_PKG) + +ITEST_DB := $(GOTEST) $(ITEST_DB_COVERAGE) -tags="itest $(DEV_TAGS) $(LOG_TAGS) $(IT_TAGS)" $(TEST_FLAGS) $(PKG)/wallet/internal/db/itest +ITEST_DB_RACE := $(GOTEST) -race -tags="itest $(DEV_TAGS) $(LOG_TAGS) $(IT_TAGS)" $(TEST_FLAGS) $(PKG)/wallet/internal/db/itest diff --git a/scripts/filter_coverage.sh b/scripts/filter_coverage.sh new file mode 100755 index 0000000000..2696c04b00 --- /dev/null +++ b/scripts/filter_coverage.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Filter coverage files to exclude opposite backend implementations +# Usage: filter_coverage.sh +# Where db_type is 'sqlite' or 'postgres' + +set -e + +DB_TYPE="$1" +if [ "$DB_TYPE" != "sqlite" ] && [ "$DB_TYPE" != "postgres" ]; then + echo "Usage: $0 " + exit 1 +fi + +COVERAGE_FILE="coverage-itest-${DB_TYPE}.txt" +if [ ! -f "$COVERAGE_FILE" ]; then + echo "Coverage file $COVERAGE_FILE not found" + exit 1 +fi + +# Create filtered version +FILTERED_FILE="${COVERAGE_FILE}.filtered" + +# Keep the mode line, filter out opposite backend files +head -1 "$COVERAGE_FILE" >"$FILTERED_FILE" + +if [ "$DB_TYPE" = "sqlite" ]; then + # For sqlite: exclude postgres files + tail -n +2 "$COVERAGE_FILE" | grep -Ev 'pg|postgres' >>"$FILTERED_FILE" +else + # For postgres: exclude sqlite files + tail -n +2 "$COVERAGE_FILE" | grep -Ev 'sqlite' >>"$FILTERED_FILE" +fi + +# Replace original with filtered +mv "$FILTERED_FILE" "$COVERAGE_FILE" + +# Output the filtered coverage percentage +go tool cover -func="$COVERAGE_FILE" | + awk '/^total:/ { print "Filtered test coverage for '"$DB_TYPE"': " $3 }' diff --git a/scripts/tidy_modules.sh b/scripts/tidy_modules.sh index 3fa5bfb252..4dc7e0d738 100755 --- a/scripts/tidy_modules.sh +++ b/scripts/tidy_modules.sh @@ -1,17 +1,58 @@ #!/bin/bash -SUBMODULES=$(find . -mindepth 2 -name "go.mod" | cut -d'/' -f2) +# Keep unset-variable and pipeline safety, but do NOT use `set -e` here. +# +# We want two properties at the same time: +# 1. `go mod tidy` should run for the root module and every nested module, so we +# still auto-fix any module that *can* be tidied in the current run. +# 2. A failure in one module must still make the overall script fail so CI does +# not silently pass over a broken submodule. +# +# Using `set -e` would stop on the first failing module and skip the remaining +# tidies. Instead, we run each module explicitly, collect failures, and exit +# non-zero at the end if any module failed. +set -uo pipefail +# Collect module directories whose `go mod tidy` invocation failed. +failures=() -# Run 'go mod tidy' for root. -go mod tidy +run_tidy() { + local module_dir="$1" -# Run 'go mod tidy' for each module. -for submodule in $SUBMODULES -do - pushd $submodule + # Run each tidy in the target module directory so the command behaves as if a + # developer had entered that module and run `go mod tidy` manually. + echo "Running 'go mod tidy' in ${module_dir}" - go mod tidy + if ! ( + # Turn off any parent go.work file. We want to validate each module as an + # independent module, because CI and release consumers will resolve module + # dependencies that way. + cd "$module_dir" + GOWORK=off go mod tidy + ); then + # Keep going so other modules still get tidied, but remember the failure so + # the script can fail loudly once every module has been attempted. + failures+=("$module_dir") + fi +} - popd -done +# Tidy the repository root module first. +run_tidy "." + +# Tidy every actual nested Go module. +# +# We intentionally discover module directories from real `go.mod` paths instead +# of truncating the path (for example, `wallet/txauthor`, `wallet/txrules`, and +# `wallet/txsizes` are distinct modules and must be tidied separately). +while IFS= read -r submodule; do + run_tidy "$submodule" +done < <(find . -mindepth 2 -name "go.mod" -exec dirname {} \; | sort -u) + +# Fail at the end if any module failed to tidy. This preserves the previous +# “tidy as much as possible” behavior while making module errors visible to CI. +if [ ${#failures[@]} -ne 0 ]; then + echo + echo "go mod tidy failed for the following modules:" + printf ' - %s\n' "${failures[@]}" + exit 1 +fi diff --git a/sqlc.yaml b/sqlc.yaml new file mode 100644 index 0000000000..21de01f0d0 --- /dev/null +++ b/sqlc.yaml @@ -0,0 +1,45 @@ +# sqlc configuration file for generating type-safe Go code from SQL. +# For full documentation, see: https://docs.sqlc.dev/en/stable/reference/config.html +version: "2" +sql: + - engine: "postgresql" + schema: "wallet/internal/sql/pg/migrations" + queries: "wallet/internal/sql/pg/queries" + gen: + go: + out: "wallet/internal/sql/pg/sqlc" + package: "sqlc" + + # This is the driver package that sqlc will use in the generated code. + sql_package: database/sql + + # Generate a `Querier` interface of all query methods. It's useful for + # mocking in tests. + emit_interface: true + + # Export generated SQL statements so they're usable from other packages. + emit_exported_queries: true + + # Generate prepared statements for better performance with database/sql. + emit_prepared_queries: true + + - engine: "sqlite" + schema: "wallet/internal/sql/sqlite/migrations" + queries: "wallet/internal/sql/sqlite/queries" + gen: + go: + out: "wallet/internal/sql/sqlite/sqlc" + package: "sqlc" + + # This is the driver package that sqlc will use in the generated code. + sql_package: database/sql + + # Generate a `Querier` interface of all query methods. It's useful for + # mocking in tests. + emit_interface: true + + # Export generated SQL statements so they're usable from other packages. + emit_exported_queries: true + + # Generate prepared statements for better performance with database/sql. + emit_prepared_queries: true diff --git a/tools/Dockerfile b/tools/Dockerfile index bde0dbf984..1c2c7daf51 100644 --- a/tools/Dockerfile +++ b/tools/Dockerfile @@ -2,15 +2,17 @@ FROM golang:1.25.11-alpine ENV GOFLAGS="-buildvcs=false" -RUN apk update && apk add --no-cache git +RUN apk update && apk add --no-cache git bash WORKDIR /build COPY tools/go.mod tools/go.sum ./ RUN go install -trimpath \ - github.com/golangci/golangci-lint/v2/cmd/golangci-lint \ - github.com/rinchsan/gosimports/cmd/gosimports \ + github.com/golangci/golangci-lint/v2/cmd/golangci-lint \ + github.com/rinchsan/gosimports/cmd/gosimports \ + github.com/sqlc-dev/sqlc/cmd/sqlc \ + github.com/yoheimuta/protolint/cmd/protolint \ && rm -rf /go/pkg/mod \ && rm -rf /root/.cache/go-build \ && rm -rf /go/src \ diff --git a/tools/go.mod b/tools/go.mod index 306d9a6b4b..30c8651773 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -5,14 +5,18 @@ go 1.25.11 tool ( github.com/golangci/golangci-lint/v2/cmd/golangci-lint github.com/rinchsan/gosimports/cmd/gosimports + github.com/sqlc-dev/sqlc/cmd/sqlc + github.com/yoheimuta/protolint/cmd/protolint ) require ( 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect 4d63.com/gochecknoglobals v0.2.2 // indirect + cel.dev/expr v0.24.0 // indirect codeberg.org/chavacava/garif v0.2.0 // indirect dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect dev.gaijin.team/go/golib v0.6.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/4meepo/tagalign v1.4.3 // indirect github.com/Abirdcfly/dupword v0.1.6 // indirect github.com/AlwxSin/noinlineerr v1.0.5 // indirect @@ -30,6 +34,7 @@ require ( github.com/alfatraining/structtag v1.0.0 // indirect github.com/alingse/asasalint v0.0.11 // indirect github.com/alingse/nilnesserr v0.2.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect github.com/ashanbrown/forbidigo/v2 v2.1.0 // indirect github.com/ashanbrown/makezero/v2 v2.0.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect @@ -51,21 +56,26 @@ require ( github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/chavacava/garif v0.1.0 // indirect github.com/ckaznocha/intrange v0.3.1 // indirect + github.com/cubicdaiya/gonp v1.0.4 // indirect github.com/curioswitch/go-reassign v0.3.0 // indirect github.com/daixiang0/gci v0.13.7 // indirect github.com/dave/dst v0.27.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/denis-tingaikin/go-header v0.5.0 // indirect github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/ettle/strcase v0.2.0 // indirect github.com/fatih/color v1.18.0 // indirect github.com/fatih/structtag v1.2.0 // indirect github.com/firefart/nonamedreturns v1.0.6 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect + github.com/gertd/go-pluralize v0.2.1 // indirect github.com/ghostiam/protogetter v0.3.15 // indirect github.com/go-critic/go-critic v0.13.0 // indirect + github.com/go-sql-driver/mysql v1.9.3 // indirect github.com/go-toolsmith/astcast v1.1.0 // indirect github.com/go-toolsmith/astcopy v1.1.0 // indirect github.com/go-toolsmith/astequal v1.2.0 // indirect @@ -77,7 +87,7 @@ require ( github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/flock v0.12.1 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golangci/asciicheck v0.5.0 // indirect github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect github.com/golangci/go-printf-func-name v0.1.0 // indirect @@ -89,20 +99,30 @@ require ( github.com/golangci/revgrep v0.8.0 // indirect github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect + github.com/google/cel-go v0.26.1 // indirect github.com/google/go-cmp v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/gordonklaus/ineffassign v0.1.0 // indirect github.com/gostaticanalysis/analysisutil v0.7.1 // indirect github.com/gostaticanalysis/comment v1.5.0 // indirect github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect github.com/gostaticanalysis/nilerr v0.1.1 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect + github.com/hashicorp/go-plugin v1.6.3 // indirect github.com/hashicorp/go-version v1.7.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect github.com/hexops/gotextdiff v1.0.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.5 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jgautheron/goconst v1.8.2 // indirect github.com/jingyugao/rowserrcheck v1.1.1 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect github.com/jjti/go-spancheck v0.6.5 // indirect github.com/julz/importas v0.2.0 // indirect github.com/karamaru-alpha/copyloopvar v1.2.1 // indirect @@ -135,11 +155,18 @@ require ( github.com/moricho/tparallel v0.3.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/nakabonne/nestif v0.3.1 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nishanths/exhaustive v0.12.0 // indirect github.com/nishanths/predeclared v0.2.2 // indirect github.com/nunnatsa/ginkgolinter v0.20.0 // indirect + github.com/oklog/run v1.0.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/pganalyze/pg_query_go/v6 v6.1.0 // indirect + github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb // indirect + github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 // indirect + github.com/pingcap/log v1.1.0 // indirect + github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polyfloyd/go-errorlint v1.8.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect @@ -152,8 +179,10 @@ require ( github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect github.com/raeperd/recvcheck v0.2.0 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rinchsan/gosimports v0.3.8 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/riza-io/grpc-go v0.2.0 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/ryancurrah/gomodguard v1.4.1 // indirect github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect @@ -172,12 +201,15 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/spf13/viper v1.12.0 // indirect + github.com/sqlc-dev/sqlc v1.30.0 // indirect github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect github.com/stbenjam/no-sprintf-host-port v0.2.0 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/testify v1.10.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/tetafro/godot v1.5.1 // indirect + github.com/tetratelabs/wazero v1.9.0 // indirect github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect github.com/timonwong/loggercheck v0.11.0 // indirect github.com/tomarrell/wrapcheck/v2 v2.11.0 // indirect @@ -186,30 +218,46 @@ require ( github.com/ultraware/whitespace v0.2.0 // indirect github.com/uudashr/gocognit v1.2.0 // indirect github.com/uudashr/iface v1.4.1 // indirect + github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 // indirect + github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 // indirect github.com/xen0n/gosmopolitan v1.3.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect github.com/yagipy/maintidx v1.0.0 // indirect github.com/yeya24/promlinter v0.3.0 // indirect github.com/ykadowak/zerologlint v0.1.5 // indirect + github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect + github.com/yoheimuta/protolint v0.56.4 // indirect gitlab.com/bosi/decorder v0.4.2 // indirect go-simpler.org/musttag v0.14.0 // indirect go-simpler.org/sloglint v0.11.1 // indirect go.augendre.info/arangolint v0.2.0 // indirect go.augendre.info/fatcontext v0.8.1 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/multierr v1.10.0 // indirect + go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/mod v0.27.0 // indirect + golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.16.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/text v0.28.0 // indirect golang.org/x/tools v0.36.0 // indirect - google.golang.org/protobuf v1.36.6 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect + google.golang.org/grpc v1.75.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.6.1 // indirect + modernc.org/libc v1.66.3 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + modernc.org/sqlite v1.38.2 // indirect mvdan.cc/gofumpt v0.8.0 // indirect mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 // indirect ) diff --git a/tools/go.sum b/tools/go.sum index 58b944c06d..17c6b6fd96 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -2,6 +2,8 @@ 4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= 4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= 4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= @@ -41,6 +43,8 @@ dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88 dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= github.com/Abirdcfly/dupword v0.1.6 h1:qeL6u0442RPRe3mcaLcbaCi2/Y/hOcdtw6DE9odjz9c= @@ -86,12 +90,15 @@ github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQ github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/ashanbrown/forbidigo/v2 v2.1.0 h1:NAxZrWqNUQiDz19FKScQ/xvwzmij6BiOw3S0+QUQ+Hs= github.com/ashanbrown/forbidigo/v2 v2.1.0/go.mod h1:0zZfdNAuZIL7rSComLGthgc/9/n2FqspBOH90xlCHdA= github.com/ashanbrown/makezero/v2 v2.0.1 h1:r8GtKetWOgoJ4sLyUx97UTwyt2dO7WkGFHizn/Lo8TY= github.com/ashanbrown/makezero/v2 v2.0.1/go.mod h1:kKU4IMxmYW1M4fiEHMb2vc5SFoPzXvgbMR9gIp5pjSw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -108,6 +115,8 @@ github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E= github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= @@ -133,6 +142,8 @@ github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0G github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc= +github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -141,6 +152,8 @@ github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nW github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cubicdaiya/gonp v1.0.4 h1:ky2uIAJh81WiLcGKBVD5R7KsM/36W6IqqTy6Bo6rGws= +github.com/cubicdaiya/gonp v1.0.4/go.mod h1:iWGuP/7+JVTn02OWhRemVbMmG1DOUnmrGTYYACpOI0I= github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= @@ -156,12 +169,15 @@ github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42 github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= @@ -174,6 +190,8 @@ github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwV github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= +github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA= +github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk= github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY= github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA= github.com/go-critic/go-critic v0.13.0 h1:kJzM7wzltQasSUXtYyTl6UaPVySO6GkaR1thFnJ6afY= @@ -189,8 +207,12 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= @@ -249,8 +271,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= @@ -275,6 +297,8 @@ github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqt github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cel-go v0.26.1 h1:iPbVVEdkhTX++hpe3lzSk7D3G3QSYqLGoHOcEio+UXQ= +github.com/google/cel-go v0.26.1/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -301,6 +325,8 @@ github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a h1://KbezygeMJZCSHH+HgUZiTeSoiuFspbMg1ge+eFj18= github.com/google/pprof v0.0.0-20250607225305-033d6d78b36a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gordonklaus/ineffassign v0.1.0 h1:y2Gd/9I7MdY1oEIt+n+rowjBNDcLQq3RsH5hwJd0f9s= @@ -318,8 +344,12 @@ github.com/gostaticanalysis/nilerr v0.1.1/go.mod h1:wZYb6YI5YAxxq0i1+VJbY0s2YONW github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= github.com/gostaticanalysis/testutil v0.5.0 h1:Dq4wT1DdTwTGCQQv3rl3IvD5Ld0E6HiY+3Zh0sUGqw8= github.com/gostaticanalysis/testutil v0.5.0/go.mod h1:OLQSbuM6zw2EvCcXTz1lVq5unyoNft372msDY0nY5Hs= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= +github.com/hashicorp/go-plugin v1.6.3 h1:xgHB+ZUSYeuJi96WtxEjzi23uh7YQpznjGh0U0UUrwg= +github.com/hashicorp/go-plugin v1.6.3/go.mod h1:MRobyh+Wc/nYy1V4KAXUiYfzxoYhs7V1mlH1Z7iY2h0= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -331,15 +361,29 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -406,8 +450,12 @@ github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= @@ -433,12 +481,16 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= github.com/nunnatsa/ginkgolinter v0.20.0 h1:OmWLkAFO2HUTYcU6mprnKud1Ey5pVdiVNYGO5HVicx8= github.com/nunnatsa/ginkgolinter v0.20.0/go.mod h1:dCIuFlTPfQerXgGUju3VygfAFPdC5aE1mdacCDKDJcQ= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.38.0 h1:c/WX+w8SLAinvuKKQFh77WEucCnPk4j2OTUr7lt7BeY= @@ -454,6 +506,17 @@ github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3v github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pganalyze/pg_query_go/v6 v6.1.0 h1:jG5ZLhcVgL1FAw4C/0VNQaVmX1SUJx71wBGdtTtBvls= +github.com/pganalyze/pg_query_go/v6 v6.1.0/go.mod h1:nvTHIuoud6e1SfrUaFwHqT0i4b5Nr+1rPWVds3B5+50= +github.com/pingcap/errors v0.11.0/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb h1:3pSi4EDG6hg0orE1ndHkXvX6Qdq2cZn8gAPir8ymKZk= +github.com/pingcap/errors v0.11.5-0.20240311024730-e056997136bb/go.mod h1:X2r9ueLEUZgtx2cIogM0v4Zj5uvvzhuuiu7Pn8HzMPg= +github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86 h1:tdMsjOqUR7YXHoBitzdebTvOjs/swniBTOLy5XiMtuE= +github.com/pingcap/failpoint v0.0.0-20240528011301-b51a646c7c86/go.mod h1:exzhVYca3WRtd6gclGNErRWb1qEgff3LYta0LvRmON4= +github.com/pingcap/log v1.1.0 h1:ELiPxACz7vdo1qAvvaWJg1NrYFoY6gqAh/+Uo6aXdD8= +github.com/pingcap/log v1.1.0/go.mod h1:DWQW5jICDR7UJh4HtxXSM20Churx4CQL0fwL/SoOSA4= +github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0 h1:W3rpAI3bubR6VWOcwxDIG0Gz9G5rl5b3SL116T0vBt0= +github.com/pingcap/tidb/pkg/parser v0.0.0-20250324122243-d51e00e5bbf0/go.mod h1:+8feuexTKcXHZF/dkDfvCwEyBAmgb4paFc3/WeYV2eE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -497,11 +560,15 @@ github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4l github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rinchsan/gosimports v0.3.8 h1:X4Pb9yFf6teHvogorT04yK/0W2Df7eHO79biCcYrA4c= github.com/rinchsan/gosimports v0.3.8/go.mod h1:t0567k69sUHjLvJMPDsV31THZC+8UIbY1oL7NW+0I2c= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/riza-io/grpc-go v0.2.0 h1:2HxQKFVE7VuYstcJ8zqpN84VnAoJ4dCL6YFhJewNcHQ= +github.com/riza-io/grpc-go v0.2.0/go.mod h1:2bDvR9KkKC3KhtlSHfR3dAXjUMT86kg4UfWFyVGWqi8= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= @@ -549,18 +616,29 @@ github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/sqlc-dev/sqlc v1.30.0 h1:H4HrNwPc0hntxGWzAbhlfplPRN4bQpXFx+CaEMcKz6c= +github.com/sqlc-dev/sqlc v1.30.0/go.mod h1:QnEN+npugyhUg1A+1kkYM3jc2OMOFsNlZ1eh8mdhad0= github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= github.com/stbenjam/no-sprintf-host-port v0.2.0 h1:i8pxvGrt1+4G0czLr/WnmyH7zbZ8Bg8etvARQ1rpyl4= github.com/stbenjam/no-sprintf-host-port v0.2.0/go.mod h1:eL0bQ9PasS0hsyTyfTjjG+E80QIyPnBVQbYZyv20Jfk= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= @@ -571,6 +649,8 @@ github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3 h1:f+jULpR github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= github.com/tetafro/godot v1.5.1 h1:PZnjCol4+FqaEzvZg5+O8IY2P3hfY9JzRBNPv1pEDS4= github.com/tetafro/godot v1.5.1/go.mod h1:cCdPtEndkmqqrhiCfkmxDodMQJ/f3L1BCNskCUZdTwk= +github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= +github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= @@ -587,6 +667,10 @@ github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYR github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= +github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07 h1:mJdDDPblDfPe7z7go8Dvv1AJQDI3eQ/5xith3q2mFlo= +github.com/wasilibs/go-pgquery v0.0.0-20250409022910-10ac41983c07/go.mod h1:Ak17IJ037caFp4jpCw/iQQ7/W74Sqpb1YuKJU6HTKfM= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52 h1:OvLBa8SqJnZ6P+mjlzc2K7PM22rRUPE1x32G9DTPrC4= +github.com/wasilibs/wazero-helpers v0.0.0-20240620070341-3dff1577cd52/go.mod h1:jMeV4Vpbi8osrE/pKUxRZkVaA0EX7NZN0A9/oRzgpgY= github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= @@ -597,6 +681,10 @@ github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5Jsjqto github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= +github.com/yoheimuta/go-protoparser/v4 v4.14.2 h1:/P/LlX1CF9NaTWEltGcIZVvNlPbhABuAnBtAWpb3+74= +github.com/yoheimuta/go-protoparser/v4 v4.14.2/go.mod h1:AHNNnSWnb0UoL4QgHPiOAg2BniQceFscPI5X/BZNHl8= +github.com/yoheimuta/protolint v0.56.4 h1:FWvXjVNRaKJWJFxsnilRZhfQ4tc3KS8VVGWecxnLXLo= +github.com/yoheimuta/protolint v0.56.4/go.mod h1:XrnOc0O5mckLR1GAOjqMPdb3R3ZEfLkMpLoq5RxxoG0= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -621,12 +709,33 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.19.0/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -638,6 +747,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -648,8 +759,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o= +golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20250620022241-b7579e27df2b h1:KdrhdYPDUvJTvrDK9gdjfFd6JTk8vA1WJoldYSi0kHo= @@ -763,6 +874,7 @@ golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -785,10 +897,13 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -835,6 +950,8 @@ golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -884,6 +1001,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -935,6 +1054,10 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f h1:OiFuztEyBivVKDvguQJYWq1yDcfAHIID/FVrPR4oiI0= +google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f/go.mod h1:kprOiu9Tr0JYyD6DORrc4Hfyk3RFXqkQ3ctHEum3ZbM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f h1:1FTH6cpXFsENbPR5Bu8NQddPSaUUE6NA2XdZdDSAJK4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -947,6 +1070,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -959,8 +1084,9 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -970,14 +1096,19 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -989,6 +1120,32 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= +modernc.org/cc/v4 v4.26.2 h1:991HMkLjJzYBIfha6ECZdjrIYz2/1ayr+FL8GN+CNzM= +modernc.org/cc/v4 v4.26.2/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.8 h1:qtzNm7ED75pd1C7WgAGcK4edm4fvhtBsEiI/0NQ54YM= +modernc.org/fileutil v1.3.8/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.66.3 h1:cfCbjTUcdsKyyZZfEUKfoHcP3S0Wkvz3jgSzByEWVCQ= +modernc.org/libc v1.66.3/go.mod h1:XD9zO8kt59cANKvHPXpx7yS2ELPheAey0vjIuZOhOU8= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.38.2 h1:Aclu7+tgjgcQVShZqim41Bbw9Cho0y/7WzYptXqkEek= +modernc.org/sqlite v1.38.2/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= mvdan.cc/gofumpt v0.8.0 h1:nZUCeC2ViFaerTcYKstMmfysj6uhQrA2vJe+2vwGU6k= mvdan.cc/gofumpt v0.8.0/go.mod h1:vEYnSzyGPmjvFkqJWtXkh79UwPWP9/HMxQdGEXZHjpg= mvdan.cc/unparam v0.0.0-20250301125049-0df0534333a4 h1:WjUu4yQoT5BHT1w8Zu56SP8367OuBV5jvo+4Ulppyf8= diff --git a/waddrmgr/addr_type.go b/waddrmgr/addr_type.go new file mode 100644 index 0000000000..ec3e52ba4b --- /dev/null +++ b/waddrmgr/addr_type.go @@ -0,0 +1,454 @@ +package waddrmgr + +import ( + "errors" + "fmt" + + "github.com/btcsuite/btcd/address/v2" + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcec/v2/schnorr" + "github.com/btcsuite/btcd/chaincfg/v2" + "github.com/btcsuite/btcd/txscript/v2" +) + +const ( + // p2pkhPkScriptSize is the fixed scriptPubKey size for P2PKH outputs. + // 25 bytes = OP_DUP (1) + OP_HASH160 (1) + OP_DATA_20 (1) + + // (20) + OP_EQUALVERIFY (1) + OP_CHECKSIG (1). + p2pkhPkScriptSize = 25 + + // p2shPkScriptSize is the fixed scriptPubKey size for P2SH outputs. + // 23 bytes = OP_HASH160 (1) + OP_DATA_20 (1) +