diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4329b49c9d8..cb5f765e0a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: goos: ["linux"] timeout-minutes: 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - id: filter uses: dorny/paths-filter@v4 @@ -38,7 +38,7 @@ jobs: - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV if: steps.filter.outputs.code == 'true' - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 if: steps.filter.outputs.code == 'true' with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/check-generated.yml b/.github/workflows/check-generated.yml index c94dbc1fb2d..e298717fd5c 100644 --- a/.github/workflows/check-generated.yml +++ b/.github/workflows/check-generated.yml @@ -20,11 +20,11 @@ jobs: check-mocks-metrics: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} @@ -48,13 +48,13 @@ jobs: check-proto: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 with: fetch-depth: 1 # we need a .git directory to run git diff - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/docker-build-cometbft.yml b/.github/workflows/docker-build-cometbft.yml index a51fc0f5a3e..462832e76b7 100644 --- a/.github/workflows/docker-build-cometbft.yml +++ b/.github/workflows/docker-build-cometbft.yml @@ -60,7 +60,7 @@ jobs: TAGS=$(echo "${{ needs.vars.outputs.tags }}" | sed "s/[^,]*/&-${{ matrix.arch }}/g") echo "tags=${TAGS}" >> $GITHUB_OUTPUT - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/docker-build-e2e-node.yml b/.github/workflows/docker-build-e2e-node.yml index 3e2dc2d32fd..b50ea0abd51 100644 --- a/.github/workflows/docker-build-e2e-node.yml +++ b/.github/workflows/docker-build-e2e-node.yml @@ -60,7 +60,7 @@ jobs: TAGS=$(echo "${{ needs.vars.outputs.tags }}" | sed "s/[^,]*/&-${{ matrix.arch }}/g") echo "tags=${TAGS}" >> $GITHUB_OUTPUT - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} diff --git a/.github/workflows/docs-toc.yml b/.github/workflows/docs-toc.yml index 7022c13b3d2..91f6ac10815 100644 --- a/.github/workflows/docs-toc.yml +++ b/.github/workflows/docs-toc.yml @@ -10,7 +10,7 @@ jobs: check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: technote-space/get-diff-action@v6 with: PATTERNS: | diff --git a/.github/workflows/e2e-manual-multiversion.yml b/.github/workflows/e2e-manual-multiversion.yml index 7b209dc9c42..24bdcb6cdc2 100644 --- a/.github/workflows/e2e-manual-multiversion.yml +++ b/.github/workflows/e2e-manual-multiversion.yml @@ -15,11 +15,11 @@ jobs: runs-on: depot-ubuntu-24.04-4 timeout-minutes: 60 steps: - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: '1.22' - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Build working-directory: test/e2e diff --git a/.github/workflows/e2e-manual.yml b/.github/workflows/e2e-manual.yml index 4dfb7dab26a..d4c7479e025 100644 --- a/.github/workflows/e2e-manual.yml +++ b/.github/workflows/e2e-manual.yml @@ -15,11 +15,11 @@ jobs: runs-on: depot-ubuntu-24.04-4 timeout-minutes: 60 steps: - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: '1.22' - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Build working-directory: test/e2e diff --git a/.github/workflows/e2e-nightly-main.yml b/.github/workflows/e2e-nightly-main.yml index a3750d3f58e..f6e88edf94f 100644 --- a/.github/workflows/e2e-nightly-main.yml +++ b/.github/workflows/e2e-nightly-main.yml @@ -20,11 +20,11 @@ jobs: runs-on: depot-ubuntu-24.04-4 timeout-minutes: 60 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} @@ -55,7 +55,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify Slack on failure - uses: slackapi/slack-github-action@v3.0.3 + uses: slackapi/slack-github-action@v4.0.0 env: BRANCH: ${{ github.ref_name }} RUN_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 6411379b5bd..00cfb0581b6 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -25,7 +25,7 @@ jobs: runs-on: depot-ubuntu-24.04-4 timeout-minutes: 15 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - id: filter uses: dorny/paths-filter@v4 @@ -39,7 +39,7 @@ jobs: - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV if: steps.filter.outputs.code == 'true' - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 if: steps.filter.outputs.code == 'true' with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/fuzz-nightly.yml b/.github/workflows/fuzz-nightly.yml index 46c4b5ca59b..69d7d73672b 100644 --- a/.github/workflows/fuzz-nightly.yml +++ b/.github/workflows/fuzz-nightly.yml @@ -18,10 +18,10 @@ jobs: fuzz-nightly-test: runs-on: depot-ubuntu-24.04-4 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} @@ -82,7 +82,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify Slack on failure - uses: slackapi/slack-github-action@v3.0.3 + uses: slackapi/slack-github-action@v4.0.0 env: BRANCH: ${{ github.ref_name }} CRASHERS: ${{ needs.fuzz-nightly-test.outputs.crashers-count }} diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml index a1a24cde785..0697d86d5ba 100644 --- a/.github/workflows/integration_tests.yml +++ b/.github/workflows/integration_tests.yml @@ -16,7 +16,7 @@ jobs: integration_tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - id: filter uses: dorny/paths-filter@v4 @@ -30,7 +30,7 @@ jobs: - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV if: steps.filter.outputs.code == 'true' - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 if: steps.filter.outputs.code == 'true' with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/kms.yml b/.github/workflows/kms.yml deleted file mode 100644 index 23f41a2c1cf..00000000000 --- a/.github/workflows/kms.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: KMS -# Build, test, and lint the cometkms nested module (kms/). -# Runs on every pull request and push to main. Each job is gated by a paths -# filter so it is skipped when no Go/Makefile/config files have changed. The -# filter includes all *.go files (not just kms/) because the kms module depends -# on the parent cometbft module via `replace => ../`, so parent changes can -# affect the kms build. - -on: - pull_request: - merge_group: - push: - branches: - - main - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} - -jobs: - build: - name: Build - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v6 - - - id: filter - uses: dorny/paths-filter@v4 - with: - filters: | - code: - - '**/*.go' - - 'Makefile' - - 'go.*' - - 'kms/go.*' - - '.github/workflows/kms.yml' - - - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - - - uses: actions/setup-go@v6 - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - with: - go-version: ${{ env.GO_VERSION }} - - - name: Build cometkms - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - run: make kms-build - - test: - name: Test - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v6 - - - id: filter - uses: dorny/paths-filter@v4 - with: - filters: | - code: - - '**/*.go' - - 'Makefile' - - 'go.*' - - 'kms/go.*' - - '.github/workflows/kms.yml' - - - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - - - uses: actions/setup-go@v6 - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - with: - go-version: ${{ env.GO_VERSION }} - - - name: Test cometkms (race detector) - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - run: make kms-test-race - - lint: - name: Lint - runs-on: ubuntu-latest - timeout-minutes: 15 - steps: - - uses: actions/checkout@v6 - - - id: filter - uses: dorny/paths-filter@v4 - with: - filters: | - code: - - '**/*.go' - - 'Makefile' - - 'go.*' - - 'kms/go.*' - - '.golangci.yml' - - '.github/workflows/kms.yml' - - - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - - - uses: actions/setup-go@v6 - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - with: - go-version: ${{ env.GO_VERSION }} - - - name: Lint cometkms - if: steps.filter.outputs.code == 'true' && hashFiles('kms/go.mod') != '' - run: make kms-lint diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f12c7d92cdf..ae11b580325 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,14 +22,14 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 + - uses: actions/checkout@v7 + - uses: actions/setup-python@v7 with: python-version: '3.x' - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/markdown-linter.yml b/.github/workflows/markdown-linter.yml index 2bfbb8cecef..962a0b2f781 100644 --- a/.github/workflows/markdown-linter.yml +++ b/.github/workflows/markdown-linter.yml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Lint Code Base uses: docker://github/super-linter:v4 env: diff --git a/.github/workflows/pre-release.yml b/.github/workflows/pre-release.yml index ea40d9c5d30..95b8697be04 100644 --- a/.github/workflows/pre-release.yml +++ b/.github/workflows/pre-release.yml @@ -12,11 +12,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: '1.22' @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify Slack upon pre-release - uses: slackapi/slack-github-action@v3.0.3 + uses: slackapi/slack-github-action@v4.0.0 env: RELEASE_URL: "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}" with: diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index d6fe302448e..b9735a866bd 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -18,7 +18,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: bufbuild/buf-action@v1 with: input: 'proto' diff --git a/.github/workflows/release-version.yml b/.github/workflows/release-version.yml index 51f5eb032a6..8c1961a6170 100644 --- a/.github/workflows/release-version.yml +++ b/.github/workflows/release-version.yml @@ -11,11 +11,11 @@ jobs: check-version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80215c0e357..63f477966ae 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,13 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: fetch-depth: 0 - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 with: go-version: ${{ env.GO_VERSION }} @@ -58,7 +58,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify Slack upon release - uses: slackapi/slack-github-action@v3.0.3 + uses: slackapi/slack-github-action@v4.0.0 env: RELEASE_URL: "${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}" with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 94f87405818..8604ed92e29 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -18,7 +18,7 @@ jobs: matrix: part: ["00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19"] steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - id: filter uses: dorny/paths-filter@v4 @@ -32,7 +32,7 @@ jobs: - run: echo "GO_VERSION=$(cat .github/workflows/go-version.env | grep GO_VERSION | cut -d '=' -f2)" >> $GITHUB_ENV if: steps.filter.outputs.code == 'true' - - uses: actions/setup-go@v6 + - uses: actions/setup-go@v7 if: steps.filter.outputs.code == 'true' with: go-version: ${{ env.GO_VERSION }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 487e01c7b4d..3d35970ed62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,19 @@ ### BUG FIXES +- `[blocksync]` tolerate late BlockResponse from honest peers after switching to consensus + ([\#5959](https://github.com/cometbft/cometbft/pull/5959)) +- `[mempool]` include proto framing overhead in AppReactor batch size to prevent peer teardown + ([\#5956](https://github.com/cometbft/cometbft/pull/5956)) +- `[blocksync]` document `adaptive_sync` equivocation risk for validator nodes + ([\#5953](https://github.com/cometbft/cometbft/pull/5953)) +- `[abci]` fix socket transport missing `InsertTx` and `ReapTxs` cases in + `handleRequest` and `resMatchesReq`, causing `ErrUnexpectedResponse` and + node self-kill when `mempool.type = "app"` with the default socket transport + ([\#5958](https://github.com/cometbft/cometbft/pull/5958)) +- `[flowrate]` fix flaky `TestWriter` by comparing `Idle` with a duration + tolerance instead of exact equality + ([\#5929](https://github.com/cometbft/cometbft/pull/5929)) - `[rpc]` escape the request `Host` in the endpoints listing page so it cannot break out of the generated HTML ([\#5921](https://github.com/cometbft/cometbft/pull/5921)) @@ -22,6 +35,9 @@ `VerifyVoteExtension` handlers are inconsistent halts the node with a clear `CONSENSUS FAILURE` instead of stalling the whole network ([\#5204](https://github.com/cometbft/cometbft/issues/5204)) +- `[blocksync]` fix deadlock in `AddBlock` caused by holding `pool.mtx` during + `sendError` + ([\#5931](https://github.com/cometbft/cometbft/pull/5931)) - `[blocksync]` hold `pool.mtx` and recompute `maxPeerHeight` in `Enable()` ([\#5888](https://github.com/cometbft/cometbft/pull/5888)) - `[inspect]` fix flaky `TestInspectRun` and consolidate start/stop handshake @@ -44,8 +60,16 @@ ([\#5868](https://github.com/cometbft/cometbft/pull/5868)) - `[node]` close partial listeners on startRPC failure ([\#5869](https://github.com/cometbft/cometbft/pull/5869)) +- `[lp2p]` remove `MaxStreamSize` clamp in `StreamReadSized` + ([\#5954](https://github.com/cometbft/cometbft/pull/5954)) +- `[lp2p]` fallback to conn remote addr when resolving inbound peer + ([\#5879](https://github.com/cometbft/cometbft/pull/5879)) - `[consensus]` release cs.mtx before sending to statsMsgQueue ([\#5813](https://github.com/cometbft/cometbft/pull/5813)) +- `[mempool]` truncate proto field number to int32 in filter's ReadTag + ([\#5948](https://github.com/cometbft/cometbft/pull/5948)) +- `[privval]` preempt sleep retries in privval signer client + ([\#5934](https://github.com/cometbft/cometbft/pull/5934)) ### IMPROVEMENTS @@ -53,12 +77,24 @@ ([\#5889](https://github.com/cometbft/cometbft/pull/5889)) - `[execution]` cache validator set within a block cycle. ([\#5834](https://github.com/cometbft/cometbft/pull/5834)) +- `[state]` skip the proposer-priority advance when loading validators for the + block-replay commit-info path (`LoadValidatorsFast`); up to ~900x faster at + the largest checkpoint offsets. + ([\#5204](https://github.com/cometbft/cometbft/issues/5204)) - `[consensus]` reuse encode/decode buffers in WALEncoder and WALDecoder. ([\#5865](https://github.com/cometbft/cometbft/pull/5865)) - `[blocksync]` validate blocksync response sender and signature count ([\#5860](https://github.com/cometbft/cometbft/pull/5860)) - `[autofile]` skip fsync in `FlushAndSync` when no new data was written ([\#5866](https://github.com/cometbft/cometbft/pull/5866)) +- `[mempool]` Implement `MsgBytesFilter` in Reactor to prevent heap amplification attack + ([\#5946](https://github.com/cometbft/cometbft/pull/5946)) +- `[privval]` Dynamically calculate privval maxRemoteSignerMsgSize. + ([\#5985](https://github.com/cometbft/cometbft/pull/5985)) +- `[types]` Update default max block bytes param to account for increased signature size of mldsa65. + ([\#5987](https://github.com/cometbft/cometbft/pull/5987)) +- `[config]` Update the default max_tx_bytes to account for increased signature size of mlsdsa65. + ([\#5989](https://github.com/cometbft/cometbft/pull/5989)) ### FEATURES @@ -193,6 +229,8 @@ ([\#5692](https://github.com/cometbft/cometbft/pull/5692)) - `[p2p]` feat(p2p): add adaptive sync for comet-p2p ([\#5705](https://github.com/cometbft/cometbft/pull/5705)) +- `[blocksync]` fix redo event loss, stale event cancellation, and optimize retry timer + ([\#5592](https://github.com/cometbft/cometbft/pull/5592)) ### FEATURES diff --git a/Makefile b/Makefile index e1ac384cac3..7dc53a51008 100644 --- a/Makefile +++ b/Makefile @@ -88,65 +88,6 @@ install: CGO_ENABLED=$(CGO_ENABLED) go install $(BUILD_FLAGS) -tags '$(BUILD_TAGS)' ./cmd/cometbft .PHONY: install -### cometkms (external remote signer; nested module in kms/) ### - -KMS_OUTPUT ?= $(BUILDDIR)/cometkms -KMS_VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.1.0-dev") -KMS_LDFLAGS := -X github.com/cometbft/cometbft/kms/internal/version.Version=$(KMS_VERSION) -KMS_BUILD_FLAGS := -mod=readonly -ldflags "$(KMS_LDFLAGS)" -KMS_COVERPROFILE ?= $(BUILDDIR)/kms-coverage.out -KMS_COVERHTML ?= $(BUILDDIR)/kms-coverage.html - -#? kms-build: Build cometkms into $(BUILDDIR) -kms-build: - CGO_ENABLED=1 go build -C kms $(KMS_BUILD_FLAGS) -o $(KMS_OUTPUT) ./cmd/cometkms -.PHONY: kms-build - -#? kms-install: Install cometkms to GOBIN -kms-install: - CGO_ENABLED=1 go install -C kms $(KMS_BUILD_FLAGS) ./cmd/cometkms -.PHONY: kms-install - -#? kms-test: Run the cometkms test suite -kms-test: - CGO_ENABLED=1 go test -C kms ./... -count=1 -.PHONY: kms-test - -#? kms-test-race: Run the cometkms test suite with the race detector -kms-test-race: - CGO_ENABLED=1 go test -C kms ./... -race -count=1 -.PHONY: kms-test-race - -#? kms-cover: Run kms tests with coverage; write profile + HTML to $(BUILDDIR) and print the total -kms-cover: - @mkdir -p $(BUILDDIR) - go test -C kms ./... -covermode=atomic -coverpkg=./... -coverprofile=$(KMS_COVERPROFILE) -count=1 - go -C kms tool cover -func=$(KMS_COVERPROFILE) | tail -n 1 - go -C kms tool cover -html=$(KMS_COVERPROFILE) -o $(KMS_COVERHTML) - @echo "coverage profile: $(KMS_COVERPROFILE)" - @echo "coverage html: $(KMS_COVERHTML)" -.PHONY: kms-cover - -#? kms-vet: Vet the cometkms module -kms-vet: - CGO_ENABLED=1 go vet -C kms ./... -.PHONY: kms-vet - -#? kms-lint: Lint the cometkms module (pinned golangci-lint, same version as the root lint target) -kms-lint: - cd kms && CGO_ENABLED=1 go run github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.3 run -.PHONY: kms-lint - -#? kms-tidy: Tidy the cometkms module dependencies -kms-tidy: - go mod tidy -C kms -.PHONY: kms-tidy - -#? kms-clean: Remove cometkms build and coverage artifacts -kms-clean: - rm -f $(KMS_OUTPUT) $(KMS_COVERPROFILE) $(KMS_COVERHTML) -.PHONY: kms-clean - ############################################################################### ### Metrics ### ############################################################################### diff --git a/abci/client/socket_client.go b/abci/client/socket_client.go index 9dea892a370..45c3d5d16e6 100644 --- a/abci/client/socket_client.go +++ b/abci/client/socket_client.go @@ -494,6 +494,10 @@ func resMatchesReq(req *types.Request, res *types.Response) (ok bool) { _, ok = res.Value.(*types.Response_Info) case *types.Request_CheckTx: _, ok = res.Value.(*types.Response_CheckTx) + case *types.Request_InsertTx: + _, ok = res.Value.(*types.Response_InsertTx) + case *types.Request_ReapTxs: + _, ok = res.Value.(*types.Response_ReapTxs) case *types.Request_Commit: _, ok = res.Value.(*types.Response_Commit) case *types.Request_Query: diff --git a/abci/client/socket_client_test.go b/abci/client/socket_client_test.go index 5c6ded36223..a4ae9fb0676 100644 --- a/abci/client/socket_client_test.go +++ b/abci/client/socket_client_test.go @@ -153,6 +153,19 @@ func setupClientServer(t *testing.T, app types.Application) ( return s, c } +func TestInsertTxReapTxsSocket(t *testing.T) { + ctx := t.Context() + _, c := setupClientServer(t, types.BaseApplication{}) + + insertRes, err := c.InsertTx(ctx, &types.RequestInsertTx{Tx: []byte("test")}) + require.NoError(t, err) + require.NotNil(t, insertRes) + + reapRes, err := c.ReapTxs(ctx, &types.RequestReapTxs{}) + require.NoError(t, err) + require.NotNil(t, reapRes) +} + type slowApp struct { types.BaseApplication } diff --git a/abci/server/socket_server.go b/abci/server/socket_server.go index 5fb1ecd68b0..096227c7d9a 100644 --- a/abci/server/socket_server.go +++ b/abci/server/socket_server.go @@ -246,6 +246,18 @@ func (s *SocketServer) handleRequest(ctx context.Context, req *types.Request) (* return nil, err } return types.ToResponseCheckTx(res), nil + case *types.Request_InsertTx: + res, err := s.app.InsertTx(ctx, r.InsertTx) + if err != nil { + return nil, err + } + return types.ToResponseInsertTx(res), nil + case *types.Request_ReapTxs: + res, err := s.app.ReapTxs(ctx, r.ReapTxs) + if err != nil { + return nil, err + } + return types.ToResponseReapTxs(res), nil case *types.Request_Commit: res, err := s.app.Commit(ctx, r.Commit) if err != nil { diff --git a/abci/types/messages.go b/abci/types/messages.go index d8d3c69a9a5..102140a6550 100644 --- a/abci/types/messages.go +++ b/abci/types/messages.go @@ -168,6 +168,18 @@ func ToResponseCheckTx(res *ResponseCheckTx) *Response { } } +func ToResponseInsertTx(res *ResponseInsertTx) *Response { + return &Response{ + Value: &Response_InsertTx{res}, + } +} + +func ToResponseReapTxs(res *ResponseReapTxs) *Response { + return &Response{ + Value: &Response_ReapTxs{res}, + } +} + func ToResponseCommit(res *ResponseCommit) *Response { return &Response{ Value: &Response_Commit{res}, diff --git a/blocksync/pool.go b/blocksync/pool.go index e520c351e48..7ca88967166 100644 --- a/blocksync/pool.go +++ b/blocksync/pool.go @@ -366,7 +366,13 @@ func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, extCommit *ty } pool.mtx.Lock() - defer pool.mtx.Unlock() + var sendErr error + defer func() { + pool.mtx.Unlock() + if sendErr != nil { + pool.sendError(sendErr, peerID) + } + }() requester := pool.requesters[block.Height] if requester == nil { @@ -375,19 +381,17 @@ func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, extCommit *ty // can't punish it. But if the peer sent us a block we clearly didn't // request, we disconnect. if block.Height > pool.height || block.Height < pool.startHeight { - err := fmt.Errorf("peer sent us block #%d we didn't expect (current height: %d, start height: %d)", + sendErr = fmt.Errorf("peer sent us block #%d we didn't expect (current height: %d, start height: %d)", block.Height, pool.height, pool.startHeight) - pool.sendError(err, peerID) - return err + return sendErr } return fmt.Errorf("got an already committed block #%d (possibly from the slow peer %s)", block.Height, peerID) } if !requester.setBlock(block, extCommit, peerID) { - err := fmt.Errorf("requested block #%d from %v, not %s", block.Height, requester.requestedFrom(), peerID) - pool.sendError(err, peerID) - return err + sendErr = fmt.Errorf("requested block #%d from %v, not %s", block.Height, requester.requestedFrom(), peerID) + return sendErr } pool.numPending.Add(-1) @@ -722,23 +726,32 @@ type bpRequester struct { pool *BlockPool height int64 gotBlockCh chan struct{} - redoCh chan p2p.ID // redo may got multiple messages, add peerId to identify repeat + redoCh chan struct{} // coalesced wake-up: capacity 1, signals that redoPeers has entries newHeightCh chan int64 mtx cmtsync.Mutex peerID p2p.ID secondPeerID p2p.ID // alternative peer to request from (if close to pool's height) gotBlockFrom p2p.ID + generation uint64 // incremented each outer loop; protects against stale redo events; protected by mtx + redoPeers []redoEvent // peers that need to be re-requested; protected by mtx block *types.Block extCommit *types.ExtendedCommit } +// redoEvent pairs a peer ID with the generation in which the redo was requested, +// so stale events from a previous outer-loop iteration can be discarded. +type redoEvent struct { + peerID p2p.ID + gen uint64 +} + func newBPRequester(pool *BlockPool, height int64) *bpRequester { bpr := &bpRequester{ pool: pool, height: height, gotBlockCh: make(chan struct{}, 1), - redoCh: make(chan p2p.ID, 1), + redoCh: make(chan struct{}, 1), newHeightCh: make(chan int64, 1), peerID: "", @@ -842,11 +855,13 @@ func (bpr *bpRequester) reset(peerID p2p.ID) (removedBlock bool) { } // Tells bpRequester to pick another peer and try again. -// NOTE: Nonblocking, and does nothing if another redo -// was already requested. +// Nonblocking: state is stored under mtx; the channel is a coalesced wake-up. func (bpr *bpRequester) redo(peerID p2p.ID) { + bpr.mtx.Lock() + bpr.redoPeers = append(bpr.redoPeers, redoEvent{peerID, bpr.generation}) + bpr.mtx.Unlock() select { - case bpr.redoCh <- peerID: + case bpr.redoCh <- struct{}{}: default: } } @@ -909,13 +924,56 @@ func (bpr *bpRequester) newHeight(height int64) { } } +// requestRetryTimer wraps time.Timer with helpers that safely drain pending +// events before reuse so bpRequester can reset a single timer across retries. +type requestRetryTimer struct { + timer *time.Timer + duration time.Duration +} + +func newRequestRetryTimer(duration time.Duration) *requestRetryTimer { + t := time.NewTimer(duration) + if !t.Stop() { + <-t.C + } + return &requestRetryTimer{ + timer: t, + duration: duration, + } +} + +func (rt *requestRetryTimer) Stop() { + if !rt.timer.Stop() { + select { + case <-rt.timer.C: + default: + } + } +} + +func (rt *requestRetryTimer) Reset() { + rt.Stop() + rt.timer.Reset(rt.duration) +} + +func (rt *requestRetryTimer) C() <-chan time.Time { + return rt.timer.C +} + // Responsible for making more requests as necessary // Returns only when a block is found (e.g. AddBlock() is called) func (bpr *bpRequester) requestRoutine() { gotBlock := false + retryTimer := newRequestRetryTimer(requestRetrySeconds * time.Second) + defer retryTimer.Stop() OUTER_LOOP: for { + bpr.mtx.Lock() + bpr.generation++ + currentGen := bpr.generation + bpr.mtx.Unlock() + bpr.pickPeerAndSendRequest() poolHeight := bpr.pool.Height() @@ -923,8 +981,7 @@ OUTER_LOOP: bpr.pickSecondPeerAndSendRequest() } - retryTimer := time.NewTimer(requestRetrySeconds * time.Second) - defer retryTimer.Stop() + retryTimer.Reset() for { select { @@ -935,24 +992,35 @@ OUTER_LOOP: return case <-bpr.Quit(): return - case <-retryTimer.C: + case <-retryTimer.C(): if !gotBlock { bpr.Logger.Debug("Retrying block request(s) after timeout", "height", bpr.height, "peer", bpr.peerID, "secondPeerID", bpr.secondPeerID) + bpr.mtx.Lock() + bpr.redoPeers = nil + bpr.mtx.Unlock() bpr.reset(bpr.peerID) bpr.reset(bpr.secondPeerID) continue OUTER_LOOP } - case peerID := <-bpr.redoCh: - if bpr.didRequestFrom(peerID) { - removedBlock := bpr.reset(peerID) - if removedBlock { - gotBlock = false + case <-bpr.redoCh: + bpr.mtx.Lock() + events := bpr.redoPeers + bpr.redoPeers = nil + bpr.mtx.Unlock() + for _, ev := range events { + if ev.gen != currentGen { + continue // stale event from a previous iteration + } + if bpr.didRequestFrom(ev.peerID) { + removedBlock := bpr.reset(ev.peerID) + if removedBlock { + gotBlock = false + } } } // If both peers returned NoBlockResponse or bad block, reschedule both // requests. If not, wait for the other peer. if len(bpr.requestedFrom()) == 0 { - retryTimer.Stop() continue OUTER_LOOP } case newHeight := <-bpr.newHeightCh: @@ -962,10 +1030,7 @@ OUTER_LOOP: // If the second peer was just set, reset the retryTimer to give the // second peer a chance to respond. if picked := bpr.pickSecondPeerAndSendRequest(); picked { - if !retryTimer.Stop() { - <-retryTimer.C - } - retryTimer.Reset(requestRetrySeconds * time.Second) + retryTimer.Reset() } } case <-bpr.gotBlockCh: diff --git a/blocksync/pool_bench_test.go b/blocksync/pool_bench_test.go new file mode 100644 index 00000000000..03b1387ea09 --- /dev/null +++ b/blocksync/pool_bench_test.go @@ -0,0 +1,44 @@ +package blocksync + +import ( + "testing" + "time" +) + +// benchmarkRetryTimer simulates the retry loop in bpRequester. The legacy version +// allocated a fresh timer per iteration, while the optimized version reuses a single +// requestRetryTimer and simply resets it. The difference in allocations mirrors what +// we expect when a requester repeatedly retries fetching the same block. +func benchmarkRetryTimer(b *testing.B, reuse bool) { + const iterationsPerOp = 256 + b.ReportAllocs() + + b.ResetTimer() + for n := 0; n < b.N; n++ { + if reuse { + rt := newRequestRetryTimer(time.Nanosecond) + for i := 0; i < iterationsPerOp; i++ { + rt.Reset() + } + rt.Stop() + } else { + for i := 0; i < iterationsPerOp; i++ { + timer := time.NewTimer(time.Nanosecond) + if !timer.Stop() { + select { + case <-timer.C: + default: + } + } + } + } + } +} + +func BenchmarkRetryTimerLegacy(b *testing.B) { + benchmarkRetryTimer(b, false) +} + +func BenchmarkRetryTimerReusable(b *testing.B) { + benchmarkRetryTimer(b, true) +} diff --git a/blocksync/pool_test.go b/blocksync/pool_test.go index 44a6df24256..be0cf8749f6 100644 --- a/blocksync/pool_test.go +++ b/blocksync/pool_test.go @@ -260,6 +260,53 @@ func TestBlockPoolTimeout(t *testing.T) { } } +func TestBPRequesterRedoPreservesBothPeers(t *testing.T) { + requester := newBPRequester(nil, 1) + requester.redo("peerA") + requester.redo("peerB") + + // Exactly one coalesced wake-up signal. + require.Equal(t, 1, len(requester.redoCh)) + + requester.mtx.Lock() + events := requester.redoPeers + requester.mtx.Unlock() + + peerSet := map[p2p.ID]struct{}{} + for _, ev := range events { + peerSet[ev.peerID] = struct{}{} + } + require.Contains(t, peerSet, p2p.ID("peerA")) + require.Contains(t, peerSet, p2p.ID("peerB")) +} + +// Regression test: with the old chan p2p.ID capacity-1 design, if a redo signal +// was already pending in the channel, a concurrent redo for the second peer would +// be silently dropped. Verify that no redo event is ever lost. +func TestBPRequesterRedoNeverDropsEvent(t *testing.T) { + requester := newBPRequester(nil, 1) + + // Two redo calls for peerA fill the old capacity-2 channel, then peerB's + // redo would have been dropped with the previous implementation. + requester.redo("peerA") + requester.redo("peerA") + requester.redo("peerB") + + // Still exactly one wake-up signal (coalesced). + require.Equal(t, 1, len(requester.redoCh)) + + requester.mtx.Lock() + events := requester.redoPeers + requester.mtx.Unlock() + + counts := map[p2p.ID]int{} + for _, ev := range events { + counts[ev.peerID]++ + } + require.Equal(t, 2, counts["peerA"]) + require.Equal(t, 1, counts["peerB"], "peerB redo must not be dropped") +} + func TestBlockPoolRemovePeer(t *testing.T) { peers := make(testPeers, 10) for i := 0; i < 10; i++ { @@ -593,6 +640,46 @@ func TestBlockPoolMaxPeerHeightRefreshesOnPopRequest(t *testing.T) { "peer B must contribute to maxPeerHeight once pool.height reaches its base") } +// TestAddBlockDoesNotDeadlockOnSendError is a regression test for AddBlock +// holding pool.mtx while calling sendError on an unbuffered channel. +func TestAddBlockDoesNotDeadlockOnSendError(t *testing.T) { + requestsCh := make(chan BlockRequest, 10) + errorsCh := make(chan peerError) // unbuffered: keeps AddBlock blocked in sendError + + pool := NewBlockPool(1, requestsCh, errorsCh, time.Second) + pool.SetLogger(log.TestingLogger()) + require.NoError(t, pool.Start()) + t.Cleanup(func() { _ = pool.Stop() }) + + pool.mtx.Lock() + req := newBPRequester(pool, 1) + req.peerID = "A" + pool.requesters[1] = req + pool.mtx.Unlock() + + block := &types.Block{Header: types.Header{Height: 1}, LastCommit: &types.Commit{}} + extCommit := &types.ExtendedCommit{Height: 1} + + // "B" did not request the block; setBlock fails → sendError while holding pool.mtx. + go func() { _ = pool.AddBlock("B", block, extCommit, 123) }() + time.Sleep(50 * time.Millisecond) + + heightDone := make(chan struct{}) + go func() { + pool.Height() + close(heightDone) + }() + + select { + case <-heightDone: + <-errorsCh + case <-time.After(500 * time.Millisecond): + <-errorsCh + <-heightDone + t.Fatal("deadlock: AddBlock held pool.mtx while blocked in sendError") + } +} + func TestBlockPoolHasPendingRequestFrom(t *testing.T) { requestsCh := make(chan BlockRequest, 10) errorsCh := make(chan peerError, 10) diff --git a/blocksync/pool_timer_test.go b/blocksync/pool_timer_test.go new file mode 100644 index 00000000000..43cd90d84ed --- /dev/null +++ b/blocksync/pool_timer_test.go @@ -0,0 +1,52 @@ +package blocksync + +import ( + "testing" + "time" +) + +func TestRequestRetryTimerStopDrainsChannel(t *testing.T) { + rt := newRequestRetryTimer(5 * time.Millisecond) + t.Cleanup(rt.Stop) + + time.Sleep(10 * time.Millisecond) // let the timer fire + rt.Stop() + + select { + case <-rt.C(): + t.Fatal("timer channel should have been drained") + default: + } +} + +func TestRequestRetryTimerResetRearms(t *testing.T) { + rt := newRequestRetryTimer(5 * time.Millisecond) + t.Cleanup(rt.Stop) + + start := time.Now() + rt.Reset() + + select { + case <-rt.C(): + if time.Since(start) < 5*time.Millisecond { + t.Fatalf("timer fired too early: %v", time.Since(start)) + } + case <-time.After(50 * time.Millisecond): + t.Fatal("timer never fired after reset") + } +} + +func TestRequestRetryTimerResetDropsStaleEvents(t *testing.T) { + rt := newRequestRetryTimer(20 * time.Millisecond) + t.Cleanup(rt.Stop) + + for i := 0; i < 5; i++ { + rt.Reset() + select { + case <-rt.C(): + t.Fatalf("unexpected retry event on iteration %d", i) + case <-time.After(2 * time.Millisecond): + // retry signal should not fire before duration elapses + } + } +} diff --git a/blocksync/reactor.go b/blocksync/reactor.go index c34a47a24df..510d7225df8 100644 --- a/blocksync/reactor.go +++ b/blocksync/reactor.go @@ -76,7 +76,7 @@ type Reactor struct { adaptiveSyncEnabled bool blockExec *sm.BlockExecutor - store sm.BlockStore + store *store.BlockStore pool *BlockPool localAddr crypto.Address poolRoutineWg sync.WaitGroup @@ -288,8 +288,9 @@ func (r *Reactor) RemovePeer(peer p2p.Peer, _ any) { // respondToPeer loads a block and sends it to the requesting peer, // if we have it. Otherwise, we'll respond saying we don't have it. func (r *Reactor) respondToPeer(msg *bcproto.BlockRequest, src p2p.Peer) { - block := r.store.LoadBlock(msg.Height) - if block == nil { + // Load the block directly in proto form to skip the BlockFromProto+ToProto round-trip. + bl := r.store.LoadBlockProto(msg.Height) + if bl == nil { r.Logger.Info("Peer asking for a block we don't have", "src", src, "height", msg.Height) src.TrySend(p2p.Envelope{ ChannelID: BlocksyncChannel, @@ -309,17 +310,11 @@ func (r *Reactor) respondToPeer(msg *bcproto.BlockRequest, src p2p.Peer) { if state.ConsensusParams.ABCI.VoteExtensionsEnabled(msg.Height) { extCommit = r.store.LoadBlockExtendedCommit(msg.Height) if extCommit == nil { - r.Logger.Error("Found block in store with no extended commit", "block", block) + r.Logger.Error("Found block in store with no extended commit", "height", msg.Height) return } } - bl, err := block.ToProto() - if err != nil { - r.Logger.Error("Unable to convert the block to protobuf", "err", err) - return - } - src.TrySend(p2p.Envelope{ ChannelID: BlocksyncChannel, Message: &bcproto.BlockResponse{ @@ -372,10 +367,17 @@ func (r *Reactor) FilterMsgBytes(chID byte, src p2p.Peer, msgBytes []byte) error return nil } - // blocksync not running, we should not be getting a BlockResponse - if !r.enabled.Load() || !r.pool.IsRunning() { + // Never ran blocksync on this node — any BlockResponse is unsolicited. + if !r.enabled.Load() { return errors.New("unsolicited BlockResponse: blocksync not active") } + // Pool has stopped (switched to consensus). Requests we sent before the + // transition are still in flight; the peers are honest and must not be + // disconnected for answering our own requests. Still enforce the sig-count + // guard below, since any connected peer can reach this path now. + if !r.pool.IsRunning() { + return validateMaxVotes(stub.BlockResponse) + } // ensure we have an outstanding request to this peer if !r.pool.HasPendingRequestFrom(src.ID()) { diff --git a/blocksync/reactor_test.go b/blocksync/reactor_test.go index 4ff06418bf9..962e9723bfd 100644 --- a/blocksync/reactor_test.go +++ b/blocksync/reactor_test.go @@ -555,6 +555,34 @@ func TestFilterMsgBytes(t *testing.T) { bytesFn: blockResponseBytes, expectErr: "blocksync not active", }, + { + // After catching up, the pool stops but in-flight responses from our + // own requests arrive after the switch to consensus. The peer is + // honest and must not be disconnected. + name: "allows late BlockResponse after pool stops for consensus switch", + setup: func(t *testing.T) *Reactor { + r := newFilterReactor(t, true) + require.NoError(t, r.pool.Stop()) + return r + }, + chID: BlocksyncChannel, + peer: unexpected, + bytesFn: blockResponseBytes, + }, + { + // Any connected peer can reach this path once the pool is stopped, + // so the sig-count guard must still apply here. + name: "rejects oversized BlockResponse after pool stops for consensus switch", + setup: func(t *testing.T) *Reactor { + r := newFilterReactor(t, true) + require.NoError(t, r.pool.Stop()) + return r + }, + chID: BlocksyncChannel, + peer: unexpected, + bytesFn: func(t *testing.T) []byte { return blockResponseBytesWithSigs(t, types.MaxVotesCount+1, 0) }, + expectErr: "too many commit signatures", + }, { name: "rejects unsolicited BlockResponse with no requesters", setup: func(t *testing.T) *Reactor { return newFilterReactor(t, true) }, @@ -607,19 +635,6 @@ func TestFilterMsgBytes(t *testing.T) { peer: "any", bytesFn: func(*testing.T) []byte { return nil }, }, - { - name: "rejects BlockResponse after pool stopped", - setup: func(t *testing.T) *Reactor { - r := newFilterReactor(t, true) - seedRequester(r, 1, expected) - require.NoError(t, r.pool.Stop()) - return r - }, - chID: BlocksyncChannel, - peer: expected, - bytesFn: blockResponseBytes, - expectErr: "blocksync not active", - }, { name: "allows BlockResponse at MaxVotesCount commit signatures", setup: func(t *testing.T) *Reactor { @@ -1000,3 +1015,57 @@ func TestEnableRecomputesMaxPeerHeight(t *testing.T) { _ = r.Enable(sm.State{LastBlockHeight: 100}) require.Equal(t, int64(200), pool.MaxPeerHeight()) } + +func TestPeerNotDisconnectedOnLateBlockResponseAfterConsensusSwitch(t *testing.T) { + config = test.ResetTestRoot("blocksync_reactor_test") + defer os.RemoveAll(config.RootDir) + + const nBlocks = int64(10) + genDoc, privVals := genesisDocWithValsPowers([]int64{30}) + + servingPair := newReactor(t, log.TestingLogger(), genDoc, privVals, nBlocks) + defer func() { + require.NoError(t, servingPair.reactor.Stop()) + require.NoError(t, servingPair.app.Stop()) + }() + + syncingPair := newReactor(t, log.TestingLogger(), genDoc, privVals, 0) + syncingPair.reactor.intervalSwitchToConsensus = 20 * time.Millisecond + defer func() { + require.NoError(t, syncingPair.reactor.Stop()) + require.NoError(t, syncingPair.app.Stop()) + }() + + switches := p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { + if i == 0 { + s.AddReactor("BLOCKSYNC", syncingPair.reactor) + } else { + s.AddReactor("BLOCKSYNC", servingPair.reactor) + } + return s + }, p2p.Connect2Switches) + + require.Eventually(t, func() bool { + return !syncingPair.reactor.pool.IsRunning() + }, 30*time.Second, 5*time.Millisecond, "syncing pool did not stop") + + // Pool stopped (consensus switch). Simulate a late in-flight response by + // sending block 1 from the serving node — exercises the onReceive → + // FilterMsgBytes path in p2p/peer.go. + block := servingPair.reactor.store.LoadBlock(1) + require.NotNil(t, block) + bl, err := block.ToProto() + require.NoError(t, err) + + peers := switches[1].Peers().Copy() + require.Len(t, peers, 1) + require.True(t, peers[0].TrySend(p2p.Envelope{ + ChannelID: BlocksyncChannel, + Message: &bcproto.BlockResponse{Block: bl}, + }), "message must be queued to exercise the filter path") + + require.Never(t, func() bool { + return switches[0].Peers().Size() != 1 + }, 200*time.Millisecond, 5*time.Millisecond, + "serving peer was incorrectly disconnected by a late in-flight BlockResponse") +} diff --git a/config/config.go b/config/config.go index c917b5427a6..aa5a806d6b5 100644 --- a/config/config.go +++ b/config/config.go @@ -1030,7 +1030,7 @@ func DefaultMempoolConfig() *MempoolConfig { Size: 5000, MaxTxsBytes: 1024 * 1024 * 1024, // 1GB CacheSize: 10000, - MaxTxBytes: 1024 * 1024, // 1MB + MaxTxBytes: 4 * 1024 * 1024, // 4MB ExperimentalMaxGossipConnectionsToNonPersistentPeers: 0, ExperimentalMaxGossipConnectionsToPersistentPeers: 0, // App mempool defaults diff --git a/config/toml.go b/config/toml.go index 68b03b3f41b..493a06355db 100644 --- a/config/toml.go +++ b/config/toml.go @@ -555,6 +555,10 @@ version = "{{ .BlockSync.Version }}" # Experimental Adaptive sync (bool): # # Run both BLOCKSYNC and CONSENSUS for improved liveness, connectivity, and performance. +# NOTE: On validator nodes, running consensus concurrently with blocksync while catching up +# risks equivocation — consensus can sign votes for heights where the ingestor has not yet +# committed the already-decided block. The HRS file and KMS are not sufficient backstops +# for this scenario. Only enable on validators if you understand and accept this risk. adaptive_sync = {{ .BlockSync.AdaptiveSync }} ####################################################### diff --git a/docs/core/block-sync.md b/docs/core/block-sync.md index 6595e8671d8..f32225c9e22 100644 --- a/docs/core/block-sync.md +++ b/docs/core/block-sync.md @@ -52,5 +52,7 @@ adaptive_sync = false When `adaptive_sync` is enabled, the node runs BLOCKSYNC and CONSENSUS simultaneously instead of the traditional flow where blocksync runs first and then hands off to consensus. This can improve liveness, connectivity, and performance during catch-up. Blocks are ingested through consensus internals as they are received. +> **Warning — validator equivocation risk:** With adaptive sync enabled, consensus starts immediately and `signAddVote` fires whenever the node is in the validator set. This means a catching-up validator can sign a vote for block B at height H while the blocksync ingestor concurrently commits a different block B' at H (already decided by the network). The HRS file and KMS double-sign protection are not sufficient backstops for this scenario — the conflicting messages are at different (H,R,step) tuples so `CheckHRS` never fires. Only enable `adaptive_sync` on validator nodes if you understand and accept this risk; non-validator full nodes are not affected. + If we're lagging sufficiently, we should go back to block syncing, but this is an [open issue](https://github.com/tendermint/tendermint/issues/129). diff --git a/docs/core/configuration.md b/docs/core/configuration.md index 88aae7355e1..60836e54ee4 100644 --- a/docs/core/configuration.md +++ b/docs/core/configuration.md @@ -350,7 +350,7 @@ keep-invalid-txs-in-cache = false # Maximum size of a single transaction. # NOTE: the max size of a tx transmitted over the network is {max_tx_bytes}. -max_tx_bytes = 1048576 +max_tx_bytes = 4194304 # Maximum size of a batch of transactions to send to a peer # Including space needed by encoding (one varint per transaction). diff --git a/docs/references/config/config.toml.md b/docs/references/config/config.toml.md index a5b65d5653a..f7880504b28 100644 --- a/docs/references/config/config.toml.md +++ b/docs/references/config/config.toml.md @@ -1318,7 +1318,7 @@ The value `0` is undefined. ### mempool.max_tx_bytes Maximum size in bytes of a single transaction accepted into the mempool. ```toml -max_tx_bytes = 1048576 +max_tx_bytes = 4194304 ``` | Value type | integer | diff --git a/go.mod b/go.mod index 17025178092..ab612cc176f 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 - github.com/prometheus/common v0.68.1 + github.com/prometheus/common v0.70.0 github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 github.com/rs/cors v1.11.1 github.com/sasha-s/go-deadlock v0.3.9 @@ -43,11 +43,11 @@ require ( github.com/stretchr/testify v1.11.1 github.com/supranational/blst v0.3.16 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 - golang.org/x/crypto v0.52.0 - golang.org/x/net v0.55.0 - golang.org/x/sync v0.20.0 + golang.org/x/crypto v0.54.0 + golang.org/x/net v0.57.0 + golang.org/x/sync v0.22.0 gonum.org/v1/gonum v0.17.0 - google.golang.org/grpc v1.81.1 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 ) @@ -132,7 +132,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/opencontainers/runc v1.2.8 // indirect + github.com/opencontainers/runc v1.3.6 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect @@ -158,7 +158,7 @@ require ( github.com/pion/webrtc/v4 v4.1.2 // indirect github.com/pjbgf/sha1cd v0.6.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/procfs v0.21.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/quic-go v0.59.1 // indirect github.com/quic-go/webtransport-go v0.10.0 // indirect @@ -186,13 +186,13 @@ require ( go.uber.org/zap v1.27.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect - golang.org/x/text v0.37.0 // indirect + golang.org/x/mod v0.37.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 // indirect + golang.org/x/text v0.40.0 // indirect golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.44.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/tools v0.47.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools v2.2.0+incompatible // indirect diff --git a/go.sum b/go.sum index 4eb3ca8ef5b..0cd1ba7cf50 100644 --- a/go.sum +++ b/go.sum @@ -331,8 +331,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 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/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q= -github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI= +github.com/opencontainers/runc v1.3.6 h1:SLGIymCtsk80iNPWgbc8dtjI30r+5mTVV+4dN8/17Sk= +github.com/opencontainers/runc v1.3.6/go.mod h1:o1wyv76EDlTkcf0KTFgN8bMWLPvgF/HfX709lDv+rr4= github.com/ory/dockertest v3.3.5+incompatible h1:iLLK6SQwIhcbrG783Dghaaa3WPzGc+4Emza6EbVUUGA= github.com/ory/dockertest v3.3.5+incompatible/go.mod h1:1vX4m9wsvi00u5bseYwXaSnhNrne+V0E6LAcBILJdPs= github.com/ory/dockertest/v3 v3.12.0 h1:3oV9d0sDzlSQfHtIaB5k6ghUCVMVLpAY8hwrqoCyRCw= @@ -402,10 +402,10 @@ github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UH github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= -github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI= +github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY= +github.com/prometheus/procfs v0.21.0 h1:Qh/e6TlBjZf+XLLqNCqFGmCU6Kj/2Bu7kj3oAc0UnXc= +github.com/prometheus/procfs v0.21.0/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= @@ -530,8 +530,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= @@ -542,8 +542,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= +golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= +golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -565,8 +565,8 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -576,8 +576,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -606,10 +606,10 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57 h1:nwGZBCt+FnXUrGsj5vjzAsEmkcaFvd82BbOjECiFYZc= +golang.org/x/telemetry v0.0.0-20260625142307-59b4966ccb57/go.mod h1:3AWMyWHS+caVoiEXpiq6+tzKA40J4vQT3MYr80ZtQpc= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= @@ -617,8 +617,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -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/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -628,8 +628,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= 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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -642,8 +642,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -655,15 +655,15 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= +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.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= 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= diff --git a/internal/protowire/protowire.go b/internal/protowire/protowire.go new file mode 100644 index 00000000000..e320b7542db --- /dev/null +++ b/internal/protowire/protowire.go @@ -0,0 +1,143 @@ +// Package protowire provides a minimal, allocation-free reader for walking +// raw protobuf wire bytes. Every read is bounds-checked and advances a cursor. +package protowire + +import ( + "errors" + "fmt" +) + +// Wire types (the low 3 bits of a field tag). +const ( + WireVarint = 0 + WireFixed64 = 1 + WireBytes = 2 + WireFixed32 = 5 +) + +var ( + // ErrVarintOverflow is returned when a varint does not terminate within + // the 10 bytes that can hold a 64-bit value. + ErrVarintOverflow = errors.New("varint overflow") + // ErrTruncatedVarint is returned when the buffer ends mid-varint. + ErrTruncatedVarint = errors.New("truncated varint") + // ErrOutOfBounds is returned when a length prefix or fixed-width field + // would run past the end of the buffer. + ErrOutOfBounds = errors.New("length out of bounds") + // ErrIllegalFieldNumber is returned when a tag decodes to a field number + // that is not a legal protobuf field number (>= 1). + ErrIllegalFieldNumber = errors.New("illegal field number") + // ErrUnsupportedWireType is returned when a tag carries a wire type the + // cursor does not know how to skip. + ErrUnsupportedWireType = errors.New("unsupported wire type") +) + +// WireCursor walks a protobuf buffer left to right. Every read is bounds-checked +// and advances the cursor; a read past the end returns an error rather than +// panicking. +type WireCursor struct { + buf []byte + pos int +} + +// NewWireCursor returns a cursor positioned at the start of buf. +func NewWireCursor(buf []byte) WireCursor { + return WireCursor{buf: buf} +} + +// AtEnd reports whether the cursor has consumed the whole buffer. +func (c *WireCursor) AtEnd() bool { return c.pos >= len(c.buf) } + +// ReadVarint decodes a base-128 varint at the cursor and advances past it. +func (c *WireCursor) ReadVarint() (uint64, error) { + var v uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return 0, ErrVarintOverflow + } + if c.pos >= len(c.buf) { + return 0, ErrTruncatedVarint + } + b := c.buf[c.pos] + c.pos++ + v |= uint64(b&0x7f) << shift + if b < 0x80 { + return v, nil + } + } +} + +// ReadTag decodes a field tag, splitting it into field number and wire type. +func (c *WireCursor) ReadTag() (fieldNum, wireType int, err error) { + v, err := c.ReadVarint() + if err != nil { + return 0, 0, err + } + fieldNum = int(int32(v >> 3)) + wireType = int(v & 0x7) + if fieldNum <= 0 { + return 0, 0, fmt.Errorf("%w: %d", ErrIllegalFieldNumber, fieldNum) + } + return fieldNum, wireType, nil +} + +// ReadLengthDelimited reads a length-prefixed run of bytes (wire type 2) and +// returns it as a sub-slice of the underlying buffer, advancing past it. +func (c *WireCursor) ReadLengthDelimited() ([]byte, error) { + length, err := c.ReadVarint() + if err != nil { + return nil, err + } + start := c.pos + end := start + int(length) + if end < start || end > len(c.buf) { + return nil, ErrOutOfBounds + } + c.pos = end + return c.buf[start:end], nil +} + +// SkipField advances the cursor past the body of a field with the given wire +// type, used to ignore fields the caller does not care about. +func (c *WireCursor) SkipField(wireType int) error { + switch wireType { + case WireVarint: + _, err := c.ReadVarint() + return err + case WireFixed64: + return c.advance(8) + case WireBytes: + _, err := c.ReadLengthDelimited() + return err + case WireFixed32: + return c.advance(4) + default: + return fmt.Errorf("%w: %d", ErrUnsupportedWireType, wireType) + } +} + +// RepeatedBytesEntrySize returns the number of wire bytes proto encodes for one +// entry in a repeated bytes field: 1-byte field tag + varint(dataLen). +// dataLen must be >= 0; Go's len() guarantees this for all practical callers. +func RepeatedBytesEntrySize(dataLen int) int { + if dataLen < 0 { + panic("protowire: negative dataLen") + } + n := 1 /* tag */ + 1 /* min varint byte */ + v := uint64(dataLen) >> 7 + for v > 0 { + n++ + v >>= 7 + } + return n +} + +// advance moves the cursor forward by n bytes, bounds-checked. +func (c *WireCursor) advance(n int) error { + end := c.pos + n + if end < c.pos || end > len(c.buf) { + return ErrOutOfBounds + } + c.pos = end + return nil +} diff --git a/internal/protowire/protowire_test.go b/internal/protowire/protowire_test.go new file mode 100644 index 00000000000..1f6559d01d8 --- /dev/null +++ b/internal/protowire/protowire_test.go @@ -0,0 +1,127 @@ +package protowire + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestWireCursor_ReadVarint(t *testing.T) { + // 300 encodes as 0xac 0x02 in base-128 varint. + c := NewWireCursor([]byte{0xac, 0x02}) + v, err := c.ReadVarint() + require.NoError(t, err) + require.Equal(t, uint64(300), v) + require.True(t, c.AtEnd()) +} + +func TestWireCursor_ReadVarint_Truncated(t *testing.T) { + c := NewWireCursor([]byte{0xff}) + _, err := c.ReadVarint() + require.ErrorIs(t, err, ErrTruncatedVarint) +} + +func TestWireCursor_ReadVarint_Overflow(t *testing.T) { + // 11 continuation bytes never terminate within 64 bits. + c := NewWireCursor([]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}) + _, err := c.ReadVarint() + require.ErrorIs(t, err, ErrVarintOverflow) +} + +func TestWireCursor_ReadTag(t *testing.T) { + // Field 1, wire type 2 (length-delimited) => tag byte 0x0a. + c := NewWireCursor([]byte{0x0a}) + fieldNum, wireType, err := c.ReadTag() + require.NoError(t, err) + require.Equal(t, 1, fieldNum) + require.Equal(t, WireBytes, wireType) +} + +func TestWireCursor_ReadTag_IllegalFieldNumber(t *testing.T) { + // Tag 0x00 => field number 0, which is illegal. + c := NewWireCursor([]byte{0x00}) + _, _, err := c.ReadTag() + require.ErrorIs(t, err, ErrIllegalFieldNumber) +} + +func TestWireCursor_ReadTag_FieldNumberTruncatesTo32Bit(t *testing.T) { + // Tag varint 8a 80 80 80 80 01 decodes to wire = 2^35 + 10, so + // wire>>3 == 2^32 + 1 and wire&7 == 2 (WireBytes). int32(2^32+1) == 1. + c := NewWireCursor([]byte{0x8a, 0x80, 0x80, 0x80, 0x80, 0x01}) + fieldNum, wireType, err := c.ReadTag() + require.NoError(t, err) + require.Equal(t, 1, fieldNum, "high bits above 32 must be truncated, matching int32(wire>>3)") + require.Equal(t, WireBytes, wireType) + + // Tag varint 80 80 80 80 40 decodes to wire>>3 == 2^31, and + // int32(2^31) is negative => illegal tag, exactly as gogoproto rejects it. + c = NewWireCursor([]byte{0x80, 0x80, 0x80, 0x80, 0x40}) + _, _, err = c.ReadTag() + require.ErrorIs(t, err, ErrIllegalFieldNumber) +} + +func TestWireCursor_ReadLengthDelimited(t *testing.T) { + c := NewWireCursor([]byte{0x03, 'a', 'b', 'c'}) + b, err := c.ReadLengthDelimited() + require.NoError(t, err) + require.Equal(t, []byte("abc"), b) + require.True(t, c.AtEnd()) +} + +func TestWireCursor_ReadLengthDelimited_OutOfBounds(t *testing.T) { + // Declares length 5 but only 1 byte follows. + c := NewWireCursor([]byte{0x05, 'a'}) + _, err := c.ReadLengthDelimited() + require.ErrorIs(t, err, ErrOutOfBounds) +} + +func TestWireCursor_SkipField(t *testing.T) { + testCases := []struct { + name string + buf []byte + wireType int + }{ + {"varint", []byte{0xac, 0x02}, WireVarint}, + {"fixed64", []byte{1, 2, 3, 4, 5, 6, 7, 8}, WireFixed64}, + {"bytes", []byte{0x02, 'x', 'y'}, WireBytes}, + {"fixed32", []byte{1, 2, 3, 4}, WireFixed32}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + c := NewWireCursor(tc.buf) + require.NoError(t, c.SkipField(tc.wireType)) + require.True(t, c.AtEnd()) + }) + } +} + +func TestWireCursor_SkipField_Unsupported(t *testing.T) { + c := NewWireCursor([]byte{0x00}) + // Wire type 3 (start group) is not supported. + require.ErrorIs(t, c.SkipField(3), ErrUnsupportedWireType) +} + +func TestWireCursor_SkipField_OutOfBounds(t *testing.T) { + // Fixed64 needs 8 bytes; only 2 are present. + c := NewWireCursor([]byte{1, 2}) + require.ErrorIs(t, c.SkipField(WireFixed64), ErrOutOfBounds) +} + +func TestRepeatedBytesEntrySize(t *testing.T) { + // Verified against proto.Size on Txs{Txs: [][]byte{make([]byte, n)}}. + for _, tt := range []struct { + dataLen int + want int + }{ + {0, 2}, // tag(1) + varint(0)=1 + {1, 2}, // tag(1) + varint(1)=1 + {127, 2}, // last 1-byte varint + {128, 3}, // first 2-byte varint + {1000, 3}, + {16383, 3}, // last 2-byte varint + {16384, 4}, // first 3-byte varint + {1048576, 4}, + } { + require.Equal(t, tt.want, RepeatedBytesEntrySize(tt.dataLen), "dataLen=%d", tt.dataLen) + } +} diff --git a/kms/README.md b/kms/README.md deleted file mode 100644 index 42fd0259efc..00000000000 --- a/kms/README.md +++ /dev/null @@ -1,374 +0,0 @@ -# cometkms - -`cometkms` is an external remote signer for CometBFT validators. It lives as a -nested Go module (`github.com/cometbft/cometbft/kms`) inside the CometBFT -repository and is conceptually similar to [tmkms](https://github.com/iqlusioninc/tmkms), -but implemented entirely in Go and building directly on top of the CometBFT -libraries. It dials *out* to one or more validator nodes, authenticates each -connection with either CometBFT's SecretConnection protocol or the libp2p Noise -transport (selected per-validator via the address scheme), and serves Ed25519 -consensus signing requests (votes, proposals, and vote extensions) with -mandatory per-chain double-sign protection. - ---- - -## Status - -| Scope | Status | -|---|---| -| Ed25519 consensus signing (votes, proposals, vote extensions) | **Supported** | -| `softsign` key backend (file-based, in-memory Ed25519) | **Supported** | -| `pkcs11` key backend (HSM / token, Ed25519) | **Supported** | -| `cometp2p` transport (TCP + SecretConnection) | **Supported** | -| Multi-chain, multi-validator support | **Supported** | -| Double-sign protection (reuses CometBFT FilePV state machine) | **Supported** | -| Dial-out + automatic exponential-backoff reconnect | **Supported** | -| AWS KMS backend | Planned | -| libp2p transport (Noise) | **Supported** | -| Account / raw-bytes / ECDSA signing | Planned | -| ML-DSA / eth_secp256k1 key types | Planned | - ---- - -## How it works - -- **Dial-out model.** `cometkms` dials out to the validator's privval listener - (`priv_validator_laddr` in `config.toml`) rather than listening itself. This - removes the need to expose any port on the KMS host. -- **SecretConnection authentication.** Each connection is authenticated and - encrypted using CometBFT's SecretConnection protocol. `cometkms` uses a - dedicated Ed25519 *identity key* (distinct from the consensus signing key) to - authenticate itself to the validator. -- **Request serving.** Once connected, the KMS handles `PubKey`, `SignVote`, - `SignProposal`, and `Ping` requests using `privval.DefaultValidationRequestHandler`. -- **Double-sign protection.** Signing is delegated to CometBFT's `FilePV` state - machine, which persists the last-signed height/round/step to a per-chain state - file and refuses to sign any regression. This survives process restarts. -- **Automatic reconnect.** When a connection drops (validator restart, network - hiccup), `cometkms` reconnects with capped exponential backoff (200 ms initial, - 10 s ceiling) without any manual intervention. - ---- - -## Build - -Requires Go 1.25 or newer. Build from the repository root via the top-level -Makefile (the binary is written to `build/cometkms`): - -```sh -make kms-build # build/cometkms -make kms-install # install to GOBIN -``` - ---- - -## Quick start - -### 1. Initialise the home directory - -```sh -cometkms init --home ~/.cometkms -``` - -This creates: - -- `~/.cometkms/cometkms.toml` — a stub configuration file. -- `~/.cometkms/identity.json` — a fresh Ed25519 identity key for the - SecretConnection. - -### 2. Edit the config - -Open `~/.cometkms/cometkms.toml` and fill in the real values (see the -[example config](#example-config) below). - -### 3. Place the consensus key file - -Copy or symlink the `priv_validator_key.json` for each chain to the path you -set as `key_file` in `[[providers.softsign]]`. - -### 4. Configure the validator - -In the validator's `config.toml` enable the remote signer listener: - -```toml -priv_validator_laddr = "tcp://0.0.0.0:26659" -``` - -The address must be reachable from the host running `cometkms`, and it must -match the `addr` you set in `[[validator]]`. - -### 5. Start the KMS - -```sh -cometkms start --home ~/.cometkms -``` - -`cometkms` will dial the validator and begin serving signing requests. It logs -each connection and any signing errors to stdout. - ---- - -## Configuration reference - -### `[[chain]]` - -Declares one blockchain. You need exactly one `[[chain]]` block per chain you -want to sign for. - -| Field | Type | Required | Description | -|---|---|---|---| -| `id` | string | yes | The chain-id string (e.g. `cosmoshub-4`). | -| `state_file` | string | no | Path to the double-sign state file. Defaults to `/state/.json`. Relative paths are resolved against `--home`. | - -### `[[validator]]` - -Declares one outbound connection to a validator node. A single chain can have -multiple `[[validator]]` blocks (e.g. primary + backup nodes). - -| Field | Type | Required | Description | -|---|---|---|---| -| `chain_id` | string | yes | Must match a declared `[[chain]].id`. | -| `addr` | string | yes | Address of the validator's privval listener. Use `tcp://host:port` for the standard SecretConnection transport, or `noise://@host:port` for the libp2p Noise transport (see [libp2p Noise transport](#libp2p-noise-transport)). | -| `identity_key` | string | yes | Path to the Ed25519 identity key file used to authenticate the SecretConnection. Relative paths are resolved against `--home`. Use the file generated by `cometkms init`. | -| `reconnect` | bool | no | Whether to reconnect automatically after a dropped connection. Defaults to `true`. | - -### `[[providers.softsign]]` - -Binds a file-based Ed25519 private key to one or more chains. - -| Field | Type | Required | Description | -|---|---|---|---| -| `chain_ids` | list of strings | yes | Chain IDs this key is used to sign for. Each chain may only have one softsign provider. | -| `key_file` | string | yes | Path to the key file. Accepts either a CometBFT `priv_validator_key.json` (typed JSON with a `"priv_key"` field) or a file containing the raw base64-encoded 64-byte Ed25519 private key. | - -### `[[providers.pkcs11]]` - -Binds an Ed25519 key stored on a PKCS#11 token or HSM to one or more chains. The -private key never leaves the token: signing is performed on-device via `CKM_EDDSA`. - -| Field | Type | Required | Description | -|---|---|---|---| -| `chain_ids` | list of strings | yes | Chain IDs this key is used to sign for. Each chain may only have one backend (softsign *or* pkcs11). | -| `module` | string | yes | Path to the PKCS#11 module shared library (e.g. `/usr/lib/softhsm/libsofthsm2.so`). Relative paths are resolved against `--home`. | -| `token_label` | string | one of token_label/slot | `CKA_LABEL` of the token to use. | -| `slot` | integer | one of token_label/slot | Slot number of the token to use. Mutually exclusive with `token_label`. | -| `key_label` | string | at least one of key_label/key_id | `CKA_LABEL` of the key object. | -| `key_id` | string (hex) | at least one of key_label/key_id | Hex-encoded `CKA_ID` of the key object. | -| `pin` | string | exactly one PIN source | User PIN, inline. | -| `pin_env` | string | exactly one PIN source | Name of an environment variable holding the user PIN. Preferred over inline. | -| `pin_file` | string | exactly one PIN source | Path to a file containing the user PIN (trailing whitespace trimmed). Relative paths resolved against `--home`. | -| `algorithm` | string | no | Key algorithm. Defaults to `ed25519` (the only supported value today). | - -Provision the key with your HSM tooling before starting `cometkms`; the KMS only -*uses* an existing key, it does not generate or import keys. The key must be an -Ed25519 (`CKK_EC_EDWARDS`) signing key. Example using `pkcs11-tool` with SoftHSM2: - -```sh -softhsm2-util --init-token --free --label comet --pin 1234 --so-pin 4321 -pkcs11-tool --module /usr/lib/softhsm/libsofthsm2.so --login --pin 1234 \ - --keypairgen --key-type EC:edwards25519 --label validator --id 01 -``` - -Example provider block (PIN supplied via environment, keeping it out of the -config file): - -```toml -[[providers.pkcs11]] -chain_ids = ["cosmoshub-4"] -module = "/usr/lib/softhsm/libsofthsm2.so" -token_label = "comet" -key_label = "validator" -key_id = "01" -pin_env = "COMETKMS_PIN" -# algorithm defaults to "ed25519" -``` - ---- - -## Example config - -```toml -# ~/.cometkms/cometkms.toml - -[[chain]] -id = "cosmoshub-4" -# state_file defaults to /state/cosmoshub-4.json when omitted - -[[validator]] -chain_id = "cosmoshub-4" -addr = "tcp://10.0.0.1:26659" -identity_key = "identity.json" # relative to --home - -[[providers.softsign]] -chain_ids = ["cosmoshub-4"] -key_file = "/secrets/priv_validator_key.json" -``` - -Multi-chain example: - -```toml -[[chain]] -id = "cosmoshub-4" - -[[chain]] -id = "osmosis-1" - -[[validator]] -chain_id = "cosmoshub-4" -addr = "tcp://10.0.0.1:26659" -identity_key = "identity.json" - -[[validator]] -chain_id = "osmosis-1" -addr = "tcp://10.0.0.2:26659" -identity_key = "identity.json" - -[[providers.softsign]] -chain_ids = ["cosmoshub-4"] -key_file = "/secrets/cosmoshub_priv_validator_key.json" - -[[providers.softsign]] -chain_ids = ["osmosis-1"] -key_file = "/secrets/osmosis_priv_validator_key.json" -``` - ---- - -## libp2p Noise transport - -The libp2p Noise transport is an alternative to the default SecretConnection -(`tcp://`) channel. Both sides use the same TCP port and listener — the address -scheme (`noise://` vs `tcp://`) is what selects which handshake is performed. -No libp2p switch, host, or gossip network is involved; it is a direct TCP -connection secured by the [Noise_XX handshake](https://noiseprotocol.org/). - -The key difference from SecretConnection is **pinned peer IDs on both sides**. -SecretConnection uses an ephemeral, unpinned key on the validator's listener, -which means the KMS cannot verify it is talking to the right validator. With the -Noise transport, each side asserts a stable libp2p identity derived from its -existing keys (the validator's node key, the KMS's `identity.json`), and each -side refuses any connection from an unexpected peer. - -### Obtaining the two peer IDs - -**KMS peer ID** (give this to the validator operator so they can pin it in -`priv_validator_laddr`): - -```sh -cometkms peer-id --home ~/.cometkms -``` - -This reads `identity.json` from `` and prints the corresponding libp2p -peer ID. - -**Validator peer ID** (give this to the KMS operator so they can pin it in -`[[validator]].addr`): - -```sh -cometbft show-node-id --libp2p --home ~/.cometbft -``` - -This prints the libp2p peer ID derived from the validator's node key. - -### KMS configuration - -Set `addr` to a `noise://` URI that embeds the validator's peer ID: - -```toml -[[validator]] -chain_id = "cosmoshub-4" -addr = "noise://12D3KooW...validatorPeerID...@10.0.0.1:26659" -identity_key = "identity.json" # reused as the KMS's libp2p identity -``` - -The `identity_key` field serves double duty: it authenticates the -SecretConnection channel when `tcp://` is used, and it provides the KMS's -libp2p identity (peer ID) when `noise://` is used. No additional key file is -needed. - -### Validator configuration - -Set `priv_validator_laddr` in the validator's `config.toml` to a `noise://` -URI that embeds the KMS peer ID: - -```toml -priv_validator_laddr = "noise://12D3KooW...kmsPeerID...@0.0.0.0:26659" -``` - -The validator uses its **node key** (`node_key.json`) as its Noise identity. -Any incoming connection whose authenticated peer ID does not match the pinned -KMS peer ID is rejected before any signing request is served. - -### Mutual pinning requirement - -Both sides **must** configure the other's peer ID: - -- The KMS encodes the validator's peer ID in the `noise://` address it dials — - the handshake fails immediately if the remote key does not match. -- The validator encodes the KMS's peer ID in `priv_validator_laddr` — any - connection from a different peer is dropped. - -There is no way to disable peer-ID pinning when using `noise://`; it is -enforced unconditionally. - -### Key types - -Ed25519 and secp256k1 keys are supported. Consensus keys and node keys in -CometBFT are Ed25519, so no extra setup is needed. - - -## Security notes - -- **softsign is NOT for production custody.** The private key is loaded from - disk and held in process memory in plaintext for the lifetime of the process. - Use the `pkcs11` backend (or a future AWS KMS backend) for production - environments where the key must never leave secure hardware. -- **The identity key is not the consensus signing key.** `identity.json` - authenticates the SecretConnection channel; it does not sign consensus - messages and does not need to be protected to the same degree as the - `priv_validator_key.json`. -- **Double-sign protection is per state file.** The state file records the - highest height/round/step that has been signed. `cometkms` refuses to sign - any message that would regress this high-water mark. Never run two `cometkms` - instances against the same validator with different (or missing) state files — - doing so removes the double-sign protection. -- **Validator listener exposure.** The validator's `priv_validator_laddr` binds - a TCP port. Ensure it is not reachable from untrusted networks (use a firewall - or a private VLAN between the validator and the KMS host). - ---- - -## Testing - -From the repository root: - -```sh -make kms-test # go test ./... -count=1 -make kms-test-race # with the race detector -``` - ---- - -## Repository layout - -``` -kms/ -├── cmd/ -│ └── cometkms/ # Binary entrypoint; CLI subcommands (version, init, start, peer-id) -│ ├── main.go -│ └── main_test.go -└── internal/ - ├── version/ # Version string; overridable at link time via -ldflags - ├── config/ # TOML config types (config.go) and validation (validate.go) - ├── identity/ # Identity key load/generate (wraps CometBFT p2p.NodeKey) - ├── backend/ # backend.Signer interface - │ ├── softsign/ # File-based Ed25519 backend - │ └── pkcs11/ # PKCS#11 / HSM Ed25519 backend (+ pkcs11test helpers) - ├── signer/ # ChainSigner: double-sign protection + PrivValidator impl - │ ├── chain_signer.go - │ └── privkey_adapter.go - ├── manager/ # Manager: supervised dial-out connections with backoff - │ ├── manager.go - │ └── dialer.go - └── app/ # Wiring: Build() assembles Manager from a validated Config -``` diff --git a/kms/cmd/cometkms/main.go b/kms/cmd/cometkms/main.go deleted file mode 100644 index e690b10fa67..00000000000 --- a/kms/cmd/cometkms/main.go +++ /dev/null @@ -1,167 +0,0 @@ -// Command cometkms is an external remote signer for CometBFT validators. -package main - -import ( - "fmt" - "os" - "os/signal" - "path/filepath" - "syscall" - - "github.com/cometbft/cometbft/libs/log" - "github.com/cometbft/cometbft/lp2p" - "github.com/spf13/cobra" - - "github.com/cometbft/cometbft/kms/internal/app" - "github.com/cometbft/cometbft/kms/internal/config" - "github.com/cometbft/cometbft/kms/internal/identity" - "github.com/cometbft/cometbft/kms/internal/version" -) - -const defaultConfigTemplate = `# cometkms configuration - -[[chain]] -id = "my-chain-1" -# state_file defaults to /state/.json if omitted - -[[validator]] -chain_id = "my-chain-1" -addr = "tcp://127.0.0.1:26659" -identity_key = "identity.json" - -[[providers.softsign]] -chain_ids = ["my-chain-1"] -key_file = "priv_validator_key.json" - -# To sign from a PKCS#11 token / HSM instead of softsign, replace the -# [[providers.softsign]] block above with a pkcs11 provider: -# -# [[providers.pkcs11]] -# chain_ids = ["my-chain-1"] -# module = "/usr/lib/softhsm/libsofthsm2.so" -# token_label = "comet" -# key_label = "validator" -# pin_env = "COMETKMS_PIN" -` - -// home is the home directory of cometkms -var home string - -func main() { - if err := rootCmd().Execute(); err != nil { - fmt.Fprintln(os.Stderr, "error:", err) - os.Exit(1) - } -} - -func rootCmd() *cobra.Command { - root := &cobra.Command{Use: "cometkms", Short: "External remote signer for CometBFT validators"} - root.PersistentFlags().StringVar(&home, "home", ".", "the home directory of cometkms") - root.AddCommand(versionCmd(), initCmd(), startCmd(), peerIDCmd()) - return root -} - -func peerIDFromIdentity(path string) (string, error) { - key, err := identity.LoadOrGen(path) - if err != nil { - return "", err - } - id, err := lp2p.IDFromPrivateKey(key) - if err != nil { - return "", err - } - return id.String(), nil -} - -func peerIDCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "peer-id", - Short: "Print the libp2p peer ID of the KMS identity key (for the validator's noise allowlist)", - RunE: func(_ *cobra.Command, _ []string) error { - id, err := peerIDFromIdentity(filepath.Join(home, "identity.json")) - if err != nil { - return err - } - fmt.Println(id) - return nil - }, - } - return cmd -} - -func versionCmd() *cobra.Command { - return &cobra.Command{ - Use: "version", - Short: "Print the cometkms version", - RunE: func(_ *cobra.Command, _ []string) error { - fmt.Println(version.String()) - return nil - }, - } -} - -func initCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "init", - Short: "Scaffold a config file and generate an identity key", - RunE: func(_ *cobra.Command, _ []string) error { return runInit(home) }, - } - return cmd -} - -func runInit(home string) error { - if err := os.MkdirAll(home, 0o700); err != nil { - return err - } - path := cfgPath(home) - if _, err := os.Stat(path); os.IsNotExist(err) { - if err := os.WriteFile(path, []byte(defaultConfigTemplate), 0o600); err != nil { - return err - } - } - if _, err := identity.LoadOrGen(filepath.Join(home, "identity.json")); err != nil { - return err - } - fmt.Printf("initialized cometkms in %s\n", home) - return nil -} - -func startCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "start", - Short: "Connect to validators and serve signing requests", - RunE: func(_ *cobra.Command, _ []string) error { - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - - cfg, err := config.Load(cfgPath(home)) - if err != nil { - return err - } - if err := cfg.Validate(home); err != nil { - return err - } - - mgr, cleanup, err := app.Build(cfg, logger) - if err != nil { - return err - } - defer cleanup() - if err := mgr.Start(); err != nil { - return err - } - defer mgr.Stop() - - logger.Info("cometkms started; press Ctrl-C to stop") - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) - <-sig - logger.Info("cometkms shutting down") - return nil - }, - } - return cmd -} - -func cfgPath(home string) string { - return filepath.Join(home, "cometkms.toml") -} diff --git a/kms/cmd/cometkms/main_test.go b/kms/cmd/cometkms/main_test.go deleted file mode 100644 index 0e418b702ef..00000000000 --- a/kms/cmd/cometkms/main_test.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestInitCreatesConfigAndIdentity(t *testing.T) { - home := t.TempDir() - require.NoError(t, runInit(home)) - - _, err := os.Stat(filepath.Join(home, "cometkms.toml")) - require.NoError(t, err) - _, err = os.Stat(filepath.Join(home, "identity.json")) - require.NoError(t, err) -} - -func TestPeerIDFromIdentity(t *testing.T) { - home := t.TempDir() - require.NoError(t, runInit(home)) - id, err := peerIDFromIdentity(filepath.Join(home, "identity.json")) - require.NoError(t, err) - require.NotEmpty(t, id) -} diff --git a/kms/go.mod b/kms/go.mod deleted file mode 100644 index 6d0ddaacdc2..00000000000 --- a/kms/go.mod +++ /dev/null @@ -1,149 +0,0 @@ -module github.com/cometbft/cometbft/kms - -go 1.25.0 - -replace github.com/cometbft/cometbft => ../ - -require ( - github.com/BurntSushi/toml v1.6.0 - github.com/cometbft/cometbft v0.0.0 - github.com/libp2p/go-libp2p v0.47.0 - github.com/miekg/pkcs11 v1.1.2 - github.com/spf13/cobra v1.10.2 - github.com/stretchr/testify v1.11.1 -) - -require ( - github.com/DataDog/zstd v1.5.7 // indirect - github.com/benbjohnson/clock v1.3.5 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/cloudflare/circl v1.6.3 // indirect - github.com/cockroachdb/errors v1.11.3 // indirect - github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce // indirect - github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect - github.com/cockroachdb/pebble v1.1.1 // indirect - github.com/cockroachdb/redact v1.1.5 // indirect - github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect - github.com/cometbft/cometbft-db v0.14.1 // indirect - github.com/cosmos/gogoproto v1.7.2 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect - github.com/dgraph-io/badger/v4 v4.2.0 // indirect - github.com/dgraph-io/ristretto v0.1.1 // indirect - github.com/dunglas/httpsfv v1.1.0 // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/flynn/noise v1.1.0 // indirect - github.com/getsentry/sentry-go v0.27.0 // indirect - github.com/go-kit/kit v0.13.0 // indirect - github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.6.1 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v1.2.5 // indirect - github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.1.3 // indirect - github.com/google/flatbuffers v1.12.1 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - github.com/huin/goupnp v1.3.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/ipfs/go-cid v0.5.0 // indirect - github.com/jackpal/go-nat-pmp v1.0.2 // indirect - github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect - github.com/jmhodges/levigo v1.0.0 // indirect - github.com/klauspost/compress v1.18.0 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/koron/go-ssdp v0.0.6 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect - github.com/libp2p/go-buffer-pool v0.1.0 // indirect - github.com/libp2p/go-flow-metrics v0.2.0 // indirect - github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-netroute v0.3.0 // indirect - github.com/libp2p/go-reuseport v0.4.0 // indirect - github.com/libp2p/go-yamux/v5 v5.0.1 // indirect - github.com/linxGnu/grocksdb v1.8.14 // indirect - github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect - github.com/miekg/dns v1.1.66 // indirect - github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect - github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect - github.com/minio/sha256-simd v1.0.1 // indirect - github.com/mr-tron/base58 v1.3.0 // indirect - github.com/multiformats/go-base32 v0.1.0 // indirect - github.com/multiformats/go-base36 v0.2.0 // indirect - github.com/multiformats/go-multiaddr v0.16.1 // indirect - github.com/multiformats/go-multiaddr-dns v0.4.1 // indirect - github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect - github.com/multiformats/go-multibase v0.2.0 // indirect - github.com/multiformats/go-multicodec v0.9.1 // indirect - github.com/multiformats/go-multihash v0.2.3 // indirect - github.com/multiformats/go-multistream v0.6.1 // indirect - github.com/multiformats/go-varint v0.0.7 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a // indirect - github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe // indirect - github.com/pion/datachannel v1.5.10 // indirect - github.com/pion/dtls/v2 v2.2.12 // indirect - github.com/pion/dtls/v3 v3.0.11 // indirect - github.com/pion/ice/v4 v4.0.10 // indirect - github.com/pion/interceptor v0.1.40 // indirect - github.com/pion/logging v0.2.4 // indirect - github.com/pion/mdns/v2 v2.0.7 // indirect - github.com/pion/randutil v0.1.0 // indirect - github.com/pion/rtcp v1.2.15 // indirect - github.com/pion/rtp v1.8.19 // indirect - github.com/pion/sctp v1.8.39 // indirect - github.com/pion/sdp/v3 v3.0.13 // indirect - github.com/pion/srtp/v3 v3.0.6 // indirect - github.com/pion/stun v0.6.1 // indirect - github.com/pion/stun/v3 v3.0.0 // indirect - github.com/pion/transport/v2 v2.2.10 // indirect - github.com/pion/transport/v3 v3.0.7 // indirect - github.com/pion/transport/v4 v4.0.1 // indirect - github.com/pion/turn/v4 v4.0.2 // indirect - github.com/pion/webrtc/v4 v4.1.2 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.68.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect - github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.59.1 // indirect - github.com/quic-go/webtransport-go v0.10.0 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/sasha-s/go-deadlock v0.3.9 // indirect - github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/supranational/blst v0.3.16 // indirect - github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect - github.com/wlynxg/anet v0.0.5 // indirect - go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 // indirect - go.opencensus.io v0.24.0 // indirect - go.uber.org/dig v1.19.0 // indirect - go.uber.org/fx v1.24.0 // indirect - go.uber.org/mock v0.5.2 // indirect - go.uber.org/multierr v1.11.0 // indirect - go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.52.0 // indirect - golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/mod v0.35.0 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.45.0 // indirect - golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa // indirect - golang.org/x/text v0.37.0 // indirect - golang.org/x/time v0.12.0 // indirect - golang.org/x/tools v0.44.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/grpc v1.81.1 // indirect - google.golang.org/protobuf v1.36.11 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.4.1 // indirect -) diff --git a/kms/go.sum b/kms/go.sum deleted file mode 100644 index 1a181c38a29..00000000000 --- a/kms/go.sum +++ /dev/null @@ -1,536 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE= -github.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= -github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= -github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= -github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/btcsuite/btcd/btcutil v1.2.0 h1:p3+S2g3Q+7G5NOh4Ji+2UrBOrg5Z0Q4ykzShWG1Dhgs= -github.com/btcsuite/btcd/btcutil v1.2.0/go.mod h1:/Taflm113pYjUpbWKKQEfa6XOtI/+WS8awxeMZpY75k= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= -github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= -github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= -github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= -github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= -github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= -github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v1.1.1 h1:XnKU22oiCLy2Xn8vp1re67cXg4SAasg/WDt1NtcRFaw= -github.com/cockroachdb/pebble v1.1.1/go.mod h1:4exszw1r40423ZsmkG/09AFEG83I0uDgfujJdbL6kYU= -github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= -github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/cometbft/cometbft-db v0.14.1 h1:SxoamPghqICBAIcGpleHbmoPqy+crij/++eZz3DlerQ= -github.com/cometbft/cometbft-db v0.14.1/go.mod h1:KHP1YghilyGV/xjD5DP3+2hyigWx0WTp9X+0Gnx0RxQ= -github.com/cosmos/gogoproto v1.7.2 h1:5G25McIraOC0mRFv9TVO139Uh3OklV2hczr13KKVHCA= -github.com/cosmos/gogoproto v1.7.2/go.mod h1:8S7w53P1Y1cHwND64o0BnArT6RmdgIvsBuco6uTllsk= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/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/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c h1:pFUpOrbxDR6AkioZ1ySsx5yxlDQZ8stG2b88gTPxgJU= -github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c/go.mod h1:6UhI8N9EjYm1c2odKpFpAYeR8dsBeM7PtzQhRgxRr9U= -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.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= -github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= -github.com/dgraph-io/badger/v4 v4.2.0/go.mod h1:qfCqhPoWDFJRx1gp5QwwyGo8xk1lbHUxvK9nK0OGAak= -github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= -github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= -github.com/dunglas/httpsfv v1.1.0 h1:Jw76nAyKWKZKFrpMMcL76y35tOpYHqQPzHQiwDvpe54= -github.com/dunglas/httpsfv v1.1.0/go.mod h1:zID2mqw9mFsnt7YC3vYQ9/cjq30q41W+1AnDwH8TiMg= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -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/flynn/noise v1.1.0 h1:KjPQoQCEFdZDiP03phOvGi11+SVVhBG2wOWAorLsstg= -github.com/flynn/noise v1.1.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= -github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= -github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= -github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-kit/kit v0.13.0 h1:OoneCcHKHQ03LfBpoQCUfCluwd2Vt3ohz+kvbJneZAU= -github.com/go-kit/kit v0.13.0/go.mod h1:phqEHMMUbyrCFCTgH48JueqrM3md2HcAZ8N3XE4FKDg= -github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= -github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-logfmt/logfmt v0.6.1 h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE= -github.com/go-logfmt/logfmt v0.6.1/go.mod h1:EV2pOAQoZaT1ZXZbqDl5hrymndi4SY9ED9/z6CO0XAk= -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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I= -github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= -github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= -github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= -github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= -github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -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= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/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/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= -github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/ipfs/go-cid v0.5.0 h1:goEKKhaGm0ul11IHA7I6p1GmKz8kEYniqFopaB5Otwg= -github.com/ipfs/go-cid v0.5.0/go.mod h1:0L7vmeNXpQpUS9vt+yEARkJ8rOg43DF3iPgn4GIN0mk= -github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= -github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/jbenet/go-temp-err-catcher v0.1.0 h1:zpb3ZH6wIE8Shj2sKS+khgRvf7T7RABoLk/+KKHggpk= -github.com/jbenet/go-temp-err-catcher v0.1.0/go.mod h1:0kJRvmDZXNMIiJirNPEYfhpPwbGVtZVWC34vc5WLsDk= -github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= -github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= -github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -github.com/koron/go-ssdp v0.0.6 h1:Jb0h04599eq/CY7rB5YEqPS83HmRfHP2azkxMN2rFtU= -github.com/koron/go-ssdp v0.0.6/go.mod h1:0R9LfRJGek1zWTjN3JUNlm5INCDYGpRDfAptnct63fI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= -github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= -github.com/libp2p/go-flow-metrics v0.2.0 h1:EIZzjmeOE6c8Dav0sNv35vhZxATIXWZg6j/C08XmmDw= -github.com/libp2p/go-flow-metrics v0.2.0/go.mod h1:st3qqfu8+pMfh+9Mzqb2GTiwrAGjIPszEjZmtksN8Jc= -github.com/libp2p/go-libp2p v0.47.0 h1:qQpBjSCWNQFF0hjBbKirMXE9RHLtSuzTDkTfr1rw0yc= -github.com/libp2p/go-libp2p v0.47.0/go.mod h1:s8HPh7mMV933OtXzONaGFseCg/BE//m1V34p3x4EUOY= -github.com/libp2p/go-libp2p-asn-util v0.4.1 h1:xqL7++IKD9TBFMgnLPZR6/6iYhawHKHl950SO9L6n94= -github.com/libp2p/go-libp2p-asn-util v0.4.1/go.mod h1:d/NI6XZ9qxw67b4e+NgpQexCIiFYJjErASrYW4PFDN8= -github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= -github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= -github.com/libp2p/go-msgio v0.3.0 h1:mf3Z8B1xcFN314sWX+2vOTShIE0Mmn2TXn3YCUQGNj0= -github.com/libp2p/go-msgio v0.3.0/go.mod h1:nyRM819GmVaF9LX3l03RMh10QdOroF++NBbxAb0mmDM= -github.com/libp2p/go-netroute v0.3.0 h1:nqPCXHmeNmgTJnktosJ/sIef9hvwYCrsLxXmfNks/oc= -github.com/libp2p/go-netroute v0.3.0/go.mod h1:Nkd5ShYgSMS5MUKy/MU2T57xFoOKvvLR92Lic48LEyA= -github.com/libp2p/go-reuseport v0.4.0 h1:nR5KU7hD0WxXCJbmw7r2rhRYruNRl2koHw8fQscQm2s= -github.com/libp2p/go-reuseport v0.4.0/go.mod h1:ZtI03j/wO5hZVDFo2jKywN6bYKWLOy8Se6DrI2E1cLU= -github.com/libp2p/go-yamux/v5 v5.0.1 h1:f0WoX/bEF2E8SbE4c/k1Mo+/9z0O4oC/hWEA+nfYRSg= -github.com/libp2p/go-yamux/v5 v5.0.1/go.mod h1:en+3cdX51U0ZslwRdRLrvQsdayFt3TSUKvBGErzpWbU= -github.com/linxGnu/grocksdb v1.8.14 h1:HTgyYalNwBSG/1qCQUIott44wU5b2Y9Kr3z7SK5OfGQ= -github.com/linxGnu/grocksdb v1.8.14/go.mod h1:QYiYypR2d4v63Wj1adOOfzglnoII0gLj3PNh4fZkcFA= -github.com/marcopolo/simnet v0.0.4 h1:50Kx4hS9kFGSRIbrt9xUS3NJX33EyPqHVmpXvaKLqrY= -github.com/marcopolo/simnet v0.0.4/go.mod h1:tfQF1u2DmaB6WHODMtQaLtClEf3a296CKQLq5gAsIS0= -github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd h1:br0buuQ854V8u83wA0rVZ8ttrq5CpaPZdvrK0LP2lOk= -github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd/go.mod h1:QuCEs1Nt24+FYQEqAAncTDPJIuGs+LxK1MCiFL25pMU= -github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= -github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= -github.com/miekg/pkcs11 v1.1.2 h1:/VxmeAX5qU6Q3EwafypogwWbYryHFmF2RpkJmw3m4MQ= -github.com/miekg/pkcs11 v1.1.2/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= -github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= -github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c/go.mod h1:0SQS9kMwD2VsyFEB++InYyBJroV/FRmBgcydeSUcJms= -github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b h1:z78hV3sbSMAUoyUMM0I83AUIT6Hu17AWfgjzIbtrYFc= -github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b/go.mod h1:lxPUiZwKoFL8DUUmalo2yJJUCxbPKtm8OKfqr2/FTNU= -github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc h1:PTfri+PuQmWDqERdnNMiD9ZejrlswWrCpBEZgWOiTrc= -github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc/go.mod h1:cGKTAVKx4SxOuR/czcZ/E2RSJ3sfHs8FpHhQ5CWMf9s= -github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1/go.mod h1:pD8RvIylQ358TN4wwqatJ8rNavkEINozVn9DtGI3dfQ= -github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= -github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= -github.com/mr-tron/base58 v1.1.2/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= -github.com/mr-tron/base58 v1.3.0 h1:K6Y13R2h+dku0wOqKtecgRnBUBPrZzLZy5aIj8lCcJI= -github.com/mr-tron/base58 v1.3.0/go.mod h1:2BuubE67DCSWwVfx37JWNG8emOC0sHEU4/HpcYgCLX8= -github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE= -github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= -github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= -github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= -github.com/multiformats/go-multiaddr v0.1.1/go.mod h1:aMKBKNEYmzmDmxfX88/vz+J5IU55txyt0p4aiWVohjo= -github.com/multiformats/go-multiaddr v0.16.1 h1:fgJ0Pitow+wWXzN9do+1b8Pyjmo8m5WhGfzpL82MpCw= -github.com/multiformats/go-multiaddr v0.16.1/go.mod h1:JSVUmXDjsVFiW7RjIFMP7+Ev+h1DTbiJgVeTV/tcmP0= -github.com/multiformats/go-multiaddr-dns v0.4.1 h1:whi/uCLbDS3mSEUMb1MsoT4uzUeZB0N32yzufqS0i5M= -github.com/multiformats/go-multiaddr-dns v0.4.1/go.mod h1:7hfthtB4E4pQwirrz+J0CcDUfbWzTqEzVyYKKIKpgkc= -github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= -github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= -github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= -github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= -github.com/multiformats/go-multicodec v0.9.1 h1:x/Fuxr7ZuR4jJV4Os5g444F7xC4XmyUaT/FWtE+9Zjo= -github.com/multiformats/go-multicodec v0.9.1/go.mod h1:LLWNMtyV5ithSBUo3vFIMaeDy+h3EbkMTek1m+Fybbo= -github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= -github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= -github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= -github.com/multiformats/go-multistream v0.6.1 h1:4aoX5v6T+yWmc2raBHsTvzmFhOI8WVOer28DeBBEYdQ= -github.com/multiformats/go-multistream v0.6.1/go.mod h1:ksQf6kqHAb6zIsyw7Zm+gAuVo57Qbq84E27YlYqavqw= -github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= -github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= -github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a h1:dlRvE5fWabOchtH7znfiFCcOvmIYgOeAS5ifBXBlh9Q= -github.com/oasisprotocol/curve25519-voi v0.0.0-20230904125328-1f23a7beb09a/go.mod h1:hVoHR2EVESiICEMbg137etN/Lx+lSrHPTD39Z/uE+2s= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= -github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= -github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe h1:vHpqOnPlnkba8iSxU4j/CvDSS9J4+F4473esQsYLGoE= -github.com/petermattis/goid v0.0.0-20250813065127-a731cc31b4fe/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= -github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= -github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= -github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= -github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk= -github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -github.com/pion/dtls/v3 v3.0.11 h1:zqn8YhoAU7d9whsWLhNiQlbB8QdpJj8XQVSc5ImUons= -github.com/pion/dtls/v3 v3.0.11/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8= -github.com/pion/ice/v4 v4.0.10 h1:P59w1iauC/wPk9PdY8Vjl4fOFL5B+USq1+xbDcN6gT4= -github.com/pion/ice/v4 v4.0.10/go.mod h1:y3M18aPhIxLlcO/4dn9X8LzLLSma84cx6emMSu14FGw= -github.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4= -github.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic= -github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= -github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= -github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= -github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= -github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= -github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= -github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo= -github.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0= -github.com/pion/rtp v1.8.19 h1:jhdO/3XhL/aKm/wARFVmvTfq0lC/CvN1xwYKmduly3c= -github.com/pion/rtp v1.8.19/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk= -github.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE= -github.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE= -github.com/pion/sdp/v3 v3.0.13 h1:uN3SS2b+QDZnWXgdr69SM8KB4EbcnPnPf2Laxhty/l4= -github.com/pion/sdp/v3 v3.0.13/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E= -github.com/pion/srtp/v3 v3.0.6 h1:E2gyj1f5X10sB/qILUGIkL4C2CqK269Xq167PbGCc/4= -github.com/pion/srtp/v3 v3.0.6/go.mod h1:BxvziG3v/armJHAaJ87euvkhHqWe9I7iiOy50K2QkhY= -github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= -github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= -github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= -github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v2 v2.2.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q= -github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E= -github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= -github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= -github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o= -github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= -github.com/pion/turn/v4 v4.0.2 h1:ZqgQ3+MjP32ug30xAbD6Mn+/K4Sxi3SdNOTFf+7mpps= -github.com/pion/turn/v4 v4.0.2/go.mod h1:pMMKP/ieNAG/fN5cZiN4SDuyKsXtNTr0ccN7IToA1zs= -github.com/pion/webrtc/v4 v4.1.2 h1:mpuUo/EJ1zMNKGE79fAdYNFZBX790KE7kQQpLMjjR54= -github.com/pion/webrtc/v4 v4.1.2/go.mod h1:xsCXiNAmMEjIdFxAYU0MbB3RwRieJsegSB2JZsGN+8U= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -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/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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY= -github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= -github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= -github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic= -github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/quic-go/webtransport-go v0.10.0 h1:LqXXPOXuETY5Xe8ITdGisBzTYmUOy5eSj+9n4hLTjHI= -github.com/quic-go/webtransport-go v0.10.0/go.mod h1:LeGIXr5BQKE3UsynwVBeQrU1TPrbh73MGoC6jd+V7ow= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sasha-s/go-deadlock v0.3.9 h1:fiaT9rB7g5sr5ddNZvlwheclN9IP86eFW9WgqlEQV+w= -github.com/sasha-s/go-deadlock v0.3.9/go.mod h1:KuZj51ZFmx42q/mPaYbRk0P1xcwe697zsJKE03vD4/Y= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -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/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= -github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= -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/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= -github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5 h1:qxen9oVGzDdIRP6ejyAJc760RwW4SnVDiTYTzwnXuxo= -go.etcd.io/bbolt v1.4.0-alpha.0.0.20240404170359-43604f3112c5/go.mod h1:eW0HG9/oHQhvRCvb1/pIXW4cOvtDqeQK+XSi3TnwaXY= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -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/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= -go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= -go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= -go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= -go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= -go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= -go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= -go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= -go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= -go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4= -go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= -go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg= -go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo= -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/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= -go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= -golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= -golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= -golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -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-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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/telemetry v0.0.0-20260409153401-be6f6cb8b1fa h1:efT73AJZfAAUV7SOip6pWGkwJDzIGiKBZGVzHYa+ve4= -golang.org/x/telemetry v0.0.0-20260409153401-be6f6cb8b1fa/go.mod h1:kHjTxDEnAu6/Nl9lDkzjWpR+bmKfxeiRuSDlsMb70gE= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= -gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ= -google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I= -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= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -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/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.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.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.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= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg= -lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo= diff --git a/kms/internal/app/build.go b/kms/internal/app/build.go deleted file mode 100644 index 0d154e889ff..00000000000 --- a/kms/internal/app/build.go +++ /dev/null @@ -1,135 +0,0 @@ -// Package app wires a validated config into a runnable manager. -package app - -import ( - "encoding/hex" - "fmt" - "io" - - "github.com/cometbft/cometbft/libs/log" - - "github.com/cometbft/cometbft/kms/internal/backend" - "github.com/cometbft/cometbft/kms/internal/backend/pkcs11" - "github.com/cometbft/cometbft/kms/internal/backend/softsign" - "github.com/cometbft/cometbft/kms/internal/config" - "github.com/cometbft/cometbft/kms/internal/identity" - "github.com/cometbft/cometbft/kms/internal/manager" - "github.com/cometbft/cometbft/kms/internal/signer" - "github.com/cometbft/cometbft/kms/internal/transport" -) - -// Build constructs a Manager from a validated config. The returned cleanup -// function releases backend resources (e.g. PKCS#11 sessions) and must be called -// on shutdown. cleanup is non-nil even when an error is returned, so callers can -// always defer it. -func Build(c *config.Config, logger log.Logger) (mgr *manager.Manager, cleanup func(), err error) { - // Backends that hold OS/HSM resources are closed by cleanup. - var closers []io.Closer - cleanup = func() { - for _, cl := range closers { - _ = cl.Close() - } - } - // On error, release anything already opened before returning. - defer func() { - if err != nil { - cleanup() - } - }() - - // chainID -> backend (one backend per chain). - backends := map[string]backend.Signer{} - for _, p := range c.Providers.Softsign { - s, lerr := softsign.Load(p.KeyFile) - if lerr != nil { - return nil, cleanup, lerr - } - for _, id := range p.ChainIDs { - if _, dup := backends[id]; dup { - return nil, cleanup, fmt.Errorf("app: multiple backends bound to chain %q", id) - } - backends[id] = s - } - } - - for _, p := range c.Providers.PKCS11 { - var keyID []byte - if p.KeyID != "" { - keyID, err = hex.DecodeString(p.KeyID) - if err != nil { - return nil, cleanup, fmt.Errorf("app: pkcs11 provider key_id %q: %w", p.KeyID, err) - } - } - s, oerr := pkcs11.Open(pkcs11.Config{ - Module: p.Module, - TokenLabel: p.TokenLabel, - Slot: p.Slot, - KeyLabel: p.KeyLabel, - KeyID: keyID, - PIN: p.PIN, - PINEnv: p.PINEnv, - PINFile: p.PINFile, - Algorithm: p.Algorithm, - }) - if oerr != nil { - return nil, cleanup, oerr - } - closers = append(closers, s) - for _, id := range p.ChainIDs { - if _, dup := backends[id]; dup { - return nil, cleanup, fmt.Errorf("app: multiple backends bound to chain %q", id) - } - backends[id] = s - } - } - - // chainID -> state file. - stateFiles := map[string]string{} - for _, ch := range c.Chains { - stateFiles[ch.ID] = ch.StateFile - } - - // chainID -> *ChainSigner. - signers := map[string]*signer.ChainSigner{} - for id, be := range backends { - cs, cerr := signer.NewChainSigner(id, be, stateFiles[id]) - if cerr != nil { - return nil, cleanup, cerr - } - signers[id] = cs - } - - // One ValidatorConn per [[validator]]; validators of a chain share its signer. - var conns []manager.ValidatorConn - for _, v := range c.Validators { - cs, ok := signers[v.ChainID] - if !ok { - return nil, cleanup, fmt.Errorf("app: chain %q has no backend", v.ChainID) - } - idKey, lerr := identity.LoadOrGen(v.IdentityKey) - if lerr != nil { - return nil, cleanup, lerr - } - tr, addr, validatorPeer, perr := v.ParsedTransport() - if perr != nil { - return nil, cleanup, perr - } - vc := manager.ValidatorConn{ - ChainID: v.ChainID, - Addr: v.Addr, - IdentityKey: idKey, - Signer: cs, - Reconnect: v.ReconnectEnabled(), - } - if tr == config.TransportNoise { - d, derr := transport.NoiseDialer(addr, idKey, validatorPeer, manager.DefaultDialTimeout) - if derr != nil { - return nil, cleanup, derr - } - vc.Dialer = d - } - conns = append(conns, vc) - } - - return manager.New(logger, conns), cleanup, nil -} diff --git a/kms/internal/app/build_pkcs11_test.go b/kms/internal/app/build_pkcs11_test.go deleted file mode 100644 index edc27e6409a..00000000000 --- a/kms/internal/app/build_pkcs11_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package app_test - -import ( - "testing" - - "github.com/cometbft/cometbft/libs/log" - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/kms/internal/app" - "github.com/cometbft/cometbft/kms/internal/backend/pkcs11/pkcs11test" - "github.com/cometbft/cometbft/kms/internal/config" -) - -// TestBuildWiresPKCS11Backend verifies a [[providers.pkcs11]] block is opened and -// wired into the manager, and that cleanup releases the HSM session. -func TestBuildWiresPKCS11Backend(t *testing.T) { - module := pkcs11test.FindModule(t) - pkcs11test.SetupToken(t, module) - home := t.TempDir() - - c := &config.Config{ - Chains: []config.Chain{{ID: "c1"}}, - Validators: []config.Validator{{ChainID: "c1", Addr: "tcp://127.0.0.1:1", IdentityKey: home + "/identity.json"}}, - Providers: config.Providers{PKCS11: []config.PKCS11Provider{{ - ChainIDs: []string{"c1"}, - Module: module, - TokenLabel: pkcs11test.TokenLabel, - KeyLabel: pkcs11test.KeyLabel, - PIN: pkcs11test.UserPIN, - }}}, - } - require.NoError(t, c.Validate(home)) - - mgr, cleanup, err := app.Build(c, log.TestingLogger()) - require.NoError(t, err) - t.Cleanup(cleanup) // releases the PKCS#11 session even if assertions below fail - require.NotNil(t, mgr) -} diff --git a/kms/internal/app/build_test.go b/kms/internal/app/build_test.go deleted file mode 100644 index b6bf860b1c5..00000000000 --- a/kms/internal/app/build_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package app_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/cometbft/cometbft/crypto/ed25519" - cmtjson "github.com/cometbft/cometbft/libs/json" - "github.com/cometbft/cometbft/libs/log" - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/kms/internal/app" - "github.com/cometbft/cometbft/kms/internal/config" -) - -func TestBuildWiresChainSigners(t *testing.T) { - home := t.TempDir() - - keyPath := filepath.Join(home, "key.json") - raw, err := cmtjson.MarshalIndent(struct { - PrivKey ed25519.PrivKey `json:"priv_key"` - }{PrivKey: ed25519.GenPrivKey()}, "", " ") - require.NoError(t, err) - require.NoError(t, os.WriteFile(keyPath, raw, 0o600)) - - identPath := filepath.Join(home, "identity.json") - - c := &config.Config{ - Chains: []config.Chain{{ID: "c1"}}, - Validators: []config.Validator{{ChainID: "c1", Addr: "tcp://127.0.0.1:1", IdentityKey: identPath}}, - Providers: config.Providers{Softsign: []config.SoftsignProvider{{ChainIDs: []string{"c1"}, KeyFile: keyPath}}}, - } - require.NoError(t, c.Validate(home)) - - mgr, cleanup, err := app.Build(c, log.TestingLogger()) - require.NoError(t, err) - t.Cleanup(cleanup) - require.NotNil(t, mgr) -} - -func TestBuildFailsOnMissingKeyFile(t *testing.T) { - home := t.TempDir() - c := &config.Config{ - Chains: []config.Chain{{ID: "c1"}}, - Validators: []config.Validator{{ChainID: "c1", Addr: "tcp://127.0.0.1:1", IdentityKey: filepath.Join(home, "id.json")}}, - Providers: config.Providers{Softsign: []config.SoftsignProvider{{ChainIDs: []string{"c1"}, KeyFile: filepath.Join(home, "missing.json")}}}, - } - require.NoError(t, c.Validate(home)) - _, cleanup, err := app.Build(c, log.TestingLogger()) - t.Cleanup(cleanup) - require.Error(t, err) -} diff --git a/kms/internal/app/integration_test.go b/kms/internal/app/integration_test.go deleted file mode 100644 index 3093620085b..00000000000 --- a/kms/internal/app/integration_test.go +++ /dev/null @@ -1,350 +0,0 @@ -package app_test - -import ( - "context" - "errors" - "net" - "os" - "path/filepath" - "testing" - "time" - - "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/crypto/ed25519" - cmtjson "github.com/cometbft/cometbft/libs/json" - "github.com/cometbft/cometbft/libs/log" - "github.com/cometbft/cometbft/lp2p" - "github.com/cometbft/cometbft/privval" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - "github.com/cometbft/cometbft/types" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/kms/internal/backend" - "github.com/cometbft/cometbft/kms/internal/backend/softsign" - "github.com/cometbft/cometbft/kms/internal/manager" - "github.com/cometbft/cometbft/kms/internal/signer" - "github.com/cometbft/cometbft/kms/internal/transport" -) - -// failingBackend is a backend.Signer that is reachable and exposes a real public -// key, but whose Sign always returns a fixed error. It simulates a signing -// backend (HSM, cloud KMS, ...) that is connected yet rejects the signing -// operation itself — as opposed to a network/connection failure. -type failingBackend struct { - pub crypto.PubKey - err error -} - -var _ backend.Signer = failingBackend{} - -func (b failingBackend) PubKey(context.Context) (crypto.PubKey, error) { return b.pub, nil } -func (b failingBackend) Sign(context.Context, []byte) ([]byte, error) { return nil, b.err } - -// writeKey writes a softsign key file and returns its path. -func writeKey(t *testing.T, dir string) string { - t.Helper() - raw, err := cmtjson.MarshalIndent(struct { - PrivKey ed25519.PrivKey `json:"priv_key"` - }{PrivKey: ed25519.GenPrivKey()}, "", " ") - require.NoError(t, err) - p := filepath.Join(dir, "key.json") - require.NoError(t, os.WriteFile(p, raw, 0o600)) - return p -} - -// startListener starts a cometbft validator-side signer listener on ln and -// returns the endpoint. The KMS dials into this. -func startListener(t *testing.T, logger log.Logger, ln net.Listener) *privval.SignerListenerEndpoint { - t.Helper() - ep := privval.NewSignerListenerEndpoint(logger, privval.NewTCPListener(ln, ed25519.GenPrivKey())) - require.NoError(t, ep.Start()) - return ep -} - -func TestEndToEndSigning(t *testing.T) { - const chainID = "integration-chain" - dir := t.TempDir() - logger := log.TestingLogger() - - // cometkms (signer) side. - be, err := softsign.Load(writeKey(t, dir)) - require.NoError(t, err) - cs, err := signer.NewChainSigner(chainID, be, filepath.Join(dir, "state.json")) - require.NoError(t, err) - - // validator (listener) side. - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - addr := ln.Addr().String() - listener := startListener(t, logger, ln) - defer func() { _ = listener.Stop() }() - - // cometkms Manager dials in. - mgr := manager.New(logger, []manager.ValidatorConn{{ - ChainID: chainID, - Addr: "tcp://" + addr, - IdentityKey: ed25519.GenPrivKey(), - Signer: cs, - Reconnect: true, - }}) - require.NoError(t, mgr.Start()) - defer mgr.Stop() - - // Drive requests as the validator would. - client, err := privval.NewSignerClient(listener, chainID) - require.NoError(t, err) - require.NoError(t, client.WaitForConnection(5*time.Second)) - - pub, err := client.GetPubKey() - require.NoError(t, err) - require.NotNil(t, pub) - - // Vote — signature must verify against canonical sign-bytes. - vote := &cmtproto.Vote{Type: cmtproto.PrevoteType, Height: 5, Round: 0} - require.NoError(t, client.SignVote(chainID, vote)) - require.True(t, pub.VerifySignature(types.VoteSignBytes(chainID, vote), vote.Signature)) - - // Proposal. - prop := &cmtproto.Proposal{Type: cmtproto.ProposalType, Height: 6, Round: 0} - require.NoError(t, client.SignProposal(chainID, prop)) - require.True(t, pub.VerifySignature(types.ProposalSignBytes(chainID, prop), prop.Signature)) - - // Double-sign: a lower height must be refused. - lower := &cmtproto.Vote{Type: cmtproto.PrevoteType, Height: 4, Round: 0} - require.Error(t, client.SignVote(chainID, lower)) -} - -// TestSigningBackendErrorReturnsEmbeddedError verifies that when the signing -// backend fails the signing operation, the validator-side client receives the -// backend's error *embedded* in the signed response (a RemoteSignerError) rather -// than a transport-level disconnect/connection drop, and that the connection -// survives the failure so subsequent requests still succeed. -func TestSigningBackendErrorReturnsEmbeddedError(t *testing.T) { - const chainID = "backend-error-chain" - dir := t.TempDir() - logger := log.TestingLogger() - - // cometkms (signer) side: a backend that is reachable (real pubkey) but whose - // Sign always fails. - wantErr := errors.New("backend signing unavailable") - be := failingBackend{pub: ed25519.GenPrivKey().PubKey(), err: wantErr} - cs, err := signer.NewChainSigner(chainID, be, filepath.Join(dir, "state.json")) - require.NoError(t, err) - - // validator (listener) side. - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - addr := ln.Addr().String() - listener := startListener(t, logger, ln) - defer func() { _ = listener.Stop() }() - - // cometkms Manager dials in. - mgr := manager.New(logger, []manager.ValidatorConn{{ - ChainID: chainID, - Addr: "tcp://" + addr, - IdentityKey: ed25519.GenPrivKey(), - Signer: cs, - Reconnect: true, - }}) - require.NoError(t, mgr.Start()) - defer mgr.Stop() - - client, err := privval.NewSignerClient(listener, chainID) - require.NoError(t, err) - require.NoError(t, client.WaitForConnection(5*time.Second)) - - // The pubkey path does not exercise the failing Sign, so it must succeed — - // confirming the connection is healthy before we trigger the signing error. - pub, err := client.GetPubKey() - require.NoError(t, err) - require.NotNil(t, pub) - - // Signing must fail with the backend's error embedded in the response. The KMS - // handler wraps the backend error in a RemoteSignerError and still replies on - // the same connection — so the client sees a *privval.RemoteSignerError, not an - // EOF/timeout/connection-drop error. - vote := &cmtproto.Vote{Type: cmtproto.PrevoteType, Height: 5, Round: 0} - err = client.SignVote(chainID, vote) - require.Error(t, err) - - var rse *privval.RemoteSignerError - require.ErrorAs(t, err, &rse, - "client should receive an embedded RemoteSignerError, not a connection drop (got %T: %v)", err, err) - require.Contains(t, rse.Description, wantErr.Error(), - "embedded error should carry the signing backend's failure message") - - // A proposal goes through the same embedded-error path. - prop := &cmtproto.Proposal{Type: cmtproto.ProposalType, Height: 6, Round: 0} - err = client.SignProposal(chainID, prop) - require.Error(t, err) - require.ErrorAs(t, err, &rse, - "client should receive an embedded RemoteSignerError for proposals too (got %T: %v)", err, err) - - // The signing failures must NOT have dropped the connection: a follow-up - // request on the same client still succeeds, proving the connection survived - // (no reconnect/EOF in between). - pub2, err := client.GetPubKey() - require.NoError(t, err, "connection should survive an embedded signing error") - require.True(t, pub2.Equals(pub)) -} - -func TestEndToEndSigningNoise(t *testing.T) { - const chainID = "noise-chain" - dir := t.TempDir() - logger := log.TestingLogger() - - // Keys: validator node key (server) and KMS identity key (client). - validatorKey := ed25519.GenPrivKey() - kmsKey := ed25519.GenPrivKey() - validatorPeer, err := lp2p.IDFromPrivateKey(validatorKey) - require.NoError(t, err) - kmsPeer, err := lp2p.IDFromPrivateKey(kmsKey) - require.NoError(t, err) - - // KMS signer (softsign + ChainSigner). - be, err := softsign.Load(writeKey(t, dir)) - require.NoError(t, err) - cs, err := signer.NewChainSigner(chainID, be, filepath.Join(dir, "state.json")) - require.NoError(t, err) - - // Validator side: a Noise listener (node-key identity, allowlisting the KMS - // peer) wired into the standard SignerListenerEndpoint. - tcpLn, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - nl, err := privval.NewNoiseListener(tcpLn, validatorKey, []peer.ID{kmsPeer}) - require.NoError(t, err) - listener := privval.NewSignerListenerEndpoint(logger, nl) - require.NoError(t, listener.Start()) - defer func() { _ = listener.Stop() }() - - // KMS dials in over Noise, pinning the validator peer. - dial, err := transport.NoiseDialer(tcpLn.Addr().String(), kmsKey, validatorPeer, 3*time.Second) - require.NoError(t, err) - mgr := manager.New(logger, []manager.ValidatorConn{{ - ChainID: chainID, - Addr: "noise://" + tcpLn.Addr().String(), - IdentityKey: kmsKey, - Signer: cs, - Reconnect: true, - Dialer: dial, - }}) - require.NoError(t, mgr.Start()) - defer mgr.Stop() - - client, err := privval.NewSignerClient(listener, chainID) - require.NoError(t, err) - require.NoError(t, client.WaitForConnection(5*time.Second)) - - pub, err := client.GetPubKey() - require.NoError(t, err) - - vote := &cmtproto.Vote{Type: cmtproto.PrevoteType, Height: 7, Round: 0} - require.NoError(t, client.SignVote(chainID, vote)) - require.True(t, pub.VerifySignature(types.VoteSignBytes(chainID, vote), vote.Signature)) - - prop := &cmtproto.Proposal{Type: cmtproto.ProposalType, Height: 8, Round: 0} - require.NoError(t, client.SignProposal(chainID, prop)) - require.True(t, pub.VerifySignature(types.ProposalSignBytes(chainID, prop), prop.Signature)) -} - -func TestReconnectAfterListenerRestart(t *testing.T) { - const chainID = "rc-chain" - dir := t.TempDir() - logger := log.TestingLogger() - - be, err := softsign.Load(writeKey(t, dir)) - require.NoError(t, err) - cs, err := signer.NewChainSigner(chainID, be, filepath.Join(dir, "state.json")) - require.NoError(t, err) - - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - addr := ln.Addr().String() - listener := startListener(t, logger, ln) - - mgr := manager.New(logger, []manager.ValidatorConn{{ - ChainID: chainID, Addr: "tcp://" + addr, IdentityKey: ed25519.GenPrivKey(), Signer: cs, Reconnect: true, - }}) - require.NoError(t, mgr.Start()) - defer mgr.Stop() - - client, err := privval.NewSignerClient(listener, chainID) - require.NoError(t, err) - require.NoError(t, client.WaitForConnection(5*time.Second)) - _, err = client.GetPubKey() - require.NoError(t, err) - - // Gracefully stop the validator side (sends FIN -> KMS sees EOF) and free the - // port. listener.Stop() closes the underlying net.Listener (ln), so we do not - // close ln again here. - require.NoError(t, listener.Stop()) - time.Sleep(100 * time.Millisecond) - - // Bring the validator back on the SAME address; the KMS must redial and resume. - ln2, err := net.Listen("tcp", addr) - require.NoError(t, err) - listener2 := startListener(t, logger, ln2) - defer func() { _ = listener2.Stop() }() - - client2, err := privval.NewSignerClient(listener2, chainID) - require.NoError(t, err) - - require.Eventually(t, func() bool { - if err := client2.WaitForConnection(500 * time.Millisecond); err != nil { - return false - } - _, err := client2.GetPubKey() - return err == nil - }, 20*time.Second, 200*time.Millisecond, "manager did not reconnect after graceful restart") -} - -func TestReconnectDisabledStopsAfterDrop(t *testing.T) { - const chainID = "rc-disabled-chain" - dir := t.TempDir() - logger := log.TestingLogger() - - be, err := softsign.Load(writeKey(t, dir)) - require.NoError(t, err) - cs, err := signer.NewChainSigner(chainID, be, filepath.Join(dir, "state.json")) - require.NoError(t, err) - - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - addr := ln.Addr().String() - listener := startListener(t, logger, ln) - - mgr := manager.New(logger, []manager.ValidatorConn{{ - ChainID: chainID, Addr: "tcp://" + addr, IdentityKey: ed25519.GenPrivKey(), Signer: cs, Reconnect: false, - }}) - require.NoError(t, mgr.Start()) - defer mgr.Stop() - - client, err := privval.NewSignerClient(listener, chainID) - require.NoError(t, err) - require.NoError(t, client.WaitForConnection(5*time.Second)) - _, err = client.GetPubKey() - require.NoError(t, err) - - // Gracefully stop the validator side (KMS sees EOF) and free the port. - require.NoError(t, listener.Stop()) - time.Sleep(100 * time.Millisecond) - - // Bring the validator back on the SAME address. - ln2, err := net.Listen("tcp", addr) - require.NoError(t, err) - listener2 := startListener(t, logger, ln2) - defer func() { _ = listener2.Stop() }() - - client2, err := privval.NewSignerClient(listener2, chainID) - require.NoError(t, err) - - // With reconnect disabled, the KMS must NOT redial after the drop. - require.Never(t, func() bool { - if err := client2.WaitForConnection(300 * time.Millisecond); err != nil { - return false - } - _, err := client2.GetPubKey() - return err == nil - }, 3*time.Second, 300*time.Millisecond) -} diff --git a/kms/internal/backend/backend.go b/kms/internal/backend/backend.go deleted file mode 100644 index 6da5b759444..00000000000 --- a/kms/internal/backend/backend.go +++ /dev/null @@ -1,20 +0,0 @@ -// Package backend defines the signing-key abstraction used by cometkms. -// Implementations wrap a concrete key custodian (softsign file, PKCS#11 HSM, -// AWS KMS, ...). The interface is algorithm-agnostic: the public key carries its -// own algorithm via crypto.PubKey.Type(), and Sign produces a valid signature -// over the canonical consensus sign-bytes using whatever scheme the key requires. -package backend - -import ( - "context" - - "github.com/cometbft/cometbft/crypto" -) - -// Signer is implemented by every key backend. -type Signer interface { - // PubKey returns the validator public key. - PubKey(ctx context.Context) (crypto.PubKey, error) - // Sign signs the canonical consensus sign-bytes and returns the signature. - Sign(ctx context.Context, signBytes []byte) ([]byte, error) -} diff --git a/kms/internal/backend/pkcs11/algo.go b/kms/internal/backend/pkcs11/algo.go deleted file mode 100644 index 4486ccb6624..00000000000 --- a/kms/internal/backend/pkcs11/algo.go +++ /dev/null @@ -1,59 +0,0 @@ -package pkcs11 - -import ( - "fmt" - - "github.com/miekg/pkcs11" - - "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/crypto/ed25519" -) - -// ckmEDDSA is the standard PKCS#11 v3.0 EdDSA signing mechanism. miekg/pkcs11 -// v1.1.x does not export it, so it is defined here against the spec value. -const ckmEDDSA = 0x00001057 - -// algoEd25519 is the config "algorithm" name for Ed25519 keys (the default). -const algoEd25519 = "ed25519" - -// keyAlgo describes how one validator key algorithm maps onto PKCS#11: which -// signing mechanism to use, how to turn the token's public-key bytes into a -// crypto.PubKey, and how to normalize the raw signature the token returns. -// -// Adding a new key type (ml-dsa, secp256k1eth, ...) is a single new entry in -// algos: its mechanism, a decodePub, and (for ECDSA-family keys) a fixSig that -// converts the token's DER signature into the consensus wire format. -type keyAlgo struct { - name string - mechanism func() []*pkcs11.Mechanism - decodePub func(ckaECPoint []byte) (crypto.PubKey, error) - fixSig func(raw []byte) ([]byte, error) -} - -// algos is the registry of supported key algorithms, keyed by the config -// "algorithm" string. Ed25519 is the only entry for now. -var algos = map[string]keyAlgo{ - algoEd25519: { - name: algoEd25519, - mechanism: func() []*pkcs11.Mechanism { return []*pkcs11.Mechanism{pkcs11.NewMechanism(ckmEDDSA, nil)} }, - decodePub: decodeEd25519Pub, - fixSig: func(raw []byte) ([]byte, error) { return raw, nil }, - }, -} - -// decodeEd25519Pub turns a CKA_EC_POINT value into an ed25519 crypto.PubKey. -// PKCS#11 v3.0 encodes the point as a DER OCTET STRING wrapping the 32-byte key -// (0x04 0x20 <32 bytes>); some tokens return the raw 32 bytes. Both are accepted. -func decodeEd25519Pub(ckaECPoint []byte) (crypto.PubKey, error) { - raw := ckaECPoint - // DER OCTET STRING (tag 0x04) of length 0x20 (32) wrapping the key. - if len(ckaECPoint) == ed25519.PubKeySize+2 && ckaECPoint[0] == 0x04 && ckaECPoint[1] == ed25519.PubKeySize { - raw = ckaECPoint[2:] - } - if len(raw) != ed25519.PubKeySize { - return nil, fmt.Errorf("ed25519 CKA_EC_POINT: expected %d-byte key, got %d bytes", ed25519.PubKeySize, len(raw)) - } - pub := make(ed25519.PubKey, ed25519.PubKeySize) - copy(pub, raw) - return pub, nil -} diff --git a/kms/internal/backend/pkcs11/algo_test.go b/kms/internal/backend/pkcs11/algo_test.go deleted file mode 100644 index c3963b9f505..00000000000 --- a/kms/internal/backend/pkcs11/algo_test.go +++ /dev/null @@ -1,40 +0,0 @@ -package pkcs11 - -import ( - "testing" - - "github.com/cometbft/cometbft/crypto/ed25519" - "github.com/stretchr/testify/require" -) - -func TestEd25519DecodePub_DERWrapped(t *testing.T) { - priv := ed25519.GenPrivKey() - raw := priv.PubKey().Bytes() // 32 bytes - // PKCS#11 v3.0 returns CKA_EC_POINT as a DER OCTET STRING: 0x04 0x20 <32 bytes>. - der := append([]byte{0x04, 0x20}, raw...) - - pub, err := algos["ed25519"].decodePub(der) - require.NoError(t, err) - require.True(t, pub.Equals(priv.PubKey())) -} - -func TestEd25519DecodePub_Raw(t *testing.T) { - priv := ed25519.GenPrivKey() - raw := priv.PubKey().Bytes() // some tokens return the raw 32-byte point - - pub, err := algos["ed25519"].decodePub(raw) - require.NoError(t, err) - require.True(t, pub.Equals(priv.PubKey())) -} - -func TestEd25519DecodePub_BadLength(t *testing.T) { - _, err := algos["ed25519"].decodePub([]byte{0x01, 0x02, 0x03}) - require.Error(t, err) -} - -func TestEd25519FixSig_Identity(t *testing.T) { - sig := []byte("a-64-byte-ed25519-signature-placeholder-value-for-testing-only!!") - out, err := algos["ed25519"].fixSig(sig) - require.NoError(t, err) - require.Equal(t, sig, out) -} diff --git a/kms/internal/backend/pkcs11/module.go b/kms/internal/backend/pkcs11/module.go deleted file mode 100644 index f9dd7b78998..00000000000 --- a/kms/internal/backend/pkcs11/module.go +++ /dev/null @@ -1,68 +0,0 @@ -package pkcs11 - -import ( - "fmt" - "sync" - - "github.com/miekg/pkcs11" -) - -// A PKCS#11 module is a shared library with process-global C_Initialize state: -// it must be initialized exactly once per process and finalized once. Multiple -// signers may target the same module (e.g. several chains on one HSM), so module -// contexts are ref-counted here and shared across signers. The context is -// finalized only when the last signer using it closes. -var ( - modulesMu sync.Mutex - modules = map[string]*moduleRef{} -) - -type moduleRef struct { - ctx *pkcs11.Ctx - refs int -} - -// acquireModule returns a shared, initialized context for the module at path, -// loading and initializing it on first use and bumping its ref count otherwise. -func acquireModule(path string) (*pkcs11.Ctx, error) { - modulesMu.Lock() - defer modulesMu.Unlock() - - if m := modules[path]; m != nil { - m.refs++ - return m.ctx, nil - } - - ctx := pkcs11.New(path) - if ctx == nil { - return nil, fmt.Errorf("pkcs11: failed to load module %q", path) - } - if err := ctx.Initialize(); err != nil { - // Another consumer in this process may have already initialized the - // underlying library; treat that as success and adopt it. - if ce, ok := err.(pkcs11.Error); !ok || ce != pkcs11.CKR_CRYPTOKI_ALREADY_INITIALIZED { - ctx.Destroy() - return nil, fmt.Errorf("pkcs11: initialize module %q: %w", path, err) - } - } - modules[path] = &moduleRef{ctx: ctx, refs: 1} - return ctx, nil -} - -// releaseModule drops one reference to the module at path, finalizing and -// unloading it when the last reference is released. -func releaseModule(path string) { - modulesMu.Lock() - defer modulesMu.Unlock() - - m := modules[path] - if m == nil { - return - } - m.refs-- - if m.refs <= 0 { - _ = m.ctx.Finalize() - m.ctx.Destroy() - delete(modules, path) - } -} diff --git a/kms/internal/backend/pkcs11/pin_test.go b/kms/internal/backend/pkcs11/pin_test.go deleted file mode 100644 index 5b478fce3d2..00000000000 --- a/kms/internal/backend/pkcs11/pin_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package pkcs11 - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestResolvePIN_Inline(t *testing.T) { - pin, err := resolvePIN(Config{PIN: "1234"}) - require.NoError(t, err) - require.Equal(t, "1234", pin) -} - -func TestResolvePIN_Env(t *testing.T) { - t.Setenv("COMETKMS_TEST_PIN", "envpin") - pin, err := resolvePIN(Config{PINEnv: "COMETKMS_TEST_PIN"}) - require.NoError(t, err) - require.Equal(t, "envpin", pin) -} - -func TestResolvePIN_EnvEmpty(t *testing.T) { - t.Setenv("COMETKMS_TEST_PIN", "") - _, err := resolvePIN(Config{PINEnv: "COMETKMS_TEST_PIN"}) - require.Error(t, err) -} - -func TestResolvePIN_File(t *testing.T) { - path := filepath.Join(t.TempDir(), "pin") - require.NoError(t, os.WriteFile(path, []byte("filepin\n"), 0o600)) - pin, err := resolvePIN(Config{PINFile: path}) - require.NoError(t, err) - require.Equal(t, "filepin", pin) // trailing newline trimmed -} - -func TestResolvePIN_FileEmpty(t *testing.T) { - path := filepath.Join(t.TempDir(), "pin") - require.NoError(t, os.WriteFile(path, []byte("\n"), 0o600)) - _, err := resolvePIN(Config{PINFile: path}) - require.Error(t, err) -} - -func TestResolvePIN_FileMissing(t *testing.T) { - _, err := resolvePIN(Config{PINFile: filepath.Join(t.TempDir(), "nope")}) - require.Error(t, err) -} - -func TestResolvePIN_NoSource(t *testing.T) { - _, err := resolvePIN(Config{}) - require.Error(t, err) -} diff --git a/kms/internal/backend/pkcs11/pkcs11.go b/kms/internal/backend/pkcs11/pkcs11.go deleted file mode 100644 index 3fce8f124f2..00000000000 --- a/kms/internal/backend/pkcs11/pkcs11.go +++ /dev/null @@ -1,261 +0,0 @@ -// Package pkcs11 implements a backend.Signer backed by a PKCS#11 token or HSM. -// The private key never leaves the token: signing is performed on-device via the -// PKCS#11 C_Sign operation. Ed25519 (CKM_EDDSA) is the only key algorithm today; -// see algo.go for the per-algorithm seam. -package pkcs11 - -import ( - "context" - "fmt" - "os" - "strings" - "sync" - - "github.com/miekg/pkcs11" - - "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/kms/internal/backend" -) - -var _ backend.Signer = (*Signer)(nil) - -// Config describes how to open a key on a PKCS#11 token. Exactly one of -// TokenLabel/Slot selects the token, at least one of KeyLabel/KeyID selects the -// key, and exactly one of PIN/PINEnv/PINFile supplies the user PIN. Algorithm -// defaults to "ed25519" when empty. -type Config struct { - Module string - TokenLabel string - Slot *uint - KeyLabel string - KeyID []byte - PIN string - PINEnv string - PINFile string - Algorithm string -} - -// Signer is a backend.Signer that signs on a PKCS#11 token. It owns a single -// long-lived session; the mutex serializes signing (PKCS#11 sessions are not -// safe for concurrent use) and guards Close. -type Signer struct { - mod *pkcs11.Ctx - module string // module path, used to release the shared context on Close - session pkcs11.SessionHandle - privH pkcs11.ObjectHandle - pub crypto.PubKey - algo keyAlgo - - mu sync.Mutex - closed bool -} - -// Open loads the PKCS#11 module, logs into the selected token, locates the key, -// and caches its public key. Any failure is returned (fatal at startup for the -// chain). On success the returned Signer holds an open, logged-in session that -// must be released with Close. -func Open(cfg Config) (s *Signer, err error) { - algoName := cfg.Algorithm - if algoName == "" { - algoName = algoEd25519 - } - algo, ok := algos[algoName] - if !ok { - return nil, fmt.Errorf("pkcs11: unknown algorithm %q", algoName) - } - - pin, err := resolvePIN(cfg) - if err != nil { - return nil, err - } - - mod, err := acquireModule(cfg.Module) - if err != nil { - return nil, err - } - // Release our module reference on any error past this point. - defer func() { - if err != nil { - releaseModule(cfg.Module) - } - }() - - slot, err := selectSlot(mod, cfg) - if err != nil { - return nil, err - } - - session, err := mod.OpenSession(slot, pkcs11.CKF_SERIAL_SESSION) - if err != nil { - return nil, fmt.Errorf("pkcs11: open session on slot %d: %w", slot, err) - } - defer func() { - if err != nil { - _ = mod.CloseSession(session) - } - }() - - // Login is per-application (shared across sessions on a slot): a concurrent - // signer on the same token may already hold the login, which is fine. - if err = mod.Login(session, pkcs11.CKU_USER, pin); err != nil { - if ce, ok := err.(pkcs11.Error); !ok || ce != pkcs11.CKR_USER_ALREADY_LOGGED_IN { - return nil, fmt.Errorf("pkcs11: login: %w", err) - } - err = nil - } - - privH, err := findObject(mod, session, pkcs11.CKO_PRIVATE_KEY, cfg) - if err != nil { - return nil, fmt.Errorf("pkcs11: find private key: %w", err) - } - pubH, err := findObject(mod, session, pkcs11.CKO_PUBLIC_KEY, cfg) - if err != nil { - return nil, fmt.Errorf("pkcs11: find public key: %w", err) - } - - attrs, err := mod.GetAttributeValue(session, pubH, []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, nil), - }) - if err != nil { - return nil, fmt.Errorf("pkcs11: read public key: %w", err) - } - if len(attrs) == 0 { - return nil, fmt.Errorf("pkcs11: public key has no CKA_EC_POINT") - } - pub, err := algo.decodePub(attrs[0].Value) - if err != nil { - return nil, fmt.Errorf("pkcs11: decode public key: %w", err) - } - - return &Signer{mod: mod, module: cfg.Module, session: session, privH: privH, pub: pub, algo: algo}, nil -} - -// selectSlot returns the slot to use: the explicit Slot when set, otherwise the -// slot whose token CKA_LABEL matches TokenLabel. -func selectSlot(mod *pkcs11.Ctx, cfg Config) (uint, error) { - if cfg.Slot != nil { - return *cfg.Slot, nil - } - slots, err := mod.GetSlotList(true) - if err != nil { - return 0, fmt.Errorf("pkcs11: list slots: %w", err) - } - for _, slot := range slots { - info, err := mod.GetTokenInfo(slot) - if err != nil { - continue - } - // Token labels are space-padded to 32 bytes by the spec. - if strings.TrimRight(info.Label, " ") == cfg.TokenLabel { - return slot, nil - } - } - return 0, fmt.Errorf("pkcs11: no token with label %q", cfg.TokenLabel) -} - -// findObject locates exactly one key object of the given class matching the -// configured label and/or id. -func findObject(mod *pkcs11.Ctx, session pkcs11.SessionHandle, class uint, cfg Config) (pkcs11.ObjectHandle, error) { - template := []*pkcs11.Attribute{pkcs11.NewAttribute(pkcs11.CKA_CLASS, class)} - if cfg.KeyLabel != "" { - template = append(template, pkcs11.NewAttribute(pkcs11.CKA_LABEL, cfg.KeyLabel)) - } - if len(cfg.KeyID) > 0 { - template = append(template, pkcs11.NewAttribute(pkcs11.CKA_ID, cfg.KeyID)) - } - - if err := mod.FindObjectsInit(session, template); err != nil { - return 0, err - } - handles, _, err := mod.FindObjects(session, 2) - if finErr := mod.FindObjectsFinal(session); finErr != nil && err == nil { - err = finErr - } - if err != nil { - return 0, err - } - switch { - case len(handles) == 0: - return 0, fmt.Errorf("no object matching %s", keySelector(cfg)) - case len(handles) > 1: - return 0, fmt.Errorf("multiple objects match %s: refine key_label/key_id", keySelector(cfg)) - } - return handles[0], nil -} - -// keySelector describes the configured key search criteria for error messages. -func keySelector(cfg Config) string { - switch { - case cfg.KeyLabel != "" && len(cfg.KeyID) > 0: - return fmt.Sprintf("key_label=%q key_id=%x", cfg.KeyLabel, cfg.KeyID) - case cfg.KeyLabel != "": - return fmt.Sprintf("key_label=%q", cfg.KeyLabel) - default: - return fmt.Sprintf("key_id=%x", cfg.KeyID) - } -} - -// PubKey returns the validator public key cached at Open. -func (s *Signer) PubKey(context.Context) (crypto.PubKey, error) { return s.pub, nil } - -// Sign signs the canonical consensus sign-bytes on the token. -func (s *Signer) Sign(_ context.Context, signBytes []byte) ([]byte, error) { - s.mu.Lock() - defer s.mu.Unlock() - if s.closed { - return nil, fmt.Errorf("pkcs11: signer is closed") - } - if err := s.mod.SignInit(s.session, s.algo.mechanism(), s.privH); err != nil { - return nil, fmt.Errorf("pkcs11: sign init: %w", err) - } - raw, err := s.mod.Sign(s.session, signBytes) - if err != nil { - return nil, fmt.Errorf("pkcs11: sign: %w", err) - } - return s.algo.fixSig(raw) -} - -// Close logs out, closes the session, and tears down the module. It is -// idempotent. -func (s *Signer) Close() error { - s.mu.Lock() - defer s.mu.Unlock() - if s.closed { - return nil - } - s.closed = true - _ = s.mod.CloseSession(s.session) - // releaseModule finalizes and unloads the module once the last signer using - // it has closed (Finalize tears down login state and sessions). - releaseModule(s.module) - return nil -} - -// resolvePIN returns the user PIN from whichever source the config specifies. -// The PIN is read at open time (not stored in config files): an env var is read -// from the process environment; a file is read and stripped of trailing -// whitespace. An empty resolved PIN is an error. -func resolvePIN(cfg Config) (string, error) { - switch { - case cfg.PIN != "": - return cfg.PIN, nil - case cfg.PINEnv != "": - v := os.Getenv(cfg.PINEnv) - if v == "" { - return "", fmt.Errorf("pkcs11: pin_env %q is empty or unset", cfg.PINEnv) - } - return v, nil - case cfg.PINFile != "": - raw, err := os.ReadFile(cfg.PINFile) - if err != nil { - return "", fmt.Errorf("pkcs11: read pin_file %q: %w", cfg.PINFile, err) - } - v := strings.TrimRight(string(raw), " \t\r\n") - if v == "" { - return "", fmt.Errorf("pkcs11: pin_file %q is empty", cfg.PINFile) - } - return v, nil - default: - return "", fmt.Errorf("pkcs11: no PIN source configured (set pin, pin_env, or pin_file)") - } -} diff --git a/kms/internal/backend/pkcs11/pkcs11_test.go b/kms/internal/backend/pkcs11/pkcs11_test.go deleted file mode 100644 index 3e423618e00..00000000000 --- a/kms/internal/backend/pkcs11/pkcs11_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package pkcs11_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - pk "github.com/cometbft/cometbft/kms/internal/backend/pkcs11" - "github.com/cometbft/cometbft/kms/internal/backend/pkcs11/pkcs11test" -) - -func TestOpenPubKeySignVerify(t *testing.T) { - module := pkcs11test.FindModule(t) - want := pkcs11test.SetupToken(t, module) - - s, err := pk.Open(pk.Config{ - Module: module, - TokenLabel: pkcs11test.TokenLabel, - KeyLabel: pkcs11test.KeyLabel, - PIN: pkcs11test.UserPIN, - }) - require.NoError(t, err) - t.Cleanup(func() { _ = s.Close() }) - - pub, err := s.PubKey(context.Background()) - require.NoError(t, err) - require.True(t, pub.Equals(want), "backend pubkey must match the on-token key") - - msg := []byte("canonical-consensus-sign-bytes") - sig, err := s.Sign(context.Background(), msg) - require.NoError(t, err) - require.True(t, pub.VerifySignature(msg, sig), "signature must verify under the on-token pubkey") -} - -func TestOpenByKeyID(t *testing.T) { - module := pkcs11test.FindModule(t) - want := pkcs11test.SetupToken(t, module) - - s, err := pk.Open(pk.Config{ - Module: module, - TokenLabel: pkcs11test.TokenLabel, - KeyID: pkcs11test.KeyID, - PIN: pkcs11test.UserPIN, - }) - require.NoError(t, err) - t.Cleanup(func() { _ = s.Close() }) - - pub, err := s.PubKey(context.Background()) - require.NoError(t, err) - require.True(t, pub.Equals(want)) -} - -func TestOpenWrongPIN(t *testing.T) { - module := pkcs11test.FindModule(t) - pkcs11test.SetupToken(t, module) - - _, err := pk.Open(pk.Config{ - Module: module, - TokenLabel: pkcs11test.TokenLabel, - KeyLabel: pkcs11test.KeyLabel, - PIN: "9999", - }) - require.Error(t, err) -} - -func TestOpenUnknownToken(t *testing.T) { - module := pkcs11test.FindModule(t) - pkcs11test.SetupToken(t, module) - - _, err := pk.Open(pk.Config{ - Module: module, - TokenLabel: "no-such-token", - KeyLabel: pkcs11test.KeyLabel, - PIN: pkcs11test.UserPIN, - }) - require.Error(t, err) -} - -func TestOpenUnknownKey(t *testing.T) { - module := pkcs11test.FindModule(t) - pkcs11test.SetupToken(t, module) - - _, err := pk.Open(pk.Config{ - Module: module, - TokenLabel: pkcs11test.TokenLabel, - KeyLabel: "no-such-key", - PIN: pkcs11test.UserPIN, - }) - require.Error(t, err) - require.ErrorContains(t, err, "no object matching") -} - -func TestOpenAmbiguousKeyLabel(t *testing.T) { - module := pkcs11test.FindModule(t) - pkcs11test.SetupToken(t, module) - // A second key pair with the same label but a different id makes a - // label-only lookup ambiguous. - pkcs11test.AddKeyPair(t, module, pkcs11test.KeyLabel, []byte{0x02}) - - _, err := pk.Open(pk.Config{ - Module: module, - TokenLabel: pkcs11test.TokenLabel, - KeyLabel: pkcs11test.KeyLabel, - PIN: pkcs11test.UserPIN, - }) - require.Error(t, err) - require.ErrorContains(t, err, "multiple objects match") - - // Adding the unique key_id disambiguates. - s, err := pk.Open(pk.Config{ - Module: module, - TokenLabel: pkcs11test.TokenLabel, - KeyLabel: pkcs11test.KeyLabel, - KeyID: pkcs11test.KeyID, - PIN: pkcs11test.UserPIN, - }) - require.NoError(t, err) - t.Cleanup(func() { _ = s.Close() }) -} - -func TestDoubleCloseSafe(t *testing.T) { - module := pkcs11test.FindModule(t) - pkcs11test.SetupToken(t, module) - - s, err := pk.Open(pk.Config{ - Module: module, - TokenLabel: pkcs11test.TokenLabel, - KeyLabel: pkcs11test.KeyLabel, - PIN: pkcs11test.UserPIN, - }) - require.NoError(t, err) - require.NoError(t, s.Close()) - require.NoError(t, s.Close()) -} diff --git a/kms/internal/backend/pkcs11/pkcs11test/pkcs11test.go b/kms/internal/backend/pkcs11/pkcs11test/pkcs11test.go deleted file mode 100644 index 1632d8db3ce..00000000000 --- a/kms/internal/backend/pkcs11/pkcs11test/pkcs11test.go +++ /dev/null @@ -1,193 +0,0 @@ -// Package pkcs11test provides SoftHSM2-backed helpers for exercising the PKCS#11 -// signer in tests. It initializes an isolated, throwaway token and generates an -// Ed25519 key in it. Tests that use it auto-skip when SoftHSM2 is not installed. -package pkcs11test - -import ( - "os" - "path/filepath" - "strings" - "testing" - - "github.com/miekg/pkcs11" - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/crypto/ed25519" -) - -// PKCS#11 / SoftHSM2 constants not exported by miekg/pkcs11 v1.1.x. -const ( - ckmECEdwardsKeyPairGen = 0x00001055 - ckkECEdwards = 0x00000040 -) - -// ed25519 curve OID 1.3.101.112, DER-encoded for CKA_EC_PARAMS. -var oidEd25519 = []byte{0x06, 0x03, 0x2b, 0x65, 0x70} - -// Fixed token/key identifiers used by the helper. -const ( - TokenLabel = "comet" - KeyLabel = "validator" - UserPIN = "1234" - soPIN = "4321" -) - -// KeyID is the CKA_ID of the generated key. -var KeyID = []byte{0x01} - -// commonModulePaths lists where libsofthsm2.so is typically installed. -var commonModulePaths = []string{ - "/opt/homebrew/lib/softhsm/libsofthsm2.so", - "/usr/local/lib/softhsm/libsofthsm2.so", - "/usr/lib/softhsm/libsofthsm2.so", - "/usr/lib/x86_64-linux-gnu/softhsm/libsofthsm2.so", -} - -// findSlotByLabel returns the slot whose token label matches label. Token -// labels are space-padded to 32 bytes by the spec. -func findSlotByLabel(t *testing.T, ctx *pkcs11.Ctx, label string) uint { - t.Helper() - slots, err := ctx.GetSlotList(true) - require.NoError(t, err) - for _, s := range slots { - info, err := ctx.GetTokenInfo(s) - if err != nil { - continue - } - if strings.TrimRight(info.Label, " ") == label { - return s - } - } - t.Fatalf("no slot with token label %q", label) - return 0 -} - -// FindModule returns the SoftHSM2 module path, or skips the test if not found. -func FindModule(t *testing.T) string { - t.Helper() - if p := os.Getenv("COMETKMS_SOFTHSM_LIB"); p != "" { - return p - } - for _, p := range commonModulePaths { - if _, err := os.Stat(p); err == nil { - return p - } - } - t.Skip("SoftHSM2 module not found; set COMETKMS_SOFTHSM_LIB to run PKCS#11 integration tests") - return "" -} - -// SetupToken initializes a fresh SoftHSM2 token in an isolated temp directory and -// generates an Ed25519 key pair in it. It returns the on-token public key so the -// caller can verify what the signer reads and signs. SOFTHSM2_CONF is pointed at -// the temp dir for the duration of the test. -func SetupToken(t *testing.T, module string) ed25519.PubKey { - t.Helper() - - tokenDir := t.TempDir() - confPath := filepath.Join(t.TempDir(), "softhsm2.conf") - conf := "directories.tokendir = " + tokenDir + "\nobjectstore.backend = file\nlog.level = ERROR\n" - require.NoError(t, os.WriteFile(confPath, []byte(conf), 0o600)) - t.Setenv("SOFTHSM2_CONF", confPath) - - ctx := pkcs11.New(module) - require.NotNil(t, ctx, "load module") - require.NoError(t, ctx.Initialize()) - // Finalize this provisioning context before returning so the signer under - // test can initialize the (process-global) module cleanly. Token data - // persists on disk via the file objectstore. - defer func() { _ = ctx.Finalize(); ctx.Destroy() }() - - slots, err := ctx.GetSlotList(false) - require.NoError(t, err) - require.NotEmpty(t, slots) - slot := slots[0] - - require.NoError(t, ctx.InitToken(slot, soPIN, TokenLabel)) - - // Re-query after InitToken: the PKCS#11 spec allows the slot list to change - // (SoftHSM2 may assign the initialized token a new slot ID). Find our token - // by label rather than trusting the original slot ID. - slot = findSlotByLabel(t, ctx, TokenLabel) - - // Set the user PIN: open RW, login SO, InitPIN, logout. - sess, err := ctx.OpenSession(slot, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) - require.NoError(t, err) - require.NoError(t, ctx.Login(sess, pkcs11.CKU_SO, soPIN)) - require.NoError(t, ctx.InitPIN(sess, UserPIN)) - require.NoError(t, ctx.Logout(sess)) - - // Generate the Ed25519 key pair as the user. - require.NoError(t, ctx.Login(sess, pkcs11.CKU_USER, UserPIN)) - pubH := genKeyPair(t, ctx, sess, KeyLabel, KeyID) - - // Read the public point back so the caller can verify signatures. - attrs, err := ctx.GetAttributeValue(sess, pubH, []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_EC_POINT, nil), - }) - require.NoError(t, err) - require.Len(t, attrs, 1) - - require.NoError(t, ctx.Logout(sess)) - require.NoError(t, ctx.CloseSession(sess)) - - raw := attrs[0].Value - // Unwrap a DER OCTET STRING (0x04 0x20 <32 bytes>) if present. - if len(raw) == ed25519.PubKeySize+2 && raw[0] == 0x04 && raw[1] == ed25519.PubKeySize { - raw = raw[2:] - } - require.Len(t, raw, ed25519.PubKeySize) - pub := make(ed25519.PubKey, ed25519.PubKeySize) - copy(pub, raw) - return pub -} - -// AddKeyPair generates an additional Ed25519 key pair with the given label and -// id on the token previously provisioned by SetupToken. -func AddKeyPair(t *testing.T, module, label string, id []byte) { - t.Helper() - - ctx := pkcs11.New(module) - require.NotNil(t, ctx, "load module") - require.NoError(t, ctx.Initialize()) - defer func() { _ = ctx.Finalize(); ctx.Destroy() }() - - slot := findSlotByLabel(t, ctx, TokenLabel) - sess, err := ctx.OpenSession(slot, pkcs11.CKF_SERIAL_SESSION|pkcs11.CKF_RW_SESSION) - require.NoError(t, err) - require.NoError(t, ctx.Login(sess, pkcs11.CKU_USER, UserPIN)) - genKeyPair(t, ctx, sess, label, id) - require.NoError(t, ctx.Logout(sess)) - require.NoError(t, ctx.CloseSession(sess)) -} - -// genKeyPair generates a token-resident Ed25519 key pair with the given label -// and id, returning the public key handle. The session must be logged in as -// the user. -func genKeyPair(t *testing.T, ctx *pkcs11.Ctx, sess pkcs11.SessionHandle, label string, id []byte) pkcs11.ObjectHandle { - t.Helper() - - pubTemplate := []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PUBLIC_KEY), - pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, ckkECEdwards), - pkcs11.NewAttribute(pkcs11.CKA_EC_PARAMS, oidEd25519), - pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), - pkcs11.NewAttribute(pkcs11.CKA_VERIFY, true), - pkcs11.NewAttribute(pkcs11.CKA_LABEL, label), - pkcs11.NewAttribute(pkcs11.CKA_ID, id), - } - privTemplate := []*pkcs11.Attribute{ - pkcs11.NewAttribute(pkcs11.CKA_CLASS, pkcs11.CKO_PRIVATE_KEY), - pkcs11.NewAttribute(pkcs11.CKA_KEY_TYPE, ckkECEdwards), - pkcs11.NewAttribute(pkcs11.CKA_TOKEN, true), - pkcs11.NewAttribute(pkcs11.CKA_PRIVATE, true), - pkcs11.NewAttribute(pkcs11.CKA_SIGN, true), - pkcs11.NewAttribute(pkcs11.CKA_LABEL, label), - pkcs11.NewAttribute(pkcs11.CKA_ID, id), - } - pubH, _, err := ctx.GenerateKeyPair(sess, - []*pkcs11.Mechanism{pkcs11.NewMechanism(ckmECEdwardsKeyPairGen, nil)}, - pubTemplate, privTemplate) - require.NoError(t, err) - return pubH -} diff --git a/kms/internal/backend/softsign/softsign.go b/kms/internal/backend/softsign/softsign.go deleted file mode 100644 index dae10f97ac9..00000000000 --- a/kms/internal/backend/softsign/softsign.go +++ /dev/null @@ -1,78 +0,0 @@ -// Package softsign implements an in-memory, file-backed Ed25519 backend.Signer. -// NOT for production custody: the private key is held in process memory. -package softsign - -import ( - "bytes" - "context" - "encoding/base64" - "fmt" - "os" - - "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/crypto/ed25519" - cmtjson "github.com/cometbft/cometbft/libs/json" - - "github.com/cometbft/cometbft/kms/internal/backend" -) - -// Signer is a softsign backend holding an Ed25519 private key in memory. -type Signer struct { - priv crypto.PrivKey - pub crypto.PubKey -} - -var _ backend.Signer = (*Signer)(nil) - -// Load reads a key file. It accepts either a CometBFT priv_validator_key.json -// (typed JSON with a "priv_key" field) or a file containing the base64-encoded -// 64-byte Ed25519 private key. -func Load(path string) (*Signer, error) { - raw, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("softsign: read key file %q: %w", path, err) - } - - priv, err := parseKey(raw) - if err != nil { - return nil, fmt.Errorf("softsign: parse key file %q: %w", path, err) - } - return &Signer{priv: priv, pub: priv.PubKey()}, nil -} - -func parseKey(raw []byte) (crypto.PrivKey, error) { - // Try priv_validator_key.json shape first (both concrete and interface-typed variants). - if bytes.Contains(raw, []byte("priv_key")) { - // Try interface-typed JSON first ({"type":"...","value":"..."} envelope). - var kfIface struct { - PrivKey crypto.PrivKey `json:"priv_key"` - } - if err := cmtjson.Unmarshal(raw, &kfIface); err == nil && kfIface.PrivKey != nil { - return kfIface.PrivKey, nil - } - // Try concrete ed25519 JSON (plain base64 string value). - var kfConcrete struct { - PrivKey ed25519.PrivKey `json:"priv_key"` - } - if err := cmtjson.Unmarshal(raw, &kfConcrete); err == nil && len(kfConcrete.PrivKey) == ed25519.PrivateKeySize { - return kfConcrete.PrivKey, nil - } - } - // Fall back to base64 raw 64-byte ed25519 key. - dec, err := base64.StdEncoding.DecodeString(string(bytes.TrimSpace(raw))) - if err != nil { - return nil, fmt.Errorf("not priv_validator_key.json and not base64: %w", err) - } - if len(dec) != ed25519.PrivateKeySize { - return nil, fmt.Errorf("expected %d-byte ed25519 key, got %d", ed25519.PrivateKeySize, len(dec)) - } - return ed25519.PrivKey(dec), nil -} - -// PubKey implements backend.Signer. -func (s *Signer) PubKey(context.Context) (crypto.PubKey, error) { return s.pub, nil } - -// Sign implements backend.Signer. -func (s *Signer) Sign(_ context.Context, signBytes []byte) ([]byte, error) { - return s.priv.Sign(signBytes) -} diff --git a/kms/internal/backend/softsign/softsign_test.go b/kms/internal/backend/softsign/softsign_test.go deleted file mode 100644 index 3499e09cbbe..00000000000 --- a/kms/internal/backend/softsign/softsign_test.go +++ /dev/null @@ -1,57 +0,0 @@ -package softsign_test - -import ( - "context" - "encoding/base64" - "os" - "path/filepath" - "testing" - - "github.com/cometbft/cometbft/crypto/ed25519" - cmtjson "github.com/cometbft/cometbft/libs/json" - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/kms/internal/backend/softsign" -) - -func TestLoadBase64AndSign(t *testing.T) { - priv := ed25519.GenPrivKey() - dir := t.TempDir() - path := filepath.Join(dir, "key.b64") - require.NoError(t, os.WriteFile(path, []byte(base64.StdEncoding.EncodeToString(priv.Bytes())), 0o600)) - - s, err := softsign.Load(path) - require.NoError(t, err) - - pub, err := s.PubKey(context.Background()) - require.NoError(t, err) - require.True(t, pub.Equals(priv.PubKey())) - - msg := []byte("canonical-sign-bytes") - sig, err := s.Sign(context.Background(), msg) - require.NoError(t, err) - require.True(t, pub.VerifySignature(msg, sig)) -} - -func TestLoadPrivValidatorKeyJSON(t *testing.T) { - priv := ed25519.GenPrivKey() - raw, err := cmtjson.MarshalIndent(struct { - PrivKey ed25519.PrivKey `json:"priv_key"` - }{PrivKey: priv}, "", " ") - require.NoError(t, err) - - dir := t.TempDir() - path := filepath.Join(dir, "priv_validator_key.json") - require.NoError(t, os.WriteFile(path, raw, 0o600)) - - s, err := softsign.Load(path) - require.NoError(t, err) - pub, err := s.PubKey(context.Background()) - require.NoError(t, err) - require.True(t, pub.Equals(priv.PubKey())) -} - -func TestLoadRejectsMissingFile(t *testing.T) { - _, err := softsign.Load(filepath.Join(t.TempDir(), "nope")) - require.Error(t, err) -} diff --git a/kms/internal/config/config.go b/kms/internal/config/config.go deleted file mode 100644 index ad6f296dc0c..00000000000 --- a/kms/internal/config/config.go +++ /dev/null @@ -1,98 +0,0 @@ -// Package config defines the cometkms TOML configuration and its validation. -package config - -import ( - "fmt" - "strings" - - "github.com/BurntSushi/toml" - "github.com/cometbft/cometbft/privval" - "github.com/libp2p/go-libp2p/core/peer" -) - -// Config is the top-level cometkms configuration. -type Config struct { - Chains []Chain `toml:"chain"` - Validators []Validator `toml:"validator"` - Providers Providers `toml:"providers"` -} - -// Chain declares a chain and its double-sign state file. -type Chain struct { - ID string `toml:"id"` - StateFile string `toml:"state_file"` // optional; defaulted by Validate -} - -// Validator declares one outbound connection to a validator's listener. -type Validator struct { - ChainID string `toml:"chain_id"` - Addr string `toml:"addr"` // tcp://host:port - IdentityKey string `toml:"identity_key"` // ed25519 node-key file for SecretConnection - Reconnect *bool `toml:"reconnect"` // default true -} - -// Providers groups the configured key backends. -type Providers struct { - Softsign []SoftsignProvider `toml:"softsign"` - PKCS11 []PKCS11Provider `toml:"pkcs11"` -} - -// SoftsignProvider binds a softsign key file to one or more chains. -type SoftsignProvider struct { - ChainIDs []string `toml:"chain_ids"` - KeyFile string `toml:"key_file"` -} - -// PKCS11Provider binds a key on a PKCS#11 token/HSM to one or more chains. -// Exactly one of TokenLabel/Slot selects the token, at least one of -// KeyLabel/KeyID selects the key, and exactly one of PIN/PINEnv/PINFile supplies -// the user PIN. Algorithm defaults to "ed25519" when empty. -type PKCS11Provider struct { - ChainIDs []string `toml:"chain_ids"` - Module string `toml:"module"` // path to the PKCS#11 .so - TokenLabel string `toml:"token_label"` // CKA_LABEL of the token (XOR slot) - Slot *uint `toml:"slot"` // slot number (XOR token_label) - KeyLabel string `toml:"key_label"` // CKA_LABEL of the key - KeyID string `toml:"key_id"` // hex-encoded CKA_ID of the key - PIN string `toml:"pin"` // inline PIN - PINEnv string `toml:"pin_env"` // name of env var holding the PIN - PINFile string `toml:"pin_file"` // path to a file holding the PIN - Algorithm string `toml:"algorithm"` // key algorithm; defaults to "ed25519" -} - -// Load parses a TOML config file. -func Load(path string) (*Config, error) { - var c Config - if _, err := toml.DecodeFile(path, &c); err != nil { - return nil, fmt.Errorf("config: decode %q: %w", path, err) - } - return &c, nil -} - -// ReconnectEnabled reports the effective reconnect setting (default true). -func (v Validator) ReconnectEnabled() bool { return v.Reconnect == nil || *v.Reconnect } - -// Transport identifies the privval connection transport selected by a validator -// address scheme. -type Transport int - -const ( - // TransportTCP is tcp:// with cometbft SecretConnection (the default). - TransportTCP Transport = iota - // TransportNoise is noise://@host:port with libp2p Noise. - TransportNoise -) - -// ParsedTransport classifies v.Addr. For TCP it returns the full address -// unchanged (DialTCPFn consumes the tcp:// form) and an empty peer ID. For Noise -// it returns the host:port and the pinned validator peer ID. -func (v Validator) ParsedTransport() (tr Transport, addr string, validatorPeer peer.ID, err error) { - if strings.HasPrefix(v.Addr, "noise://") { - pid, hostport, perr := privval.ParseNoiseAddr(v.Addr) - if perr != nil { - return TransportNoise, "", "", perr - } - return TransportNoise, hostport, pid, nil - } - return TransportTCP, v.Addr, "", nil -} diff --git a/kms/internal/config/config_pkcs11_test.go b/kms/internal/config/config_pkcs11_test.go deleted file mode 100644 index 540acdc013c..00000000000 --- a/kms/internal/config/config_pkcs11_test.go +++ /dev/null @@ -1,175 +0,0 @@ -package config_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/kms/internal/config" -) - -// writeModule creates a stand-in PKCS#11 module file so readability checks pass. -func writeModule(t *testing.T, home string) string { - t.Helper() - p := filepath.Join(home, "libsofthsm2.so") - require.NoError(t, os.WriteFile(p, []byte("not-a-real-module"), 0o600)) - return p -} - -func pkcs11Cfg(module, extra string) string { - return ` -[[chain]] -id = "c1" - -[[validator]] -chain_id = "c1" -addr = "tcp://127.0.0.1:1" -identity_key = "i" - -[[providers.pkcs11]] -chain_ids = ["c1"] -module = "` + module + `" -` + extra -} - -func TestPKCS11Good(t *testing.T) { - home := t.TempDir() - mod := writeModule(t, home) - body := pkcs11Cfg(mod, `token_label = "comet" -key_label = "validator" -pin = "1234" -`) - cfgPath := filepath.Join(home, "cometkms.toml") - require.NoError(t, os.WriteFile(cfgPath, []byte(body), 0o600)) - - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.NoError(t, c.Validate(home)) -} - -func TestPKCS11SatisfiesBackendRequirement(t *testing.T) { - // A chain whose only provider is pkcs11 must not trip the "no backend" check. - home := t.TempDir() - mod := writeModule(t, home) - body := pkcs11Cfg(mod, `token_label = "comet" -key_id = "01ab" -pin = "1234" -`) - cfgPath := filepath.Join(home, "cometkms.toml") - require.NoError(t, os.WriteFile(cfgPath, []byte(body), 0o600)) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.NoError(t, c.Validate(home)) -} - -func validatePKCS11(t *testing.T, extra string) error { - t.Helper() - home := t.TempDir() - mod := writeModule(t, home) - body := pkcs11Cfg(mod, extra) - cfgPath := filepath.Join(home, "cometkms.toml") - require.NoError(t, os.WriteFile(cfgPath, []byte(body), 0o600)) - c, err := config.Load(cfgPath) - require.NoError(t, err) - return c.Validate(home) -} - -func TestPKCS11TokenAndSlotMutuallyExclusive(t *testing.T) { - err := validatePKCS11(t, `token_label = "comet" -slot = 0 -key_label = "validator" -pin = "1234" -`) - require.ErrorContains(t, err, "token_label") -} - -func TestPKCS11RequiresTokenOrSlot(t *testing.T) { - err := validatePKCS11(t, `key_label = "validator" -pin = "1234" -`) - require.ErrorContains(t, err, "token_label") -} - -func TestPKCS11RequiresKeyLabelOrID(t *testing.T) { - err := validatePKCS11(t, `token_label = "comet" -pin = "1234" -`) - require.ErrorContains(t, err, "key_label") -} - -func TestPKCS11BadKeyIDHex(t *testing.T) { - err := validatePKCS11(t, `token_label = "comet" -key_id = "zz" -pin = "1234" -`) - require.ErrorContains(t, err, "key_id") -} - -func TestPKCS11MultiplePINSources(t *testing.T) { - err := validatePKCS11(t, `token_label = "comet" -key_label = "validator" -pin = "1234" -pin_env = "X" -`) - require.ErrorContains(t, err, "pin") -} - -func TestPKCS11NoPINSource(t *testing.T) { - err := validatePKCS11(t, `token_label = "comet" -key_label = "validator" -`) - require.ErrorContains(t, err, "pin") -} - -func TestPKCS11UnknownAlgorithm(t *testing.T) { - err := validatePKCS11(t, `token_label = "comet" -key_label = "validator" -pin = "1234" -algorithm = "rsa-9000" -`) - require.ErrorContains(t, err, "algorithm") -} - -func TestPKCS11MissingModule(t *testing.T) { - home := t.TempDir() - body := pkcs11Cfg(filepath.Join(home, "does-not-exist.so"), `token_label = "comet" -key_label = "validator" -pin = "1234" -`) - cfgPath := filepath.Join(home, "cometkms.toml") - require.NoError(t, os.WriteFile(cfgPath, []byte(body), 0o600)) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.ErrorContains(t, c.Validate(home), "module") -} - -func TestPKCS11RelativePathsResolvedAgainstHome(t *testing.T) { - home := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(home, "mod.so"), []byte("x"), 0o600)) - require.NoError(t, os.WriteFile(filepath.Join(home, "pin.txt"), []byte("1234"), 0o600)) - body := ` -[[chain]] -id = "c1" - -[[validator]] -chain_id = "c1" -addr = "tcp://127.0.0.1:1" -identity_key = "i" - -[[providers.pkcs11]] -chain_ids = ["c1"] -module = "mod.so" -token_label = "comet" -key_label = "validator" -pin_file = "pin.txt" -` - cfgPath := filepath.Join(home, "cometkms.toml") - require.NoError(t, os.WriteFile(cfgPath, []byte(body), 0o600)) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.NoError(t, c.Validate(home)) - require.Equal(t, filepath.Join(home, "mod.so"), c.Providers.PKCS11[0].Module) - require.Equal(t, filepath.Join(home, "pin.txt"), c.Providers.PKCS11[0].PINFile) -} diff --git a/kms/internal/config/config_test.go b/kms/internal/config/config_test.go deleted file mode 100644 index d818dc07b99..00000000000 --- a/kms/internal/config/config_test.go +++ /dev/null @@ -1,181 +0,0 @@ -package config_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/crypto/ed25519" - "github.com/cometbft/cometbft/kms/internal/config" - "github.com/cometbft/cometbft/lp2p" -) - -const good = ` -[[chain]] -id = "cosmoshub-4" -state_file = "STATE" - -[[validator]] -chain_id = "cosmoshub-4" -addr = "tcp://127.0.0.1:26659" -identity_key = "IDENT" - -[[providers.softsign]] -chain_ids = ["cosmoshub-4"] -key_file = "KEY" -` - -func writeCfg(t *testing.T, body string) (cfgPath, home string) { - t.Helper() - home = t.TempDir() - cfgPath = filepath.Join(home, "cometkms.toml") - require.NoError(t, os.WriteFile(cfgPath, []byte(body), 0o600)) - return cfgPath, home -} - -func TestLoadAndValidateGood(t *testing.T) { - cfgPath, home := writeCfg(t, good) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.NoError(t, c.Validate(home)) - - require.Len(t, c.Chains, 1) - require.NotEmpty(t, c.Chains[0].StateFile) - _, statErr := os.Stat(filepath.Dir(c.Chains[0].StateFile)) - require.NoError(t, statErr) -} - -func TestStateFileDefaultsWhenOmitted(t *testing.T) { - body := ` -[[chain]] -id = "c1" - -[[validator]] -chain_id = "c1" -addr = "tcp://127.0.0.1:1" -identity_key = "i" - -[[providers.softsign]] -chain_ids = ["c1"] -key_file = "k" -` - cfgPath, home := writeCfg(t, body) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.NoError(t, c.Validate(home)) - require.Equal(t, filepath.Join(home, "state", "c1.json"), c.Chains[0].StateFile) -} - -func TestRelativeKeyPathsResolvedAgainstHome(t *testing.T) { - body := ` -[[chain]] -id = "c1" - -[[validator]] -chain_id = "c1" -addr = "tcp://127.0.0.1:1" -identity_key = "identity.json" - -[[providers.softsign]] -chain_ids = ["c1"] -key_file = "key.json" -` - cfgPath, home := writeCfg(t, body) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.NoError(t, c.Validate(home)) - - require.Equal(t, filepath.Join(home, "identity.json"), c.Validators[0].IdentityKey) - require.Equal(t, filepath.Join(home, "key.json"), c.Providers.Softsign[0].KeyFile) -} - -func TestAbsoluteKeyPathsLeftUnchanged(t *testing.T) { - absIdent := filepath.Join(t.TempDir(), "abs-identity.json") - absKey := filepath.Join(t.TempDir(), "abs-key.json") - body := ` -[[chain]] -id = "c1" - -[[validator]] -chain_id = "c1" -addr = "tcp://127.0.0.1:1" -identity_key = "` + absIdent + `" - -[[providers.softsign]] -chain_ids = ["c1"] -key_file = "` + absKey + `" -` - cfgPath, home := writeCfg(t, body) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.NoError(t, c.Validate(home)) - - require.Equal(t, absIdent, c.Validators[0].IdentityKey) - require.Equal(t, absKey, c.Providers.Softsign[0].KeyFile) -} - -func TestValidatorReferencesUnknownChain(t *testing.T) { - body := ` -[[chain]] -id = "c1" -[[validator]] -chain_id = "nope" -addr = "tcp://127.0.0.1:1" -identity_key = "i" -[[providers.softsign]] -chain_ids = ["c1"] -key_file = "k" -` - cfgPath, home := writeCfg(t, body) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.ErrorContains(t, c.Validate(home), "unknown chain") -} - -func TestChainWithoutBackendRejected(t *testing.T) { - body := ` -[[chain]] -id = "c1" -[[validator]] -chain_id = "c1" -addr = "tcp://127.0.0.1:1" -identity_key = "i" -` - cfgPath, home := writeCfg(t, body) - c, err := config.Load(cfgPath) - require.NoError(t, err) - require.ErrorContains(t, c.Validate(home), "no backend") -} - -func TestValidatorTransportTCPDefault(t *testing.T) { - v := config.Validator{ChainID: "c1", Addr: "tcp://1.2.3.4:26659", IdentityKey: "i"} - tr, addr, pid, err := v.ParsedTransport() - require.NoError(t, err) - require.Equal(t, config.TransportTCP, tr) - require.Equal(t, "tcp://1.2.3.4:26659", addr) // tcp keeps full addr for DialTCPFn - require.Empty(t, pid) -} - -func TestValidatorTransportNoise(t *testing.T) { - validatorPeer, err := lp2p.IDFromPrivateKey(ed25519.GenPrivKey()) - require.NoError(t, err) - - v := config.Validator{ - ChainID: "c1", - Addr: "noise://" + validatorPeer.String() + "@1.2.3.4:26659", - IdentityKey: "i", - } - tr, addr, pid, err := v.ParsedTransport() - require.NoError(t, err) - require.Equal(t, config.TransportNoise, tr) - require.Equal(t, "1.2.3.4:26659", addr) - require.Equal(t, validatorPeer, pid) -} - -func TestValidatorTransportNoiseInvalid(t *testing.T) { - v := config.Validator{ChainID: "c1", Addr: "noise://1.2.3.4:26659", IdentityKey: "i"} // missing peer id - _, _, _, err := v.ParsedTransport() - require.Error(t, err) -} diff --git a/kms/internal/config/validate.go b/kms/internal/config/validate.go deleted file mode 100644 index cd159522a48..00000000000 --- a/kms/internal/config/validate.go +++ /dev/null @@ -1,185 +0,0 @@ -package config - -import ( - "encoding/hex" - "fmt" - "os" - "path/filepath" -) - -// supportedPKCS11Algorithms mirrors the algo registry in -// internal/backend/pkcs11. It is duplicated here so config validation does not -// have to import the cgo-backed pkcs11 package. Keep the two in sync when adding -// a key type. -var supportedPKCS11Algorithms = map[string]bool{"ed25519": true} - -// Validate resolves defaults and enforces fail-fast invariants. home is the base -// directory used to resolve relative paths and default state files. -func (c *Config) Validate(home string) error { - if len(c.Chains) == 0 { - return fmt.Errorf("config: no [[chain]] declared") - } - - chainIDs := make(map[string]int, len(c.Chains)) // id -> index - for i, ch := range c.Chains { - if ch.ID == "" { - return fmt.Errorf("config: [[chain]] #%d has empty id", i) - } - if _, dup := chainIDs[ch.ID]; dup { - return fmt.Errorf("config: duplicate chain id %q", ch.ID) - } - chainIDs[ch.ID] = i - } - - // Resolve + ensure writable state files. - for i := range c.Chains { - sf := c.Chains[i].StateFile - if sf == "" { - sf = filepath.Join(home, "state", c.Chains[i].ID+".json") - } else if !filepath.IsAbs(sf) { - sf = filepath.Join(home, sf) - } - if err := os.MkdirAll(filepath.Dir(sf), 0o700); err != nil { - return fmt.Errorf("config: state dir for chain %q: %w", c.Chains[i].ID, err) - } - if err := checkWritable(filepath.Dir(sf)); err != nil { - return fmt.Errorf("config: state file for chain %q not writable: %w", c.Chains[i].ID, err) - } - c.Chains[i].StateFile = sf - } - - // Every validator references a known chain. - for i := range c.Validators { - v := c.Validators[i] - if _, ok := chainIDs[v.ChainID]; !ok { - return fmt.Errorf("config: validator references unknown chain %q", v.ChainID) - } - if v.Addr == "" { - return fmt.Errorf("config: validator for chain %q has empty addr", v.ChainID) - } - if v.IdentityKey == "" { - return fmt.Errorf("config: validator for chain %q has empty identity_key", v.ChainID) - } - if _, _, _, perr := v.ParsedTransport(); perr != nil { - return fmt.Errorf("config: validator for chain %q has invalid addr: %w", v.ChainID, perr) - } - // Resolve relative identity_key against home so app.Build consumes the - // resolved path (CWD-relative resolution would silently mint a new key). - if !filepath.IsAbs(v.IdentityKey) { - c.Validators[i].IdentityKey = filepath.Join(home, v.IdentityKey) - } - } - - // Every provider references a known chain; collect which chains have a backend. - hasBackend := make(map[string]bool) - for i := range c.Providers.Softsign { - p := c.Providers.Softsign[i] - if p.KeyFile == "" { - return fmt.Errorf("config: softsign provider has empty key_file") - } - if len(p.ChainIDs) == 0 { - return fmt.Errorf("config: softsign provider with key_file %q has no chain_ids", p.KeyFile) - } - for _, id := range p.ChainIDs { - if _, ok := chainIDs[id]; !ok { - return fmt.Errorf("config: softsign provider references unknown chain %q", id) - } - hasBackend[id] = true - } - // Resolve relative key_file against home. - if !filepath.IsAbs(p.KeyFile) { - c.Providers.Softsign[i].KeyFile = filepath.Join(home, p.KeyFile) - } - } - - for i := range c.Providers.PKCS11 { - if err := c.validatePKCS11Provider(i, home, chainIDs, hasBackend); err != nil { - return err - } - } - - // Every chain must have at least one backend. - for _, ch := range c.Chains { - if !hasBackend[ch.ID] { - return fmt.Errorf("config: chain %q has no backend", ch.ID) - } - } - return nil -} - -// validatePKCS11Provider checks one [[providers.pkcs11]] block, resolves its -// relative paths against home, and records which chains it backs. -func (c *Config) validatePKCS11Provider(i int, home string, chainIDs map[string]int, hasBackend map[string]bool) error { - p := c.Providers.PKCS11[i] - - if p.Module == "" { - return fmt.Errorf("config: pkcs11 provider has empty module") - } - if len(p.ChainIDs) == 0 { - return fmt.Errorf("config: pkcs11 provider with module %q has no chain_ids", p.Module) - } - - // Token selector: exactly one of token_label / slot. - switch { - case p.TokenLabel != "" && p.Slot != nil: - return fmt.Errorf("config: pkcs11 provider sets both token_label and slot (use exactly one)") - case p.TokenLabel == "" && p.Slot == nil: - return fmt.Errorf("config: pkcs11 provider must set token_label or slot") - } - - // Key selector: at least one of key_label / key_id. - if p.KeyLabel == "" && p.KeyID == "" { - return fmt.Errorf("config: pkcs11 provider must set key_label or key_id") - } - if p.KeyID != "" { - if _, err := hex.DecodeString(p.KeyID); err != nil { - return fmt.Errorf("config: pkcs11 provider key_id %q is not valid hex: %w", p.KeyID, err) - } - } - - // PIN source: exactly one of pin / pin_env / pin_file. - n := 0 - for _, set := range []bool{p.PIN != "", p.PINEnv != "", p.PINFile != ""} { - if set { - n++ - } - } - if n != 1 { - return fmt.Errorf("config: pkcs11 provider must set exactly one of pin, pin_env, pin_file (got %d)", n) - } - - // Algorithm: empty defaults to ed25519; otherwise must be registered. - if p.Algorithm != "" && !supportedPKCS11Algorithms[p.Algorithm] { - return fmt.Errorf("config: pkcs11 provider has unknown algorithm %q", p.Algorithm) - } - - // Resolve relative paths against home before checking the module is readable. - if !filepath.IsAbs(p.Module) { - p.Module = filepath.Join(home, p.Module) - } - if p.PINFile != "" && !filepath.IsAbs(p.PINFile) { - p.PINFile = filepath.Join(home, p.PINFile) - } - if _, err := os.Stat(p.Module); err != nil { - return fmt.Errorf("config: pkcs11 provider module %q not readable: %w", p.Module, err) - } - c.Providers.PKCS11[i] = p - - for _, id := range p.ChainIDs { - if _, ok := chainIDs[id]; !ok { - return fmt.Errorf("config: pkcs11 provider references unknown chain %q", id) - } - hasBackend[id] = true - } - return nil -} - -func checkWritable(dir string) error { - f, err := os.CreateTemp(dir, ".writecheck-*") - if err != nil { - return err - } - name := f.Name() - _ = f.Close() - return os.Remove(name) -} diff --git a/kms/internal/identity/identity.go b/kms/internal/identity/identity.go deleted file mode 100644 index da757f48f50..00000000000 --- a/kms/internal/identity/identity.go +++ /dev/null @@ -1,25 +0,0 @@ -// Package identity manages the Ed25519 key that authenticates the SecretConnection -// to a validator. It reuses CometBFT's node-key format and storage. -package identity - -import ( - "fmt" - "os" - "path/filepath" - - "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/p2p" -) - -// LoadOrGen loads the identity key at path, generating and persisting one if it -// does not exist. -func LoadOrGen(path string) (crypto.PrivKey, error) { - if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil { - return nil, fmt.Errorf("identity: mkdir: %w", err) - } - nk, err := p2p.LoadOrGenNodeKey(path) - if err != nil { - return nil, fmt.Errorf("identity: load/gen node key %q: %w", path, err) - } - return nk.PrivKey, nil -} diff --git a/kms/internal/identity/identity_test.go b/kms/internal/identity/identity_test.go deleted file mode 100644 index 1f2a1e88758..00000000000 --- a/kms/internal/identity/identity_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package identity_test - -import ( - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/kms/internal/identity" -) - -func TestLoadOrGenIsStable(t *testing.T) { - path := filepath.Join(t.TempDir(), "identity.json") - - k1, err := identity.LoadOrGen(path) - require.NoError(t, err) - require.NotNil(t, k1) - - k2, err := identity.LoadOrGen(path) - require.NoError(t, err) - require.True(t, k1.PubKey().Equals(k2.PubKey()), "reload must yield same key") -} diff --git a/kms/internal/manager/dialer.go b/kms/internal/manager/dialer.go deleted file mode 100644 index 9e0af7f0514..00000000000 --- a/kms/internal/manager/dialer.go +++ /dev/null @@ -1,45 +0,0 @@ -package manager - -import ( - "errors" - "net" - "time" - - "github.com/cometbft/cometbft/libs/log" - "github.com/cometbft/cometbft/privval" -) - -// errDialerStopped is returned by a backoff dialer after its stop channel closes. -var errDialerStopped = errors.New("cometkms: dialer stopped") - -// backoffDialer wraps a one-shot SocketDialer so it blocks, retrying with capped -// exponential backoff, until it obtains a connection or stop is closed. Because -// it blocks until success, privval's serviceLoop never exits on transient -// outages — giving cometkms full control over reconnect cadence. -func backoffDialer(base privval.SocketDialer, stop <-chan struct{}, logger log.Logger, initial, max time.Duration) privval.SocketDialer { - return func() (net.Conn, error) { - wait := initial - for { - select { - case <-stop: - return nil, errDialerStopped - default: - } - - conn, err := base() - if err == nil { - return conn, nil - } - logger.Error("cometkms: dial failed; backing off", "wait", wait, "err", err) - - select { - case <-stop: - return nil, errDialerStopped - case <-time.After(wait): - } - if wait *= 2; wait > max { - wait = max - } - } - } -} diff --git a/kms/internal/manager/dialer_test.go b/kms/internal/manager/dialer_test.go deleted file mode 100644 index 99ad36345a5..00000000000 --- a/kms/internal/manager/dialer_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package manager - -import ( - "errors" - "net" - "sync/atomic" - "testing" - "time" - - "github.com/cometbft/cometbft/libs/log" - "github.com/stretchr/testify/require" -) - -func TestBackoffDialerRetriesThenSucceeds(t *testing.T) { - var calls int32 - base := func() (net.Conn, error) { - if atomic.AddInt32(&calls, 1) < 3 { - return nil, errors.New("refused") - } - c, _ := net.Pipe() - return c, nil - } - - stop := make(chan struct{}) - d := backoffDialer(base, stop, log.TestingLogger(), time.Millisecond, 5*time.Millisecond) - - conn, err := d() - require.NoError(t, err) - require.NotNil(t, conn) - require.GreaterOrEqual(t, atomic.LoadInt32(&calls), int32(3)) -} - -func TestBackoffDialerUnblocksOnStop(t *testing.T) { - base := func() (net.Conn, error) { return nil, errors.New("always fails") } - stop := make(chan struct{}) - d := backoffDialer(base, stop, log.TestingLogger(), time.Millisecond, 5*time.Millisecond) - - done := make(chan struct{}) - go func() { - _, err := d() - require.Error(t, err) - close(done) - }() - - time.Sleep(10 * time.Millisecond) - close(stop) - - select { - case <-done: - case <-time.After(time.Second): - t.Fatal("dialer did not unblock on stop") - } -} diff --git a/kms/internal/manager/manager.go b/kms/internal/manager/manager.go deleted file mode 100644 index d2a66e5eb44..00000000000 --- a/kms/internal/manager/manager.go +++ /dev/null @@ -1,205 +0,0 @@ -package manager - -import ( - "net" - "sync" - "time" - - "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/libs/log" - "github.com/cometbft/cometbft/libs/protoio" - "github.com/cometbft/cometbft/privval" - privvalproto "github.com/cometbft/cometbft/proto/tendermint/privval" - "github.com/cometbft/cometbft/types" -) - -const ( - defaultDialTimeout = 5 * time.Second - defaultReadTimeout = 10 * time.Second // must exceed the validator's ping interval (~3.3s at default) - defaultWriteTimeout = 5 * time.Second - defaultBackoffInitial = 200 * time.Millisecond - defaultBackoffMax = 10 * time.Second - - // maxRemoteSignerMsgSize mirrors privval's framing cap (10 KiB). - maxRemoteSignerMsgSize = 1024 * 10 -) - -// DefaultDialTimeout is the per-dial timeout used for validator connections. -const DefaultDialTimeout = defaultDialTimeout - -// ValidatorConn describes one outbound signer connection. -type ValidatorConn struct { - ChainID string - Addr string - IdentityKey crypto.PrivKey - Signer types.PrivValidator - // Reconnect controls whether the manager re-dials after an established - // connection drops. The initial connect always uses backoff regardless. - Reconnect bool - // Dialer is the base (already transport-specific) SocketDialer. If nil, the - // manager builds the default tcp:// SecretConnection dialer from Addr. - Dialer privval.SocketDialer -} - -// Manager supervises one validator connection per ValidatorConn, dialing out and -// re-dialing with backoff across outages (including graceful validator restarts). -type Manager struct { - logger log.Logger - conns []ValidatorConn - stop chan struct{} - stopOnce sync.Once - wg sync.WaitGroup -} - -// New builds a Manager. -func New(logger log.Logger, conns []ValidatorConn) *Manager { - return &Manager{logger: logger, conns: conns, stop: make(chan struct{})} -} - -// Start launches one supervised goroutine per connection. It never returns an -// error (kept in the signature for API stability and future validation). -func (m *Manager) Start() error { - for _, c := range m.conns { - m.wg.Add(1) - go m.run(c) - } - return nil -} - -// Stop signals all connections to close and waits for their goroutines to exit. -// Safe to call multiple times. -func (m *Manager) Stop() { - m.stopOnce.Do(func() { close(m.stop) }) - m.wg.Wait() -} - -// run maintains one validator connection: dial (blocking, with backoff), serve -// until the connection breaks (EOF or timeout), then redial — until Stop. -func (m *Manager) run(c ValidatorConn) { - defer m.wg.Done() - - logger := m.logger.With("chain", c.ChainID, "addr", c.Addr) - base := c.Dialer - if base == nil { - base = privval.DialTCPFn(c.Addr, defaultDialTimeout, c.IdentityKey) - } - dialer := backoffDialer(base, m.stop, logger, defaultBackoffInitial, defaultBackoffMax) - - for { - select { - case <-m.stop: - return - default: - } - - conn, err := dialer() - if err != nil { - return // errDialerStopped: shutting down - } - logger.Info("cometkms: connected") - - m.handleConnection(conn, c, logger) - - if !c.Reconnect { - logger.Info("cometkms: reconnect disabled; connection closed") - return - } - logger.Info("cometkms: connection closed; will redial") - } -} - -// handleConnection serves a single established connection until it breaks or the -// manager stops. It owns the connection lifecycle: a monitor goroutine closes the -// conn on shutdown (unblocking a pending read), and deferred cleanup guarantees -// both the conn and the monitor goroutine are released when this returns. Scoping -// this to its own function (rather than deferring inside run's loop) keeps the -// defers per-connection and ensures they still run if serveConn panics mid-unwind. -func (m *Manager) handleConnection(conn net.Conn, c ValidatorConn, logger log.Logger) { - closed := make(chan struct{}) - defer close(closed) - defer func() { _ = conn.Close() }() - - go func() { - select { - case <-m.stop: - _ = conn.Close() - case <-closed: - } - }() - - m.serveConn(conn, c.ChainID, c.Signer, logger) -} - -// serveConn reads validation requests off conn and answers them with the reused -// privval.DefaultValidationRequestHandler, until any read/write error (EOF on a -// graceful restart, or a read-deadline timeout on a silently dead peer) or until -// stop. Returning signals the caller to redial. -func (m *Manager) serveConn(conn net.Conn, chainID string, signer types.PrivValidator, logger log.Logger) { - reader := protoio.NewDelimitedReader(conn, maxRemoteSignerMsgSize) - writer := protoio.NewDelimitedWriter(conn) - - for { - select { - case <-m.stop: - return - default: - } - - if err := conn.SetReadDeadline(time.Now().Add(defaultReadTimeout)); err != nil { - logger.Error("cometkms: set read deadline", "err", err) - return - } - var req privvalproto.Message - if _, err := reader.ReadMsg(&req); err != nil { - logger.Info("cometkms: read failed; dropping connection", "err", err) - return - } - - resp, err := privval.DefaultValidationRequestHandler(signer, req, chainID) - kind, kv, ping := describeRequest(req) - switch { - case err != nil: - // resp already carries an embedded RemoteSignerError; log loudly and still send it. - logger.Error("cometkms: "+kind+" request rejected", append(kv, "err", err)...) - case ping: - logger.Debug("cometkms: ping") - case kind == "pubkey": - logger.Info("cometkms: served pubkey request") - default: - logger.Info("cometkms: signed "+kind, kv...) - } - - if err := conn.SetWriteDeadline(time.Now().Add(defaultWriteTimeout)); err != nil { - logger.Error("cometkms: set write deadline", "err", err) - return - } - if _, err := writer.WriteMsg(&resp); err != nil { - logger.Error("cometkms: write failed; dropping connection", "err", err) - return - } - } -} - -// describeRequest returns a short kind, structured log keyvals, and whether the -// request is a ping (which is logged at debug to avoid flooding the log every -// few seconds). It is used to emit one log line per served request. -func describeRequest(req privvalproto.Message) (kind string, kv []any, ping bool) { - switch r := req.Sum.(type) { - case *privvalproto.Message_SignVoteRequest: - if v := r.SignVoteRequest.GetVote(); v != nil { - return "vote", []any{"height", v.Height, "round", v.Round, "type", v.Type}, false - } - return "vote", nil, false - case *privvalproto.Message_SignProposalRequest: - if p := r.SignProposalRequest.GetProposal(); p != nil { - return "proposal", []any{"height", p.Height, "round", p.Round}, false - } - return "proposal", nil, false - case *privvalproto.Message_PubKeyRequest: - return "pubkey", nil, false - case *privvalproto.Message_PingRequest: - return "ping", nil, true - default: - return "unknown", nil, false - } -} diff --git a/kms/internal/signer/chain_signer.go b/kms/internal/signer/chain_signer.go deleted file mode 100644 index 2a23800f734..00000000000 --- a/kms/internal/signer/chain_signer.go +++ /dev/null @@ -1,94 +0,0 @@ -package signer - -import ( - "context" - "fmt" - "os" - "sync" - - "github.com/cometbft/cometbft/crypto" - cmtjson "github.com/cometbft/cometbft/libs/json" - "github.com/cometbft/cometbft/privval" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - "github.com/cometbft/cometbft/types" - - "github.com/cometbft/cometbft/kms/internal/backend" -) - -// ChainSigner signs consensus messages for one chain, enforcing double-sign -// protection by delegating to a CometBFT *privval.FilePV (identical guard + -// crash-recovery logic). It is safe for concurrent use across multiple validator -// connections for the same chain. -type ChainSigner struct { - chainID string - mu sync.Mutex - fpv *privval.FilePV -} - -var _ types.PrivValidator = (*ChainSigner)(nil) - -// NewChainSigner builds the signer. The backend is wrapped as a crypto.PrivKey -// and handed to privval.NewFilePV; any pre-existing sign-state at stateFile is -// reloaded so double-sign protection survives restarts. The directory containing -// stateFile must already exist (config validation guarantees this). -func NewChainSigner(chainID string, be backend.Signer, stateFile string) (*ChainSigner, error) { - adapter, err := newBackendPrivKey(context.Background(), be) - if err != nil { - return nil, fmt.Errorf("chain %q: load pubkey: %w", chainID, err) - } - - fpv := privval.NewFilePV(adapter, "", stateFile) - - if err := reloadState(fpv, stateFile); err != nil { - return nil, fmt.Errorf("chain %q: reload sign-state: %w", chainID, err) - } - - return &ChainSigner{chainID: chainID, fpv: fpv}, nil -} - -// reloadState loads persisted FilePVLastSignState JSON into fpv.LastSignState, -// preserving the private filePath set by NewFilePV (JSON has no such field). -func reloadState(fpv *privval.FilePV, stateFile string) error { - raw, err := os.ReadFile(stateFile) - if os.IsNotExist(err) { - return nil // fresh start; FilePV begins at height 0 - } - if err != nil { - return err - } - if len(raw) == 0 { - return nil - } - return cmtjson.Unmarshal(raw, &fpv.LastSignState) -} - -// GetPubKey implements types.PrivValidator. -func (c *ChainSigner) GetPubKey() (crypto.PubKey, error) { - c.mu.Lock() - defer c.mu.Unlock() - return c.fpv.GetPubKey() -} - -// SignVote implements types.PrivValidator. -func (c *ChainSigner) SignVote(chainID string, vote *cmtproto.Vote) (err error) { - c.mu.Lock() - defer c.mu.Unlock() - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("chain %q: sign vote failed (state persistence): %v", c.chainID, r) - } - }() - return c.fpv.SignVote(chainID, vote) -} - -// SignProposal implements types.PrivValidator. -func (c *ChainSigner) SignProposal(chainID string, proposal *cmtproto.Proposal) (err error) { - c.mu.Lock() - defer c.mu.Unlock() - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("chain %q: sign proposal failed (state persistence): %v", c.chainID, r) - } - }() - return c.fpv.SignProposal(chainID, proposal) -} diff --git a/kms/internal/signer/chain_signer_test.go b/kms/internal/signer/chain_signer_test.go deleted file mode 100644 index 1aa47223b77..00000000000 --- a/kms/internal/signer/chain_signer_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package signer_test - -import ( - "context" - "os" - "path/filepath" - "testing" - - "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/crypto/ed25519" - cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" - "github.com/cometbft/cometbft/types" - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/kms/internal/signer" -) - -type memBackend struct{ priv crypto.PrivKey } - -func (m memBackend) PubKey(context.Context) (crypto.PubKey, error) { return m.priv.PubKey(), nil } -func (m memBackend) Sign(_ context.Context, b []byte) ([]byte, error) { return m.priv.Sign(b) } - -const chainID = "test-chain-1" - -func newSigner(t *testing.T) (*signer.ChainSigner, crypto.PubKey, string) { - t.Helper() - priv := ed25519.GenPrivKey() - state := filepath.Join(t.TempDir(), "state.json") - cs, err := signer.NewChainSigner(chainID, memBackend{priv: priv}, state) - require.NoError(t, err) - return cs, priv.PubKey(), state -} - -func precommit(h int64, r int32) *cmtproto.Vote { - return &cmtproto.Vote{Type: cmtproto.PrecommitType, Height: h, Round: r} -} - -func TestSignVoteProducesVerifiableSignature(t *testing.T) { - cs, pub, _ := newSigner(t) - v := precommit(10, 0) - require.NoError(t, cs.SignVote(chainID, v)) - require.True(t, pub.VerifySignature(types.VoteSignBytes(chainID, v), v.Signature)) -} - -func TestDoubleSignRegressionRejected(t *testing.T) { - cs, _, _ := newSigner(t) - require.NoError(t, cs.SignVote(chainID, precommit(10, 0))) - err := cs.SignVote(chainID, precommit(9, 0)) - require.Error(t, err) -} - -func TestStatePersistsAcrossReload(t *testing.T) { - priv := ed25519.GenPrivKey() - state := filepath.Join(t.TempDir(), "state.json") - - cs1, err := signer.NewChainSigner(chainID, memBackend{priv: priv}, state) - require.NoError(t, err) - require.NoError(t, cs1.SignVote(chainID, precommit(100, 0))) - - cs2, err := signer.NewChainSigner(chainID, memBackend{priv: priv}, state) - require.NoError(t, err) - require.Error(t, cs2.SignVote(chainID, precommit(50, 0))) - - _, statErr := os.Stat(state) - require.NoError(t, statErr) -} - -func TestVoteExtensionSignedForNonNilPrecommit(t *testing.T) { - cs, pub, _ := newSigner(t) - v := &cmtproto.Vote{ - Type: cmtproto.PrecommitType, - Height: 10, - Round: 0, - BlockID: cmtproto.BlockID{Hash: []byte("01234567890123456789012345678901")}, - } - require.NoError(t, cs.SignVote(chainID, v)) - require.NotEmpty(t, v.ExtensionSignature) - require.True(t, pub.VerifySignature(types.VoteExtensionSignBytes(chainID, v), v.ExtensionSignature)) -} - -func TestConflictingBlockSameHRSRejected(t *testing.T) { - cs, _, _ := newSigner(t) - - blockA := &cmtproto.Vote{ - Type: cmtproto.PrecommitType, - Height: 10, - Round: 0, - BlockID: cmtproto.BlockID{Hash: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")}, - } - require.NoError(t, cs.SignVote(chainID, blockA)) - - // Same H/R/step, DIFFERENT block -> must be refused (conflicting data). - blockB := &cmtproto.Vote{ - Type: cmtproto.PrecommitType, - Height: 10, - Round: 0, - BlockID: cmtproto.BlockID{Hash: []byte("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB")}, - } - require.Error(t, cs.SignVote(chainID, blockB)) -} - -func TestSignProposalVerifiableAndRegressionRejected(t *testing.T) { - cs, pub, _ := newSigner(t) - - prop := &cmtproto.Proposal{Type: cmtproto.ProposalType, Height: 20, Round: 0} - require.NoError(t, cs.SignProposal(chainID, prop)) - require.True(t, pub.VerifySignature(types.ProposalSignBytes(chainID, prop), prop.Signature)) - - // Lower height must be refused. - lower := &cmtproto.Proposal{Type: cmtproto.ProposalType, Height: 19, Round: 0} - require.Error(t, cs.SignProposal(chainID, lower)) -} - -func TestStateSaveFailureReturnsErrorNotPanic(t *testing.T) { - priv := ed25519.GenPrivKey() - dir := t.TempDir() - statePath := filepath.Join(dir, "state.json") - - cs, err := signer.NewChainSigner(chainID, memBackend{priv: priv}, statePath) - require.NoError(t, err) - - // Sabotage persistence: remove any state file and create a DIRECTORY at the - // state path, so the atomic write (temp file + rename onto statePath) fails. - _ = os.Remove(statePath) - require.NoError(t, os.Mkdir(statePath, 0o755)) - - // SignVote must return an error (recovered panic), and must NOT panic the test. - err = cs.SignVote(chainID, precommit(30, 0)) - require.Error(t, err) -} diff --git a/kms/internal/signer/privkey_adapter.go b/kms/internal/signer/privkey_adapter.go deleted file mode 100644 index 6aa967514aa..00000000000 --- a/kms/internal/signer/privkey_adapter.go +++ /dev/null @@ -1,44 +0,0 @@ -package signer - -import ( - "context" - - "github.com/cometbft/cometbft/crypto" - - "github.com/cometbft/cometbft/kms/internal/backend" -) - -// backendPrivKey adapts a backend.Signer to crypto.PrivKey so it can be handed -// to privval.NewFilePV. Only Sign, PubKey, and Type are exercised by the FilePV -// signing path; Bytes and Equals are intentionally unsupported for remote keys. -type backendPrivKey struct { - ctx context.Context - be backend.Signer - pub crypto.PubKey -} - -var _ crypto.PrivKey = (*backendPrivKey)(nil) - -// newBackendPrivKey caches the public key (so PubKey is cheap and FilePV's -// address computation works) and returns the adapter. -func newBackendPrivKey(ctx context.Context, be backend.Signer) (crypto.PrivKey, error) { - pub, err := be.PubKey(ctx) - if err != nil { - return nil, err - } - return &backendPrivKey{ctx: ctx, be: be, pub: pub}, nil -} - -func (k *backendPrivKey) Sign(msg []byte) ([]byte, error) { return k.be.Sign(k.ctx, msg) } -func (k *backendPrivKey) PubKey() crypto.PubKey { return k.pub } -func (k *backendPrivKey) Type() string { return k.pub.Type() } - -// Bytes is unsupported: remote/HSM keys never expose private material. It returns -// nil rather than panicking because crypto.PrivKey requires the method; it is not -// called on the FilePV signing path. -func (k *backendPrivKey) Bytes() []byte { return nil } - -// Equals compares by public key (private material is unavailable). -func (k *backendPrivKey) Equals(other crypto.PrivKey) bool { - return k.pub.Equals(other.PubKey()) -} diff --git a/kms/internal/signer/privkey_adapter_test.go b/kms/internal/signer/privkey_adapter_test.go deleted file mode 100644 index 44aea760efb..00000000000 --- a/kms/internal/signer/privkey_adapter_test.go +++ /dev/null @@ -1,34 +0,0 @@ -package signer - -import ( - "context" - "testing" - - "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/crypto/ed25519" - "github.com/stretchr/testify/require" -) - -// stubBackend is a minimal backend.Signer for tests. -type stubBackend struct{ priv crypto.PrivKey } - -func (s stubBackend) PubKey(context.Context) (crypto.PubKey, error) { return s.priv.PubKey(), nil } -func (s stubBackend) Sign(_ context.Context, b []byte) ([]byte, error) { return s.priv.Sign(b) } - -func TestAdapterSatisfiesPrivKeyAndSigns(t *testing.T) { - priv := ed25519.GenPrivKey() - be := stubBackend{priv: priv} - - pk, err := newBackendPrivKey(context.Background(), be) - require.NoError(t, err) - - // The compile-time interface assertion lives in privkey_adapter.go - // (var _ crypto.PrivKey = (*backendPrivKey)(nil)); here we exercise behavior. - require.True(t, pk.PubKey().Equals(priv.PubKey())) - require.Equal(t, "ed25519", pk.Type()) - - msg := []byte("hello") - sig, err := pk.Sign(msg) - require.NoError(t, err) - require.True(t, pk.PubKey().VerifySignature(msg, sig)) -} diff --git a/kms/internal/transport/noise.go b/kms/internal/transport/noise.go deleted file mode 100644 index fc23cf1951e..00000000000 --- a/kms/internal/transport/noise.go +++ /dev/null @@ -1,44 +0,0 @@ -// Package transport provides privval connection transports for cometkms. -package transport - -import ( - "context" - "fmt" - "net" - "time" - - cmtcrypto "github.com/cometbft/cometbft/crypto" - "github.com/cometbft/cometbft/lp2p" - "github.com/cometbft/cometbft/privval" - "github.com/libp2p/go-libp2p/core/peer" - libp2pnoise "github.com/libp2p/go-libp2p/p2p/security/noise" -) - -// NoiseDialer returns a privval.SocketDialer that TCP-dials addr (host:port) and -// secures the connection with libp2p Noise, using identity as the KMS's libp2p -// key and pinning the validator's peer ID. The returned net.Conn is a -// sec.SecureConn and feeds the existing serve loop unchanged. -func NoiseDialer(addr string, identity cmtcrypto.PrivKey, validator peer.ID, timeout time.Duration) (privval.SocketDialer, error) { - lpk, err := lp2p.PrivateKeyFromCosmosKey(identity) - if err != nil { - return nil, fmt.Errorf("cometkms: noise identity: %w", err) - } - tr, err := libp2pnoise.New(libp2pnoise.ID, lpk, nil) - if err != nil { - return nil, fmt.Errorf("cometkms: noise transport: %w", err) - } - return func() (net.Conn, error) { - raw, err := net.DialTimeout("tcp", addr, timeout) - if err != nil { - return nil, err - } - ctx, cancel := context.WithTimeout(context.Background(), timeout) - defer cancel() - secured, err := tr.SecureOutbound(ctx, raw, validator) - if err != nil { - _ = raw.Close() - return nil, fmt.Errorf("cometkms: noise handshake with %s: %w", validator, err) - } - return secured, nil - }, nil -} diff --git a/kms/internal/transport/noise_test.go b/kms/internal/transport/noise_test.go deleted file mode 100644 index 8ddf39370de..00000000000 --- a/kms/internal/transport/noise_test.go +++ /dev/null @@ -1,69 +0,0 @@ -package transport_test - -import ( - "net" - "testing" - "time" - - "github.com/cometbft/cometbft/crypto/ed25519" - "github.com/cometbft/cometbft/lp2p" - "github.com/cometbft/cometbft/privval" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/stretchr/testify/require" - - "github.com/cometbft/cometbft/kms/internal/transport" -) - -func TestNoiseDialerRoundTrip(t *testing.T) { - serverKey := ed25519.GenPrivKey() - clientKey := ed25519.GenPrivKey() - serverPeer, err := lp2p.IDFromPrivateKey(serverKey) - require.NoError(t, err) - clientPeer, err := lp2p.IDFromPrivateKey(clientKey) - require.NoError(t, err) - - tcpLn, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - nl, err := privval.NewNoiseListener(tcpLn, serverKey, []peer.ID{clientPeer}) - require.NoError(t, err) - defer nl.Close() - - go func() { - c, aerr := nl.Accept() - if aerr == nil { - _, _ = c.Write([]byte("hi")) - _ = c.Close() - } - }() - - dial, err := transport.NoiseDialer(tcpLn.Addr().String(), clientKey, serverPeer, 2*time.Second) - require.NoError(t, err) - conn, err := dial() - require.NoError(t, err) - defer conn.Close() - - buf := make([]byte, 2) - _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second)) - _, err = conn.Read(buf) - require.NoError(t, err) - require.Equal(t, "hi", string(buf)) -} - -func TestNoiseDialerWrongPeerRejected(t *testing.T) { - serverKey := ed25519.GenPrivKey() - clientKey := ed25519.GenPrivKey() - wrongPeer, err := lp2p.IDFromPrivateKey(ed25519.GenPrivKey()) - require.NoError(t, err) - - tcpLn, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - nl, err := privval.NewNoiseListener(tcpLn, serverKey, nil) // allow any - require.NoError(t, err) - defer nl.Close() - go func() { _, _ = nl.Accept() }() - - dial, err := transport.NoiseDialer(tcpLn.Addr().String(), clientKey, wrongPeer, 2*time.Second) - require.NoError(t, err) - _, err = dial() // must fail: server's key != wrongPeer - require.Error(t, err) -} diff --git a/kms/internal/version/version.go b/kms/internal/version/version.go deleted file mode 100644 index 5a13ce52e2e..00000000000 --- a/kms/internal/version/version.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package version exposes the cometkms build version. -package version - -// Version is the cometkms semantic version. Overridable at build time via -// -ldflags "-X github.com/cometbft/cometbft/kms/internal/version.Version=...". -var Version = "0.1.0-dev" - -// String returns the cometkms version string. -func String() string { return Version } diff --git a/kms/internal/version/version_test.go b/kms/internal/version/version_test.go deleted file mode 100644 index 12ef4959767..00000000000 --- a/kms/internal/version/version_test.go +++ /dev/null @@ -1,9 +0,0 @@ -package version - -import "testing" - -func TestStringNotEmpty(t *testing.T) { - if String() == "" { - t.Fatal("version.String() must not be empty") - } -} diff --git a/libs/flowrate/io_test.go b/libs/flowrate/io_test.go index b82f8ff2250..9acd6633fbc 100644 --- a/libs/flowrate/io_test.go +++ b/libs/flowrate/io_test.go @@ -168,7 +168,7 @@ func statusesAreEqual(s1 *Status, s2 *Status) bool { if s1.Active == s2.Active && s1.Start.Equal(s2.Start) && durationsAreEqual(s1.Duration, s2.Duration, maxDeviationForDuration) && - s1.Idle == s2.Idle && + durationsAreEqual(s1.Idle, s2.Idle, maxDeviationForDuration) && s1.Bytes == s2.Bytes && s1.Samples == s2.Samples && ratesAreEqual(s1.InstRate, s2.InstRate, maxDeviationForRate) && @@ -184,7 +184,11 @@ func statusesAreEqual(s1 *Status, s2 *Status) bool { } func durationsAreEqual(d1 time.Duration, d2 time.Duration, maxDeviation time.Duration) bool { - return d2-d1 <= maxDeviation + diff := d2 - d1 + if diff < 0 { + diff = -diff + } + return diff <= maxDeviation } func ratesAreEqual(r1 int64, r2 int64, maxDeviation int64) bool { diff --git a/lp2p/stream.go b/lp2p/stream.go index 0c146cc5b08..07332d96097 100644 --- a/lp2p/stream.go +++ b/lp2p/stream.go @@ -103,7 +103,11 @@ func StreamReadSized(s network.Stream, maxSize uint64) ([]byte, error) { return nil, errors.Wrap(err, "failed to read payload size") } - payloadLimit := min(maxSize, MaxStreamSize) + // Honor the caller's limit; MaxStreamSize is a fallback, not a hard cap. + payloadLimit := maxSize + if payloadLimit == 0 { + payloadLimit = MaxStreamSize + } if payloadSize > payloadLimit { return nil, errors.Errorf("payload is too large (got %d, max %d)", payloadSize, payloadLimit) diff --git a/lp2p/stream_test.go b/lp2p/stream_test.go index 99eb139269c..5c564c43497 100644 --- a/lp2p/stream_test.go +++ b/lp2p/stream_test.go @@ -168,6 +168,22 @@ func TestStream(t *testing.T) { require.Nil(t, out) }) + t.Run("LargerThanMaxStreamSize", func(t *testing.T) { + // maxSize > MaxStreamSize must NOT be clamped — blocksync passes + // RecvMessageCapacity (~100MB) and must be able to receive large blocks. + payload := bytes.Repeat([]byte("x"), MaxStreamSize+1) + frame := append(uint64ToUvarint(uint64(len(payload))), payload...) + + stream := &streamStub{ + conn: &connStub{closed: false}, + readFn: bytes.NewReader(frame).Read, + } + + out, err := StreamReadSized(stream, uint64(len(payload))) + require.NoError(t, err) + require.Equal(t, payload, out) + }) + t.Run("InvalidHeader", func(t *testing.T) { // ARRANGE invalidHeader := []byte{0x80} diff --git a/lp2p/switch.go b/lp2p/switch.go index 1de72edf8f6..cd899de0067 100644 --- a/lp2p/switch.go +++ b/lp2p/switch.go @@ -16,6 +16,7 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" "github.com/libp2p/go-libp2p/core/protocol" + ma "github.com/multiformats/go-multiaddr" "github.com/pkg/errors" ) @@ -404,7 +405,7 @@ func (s *Switch) handleStream(stream network.Stream) { } // 3. Retrieve the peer from the peerSet (or provision if it's not). - peer, err := s.resolvePeer(peerID) + peer, err := s.resolvePeer(peerID, stream.Conn().RemoteMultiaddr()) if err != nil { s.Logger.Error("Failed to resolve peer", "protocol", protocolID, "peer_id", peerID.String(), "err", err) return @@ -465,7 +466,7 @@ func (s *Switch) handleStream(stream network.Stream) { s.reactors.Receive(reactor.name, messageType, envelope, priority) } -func (s *Switch) resolvePeer(id peer.ID) (p2p.Peer, error) { +func (s *Switch) resolvePeer(id peer.ID, connRemoteAddr ma.Multiaddr) (p2p.Peer, error) { key := peerIDToKey(id) // peer exists (99% of the time) @@ -473,9 +474,16 @@ func (s *Switch) resolvePeer(id peer.ID) (p2p.Peer, error) { return peer, nil } + // addrInfo most likely exists... addrInfo := s.host.Peerstore().PeerInfo(id) + + // ... but peer identification runs asynchronously to stream handling, + // thus addrInfo may not yet be populated. fallback to connRemoteAddr. if len(addrInfo.Addrs) == 0 { - return nil, errors.New("peer has no addresses in peerstore") + addrInfo = peer.AddrInfo{ + ID: id, + Addrs: []ma.Multiaddr{connRemoteAddr}, + } } var isPrivate, isPersistent, isUnconditional bool diff --git a/lp2p/switch_test.go b/lp2p/switch_test.go index 06efb8bc902..702b70b0ea9 100644 --- a/lp2p/switch_test.go +++ b/lp2p/switch_test.go @@ -19,7 +19,9 @@ import ( "github.com/cometbft/cometbft/p2p/conn" p2pmock "github.com/cometbft/cometbft/p2p/mock" "github.com/cometbft/cometbft/test/utils" + "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/p2p/protocol/identify" ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" ) @@ -580,6 +582,70 @@ func TestSwitch(t *testing.T) { require.Equal(t, 1, switchB.Peers().Size(), "B should still be connected to A on filter pass") }) + + t.Run("ResolvePeerBeforeIdentify", func(t *testing.T) { + // ARRANGE + // Reproduce the race where B receives a CometBFT stream from A before + // B's identify goroutine has populated B's peerstore with A's address. + // Slowing A's identify responder keeps B's peerstore empty for A. + const ( + channelID = byte(0xF2) + identifyDelay = 500 * time.Millisecond + ) + + hosts := makeTestHosts(t, 2, withLogging()) + hostA, hostB := hosts[0], hosts[1] + + hostA.SetStreamHandler(identify.ID, func(s network.Stream) { + time.Sleep(identifyDelay) + _ = s.Reset() + }) + + channelDescriptor := &conn.ChannelDescriptor{ + ID: channelID, + Priority: 1, + RecvMessageCapacity: 1024, + MessageType: &types.RequestEcho{}, + } + + reactorA := newReactorMock([]*conn.ChannelDescriptor{channelDescriptor}, hostA.Logger()) + switchA, err := NewSwitch(nil, hostA, []SwitchReactor{{Name: "echo", Reactor: reactorA}}, p2p.NopMetrics(), hostA.Logger()) + require.NoError(t, err) + + reactorB := newReactorMock([]*conn.ChannelDescriptor{channelDescriptor}, hostB.Logger()) + switchB, err := NewSwitch(nil, hostB, []SwitchReactor{{Name: "echo", Reactor: reactorB}}, p2p.NopMetrics(), hostB.Logger()) + require.NoError(t, err) + + connectSwitches(t, []*Switch{switchA, switchB}) + + require.Eventually(t, func() bool { + return switchA.Peers().Size() == 1 + }, time.Second, 20*time.Millisecond, "A should see B as peer") + + // Pre-condition: B's peerstore must not have A's address yet. + // libp2p only populates the peerstore via identify, not from the raw + // connection dial. Blocking A's identify responder keeps B's peerstore + // for A empty. + require.Empty(t, hostB.Peerstore().Addrs(hostA.ID()), + "B should not know A's address yet (identify blocked)") + + // ACT + switchA.BroadcastAsync(p2p.Envelope{ + ChannelID: channelID, + Message: &types.RequestEcho{Message: "hello"}, + }) + + // ASSERT + require.Eventually(t, func() bool { + envs := reactorB.receivedEnvelopes() + if len(envs) != 1 { + return false + } + req, ok := envs[0].Message.(*types.RequestEcho) + return ok && req.Message == "hello" + }, 2*time.Second, 20*time.Millisecond, + "B must receive the message even though B's peerstore was empty for A at stream time") + }) } // filteringReactor is a mock reactor that optionally filters messages via the diff --git a/mempool/app_reactor.go b/mempool/app_reactor.go index b73ac8c2393..155956a4a84 100644 --- a/mempool/app_reactor.go +++ b/mempool/app_reactor.go @@ -6,12 +6,15 @@ import ( "sync/atomic" "github.com/cometbft/cometbft/config" + "github.com/cometbft/cometbft/internal/protowire" "github.com/cometbft/cometbft/p2p" protomem "github.com/cometbft/cometbft/proto/tendermint/mempool" "github.com/cometbft/cometbft/types" "github.com/pkg/errors" ) +var _ p2p.MsgBytesFilter = (*AppReactor)(nil) + // AppReactor for interacting with AppMempool type AppReactor struct { p2p.BaseReactor @@ -73,10 +76,7 @@ func (r *AppReactor) OnStart() error { // fallback to max tx bytes if max batch bytes is not set // most chains use 1MB which will definitely fit many small txs - maxBatchSizeBytes := r.config.MaxTxBytes - if r.config.MaxBatchBytes > 0 { - maxBatchSizeBytes = r.config.MaxBatchBytes - } + maxBatchSizeBytes := r.effectiveMaxBatchBytes() if !r.switchedOn.Load() { select { @@ -101,10 +101,10 @@ func (r *AppReactor) OnStop() { // GetChannels implements p2p.BaseReactor. func (r *AppReactor) GetChannels() []*p2p.ChannelDescriptor { - largestTx := make([]byte, r.config.MaxTxBytes) + // Mirror effectiveMaxBatchBytes so RecvMessageCapacity is always >= the largest Message we send. batchMsg := protomem.Message{ Sum: &protomem.Message_Txs{ - Txs: &protomem.Txs{Txs: [][]byte{largestTx}}, + Txs: &protomem.Txs{Txs: [][]byte{make([]byte, r.effectiveMaxBatchBytes())}}, }, } @@ -118,6 +118,14 @@ func (r *AppReactor) GetChannels() []*p2p.ChannelDescriptor { } } +// effectiveMaxBatchBytes returns MaxBatchBytes when set, else MaxTxBytes. +func (r *AppReactor) effectiveMaxBatchBytes() int { + if r.config.MaxBatchBytes > 0 { + return r.config.MaxBatchBytes + } + return r.config.MaxTxBytes +} + // WaitSync used for backward compatibility with external callers func (r *AppReactor) WaitSync() bool { return !r.enabled() @@ -134,6 +142,14 @@ func (r *AppReactor) EnableInOutTxs() { close(r.waitForSwitchingOnCh) } +// FilterMsgBytes implements p2p.MsgBytesFilter. +func (r *AppReactor) FilterMsgBytes(chID byte, _ p2p.Peer, msgBytes []byte) error { + if chID != MempoolChannel || len(msgBytes) == 0 { + return nil + } + return filterMempoolMsgBytes(msgBytes, r.config.MaxTxBytes, r.config.MaxBatchBytes) +} + func (r *AppReactor) Receive(e p2p.Envelope) { if !r.enabled() { r.Logger.Debug("Ignored mempool message received while syncing") @@ -226,7 +242,11 @@ func txsFromEnvelope(e p2p.Envelope) ([]types.Tx, error) { } } -// chunkTxs chunks transactions into batches of maxBatchSizeBytes +// chunkTxs chunks transactions into batches of maxBatchSizeBytes. +// Each tx contributes len(tx) + proto framing (field tag + varint length prefix) +// to the serialized Txs message size; we account for that overhead here so the +// encoded batch never exceeds the receiver's RecvMessageCapacity. +// // example: [tx1, tx2, tx3, tx4, tx5, ...] -> [[tx1, tx2], [tx3, tx4], [tx5], ...] // // note: we can optimize []types.Txs to [][][]byte + have less allocs, @@ -243,7 +263,7 @@ func chunkTxs(txs types.Txs, maxBatchSizeBytes int) []types.Txs { lastChunk := types.Txs{} for _, tx := range txs { - txSizeBytes := len(tx) + txSizeBytes := len(tx) + protowire.RepeatedBytesEntrySize(len(tx)) // tx won't fit into chunk, add current chunk to chunks and start a new one if (lastChunkSizeBytes + txSizeBytes) > maxBatchSizeBytes { diff --git a/mempool/app_reactor_test.go b/mempool/app_reactor_test.go index d77ebc05a20..c18030b3d1e 100644 --- a/mempool/app_reactor_test.go +++ b/mempool/app_reactor_test.go @@ -9,15 +9,85 @@ import ( abcimock "github.com/cometbft/cometbft/abci/client/mocks" abci "github.com/cometbft/cometbft/abci/types" "github.com/cometbft/cometbft/config" + "github.com/cometbft/cometbft/internal/protowire" "github.com/cometbft/cometbft/libs/log" "github.com/cometbft/cometbft/libs/rand" "github.com/cometbft/cometbft/p2p" + protomem "github.com/cometbft/cometbft/proto/tendermint/mempool" "github.com/cometbft/cometbft/types" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) func TestAppReactor(t *testing.T) { + t.Run("effectiveMaxBatchBytes", func(t *testing.T) { + for _, tt := range []struct { + name string + maxTxBytes int + maxBatchBytes int + want int + }{ + {"MaxBatchBytes unset falls back to MaxTxBytes", 1000, 0, 1000}, + {"MaxBatchBytes set and larger than MaxTxBytes", 1000, 5000, 5000}, + {"MaxBatchBytes set and smaller than MaxTxBytes", 1000, 500, 500}, + } { + t.Run(tt.name, func(t *testing.T) { + r := &AppReactor{config: &config.MempoolConfig{ + MaxTxBytes: tt.maxTxBytes, + MaxBatchBytes: tt.maxBatchBytes, + }} + require.Equal(t, tt.want, r.effectiveMaxBatchBytes()) + }) + } + }) + + // Regression test for the peer-teardown bug: every batch produced by chunkTxs + // must serialize to a Message that fits within RecvMessageCapacity. + t.Run("GetChannels", func(t *testing.T) { + for _, tt := range []struct { + name string + maxTxBytes int + maxBatchBytes int + txSize int + txCount int + }{ + // Many 1-byte txs: worst-case proto framing ratio (2 overhead bytes per 1 data byte). + {"1-byte txs default batch limit", 10000, 0, 1, 10000}, + // Txs at the 127-byte boundary (varint transitions from 1 to 2 bytes at 128). + {"127-byte txs default batch limit", 10000, 0, 127, 100}, + {"128-byte txs default batch limit", 10000, 0, 128, 100}, + // Large txs near MaxTxBytes. + {"large txs default batch limit", 10000, 0, 9000, 5}, + // MaxBatchBytes set larger than MaxTxBytes. + {"small txs explicit batch limit", 1000, 5000, 1, 5000}, + } { + t.Run(tt.name, func(t *testing.T) { + r := &AppReactor{config: &config.MempoolConfig{ + MaxTxBytes: tt.maxTxBytes, + MaxBatchBytes: tt.maxBatchBytes, + }} + + txs := make(types.Txs, tt.txCount) + for i := range txs { + txs[i] = make(types.Tx, tt.txSize) + } + + recvCap := r.GetChannels()[0].RecvMessageCapacity + batches := chunkTxs(txs, r.effectiveMaxBatchBytes()) + + for i, batch := range batches { + msg := &protomem.Message{ + Sum: &protomem.Message_Txs{ + Txs: &protomem.Txs{Txs: batch.ToSliceOfBytes()}, + }, + } + msgSize := msg.Size() + require.LessOrEqual(t, msgSize, recvCap, + "batch %d serialized size %d exceeds RecvMessageCapacity %d", i, msgSize, recvCap) + } + }) + } + }) const ( timeout = 5 * time.Second interval = 200 * time.Millisecond @@ -172,9 +242,11 @@ func TestChunkTxs(t *testing.T) { output: [][]int{{100}}, }, { + // Two 100-byte txs each cost 102 effective bytes (2 bytes proto framing). + // 204 fits exactly two; a third would overflow. name: "basic", input: []int{100, 100, 100}, - size: 200, + size: 204, output: [][]int{{100, 100}, {100}}, }, { @@ -184,10 +256,12 @@ func TestChunkTxs(t *testing.T) { output: [][]int{{100}, {100}, {100}}, }, { + // With 2-byte proto framing per tx, [20,30] sums to 54 effective bytes; + // adding 50 would reach 106 > 100, so [50] starts a new chunk with [2]. name: "edge-case", input: []int{101, 20, 30, 50, 2, 102, 3}, size: 100, - output: [][]int{{101}, {20, 30, 50}, {2}, {102}, {3}}, + output: [][]int{{101}, {20, 30}, {50, 2}, {102}, {3}}, }, } { t.Run(tt.name, func(t *testing.T) { @@ -207,6 +281,18 @@ func TestChunkTxs(t *testing.T) { for i, chunk := range actual { require.Equal(t, len(expected[i]), len(chunk), "chunk length mismatch (#%d)", i) + + // Verify each multi-tx chunk's effective proto size stays within the limit. + // Single-tx chunks are exempt: an oversized lone tx must still be forwarded. + if len(chunk) <= 1 { + continue + } + effectiveSize := 0 + for _, tx := range chunk { + effectiveSize += len(tx) + protowire.RepeatedBytesEntrySize(len(tx)) + } + require.LessOrEqual(t, effectiveSize, tt.size, + "chunk #%d effective size %d exceeds limit %d", i, effectiveSize, tt.size) } }) } diff --git a/mempool/filter.go b/mempool/filter.go new file mode 100644 index 00000000000..2cd56704c33 --- /dev/null +++ b/mempool/filter.go @@ -0,0 +1,129 @@ +package mempool + +import ( + "errors" + "fmt" + + "github.com/cometbft/cometbft/internal/protowire" +) + +// Field numbers we care about. Both happen to be 1: the Message oneof's only +// member is Txs (field 1), and within Txs the repeated tx entries are field 1. +const ( + fieldMessageTxs = 1 // Message.sum: `Txs txs = 1` + fieldTxsEntry = 1 // Txs: `repeated bytes txs = 1` +) + +var ( + errNoTransactions = errors.New("mempool message contains no transactions") + errEmptyTransaction = errors.New("mempool batch contains an empty transaction") +) + +// batchTally accumulates what has been seen so far while scanning a batch. +type batchTally struct { + count int // number of transaction entries + totalBytes int // sum of their sizes +} + +// filterMempoolMsgBytes enforces the filtering rules above against +// the raw wire bytes of a tendermint.mempool.Message. +// +// A single tx can be as large as maxTxBytes even when maxBatchBytes is smaller, +// so the whole-message ceiling is the larger of the two. A non-positive value +// for either limit disables that check. +// +// Filtering rules: +// +// 1. Well-formed: every varint, tag and length prefix must stay within the +// buffer. Truncated, overflowing or out-of-bounds encodings are rejected. +// 2. No empty entries: every transaction must have at least one byte. An empty +// entry carries no payload yet still costs an allocation, so it is never +// legitimate — this is the core of the amplification attack. +// 3. Per-tx bound: no single transaction may exceed maxTxBytes. +// 4. Per-batch bound: the sum of all transaction sizes may not exceed +// max(maxTxBytes, maxBatchBytes). +// 5. Non-empty message: the message must carry at least one transaction. +func filterMempoolMsgBytes(msgBytes []byte, maxTxBytes, maxBatchBytes int) error { + batchBudget := max(maxTxBytes, maxBatchBytes) + var tally batchTally + + msg := protowire.NewWireCursor(msgBytes) + for !msg.AtEnd() { + fieldNum, wireType, err := msg.ReadTag() + if err != nil { + return err + } + + // Anything that is not the Txs submessage is skipped, so unknown or + // future fields do not break the scan. + if fieldNum != fieldMessageTxs || wireType != protowire.WireBytes { + if err := msg.SkipField(wireType); err != nil { + return err + } + continue + } + + txsBytes, err := msg.ReadLengthDelimited() + if err != nil { + return err + } + if err := scanTxsSubmessage(txsBytes, maxTxBytes, batchBudget, &tally); err != nil { + return err + } + } + + if tally.count == 0 { + return errNoTransactions + } + return nil +} + +// scanTxsSubmessage walks a Txs submessage (`repeated bytes txs = 1`), +// validating each transaction entry against the limits and folding it into the +// tally. +func scanTxsSubmessage(txsBytes []byte, maxTxBytes, batchBudget int, tally *batchTally) error { + txs := protowire.NewWireCursor(txsBytes) + for !txs.AtEnd() { + fieldNum, wireType, err := txs.ReadTag() + if err != nil { + return err + } + + if fieldNum != fieldTxsEntry || wireType != protowire.WireBytes { + if err := txs.SkipField(wireType); err != nil { + return err + } + continue + } + + tx, err := txs.ReadLengthDelimited() + if err != nil { + return err + } + if err := applyRules(tx, maxTxBytes, batchBudget, tally); err != nil { + return err + } + } + return nil +} + +// applyRules applies the per-entry rules (2-4) to a single transaction. +func applyRules(tx []byte, maxTxBytes, batchBudget int, tally *batchTally) error { + // No empty transaction + if len(tx) == 0 { + return errEmptyTransaction + } + // Transaction cannot exceed maxTxBytes + if maxTxBytes > 0 && len(tx) > maxTxBytes { + return fmt.Errorf("transaction size %d exceeds max_tx_bytes %d", len(tx), maxTxBytes) + } + + tally.count++ + tally.totalBytes += len(tx) + + // The sum of all transaction sizes may not exceed the batch budget. + if batchBudget > 0 && tally.totalBytes > batchBudget { + return fmt.Errorf("mempool batch exceeds %d byte budget", batchBudget) + } + return nil +} diff --git a/mempool/filter_test.go b/mempool/filter_test.go new file mode 100644 index 00000000000..dbbaaddc966 --- /dev/null +++ b/mempool/filter_test.go @@ -0,0 +1,161 @@ +package mempool + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cometbft/cometbft/config" + protomem "github.com/cometbft/cometbft/proto/tendermint/mempool" +) + +// marshalTxsMsg builds the wire bytes for a wrapped tendermint.mempool.Message +// carrying the given raw transactions, exactly as it would appear on the wire. +func marshalTxsMsg(t *testing.T, txs [][]byte) []byte { + t.Helper() + msg := &protomem.Message{Sum: &protomem.Message_Txs{Txs: &protomem.Txs{Txs: txs}}} + b, err := msg.Marshal() + require.NoError(t, err) + return b +} + +func TestFilterMempoolMsgBytes(t *testing.T) { + const maxTxBytes = 1024 + const maxBatchBytes = 4096 + + // A batch packed with empty entries: cheap on the wire, but len(txs) is huge. + emptyEntries := make([][]byte, 10000) + for i := range emptyEntries { + emptyEntries[i] = []byte{} + } + + // A batch of one-byte entries whose declared sizes blow the byte budget. + overBudget := make([][]byte, maxBatchBytes+1) + for i := range overBudget { + overBudget[i] = []byte{0x01} + } + + testCases := []struct { + name string + txs [][]byte + wantErr bool + }{ + { + name: "valid single tx", + txs: [][]byte{[]byte("hello world")}, + wantErr: false, + }, + { + name: "valid batch", + txs: [][]byte{[]byte("tx-one"), []byte("tx-two"), []byte("tx-three")}, + wantErr: false, + }, + { + name: "valid tx at max size", + txs: [][]byte{make([]byte, maxTxBytes)}, + wantErr: false, + }, + { + name: "empty txs list", + txs: [][]byte{}, + wantErr: true, + }, + { + name: "single empty entry", + txs: [][]byte{{}}, + wantErr: true, + }, + { + name: "empty-entry packing attack", + txs: emptyEntries, + wantErr: true, + }, + { + name: "tx exceeds max_tx_bytes", + txs: [][]byte{make([]byte, maxTxBytes+1)}, + wantErr: true, + }, + { + name: "batch exceeds byte budget", + txs: overBudget, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + msgBytes := marshalTxsMsg(t, tc.txs) + err := filterMempoolMsgBytes(msgBytes, maxTxBytes, maxBatchBytes) + if tc.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestFilterMempoolMsgBytes_Unlimited(t *testing.T) { + // With maxTxBytes == 0 and maxBatchBytes == 0 the size checks are disabled, + // but empty entries are still rejected (they are never valid txs). + big := marshalTxsMsg(t, [][]byte{make([]byte, 1<<20)}) + require.NoError(t, filterMempoolMsgBytes(big, 0, 0)) + + require.Error(t, filterMempoolMsgBytes(marshalTxsMsg(t, [][]byte{{}}), 0, 0)) +} + +func TestFilterMempoolMsgBytes_Malformed(t *testing.T) { + // Truncated varint for the outer Message field length. + require.Error(t, filterMempoolMsgBytes([]byte{0x0a, 0xff}, 1024, 4096)) + + // Length that runs past the end of the buffer. + require.Error(t, filterMempoolMsgBytes([]byte{0x0a, 0x7f}, 1024, 4096)) +} + +func TestFilterMempoolMsgBytes_DisguisedFieldNumberNotBypassed(t *testing.T) { + inner := []byte{ + 0x0a, 0x01, 0x41, // field 1, len 1, "A" -> real tx + 0x8a, 0x80, 0x80, 0x80, 0x80, 0x01, 0x00, // disguised field 1, len 0 -> empty tx + } + msgBytes := append([]byte{0x0a, byte(len(inner))}, inner...) // Message.txs = inner + + // Sanity check: the real parser really does decode two entries, the second + // empty + var msg protomem.Message + require.NoError(t, msg.Unmarshal(msgBytes)) + require.Len(t, msg.GetTxs().GetTxs(), 2) + require.Empty(t, msg.GetTxs().GetTxs()[1]) + + // The filter must reject it (empty transaction), matching the parser. + require.Error(t, filterMempoolMsgBytes(msgBytes, 1024, 4096)) +} + +// The per-batch budget is the larger of maxTxBytes and maxBatchBytes, so a +// single tx bigger than maxBatchBytes but within maxTxBytes must still pass. +func TestFilterMempoolMsgBytes_BatchBudgetIsMax(t *testing.T) { + const maxTxBytes = 4096 + const maxBatchBytes = 1024 + + // One tx larger than maxBatchBytes but within maxTxBytes: allowed. + big := marshalTxsMsg(t, [][]byte{make([]byte, maxBatchBytes+1)}) + require.NoError(t, filterMempoolMsgBytes(big, maxTxBytes, maxBatchBytes)) + + // A batch whose total exceeds the larger limit is still rejected. + over := marshalTxsMsg(t, [][]byte{make([]byte, maxTxBytes), make([]byte, 1)}) + require.Error(t, filterMempoolMsgBytes(over, maxTxBytes, maxBatchBytes)) +} + +func TestReactorFilterMsgBytes_ChannelGuard(t *testing.T) { + cfg := config.DefaultMempoolConfig() + r := &AppReactor{config: cfg} + + bad := marshalTxsMsg(t, [][]byte{{}, {}, {}}) + + // Non-mempool channel: not our concern, must pass through untouched. + assert.NoError(t, r.FilterMsgBytes(byte(0x00), nil, bad)) + // Empty payload: nothing to validate. + assert.NoError(t, r.FilterMsgBytes(MempoolChannel, nil, nil)) + // Mempool channel with abusive payload: rejected. + assert.Error(t, r.FilterMsgBytes(MempoolChannel, nil, bad)) +} diff --git a/mempool/reactor.go b/mempool/reactor.go index 2548cd28db9..c5951dc494e 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -16,6 +16,8 @@ import ( "golang.org/x/sync/semaphore" ) +var _ p2p.MsgBytesFilter = (*Reactor)(nil) + // Reactor handles mempool tx broadcasting amongst peers. // It maintains a map from peer ID to counter, to prevent gossiping txs to the // peers you received it from. @@ -146,6 +148,14 @@ func (memR *Reactor) RemovePeer(peer p2p.Peer, _ any) { // broadcast routine checks if peer is gone and returns } +// FilterMsgBytes implements p2p.MsgBytesFilter. +func (memR *Reactor) FilterMsgBytes(chID byte, _ p2p.Peer, msgBytes []byte) error { + if chID != MempoolChannel || len(msgBytes) == 0 { + return nil + } + return filterMempoolMsgBytes(msgBytes, memR.config.MaxTxBytes, memR.config.MaxBatchBytes) +} + // Receive implements Reactor. // It adds any received transactions to the mempool. func (memR *Reactor) Receive(e p2p.Envelope) { diff --git a/node/node.go b/node/node.go index f1a66a72c2e..896d4128aea 100644 --- a/node/node.go +++ b/node/node.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "io" "net" "net/http" "os" @@ -744,6 +745,16 @@ func (n *Node) OnStop() { n.Logger.Error("Error closing indexerService", "err", err) } } + // Close the priv validator before stopping the reactors: sw.Stop waits on + // the consensus receiveRoutine, which can be stuck retrying a gone remote + // signer. Closing aborts that retry loop. (RetrySignerClient is not a + // service.Service, so the assertion below never fires for the socket client.) + if c, ok := n.privValidator.(io.Closer); ok { + if err := c.Close(); err != nil { + n.Logger.Error("Error closing private validator", "err", err) + } + } + // now stop the reactors if err := n.sw.Stop(); err != nil { n.Logger.Error("Error closing switch", "err", err) diff --git a/privval/retry_signer_client.go b/privval/retry_signer_client.go index 271e146474c..606f1b0876b 100644 --- a/privval/retry_signer_client.go +++ b/privval/retry_signer_client.go @@ -2,6 +2,7 @@ package privval import ( "fmt" + "sync" "time" "github.com/cometbft/cometbft/crypto" @@ -15,20 +16,37 @@ type RetrySignerClient struct { next *SignerClient retries int timeout time.Duration + + quitOnce sync.Once + quit chan struct{} // closed by Close to abort in-flight retry loops } // NewRetrySignerClient returns RetrySignerClient. If +retries+ is 0, the // client will be retrying each operation indefinitely. func NewRetrySignerClient(sc *SignerClient, retries int, timeout time.Duration) *RetrySignerClient { - return &RetrySignerClient{sc, retries, timeout} + return &RetrySignerClient{next: sc, retries: retries, timeout: timeout, quit: make(chan struct{})} } var _ types.PrivValidator = (*RetrySignerClient)(nil) +// Close aborts any in-flight retry loop and closes the underlying client. Safe +// to call more than once. The abort unblocks node shutdown, which waits on the +// consensus receiveRoutine that signs synchronously. func (sc *RetrySignerClient) Close() error { + sc.quitOnce.Do(func() { close(sc.quit) }) return sc.next.Close() } +// sleep waits for timeout, returning false if Close was called first. +func (sc *RetrySignerClient) sleep() bool { + select { + case <-sc.quit: + return false + case <-time.After(sc.timeout): + return true + } +} + func (sc *RetrySignerClient) IsConnected() bool { return sc.next.IsConnected() } @@ -58,7 +76,9 @@ func (sc *RetrySignerClient) GetPubKey() (crypto.PubKey, error) { if _, ok := err.(*RemoteSignerError); ok { return nil, err } - time.Sleep(sc.timeout) + if !sc.sleep() { + return nil, fmt.Errorf("aborted getting pubkey: %w", err) + } } return nil, fmt.Errorf("exhausted all attempts to get pubkey: %w", err) } @@ -74,7 +94,9 @@ func (sc *RetrySignerClient) SignVote(chainID string, vote *cmtproto.Vote) error if _, ok := err.(*RemoteSignerError); ok { return err } - time.Sleep(sc.timeout) + if !sc.sleep() { + return fmt.Errorf("aborted signing vote: %w", err) + } } return fmt.Errorf("exhausted all attempts to sign vote: %w", err) } @@ -90,7 +112,9 @@ func (sc *RetrySignerClient) SignProposal(chainID string, proposal *cmtproto.Pro if _, ok := err.(*RemoteSignerError); ok { return err } - time.Sleep(sc.timeout) + if !sc.sleep() { + return fmt.Errorf("aborted signing proposal: %w", err) + } } return fmt.Errorf("exhausted all attempts to sign proposal: %w", err) } diff --git a/privval/retry_signer_client_test.go b/privval/retry_signer_client_test.go new file mode 100644 index 00000000000..30999fc2953 --- /dev/null +++ b/privval/retry_signer_client_test.go @@ -0,0 +1,46 @@ +package privval + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/cometbft/cometbft/crypto/ed25519" + "github.com/cometbft/cometbft/libs/log" + cmtproto "github.com/cometbft/cometbft/proto/tendermint/types" +) + +// TestRetrySignerClientCloseAborts checks Close aborts an in-flight retry loop +// instead of draining the full retry budget (which would wedge node shutdown). +func TestRetrySignerClientCloseAborts(t *testing.T) { + // Listener with no remote signer ever dialing in: every attempt blocks in + // WaitConnection until timeoutAccept, then errors. + endpoint := newSignerListenerEndpoint(log.TestingLogger(), "tcp://127.0.0.1:0", testTimeoutReadWrite) + require.NoError(t, endpoint.Start()) + t.Cleanup(func() { _ = endpoint.Stop() }) + + sc, err := NewSignerClient(endpoint, "chain-id") + require.NoError(t, err) + + // Large budget: if Close didn't abort, this would take many seconds. + rsc := NewRetrySignerClient(sc, 100, 50*time.Millisecond) + + done := make(chan error, 1) + go func() { + done <- rsc.SignVote("chain-id", &cmtproto.Vote{ValidatorAddress: ed25519.GenPrivKey().PubKey().Address()}) + }() + + // Let one attempt get underway, then close. + time.Sleep(100 * time.Millisecond) + require.NoError(t, rsc.Close()) + + select { + case err := <-done: + require.Error(t, err) // aborted, not signed + case <-time.After(testTimeoutAccept + 2*time.Second): + t.Fatal("SignVote did not abort after Close") + } + + require.NotPanics(t, func() { _ = rsc.Close() }) // idempotent +} diff --git a/privval/signer_client_test.go b/privval/signer_client_test.go index 91fe69c3094..6a8d66d98aa 100644 --- a/privval/signer_client_test.go +++ b/privval/signer_client_test.go @@ -197,6 +197,48 @@ func TestSignerVote(t *testing.T) { } } +// A precommit carrying a max-size vote extension (plus two signatures) far +// exceeds the old 10 KiB remote signer message cap; it must round-trip. +func TestSignerVoteMaxSizeExtension(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + hash := cmtrand.Bytes(tmhash.Size) + valAddr := cmtrand.Bytes(crypto.AddressSize) + ext := cmtrand.Bytes(types.MaxVoteExtensionSize) + newVote := func() *types.Vote { + return &types.Vote{ + Type: cmtproto.PrecommitType, + Height: 1, + Round: 2, + BlockID: types.BlockID{Hash: hash, PartSetHeader: types.PartSetHeader{Hash: hash, Total: 2}}, + Timestamp: ts, + ValidatorAddress: valAddr, + ValidatorIndex: 1, + Extension: ext, + } + } + want, have := newVote(), newVote() + + t.Cleanup(func() { + if err := tc.signerServer.Stop(); err != nil { + t.Error(err) + } + }) + t.Cleanup(func() { + if err := tc.signerClient.Close(); err != nil { + t.Error(err) + } + }) + + wantpb, havepb := want.ToProto(), have.ToProto() + require.NoError(t, tc.mockPV.SignVote(tc.chainID, wantpb)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, havepb)) + + assert.Equal(t, wantpb.Signature, havepb.Signature) + assert.Equal(t, wantpb.ExtensionSignature, havepb.ExtensionSignature) + } +} + func TestSignerVoteResetDeadline(t *testing.T) { for _, tc := range getSignerTestCases(t) { ts := time.Now() diff --git a/privval/signer_endpoint.go b/privval/signer_endpoint.go index 84dbde8f355..6489c9925b8 100644 --- a/privval/signer_endpoint.go +++ b/privval/signer_endpoint.go @@ -9,12 +9,19 @@ import ( "github.com/cometbft/cometbft/libs/service" cmtsync "github.com/cometbft/cometbft/libs/sync" privvalproto "github.com/cometbft/cometbft/proto/tendermint/privval" + "github.com/cometbft/cometbft/types" ) const ( defaultTimeoutReadWriteSeconds = 5 ) +// maxRemoteSignerMsgSize bounds messages read from the remote signer +// connection. The largest legitimate message is a precommit vote carrying a +// max-size extension and two max-size signatures (e.g. ML-DSA-65); 1 KiB of +// slack covers the remaining vote fields and proto framing. +var maxRemoteSignerMsgSize = types.MaxVoteExtensionSize + 2*types.MaxSignatureSize + 1024 + type signerEndpoint struct { service.BaseService @@ -92,7 +99,6 @@ func (se *signerEndpoint) ReadMessage() (msg privvalproto.Message, err error) { if err != nil { return } - const maxRemoteSignerMsgSize = 1024 * 10 protoReader := protoio.NewDelimitedReader(se.conn, maxRemoteSignerMsgSize) _, err = protoReader.ReadMsg(&msg) if _, ok := err.(timeoutError); ok { diff --git a/scripts/qa/reporting/requirements.txt b/scripts/qa/reporting/requirements.txt index 6e1149443fc..c35bf42f610 100644 --- a/scripts/qa/reporting/requirements.txt +++ b/scripts/qa/reporting/requirements.txt @@ -5,7 +5,7 @@ kiwisolver==1.4.4 matplotlib==3.6.3 numpy==1.24.2 packaging==21.3 -Pillow==12.2.0 +Pillow==12.3.0 pyparsing==3.0.9 python-dateutil==2.8.2 six==1.16.0 diff --git a/state/execution.go b/state/execution.go index d682def3171..b21acec2f29 100644 --- a/state/execution.go +++ b/state/execution.go @@ -538,6 +538,29 @@ func (blockExec *BlockExecutor) setLastValidatedBlock(old, new *types.Block) { //--------------------------------------------------------- // Helper functions for executing blocks and updating state +// fastValidatorLoader is the duck-typed interface for stores that can return a +// ValidatorSet without advancing proposer priorities. dbStore implements it +// (see state/store.go); external Store implementations don't have to. The +// helper below uses the fast path when available and falls back to +// LoadValidators otherwise, so the addition stays additive for third-party +// Store implementors. +type fastValidatorLoader interface { + LoadValidatorsFast(int64) (*types.ValidatorSet, error) +} + +// loadValidatorsForCommitInfo loads the ValidatorSet at height for building ABCI +// CommitInfo records, which only read validator addresses and voting powers. +// When store implements LoadValidatorsFast it is used to skip the +// IncrementProposerPriority loop that dominates replay time on chains with +// infrequent validator-set changes (issue #1693); otherwise it falls back to +// LoadValidators. +func loadValidatorsForCommitInfo(store Store, height int64) (*types.ValidatorSet, error) { + if fast, ok := store.(fastValidatorLoader); ok { + return fast.LoadValidatorsFast(height) + } + return store.LoadValidators(height) +} + func buildLastCommitInfoFromStore(block *types.Block, store Store, initialHeight int64) abci.CommitInfo { if block.Height == initialHeight { // check for initial height before loading validators // there is no last commit for the initial height. @@ -545,7 +568,7 @@ func buildLastCommitInfoFromStore(block *types.Block, store Store, initialHeight return abci.CommitInfo{} } - lastValSet, err := store.LoadValidators(block.Height - 1) + lastValSet, err := loadValidatorsForCommitInfo(store, block.Height-1) if err != nil { panic(fmt.Errorf("failed to load validator set at height %d: %w", block.Height-1, err)) } diff --git a/state/store.go b/state/store.go index b4e12f7ed72..e2052382b57 100644 --- a/state/store.go +++ b/state/store.go @@ -547,44 +547,73 @@ func (store dbStore) SaveFinalizeBlockResponse(height int64, resp *abci.Response // LoadValidators loads the ValidatorSet for a given height. // Returns ErrNoValSetForHeight if the validator set can't be found for this height. +// +// If the validator set was last persisted at a checkpoint earlier than height, +// LoadValidators advances proposer priorities forward via +// IncrementProposerPriority. That advance is O(height - lastStoredHeight) and +// can dominate replay time on chains with infrequent validator-set changes +// (issue #1693). For call sites that only read validator addresses and voting +// powers (e.g. building ABCI VoteInfo / CommitInfo), prefer LoadValidatorsFast. func (store dbStore) LoadValidators(height int64) (*types.ValidatorSet, error) { - valInfo, err := loadValidatorsInfo(store.db, height) + valInfo, lastStoredHeight, err := store.loadValidatorsAtHeight(height) if err != nil { - return nil, ErrNoValSetForHeight{height} + return nil, err } - if valInfo.ValidatorSet == nil { - lastStoredHeight := lastStoredHeightFor(height, valInfo.LastHeightChanged) - valInfo2, err := loadValidatorsInfo(store.db, lastStoredHeight) - if err != nil || valInfo2.ValidatorSet == nil { - return nil, - fmt.Errorf("couldn't find validators at height %d (height %d was originally requested): %w", - lastStoredHeight, - height, - err, - ) - } - - vs, err := types.ValidatorSetFromProto(valInfo2.ValidatorSet) - if err != nil { - return nil, err - } - + vs, err := types.ValidatorSetFromProto(valInfo.ValidatorSet) + if err != nil { + return nil, err + } + if lastStoredHeight != height { vs.IncrementProposerPriority(cmtmath.SafeConvertInt32(height - lastStoredHeight)) // mutate - vi2, err := vs.ToProto() - if err != nil { - return nil, err - } - - valInfo2.ValidatorSet = vi2 - valInfo = valInfo2 } + return vs, nil +} - vip, err := types.ValidatorSetFromProto(valInfo.ValidatorSet) +// LoadValidatorsFast loads the ValidatorSet for a given height without +// advancing proposer priorities forward from the most recent checkpoint. +// +// The returned ValidatorSet has correct addresses, public keys, and voting +// powers; ProposerPriority fields reflect the last stored checkpoint, not the +// requested height. Callers that need accurate proposer priority MUST use +// LoadValidators instead. +// +// This method is intentionally not part of the Store interface — adding it +// there would force every external Store implementation to provide it. +// Callers should use the duck-typed helper in state/execution.go which falls +// back to LoadValidators when the underlying store does not implement this +// fast path. +func (store dbStore) LoadValidatorsFast(height int64) (*types.ValidatorSet, error) { + valInfo, _, err := store.loadValidatorsAtHeight(height) if err != nil { return nil, err } + return types.ValidatorSetFromProto(valInfo.ValidatorSet) +} + +// loadValidatorsAtHeight returns the ValidatorsInfo whose ValidatorSet covers +// height, together with the height at which that ValidatorSet was actually +// stored. If the requested height has no stored ValidatorSet (i.e. unchanged +// since the last checkpoint), the call falls back to the most recent stored +// checkpoint and returns the difference via lastStoredHeight so callers can +// decide whether to apply IncrementProposerPriority. +func (store dbStore) loadValidatorsAtHeight(height int64) (*cmtstate.ValidatorsInfo, int64, error) { + valInfo, err := loadValidatorsInfo(store.db, height) + if err != nil { + return nil, 0, ErrNoValSetForHeight{height} + } + if valInfo.ValidatorSet != nil { + return valInfo, height, nil + } - return vip, nil + lastStoredHeight := lastStoredHeightFor(height, valInfo.LastHeightChanged) + valInfo2, err := loadValidatorsInfo(store.db, lastStoredHeight) + if err != nil || valInfo2.ValidatorSet == nil { + return nil, 0, fmt.Errorf( + "couldn't find validators at height %d (height %d was originally requested): %w", + lastStoredHeight, height, err, + ) + } + return valInfo2, lastStoredHeight, nil } func lastStoredHeightFor(height, lastHeightChanged int64) int64 { diff --git a/state/store_test.go b/state/store_test.go index ebc085acbf4..fb536559352 100644 --- a/state/store_test.go +++ b/state/store_test.go @@ -84,6 +84,123 @@ func BenchmarkLoadValidators(b *testing.B) { } } +// fastLoader is the duck-typed interface tests use to reach LoadValidatorsFast +// without exporting the dbStore concrete type or adding the method to the +// public Store interface. Mirrors the production helper in state/execution.go. +type fastLoader interface { + LoadValidatorsFast(int64) (*types.ValidatorSet, error) +} + +// BenchmarkLoadValidatorsSawtooth reproduces the issue #1693 sawtooth pattern: +// a ValidatorsInfo header is persisted at every height (recording +// LastHeightChanged) but the full ValidatorSet is only stored at checkpoints. +// LoadValidators must therefore advance proposer priorities from the most +// recent checkpoint to the requested height, an O(offset) loop that dominates +// replay time at large offsets. LoadValidatorsFast skips that loop. +func BenchmarkLoadValidatorsSawtooth(b *testing.B) { + const valSetSize = 100 + + config := test.ResetTestRoot("state_sawtooth_") + defer os.RemoveAll(config.RootDir) + dbType := dbm.BackendType(config.DBBackend) + stateDB, err := dbm.NewDB("state", dbType, config.DBDir()) + require.NoError(b, err) + stateStore := sm.NewStore(stateDB, sm.StoreOptions{DiscardABCIResponses: false}) + state, err := stateStore.LoadFromDBOrGenesisFile(config.GenesisFile()) + require.NoError(b, err) + + state.Validators = genValSet(valSetSize) + state.NextValidators = state.Validators.CopyIncrementProposerPriority(1) + require.NoError(b, stateStore.Save(state)) + + const checkpoint = int64(sm.ValSetCheckpointInterval) + // Persist the full ValidatorSet only at the checkpoint; for offsets above + // it we write a header that records LastHeightChanged but leaves + // ValidatorSet nil, forcing LoadValidators back to the checkpoint. + require.NoError(b, sm.SaveValidatorsInfo(stateDB, checkpoint, 1, state.Validators)) + + fast, ok := stateStore.(fastLoader) + require.True(b, ok, "dbStore must implement LoadValidatorsFast") + + offsets := []struct { + name string + height int64 + }{ + {"at_checkpoint", checkpoint}, + {"+1", checkpoint + 1}, + {"+100", checkpoint + 100}, + {"+10000", checkpoint + 10_000}, + {"+99999", checkpoint + 99_999}, + } + + for _, o := range offsets { + // header-only record so LoadValidators falls back to the checkpoint + if o.height != checkpoint { + // saveValidatorsInfo writes a header-only record (no full + // ValidatorSet) whenever height != LastHeightChanged and + // height % valSetCheckpointInterval != 0, which is exactly what + // we need for the sawtooth setup. + require.NoError(b, sm.SaveValidatorsInfo(stateDB, o.height, 1, state.Validators)) + } + + b.Run("LoadValidators/"+o.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + if _, err := stateStore.LoadValidators(o.height); err != nil { + b.Fatal(err) + } + } + }) + b.Run("LoadValidatorsFast/"+o.name, func(b *testing.B) { + for n := 0; n < b.N; n++ { + if _, err := fast.LoadValidatorsFast(o.height); err != nil { + b.Fatal(err) + } + } + }) + } +} + +// TestLoadValidatorsFastParity verifies LoadValidatorsFast returns the same +// validator identities (addresses, public keys, voting powers) as LoadValidators +// across a sawtooth of off-checkpoint heights. The only intentional difference +// is ProposerPriority — Fast leaves it at the checkpoint snapshot — which is +// safe for the ABCI VoteInfo / CommitInfo call sites that only consume the +// other fields. +func TestLoadValidatorsFastParity(t *testing.T) { + stateDB := dbm.NewMemDB() + stateStore := sm.NewStore(stateDB, sm.StoreOptions{DiscardABCIResponses: false}) + + vals := genValSet(10) + + const checkpoint = int64(sm.ValSetCheckpointInterval) + require.NoError(t, sm.SaveValidatorsInfo(stateDB, checkpoint, 1, vals)) + + fast, ok := stateStore.(fastLoader) + require.True(t, ok, "dbStore must implement LoadValidatorsFast") + + for _, offset := range []int64{0, 1, 100, 10_000, 99_999} { + h := checkpoint + offset + if offset != 0 { + // Header-only record so LoadValidators falls back to the + // checkpoint and applies IncrementProposerPriority(offset). + require.NoError(t, sm.SaveValidatorsInfo(stateDB, h, 1, vals)) + } + + slow, err := stateStore.LoadValidators(h) + require.NoError(t, err, "LoadValidators(h=%d)", h) + quick, err := fast.LoadValidatorsFast(h) + require.NoError(t, err, "LoadValidatorsFast(h=%d)", h) + + require.Equal(t, slow.Size(), quick.Size(), "size mismatch at offset=%d", offset) + for i := range slow.Validators { + s, q := slow.Validators[i], quick.Validators[i] + require.Equal(t, s.Address, q.Address, "address mismatch at offset=%d idx=%d", offset, i) + require.True(t, s.PubKey.Equals(q.PubKey), "pubkey mismatch at offset=%d idx=%d", offset, i) + require.Equal(t, s.VotingPower, q.VotingPower, "voting power mismatch at offset=%d idx=%d", offset, i) + } + } +} + func TestPruneStates(t *testing.T) { testcases := map[string]struct { makeHeights int64 diff --git a/store/store.go b/store/store.go index 4b9ab564dab..1e081b0a018 100644 --- a/store/store.go +++ b/store/store.go @@ -133,9 +133,9 @@ func (bs *BlockStore) LoadBaseMeta() *types.BlockMeta { return bs.LoadBlockMeta(bs.base) } -// LoadBlock returns the block with the given height. -// If no block is found for that height, it returns nil. -func (bs *BlockStore) LoadBlock(height int64) *types.Block { +// LoadBlockProto returns the block at the given height in its proto representation, +// without deserializing into a [types.Block]. Returns nil if no block is found. +func (bs *BlockStore) LoadBlockProto(height int64) *cmtproto.Block { blockMeta := bs.LoadBlockMeta(height) if blockMeta == nil { return nil @@ -159,6 +159,17 @@ func (bs *BlockStore) LoadBlock(height int64) *types.Block { panic(fmt.Sprintf("Error reading block: %v", err)) } + return pbb +} + +// LoadBlock returns the block with the given height. +// If no block is found for that height, it returns nil. +func (bs *BlockStore) LoadBlock(height int64) *types.Block { + pbb := bs.LoadBlockProto(height) + if pbb == nil { + return nil + } + block, err := types.BlockFromProto(pbb) if err != nil { panic(cmterrors.ErrMsgFromProto{MessageName: "Block", Err: err}) diff --git a/store/store_test.go b/store/store_test.go index 155655d36f4..0008b25d91f 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -382,6 +382,33 @@ func stripExtensions(ec *types.ExtendedCommit) bool { return stripped } +func TestLoadBlockProto(t *testing.T) { + state, bs, cleanup := makeStateAndBlockStore() + defer cleanup() + + // missing block returns nil + require.Nil(t, bs.LoadBlockProto(1)) + + block, err := state.MakeBlock(1, nil, new(types.Commit), nil, state.Validators.GetProposer().Address) + require.NoError(t, err) + partSet, err := block.MakePartSet(types.BlockPartSizeBytes) + require.NoError(t, err) + seenCommit := makeTestExtCommit(1, cmttime.Now()).ToCommit() + bs.SaveBlock(block, partSet, seenCommit) + + pbb := bs.LoadBlockProto(1) + require.NotNil(t, pbb) + + // proto representation must round-trip back to the same block + loaded, err := types.BlockFromProto(pbb) + require.NoError(t, err) + require.Equal(t, block.Hash(), loaded.Hash()) + + // LoadBlock and LoadBlockProto must agree + direct := bs.LoadBlock(1) + require.Equal(t, direct.Hash(), loaded.Hash()) +} + // TestSaveBlockWithExtendedCommitPanicOnAbsentExtension tests that saving a // block with an extended commit panics when the extension data is absent. func TestSaveBlockWithExtendedCommitPanicOnAbsentExtension(t *testing.T) { diff --git a/types/block.go b/types/block.go index f8aeb5eaa09..efdd4b6fcac 100644 --- a/types/block.go +++ b/types/block.go @@ -280,7 +280,11 @@ func BlockFromProto(bp *cmtproto.Block) (*Block, error) { // MaxDataBytes returns the maximum size of block's data. // -// XXX: Panics on negative result. +// XXX: Panics on negative result. This halts chains whose Block.MaxBytes is +// too small to fit the worst-case commit — reachable with large-signature key +// types such as ml_dsa_65 (~3.4KB per validator) — and that is the expected +// behavior: the misconfiguration must be fixed by raising Block.MaxBytes, not +// papered over by producing blocks with no room for data. func MaxDataBytes(maxBytes, evidenceBytes int64, valsCount int) int64 { maxDataBytes := maxBytes - MaxOverheadForBlock - @@ -302,7 +306,8 @@ func MaxDataBytes(maxBytes, evidenceBytes int64, valsCount int) int64 { // MaxDataBytesNoEvidence returns the maximum size of block's data when // evidence count is unknown (will be assumed to be 0). // -// XXX: Panics on negative result. +// XXX: Panics on negative result. Halts misconfigured chains; expected +// behavior — see MaxDataBytes. func MaxDataBytesNoEvidence(maxBytes int64, valsCount int) int64 { maxDataBytes := maxBytes - MaxOverheadForBlock - diff --git a/types/params.go b/types/params.go index 3d3af2bb812..5278ff141d4 100644 --- a/types/params.go +++ b/types/params.go @@ -57,6 +57,13 @@ type ConsensusParams struct { // BlockParams define limits on the block size and gas plus minimum time // between blocks. +// +// MaxBytes must cover the block's fixed overhead, header, evidence, and the +// worst-case commit (MaxCommitBytes), which grows with validator count and +// signature size. With large-signature key types such as ml_dsa_65 (3309-byte +// signatures, ~3.4KB per validator), an undersized MaxBytes leaves no room for +// data and block construction panics. Size it for the largest expected +// validator set when enabling such key types. type BlockParams struct { MaxBytes int64 `json:"max_bytes"` MaxGas int64 `json:"max_gas"` @@ -71,6 +78,10 @@ type EvidenceParams struct { // ValidatorParams restrict the public key types validators can use. // NOTE: uses ABCI pubkey naming, not Amino names. +// +// Enabling large-signature key types (e.g. ml_dsa_65) inflates the +// per-validator commit reserve; adjust Block.MaxBytes accordingly (see +// BlockParams). type ValidatorParams struct { PubKeyTypes []string `json:"pub_key_types"` } @@ -116,10 +127,13 @@ func DefaultConsensusParams() *ConsensusParams { } } -// DefaultBlockParams returns a default BlockParams. +// DefaultBlockParams returns a default BlockParams. MaxBytes budgets 21MiB +// for data plus the worst-case commit for a maximum-size validator set +// (MaxVotesCount validators at MaxSignatureSize), so the default stays valid +// for any pub key type and validator count. func DefaultBlockParams() BlockParams { return BlockParams{ - MaxBytes: 22020096, // 21MB + MaxBytes: 22020096 + MaxCommitBytes(MaxVotesCount), // ~53MiB MaxGas: -1, } }