diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e7e610edf8..f9ac165e7a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -7,6 +7,6 @@ # global owners are only requested if there isn't a more specific # codeowner specified below. For this reason, the global codeowners # are often repeated in package-level definitions. -* @renaynay @Wondertan @vgonkivs @distractedm1nd @walldiss +* @renaynay @Wondertan @vgonkivs @distractedm1nd @walldiss @ramin docs/adr @adlerjohn @liamsi diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 96228eea15..edcd86e11d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -7,6 +7,8 @@ updates: day: monday time: "11:00" open-pull-requests-limit: 10 + labels: + - kind:deps - package-ecosystem: gomod directory: "/" schedule: @@ -19,6 +21,13 @@ updates: - renaynay labels: - kind:deps + ignore: + - dependency-name: "*otel*" + update-types: ["version-update:semver-patch"] + groups: + otel: + patterns: + - "go.opentelemetry.io/otel*" - package-ecosystem: docker directory: "/" schedule: @@ -26,3 +35,5 @@ updates: day: monday time: "11:00" open-pull-requests-limit: 10 + labels: + - kind:deps diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..61c286f936 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,14 @@ + diff --git a/.github/workflows/ci_release.yml b/.github/workflows/ci_release.yml index 574f425c7b..ad15a083bc 100644 --- a/.github/workflows/ci_release.yml +++ b/.github/workflows/ci_release.yml @@ -1,5 +1,6 @@ name: CI and Release on: + merge_group: push: branches: - main @@ -8,7 +9,6 @@ on: - "v*" pull_request: workflow_dispatch: - # Inputs the workflow accepts. inputs: version: # Friendly description to be shown in the UI instead of 'name' @@ -22,24 +22,55 @@ on: - major jobs: + # set up go version for use through pipelines, setting + # variable one time and setting outputs to access passing it + # to other jobs + setup: + runs-on: ubuntu-latest + env: + # upgrade go version throughout pipeline here + GO_VERSION: "1.21" + outputs: + go-version: ${{ steps.set-vars.outputs.go-version }} + branch: ${{ steps.trim_ref.outputs.branch }} + debug: ${{ steps.debug.outputs.debug }} + steps: + - name: Set go version + id: set-vars + run: echo "go-version=${{env.GO_VERSION}}" >> "$GITHUB_OUTPUT" + + - name: Trim branch name + id: trim_ref + run: | + echo "branch=$(${${{ github.ref }}:11})" >> $GITHUB_OUTPUT + + - name: Set debug output + id: debug + run: | + if [[ "${{ runner.debug }}" == "true" ]]; then + echo "debug=true" >> $GITHUB_ENV + else + echo "debug=false" >> $GITHUB_ENV + fi + # Dockerfile Linting hadolint: - uses: celestiaorg/.github/.github/workflows/reusable_dockerfile_lint.yml@v0.1.1 # yamllint disable-line rule:line-length + uses: celestiaorg/.github/.github/workflows/reusable_dockerfile_lint.yml@v0.2.8 # yamllint disable-line rule:line-length with: dockerfile: Dockerfile yamllint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: celestiaorg/.github/.github/actions/yamllint@v0.1.1 + - uses: actions/checkout@v4 + - uses: celestiaorg/.github/.github/actions/yamllint@v0.2.8 markdown-lint: name: Markdown Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: node-version: 18 - run: | @@ -47,21 +78,66 @@ jobs: markdownlint --config .markdownlint.yaml '**/*.md' go-ci: + needs: setup uses: ./.github/workflows/go-ci.yml + with: + go-version: ${{ needs.setup.outputs.go-version }} - # Make a release if this is a manually trigger job, i.e. workflow_dispatch - release: + # If this was a workflow dispatch event, we need to generate and push a tag + # for goreleaser to grab + version_bump: needs: [hadolint, yamllint, markdown-lint, go-ci] runs-on: ubuntu-latest - if: ${{ github.event_name == 'workflow_dispatch' }} permissions: "write-all" steps: - - uses: actions/checkout@v3 - - name: Version Release - uses: celestiaorg/.github/.github/actions/version-release@v0.1.1 + - uses: actions/checkout@v4 + + - name: Bump version and push tag + # Placing the if condition here is a workaround for needing to block + # on this step during workflow dispatch events but the step not + # needing to run on tags. If we had the if condition on the full + # version_bump section, it would skip and not run, which would result + # in goreleaser not running either. + if: ${{ github.event_name == 'workflow_dispatch' }} + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + default_bump: ${{ inputs.version }} + release_branches: ${{ needs.setup.outputs.branch }} + + # Generate the release with goreleaser to include pre-built binaries + goreleaser: + needs: version_bump + runs-on: ubuntu-latest + if: | + github.event_name == 'workflow_dispatch' || + (github.event_name == 'push' && contains(github.ref, 'refs/tags/')) + permissions: "write-all" + steps: + - uses: actions/checkout@v4 + + - run: git fetch --force --tags + + - uses: actions/setup-go@v5 + with: + go-version: ${{ needs.setup.outputs.go-version }} + + - name: Import GPG key + id: import_gpg + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_SIGNING_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + + # Generate the binaries and release + - uses: goreleaser/goreleaser-action@v5 with: - github-token: ${{secrets.GITHUB_TOKEN}} - version-bump: ${{inputs.version}} + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} # TODO: permission issue, but not worth fixing as this should be refactored # into the celestiaorg/.github repo, at which point any permission issues will diff --git a/.github/workflows/docker-build-publish.yml b/.github/workflows/docker-build-publish.yml index ce629145b9..b8322d5e17 100644 --- a/.github/workflows/docker-build-publish.yml +++ b/.github/workflows/docker-build-publish.yml @@ -2,9 +2,10 @@ name: Docker Build & Publish # Trigger on all push events, new semantic version tags, and all PRs on: + merge_group: push: branches: - - "**" + - "main" tags: - "v[0-9]+.[0-9]+.[0-9]+" - "v[0-9]+.[0-9]+.[0-9]+-alpha.[0-9]+" @@ -17,6 +18,6 @@ jobs: permissions: contents: write packages: write - uses: celestiaorg/.github/.github/workflows/reusable_dockerfile_pipeline.yml@v0.1.1 # yamllint disable-line rule:line-length + uses: celestiaorg/.github/.github/workflows/reusable_dockerfile_pipeline.yml@v0.2.8 # yamllint disable-line rule:line-length with: dockerfile: Dockerfile diff --git a/.github/workflows/go-ci.yml b/.github/workflows/go-ci.yml index 8f4729c7c8..0a8807f212 100644 --- a/.github/workflows/go-ci.yml +++ b/.github/workflows/go-ci.yml @@ -2,41 +2,63 @@ name: Go CI on: workflow_call: - -env: - GO_VERSION: '1.20' + inputs: + go-version: + description: 'Go version' + required: true + type: string concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} cancel-in-progress: true jobs: + setup: + name: Setup + runs-on: ubuntu-latest + outputs: + debug: ${{ steps.debug.outputs.debug }} + steps: + - name: Set debug output + id: debug + run: | + if [[ "${{ runner.debug }}" == "true" ]]; then + echo "debug=true" >> $GITHUB_ENV + else + echo "debug=false" >> $GITHUB_ENV + fi + lint: + needs: [setup] name: Lint runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version: ${{ inputs.go-version }} - name: golangci-lint - uses: golangci/golangci-lint-action@v3.4.0 + uses: golangci/golangci-lint-action@v3.7.0 with: - version: v1.52.2 + args: --timeout 10m + version: v1.55 + skip-pkg-cache: true + skip-build-cache: true go_mod_tidy_check: + needs: [setup] name: Go Mod Tidy Check runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version: ${{ inputs.go-version }} - run: go mod tidy @@ -44,55 +66,70 @@ jobs: run: git diff --exit-code test_coverage: + needs: [lint, go_mod_tidy_check] name: Unit Tests Coverage - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest] + runs-on: ${{ matrix.os }} + env: + OS: ${{ matrix.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: set up go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version: ${{ inputs.go-version }} - name: run unit tests - run: make test-unit + run: make test-unit ENABLE_VERBOSE=${{ needs.setup.outputs.debug }} + + - name: Upload unit test output + uses: actions/upload-artifact@v4 + if: always() && needs.setup.outputs.debug == 'true' + with: + name: unit-test-output-${{ matrix.os }} + path: | + debug.log + coverage.txt + retention-days: 5 - name: upload coverage - uses: codecov/codecov-action@v3.1.3 + uses: codecov/codecov-action@v3.1.6 with: + env_vars: OS token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt - - unit_race_test: - name: Run Unit Tests with Race Detector - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: set up go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: execute test run - run: make test-unit-race + name: coverage-${{ matrix.os }} + + # @ramin - Temporarily removed while we figure out getting + # these unit tests consistently running on ubuntu-latest + # and then enabled for macos-latest. We aren't requiring + # unit_race_test to pass for PRs so lets remove and reintroduce + # once green + # + # unit_test_race: + # needs: [lint, go_mod_tidy_check] + # name: Unit Tests with Race Detector (ubuntu-latest) + # runs-on: ubuntu-latest + + # steps: + # - uses: actions/checkout@v4 + + # - name: set up go + # uses: actions/setup-go@v5 + # with: + # go-version: ${{ inputs.go-version }} + + # - name: execute test run + # run: make test-unit-race ENABLE_VERBOSE=${{ needs.setup.outputs.debug }} integration_test: - name: Run Integration Tests - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: set up go - uses: actions/setup-go@v4 - with: - go-version: ${{ env.GO_VERSION }} - - - name: Swamp Tests - run: make test-swamp - - - name: Swamp Tests with Race Detector - run: make test-swamp-race + name: Integration Tests + needs: [lint, go_mod_tidy_check] + uses: ./.github/workflows/integration-tests.yml + with: + go-version: ${{ inputs.go-version }} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000000..cd0fc62385 --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,115 @@ +name: Integration Tests + +on: + workflow_call: + inputs: + go-version: + description: 'Go version' + required: true + type: string + +jobs: + api_tests: + name: Integration Tests API + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: set up go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: run API tests + run: make test-integration TAGS=api + + blob_tests: + name: Integration Tests Blob + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: set up go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: run blob tests + run: make test-integration TAGS=blob + + fraud_tests: + name: Integration Tests Fraud + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: set up go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: run fraud tests + run: make test-integration TAGS=fraud + + nd_tests: + name: Integration Tests ND + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: set up go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: run nd tests + run: make test-integration TAGS=nd + + p2p_tests: + name: Integration Tests p2p + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: set up go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: run p2p tests + run: make test-integration TAGS=p2p + + reconstruction_tests: + name: Integration Tests Reconstruction + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: set up go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: run reconstruction tests + run: make test-integration SHORT=true TAGS=reconstruction + + sync_tests: + name: Integration Tests Sync + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: set up go + uses: actions/setup-go@v5 + with: + go-version: ${{ inputs.go-version }} + + - name: run sync tests + run: make test-integration SHORT=true TAGS=sync diff --git a/.github/workflows/issue-label-automation.yml b/.github/workflows/issue-label-automation.yml index 4b24bca99c..0d2d37ca49 100644 --- a/.github/workflows/issue-label-automation.yml +++ b/.github/workflows/issue-label-automation.yml @@ -5,15 +5,18 @@ on: types: [opened] issues: types: [opened] + jobs: automate-labels: runs-on: ubuntu-latest + if: ${{ github.actor != 'dependabot[bot]' }} permissions: issues: write pull-requests: write + steps: - name: Check for External Contributor - uses: tspascoal/get-user-teams-membership@v2 + uses: tspascoal/get-user-teams-membership@v3 id: teamCheck with: username: ${{ github.actor }} diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 314f2fcb14..b9d4351bbd 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -12,8 +12,8 @@ jobs: label: runs-on: ubuntu-latest steps: - - uses: mheap/github-action-required-labels@v4 + - uses: mheap/github-action-required-labels@v5 with: mode: minimum count: 1 - labels: "kind:fix, kind:misc, kind:break!, kind:refactor, kind:feat, kind:deps, kind:docs, kind:ci, kind:chore" # yamllint disable-line rule:line-length + labels: "kind:fix, kind:misc, kind:break!, kind:refactor, kind:feat, kind:deps, kind:docs, kind:ci, kind:chore, kind:testing" # yamllint disable-line rule:line-length diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000000..595effed1f --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,18 @@ +name: 'Close stale issues and PRs' +on: + schedule: + - cron: '30 8 * * *' + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + days-before-stale: 60 + days-before-close: 14 + days-before-issue-stale: 90 + days-before-issue-close: 21 + exempt-issue-labels: 'keep-open' + start-date: '2023-09-01T00:00:00Z' diff --git a/.golangci.yml b/.golangci.yml index 5f7b13e6a5..a0f2754a9b 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -4,7 +4,7 @@ run: linters: enable: - bodyclose - - depguard + # - depguard as of v1.54.2, the default config throws errors on our repo - dogsled - dupl - errcheck diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000000..9a3991ecae --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,69 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com +before: + hooks: + - go mod tidy +builds: + - main: ./cmd/celestia + binary: celestia + env: + # NOTE: goreleaser doesn't fully support CGO natively. If CGO is needed + # for any node features, this should be removed and a workaround might + # need to be created. + # REF: https://goreleaser.com/limitations/cgo/ + - CGO_ENABLED=0 + - VersioningPath={{ "github.com/celestiaorg/celestia-node/nodebuilder/node" }} + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + # Ref: https://goreleaser.com/customization/templates/#common-fields + # + # .CommitDate is used to help with reproducible builds, ensuring that the + # same date is always used + # + # .FullCommit is git commit hash goreleaser is using for the release + # + # .Version is the version being released + - -X "{{ .Env.VersioningPath }}.buildTime={{ .CommitDate }}" + - -X "{{ .Env.VersioningPath }}.lastCommit={{ .FullCommit }}" + - -X "{{ .Env.VersioningPath }}.semanticVersion={{ .Version }}" +dist: ./build/goreleaser +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of + # uname. + name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}x86_64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} +checksum: + name_template: "checksums.txt" +signs: + - artifacts: checksum + args: + [ + "--batch", + "-u", + "{{ .Env.GPG_FINGERPRINT }}", + "--output", + "${signature}", + "--detach-sign", + "${artifact}", + ] +snapshot: + name_template: "{{ incpatch .Version }}-next" +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" +git: + prerelease_suffix: "-" diff --git a/.yamllint.yml b/.yamllint.yml new file mode 100644 index 0000000000..cd2a9e8293 --- /dev/null +++ b/.yamllint.yml @@ -0,0 +1,9 @@ +--- +# Built from docs https://yamllint.readthedocs.io/en/stable/configuration.html +extends: default + +rules: + # 120 chars should be enough, but don't fail if a line is longer + line-length: + max: 120 + level: warning diff --git a/Dockerfile b/Dockerfile index 214003dc08..01fccafe2e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,15 @@ -FROM docker.io/golang:1.20-alpine3.17 as builder +FROM --platform=$BUILDPLATFORM docker.io/golang:1.21-alpine3.18 as builder + +ARG TARGETPLATFORM +ARG BUILDPLATFORM +ARG TARGETOS +ARG TARGETARCH + +ENV CGO_ENABLED=0 +ENV GO111MODULE=on # hadolint ignore=DL3018 -RUN apk update && apk add --no-cache \ +RUN uname -a && apk update && apk add --no-cache \ bash \ gcc \ git \ @@ -13,9 +21,11 @@ COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN make build && make cel-key +RUN uname -a &&\ + CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + make build && make cel-key -FROM docker.io/alpine:3.17.3 +FROM docker.io/alpine:3.19.1 # Read here why UID 10001: https://github.com/hexops/dockerfile/blob/main/README.md#do-not-use-a-uid-below-10000 ARG UID=10001 @@ -28,8 +38,11 @@ ENV NODE_TYPE bridge ENV P2P_NETWORK mocha # hadolint ignore=DL3018 -RUN apk update && apk add --no-cache \ +RUN uname -a &&\ + apk update && apk add --no-cache \ bash \ + curl \ + jq \ # Creates a user with $UID and $GID=$UID && adduser ${USER_NAME} \ -D \ diff --git a/Makefile b/Makefile index 79c45d6597..a43c917345 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,25 @@ SHELL=/usr/bin/env bash PROJECTNAME=$(shell basename "$(PWD)") -LDFLAGS=-ldflags="-X 'main.buildTime=$(shell date)' -X 'main.lastCommit=$(shell git rev-parse HEAD)' -X 'main.semanticVersion=$(shell git describe --tags --dirty=-dev)'" +DIR_FULLPATH=$(shell pwd) +versioningPath := "github.com/celestiaorg/celestia-node/nodebuilder/node" +LDFLAGS=-ldflags="-X '$(versioningPath).buildTime=$(shell date)' -X '$(versioningPath).lastCommit=$(shell git rev-parse HEAD)' -X '$(versioningPath).semanticVersion=$(shell git describe --tags --dirty=-dev 2>/dev/null || git rev-parse --abbrev-ref HEAD)'" +TAGS=integration +SHORT= ifeq (${PREFIX},) PREFIX := /usr/local endif +ifeq ($(ENABLE_VERBOSE),true) + LOG_AND_FILTER = | tee debug.log + VERBOSE = -v +else + VERBOSE = + LOG_AND_FILTER = +endif +ifeq ($(SHORT),true) + INTEGRATION_RUN_LENGTH = -short +else + INTEGRATION_RUN_LENGTH = +endif ## help: Get more info on make commands. help: Makefile @echo " Choose a command run in "$(PROJECTNAME)":" @@ -22,6 +38,12 @@ build: @go build -o build/ ${LDFLAGS} ./cmd/celestia .PHONY: build +## build-jemalloc: Build celestia-node binary with jemalloc allocator for BadgerDB instead of Go's native one +build-jemalloc: jemalloc + @echo "--> Building Celestia with jemalloc" + @go build -o build/ ${LDFLAGS} -tags jemalloc ./cmd/celestia +.PHONY: build-jemalloc + ## clean: Clean up celestia-node binary. clean: @echo "--> Cleaning up ./build" @@ -81,7 +103,7 @@ install-key: fmt: sort-imports @find . -name '*.go' -type f -not -path "*.git*" -not -name '*.pb.go' -not -name '*pb_test.go' | xargs gofmt -w -s @find . -name '*.go' -type f -not -path "*.git*" -not -name '*.pb.go' -not -name '*pb_test.go' | xargs goimports -w -local github.com/celestiaorg - @go mod tidy -compat=1.17 + @go mod tidy -compat=1.20 @cfmt -w -m=100 ./... @markdownlint --fix --quiet --config .markdownlint.yaml . .PHONY: fmt @@ -97,34 +119,26 @@ lint: lint-imports ## test-unit: Running unit tests test-unit: @echo "--> Running unit tests" - @go test -covermode=atomic -coverprofile=coverage.txt `go list ./... | grep -v nodebuilder/tests` + @go test $(VERBOSE) -covermode=atomic -coverprofile=coverage.txt `go list ./... | grep -v nodebuilder/tests` $(LOG_AND_FILTER) .PHONY: test-unit ## test-unit-race: Running unit tests with data race detector test-unit-race: @echo "--> Running unit tests with data race detector" - @go test -race `go list ./... | grep -v nodebuilder/tests` + @go test $(VERBOSE) -race -covermode=atomic -coverprofile=coverage.txt `go list ./... | grep -v nodebuilder/tests` $(LOG_AND_FILTER) .PHONY: test-unit-race -## test-swamp: Running swamp tests located in nodebuilder/tests -test-swamp: - @echo "--> Running swamp tests" - @go test ./nodebuilder/tests -.PHONY: test-swamp - -## test-swamp-race: Running swamp tests with data race detector located in node/tests -test-swamp-race: - @echo "--> Running swamp tests with data race detector" - @go test -race ./nodebuilder/tests -.PHONY: test-swamp-race - -## test: Running both unit and swamp tests -test: - @echo "--> Running all tests without data race detector" - @go test ./... - @echo "--> Running all tests with data race detector" - @go test -race ./... -.PHONY: test +## test-integration: Running /integration tests located in nodebuilder/tests +test-integration: + @echo "--> Running integrations tests $(VERBOSE) -tags=$(TAGS) $(INTEGRATION_RUN_LENGTH)" + @go test $(VERBOSE) -tags=$(TAGS) $(INTEGRATION_RUN_LENGTH) ./nodebuilder/tests +.PHONY: test-integration + +## test-integration-race: Running integration tests with data race detector located in node/tests +test-integration-race: + @echo "--> Running integration tests with data race detector -tags=$(TAGS)" + @go test -race -tags=$(TAGS) ./nodebuilder/tests +.PHONY: test-integration-race ## benchmark: Running all benchmarks benchmark: @@ -136,37 +150,38 @@ PB_PKGS=$(shell find . -name 'pb' -type d) PB_CORE=$(shell go list -f {{.Dir}} -m github.com/tendermint/tendermint) PB_GOGO=$(shell go list -f {{.Dir}} -m github.com/gogo/protobuf) PB_CELESTIA_APP=$(shell go list -f {{.Dir}} -m github.com/celestiaorg/celestia-app) +PB_NMT=$(shell go list -f {{.Dir}} -m github.com/celestiaorg/nmt) ## pb-gen: Generate protobuf code for all /pb/*.proto files in the project. pb-gen: @echo '--> Generating protobuf' @for dir in $(PB_PKGS); \ do for file in `find $$dir -type f -name "*.proto"`; \ - do protoc -I=. -I=${PB_CORE}/proto/ -I=${PB_GOGO} -I=${PB_CELESTIA_APP}/proto --gogofaster_out=paths=source_relative:. $$file; \ + do protoc -I=. -I=${PB_CORE}/proto/ -I=${PB_GOGO} -I=${PB_CELESTIA_APP}/proto -I=${PB_NMT} --gogofaster_out=paths=source_relative:. $$file; \ echo '-->' $$file; \ done; \ done; .PHONY: pb-gen - ## openrpc-gen: Generate OpenRPC spec for Celestia-Node's RPC api openrpc-gen: @echo "--> Generating OpenRPC spec" - @go run ./cmd/docgen fraud header state share das p2p node + @go run ./cmd/docgen fraud header state share das p2p node blob da .PHONY: openrpc-gen ## lint-imports: Lint only Go imports. +## flag -set-exit-status doesn't exit with code 1 as it should, so we use find until it is fixed by goimports-reviser lint-imports: @echo "--> Running imports linter" @for file in `find . -type f -name '*.go'`; \ - do goimports-reviser -list-diff -set-exit-status -company-prefixes "github.com/celestiaorg" -project-name "github.com/celestiaorg/celestia-node" -output stdout $$file \ + do goimports-reviser -list-diff -set-exit-status -company-prefixes "github.com/celestiaorg" -project-name "github.com/celestiaorg/celestia-node" -output stdout $$file \ || exit 1; \ done; .PHONY: lint-imports ## sort-imports: Sort Go imports. sort-imports: - @goimports-reviser -company-prefixes "github.com/celestiaorg" -project-name "github.com/celestiaorg/celestia-node" -output stdout ./... + @goimports-reviser -company-prefixes "github.com/celestiaorg" -project-name "github.com/celestiaorg/celestia-node" -output stdout ./... .PHONY: sort-imports ## adr-gen: Generate ADR from template. Must set NUM and TITLE parameters. @@ -174,3 +189,57 @@ adr-gen: @echo "--> Generating ADR" @curl -sSL https://raw.githubusercontent.com/celestiaorg/.github/main/adr-template.md > docs/architecture/adr-$(NUM)-$(TITLE).md .PHONY: adr-gen + +## telemetry-infra-up: launches local telemetry infrastructure. This includes grafana, jaeger, loki, pyroscope, and an otel-collector. +## you can access the grafana instance at localhost:3000 and login with admin:admin. +telemetry-infra-up: + PWD="${DIR_FULLPATH}/docker/telemetry" docker-compose -f ./docker/telemetry/docker-compose.yml up +.PHONY: telemetry-infra-up + +## telemetry-infra-down: tears the telemetry infrastructure down. The stores for grafana, prometheus, and loki will persist. +telemetry-infra-down: + PWD="${DIR_FULLPATH}/docker/telemetry" docker-compose -f ./docker/telemetry/docker-compose.yml down +.PHONY: telemetry-infra-down + +## goreleaser: List Goreleaser commands and checks if GoReleaser is installed. +goreleaser: Makefile + @echo " Choose a goreleaser command to run:" + @sed -n 's/^## goreleaser/goreleaser/p' $< | column -t -s ':' | sed -e 's/^/ /' + @goreleaser --version +.PHONY: goreleaser + +## goreleaser-build: Builds the celestia binary using GoReleaser for your local OS. +goreleaser-build: + goreleaser build --snapshot --clean --single-target +.PHONY: goreleaser-build + +## goreleaser-release: Builds the release celestia binaries as defined in .goreleaser.yaml. This requires there be a git tag for the release in the local git history. +goreleaser-release: + goreleaser release --clean --fail-fast --skip-publish +.PHONY: goreleaser-release + +# Copied from https://github.com/dgraph-io/badger/blob/main/Makefile +USER_ID = $(shell id -u) +HAS_JEMALLOC = $(shell test -f /usr/local/lib/libjemalloc.a && echo "jemalloc") +JEMALLOC_URL = "https://github.com/jemalloc/jemalloc/releases/download/5.2.1/jemalloc-5.2.1.tar.bz2" + +## jemalloc installs jemalloc allocator +jemalloc: + @if [ -z "$(HAS_JEMALLOC)" ] ; then \ + mkdir -p /tmp/jemalloc-temp && cd /tmp/jemalloc-temp ; \ + echo "Downloading jemalloc..." ; \ + curl -s -L ${JEMALLOC_URL} -o jemalloc.tar.bz2 ; \ + tar xjf ./jemalloc.tar.bz2 ; \ + cd jemalloc-5.2.1 ; \ + ./configure --with-jemalloc-prefix='je_' --with-malloc-conf='background_thread:true,metadata_thp:auto'; \ + make ; \ + if [ "$(USER_ID)" -eq "0" ]; then \ + make install ; \ + else \ + echo "==== Need sudo access to install jemalloc" ; \ + sudo make install ; \ + fi ; \ + cd /tmp ; \ + rm -rf /tmp/jemalloc-temp ; \ + fi +.PHONY: jemalloc diff --git a/README.md b/README.md index 76c722275d..3a552495ad 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Continue reading [here](https://blog.celestia.org/celestia-mvp-release-data-avai - [API docs](#api-docs) - [Node types](#node-types) - [Run a node](#run-a-node) - - [Environment variables](#environment-variables) + - [Environment variables](#environment-variables) - [Package-specific documentation](#package-specific-documentation) - [Code of Conduct](#code-of-conduct) @@ -32,7 +32,7 @@ Continue reading [here](https://blog.celestia.org/celestia-mvp-release-data-avai | Requirement | Notes | | ----------- |----------------| -| Go version | 1.20 or higher | +| Go version | 1.21 or higher | ## System Requirements @@ -55,7 +55,7 @@ For more information on setting up a node and the hardware requirements needed, ## API docs -Celestia-node public API is documented [here](https://docs.celestia.org/category/node-api/). +The celestia-node public API is documented [here](https://node-rpc-docs.celestia.org/). ## Node types @@ -67,7 +67,7 @@ More information can be found [here](https://github.com/celestiaorg/celestia-nod ## Run a node -`` can be `bridge`, `full` or `light`. +`` can be: `bridge`, `full` or `light`. ```sh celestia init @@ -77,7 +77,9 @@ celestia init celestia start ``` -### Environment variables +Please refer to [this guide](https://docs.celestia.org/nodes/celestia-node/) for more information on running a node. + +## Environment variables | Variable | Explanation | Default value | Required | | ----------------------- | ----------------------------------- | ------------- | -------- | diff --git a/api/docgen/examples.go b/api/docgen/examples.go index ae65501ba6..b873e7e050 100644 --- a/api/docgen/examples.go +++ b/api/docgen/examples.go @@ -18,8 +18,10 @@ import ( "golang.org/x/text/language" "github.com/celestiaorg/go-fraud" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/blob" "github.com/celestiaorg/celestia-node/das" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/nodebuilder/node" @@ -54,7 +56,7 @@ var ExampleValues = map[reflect.Type]interface{}{ reflect.TypeOf(node.Full): node.Full, reflect.TypeOf(auth.Permission("admin")): auth.Permission("admin"), reflect.TypeOf(byzantine.BadEncoding): byzantine.BadEncoding, - reflect.TypeOf((*fraud.Proof)(nil)).Elem(): byzantine.CreateBadEncodingProof( + reflect.TypeOf((*fraud.Proof[*header.ExtendedHeader])(nil)).Elem(): byzantine.CreateBadEncodingProof( []byte("bad encoding proof"), 42, &byzantine.ErrByzantine{ @@ -81,6 +83,8 @@ func init() { } addToExampleValues(valAddr) + addToExampleValues(state.Address{Address: addr}) + var txResponse *state.TxResponse err = json.Unmarshal([]byte(exampleTxResponse), &txResponse) if err != nil { @@ -128,6 +132,22 @@ func init() { Addrs: []multiaddr.Multiaddr{ma}, } addToExampleValues(addrInfo) + + namespace, err := share.NewBlobNamespaceV0([]byte{0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0x10}) + if err != nil { + panic(err) + } + addToExampleValues(namespace) + + generatedBlob, err := blob.NewBlobV0(namespace, []byte("This is an example of some blob data")) + if err != nil { + panic(err) + } + addToExampleValues(generatedBlob) + + proof := nmt.NewInclusionProof(0, 4, [][]byte{[]byte("test")}, true) + blobProof := &blob.Proof{&proof} + addToExampleValues(blobProof) } func addToExampleValues(v interface{}) { diff --git a/api/gateway/availability.go b/api/gateway/availability.go index b35593e24f..70341b34f2 100644 --- a/api/gateway/availability.go +++ b/api/gateway/availability.go @@ -2,7 +2,6 @@ package gateway import ( "encoding/json" - "fmt" "net/http" "strconv" @@ -16,8 +15,7 @@ const heightAvailabilityEndpoint = "/data_available" // AvailabilityResponse represents the response to a // `/data_available` request. type AvailabilityResponse struct { - Available bool `json:"available"` - Probability string `json:"probability_of_availability"` + Available bool `json:"available"` } func (h *Handler) handleHeightAvailabilityRequest(w http.ResponseWriter, r *http.Request) { @@ -28,36 +26,16 @@ func (h *Handler) handleHeightAvailabilityRequest(w http.ResponseWriter, r *http return } - //TODO: change this to NetworkHead once the adjacency in the store is fixed. - head, err := h.header.LocalHead(r.Context()) - if err != nil { - writeError(w, http.StatusInternalServerError, heightAvailabilityEndpoint, err) - return - } - if headHeight := int(head.Height()); headHeight < height { - err = fmt.Errorf( - "current head local chain head: %d is lower than requested height: %d"+ - " give header sync some time and retry later", headHeight, height) - writeError(w, http.StatusServiceUnavailable, heightAvailabilityEndpoint, err) - return - } - header, err := h.header.GetByHeight(r.Context(), uint64(height)) if err != nil { writeError(w, http.StatusInternalServerError, heightAvailabilityEndpoint, err) return } - availResp := &AvailabilityResponse{ - Probability: strconv.FormatFloat( - h.share.ProbabilityOfAvailability(r.Context()), 'g', -1, 64), - } - - err = h.share.SharesAvailable(r.Context(), header.DAH) + err = h.share.SharesAvailable(r.Context(), header) switch err { case nil: - availResp.Available = true - resp, err := json.Marshal(availResp) + resp, err := json.Marshal(&AvailabilityResponse{Available: true}) if err != nil { writeError(w, http.StatusInternalServerError, heightAvailabilityEndpoint, err) return @@ -67,8 +45,7 @@ func (h *Handler) handleHeightAvailabilityRequest(w http.ResponseWriter, r *http log.Errorw("serving request", "endpoint", heightAvailabilityEndpoint, "err", err) } case share.ErrNotAvailable: - availResp.Available = false - resp, err := json.Marshal(availResp) + resp, err := json.Marshal(&AvailabilityResponse{Available: false}) if err != nil { writeError(w, http.StatusInternalServerError, heightAvailabilityEndpoint, err) return diff --git a/api/gateway/bindings.go b/api/gateway/bindings.go new file mode 100644 index 0000000000..c01bd2da47 --- /dev/null +++ b/api/gateway/bindings.go @@ -0,0 +1,73 @@ +package gateway + +import ( + "fmt" + "net/http" +) + +func (h *Handler) RegisterEndpoints(rpc *Server) { + // state endpoints + rpc.RegisterHandlerFunc( + fmt.Sprintf("%s/{%s}", balanceEndpoint, addrKey), + h.handleBalanceRequest, + http.MethodGet, + ) + + rpc.RegisterHandlerFunc( + submitTxEndpoint, + h.handleSubmitTx, + http.MethodPost, + ) + + rpc.RegisterHandlerFunc( + healthEndpoint, + h.handleHealthRequest, + http.MethodGet, + ) + + // share endpoints + rpc.RegisterHandlerFunc( + fmt.Sprintf( + "%s/{%s}/height/{%s}", + namespacedSharesEndpoint, + namespaceKey, + heightKey, + ), + h.handleSharesByNamespaceRequest, + http.MethodGet, + ) + + rpc.RegisterHandlerFunc( + fmt.Sprintf("%s/{%s}", namespacedSharesEndpoint, namespaceKey), + h.handleSharesByNamespaceRequest, + http.MethodGet, + ) + + rpc.RegisterHandlerFunc( + fmt.Sprintf("%s/{%s}/height/{%s}", namespacedDataEndpoint, namespaceKey, heightKey), + h.handleDataByNamespaceRequest, + http.MethodGet, + ) + + rpc.RegisterHandlerFunc( + fmt.Sprintf("%s/{%s}", namespacedDataEndpoint, namespaceKey), + h.handleDataByNamespaceRequest, + http.MethodGet, + ) + + // DAS endpoints + rpc.RegisterHandlerFunc( + fmt.Sprintf("%s/{%s}", heightAvailabilityEndpoint, heightKey), + h.handleHeightAvailabilityRequest, + http.MethodGet, + ) + + // header endpoints + rpc.RegisterHandlerFunc( + fmt.Sprintf("%s/{%s}", headerByHeightEndpoint, heightKey), + h.handleHeaderRequest, + http.MethodGet, + ) + + rpc.RegisterHandlerFunc(headEndpoint, h.handleHeadRequest, http.MethodGet) +} diff --git a/api/gateway/bindings_test.go b/api/gateway/bindings_test.go new file mode 100644 index 0000000000..5d27d5e4c7 --- /dev/null +++ b/api/gateway/bindings_test.go @@ -0,0 +1,119 @@ +package gateway + +import ( + "fmt" + "net/http" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" +) + +func TestRegisterEndpoints(t *testing.T) { + handler := &Handler{} + rpc := NewServer("localhost", "6969") + + handler.RegisterEndpoints(rpc) + + testCases := []struct { + name string + path string + method string + expected bool + }{ + { + name: "Get balance endpoint", + path: fmt.Sprintf("%s/{%s}", balanceEndpoint, addrKey), + method: http.MethodGet, + expected: true, + }, + { + name: "Submit transaction endpoint", + path: submitTxEndpoint, + method: http.MethodPost, + expected: true, + }, + { + name: "Get namespaced shares by height endpoint", + path: fmt.Sprintf("%s/{%s}/height/{%s}", namespacedSharesEndpoint, namespaceKey, heightKey), + method: http.MethodGet, + expected: true, + }, + { + name: "Get namespaced shares endpoint", + path: fmt.Sprintf("%s/{%s}", namespacedSharesEndpoint, namespaceKey), + method: http.MethodGet, + expected: true, + }, + { + name: "Get namespaced data by height endpoint", + path: fmt.Sprintf("%s/{%s}/height/{%s}", namespacedDataEndpoint, namespaceKey, heightKey), + method: http.MethodGet, + expected: true, + }, + { + name: "Get namespaced data endpoint", + path: fmt.Sprintf("%s/{%s}", namespacedDataEndpoint, namespaceKey), + method: http.MethodGet, + expected: true, + }, + { + name: "Get health endpoint", + path: "/status/health", + method: http.MethodGet, + expected: true, + }, + + // Going forward, we can add previously deprecated and since + // removed endpoints here to ensure we don't accidentally re-enable + // them in the future and accidentally expand surface area + { + name: "example totally bogus endpoint", + path: fmt.Sprintf("/wutang/{%s}/%s", "chambers", "36"), + method: http.MethodGet, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require.Equal( + t, + tc.expected, + hasEndpointRegistered(rpc.Router(), tc.path, tc.method), + "Endpoint registration mismatch for: %s %s %s", tc.name, tc.method, tc.path) + }) + } +} + +func hasEndpointRegistered(router *mux.Router, path string, method string) bool { + var registered bool + err := router.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + template, err := route.GetPathTemplate() + if err != nil { + return err + } + + if template == path { + methods, err := route.GetMethods() + if err != nil { + return err + } + + for _, m := range methods { + if m == method { + registered = true + return nil + } + } + } + return nil + }) + + if err != nil { + fmt.Println("Error walking through routes:", err) + return false + } + + return registered +} diff --git a/api/gateway/config.go b/api/gateway/config.go index f7d8bb44b1..0485da486e 100644 --- a/api/gateway/config.go +++ b/api/gateway/config.go @@ -11,14 +11,6 @@ type Config struct { Port string } -func DefaultConfig() Config { - return Config{ - Address: "0.0.0.0", - // do NOT expose the same port as celestia-core by default so that both can run on the same machine - Port: "26658", - } -} - func (cfg *Config) Validate() error { if ip := net.ParseIP(cfg.Address); ip == nil { return fmt.Errorf("service/gateway: invalid listen address format: %s", cfg.Address) diff --git a/api/gateway/das.go b/api/gateway/das.go deleted file mode 100644 index 565cbd7460..0000000000 --- a/api/gateway/das.go +++ /dev/null @@ -1,27 +0,0 @@ -package gateway - -import ( - "encoding/json" - "net/http" -) - -const ( - dasStateEndpoint = "/daser/state" -) - -func (h *Handler) handleDASStateRequest(w http.ResponseWriter, r *http.Request) { - stats, err := h.das.SamplingStats(r.Context()) - if err != nil { - writeError(w, http.StatusInternalServerError, dasStateEndpoint, err) - return - } - resp, err := json.Marshal(stats) - if err != nil { - writeError(w, http.StatusInternalServerError, dasStateEndpoint, err) - return - } - _, err = w.Write(resp) - if err != nil { - log.Errorw("serving request", "endpoint", dasStateEndpoint, "err", err) - } -} diff --git a/api/gateway/endpoints.go b/api/gateway/endpoints.go deleted file mode 100644 index dfcb96bd06..0000000000 --- a/api/gateway/endpoints.go +++ /dev/null @@ -1,48 +0,0 @@ -package gateway - -import ( - "fmt" - "net/http" -) - -func (h *Handler) RegisterEndpoints(rpc *Server) { - // state endpoints - rpc.RegisterHandlerFunc(balanceEndpoint, h.handleBalanceRequest, http.MethodGet) - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}", balanceEndpoint, addrKey), h.handleBalanceRequest, - http.MethodGet) - rpc.RegisterHandlerFunc(submitTxEndpoint, h.handleSubmitTx, http.MethodPost) - rpc.RegisterHandlerFunc(submitPFBEndpoint, h.handleSubmitPFB, http.MethodPost) - - // staking queries - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}", queryDelegationEndpoint, addrKey), h.handleQueryDelegation, - http.MethodGet) - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}", queryUnbondingEndpoint, addrKey), h.handleQueryUnbonding, - http.MethodGet) - rpc.RegisterHandlerFunc(queryRedelegationsEndpoint, h.handleQueryRedelegations, - http.MethodPost) - - // share endpoints - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}/height/{%s}", namespacedSharesEndpoint, nIDKey, heightKey), - h.handleSharesByNamespaceRequest, http.MethodGet) - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}", namespacedSharesEndpoint, nIDKey), - h.handleSharesByNamespaceRequest, http.MethodGet) - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}/height/{%s}", namespacedDataEndpoint, nIDKey, heightKey), - h.handleDataByNamespaceRequest, http.MethodGet) - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}", namespacedDataEndpoint, nIDKey), - h.handleDataByNamespaceRequest, http.MethodGet) - - // DAS endpoints - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}", heightAvailabilityEndpoint, heightKey), - h.handleHeightAvailabilityRequest, http.MethodGet) - - // header endpoints - rpc.RegisterHandlerFunc(fmt.Sprintf("%s/{%s}", headerByHeightEndpoint, heightKey), h.handleHeaderRequest, - http.MethodGet) - rpc.RegisterHandlerFunc(headEndpoint, h.handleHeadRequest, http.MethodGet) - - // DASer endpoints - // only register if DASer service is available - if h.das != nil { - rpc.RegisterHandlerFunc(dasStateEndpoint, h.handleDASStateRequest, http.MethodGet) - } -} diff --git a/api/gateway/header.go b/api/gateway/header.go index 88ac3b0923..5b8a82351c 100644 --- a/api/gateway/header.go +++ b/api/gateway/header.go @@ -2,7 +2,6 @@ package gateway import ( "encoding/json" - "fmt" "net/http" "strconv" @@ -70,24 +69,12 @@ func (h *Handler) performGetHeaderRequest( writeError(w, http.StatusBadRequest, endpoint, err) return nil, err } - //TODO: change this to NetworkHead once the adjacency in the store is fixed. - head, err := h.header.LocalHead(r.Context()) - if err != nil { - writeError(w, http.StatusInternalServerError, heightAvailabilityEndpoint, err) - return nil, err - } - if headHeight := int(head.Height()); headHeight < height { - err = fmt.Errorf( - "current head local chain head: %d is lower than requested height: %d"+ - " give header sync some time and retry later", headHeight, height) - writeError(w, http.StatusServiceUnavailable, endpoint, err) - return nil, err - } - // perform request + header, err := h.header.GetByHeight(r.Context(), uint64(height)) if err != nil { writeError(w, http.StatusInternalServerError, endpoint, err) return nil, err } + return header, nil } diff --git a/api/gateway/health.go b/api/gateway/health.go new file mode 100644 index 0000000000..0ddb56bb67 --- /dev/null +++ b/api/gateway/health.go @@ -0,0 +1,16 @@ +package gateway + +import "net/http" + +const ( + healthEndpoint = "/status/health" +) + +func (h *Handler) handleHealthRequest(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte("ok")) + if err != nil { + log.Errorw("serving request", "endpoint", healthEndpoint, "err", err) + writeError(w, http.StatusBadGateway, healthEndpoint, err) + return + } +} diff --git a/api/gateway/middleware.go b/api/gateway/middleware.go index 498b9c5d64..4b669113dd 100644 --- a/api/gateway/middleware.go +++ b/api/gateway/middleware.go @@ -2,13 +2,8 @@ package gateway import ( "context" - "errors" "net/http" "time" - - "github.com/gorilla/mux" - - "github.com/celestiaorg/celestia-node/nodebuilder/state" ) const timeout = time.Minute @@ -16,30 +11,23 @@ const timeout = time.Minute func (h *Handler) RegisterMiddleware(srv *Server) { srv.RegisterMiddleware( setContentType, - checkPostDisabled(h.state), wrapRequestContext, + enableCors, ) } -func setContentType(next http.Handler) http.Handler { +func enableCors(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") next.ServeHTTP(w, r) }) } -// checkPostDisabled ensures that context was canceled and prohibit POST requests. -func checkPostDisabled(state state.Module) mux.MiddlewareFunc { - return func(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // check if state service was halted and deny the transaction - if r.Method == http.MethodPost && state.IsStopped(r.Context()) { - writeError(w, http.StatusMethodNotAllowed, r.URL.Path, errors.New("not possible to submit data")) - return - } - next.ServeHTTP(w, r) - }) - } +func setContentType(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "application/json") + next.ServeHTTP(w, r) + }) } // wrapRequestContext ensures we implement a deadline on serving requests diff --git a/api/gateway/server.go b/api/gateway/server.go index 181bfdfe55..7eab7c7bf9 100644 --- a/api/gateway/server.go +++ b/api/gateway/server.go @@ -36,6 +36,10 @@ func NewServer(address, port string) *Server { return server } +func (s *Server) Router() *mux.Router { + return s.srvMux +} + // Start starts the gateway Server, listening on the given address. func (s *Server) Start(context.Context) error { couldStart := s.started.CompareAndSwap(false, true) diff --git a/api/gateway/server_test.go b/api/gateway/server_test.go index e98d7a6091..cb8e3d17ae 100644 --- a/api/gateway/server_test.go +++ b/api/gateway/server_test.go @@ -12,8 +12,12 @@ import ( "github.com/stretchr/testify/require" ) +const ( + address = "localhost" + port = "0" +) + func TestServer(t *testing.T) { - address, port := "localhost", "0" server := NewServer(address, port) ctx, cancel := context.WithCancel(context.Background()) @@ -42,10 +46,33 @@ func TestServer(t *testing.T) { require.NoError(t, err) } +func TestCorsEnabled(t *testing.T) { + server := NewServer(address, port) + server.RegisterMiddleware(enableCors) + + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + err := server.Start(ctx) + require.NoError(t, err) + + // register ping handler + ping := new(ping) + server.RegisterHandlerFunc("/ping", ping.ServeHTTP, http.MethodGet) + + url := fmt.Sprintf("http://%s/ping", server.ListenAddr()) + + resp, err := http.Get(url) + require.NoError(t, err) + defer resp.Body.Close() + + require.NoError(t, err) + require.Equal(t, resp.Header.Get("Access-Control-Allow-Origin"), "*") +} + // TestServer_contextLeakProtection tests to ensure a context // deadline was added by the context wrapper middleware server-side. func TestServer_contextLeakProtection(t *testing.T) { - address, port := "localhost", "0" server := NewServer(address, port) server.RegisterMiddleware(wrapRequestContext) diff --git a/api/gateway/share.go b/api/gateway/share.go index 13903fd14b..36c1f94a0a 100644 --- a/api/gateway/share.go +++ b/api/gateway/share.go @@ -4,16 +4,13 @@ import ( "context" "encoding/hex" "encoding/json" - "fmt" "net/http" "strconv" "github.com/gorilla/mux" "github.com/celestiaorg/celestia-app/pkg/shares" - "github.com/celestiaorg/nmt/namespace" - "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share" ) @@ -22,7 +19,7 @@ const ( namespacedDataEndpoint = "/namespaced_data" ) -var nIDKey = "nid" +var namespaceKey = "nid" // NamespacedSharesResponse represents the response to a // SharesByNamespace request. @@ -39,19 +36,19 @@ type NamespacedDataResponse struct { } func (h *Handler) handleSharesByNamespaceRequest(w http.ResponseWriter, r *http.Request) { - height, nID, err := parseGetByNamespaceArgs(r) + height, namespace, err := parseGetByNamespaceArgs(r) if err != nil { writeError(w, http.StatusBadRequest, namespacedSharesEndpoint, err) return } - shares, headerHeight, err := h.getShares(r.Context(), height, nID) + shares, err := h.getShares(r.Context(), height, namespace) if err != nil { writeError(w, http.StatusInternalServerError, namespacedSharesEndpoint, err) return } resp, err := json.Marshal(&NamespacedSharesResponse{ Shares: shares, - Height: uint64(headerHeight), + Height: height, }) if err != nil { writeError(w, http.StatusInternalServerError, namespacedSharesEndpoint, err) @@ -64,12 +61,12 @@ func (h *Handler) handleSharesByNamespaceRequest(w http.ResponseWriter, r *http. } func (h *Handler) handleDataByNamespaceRequest(w http.ResponseWriter, r *http.Request) { - height, nID, err := parseGetByNamespaceArgs(r) + height, namespace, err := parseGetByNamespaceArgs(r) if err != nil { writeError(w, http.StatusBadRequest, namespacedDataEndpoint, err) return } - shares, headerHeight, err := h.getShares(r.Context(), height, nID) + shares, err := h.getShares(r.Context(), height, namespace) if err != nil { writeError(w, http.StatusInternalServerError, namespacedDataEndpoint, err) return @@ -81,7 +78,7 @@ func (h *Handler) handleDataByNamespaceRequest(w http.ResponseWriter, r *http.Re } resp, err := json.Marshal(&NamespacedDataResponse{ Data: data, - Height: uint64(headerHeight), + Height: height, }) if err != nil { writeError(w, http.StatusInternalServerError, namespacedDataEndpoint, err) @@ -93,37 +90,26 @@ func (h *Handler) handleDataByNamespaceRequest(w http.ResponseWriter, r *http.Re } } -func (h *Handler) getShares(ctx context.Context, height uint64, nID namespace.ID) ([]share.Share, int64, error) { - // get header - var ( - err error - header *header.ExtendedHeader - ) - - //TODO: change this to NetworkHead once the adjacency in the store is fixed. - header, err = h.header.LocalHead(ctx) +func (h *Handler) getShares(ctx context.Context, height uint64, namespace share.Namespace) ([]share.Share, error) { + header, err := h.header.GetByHeight(ctx, height) if err != nil { - return nil, 0, err + return nil, err } - if height > 0 { - if storeHeight := uint64(header.Height()); storeHeight < height { - return nil, 0, fmt.Errorf( - "current head local chain head: %d is lower than requested height: %d"+ - " give header sync some time and retry later", storeHeight, height) - } - header, err = h.header.GetByHeight(ctx, height) - } + shares, err := h.share.GetSharesByNamespace(ctx, header, namespace) if err != nil { - return nil, 0, err + return nil, err } - // perform request - shares, err := h.share.GetSharesByNamespace(ctx, header.DAH, nID) - return shares.Flatten(), header.Height(), err + + return shares.Flatten(), nil } func dataFromShares(input []share.Share) (data [][]byte, err error) { - sequences, err := shares.ParseShares(input) + appShares, err := shares.FromBytes(input) + if err != nil { + return nil, err + } + sequences, err := shares.ParseShares(appShares, false) if err != nil { return nil, err } @@ -137,7 +123,7 @@ func dataFromShares(input []share.Share) (data [][]byte, err error) { return data, nil } -func parseGetByNamespaceArgs(r *http.Request) (height uint64, nID namespace.ID, err error) { +func parseGetByNamespaceArgs(r *http.Request) (height uint64, namespace share.Namespace, err error) { vars := mux.Vars(r) // if a height was given, parse it, otherwise get namespaced shares/data from the latest header if strHeight, ok := vars[heightKey]; ok { @@ -146,11 +132,10 @@ func parseGetByNamespaceArgs(r *http.Request) (height uint64, nID namespace.ID, return 0, nil, err } } - hexNID := vars[nIDKey] - nID, err = hex.DecodeString(hexNID) + hexNamespace := vars[namespaceKey] + namespace, err = hex.DecodeString(hexNamespace) if err != nil { return 0, nil, err } - - return height, nID, nil + return height, namespace, namespace.ValidateForData() } diff --git a/api/gateway/share_test.go b/api/gateway/share_test.go index 92b7a2c853..9b12240f62 100644 --- a/api/gateway/share_test.go +++ b/api/gateway/share_test.go @@ -1,159 +1,48 @@ package gateway import ( - "bytes" _ "embed" - "encoding/base64" - "encoding/json" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + coretypes "github.com/tendermint/tendermint/types" "github.com/celestiaorg/celestia-app/pkg/appconsts" + "github.com/celestiaorg/celestia-app/pkg/shares" + + "github.com/celestiaorg/celestia-node/share/sharetest" ) func Test_dataFromShares(t *testing.T) { - type testCase struct { - name string - input [][]byte - want [][]byte - wantErr bool - } - - smallTxInput := padShare([]uint8{ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // namespace id - 0x1, // info byte - 0x0, 0x0, 0x0, 0x2, // 1 byte (unit) + 1 byte (unit length) = 2 bytes sequence length - 0x0, 0x0, 0x0, 17, // reserved bytes - 0x1, // unit length of first transaction - 0xa, // data of first transaction - }) - smallTxData := []byte{0x1, 0xa} - - largeTxInput := [][]byte{ - fillShare([]uint8{ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // namespace id - 0x1, // info byte - 0x0, 0x0, 0x2, 0x2, // 512 (unit) + 2 (unit length) = 514 sequence length - 0x0, 0x0, 0x0, 17, // reserved bytes - 128, 4, // unit length of transaction is 512 - }, 0xc), // data of transaction - padShare(append([]uint8{ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, // namespace id - 0x0, // info byte - 0x0, 0x0, 0x0, 0x0, // reserved bytes - }, bytes.Repeat([]byte{0xc}, 19)..., // continuation data of transaction - )), + testData := [][]byte{ + []byte("beep"), + []byte("beeap"), + []byte("BEEEEAHP"), } - largeTxData := []byte{128, 4} - largeTxData = append(largeTxData, bytes.Repeat([]byte{0xc}, 512)...) - largePfbTxInput := [][]byte{ - fillShare([]uint8{ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, // namespace id - 0x1, // info byte - 0x0, 0x0, 0x2, 0x2, // 512 (unit) + 2 (unit length) = 514 sequence length - 0x0, 0x0, 0x0, 17, // reserved bytes - 128, 4, // unit length of transaction is 512 - }, 0xc), // data of transaction - padShare(append([]uint8{ - 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4, // namespace id - 0x0, // info byte - 0x0, 0x0, 0x0, 0x0, // reserved bytes - }, bytes.Repeat([]byte{0xc}, 19)..., // continuation data of transaction - )), + ns := sharetest.RandV0Namespace() + sss := shares.NewSparseShareSplitter() + for _, data := range testData { + b := coretypes.Blob{ + Data: data, + NamespaceID: ns.ID(), + NamespaceVersion: ns.Version(), + ShareVersion: appconsts.ShareVersionZero, + } + err := sss.Write(b) + require.NoError(t, err) } - largePfbTxData := []byte{128, 4} - largePfbTxData = append(largePfbTxData, bytes.Repeat([]byte{0xc}, 512)...) - testCases := []testCase{ - { - name: "empty", - input: [][]byte{}, - want: nil, - wantErr: false, - }, - { - name: "returns an error when shares contain two different namespaces", - input: [][]byte{ - {0, 0, 0, 0, 0, 0, 0, 1}, - {0, 0, 0, 0, 0, 0, 0, 2}, - }, - want: nil, - wantErr: true, - }, - { - name: "returns raw data of a single tx share", - input: [][]byte{smallTxInput}, - want: [][]byte{smallTxData}, - wantErr: false, - }, - { - name: "returns raw data of a large tx that spans two shares", - input: largeTxInput, - want: [][]byte{largeTxData}, - wantErr: false, - }, - { - name: "returns raw data of a large PFB tx that spans two shares", - input: largePfbTxInput, - want: [][]byte{largePfbTxData}, - wantErr: false, - }, - } + sssShares := sss.Export() - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - got, err := dataFromShares(tc.input) - if tc.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - assert.Equal(t, tc.want, got) - }) + rawSSSShares := make([][]byte, len(sssShares)) + for i := 0; i < len(sssShares); i++ { + d := sssShares[i].ToBytes() + rawSSSShares[i] = d } -} - -// padShare returns a share padded with trailing zeros. -func padShare(share []byte) (paddedShare []byte) { - return fillShare(share, 0) -} - -// fillShare returns a share filled with filler so that the share length -// is equal to appconsts.ShareSize. -func fillShare(share []byte, filler byte) (paddedShare []byte) { - return append(share, bytes.Repeat([]byte{filler}, appconsts.ShareSize-len(share))...) -} - -// sharesBase64JSON is the base64 encoded share data from Blockspace Race -// block height 559108 and namespace e8e5f679bf7116cb. -// -//go:embed "testdata/sharesBase64.json" -var sharesBase64JSON string -// Test_dataFromSharesBSR reproduces an error that occurred when parsing shares -// on Blockspace Race block height 559108 namespace e8e5f679bf7116cb. -// -// https://github.com/celestiaorg/celestia-app/issues/1816 -func Test_dataFromSharesBSR(t *testing.T) { - var sharesBase64 []string - err := json.Unmarshal([]byte(sharesBase64JSON), &sharesBase64) - assert.NoError(t, err) - input := decode(sharesBase64) + parsedSSSShares, err := dataFromShares(rawSSSShares) + require.NoError(t, err) - _, err = dataFromShares(input) - assert.NoError(t, err) -} - -// decode returns the raw shares from base64Encoded. -func decode(base64Encoded []string) (rawShares [][]byte) { - for _, share := range base64Encoded { - rawShare, err := base64.StdEncoding.DecodeString(share) - if err != nil { - panic(err) - } - rawShares = append(rawShares, rawShare) - } - return rawShares + require.Equal(t, testData, parsedSSSShares) } diff --git a/api/gateway/state.go b/api/gateway/state.go index 00a318c5ae..13cf729cc6 100644 --- a/api/gateway/state.go +++ b/api/gateway/state.go @@ -13,12 +13,8 @@ import ( ) const ( - balanceEndpoint = "/balance" - submitTxEndpoint = "/submit_tx" - submitPFBEndpoint = "/submit_pfb" - queryDelegationEndpoint = "/query_delegation" - queryUnbondingEndpoint = "/query_unbonding" - queryRedelegationsEndpoint = "/query_redelegations" + balanceEndpoint = "/balance" + submitTxEndpoint = "/submit_tx" ) const addrKey = "address" @@ -33,21 +29,6 @@ type submitTxRequest struct { Tx string `json:"tx"` } -// submitPFBRequest represents a request to submit a PayForBlob -// transaction. -type submitPFBRequest struct { - NamespaceID string `json:"namespace_id"` - Data string `json:"data"` - Fee int64 `json:"fee"` - GasLimit uint64 `json:"gas_limit"` -} - -// queryRedelegationsRequest represents a request to query redelegations -type queryRedelegationsRequest struct { - From string `json:"from"` - To string `json:"to"` -} - func (h *Handler) handleBalanceRequest(w http.ResponseWriter, r *http.Request) { var ( bal *state.Balance @@ -56,23 +37,25 @@ func (h *Handler) handleBalanceRequest(w http.ResponseWriter, r *http.Request) { // read and parse request vars := mux.Vars(r) addrStr, exists := vars[addrKey] - if exists { - // convert address to Address type - var addr state.AccAddress - addr, err = types.AccAddressFromBech32(addrStr) + if !exists { + writeError(w, http.StatusBadRequest, balanceEndpoint, errors.New("balance endpoint requires address")) + return + } + + // convert address to Address type + var addr state.AccAddress + addr, err = types.AccAddressFromBech32(addrStr) + if err != nil { + // first check if it is a validator address and can be converted + valAddr, err := types.ValAddressFromBech32(addrStr) if err != nil { - // first check if it is a validator address and can be converted - valAddr, err := types.ValAddressFromBech32(addrStr) - if err != nil { - writeError(w, http.StatusBadRequest, balanceEndpoint, ErrInvalidAddressFormat) - return - } - addr = valAddr.Bytes() + writeError(w, http.StatusBadRequest, balanceEndpoint, ErrInvalidAddressFormat) + return } - bal, err = h.state.BalanceForAddress(r.Context(), addr) - } else { - bal, err = h.state.Balance(r.Context()) + addr = valAddr.Bytes() } + + bal, err = h.state.BalanceForAddress(r.Context(), state.Address{Address: addr}) if err != nil { writeError(w, http.StatusInternalServerError, balanceEndpoint, err) return @@ -117,134 +100,3 @@ func (h *Handler) handleSubmitTx(w http.ResponseWriter, r *http.Request) { log.Errorw("writing response", "endpoint", submitTxEndpoint, "err", err) } } - -func (h *Handler) handleSubmitPFB(w http.ResponseWriter, r *http.Request) { - // decode request - var req submitPFBRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - writeError(w, http.StatusBadRequest, submitPFBEndpoint, err) - return - } - nID, err := hex.DecodeString(req.NamespaceID) - if err != nil { - writeError(w, http.StatusBadRequest, submitPFBEndpoint, err) - return - } - data, err := hex.DecodeString(req.Data) - if err != nil { - writeError(w, http.StatusBadRequest, submitPFBEndpoint, err) - return - } - fee := types.NewInt(req.Fee) - // perform request - txResp, err := h.state.SubmitPayForBlob(r.Context(), nID, data, fee, req.GasLimit) - if err != nil { - writeError(w, http.StatusInternalServerError, submitPFBEndpoint, err) - return - } - resp, err := json.Marshal(txResp) - if err != nil { - writeError(w, http.StatusInternalServerError, submitPFBEndpoint, err) - return - } - _, err = w.Write(resp) - if err != nil { - log.Errorw("writing response", "endpoint", submitPFBEndpoint, "err", err) - } -} - -func (h *Handler) handleQueryDelegation(w http.ResponseWriter, r *http.Request) { - // read and parse request - vars := mux.Vars(r) - addrStr, exists := vars[addrKey] - if !exists { - writeError(w, http.StatusBadRequest, queryDelegationEndpoint, ErrMissingAddress) - return - } - - // convert address to Address type - addr, err := types.ValAddressFromBech32(addrStr) - if err != nil { - writeError(w, http.StatusBadRequest, queryDelegationEndpoint, err) - return - } - delegation, err := h.state.QueryDelegation(r.Context(), addr) - if err != nil { - writeError(w, http.StatusInternalServerError, queryDelegationEndpoint, err) - return - } - resp, err := json.Marshal(delegation) - if err != nil { - writeError(w, http.StatusInternalServerError, queryDelegationEndpoint, err) - return - } - _, err = w.Write(resp) - if err != nil { - log.Errorw("writing response", "endpoint", queryDelegationEndpoint, "err", err) - } -} - -func (h *Handler) handleQueryUnbonding(w http.ResponseWriter, r *http.Request) { - // read and parse request - vars := mux.Vars(r) - addrStr, exists := vars[addrKey] - if !exists { - writeError(w, http.StatusBadRequest, queryUnbondingEndpoint, ErrMissingAddress) - return - } - - // convert address to Address type - addr, err := types.ValAddressFromBech32(addrStr) - if err != nil { - writeError(w, http.StatusBadRequest, queryUnbondingEndpoint, err) - return - } - unbonding, err := h.state.QueryUnbonding(r.Context(), addr) - if err != nil { - writeError(w, http.StatusInternalServerError, queryUnbondingEndpoint, err) - return - } - resp, err := json.Marshal(unbonding) - if err != nil { - writeError(w, http.StatusInternalServerError, queryUnbondingEndpoint, err) - return - } - _, err = w.Write(resp) - if err != nil { - log.Errorw("writing response", "endpoint", queryUnbondingEndpoint, "err", err) - } -} - -func (h *Handler) handleQueryRedelegations(w http.ResponseWriter, r *http.Request) { - var req queryRedelegationsRequest - err := json.NewDecoder(r.Body).Decode(&req) - if err != nil { - writeError(w, http.StatusBadRequest, queryRedelegationsEndpoint, err) - return - } - srcValAddr, err := types.ValAddressFromBech32(req.From) - if err != nil { - writeError(w, http.StatusBadRequest, queryRedelegationsEndpoint, err) - return - } - dstValAddr, err := types.ValAddressFromBech32(req.To) - if err != nil { - writeError(w, http.StatusBadRequest, queryRedelegationsEndpoint, err) - return - } - unbonding, err := h.state.QueryRedelegations(r.Context(), srcValAddr, dstValAddr) - if err != nil { - writeError(w, http.StatusInternalServerError, queryRedelegationsEndpoint, err) - return - } - resp, err := json.Marshal(unbonding) - if err != nil { - writeError(w, http.StatusInternalServerError, queryRedelegationsEndpoint, err) - return - } - _, err = w.Write(resp) - if err != nil { - log.Errorw("writing response", "endpoint", queryRedelegationsEndpoint, "err", err) - } -} diff --git a/api/gateway/testdata/sharesBase64.json b/api/gateway/testdata/sharesBase64.json deleted file mode 100644 index e73d69f420..0000000000 --- a/api/gateway/testdata/sharesBase64.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - "6OX2eb9xFssBAAAEQwBIdZAtC1rH+hFrZ7F6azCBAAAAAAQreNqM02s323cAAGBVRFEjy9ENcVuibWqbtSpxGWWVuESNkhDZEYxlVZdqWd2bLhU1GkFHxzQRl6TVTpW4xQ5FXdJqyTnp1KXuSnpaC0GJbKdn/73/fYHn3dNG1Nj24dbtlBwl5gUX4fwF1xmJg2X25QTU1a3ApPnwUCaSjDxwhTuIScHdY0ipKG1D5JOi66xWtbDcgtinX1PTO1YnWtRkfh+Pt+r+TwvkrrldM1ffDDrrg3sRRhsNFCj98EYXCuMeuapztgzSwTQbSKPKlnK3O3yDjyPuyDoNx7F5kx6jLc18UZZPpBdjJVMLTENBmrYF5WeOjgmentI1pBO5ILuduFCxc0PBIorzxZqvO4Vgmi2kuWlLZc5we9RFzue3XIuteS5tB8iTjcnn43hTXxM0JqLBtCOQFnN5IcY6Ej3Yo+GJx2O5dITSJM8v6/UxV1fmUDH12TqYhoE0UrYG7RAimeQxUfLTwQBLA7b8L/um/ggX+WL/ZlhqhBxMs4M03Z5ShCJ7Zrde/eKZZC/mCL+I8ZX7LEwrqIo8M8DM2QHTvoA0ZIfXStvLhrtI80zlYX3vSyFa5yzYn9ClG5Fqn870iex9XHv42Kg08S75gTytyENSVZFXTh/Zh6SGiHM=", - "6OX2eb9xFssA0rF2e0KnD5o9pNk4qqwxTgH1FbeHqPQuwUvJSU27f1b2T519HL3DZ9lSwLTjkPbG7cf37HVVvaXYqd0lMXg5VPSb70CdMLQ1vHV5ulsvDUxzgDS8V3KWimzsUHi5LHBjweGbAiXFdthUl1peSsrSM5sPAtMcIU2yjHIswLYbCPiMj30cWbrnl725tuPTubW3Vpk80nw1mIaDNIXV7KE9dP7bRsuUORVsMeddZT9WFNID68HBK3v5ccZgmjOkoSitGRSj8BsXN/EwuLHcv3dpd9P0lzX+meGPnu9/+h0PTHOFNIyxC83lyjHh7vBUB/nPNPjbLF1CPC9Tf+zvExnNY5/qaHITwuo5KNOh2ldOc32xJu65QQokrMBuWPBtiWfA6dWT7A+aG6SZPM+Z/cPPxqzhAnrVrSO2ITpx8sRDkacyOM5PPllcfBRMOwVpucEoTKlSsHL/Uizd5ipHIduZdbb0Cqh413zwzpy6MQlM84Q0QkztzIj3Lq6FMXCzxQvVa06MFY4+fiJYIiA5NMziHph2+v9Z7cO8X839w++Nm1TTspMyzFYf3I9HM9fLih7JESMGNDAND2nVnEppVJOlSCTVH1yrx+zJtlJ5hlhPfnxVoQqbxnME07whracrjtJM9+3bFs8oFuGq+agEnFNS8kYCCV36mR4=", - "6OX2eb9xFssATGwIpvlCmojMCZnoPswyLpXMPWwValWdC3rzlGDUhOhApyZ8+XsUmEaEtOk6q/c895prbA8li9SZP3WKrTH7ymMgMmJB8oNyTo8Kpp3p/jcAAP//G9WQYQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "6OX2eb9xFssBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", - "6OX2eb9xFssBAAAC7gBH3m9XunkTbdymgmQ9EGUAAAAAAALWeNra4cfww3vB69mdR5Z4p8T9O/XhRMXJL1Y3mXwXyZuY/cyXUVk5vXTt8iYVvQXH1vN+kTk1L/iF77IaW02r+b9ril+9vVWz49KhjQbHPiyob0nJddM7ADFvpvxmVyfvNdsq330QChUvZipu3Dqhskl/Qo+YzFX+9a/2NanoL1jyNeZ8n1tC5bU/3WfbtJqrrW90nmiZ+LvD8Dqn6I+IPa9A5hlAzTPc/3d2j8lDd/7JfpVtnZkHnH9OPuZ8smOP95/c6co3VKcTa54R1Ly6unfXHLac3aclWXv63/LJJ2riWVoLc5YmxSw/WJHQ7bCfWPNMoOZtOGxz7dKGu3sKROMqAjrMK3xnn3t3e3etf47iluULrmsxEGueGdS83vsqTC/4X57I17nRpxCi8X1lF9/Mk4V5txbyBq/R+mG2kVjzLKDm/e81kP/w7+n9uPsf2SKO37f31Gx72SoTFaGzdHF55Ksvi4k1zwpqngV3p2344yMvVj/TeC3lMnHprSdT1YJfGHjyz+LrnZ6q3EqseTZQ88S+W96xfj1F2f2eUU/y16CrHcfnvf3qdOtlkHXH86+pK5ybVAwW/Dr1pGCZzK5QtStHtb6sVFuQsig4ftZq1a46qUdT7m3hTwc=", - "6OX2eb9xFssAmWcHNW+S6eFdYXNeXPrbcmVS4Jo9zG/2vrsme05s1tMfW6ILLH3mEmueA9S8tka+O/zFxSXz6hqnZpm2qt393sGal79490QTjXrxzf8/EmueE9S8yDZJq+P6fi+8XnRW7n/yzPKX4gfLkLrsmOPuDYuKWus1iTXPBWrem89/37l9ejxT/s0cZYe5K+IN6h6qzF377/rp9dnvVBrmXSLWPDeoeclv12ZbWd3OC3QV35SZeZija9qGZcu/bmfJtjc4azH/YS+x5nlAzWO7/o/LxPc+X1DUE8299hUT5bYXWs6c+FBhWuPC6T5sPMuINc/rACAAAP//N89ueAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" -] diff --git a/api/gateway/util.go b/api/gateway/util.go index bffd7ebc88..d3739f9e9c 100644 --- a/api/gateway/util.go +++ b/api/gateway/util.go @@ -1,7 +1,6 @@ package gateway import ( - "encoding/json" "net/http" ) @@ -9,13 +8,12 @@ func writeError(w http.ResponseWriter, statusCode int, endpoint string, err erro log.Debugw("serving request", "endpoint", endpoint, "err", err) w.WriteHeader(statusCode) - errBody, jerr := json.Marshal(err.Error()) - if jerr != nil { - log.Errorw("serializing error", "endpoint", endpoint, "err", jerr) - return - } - _, werr := w.Write(errBody) - if werr != nil { - log.Errorw("writing error response", "endpoint", endpoint, "err", werr) + + errorMessage := err.Error() // Get the error message as a string + errorBytes := []byte(errorMessage) + + _, err = w.Write(errorBytes) + if err != nil { + log.Errorw("writing error response", "endpoint", endpoint, "err", err) } } diff --git a/api/gateway/util_test.go b/api/gateway/util_test.go new file mode 100644 index 0000000000..d41b0918d2 --- /dev/null +++ b/api/gateway/util_test.go @@ -0,0 +1,24 @@ +package gateway + +import ( + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestWriteError(t *testing.T) { + t.Run("writeError", func(t *testing.T) { + // Create a mock HTTP response writer + w := httptest.NewRecorder() + + testErr := errors.New("test error") + + writeError(w, http.StatusInternalServerError, "/api/endpoint", testErr) + assert.Equal(t, http.StatusInternalServerError, w.Code) + responseBody := w.Body.Bytes() + assert.Equal(t, testErr.Error(), string(responseBody)) + }) +} diff --git a/api/rpc/client/client.go b/api/rpc/client/client.go index 783d3f53cd..1d8142048b 100644 --- a/api/rpc/client/client.go +++ b/api/rpc/client/client.go @@ -8,6 +8,8 @@ import ( "github.com/filecoin-project/go-jsonrpc" "github.com/celestiaorg/celestia-node/api/rpc/perms" + "github.com/celestiaorg/celestia-node/nodebuilder/blob" + "github.com/celestiaorg/celestia-node/nodebuilder/da" "github.com/celestiaorg/celestia-node/nodebuilder/das" "github.com/celestiaorg/celestia-node/nodebuilder/fraud" "github.com/celestiaorg/celestia-node/nodebuilder/header" @@ -17,17 +19,11 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder/state" ) -// TODO: this duplication of strings many times across the codebase can be avoided with issue #1176 -var client Client -var Modules = map[string]interface{}{ - "share": &client.Share.Internal, - "state": &client.State.Internal, - "header": &client.Header.Internal, - "fraud": &client.Fraud.Internal, - "das": &client.DAS.Internal, - "p2p": &client.P2P.Internal, - "node": &client.Node.Internal, -} +var ( + // staticClient is used for generating the OpenRPC spec. + staticClient Client + Modules = moduleMap(&staticClient) +) type Client struct { Fraud fraud.API @@ -37,6 +33,8 @@ type Client struct { DAS das.API P2P p2p.API Node node.API + Blob blob.API + DA da.API closer multiClientCloser } @@ -58,16 +56,11 @@ func (m *multiClientCloser) closeAll() { } } -// Close closes the connections to all namespaces registered on the client. +// Close closes the connections to all namespaces registered on the staticClient. func (c *Client) Close() { c.closer.closeAll() } -// NewPublicClient creates a new Client with one connection per namespace. -func NewPublicClient(ctx context.Context, addr string) (*Client, error) { - return newClient(ctx, addr, nil) -} - // NewClient creates a new Client with one connection per namespace with the // given token as the authorization token. func NewClient(ctx context.Context, addr string, token string) (*Client, error) { @@ -77,7 +70,8 @@ func NewClient(ctx context.Context, addr string, token string) (*Client, error) func newClient(ctx context.Context, addr string, authHeader http.Header) (*Client, error) { var multiCloser multiClientCloser - for name, module := range Modules { + var client Client + for name, module := range moduleMap(&client) { closer, err := jsonrpc.NewClient(ctx, addr, name, module, authHeader) if err != nil { return nil, err @@ -87,3 +81,18 @@ func newClient(ctx context.Context, addr string, authHeader http.Header) (*Clien return &client, nil } + +func moduleMap(client *Client) map[string]interface{} { + // TODO: this duplication of strings many times across the codebase can be avoided with issue #1176 + return map[string]interface{}{ + "share": &client.Share.Internal, + "state": &client.State.Internal, + "header": &client.Header.Internal, + "fraud": &client.Fraud.Internal, + "das": &client.DAS.Internal, + "p2p": &client.P2P.Internal, + "node": &client.Node.Internal, + "blob": &client.Blob.Internal, + "da": &client.DA.Internal, + } +} diff --git a/api/rpc/server.go b/api/rpc/server.go index a4c8c21ce7..f247682083 100644 --- a/api/rpc/server.go +++ b/api/rpc/server.go @@ -2,7 +2,6 @@ package rpc import ( "context" - "encoding/json" "net" "net/http" "reflect" @@ -15,21 +14,23 @@ import ( logging "github.com/ipfs/go-log/v2" "github.com/celestiaorg/celestia-node/api/rpc/perms" + "github.com/celestiaorg/celestia-node/libs/authtoken" ) var log = logging.Logger("rpc") type Server struct { - srv *http.Server - rpc *jsonrpc.RPCServer - listener net.Listener + srv *http.Server + rpc *jsonrpc.RPCServer + listener net.Listener + authDisabled bool started atomic.Bool auth jwt.Signer } -func NewServer(address, port string, secret jwt.Signer) *Server { +func NewServer(address, port string, authDisabled bool, secret jwt.Signer) *Server { rpc := jsonrpc.NewServer() srv := &Server{ rpc: rpc, @@ -38,7 +39,8 @@ func NewServer(address, port string, secret jwt.Signer) *Server { // the amount of time allowed to read request headers. set to the default 2 seconds ReadHeaderTimeout: 2 * time.Second, }, - auth: secret, + auth: secret, + authDisabled: authDisabled, } srv.srv.Handler = &auth.Handler{ Verify: srv.verifyAuth, @@ -51,30 +53,22 @@ func NewServer(address, port string, secret jwt.Signer) *Server { // reached if a token is provided in the header of the request, otherwise only // methods with `read` permissions are accessible. func (s *Server) verifyAuth(_ context.Context, token string) ([]auth.Permission, error) { - tk, err := jwt.ParseAndVerifyString(token, s.auth) - if err != nil { - return nil, err - } - p := new(perms.JWTPayload) - err = json.Unmarshal(tk.RawClaims(), p) - if err != nil { - return nil, err + if s.authDisabled { + return perms.AllPerms, nil } - // check permissions - return p.Allow, nil + return authtoken.ExtractSignedPermissions(s.auth, token) } // RegisterService registers a service onto the RPC server. All methods on the service will then be // exposed over the RPC. -func (s *Server) RegisterService(namespace string, service interface{}) { - s.rpc.Register(namespace, service) -} +func (s *Server) RegisterService(namespace string, service interface{}, out interface{}) { + if s.authDisabled { + s.rpc.Register(namespace, service) + return + } -// RegisterAuthedService registers a service onto the RPC server. All methods on the service will -// then be exposed over the RPC. -func (s *Server) RegisterAuthedService(namespace string, service interface{}, out interface{}) { auth.PermissionedProxy(perms.AllPerms, perms.DefaultPerms, service, getInternalStruct(out)) - s.RegisterService(namespace, out) + s.rpc.Register(namespace, out) } func getInternalStruct(api interface{}) interface{} { diff --git a/api/rpc_test.go b/api/rpc_test.go index f2e088ff1f..ff38a42045 100644 --- a/api/rpc_test.go +++ b/api/rpc_test.go @@ -19,8 +19,11 @@ import ( "github.com/celestiaorg/celestia-node/api/rpc/client" "github.com/celestiaorg/celestia-node/api/rpc/perms" daspkg "github.com/celestiaorg/celestia-node/das" - headerpkg "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/nodebuilder" + "github.com/celestiaorg/celestia-node/nodebuilder/blob" + blobMock "github.com/celestiaorg/celestia-node/nodebuilder/blob/mocks" + "github.com/celestiaorg/celestia-node/nodebuilder/da" + daMock "github.com/celestiaorg/celestia-node/nodebuilder/da/mocks" "github.com/celestiaorg/celestia-node/nodebuilder/das" dasMock "github.com/celestiaorg/celestia-node/nodebuilder/das/mocks" "github.com/celestiaorg/celestia-node/nodebuilder/fraud" @@ -41,16 +44,24 @@ import ( func TestRPCCallsUnderlyingNode(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - nd, server := setupNodeWithModifiedRPC(t) + + // generate dummy signer and sign admin perms token with it + signer, err := jwt.NewHS256(make([]byte, 32)) + require.NoError(t, err) + + nd, server := setupNodeWithAuthedRPC(t, signer) url := nd.RPCServer.ListenAddr() + + adminToken, err := perms.NewTokenWithPerms(signer, perms.AllPerms) + require.NoError(t, err) + // we need to run this a few times to prevent the race where the server is not yet started var ( rpcClient *client.Client - err error ) for i := 0; i < 3; i++ { time.Sleep(time.Second * 1) - rpcClient, err = client.NewPublicClient(ctx, "http://"+url) + rpcClient, err = client.NewClient(ctx, "http://"+url, string(adminToken)) if err == nil { t.Cleanup(rpcClient.Close) break @@ -81,6 +92,8 @@ type api struct { DAS das.Module Node node.Module P2P p2p.Module + Blob blob.Module + DA da.Module } func TestModulesImplementFullAPI(t *testing.T) { @@ -151,13 +164,7 @@ func TestAuthedRPC(t *testing.T) { require.NotNil(t, rpcClient) require.NoError(t, err) - // 1. Test method with public permissions - server.Header.EXPECT().NetworkHead(gomock.Any()).Return(new(headerpkg.ExtendedHeader), nil) - got, err := rpcClient.Header.NetworkHead(ctx) - require.NoError(t, err) - require.NotNil(t, got) - - // 2. Test method with read-level permissions + // 1. Test method with read-level permissions expected := daspkg.SamplingStats{ SampledChainHead: 100, CatchupHead: 100, @@ -179,20 +186,23 @@ func TestAuthedRPC(t *testing.T) { require.ErrorContains(t, err, "missing permission") } - // 3. Test method with write-level permissions + // 2. Test method with write-level permissions expectedResp := &state.TxResponse{} if tt.perm > 2 { - server.State.EXPECT().SubmitTx(gomock.Any(), gomock.Any()).Return(expectedResp, nil) - txResp, err := rpcClient.State.SubmitTx(ctx, []byte{}) + server.State.EXPECT().Delegate(gomock.Any(), gomock.Any(), + gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedResp, nil) + txResp, err := rpcClient.State.Delegate(ctx, + state.ValAddress{}, state.Int{}, state.Int{}, 0) require.NoError(t, err) require.Equal(t, expectedResp, txResp) } else { - _, err := rpcClient.State.SubmitTx(ctx, []byte{}) + _, err := rpcClient.State.Delegate(ctx, + state.ValAddress{}, state.Int{}, state.Int{}, 0) require.Error(t, err) require.ErrorContains(t, err, "missing permission") } - // 4. Test method with admin-level permissions + // 3. Test method with admin-level permissions expectedReachability := network.Reachability(3) if tt.perm > 3 { server.P2P.EXPECT().NATStatus(gomock.Any()).Return(expectedReachability, nil) @@ -210,55 +220,6 @@ func TestAuthedRPC(t *testing.T) { } } -// TestPublicClient tests that the public rpc client can only -// access public methods. -func TestPublicClient(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - // generate dummy signer and sign admin perms token with it - signer, err := jwt.NewHS256(make([]byte, 32)) - require.NoError(t, err) - - nd, server := setupNodeWithAuthedRPC(t, signer) - url := nd.RPCServer.ListenAddr() - - // we need to run this a few times to prevent the race where the server is not yet started - var rpcClient *client.Client - require.NoError(t, err) - for i := 0; i < 3; i++ { - time.Sleep(time.Second * 1) - rpcClient, err = client.NewPublicClient(ctx, "http://"+url) - if err == nil { - t.Cleanup(rpcClient.Close) - break - } - } - require.NotNil(t, rpcClient) - require.NoError(t, err) - - // 1. Test method with public permissions - server.Header.EXPECT().NetworkHead(gomock.Any()).Return(new(headerpkg.ExtendedHeader), nil) - got, err := rpcClient.Header.NetworkHead(ctx) - require.NoError(t, err) - require.NotNil(t, got) - - // 2. Test method with read-level permissions - _, err = rpcClient.DAS.SamplingStats(ctx) - require.Error(t, err) - require.ErrorContains(t, err, "missing permission") - - // 3. Test method with write-level permissions - _, err = rpcClient.State.SubmitTx(ctx, []byte{}) - require.Error(t, err) - require.ErrorContains(t, err, "missing permission") - - // 4. Test method with admin-level permissions - _, err = rpcClient.P2P.NATStatus(ctx) - require.Error(t, err) - require.ErrorContains(t, err, "missing permission") -} - func TestAllReturnValuesAreMarshalable(t *testing.T) { ra := reflect.TypeOf(new(api)).Elem() for i := 0; i < ra.NumMethod(); i++ { @@ -322,44 +283,6 @@ func implementsMarshaler(t *testing.T, typ reflect.Type) { } -func setupNodeWithModifiedRPC(t *testing.T) (*nodebuilder.Node, *mockAPI) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - ctrl := gomock.NewController(t) - - mockAPI := &mockAPI{ - stateMock.NewMockModule(ctrl), - shareMock.NewMockModule(ctrl), - fraudMock.NewMockModule(ctrl), - headerMock.NewMockModule(ctrl), - dasMock.NewMockModule(ctrl), - p2pMock.NewMockModule(ctrl), - nodeMock.NewMockModule(ctrl), - } - - // given the behavior of fx.Invoke, this invoke will be called last as it is added at the root - // level module. For further information, check the documentation on fx.Invoke. - invokeRPC := fx.Invoke(func(srv *rpc.Server) { - srv.RegisterService("state", mockAPI.State) - srv.RegisterService("share", mockAPI.Share) - srv.RegisterService("fraud", mockAPI.Fraud) - srv.RegisterService("header", mockAPI.Header) - srv.RegisterService("das", mockAPI.Das) - srv.RegisterService("p2p", mockAPI.P2P) - srv.RegisterService("node", mockAPI.Node) - }) - nd := nodebuilder.TestNode(t, node.Full, invokeRPC) - // start node - err := nd.Start(ctx) - require.NoError(t, err) - t.Cleanup(func() { - err = nd.Stop(ctx) - require.NoError(t, err) - }) - return nd, mockAPI -} - // setupNodeWithAuthedRPC sets up a node and overrides its JWT // signer with the given signer. func setupNodeWithAuthedRPC(t *testing.T, auth jwt.Signer) (*nodebuilder.Node, *mockAPI) { @@ -376,18 +299,22 @@ func setupNodeWithAuthedRPC(t *testing.T, auth jwt.Signer) (*nodebuilder.Node, * dasMock.NewMockModule(ctrl), p2pMock.NewMockModule(ctrl), nodeMock.NewMockModule(ctrl), + blobMock.NewMockModule(ctrl), + daMock.NewMockModule(ctrl), } // given the behavior of fx.Invoke, this invoke will be called last as it is added at the root // level module. For further information, check the documentation on fx.Invoke. invokeRPC := fx.Invoke(func(srv *rpc.Server) { - srv.RegisterAuthedService("state", mockAPI.State, &statemod.API{}) - srv.RegisterAuthedService("share", mockAPI.Share, &share.API{}) - srv.RegisterAuthedService("fraud", mockAPI.Fraud, &fraud.API{}) - srv.RegisterAuthedService("header", mockAPI.Header, &header.API{}) - srv.RegisterAuthedService("das", mockAPI.Das, &das.API{}) - srv.RegisterAuthedService("p2p", mockAPI.P2P, &p2p.API{}) - srv.RegisterAuthedService("node", mockAPI.Node, &node.API{}) + srv.RegisterService("fraud", mockAPI.Fraud, &fraud.API{}) + srv.RegisterService("das", mockAPI.Das, &das.API{}) + srv.RegisterService("header", mockAPI.Header, &header.API{}) + srv.RegisterService("state", mockAPI.State, &statemod.API{}) + srv.RegisterService("share", mockAPI.Share, &share.API{}) + srv.RegisterService("p2p", mockAPI.P2P, &p2p.API{}) + srv.RegisterService("node", mockAPI.Node, &node.API{}) + srv.RegisterService("blob", mockAPI.Blob, &blob.API{}) + srv.RegisterService("da", mockAPI.DA, &da.API{}) }) // fx.Replace does not work here, but fx.Decorate does nd := nodebuilder.TestNode(t, node.Full, invokeRPC, fx.Decorate(func() (jwt.Signer, error) { @@ -411,4 +338,6 @@ type mockAPI struct { Das *dasMock.MockModule P2P *p2pMock.MockModule Node *nodeMock.MockModule + Blob *blobMock.MockModule + DA *daMock.MockModule } diff --git a/blob/blob.go b/blob/blob.go new file mode 100644 index 0000000000..9843441dd2 --- /dev/null +++ b/blob/blob.go @@ -0,0 +1,203 @@ +package blob + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + + "github.com/celestiaorg/celestia-app/pkg/appconsts" + "github.com/celestiaorg/celestia-app/pkg/shares" + "github.com/celestiaorg/celestia-app/x/blob/types" + "github.com/celestiaorg/nmt" + + "github.com/celestiaorg/celestia-node/share" +) + +// Commitment is a Merkle Root of the subtree built from shares of the Blob. +// It is computed by splitting the blob into shares and building the Merkle subtree to be included +// after Submit. +type Commitment []byte + +func (com Commitment) String() string { + return string(com) +} + +// Equal ensures that commitments are the same +func (com Commitment) Equal(c Commitment) bool { + return bytes.Equal(com, c) +} + +// Proof is a collection of nmt.Proofs that verifies the inclusion of the data. +type Proof []*nmt.Proof + +func (p Proof) Len() int { return len(p) } + +func (p Proof) MarshalJSON() ([]byte, error) { + proofs := make([]string, 0, len(p)) + for _, proof := range p { + proofBytes, err := proof.MarshalJSON() + if err != nil { + return nil, err + } + proofs = append(proofs, string(proofBytes)) + } + return json.Marshal(proofs) +} + +func (p *Proof) UnmarshalJSON(b []byte) error { + var proofs []string + if err := json.Unmarshal(b, &proofs); err != nil { + return err + } + for _, proof := range proofs { + var nmtProof nmt.Proof + if err := nmtProof.UnmarshalJSON([]byte(proof)); err != nil { + return err + } + *p = append(*p, &nmtProof) + } + return nil +} + +// equal is a temporary method that compares two proofs. +// should be removed in BlobService V1. +func (p Proof) equal(input Proof) error { + if p.Len() != input.Len() { + return ErrInvalidProof + } + + for i, proof := range p { + pNodes := proof.Nodes() + inputNodes := input[i].Nodes() + for i, node := range pNodes { + if !bytes.Equal(node, inputNodes[i]) { + return ErrInvalidProof + } + } + + if proof.Start() != input[i].Start() || proof.End() != input[i].End() { + return ErrInvalidProof + } + + if !bytes.Equal(proof.LeafHash(), input[i].LeafHash()) { + return ErrInvalidProof + } + + } + return nil +} + +// Blob represents any application-specific binary data that anyone can submit to Celestia. +type Blob struct { + types.Blob `json:"blob"` + + Commitment Commitment `json:"commitment"` + + // the celestia-node's namespace type + // this is to avoid converting to and from app's type + namespace share.Namespace +} + +// NewBlobV0 constructs a new blob from the provided Namespace and data. +// The blob will be formatted as v0 shares. +func NewBlobV0(namespace share.Namespace, data []byte) (*Blob, error) { + return NewBlob(appconsts.ShareVersionZero, namespace, data) +} + +// NewBlob constructs a new blob from the provided Namespace, data and share version. +func NewBlob(shareVersion uint8, namespace share.Namespace, data []byte) (*Blob, error) { + if len(data) == 0 || len(data) > appconsts.DefaultMaxBytes { + return nil, fmt.Errorf("blob data must be > 0 && <= %d, but it was %d bytes", appconsts.DefaultMaxBytes, len(data)) + } + if err := namespace.ValidateForBlob(); err != nil { + return nil, err + } + + blob := tmproto.Blob{ + NamespaceId: namespace.ID(), + Data: data, + ShareVersion: uint32(shareVersion), + NamespaceVersion: uint32(namespace.Version()), + } + + com, err := types.CreateCommitment(&blob) + if err != nil { + return nil, err + } + return &Blob{Blob: blob, Commitment: com, namespace: namespace}, nil +} + +// Namespace returns blob's namespace. +func (b *Blob) Namespace() share.Namespace { + return b.namespace +} + +type jsonBlob struct { + Namespace share.Namespace `json:"namespace"` + Data []byte `json:"data"` + ShareVersion uint32 `json:"share_version"` + Commitment Commitment `json:"commitment"` +} + +func (b *Blob) MarshalJSON() ([]byte, error) { + blob := &jsonBlob{ + Namespace: b.Namespace(), + Data: b.Data, + ShareVersion: b.ShareVersion, + Commitment: b.Commitment, + } + return json.Marshal(blob) +} + +func (b *Blob) UnmarshalJSON(data []byte) error { + var blob jsonBlob + err := json.Unmarshal(data, &blob) + if err != nil { + return err + } + + b.Blob.NamespaceVersion = uint32(blob.Namespace.Version()) + b.Blob.NamespaceId = blob.Namespace.ID() + b.Blob.Data = blob.Data + b.Blob.ShareVersion = blob.ShareVersion + b.Commitment = blob.Commitment + b.namespace = blob.Namespace + return nil +} + +// buildBlobsIfExist takes shares and tries building the Blobs from them. +// It will build blobs either until appShares will be empty or the first incomplete blob will +// appear, so in this specific case it will return all built blobs + remaining shares. +func buildBlobsIfExist(appShares []shares.Share) ([]*Blob, []shares.Share, error) { + if len(appShares) == 0 { + return nil, nil, errors.New("empty shares received") + } + blobs := make([]*Blob, 0, len(appShares)) + for { + length, err := appShares[0].SequenceLen() + if err != nil { + return nil, nil, err + } + + amount := shares.SparseSharesNeeded(length) + if amount > len(appShares) { + return blobs, appShares, nil + } + + b, err := parseShares(appShares[:amount]) + if err != nil { + return nil, nil, err + } + + // only 1 blob will be created bc we passed the exact amount of shares + blobs = append(blobs, b[0]) + + if amount == len(appShares) { + return blobs, nil, nil + } + appShares = appShares[amount:] + } +} diff --git a/blob/blob_test.go b/blob/blob_test.go new file mode 100644 index 0000000000..85486ad125 --- /dev/null +++ b/blob/blob_test.go @@ -0,0 +1,90 @@ +package blob + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/types" + + apptypes "github.com/celestiaorg/celestia-app/x/blob/types" + + "github.com/celestiaorg/celestia-node/blob/blobtest" +) + +func TestBlob(t *testing.T) { + appBlobs, err := blobtest.GenerateV0Blobs([]int{1}, false) + require.NoError(t, err) + blob, err := convertBlobs(appBlobs...) + require.NoError(t, err) + + var test = []struct { + name string + expectedRes func(t *testing.T) + }{ + { + name: "new blob", + expectedRes: func(t *testing.T) { + require.NotEmpty(t, blob) + require.NotEmpty(t, blob[0].Namespace()) + require.NotEmpty(t, blob[0].Data) + require.NotEmpty(t, blob[0].Commitment) + }, + }, + { + name: "compare commitments", + expectedRes: func(t *testing.T) { + comm, err := apptypes.CreateCommitment(&blob[0].Blob) + require.NoError(t, err) + assert.Equal(t, blob[0].Commitment, Commitment(comm)) + }, + }, + { + name: "verify namespace", + expectedRes: func(t *testing.T) { + ns := blob[0].Namespace().ToAppNamespace() + require.NoError(t, err) + require.NoError(t, apptypes.ValidateBlobNamespace(ns)) + }, + }, + { + name: "shares to blobs", + expectedRes: func(t *testing.T) { + sh, err := BlobsToShares(blob...) + require.NoError(t, err) + b, err := SharesToBlobs(sh) + require.NoError(t, err) + assert.Equal(t, len(b), 1) + assert.Equal(t, blob[0].Commitment, b[0].Commitment) + }, + }, + { + name: "blob marshaling", + expectedRes: func(t *testing.T) { + data, err := blob[0].MarshalJSON() + require.NoError(t, err) + + newBlob := &Blob{} + require.NoError(t, newBlob.UnmarshalJSON(data)) + require.True(t, reflect.DeepEqual(blob[0], newBlob)) + }, + }, + } + + for _, tt := range test { + t.Run(tt.name, tt.expectedRes) + } +} + +func convertBlobs(appBlobs ...types.Blob) ([]*Blob, error) { + blobs := make([]*Blob, 0, len(appBlobs)) + for _, b := range appBlobs { + blob, err := NewBlob(b.ShareVersion, append([]byte{b.NamespaceVersion}, b.NamespaceID...), b.Data) + if err != nil { + return nil, err + } + blobs = append(blobs, blob) + } + return blobs, nil +} diff --git a/blob/blobtest/testing.go b/blob/blobtest/testing.go new file mode 100644 index 0000000000..a22f22f790 --- /dev/null +++ b/blob/blobtest/testing.go @@ -0,0 +1,38 @@ +package blobtest + +import ( + tmrand "github.com/tendermint/tendermint/libs/rand" + "github.com/tendermint/tendermint/types" + + "github.com/celestiaorg/celestia-app/pkg/appconsts" + "github.com/celestiaorg/celestia-app/pkg/shares" + "github.com/celestiaorg/celestia-app/test/util/testfactory" + + "github.com/celestiaorg/celestia-node/share" +) + +// GenerateV0Blobs is a test utility producing v0 share formatted blobs with the +// requested size and random namespaces. +func GenerateV0Blobs(sizes []int, sameNamespace bool) ([]types.Blob, error) { + blobs := make([]types.Blob, 0, len(sizes)) + + for _, size := range sizes { + size := rawBlobSize(appconsts.FirstSparseShareContentSize * size) + appBlob := testfactory.GenerateRandomBlob(size) + if !sameNamespace { + nid, err := share.NewBlobNamespaceV0(tmrand.Bytes(7)) + if err != nil { + return nil, err + } + appBlob.NamespaceVersion = nid[0] + appBlob.NamespaceID = nid[1:] + } + + blobs = append(blobs, appBlob) + } + return blobs, nil +} + +func rawBlobSize(totalSize int) int { + return totalSize - shares.DelimLen(uint64(totalSize)) +} diff --git a/blob/helper.go b/blob/helper.go new file mode 100644 index 0000000000..72a56c7889 --- /dev/null +++ b/blob/helper.go @@ -0,0 +1,86 @@ +package blob + +import ( + "bytes" + "sort" + + "github.com/tendermint/tendermint/types" + + "github.com/celestiaorg/celestia-app/pkg/shares" + + "github.com/celestiaorg/celestia-node/share" +) + +// SharesToBlobs takes raw shares and converts them to the blobs. +func SharesToBlobs(rawShares []share.Share) ([]*Blob, error) { + if len(rawShares) == 0 { + return nil, ErrBlobNotFound + } + + appShares, err := toAppShares(rawShares...) + if err != nil { + return nil, err + } + return parseShares(appShares) +} + +// parseShares takes shares and converts them to the []*Blob. +func parseShares(appShrs []shares.Share) ([]*Blob, error) { + shareSequences, err := shares.ParseShares(appShrs, true) + if err != nil { + return nil, err + } + + // ensure that sequence length is not 0 + if len(shareSequences) == 0 { + return nil, ErrBlobNotFound + } + + blobs := make([]*Blob, len(shareSequences)) + for i, sequence := range shareSequences { + data, err := sequence.RawData() + if err != nil { + return nil, err + } + if len(data) == 0 { + continue + } + + shareVersion, err := sequence.Shares[0].Version() + if err != nil { + return nil, err + } + + blob, err := NewBlob(shareVersion, sequence.Namespace.Bytes(), data) + if err != nil { + return nil, err + } + blobs[i] = blob + } + return blobs, nil +} + +// BlobsToShares accepts blobs and convert them to the Shares. +func BlobsToShares(blobs ...*Blob) ([]share.Share, error) { + b := make([]types.Blob, len(blobs)) + for i, blob := range blobs { + namespace := blob.Namespace() + b[i] = types.Blob{ + NamespaceVersion: namespace[0], + NamespaceID: namespace[1:], + Data: blob.Data, + ShareVersion: uint8(blob.ShareVersion), + } + } + + sort.Slice(b, func(i, j int) bool { + val := bytes.Compare(b[i].NamespaceID, b[j].NamespaceID) + return val <= 0 + }) + + rawShares, err := shares.SplitBlobs(b...) + if err != nil { + return nil, err + } + return shares.ToBytes(rawShares), nil +} diff --git a/blob/service.go b/blob/service.go new file mode 100644 index 0000000000..fc1d630e62 --- /dev/null +++ b/blob/service.go @@ -0,0 +1,365 @@ +package blob + +import ( + "context" + "errors" + "fmt" + "math" + "sync" + + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/types" + auth "github.com/cosmos/cosmos-sdk/x/auth/types" + logging "github.com/ipfs/go-log/v2" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" + + "github.com/celestiaorg/celestia-app/pkg/appconsts" + "github.com/celestiaorg/celestia-app/pkg/shares" + blobtypes "github.com/celestiaorg/celestia-app/x/blob/types" + + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/libs/utils" + "github.com/celestiaorg/celestia-node/share" +) + +var ( + ErrBlobNotFound = errors.New("blob: not found") + ErrInvalidProof = errors.New("blob: invalid proof") + + log = logging.Logger("blob") + tracer = otel.Tracer("blob/service") +) + +// GasPrice represents the amount to be paid per gas unit. Fee is set by +// multiplying GasPrice by GasLimit, which is determined by the blob sizes. +type GasPrice float64 + +// DefaultGasPrice returns the default gas price, letting node automatically +// determine the Fee based on the passed blob sizes. +func DefaultGasPrice() GasPrice { + return -1.0 +} + +// Submitter is an interface that allows submitting blobs to the celestia-core. It is used to +// avoid a circular dependency between the blob and the state package, since the state package needs +// the blob.Blob type for this signature. +type Submitter interface { + SubmitPayForBlob(ctx context.Context, fee sdkmath.Int, gasLim uint64, blobs []*Blob) (*types.TxResponse, error) +} + +type Service struct { + // accessor dials the given celestia-core endpoint to submit blobs. + blobSubmitter Submitter + // shareGetter retrieves the EDS to fetch all shares from the requested header. + shareGetter share.Getter + // headerGetter fetches header by the provided height + headerGetter func(context.Context, uint64) (*header.ExtendedHeader, error) +} + +func NewService( + submitter Submitter, + getter share.Getter, + headerGetter func(context.Context, uint64) (*header.ExtendedHeader, error), +) *Service { + return &Service{ + blobSubmitter: submitter, + shareGetter: getter, + headerGetter: headerGetter, + } +} + +// SubmitOptions contains the information about fee and gasLimit price in order to configure the +// Submit request. +type SubmitOptions struct { + Fee int64 + GasLimit uint64 +} + +// DefaultSubmitOptions creates a default fee and gas price values. +func DefaultSubmitOptions() *SubmitOptions { + return &SubmitOptions{ + Fee: -1, + GasLimit: 0, + } +} + +// Submit sends PFB transaction and reports the height at which it was included. +// Allows sending multiple Blobs atomically synchronously. +// Uses default wallet registered on the Node. +// Handles gas estimation and fee calculation. +func (s *Service) Submit(ctx context.Context, blobs []*Blob, gasPrice GasPrice) (uint64, error) { + log.Debugw("submitting blobs", "amount", len(blobs)) + + options := DefaultSubmitOptions() + if gasPrice >= 0 { + blobSizes := make([]uint32, len(blobs)) + for i, blob := range blobs { + blobSizes[i] = uint32(len(blob.Data)) + } + options.GasLimit = blobtypes.EstimateGas(blobSizes, appconsts.DefaultGasPerBlobByte, auth.DefaultTxSizeCostPerByte) + options.Fee = types.NewInt(int64(math.Ceil(float64(gasPrice) * float64(options.GasLimit)))).Int64() + } + + resp, err := s.blobSubmitter.SubmitPayForBlob(ctx, types.NewInt(options.Fee), options.GasLimit, blobs) + if err != nil { + return 0, err + } + return uint64(resp.Height), nil +} + +// Get retrieves all the blobs for given namespaces at the given height by commitment. +func (s *Service) Get(ctx context.Context, height uint64, ns share.Namespace, commitment Commitment) (*Blob, error) { + blob, _, err := s.getByCommitment(ctx, height, ns, commitment) + if err != nil { + return nil, err + } + return blob, nil +} + +// GetProof retrieves all blobs in the given namespaces at the given height by commitment +// and returns their Proof. +func (s *Service) GetProof( + ctx context.Context, + height uint64, + namespace share.Namespace, + commitment Commitment, +) (*Proof, error) { + _, proof, err := s.getByCommitment(ctx, height, namespace, commitment) + if err != nil { + return nil, err + } + return proof, nil +} + +// GetAll returns all blobs under the given namespaces at the given height. +// GetAll can return blobs and an error in case if some requests failed. +func (s *Service) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*Blob, error) { + header, err := s.headerGetter(ctx, height) + if err != nil { + return nil, err + } + + var ( + resultBlobs = make([][]*Blob, len(namespaces)) + resultErr = make([]error, len(namespaces)) + ) + + for _, ns := range namespaces { + log.Debugw("performing GetAll request", "namespace", ns.String(), "height", height) + } + + wg := sync.WaitGroup{} + for i, namespace := range namespaces { + wg.Add(1) + go func(i int, namespace share.Namespace) { + defer wg.Done() + blobs, err := s.getBlobs(ctx, namespace, header) + if err != nil { + resultErr[i] = fmt.Errorf("getting blobs for namespace(%s): %w", namespace.String(), err) + return + } + + log.Debugw("receiving blobs", "height", height, "total", len(blobs)) + resultBlobs[i] = blobs + }(i, namespace) + } + wg.Wait() + + blobs := make([]*Blob, 0) + for _, resBlobs := range resultBlobs { + if len(resBlobs) > 0 { + blobs = append(blobs, resBlobs...) + } + } + + if len(blobs) == 0 { + resultErr = append(resultErr, ErrBlobNotFound) + } + return blobs, errors.Join(resultErr...) +} + +// Included verifies that the blob was included in a specific height. +// To ensure that blob was included in a specific height, we need: +// 1. verify the provided commitment by recomputing it; +// 2. verify the provided Proof against subtree roots that were used in 1.; +func (s *Service) Included( + ctx context.Context, + height uint64, + namespace share.Namespace, + proof *Proof, + com Commitment, +) (_ bool, err error) { + ctx, span := tracer.Start(ctx, "included") + defer func() { + utils.SetStatusAndEnd(span, err) + }() + // In the current implementation, LNs will have to download all shares to recompute the commitment. + // To achieve 1. we need to modify Proof structure and to store all subtree roots, that were + // involved in commitment creation and then call `merkle.HashFromByteSlices`(tendermint package). + // nmt.Proof is verifying share inclusion by recomputing row roots, so, theoretically, we can do + // the same but using subtree roots. For this case, we need an extra method in nmt.Proof + // that will perform all reconstructions, + // but we have to guarantee that all our stored subtree roots will be on the same height(e.g. one + // level above shares). + // TODO(@vgonkivs): rework the implementation to perform all verification without network requests. + _, resProof, err := s.getByCommitment(ctx, height, namespace, com) + switch err { + case nil: + case ErrBlobNotFound: + return false, nil + default: + return false, err + } + return true, resProof.equal(*proof) +} + +// getByCommitment retrieves the DAH row by row, fetching shares and constructing blobs in order to +// compare Commitments. Retrieving is stopped once the requested blob/proof is found. +func (s *Service) getByCommitment( + ctx context.Context, + height uint64, + namespace share.Namespace, + commitment Commitment, +) (_ *Blob, _ *Proof, err error) { + log.Infow("requesting blob", + "height", height, + "namespace", namespace.String()) + + ctx, span := tracer.Start(ctx, "get-by-commitment") + defer func() { + utils.SetStatusAndEnd(span, err) + }() + span.SetAttributes( + attribute.Int64("height", int64(height)), + attribute.String("commitment", string(commitment)), + ) + + getCtx, headerGetterSpan := tracer.Start(ctx, "header-getter") + + header, err := s.headerGetter(getCtx, height) + if err != nil { + headerGetterSpan.SetStatus(codes.Error, err.Error()) + return nil, nil, err + } + + headerGetterSpan.SetStatus(codes.Ok, "") + headerGetterSpan.AddEvent("received eds", trace.WithAttributes( + attribute.Int64("eds-size", int64(len(header.DAH.RowRoots))))) + + getCtx, getSharesSpan := tracer.Start(ctx, "get-shares-by-namespace") + + namespacedShares, err := s.shareGetter.GetSharesByNamespace(getCtx, header, namespace) + if err != nil { + if errors.Is(err, share.ErrNotFound) { + err = ErrBlobNotFound + } + getSharesSpan.SetStatus(codes.Error, err.Error()) + return nil, nil, err + } + + getSharesSpan.SetStatus(codes.Ok, "") + getSharesSpan.AddEvent("received shares", trace.WithAttributes( + attribute.Int64("eds-size", int64(len(header.DAH.RowRoots))))) + + var ( + rawShares = make([]shares.Share, 0) + proofs = make(Proof, 0) + // spansMultipleRows specifies whether blob is expanded into multiple rows + spansMultipleRows bool + ) + + for _, row := range namespacedShares { + if len(row.Shares) == 0 { + // the above condition means that we've faced with an Absence Proof. + // This Proof proves that the namespace was not found in the DAH, so + // we can return `ErrBlobNotFound`. + return nil, nil, ErrBlobNotFound + } + + appShares, err := toAppShares(row.Shares...) + if err != nil { + return nil, nil, err + } + rawShares = append(rawShares, appShares...) + proofs = append(proofs, row.Proof) + + var blobs []*Blob + blobs, rawShares, err = buildBlobsIfExist(rawShares) + if err != nil { + return nil, nil, err + } + for _, b := range blobs { + if b.Commitment.Equal(commitment) { + span.AddEvent("blob reconstructed") + return b, &proofs, nil + } + // Falling under this flag means that the data from the last row + // was insufficient to create a complete blob. As a result, + // the first blob received spans two rows and includes proofs + // for both of these rows. All other blobs in the result will relate + // to the current row and have a single proof. + if spansMultipleRows { + spansMultipleRows = false + // leave proof only for the current row + proofs = proofs[len(proofs)-1:] + } + } + + if len(rawShares) > 0 { + spansMultipleRows = true + continue + } + proofs = nil + } + + err = ErrBlobNotFound + if len(rawShares) > 0 { + err = fmt.Errorf("incomplete blob with the "+ + "namespace: %s detected at %d: %w", namespace.String(), height, err) + log.Error(err) + } + return nil, nil, err +} + +// getBlobs retrieves the DAH and fetches all shares from the requested Namespace and converts +// them to Blobs. +func (s *Service) getBlobs( + ctx context.Context, + namespace share.Namespace, + header *header.ExtendedHeader, +) (_ []*Blob, err error) { + ctx, span := tracer.Start(ctx, "get-blobs") + defer func() { + utils.SetStatusAndEnd(span, err) + }() + namespacedShares, err := s.shareGetter.GetSharesByNamespace(ctx, header, namespace) + if err != nil { + return nil, err + } + return SharesToBlobs(namespacedShares.Flatten()) +} + +// toAppShares converts node's raw shares to the app shares, skipping padding +func toAppShares(shrs ...share.Share) ([]shares.Share, error) { + appShrs := make([]shares.Share, 0, len(shrs)) + for _, shr := range shrs { + bShare, err := shares.NewShare(shr) + if err != nil { + return nil, err + } + + ok, err := bShare.IsPadding() + if err != nil { + return nil, err + } + if ok { + continue + } + + appShrs = append(appShrs, *bShare) + } + return appShrs, nil +} diff --git a/blob/service_test.go b/blob/service_test.go new file mode 100644 index 0000000000..3e22f887af --- /dev/null +++ b/blob/service_test.go @@ -0,0 +1,456 @@ +package blob + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "testing" + "time" + + ds "github.com/ipfs/go-datastore" + ds_sync "github.com/ipfs/go-datastore/sync" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + tmrand "github.com/tendermint/tendermint/libs/rand" + + "github.com/celestiaorg/celestia-app/pkg/appconsts" + "github.com/celestiaorg/celestia-app/pkg/shares" + "github.com/celestiaorg/go-header/store" + + "github.com/celestiaorg/celestia-node/blob/blobtest" + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/header/headertest" + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/getters" + "github.com/celestiaorg/celestia-node/share/ipld" +) + +func TestBlobService_Get(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + var ( + blobSize0 = 18 + blobSize1 = 14 + blobSize2 = 20 + blobSize3 = 12 + ) + + appBlobs, err := blobtest.GenerateV0Blobs([]int{blobSize0, blobSize1}, false) + require.NoError(t, err) + blobs0, err := convertBlobs(appBlobs...) + require.NoError(t, err) + + appBlobs, err = blobtest.GenerateV0Blobs([]int{blobSize2, blobSize3}, true) + require.NoError(t, err) + blobs1, err := convertBlobs(appBlobs...) + require.NoError(t, err) + + service := createService(ctx, t, append(blobs0, blobs1...)) + var test = []struct { + name string + doFn func() (interface{}, error) + expectedResult func(interface{}, error) + }{ + { + name: "get single blob", + doFn: func() (interface{}, error) { + b, err := service.Get(ctx, 1, blobs0[0].Namespace(), blobs0[0].Commitment) + return []*Blob{b}, err + }, + expectedResult: func(res interface{}, err error) { + require.NoError(t, err) + assert.NotEmpty(t, res) + + blobs, ok := res.([]*Blob) + assert.True(t, ok) + assert.Len(t, blobs, 1) + + assert.Equal(t, blobs0[0].Commitment, blobs[0].Commitment) + }, + }, + { + name: "get all with the same namespace", + doFn: func() (interface{}, error) { + b, err := service.GetAll(ctx, 1, []share.Namespace{blobs1[0].Namespace()}) + return b, err + }, + expectedResult: func(res interface{}, err error) { + require.NoError(t, err) + + blobs, ok := res.([]*Blob) + assert.True(t, ok) + assert.NotEmpty(t, blobs) + + assert.Len(t, blobs, 2) + + for i := range blobs1 { + bytes.Equal(blobs1[i].Commitment, blobs[i].Commitment) + } + }, + }, + { + name: "get all with different namespaces", + doFn: func() (interface{}, error) { + b, err := service.GetAll(ctx, 1, []share.Namespace{blobs0[0].Namespace(), blobs0[1].Namespace()}) + return b, err + }, + expectedResult: func(res interface{}, err error) { + require.NoError(t, err) + + blobs, ok := res.([]*Blob) + assert.True(t, ok) + assert.NotEmpty(t, blobs) + + assert.Len(t, blobs, 2) + // check the order + require.True(t, bytes.Equal(blobs[0].Namespace(), blobs0[0].Namespace())) + require.True(t, bytes.Equal(blobs[1].Namespace(), blobs0[1].Namespace())) + }, + }, + { + name: "get blob with incorrect commitment", + doFn: func() (interface{}, error) { + b, err := service.Get(ctx, 1, blobs0[0].Namespace(), blobs0[1].Commitment) + return []*Blob{b}, err + }, + expectedResult: func(res interface{}, err error) { + require.Error(t, err) + + blobs, ok := res.([]*Blob) + assert.True(t, ok) + assert.Empty(t, blobs[0]) + }, + }, + { + name: "get invalid blob", + doFn: func() (interface{}, error) { + appBlob, err := blobtest.GenerateV0Blobs([]int{10}, false) + require.NoError(t, err) + blob, err := convertBlobs(appBlob...) + require.NoError(t, err) + + b, err := service.Get(ctx, 1, blob[0].Namespace(), blob[0].Commitment) + return []*Blob{b}, err + }, + expectedResult: func(res interface{}, err error) { + require.Error(t, err) + + blobs, ok := res.([]*Blob) + assert.True(t, ok) + assert.Empty(t, blobs[0]) + }, + }, + { + name: "get proof", + doFn: func() (interface{}, error) { + proof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + return proof, err + }, + expectedResult: func(res interface{}, err error) { + require.NoError(t, err) + + header, err := service.headerGetter(ctx, 1) + require.NoError(t, err) + + proof, ok := res.(*Proof) + assert.True(t, ok) + + verifyFn := func(t *testing.T, rawShares [][]byte, proof *Proof, namespace share.Namespace) { + for _, row := range header.DAH.RowRoots { + to := 0 + for _, p := range *proof { + from := to + to = p.End() - p.Start() + from + eq := p.VerifyInclusion(sha256.New(), namespace.ToNMT(), rawShares[from:to], row) + if eq == true { + return + } + } + } + t.Fatal("could not prove the shares") + } + + rawShares, err := BlobsToShares(blobs0[1]) + require.NoError(t, err) + verifyFn(t, rawShares, proof, blobs0[1].Namespace()) + }, + }, + { + name: "verify inclusion", + doFn: func() (interface{}, error) { + proof, err := service.GetProof(ctx, 1, blobs0[0].Namespace(), blobs0[0].Commitment) + require.NoError(t, err) + return service.Included(ctx, 1, blobs0[0].Namespace(), proof, blobs0[0].Commitment) + }, + expectedResult: func(res interface{}, err error) { + require.NoError(t, err) + included, ok := res.(bool) + require.True(t, ok) + require.True(t, included) + }, + }, + { + name: "verify inclusion fails with different proof", + doFn: func() (interface{}, error) { + proof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + require.NoError(t, err) + return service.Included(ctx, 1, blobs0[0].Namespace(), proof, blobs0[0].Commitment) + }, + expectedResult: func(res interface{}, err error) { + require.Error(t, err) + require.ErrorIs(t, err, ErrInvalidProof) + included, ok := res.(bool) + require.True(t, ok) + require.True(t, included) + }, + }, + { + name: "not included", + doFn: func() (interface{}, error) { + appBlob, err := blobtest.GenerateV0Blobs([]int{10}, false) + require.NoError(t, err) + blob, err := convertBlobs(appBlob...) + require.NoError(t, err) + + proof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + require.NoError(t, err) + return service.Included(ctx, 1, blob[0].Namespace(), proof, blob[0].Commitment) + }, + expectedResult: func(res interface{}, err error) { + require.NoError(t, err) + included, ok := res.(bool) + require.True(t, ok) + require.False(t, included) + }, + }, + { + name: "count proofs for the blob", + doFn: func() (interface{}, error) { + proof0, err := service.GetProof(ctx, 1, blobs0[0].Namespace(), blobs0[0].Commitment) + if err != nil { + return nil, err + } + proof1, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + if err != nil { + return nil, err + } + return []*Proof{proof0, proof1}, nil + }, + expectedResult: func(i interface{}, err error) { + require.NoError(t, err) + proofs, ok := i.([]*Proof) + require.True(t, ok) + + h, err := service.headerGetter(ctx, 1) + require.NoError(t, err) + + originalDataWidth := len(h.DAH.RowRoots) / 2 + sizes := []int{blobSize0, blobSize1} + for i, proof := range proofs { + require.True(t, sizes[i]/originalDataWidth+1 == proof.Len()) + } + }, + }, + { + name: "get all not found", + doFn: func() (interface{}, error) { + namespace := share.Namespace(tmrand.Bytes(share.NamespaceSize)) + return service.GetAll(ctx, 1, []share.Namespace{namespace}) + }, + expectedResult: func(i interface{}, err error) { + blobs, ok := i.([]*Blob) + require.True(t, ok) + assert.Empty(t, blobs) + require.Error(t, err) + require.ErrorIs(t, err, ErrBlobNotFound) + + }, + }, + { + name: "marshal proof", + doFn: func() (interface{}, error) { + proof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + require.NoError(t, err) + return json.Marshal(proof) + }, + expectedResult: func(i interface{}, err error) { + require.NoError(t, err) + jsonData, ok := i.([]byte) + require.True(t, ok) + var proof Proof + require.NoError(t, json.Unmarshal(jsonData, &proof)) + + newProof, err := service.GetProof(ctx, 1, blobs0[1].Namespace(), blobs0[1].Commitment) + require.NoError(t, err) + require.NoError(t, proof.equal(*newProof)) + }, + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + blobs, err := tt.doFn() + tt.expectedResult(blobs, err) + }) + } +} + +// TestService_GetSingleBlobWithoutPadding creates two blobs with the same namespace +// But to satisfy the rule of eds creating, padding namespace share is placed between +// blobs. Test ensures that blob service will skip padding share and return the correct blob. +func TestService_GetSingleBlobWithoutPadding(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + appBlob, err := blobtest.GenerateV0Blobs([]int{9, 5}, true) + require.NoError(t, err) + blobs, err := convertBlobs(appBlob...) + require.NoError(t, err) + + ns1, ns2 := blobs[0].Namespace().ToAppNamespace(), blobs[1].Namespace().ToAppNamespace() + + padding0, err := shares.NamespacePaddingShare(ns1, appconsts.ShareVersionZero) + require.NoError(t, err) + padding1, err := shares.NamespacePaddingShare(ns2, appconsts.ShareVersionZero) + require.NoError(t, err) + rawShares0, err := BlobsToShares(blobs[0]) + require.NoError(t, err) + rawShares1, err := BlobsToShares(blobs[1]) + require.NoError(t, err) + + rawShares := make([][]byte, 0) + rawShares = append(rawShares, append(rawShares0, padding0.ToBytes())...) + rawShares = append(rawShares, append(rawShares1, padding1.ToBytes())...) + + bs := ipld.NewMemBlockservice() + batching := ds_sync.MutexWrap(ds.NewMapDatastore()) + headerStore, err := store.NewStore[*header.ExtendedHeader](batching) + require.NoError(t, err) + eds, err := ipld.AddShares(ctx, rawShares, bs) + require.NoError(t, err) + + h := headertest.ExtendedHeaderFromEDS(t, 1, eds) + err = headerStore.Init(ctx, h) + require.NoError(t, err) + + fn := func(ctx context.Context, height uint64) (*header.ExtendedHeader, error) { + return headerStore.GetByHeight(ctx, height) + } + service := NewService(nil, getters.NewIPLDGetter(bs), fn) + + newBlob, err := service.Get(ctx, 1, blobs[1].Namespace(), blobs[1].Commitment) + require.NoError(t, err) + assert.Equal(t, newBlob.Commitment, blobs[1].Commitment) +} + +func TestService_Get(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + sizes := []int{1, 6, 3, 2, 4, 6, 8, 2, 15, 17} + + appBlobs, err := blobtest.GenerateV0Blobs(sizes, true) + require.NoError(t, err) + blobs, err := convertBlobs(appBlobs...) + require.NoError(t, err) + + service := createService(ctx, t, blobs) + for _, blob := range blobs { + b, err := service.Get(ctx, 1, blob.Namespace(), blob.Commitment) + require.NoError(t, err) + assert.Equal(t, b.Commitment, blob.Commitment) + } +} + +func TestService_GetAllWithoutPadding(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + appBlob, err := blobtest.GenerateV0Blobs([]int{9, 5}, true) + require.NoError(t, err) + blobs, err := convertBlobs(appBlob...) + require.NoError(t, err) + + ns1, ns2 := blobs[0].Namespace().ToAppNamespace(), blobs[1].Namespace().ToAppNamespace() + + padding0, err := shares.NamespacePaddingShare(ns1, appconsts.ShareVersionZero) + require.NoError(t, err) + padding1, err := shares.NamespacePaddingShare(ns2, appconsts.ShareVersionZero) + require.NoError(t, err) + rawShares0, err := BlobsToShares(blobs[0]) + require.NoError(t, err) + rawShares1, err := BlobsToShares(blobs[1]) + require.NoError(t, err) + rawShares := make([][]byte, 0) + + // create shares in correct order with padding shares + if bytes.Compare(blobs[0].Namespace(), blobs[1].Namespace()) <= 0 { + rawShares = append(rawShares, append(rawShares0, padding0.ToBytes())...) + rawShares = append(rawShares, append(rawShares1, padding1.ToBytes())...) + } else { + rawShares = append(rawShares, append(rawShares1, padding1.ToBytes())...) + rawShares = append(rawShares, append(rawShares0, padding0.ToBytes())...) + } + + bs := ipld.NewMemBlockservice() + batching := ds_sync.MutexWrap(ds.NewMapDatastore()) + headerStore, err := store.NewStore[*header.ExtendedHeader](batching) + require.NoError(t, err) + eds, err := ipld.AddShares(ctx, rawShares, bs) + require.NoError(t, err) + + h := headertest.ExtendedHeaderFromEDS(t, 1, eds) + err = headerStore.Init(ctx, h) + require.NoError(t, err) + + fn := func(ctx context.Context, height uint64) (*header.ExtendedHeader, error) { + return headerStore.GetByHeight(ctx, height) + } + + service := NewService(nil, getters.NewIPLDGetter(bs), fn) + + _, err = service.GetAll(ctx, 1, []share.Namespace{blobs[0].Namespace(), blobs[1].Namespace()}) + require.NoError(t, err) +} + +// BenchmarkGetByCommitment-12 3139 380827 ns/op 701647 B/op 4990 allocs/op +func BenchmarkGetByCommitment(b *testing.B) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + b.Cleanup(cancel) + appBlobs, err := blobtest.GenerateV0Blobs([]int{32, 32}, true) + require.NoError(b, err) + + blobs, err := convertBlobs(appBlobs...) + require.NoError(b, err) + + service := createService(ctx, b, blobs) + b.ResetTimer() + for i := 0; i < b.N; i++ { + b.ReportAllocs() + _, _, err = service.getByCommitment( + ctx, 1, blobs[1].Namespace(), blobs[1].Commitment, + ) + require.NoError(b, err) + } +} + +func createService(ctx context.Context, t testing.TB, blobs []*Blob) *Service { + bs := ipld.NewMemBlockservice() + batching := ds_sync.MutexWrap(ds.NewMapDatastore()) + headerStore, err := store.NewStore[*header.ExtendedHeader](batching) + require.NoError(t, err) + rawShares, err := BlobsToShares(blobs...) + require.NoError(t, err) + eds, err := ipld.AddShares(ctx, rawShares, bs) + require.NoError(t, err) + + h := headertest.ExtendedHeaderFromEDS(t, 1, eds) + err = headerStore.Init(ctx, h) + require.NoError(t, err) + + fn := func(ctx context.Context, height uint64) (*header.ExtendedHeader, error) { + return headerStore.GetByHeight(ctx, height) + } + return NewService(nil, getters.NewIPLDGetter(bs), fn) +} diff --git a/cmd/auth.go b/cmd/auth.go index c4eb057a29..a637373242 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -14,6 +14,7 @@ import ( flag "github.com/spf13/pflag" "github.com/celestiaorg/celestia-node/api/rpc/perms" + "github.com/celestiaorg/celestia-node/libs/authtoken" "github.com/celestiaorg/celestia-node/libs/keystore" nodemod "github.com/celestiaorg/celestia-node/nodebuilder/node" ) @@ -24,61 +25,61 @@ func AuthCmd(fsets ...*flag.FlagSet) *cobra.Command { Short: "Signs and outputs a hex-encoded JWT token with the given permissions.", Long: "Signs and outputs a hex-encoded JWT token with the given permissions. NOTE: only use this command when " + "the node has already been initialized and started.", - RunE: newToken, - } + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return fmt.Errorf("must specify permissions") + } + permissions, err := convertToPerms(args[0]) + if err != nil { + return err + } - for _, set := range fsets { - cmd.Flags().AddFlagSet(set) - } - return cmd -} + ks, err := newKeystore(StorePath(cmd.Context())) + if err != nil { + return err -func newToken(cmd *cobra.Command, args []string) error { - if len(args) != 1 { - return fmt.Errorf("must specify permissions") - } + } - permissions, err := convertToPerms(args[0]) - if err != nil { - return err - } + key, err := ks.Get(nodemod.SecretName) + if err != nil { + if !errors.Is(err, keystore.ErrNotFound) { + return err + } + key, err = generateNewKey(ks) + if err != nil { + return err + } + } - expanded, err := homedir.Expand(filepath.Clean(StorePath(cmd.Context()))) - if err != nil { - return err - } - ks, err := keystore.NewFSKeystore(filepath.Join(expanded, "keys"), nil) - if err != nil { - return err + token, err := buildJWTToken(key.Body, permissions) + if err != nil { + return err + } + fmt.Printf("%s\n", token) + return nil + }, } - var key keystore.PrivKey - key, err = ks.Get(nodemod.SecretName) - if err != nil { - if !errors.Is(err, keystore.ErrNotFound) { - return err - } - // otherwise, generate and save new priv key - key, err = generateNewKey(ks) - if err != nil { - return err - } + for _, set := range fsets { + cmd.Flags().AddFlagSet(set) } + return cmd +} - signer, err := jwt.NewHS256(key.Body) +func newKeystore(path string) (keystore.Keystore, error) { + expanded, err := homedir.Expand(filepath.Clean(path)) if err != nil { - return err + return nil, err } + return keystore.NewFSKeystore(filepath.Join(expanded, "keys"), nil) +} - token, err := jwt.NewTokenBuilder(signer).Build(&perms.JWTPayload{ - Allow: permissions, - }) +func buildJWTToken(body []byte, permissions []auth.Permission) (string, error) { + signer, err := jwt.NewHS256(body) if err != nil { - return err + return "", err } - - fmt.Printf("%s", token.InsecureString()) - return nil + return authtoken.NewSignedJWT(signer, permissions) } func generateNewKey(ks keystore.Keystore) (keystore.PrivKey, error) { diff --git a/cmd/cel-shed/eds_store_stress.go b/cmd/cel-shed/eds_store_stress.go new file mode 100644 index 0000000000..d106f73152 --- /dev/null +++ b/cmd/cel-shed/eds_store_stress.go @@ -0,0 +1,165 @@ +package main + +import ( + "context" + "errors" + _ "expvar" + "fmt" + "math" + "net/http" + "os" + "time" + + pyroscope "github.com/grafana/pyroscope-go" + logging "github.com/ipfs/go-log/v2" + "github.com/mitchellh/go-homedir" + "github.com/spf13/cobra" + + "github.com/celestiaorg/celestia-node/libs/edssser" + "github.com/celestiaorg/celestia-node/nodebuilder" + "github.com/celestiaorg/celestia-node/nodebuilder/node" +) + +const ( + edsStorePathFlag = "path" + edsWritesFlag = "writes" + edsSizeFlag = "size" + edsDisableLogFlag = "disable-log" + edsLogStatFreqFlag = "log-stat-freq" + edsCleanupFlag = "cleanup" + edsFreshStartFlag = "fresh" + + pyroscopeEndpointFlag = "pyroscope" + putTimeoutFlag = "timeout" + badgerLogLevelFlag = "badger-log-level" +) + +func init() { + edsStoreCmd.AddCommand(edsStoreStress) + + defaultPath := "~/.edssser" + path, err := homedir.Expand(defaultPath) + if err != nil { + panic(err) + } + + pathFlagUsage := fmt.Sprintf("Directory path to use for stress test. Uses %s by default.", defaultPath) + edsStoreStress.Flags().String(edsStorePathFlag, path, pathFlagUsage) + edsStoreStress.Flags().String(pyroscopeEndpointFlag, "", + "Pyroscope address. If no address provided, pyroscope will be disabled") + edsStoreStress.Flags().Int(edsWritesFlag, math.MaxInt, "Total EDS writes to make. MaxInt by default.") + edsStoreStress.Flags().Int(edsSizeFlag, 128, "Chooses EDS size. 128 by default.") + edsStoreStress.Flags().Bool(edsDisableLogFlag, false, "Disables logging. Enabled by default.") + edsStoreStress.Flags().Int(edsLogStatFreqFlag, 10, "Write statistic logging frequency. 10 by default.") + edsStoreStress.Flags().Bool(edsCleanupFlag, false, "Cleans up the store on stop. Disabled by default.") + edsStoreStress.Flags().Bool(edsFreshStartFlag, false, "Cleanup previous state on start. Disabled by default.") + edsStoreStress.Flags().Int(putTimeoutFlag, 30, "Sets put timeout in seconds. 30 sec by default.") + edsStoreStress.Flags().String(badgerLogLevelFlag, "INFO", "Badger log level, Defaults to INFO") + + // kill redundant print + nodebuilder.PrintKeyringInfo = false +} + +var edsStoreCmd = &cobra.Command{ + Use: "eds-store [subcommand]", + Short: "Collection of eds-store related utilities", +} + +var edsStoreStress = &cobra.Command{ + Use: "stress", + Short: `Runs eds.Store stress test over default node.Store Datastore backend (e.g. Badger).`, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) (err error) { + // expose expvar vars over http + go http.ListenAndServe(":9999", http.DefaultServeMux) //nolint:errcheck,gosec + + endpoint, _ := cmd.Flags().GetString(pyroscopeEndpointFlag) + if endpoint != "" { + _, err = pyroscope.Start(pyroscope.Config{ + ApplicationName: "cel-shred.stresser", + ServerAddress: endpoint, + ProfileTypes: []pyroscope.ProfileType{ + pyroscope.ProfileCPU, + pyroscope.ProfileAllocObjects, + pyroscope.ProfileAllocSpace, + pyroscope.ProfileInuseObjects, + pyroscope.ProfileInuseSpace, + }, + }) + if err != nil { + fmt.Printf("failed to launch pyroscope with addr: %s err: %s\n", endpoint, err.Error()) + } else { + fmt.Println("connected pyroscope to:", endpoint) + } + } + + path, _ := cmd.Flags().GetString(edsStorePathFlag) + fmt.Printf("using %s\n", path) + + freshStart, _ := cmd.Flags().GetBool(edsFreshStartFlag) + if freshStart { + err = os.RemoveAll(path) + if err != nil { + return err + } + } + + cleanup, _ := cmd.Flags().GetBool(edsCleanupFlag) + if cleanup { + defer func() { + err = errors.Join(err, os.RemoveAll(path)) + }() + } + + loglevel, _ := cmd.Flags().GetString(badgerLogLevelFlag) + if err = logging.SetLogLevel("badger", loglevel); err != nil { + return err + } + + disableLog, _ := cmd.Flags().GetBool(edsDisableLogFlag) + logFreq, _ := cmd.Flags().GetInt(edsLogStatFreqFlag) + edsWrites, _ := cmd.Flags().GetInt(edsWritesFlag) + edsSize, _ := cmd.Flags().GetInt(edsSizeFlag) + putTimeout, _ := cmd.Flags().GetInt(putTimeoutFlag) + + cfg := edssser.Config{ + EDSSize: edsSize, + EDSWrites: edsWrites, + EnableLog: !disableLog, + LogFilePath: path, + StatLogFreq: logFreq, + OpTimeout: time.Duration(putTimeout) * time.Second, + } + + err = nodebuilder.Init(*nodebuilder.DefaultConfig(node.Full), path, node.Full) + if err != nil { + return err + } + + nodestore, err := nodebuilder.OpenStore(path, nil) + if err != nil { + return err + } + defer func() { + err = errors.Join(err, nodestore.Close()) + }() + + datastore, err := nodestore.Datastore() + if err != nil { + return err + } + + stresser, err := edssser.NewEDSsser(path, datastore, cfg) + if err != nil { + return err + } + + stats, err := stresser.Run(cmd.Context()) + if !errors.Is(err, context.Canceled) { + return err + } + + fmt.Printf("%s", stats.Finalize()) + return nil + }, +} diff --git a/cmd/cel-shed/main.go b/cmd/cel-shed/main.go index 7982cfc1be..872bbb48a9 100644 --- a/cmd/cel-shed/main.go +++ b/cmd/cel-shed/main.go @@ -3,12 +3,14 @@ package main import ( "context" "os" + "os/signal" + "syscall" "github.com/spf13/cobra" ) func init() { - rootCmd.AddCommand(p2pCmd, headerCmd) + rootCmd.AddCommand(p2pCmd, headerCmd, edsStoreCmd) } var rootCmd = &cobra.Command{ @@ -26,5 +28,8 @@ func main() { } func run() error { - return rootCmd.ExecuteContext(context.Background()) + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + return rootCmd.ExecuteContext(ctx) } diff --git a/cmd/celestia/bridge.go b/cmd/celestia/bridge.go deleted file mode 100644 index fb5066e5d4..0000000000 --- a/cmd/celestia/bridge.go +++ /dev/null @@ -1,47 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - - cmdnode "github.com/celestiaorg/celestia-node/cmd" - "github.com/celestiaorg/celestia-node/nodebuilder/core" - "github.com/celestiaorg/celestia-node/nodebuilder/gateway" - "github.com/celestiaorg/celestia-node/nodebuilder/node" - "github.com/celestiaorg/celestia-node/nodebuilder/p2p" - "github.com/celestiaorg/celestia-node/nodebuilder/rpc" - "github.com/celestiaorg/celestia-node/nodebuilder/state" -) - -// NOTE: We should always ensure that the added Flags below are parsed somewhere, like in the -// PersistentPreRun func on parent command. - -func init() { - flags := []*pflag.FlagSet{ - cmdnode.NodeFlags(), - p2p.Flags(), - core.Flags(), - cmdnode.MiscFlags(), - rpc.Flags(), - gateway.Flags(), - state.Flags(), - } - - bridgeCmd.AddCommand( - cmdnode.Init(flags...), - cmdnode.Start(flags...), - cmdnode.AuthCmd(flags...), - cmdnode.ResetStore(flags...), - cmdnode.RemoveConfigCmd(flags...), - cmdnode.UpdateConfigCmd(flags...), - ) -} - -var bridgeCmd = &cobra.Command{ - Use: "bridge [subcommand]", - Args: cobra.NoArgs, - Short: "Manage your Bridge node", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return persistentPreRunEnv(cmd, node.Bridge, args) - }, -} diff --git a/cmd/celestia/cmd_test.go b/cmd/celestia/cmd_test.go index 503548b708..94dd3625b8 100644 --- a/cmd/celestia/cmd_test.go +++ b/cmd/celestia/cmd_test.go @@ -33,7 +33,7 @@ func TestCompletionHelpString(t *testing.T) { } methods := reflect.VisibleFields(reflect.TypeOf(TestFields{})) for i, method := range methods { - require.Equal(t, testOutputs[i], parseSignatureForHelpstring(method)) + require.Equal(t, testOutputs[i], parseSignatureForHelpString(method)) } } @@ -128,3 +128,23 @@ func TestBridge(t *testing.T) { }) */ } + +func parseSignatureForHelpString(methodSig reflect.StructField) string { + simplifiedSignature := "(" + in, out := methodSig.Type.NumIn(), methodSig.Type.NumOut() + for i := 1; i < in; i++ { + simplifiedSignature += methodSig.Type.In(i).String() + if i != in-1 { + simplifiedSignature += ", " + } + } + simplifiedSignature += ") -> (" + for i := 0; i < out-1; i++ { + simplifiedSignature += methodSig.Type.Out(i).String() + if i != out-2 { + simplifiedSignature += ", " + } + } + simplifiedSignature += ")" + return simplifiedSignature +} diff --git a/cmd/celestia/full.go b/cmd/celestia/full.go deleted file mode 100644 index 912de0bca8..0000000000 --- a/cmd/celestia/full.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - - cmdnode "github.com/celestiaorg/celestia-node/cmd" - "github.com/celestiaorg/celestia-node/nodebuilder/core" - "github.com/celestiaorg/celestia-node/nodebuilder/gateway" - "github.com/celestiaorg/celestia-node/nodebuilder/header" - "github.com/celestiaorg/celestia-node/nodebuilder/node" - "github.com/celestiaorg/celestia-node/nodebuilder/p2p" - "github.com/celestiaorg/celestia-node/nodebuilder/rpc" - "github.com/celestiaorg/celestia-node/nodebuilder/state" -) - -// NOTE: We should always ensure that the added Flags below are parsed somewhere, like in the -// PersistentPreRun func on parent command. - -func init() { - flags := []*pflag.FlagSet{ - cmdnode.NodeFlags(), - p2p.Flags(), - header.Flags(), - cmdnode.MiscFlags(), - // NOTE: for now, state-related queries can only be accessed - // over an RPC connection with a celestia-core node. - core.Flags(), - rpc.Flags(), - gateway.Flags(), - state.Flags(), - } - - fullCmd.AddCommand( - cmdnode.Init(flags...), - cmdnode.Start(flags...), - cmdnode.AuthCmd(flags...), - cmdnode.ResetStore(flags...), - cmdnode.RemoveConfigCmd(flags...), - cmdnode.UpdateConfigCmd(flags...), - ) -} - -var fullCmd = &cobra.Command{ - Use: "full [subcommand]", - Args: cobra.NoArgs, - Short: "Manage your Full node", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return persistentPreRunEnv(cmd, node.Full, args) - }, -} diff --git a/cmd/celestia/light.go b/cmd/celestia/light.go deleted file mode 100644 index 9c63945445..0000000000 --- a/cmd/celestia/light.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - - cmdnode "github.com/celestiaorg/celestia-node/cmd" - "github.com/celestiaorg/celestia-node/nodebuilder/core" - "github.com/celestiaorg/celestia-node/nodebuilder/gateway" - "github.com/celestiaorg/celestia-node/nodebuilder/header" - "github.com/celestiaorg/celestia-node/nodebuilder/node" - "github.com/celestiaorg/celestia-node/nodebuilder/p2p" - "github.com/celestiaorg/celestia-node/nodebuilder/rpc" - "github.com/celestiaorg/celestia-node/nodebuilder/state" -) - -// NOTE: We should always ensure that the added Flags below are parsed somewhere, like in the -// PersistentPreRun func on parent command. - -func init() { - flags := []*pflag.FlagSet{ - cmdnode.NodeFlags(), - p2p.Flags(), - header.Flags(), - cmdnode.MiscFlags(), - // NOTE: for now, state-related queries can only be accessed - // over an RPC connection with a celestia-core node. - core.Flags(), - rpc.Flags(), - gateway.Flags(), - state.Flags(), - } - - lightCmd.AddCommand( - cmdnode.Init(flags...), - cmdnode.Start(flags...), - cmdnode.AuthCmd(flags...), - cmdnode.ResetStore(flags...), - cmdnode.RemoveConfigCmd(flags...), - cmdnode.UpdateConfigCmd(flags...), - ) -} - -var lightCmd = &cobra.Command{ - Use: "light [subcommand]", - Args: cobra.NoArgs, - Short: "Manage your Light node", - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return persistentPreRunEnv(cmd, node.Light, args) - }, -} diff --git a/cmd/celestia/main.go b/cmd/celestia/main.go index 24c9423100..76287d998f 100644 --- a/cmd/celestia/main.go +++ b/cmd/celestia/main.go @@ -5,9 +5,28 @@ import ( "os" "github.com/spf13/cobra" + "github.com/spf13/pflag" + + cmdnode "github.com/celestiaorg/celestia-node/cmd" ) +func WithSubcommands() func(*cobra.Command, []*pflag.FlagSet) { + return func(c *cobra.Command, flags []*pflag.FlagSet) { + c.AddCommand( + cmdnode.Init(flags...), + cmdnode.Start(cmdnode.WithFlagSet(flags)), + cmdnode.AuthCmd(flags...), + cmdnode.ResetStore(flags...), + cmdnode.RemoveConfigCmd(flags...), + cmdnode.UpdateConfigCmd(flags...), + ) + } +} + func init() { + bridgeCmd := cmdnode.NewBridge(WithSubcommands()) + lightCmd := cmdnode.NewLight(WithSubcommands()) + fullCmd := cmdnode.NewFull(WithSubcommands()) rootCmd.AddCommand( bridgeCmd, lightCmd, diff --git a/cmd/celestia/rpc.go b/cmd/celestia/rpc.go index afbe13c3f8..11e96c2e46 100644 --- a/cmd/celestia/rpc.go +++ b/cmd/celestia/rpc.go @@ -1,329 +1,32 @@ package main import ( - "bytes" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "io" - "log" - "net/http" - "os" - "reflect" - "strconv" - "strings" - - "github.com/cosmos/cosmos-sdk/types" - "github.com/spf13/cobra" - - "github.com/celestiaorg/celestia-node/api/rpc/client" - "github.com/celestiaorg/celestia-node/state" -) - -const ( - authEnvKey = "CELESTIA_NODE_AUTH_TOKEN" + "github.com/celestiaorg/celestia-node/cmd" + blob "github.com/celestiaorg/celestia-node/nodebuilder/blob/cmd" + das "github.com/celestiaorg/celestia-node/nodebuilder/das/cmd" + header "github.com/celestiaorg/celestia-node/nodebuilder/header/cmd" + node "github.com/celestiaorg/celestia-node/nodebuilder/node/cmd" + p2p "github.com/celestiaorg/celestia-node/nodebuilder/p2p/cmd" + share "github.com/celestiaorg/celestia-node/nodebuilder/share/cmd" + state "github.com/celestiaorg/celestia-node/nodebuilder/state/cmd" ) -var requestURL string -var authTokenFlag string - -type jsonRPCRequest struct { - ID int64 `json:"id"` - JSONRPC string `json:"jsonrpc"` - Method string `json:"method"` - Params []interface{} `json:"params"` -} - func init() { - rpcCmd.PersistentFlags().StringVar( - &requestURL, - "url", - "http://localhost:26658", - "Request URL", - ) - rpcCmd.PersistentFlags().StringVar( - &authTokenFlag, - "auth", - "", - "Authorization token (if not provided, the "+authEnvKey+" environment variable will be used)", + blob.Cmd.PersistentFlags().AddFlagSet(cmd.RPCFlags()) + das.Cmd.PersistentFlags().AddFlagSet(cmd.RPCFlags()) + header.Cmd.PersistentFlags().AddFlagSet(cmd.RPCFlags()) + p2p.Cmd.PersistentFlags().AddFlagSet(cmd.RPCFlags()) + share.Cmd.PersistentFlags().AddFlagSet(cmd.RPCFlags()) + state.Cmd.PersistentFlags().AddFlagSet(cmd.RPCFlags()) + node.Cmd.PersistentFlags().AddFlagSet(cmd.RPCFlags()) + + rootCmd.AddCommand( + blob.Cmd, + das.Cmd, + header.Cmd, + p2p.Cmd, + share.Cmd, + state.Cmd, + node.Cmd, ) - rootCmd.AddCommand(rpcCmd) -} - -var rpcCmd = &cobra.Command{ - Use: "rpc [namespace] [method] [params...]", - Short: "Send JSON-RPC request", - Args: cobra.MinimumNArgs(2), - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - modules := client.Modules - if len(args) == 0 { - // get keys from modules (map[string]interface{}) - var keys []string - for k := range modules { - keys = append(keys, k) - } - return keys, cobra.ShellCompDirectiveNoFileComp - } else if len(args) == 1 { - // get methods from module - module := modules[args[0]] - methods := reflect.VisibleFields(reflect.TypeOf(module).Elem()) - var methodNames []string - for _, m := range methods { - methodNames = append(methodNames, m.Name+"\t"+parseSignatureForHelpstring(m)) - } - return methodNames, cobra.ShellCompDirectiveNoFileComp - } - return nil, cobra.ShellCompDirectiveNoFileComp - }, - Run: func(cmd *cobra.Command, args []string) { - namespace := args[0] - method := args[1] - params := parseParams(method, args[2:]) - - sendJSONRPCRequest(namespace, method, params) - }, -} - -func parseParams(method string, params []string) []interface{} { - parsedParams := make([]interface{}, len(params)) - - switch method { - case "GetSharesByNamespace": - // 1. Share Root - root, err := parseJSON(params[0]) - if err != nil { - panic(fmt.Errorf("couldn't parse share root as json: %v", err)) - } - parsedParams[0] = root - // 2. NamespaceID - if strings.HasPrefix(params[1], "0x") { - decoded, err := hex.DecodeString(params[1][2:]) - if err != nil { - panic("Error decoding namespace ID: hex string could not be decoded.") - } - parsedParams[1] = decoded - } else { - // otherwise, it's just a base64 string - parsedParams[1] = params[1] - } - return parsedParams - case "SubmitPayForBlob": - // 1. NamespaceID - if strings.HasPrefix(params[0], "0x") { - decoded, err := hex.DecodeString(params[0][2:]) - if err != nil { - panic("Error decoding namespace ID: hex string could not be decoded.") - } - parsedParams[0] = decoded - } else { - // otherwise, it's just a base64 string - parsedParams[0] = params[0] - } - // 2. Blob - switch { - case strings.HasPrefix(params[1], "0x"): - decoded, err := hex.DecodeString(params[1][2:]) - if err != nil { - panic("Error decoding blob: hex string could not be decoded.") - } - parsedParams[0] = decoded - case strings.HasPrefix(params[1], "\""): - // user input an utf string that needs to be encoded to base64 - parsedParams[1] = base64.StdEncoding.EncodeToString([]byte(params[1])) - default: - // otherwise, we assume the user has already encoded their input to base64 - parsedParams[1] = params[1] - } - // 3. Fee (state.Int is a string) - parsedParams[2] = params[2] - // 4. GasLimit (uint64) - num, err := strconv.ParseUint(params[3], 10, 64) - if err != nil { - panic("Error parsing gas limit: uint64 could not be parsed.") - } - parsedParams[3] = num - return parsedParams - case "QueryDelegation", "QueryUnbonding", "BalanceForAddress": - var err error - parsedParams[0], err = parseAddressFromString(params[0]) - if err != nil { - panic(fmt.Errorf("error parsing address: %w", err)) - } - return parsedParams - case "QueryRedelegations": - var err error - parsedParams[0], err = parseAddressFromString(params[0]) - if err != nil { - panic(fmt.Errorf("error parsing address: %w", err)) - } - parsedParams[1], err = parseAddressFromString(params[1]) - if err != nil { - panic(fmt.Errorf("error parsing address: %w", err)) - } - return parsedParams - case "Transfer", "Delegate", "Undelegate": - // 1. Address - var err error - parsedParams[0], err = parseAddressFromString(params[0]) - if err != nil { - panic(fmt.Errorf("error parsing address: %w", err)) - } - // 2. Amount + Fee - parsedParams[1] = params[1] - parsedParams[2] = params[2] - // 3. GasLimit (uint64) - num, err := strconv.ParseUint(params[3], 10, 64) - if err != nil { - panic("Error parsing gas limit: uint64 could not be parsed.") - } - parsedParams[3] = num - return parsedParams - case "CancelUnbondingDelegation": - // 1. Validator Address - var err error - parsedParams[0], err = parseAddressFromString(params[0]) - if err != nil { - panic(fmt.Errorf("error parsing address: %w", err)) - } - // 2. Amount + Height + Fee - parsedParams[1] = params[1] - parsedParams[2] = params[2] - parsedParams[3] = params[3] - // 4. GasLimit (uint64) - num, err := strconv.ParseUint(params[4], 10, 64) - if err != nil { - panic("Error parsing gas limit: uint64 could not be parsed.") - } - parsedParams[4] = num - case "BeginRedelegate": - // 1. Source Validator Address - var err error - parsedParams[0], err = parseAddressFromString(params[0]) - if err != nil { - panic(fmt.Errorf("error parsing address: %w", err)) - } - // 2. Destination Validator Address - parsedParams[1], err = parseAddressFromString(params[1]) - if err != nil { - panic(fmt.Errorf("error parsing address: %w", err)) - } - // 2. Amount + Fee - parsedParams[2] = params[2] - parsedParams[3] = params[3] - // 4. GasLimit (uint64) - num, err := strconv.ParseUint(params[4], 10, 64) - if err != nil { - panic("Error parsing gas limit: uint64 could not be parsed.") - } - parsedParams[4] = num - default: - } - - for i, param := range params { - if param[0] == '{' || param[0] == '[' { - rawJSON, err := parseJSON(param) - if err != nil { - parsedParams[i] = param - } else { - parsedParams[i] = rawJSON - } - } else { - // try to parse arguments as numbers before adding them as strings - num, err := strconv.ParseInt(param, 10, 64) - if err == nil { - parsedParams[i] = num - continue - } - parsedParams[i] = param - } - } - - return parsedParams -} - -func sendJSONRPCRequest(namespace, method string, params []interface{}) { - url := requestURL - request := jsonRPCRequest{ - ID: 1, - JSONRPC: "2.0", - Method: fmt.Sprintf("%s.%s", namespace, method), - Params: params, - } - - requestBody, err := json.Marshal(request) - if err != nil { - log.Fatalf("Error marshaling JSON-RPC request: %v", err) - } - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(requestBody)) - if err != nil { - log.Fatalf("Error creating JSON-RPC request: %v", err) - } - - req.Header.Set("Content-Type", "application/json") - - authToken := authTokenFlag - if authToken == "" { - authToken = os.Getenv(authEnvKey) - } - if authToken != "" { - req.Header.Set("Authorization", "Bearer "+authToken) - } - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - log.Fatalf("Error sending JSON-RPC request: %v", err) - } - defer resp.Body.Close() - - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - log.Fatalf("Error reading response body: %v", err) //nolint:gocritic - } - - fmt.Println(string(responseBody)) -} - -func parseAddressFromString(addrStr string) (state.Address, error) { - var addr state.AccAddress - addr, err := types.AccAddressFromBech32(addrStr) - if err != nil { - // first check if it is a validator address and can be converted - valAddr, err := types.ValAddressFromBech32(addrStr) - if err != nil { - return nil, errors.New("address must be a valid account or validator address ") - } - return valAddr, nil - } - - return addr, nil -} - -func parseSignatureForHelpstring(methodSig reflect.StructField) string { - simplifiedSignature := "(" - in, out := methodSig.Type.NumIn(), methodSig.Type.NumOut() - for i := 1; i < in; i++ { - simplifiedSignature += methodSig.Type.In(i).String() - if i != in-1 { - simplifiedSignature += ", " - } - } - simplifiedSignature += ") -> (" - for i := 0; i < out-1; i++ { - simplifiedSignature += methodSig.Type.Out(i).String() - if i != out-2 { - simplifiedSignature += ", " - } - } - simplifiedSignature += ")" - return simplifiedSignature -} - -func parseJSON(param string) (json.RawMessage, error) { - var raw json.RawMessage - err := json.Unmarshal([]byte(param), &raw) - return raw, err } diff --git a/cmd/celestia/util.go b/cmd/celestia/util.go deleted file mode 100644 index 85505c3a60..0000000000 --- a/cmd/celestia/util.go +++ /dev/null @@ -1,74 +0,0 @@ -package main - -import ( - "github.com/spf13/cobra" - - cmdnode "github.com/celestiaorg/celestia-node/cmd" - "github.com/celestiaorg/celestia-node/nodebuilder/core" - "github.com/celestiaorg/celestia-node/nodebuilder/gateway" - "github.com/celestiaorg/celestia-node/nodebuilder/header" - "github.com/celestiaorg/celestia-node/nodebuilder/node" - "github.com/celestiaorg/celestia-node/nodebuilder/p2p" - "github.com/celestiaorg/celestia-node/nodebuilder/rpc" - "github.com/celestiaorg/celestia-node/nodebuilder/state" -) - -func persistentPreRunEnv(cmd *cobra.Command, nodeType node.Type, _ []string) error { - var ( - ctx = cmd.Context() - err error - ) - - ctx = cmdnode.WithNodeType(ctx, nodeType) - - parsedNetwork, err := p2p.ParseNetwork(cmd) - if err != nil { - return err - } - ctx = cmdnode.WithNetwork(ctx, parsedNetwork) - ctx = cmdnode.WithNodeBuildInfo(ctx, &node.BuildInfo{ - LastCommit: lastCommit, - SemanticVersion: semanticVersion, - SystemVersion: systemVersion, - GolangVersion: golangVersion, - }) - - // loads existing config into the environment - ctx, err = cmdnode.ParseNodeFlags(ctx, cmd, cmdnode.Network(ctx)) - if err != nil { - return err - } - - cfg := cmdnode.NodeConfig(ctx) - - err = p2p.ParseFlags(cmd, &cfg.P2P) - if err != nil { - return err - } - - err = core.ParseFlags(cmd, &cfg.Core) - if err != nil { - return err - } - - if nodeType != node.Bridge { - err = header.ParseFlags(cmd, &cfg.Header) - if err != nil { - return err - } - } - - ctx, err = cmdnode.ParseMiscFlags(ctx, cmd) - if err != nil { - return err - } - - rpc.ParseFlags(cmd, &cfg.RPC) - gateway.ParseFlags(cmd, &cfg.Gateway) - state.ParseFlags(cmd, &cfg.State) - - // set config - ctx = cmdnode.WithNodeConfig(ctx, &cfg) - cmd.SetContext(ctx) - return nil -} diff --git a/cmd/celestia/version.go b/cmd/celestia/version.go index 462f17b474..f0d379e7a7 100644 --- a/cmd/celestia/version.go +++ b/cmd/celestia/version.go @@ -2,18 +2,10 @@ package main import ( "fmt" - "runtime" "github.com/spf13/cobra" -) - -var ( - buildTime string - lastCommit string - semanticVersion string - systemVersion = fmt.Sprintf("%s/%s", runtime.GOARCH, runtime.GOOS) - golangVersion = runtime.Version() + "github.com/celestiaorg/celestia-node/nodebuilder/node" ) var versionCmd = &cobra.Command{ @@ -24,9 +16,10 @@ var versionCmd = &cobra.Command{ } func printBuildInfo(_ *cobra.Command, _ []string) { - fmt.Printf("Semantic version: %s\n", semanticVersion) - fmt.Printf("Commit: %s\n", lastCommit) - fmt.Printf("Build Date: %s\n", buildTime) - fmt.Printf("System version: %s\n", systemVersion) - fmt.Printf("Golang version: %s\n", golangVersion) + buildInfo := node.GetBuildInfo() + fmt.Printf("Semantic version: %s\n", buildInfo.SemanticVersion) + fmt.Printf("Commit: %s\n", buildInfo.LastCommit) + fmt.Printf("Build Date: %s\n", buildInfo.BuildTime) + fmt.Printf("System version: %s\n", buildInfo.SystemVersion) + fmt.Printf("Golang version: %s\n", buildInfo.GolangVersion) } diff --git a/cmd/env.go b/cmd/env.go index ca915d884f..f9860a2de8 100644 --- a/cmd/env.go +++ b/cmd/env.go @@ -38,11 +38,6 @@ func NodeConfig(ctx context.Context) nodebuilder.Config { return cfg } -// NodeInfo reads the node build inforamtion from the context. -func NodeInfo(ctx context.Context) node.BuildInfo { - return ctx.Value(buildInfo{}).(node.BuildInfo) -} - // WithNodeType sets the node type in the given context. func WithNodeType(ctx context.Context, tp node.Type) context.Context { return context.WithValue(ctx, nodeTypeKey{}, tp) @@ -78,16 +73,10 @@ func WithNodeConfig(ctx context.Context, config *nodebuilder.Config) context.Con return context.WithValue(ctx, configKey{}, *config) } -// WithNodeConfig sets the node config build information. -func WithNodeBuildInfo(ctx context.Context, info *node.BuildInfo) context.Context { - return context.WithValue(ctx, buildInfo{}, *info) -} - type ( optionsKey struct{} configKey struct{} storePathKey struct{} nodeTypeKey struct{} networkKey struct{} - buildInfo struct{} ) diff --git a/cmd/flags_misc.go b/cmd/flags_misc.go index 4483e17201..332d2f8022 100644 --- a/cmd/flags_misc.go +++ b/cmd/flags_misc.go @@ -7,17 +7,12 @@ import ( "net/http/pprof" "strings" + otelpyroscope "github.com/grafana/otel-profiling-go" logging "github.com/ipfs/go-log/v2" - otelpyroscope "github.com/pyroscope-io/otel-profiling-go" "github.com/spf13/cobra" flag "github.com/spf13/pflag" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - "go.opentelemetry.io/otel/sdk/resource" - tracesdk "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.11.0" - "go.opentelemetry.io/otel/trace" "github.com/celestiaorg/celestia-node/logs" "github.com/celestiaorg/celestia-node/nodebuilder" @@ -25,8 +20,8 @@ import ( ) var ( - logLevelFlag = "log.level" - logLevelModuleFlag = "log.level.module" + LogLevelFlag = "log.level" + LogLevelModuleFlag = "log.level.module" pprofFlag = "pprof" tracingFlag = "tracing" tracingEndpointFlag = "tracing.endpoint" @@ -45,14 +40,14 @@ func MiscFlags() *flag.FlagSet { flags := &flag.FlagSet{} flags.String( - logLevelFlag, + LogLevelFlag, "INFO", `DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL and their lower-case forms`, ) flags.StringSlice( - logLevelModuleFlag, + LogLevelModuleFlag, nil, ":, e.g. pubsub:debug", ) @@ -128,24 +123,24 @@ and their lower-case forms`, // ParseMiscFlags parses miscellaneous flags from the given cmd and applies values to Env. func ParseMiscFlags(ctx context.Context, cmd *cobra.Command) (context.Context, error) { - logLevel := cmd.Flag(logLevelFlag).Value.String() + logLevel := cmd.Flag(LogLevelFlag).Value.String() if logLevel != "" { level, err := logging.LevelFromString(logLevel) if err != nil { - return ctx, fmt.Errorf("cmd: while parsing '%s': %w", logLevelFlag, err) + return ctx, fmt.Errorf("cmd: while parsing '%s': %w", LogLevelFlag, err) } logs.SetAllLoggers(level) } - logModules, err := cmd.Flags().GetStringSlice(logLevelModuleFlag) + logModules, err := cmd.Flags().GetStringSlice(LogLevelModuleFlag) if err != nil { panic(err) } for _, ll := range logModules { params := strings.Split(ll, ":") if len(params) != 2 { - return ctx, fmt.Errorf("cmd: %s arg must be in form :, e.g. pubsub:debug", logLevelModuleFlag) + return ctx, fmt.Errorf("cmd: %s arg must be in form :, e.g. pubsub:debug", LogLevelModuleFlag) } err := logging.SetLogLevel(params[0], params[1]) @@ -199,7 +194,6 @@ func ParseMiscFlags(ctx context.Context, cmd *cobra.Command) (context.Context, e } if ok { - var tp trace.TracerProvider opts := []otlptracehttp.Option{ otlptracehttp.WithCompression(otlptracehttp.GzipCompression), otlptracehttp.WithEndpoint(cmd.Flag(tracingEndpointFlag).Value.String()), @@ -210,30 +204,13 @@ func ParseMiscFlags(ctx context.Context, cmd *cobra.Command) (context.Context, e opts = append(opts, otlptracehttp.WithInsecure()) } - exp, err := otlptracehttp.New(cmd.Context(), opts...) - if err != nil { - return ctx, err - } - - tp = tracesdk.NewTracerProvider( - tracesdk.WithSampler(tracesdk.AlwaysSample()), - // Always be sure to batch in production. - tracesdk.WithBatcher(exp), - // Record information about this application in a Resource. - tracesdk.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceNameKey.String(fmt.Sprintf("Celestia-%s", NodeType(ctx).String())), - // TODO(@Wondertan): Versioning: semconv.ServiceVersionKey - )), - ) - + pyroOpts := make([]otelpyroscope.Option, 0) ok, err = cmd.Flags().GetBool(pyroscopeTracing) if err != nil { panic(err) } if ok { - tp = otelpyroscope.NewTracerProvider( - tp, + pyroOpts = append(pyroOpts, otelpyroscope.WithAppName("celestia.da-node"), otelpyroscope.WithPyroscopeURL(cmd.Flag(pyroscopeEndpoint).Value.String()), otelpyroscope.WithRootSpanOnly(true), @@ -242,8 +219,7 @@ func ParseMiscFlags(ctx context.Context, cmd *cobra.Command) (context.Context, e otelpyroscope.WithProfileBaselineURL(true), ) } - - otel.SetTracerProvider(tp) + ctx = WithNodeOptions(ctx, nodebuilder.WithTraces(opts, pyroOpts)) } ok, err = cmd.Flags().GetBool(metricsFlag) @@ -262,7 +238,7 @@ func ParseMiscFlags(ctx context.Context, cmd *cobra.Command) (context.Context, e opts = append(opts, otlpmetrichttp.WithInsecure()) } - ctx = WithNodeOptions(ctx, nodebuilder.WithMetrics(opts, NodeType(ctx), NodeInfo(ctx))) + ctx = WithNodeOptions(ctx, nodebuilder.WithMetrics(opts, NodeType(ctx))) } ok, err = cmd.Flags().GetBool(p2pMetrics) diff --git a/cmd/flags_node.go b/cmd/flags_node.go index 8c73a06169..ef5af26580 100644 --- a/cmd/flags_node.go +++ b/cmd/flags_node.go @@ -15,7 +15,7 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder/p2p" ) -var ( +const ( nodeStoreFlag = "node.store" nodeConfigFlag = "node.config" ) @@ -87,6 +87,10 @@ func DefaultNodeStorePath(tp string, network string) (string, error) { return "", err } } + if network == p2p.Mainnet.String() { + return fmt.Sprintf("%s/.celestia-%s", home, strings.ToLower(tp)), nil + } + // only include network name in path for testnets and custom networks return fmt.Sprintf( "%s/.celestia-%s-%s", home, diff --git a/cmd/node.go b/cmd/node.go new file mode 100644 index 0000000000..51ac4a6d2e --- /dev/null +++ b/cmd/node.go @@ -0,0 +1,88 @@ +package cmd + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + "github.com/celestiaorg/celestia-node/nodebuilder/core" + "github.com/celestiaorg/celestia-node/nodebuilder/gateway" + "github.com/celestiaorg/celestia-node/nodebuilder/header" + "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/nodebuilder/p2p" + "github.com/celestiaorg/celestia-node/nodebuilder/rpc" + "github.com/celestiaorg/celestia-node/nodebuilder/state" +) + +func NewBridge(options ...func(*cobra.Command, []*pflag.FlagSet)) *cobra.Command { + flags := []*pflag.FlagSet{ + NodeFlags(), + p2p.Flags(), + MiscFlags(), + core.Flags(), + rpc.Flags(), + gateway.Flags(), + state.Flags(), + } + cmd := &cobra.Command{ + Use: "bridge [subcommand]", + Args: cobra.NoArgs, + Short: "Manage your Bridge node", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return PersistentPreRunEnv(cmd, node.Bridge, args) + }, + } + for _, option := range options { + option(cmd, flags) + } + return cmd +} + +func NewLight(options ...func(*cobra.Command, []*pflag.FlagSet)) *cobra.Command { + flags := []*pflag.FlagSet{ + NodeFlags(), + p2p.Flags(), + header.Flags(), + MiscFlags(), + core.Flags(), + rpc.Flags(), + gateway.Flags(), + state.Flags(), + } + cmd := &cobra.Command{ + Use: "light [subcommand]", + Args: cobra.NoArgs, + Short: "Manage your Light node", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return PersistentPreRunEnv(cmd, node.Light, args) + }, + } + for _, option := range options { + option(cmd, flags) + } + return cmd +} + +func NewFull(options ...func(*cobra.Command, []*pflag.FlagSet)) *cobra.Command { + flags := []*pflag.FlagSet{ + NodeFlags(), + p2p.Flags(), + header.Flags(), + MiscFlags(), + core.Flags(), + rpc.Flags(), + gateway.Flags(), + state.Flags(), + } + cmd := &cobra.Command{ + Use: "full [subcommand]", + Args: cobra.NoArgs, + Short: "Manage your Full node", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + return PersistentPreRunEnv(cmd, node.Full, args) + }, + } + for _, option := range options { + option(cmd, flags) + } + return cmd +} diff --git a/cmd/rpc.go b/cmd/rpc.go new file mode 100644 index 0000000000..62e9fe7923 --- /dev/null +++ b/cmd/rpc.go @@ -0,0 +1,98 @@ +package cmd + +import ( + "context" + "errors" + "fmt" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + rpc "github.com/celestiaorg/celestia-node/api/rpc/client" + "github.com/celestiaorg/celestia-node/api/rpc/perms" + nodemod "github.com/celestiaorg/celestia-node/nodebuilder/node" +) + +const ( + // defaultRPCAddress is a default address to dial to + defaultRPCAddress = "http://localhost:26658" +) + +var ( + requestURL string + authTokenFlag string +) + +func RPCFlags() *flag.FlagSet { + fset := &flag.FlagSet{} + + fset.StringVar( + &requestURL, + "url", + defaultRPCAddress, + "Request URL", + ) + + fset.StringVar( + &authTokenFlag, + "token", + "", + "Authorization token", + ) + + storeFlag := NodeFlags().Lookup(nodeStoreFlag) + fset.AddFlag(storeFlag) + return fset +} + +func InitClient(cmd *cobra.Command, _ []string) error { + if authTokenFlag == "" { + storePath := "" + if !cmd.Flag(nodeStoreFlag).Changed { + return fmt.Errorf("cant get the access to the auth token: token/node-store flag was not specified") + } + storePath = cmd.Flag(nodeStoreFlag).Value.String() + token, err := getToken(storePath) + if err != nil { + return fmt.Errorf("cant get the access to the auth token: %v", err) + } + authTokenFlag = token + } + + client, err := rpc.NewClient(cmd.Context(), requestURL, authTokenFlag) + if err != nil { + return err + } + + ctx := context.WithValue(cmd.Context(), rpcClientKey{}, client) + cmd.SetContext(ctx) + return nil +} + +func getToken(path string) (string, error) { + if path == "" { + return "", errors.New("root directory was not specified") + } + + ks, err := newKeystore(path) + if err != nil { + return "", err + } + + key, err := ks.Get(nodemod.SecretName) + if err != nil { + fmt.Printf("error getting the JWT secret: %v", err) + return "", err + } + return buildJWTToken(key.Body, perms.AllPerms) +} + +type rpcClientKey struct{} + +func ParseClientFromCtx(ctx context.Context) (*rpc.Client, error) { + client, ok := ctx.Value(rpcClientKey{}).(*rpc.Client) + if !ok { + return nil, errors.New("rpc client was not set") + } + return client, nil +} diff --git a/cmd/start.go b/cmd/start.go index d46b553e20..281dfcc0e4 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -9,7 +9,6 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/spf13/cobra" - flag "github.com/spf13/pflag" "github.com/celestiaorg/celestia-app/app" "github.com/celestiaorg/celestia-app/app/encoding" @@ -18,7 +17,7 @@ import ( ) // Start constructs a CLI command to start Celestia Node daemon of any type with the given flags. -func Start(fsets ...*flag.FlagSet) *cobra.Command { +func Start(options ...func(*cobra.Command)) *cobra.Command { cmd := &cobra.Command{ Use: "start", Short: `Starts Node daemon. First stopping signal gracefully stops the Node and second terminates it. @@ -72,8 +71,9 @@ Options passed on start override configuration options only on start and are not return nd.Stop(ctx) }, } - for _, set := range fsets { - cmd.Flags().AddFlagSet(set) + // Apply each passed option to the command + for _, option := range options { + option(cmd) } return cmd } diff --git a/cmd/util.go b/cmd/util.go new file mode 100644 index 0000000000..08fa02155b --- /dev/null +++ b/cmd/util.go @@ -0,0 +1,137 @@ +package cmd + +import ( + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "github.com/celestiaorg/celestia-node/nodebuilder/core" + "github.com/celestiaorg/celestia-node/nodebuilder/gateway" + "github.com/celestiaorg/celestia-node/nodebuilder/header" + "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/nodebuilder/p2p" + rpc_cfg "github.com/celestiaorg/celestia-node/nodebuilder/rpc" + "github.com/celestiaorg/celestia-node/nodebuilder/state" + "github.com/celestiaorg/celestia-node/share" +) + +func PrintOutput(data interface{}, err error, formatData func(interface{}) interface{}) error { + switch { + case err != nil: + data = err.Error() + case formatData != nil: + data = formatData(data) + } + + resp := struct { + Result interface{} `json:"result"` + }{ + Result: data, + } + + bytes, err := json.MarshalIndent(resp, "", " ") + if err != nil { + return err + } + fmt.Fprintln(os.Stdout, string(bytes)) + return nil +} + +// ParseV0Namespace parses a namespace from a base64 or hex string. The param +// is expected to be the user-specified portion of a v0 namespace ID (i.e. the +// last 10 bytes). +func ParseV0Namespace(param string) (share.Namespace, error) { + userBytes, err := DecodeToBytes(param) + if err != nil { + return nil, err + } + + // if the namespace ID is <= 10 bytes, left pad it with 0s + return share.NewBlobNamespaceV0(userBytes) +} + +// DecodeToBytes decodes a Base64 or hex input string into a byte slice. +func DecodeToBytes(param string) ([]byte, error) { + if strings.HasPrefix(param, "0x") { + decoded, err := hex.DecodeString(param[2:]) + if err != nil { + return nil, fmt.Errorf("error decoding namespace ID: %w", err) + } + return decoded, nil + } + // otherwise, it's just a base64 string + decoded, err := base64.StdEncoding.DecodeString(param) + if err != nil { + return nil, fmt.Errorf("error decoding namespace ID: %w", err) + } + return decoded, nil +} + +func PersistentPreRunEnv(cmd *cobra.Command, nodeType node.Type, _ []string) error { + var ( + ctx = cmd.Context() + err error + ) + + ctx = WithNodeType(ctx, nodeType) + + parsedNetwork, err := p2p.ParseNetwork(cmd) + if err != nil { + return err + } + ctx = WithNetwork(ctx, parsedNetwork) + + // loads existing config into the environment + ctx, err = ParseNodeFlags(ctx, cmd, Network(ctx)) + if err != nil { + return err + } + + cfg := NodeConfig(ctx) + + err = p2p.ParseFlags(cmd, &cfg.P2P) + if err != nil { + return err + } + + err = core.ParseFlags(cmd, &cfg.Core) + if err != nil { + return err + } + + if nodeType != node.Bridge { + err = header.ParseFlags(cmd, &cfg.Header) + if err != nil { + return err + } + } + + ctx, err = ParseMiscFlags(ctx, cmd) + if err != nil { + return err + } + + rpc_cfg.ParseFlags(cmd, &cfg.RPC) + gateway.ParseFlags(cmd, &cfg.Gateway) + state.ParseFlags(cmd, &cfg.State) + + // set config + ctx = WithNodeConfig(ctx, &cfg) + cmd.SetContext(ctx) + return nil +} + +// WithFlagSet adds the given flagset to the command. +func WithFlagSet(fset []*flag.FlagSet) func(*cobra.Command) { + return func(c *cobra.Command) { + for _, set := range fset { + c.Flags().AddFlagSet(set) + } + } +} diff --git a/cmd/util_test.go b/cmd/util_test.go new file mode 100644 index 0000000000..b6e245f3e2 --- /dev/null +++ b/cmd/util_test.go @@ -0,0 +1,81 @@ +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/celestiaorg/celestia-node/share" +) + +func Test_parseNamespaceID(t *testing.T) { + type testCase struct { + name string + param string + want share.Namespace + wantErr bool + } + testCases := []testCase{ + { + param: "0x0c204d39600fddd3", + name: "8 byte hex encoded namespace ID gets left padded", + want: share.Namespace{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x20, 0x4d, 0x39, 0x60, 0xf, 0xdd, 0xd3, + }, + wantErr: false, + }, + { + name: "10 byte hex encoded namespace ID", + param: "0x42690c204d39600fddd3", + want: share.Namespace{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x42, 0x69, 0xc, 0x20, 0x4d, 0x39, 0x60, 0xf, 0xdd, 0xd3, + }, + wantErr: false, + }, + { + name: "29 byte hex encoded namespace ID", + param: "0x0000000000000000000000000000000000000001010101010101010101", + want: share.Namespace{ + 0x0, // namespace version + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // v0 ID prefix + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, // namespace ID + }, + wantErr: true, + }, + { + name: "11 byte hex encoded namespace ID returns error", + param: "0x42690c204d39600fddd3a3", + want: share.Namespace{}, + wantErr: true, + }, + { + name: "10 byte base64 encoded namespace ID", + param: "QmkMIE05YA/d0w==", + want: share.Namespace{ + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x42, 0x69, 0xc, 0x20, 0x4d, 0x39, 0x60, 0xf, 0xdd, 0xd3, + }, + wantErr: false, + }, + { + name: "not base64 or hex encoded namespace ID returns error", + param: "5748493939429", + want: share.Namespace{}, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := ParseV0Namespace(tc.param) + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} diff --git a/core/client_test.go b/core/client_test.go index 7467b3c3a3..8ad9060555 100644 --- a/core/client_test.go +++ b/core/client_test.go @@ -10,7 +10,7 @@ import ( ) func TestRemoteClient_Status(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) t.Cleanup(cancel) client := StartTestNode(t).Client @@ -20,7 +20,7 @@ func TestRemoteClient_Status(t *testing.T) { } func TestRemoteClient_StartBlockSubscription_And_GetBlock(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) t.Cleanup(cancel) client := StartTestNode(t).Client diff --git a/core/eds.go b/core/eds.go index e37fa5d960..eb93c249ba 100644 --- a/core/eds.go +++ b/core/eds.go @@ -3,12 +3,17 @@ package core import ( "context" "errors" + "fmt" "github.com/filecoin-project/dagstore" "github.com/tendermint/tendermint/types" - "github.com/celestiaorg/celestia-app/pkg/da" - appshares "github.com/celestiaorg/celestia-app/pkg/shares" + "github.com/celestiaorg/celestia-app/app" + "github.com/celestiaorg/celestia-app/pkg/appconsts" + "github.com/celestiaorg/celestia-app/pkg/shares" + "github.com/celestiaorg/celestia-app/pkg/square" + "github.com/celestiaorg/celestia-app/pkg/wrapper" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/share" @@ -18,16 +23,31 @@ import ( // extendBlock extends the given block data, returning the resulting // ExtendedDataSquare (EDS). If there are no transactions in the block, // nil is returned in place of the eds. -func extendBlock(data types.Data) (*rsmt2d.ExtendedDataSquare, error) { - if len(data.Txs) == 0 && data.SquareSize == uint64(1) { +func extendBlock(data types.Data, appVersion uint64, options ...nmt.Option) (*rsmt2d.ExtendedDataSquare, error) { + if app.IsEmptyBlock(data, appVersion) { return nil, nil } - shares, err := appshares.Split(data, true) + + // Construct the data square from the block's transactions + dataSquare, err := square.Construct(data.Txs.ToSliceOfBytes(), appVersion, appconsts.SquareSizeUpperBound(appVersion)) if err != nil { return nil, err } + return extendShares(shares.ToBytes(dataSquare), options...) +} - return da.ExtendShares(data.SquareSize, appshares.ToBytes(shares)) +func extendShares(s [][]byte, options ...nmt.Option) (*rsmt2d.ExtendedDataSquare, error) { + // Check that the length of the square is a power of 2. + if !shares.IsPowerOfTwo(len(s)) { + return nil, fmt.Errorf("number of shares is not a power of 2: got %d", len(s)) + } + // here we construct a tree + // Note: uses the nmt wrapper to construct the tree. + squareSize := square.Size(len(s)) + return rsmt2d.ComputeExtendedDataSquare(s, + appconsts.DefaultCodec(), + wrapper.NewConstructor(uint64(squareSize), + options...)) } // storeEDS will only store extended block if it is not empty and doesn't already exist. diff --git a/core/eds_test.go b/core/eds_test.go index d67f5005f9..723cb77ad1 100644 --- a/core/eds_test.go +++ b/core/eds_test.go @@ -7,7 +7,8 @@ import ( "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/types" - "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/celestia-app/app" + "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-node/share" ) @@ -18,26 +19,34 @@ import ( func TestTrulyEmptySquare(t *testing.T) { data := types.Data{ Txs: []types.Tx{}, - Blobs: []types.Blob{}, SquareSize: 1, } - eds, err := extendBlock(data) + eds, err := extendBlock(data, appconsts.LatestVersion) require.NoError(t, err) assert.Nil(t, eds) } -// TestNonEmptySquareWithZeroTxs tests that a non-empty square with no -// transactions or blobs computes the correct data root (not the minimum DAH). -func TestNonEmptySquareWithZeroTxs(t *testing.T) { +// TestNonZeroSquareSize tests that the DAH hash of a block with no transactions +// is equal to the DAH hash for an empty root even if SquareSize is set to +// something non-zero. Technically, this block data is invalid because the +// construction of the square is deterministic, and the rules which dictate the +// square size do not allow for empty block data. However, should that ever +// occur, we need to ensure that the correct data root is generated. +func TestEmptySquareWithZeroTxs(t *testing.T) { data := types.Data{ - Txs: []types.Tx{}, - Blobs: []types.Blob{}, - SquareSize: 16, + Txs: []types.Tx{}, } - eds, err := extendBlock(data) + eds, err := extendBlock(data, appconsts.LatestVersion) + require.Nil(t, eds) + require.NoError(t, err) + + // force extend the square using an empty block and compare with the min DAH + eds, err = app.ExtendBlock(data, appconsts.LatestVersion) + require.NoError(t, err) + + dah, err := share.NewRoot(eds) require.NoError(t, err) - dah := da.NewDataAvailabilityHeader(eds) - assert.NotEqual(t, share.EmptyRoot().Hash(), dah.Hash()) + assert.Equal(t, share.EmptyRoot().Hash(), dah.Hash()) } diff --git a/core/exchange.go b/core/exchange.go index 70ca47c56f..cf889a38bb 100644 --- a/core/exchange.go +++ b/core/exchange.go @@ -9,9 +9,11 @@ import ( "golang.org/x/sync/errgroup" libhead "github.com/celestiaorg/go-header" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/ipld" ) const concurrencyLimit = 4 @@ -20,18 +22,38 @@ type Exchange struct { fetcher *BlockFetcher store *eds.Store construct header.ConstructFn + + metrics *exchangeMetrics } func NewExchange( fetcher *BlockFetcher, store *eds.Store, construct header.ConstructFn, -) *Exchange { + opts ...Option, +) (*Exchange, error) { + p := new(params) + for _, opt := range opts { + opt(p) + } + + var ( + metrics *exchangeMetrics + err error + ) + if p.metrics { + metrics, err = newExchangeMetrics() + if err != nil { + return nil, err + } + } + return &Exchange{ fetcher: fetcher, store: store, construct: construct, - } + metrics: metrics, + }, nil } func (ce *Exchange) GetByHeight(ctx context.Context, height uint64) (*header.ExtendedHeader, error) { @@ -40,7 +62,33 @@ func (ce *Exchange) GetByHeight(ctx context.Context, height uint64) (*header.Ext return ce.getExtendedHeaderByHeight(ctx, &intHeight) } -func (ce *Exchange) GetRangeByHeight(ctx context.Context, from, amount uint64) ([]*header.ExtendedHeader, error) { +func (ce *Exchange) GetRangeByHeight( + ctx context.Context, + from *header.ExtendedHeader, + to uint64, +) ([]*header.ExtendedHeader, error) { + start := time.Now() + + amount := to - (from.Height() + 1) + headers, err := ce.getRangeByHeight(ctx, from.Height()+1, amount) + if err != nil { + return nil, err + } + + ce.metrics.requestDurationPerHeader(ctx, time.Since(start), amount) + + for _, h := range headers { + err := libhead.Verify[*header.ExtendedHeader](from, h, libhead.DefaultHeightThreshold) + if err != nil { + return nil, fmt.Errorf("verifying next header against last verified height: %d: %w", + from.Height(), err) + } + from = h + } + return headers, nil +} + +func (ce *Exchange) getRangeByHeight(ctx context.Context, from, amount uint64) ([]*header.ExtendedHeader, error) { if amount == 0 { return nil, nil } @@ -71,27 +119,6 @@ func (ce *Exchange) GetRangeByHeight(ctx context.Context, from, amount uint64) ( return headers, nil } -func (ce *Exchange) GetVerifiedRange( - ctx context.Context, - from *header.ExtendedHeader, - amount uint64, -) ([]*header.ExtendedHeader, error) { - headers, err := ce.GetRangeByHeight(ctx, uint64(from.Height())+1, amount) - if err != nil { - return nil, err - } - - for _, h := range headers { - err := from.Verify(h) - if err != nil { - return nil, fmt.Errorf("verifying next header against last verified height: %d: %w", - from.Height(), err) - } - from = h - } - return headers, nil -} - func (ce *Exchange) Get(ctx context.Context, hash libhead.Hash) (*header.ExtendedHeader, error) { log.Debugw("requesting header", "hash", hash.String()) block, err := ce.fetcher.GetBlockByHash(ctx, hash) @@ -105,20 +132,25 @@ func (ce *Exchange) Get(ctx context.Context, hash libhead.Hash) (*header.Extende } // extend block data - eds, err := extendBlock(block.Data) + adder := ipld.NewProofsAdder(int(block.Data.SquareSize)) + defer adder.Purge() + + eds, err := extendBlock(block.Data, block.Header.Version.App, nmt.NodeVisitor(adder.VisitFn())) if err != nil { return nil, fmt.Errorf("extending block data for height %d: %w", &block.Height, err) } // construct extended header - eh, err := ce.construct(ctx, &block.Header, comm, vals, eds) + eh, err := ce.construct(&block.Header, comm, vals, eds) if err != nil { - return nil, fmt.Errorf("constructing extended header for height %d: %w", &block.Height, err) + panic(fmt.Errorf("constructing extended header for height %d: %w", &block.Height, err)) } // verify hashes match if !bytes.Equal(hash, eh.Hash()) { return nil, fmt.Errorf("incorrect hash in header at height %d: expected %x, got %x", &block.Height, hash, eh.Hash()) } + + ctx = ipld.CtxWithProofsAdder(ctx, adder) err = storeEDS(ctx, eh.DAH.Hash(), eds, ce.store) if err != nil { return nil, fmt.Errorf("storing EDS to eds.Store for height %d: %w", &block.Height, err) @@ -126,7 +158,10 @@ func (ce *Exchange) Get(ctx context.Context, hash libhead.Hash) (*header.Extende return eh, nil } -func (ce *Exchange) Head(ctx context.Context) (*header.ExtendedHeader, error) { +func (ce *Exchange) Head( + ctx context.Context, + _ ...libhead.HeadOption[*header.ExtendedHeader], +) (*header.ExtendedHeader, error) { log.Debug("requesting head") return ce.getExtendedHeaderByHeight(ctx, nil) } @@ -142,15 +177,20 @@ func (ce *Exchange) getExtendedHeaderByHeight(ctx context.Context, height *int64 log.Debugw("fetched signed block from core", "height", b.Header.Height) // extend block data - eds, err := extendBlock(b.Data) + adder := ipld.NewProofsAdder(int(b.Data.SquareSize)) + defer adder.Purge() + + eds, err := extendBlock(b.Data, b.Header.Version.App, nmt.NodeVisitor(adder.VisitFn())) if err != nil { return nil, fmt.Errorf("extending block data for height %d: %w", b.Header.Height, err) } // create extended header - eh, err := ce.construct(ctx, &b.Header, &b.Commit, &b.ValidatorSet, eds) + eh, err := ce.construct(&b.Header, &b.Commit, &b.ValidatorSet, eds) if err != nil { - return nil, fmt.Errorf("constructing extended header for height %d: %w", b.Header.Height, err) + panic(fmt.Errorf("constructing extended header for height %d: %w", b.Header.Height, err)) } + + ctx = ipld.CtxWithProofsAdder(ctx, adder) err = storeEDS(ctx, eh.DAH.Hash(), eds, ce.store) if err != nil { return nil, fmt.Errorf("storing EDS to eds.Store for block height %d: %w", b.Header.Height, err) diff --git a/core/exchange_metrics.go b/core/exchange_metrics.go new file mode 100644 index 0000000000..4e5bf5956c --- /dev/null +++ b/core/exchange_metrics.go @@ -0,0 +1,49 @@ +package core + +import ( + "context" + "time" + + "go.opentelemetry.io/otel/metric" + + "github.com/celestiaorg/celestia-node/libs/utils" +) + +type exchangeMetrics struct { + getByHeightDuration metric.Float64Histogram +} + +func newExchangeMetrics() (*exchangeMetrics, error) { + m := new(exchangeMetrics) + + var err error + m.getByHeightDuration, err = meter.Float64Histogram( + "core_ex_get_by_height_request_time", + metric.WithDescription("core exchange client getByHeight request time in seconds (per single height)"), + ) + if err != nil { + return nil, err + } + + return m, nil +} + +func (m *exchangeMetrics) observe(ctx context.Context, observeFn func(ctx context.Context)) { + if m == nil { + return + } + + ctx = utils.ResetContextOnError(ctx) + + observeFn(ctx) +} + +func (m *exchangeMetrics) requestDurationPerHeader(ctx context.Context, duration time.Duration, amount uint64) { + m.observe(ctx, func(ctx context.Context) { + if amount == 0 { + return + } + durationPerHeader := duration.Seconds() / float64(amount) + m.getByHeightDuration.Record(ctx, durationPerHeader) + }) +} diff --git a/core/exchange_test.go b/core/exchange_test.go index 928d15a9e5..95c7f83385 100644 --- a/core/exchange_test.go +++ b/core/exchange_test.go @@ -10,28 +10,50 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/celestiaorg/celestia-app/testutil/testnode" + "github.com/celestiaorg/celestia-app/test/util/testnode" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share/eds" ) func TestCoreExchange_RequestHeaders(t *testing.T) { - fetcher, _ := createCoreFetcher(t, DefaultTestConfig()) + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + cfg := DefaultTestConfig() + cfg.ChainID = networkID + fetcher, _ := createCoreFetcher(t, cfg) // generate 10 blocks generateBlocks(t, fetcher) store := createStore(t) - ce := NewExchange(fetcher, store, header.MakeExtendedHeader) - headers, err := ce.GetRangeByHeight(context.Background(), 1, 10) + ce, err := NewExchange(fetcher, store, header.MakeExtendedHeader) + require.NoError(t, err) + + // initialize store with genesis block + genHeight := int64(1) + genBlock, err := fetcher.GetBlock(ctx, &genHeight) + require.NoError(t, err) + genHeader, err := ce.Get(ctx, genBlock.Header.Hash().Bytes()) + require.NoError(t, err) + + to := uint64(10) + expectedFirstHeightInRange := genHeader.Height() + 1 + expectedLastHeightInRange := to - 1 + expectedLenHeaders := to - expectedFirstHeightInRange + + // request headers from height 1 to 10 [2:10) + headers, err := ce.GetRangeByHeight(context.Background(), genHeader, to) require.NoError(t, err) - assert.Equal(t, 10, len(headers)) + assert.Len(t, headers, int(expectedLenHeaders)) + assert.Equal(t, expectedFirstHeightInRange, headers[0].Height()) + assert.Equal(t, expectedLastHeightInRange, headers[len(headers)-1].Height()) } -func createCoreFetcher(t *testing.T, cfg *TestConfig) (*BlockFetcher, testnode.Context) { +func createCoreFetcher(t *testing.T, cfg *testnode.Config) (*BlockFetcher, testnode.Context) { cctx := StartTestNodeWithConfig(t, cfg) // wait for height 2 in order to be able to start submitting txs (this prevents // flakiness with accessing account state) @@ -43,7 +65,8 @@ func createCoreFetcher(t *testing.T, cfg *TestConfig) (*BlockFetcher, testnode.C func createStore(t *testing.T) *eds.Store { t.Helper() - store, err := eds.NewStore(t.TempDir(), ds_sync.MutexWrap(ds.NewMapDatastore())) + storeCfg := eds.DefaultParameters() + store, err := eds.NewStore(storeCfg, t.TempDir(), ds_sync.MutexWrap(ds.NewMapDatastore())) require.NoError(t, err) return store } diff --git a/core/fetcher_no_race_test.go b/core/fetcher_no_race_test.go new file mode 100644 index 0000000000..890b7c35c1 --- /dev/null +++ b/core/fetcher_no_race_test.go @@ -0,0 +1,55 @@ +//go:build !race + +package core + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/types" +) + +// TestBlockFetcherHeaderValues tests that both the Commit and ValidatorSet +// endpoints are working as intended. +func TestBlockFetcherHeaderValues(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + t.Cleanup(cancel) + + client := StartTestNode(t).Client + fetcher := NewBlockFetcher(client) + + // generate some blocks + newBlockChan, err := fetcher.SubscribeNewBlockEvent(ctx) + require.NoError(t, err) + // read once from channel to generate next block + var h int64 + select { + case evt := <-newBlockChan: + h = evt.Header.Height + case <-ctx.Done(): + require.NoError(t, ctx.Err()) + } + // get Commit from current height + commit, err := fetcher.Commit(ctx, &h) + require.NoError(t, err) + // get ValidatorSet from current height + valSet, err := fetcher.ValidatorSet(ctx, &h) + require.NoError(t, err) + // get next block + var nextBlock types.EventDataSignedBlock + select { + case nextBlock = <-newBlockChan: + case <-ctx.Done(): + require.NoError(t, ctx.Err()) + } + // compare LastCommit from next block to Commit from first block height + assert.Equal(t, nextBlock.Header.LastCommitHash, commit.Hash()) + assert.Equal(t, nextBlock.Header.Height, commit.Height+1) + // compare ValidatorSet hash to the ValidatorsHash from first block height + hexBytes := valSet.Hash() + assert.Equal(t, nextBlock.ValidatorSet.Hash(), hexBytes) + require.NoError(t, fetcher.UnsubscribeNewBlockEvent(ctx)) +} diff --git a/core/fetcher_test.go b/core/fetcher_test.go index de7d1ba05e..261b84d78c 100644 --- a/core/fetcher_test.go +++ b/core/fetcher_test.go @@ -7,11 +7,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/types" ) func TestBlockFetcher_GetBlock_and_SubscribeNewBlockEvent(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) t.Cleanup(cancel) client := StartTestNode(t).Client @@ -38,45 +37,3 @@ func TestBlockFetcher_GetBlock_and_SubscribeNewBlockEvent(t *testing.T) { } require.NoError(t, fetcher.UnsubscribeNewBlockEvent(ctx)) } - -// TestBlockFetcherHeaderValues tests that both the Commit and ValidatorSet -// endpoints are working as intended. -func TestBlockFetcherHeaderValues(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) - t.Cleanup(cancel) - - client := StartTestNode(t).Client - fetcher := NewBlockFetcher(client) - - // generate some blocks - newBlockChan, err := fetcher.SubscribeNewBlockEvent(ctx) - require.NoError(t, err) - // read once from channel to generate next block - var h int64 - select { - case evt := <-newBlockChan: - h = evt.Header.Height - case <-ctx.Done(): - require.NoError(t, ctx.Err()) - } - // get Commit from current height - commit, err := fetcher.Commit(ctx, &h) - require.NoError(t, err) - // get ValidatorSet from current height - valSet, err := fetcher.ValidatorSet(ctx, &h) - require.NoError(t, err) - // get next block - var nextBlock types.EventDataSignedBlock - select { - case nextBlock = <-newBlockChan: - case <-ctx.Done(): - require.NoError(t, ctx.Err()) - } - // compare LastCommit from next block to Commit from first block height - assert.Equal(t, nextBlock.Header.LastCommitHash, commit.Hash()) - assert.Equal(t, nextBlock.Header.Height, commit.Height+1) - // compare ValidatorSet hash to the ValidatorsHash from first block height - hexBytes := valSet.Hash() - assert.Equal(t, nextBlock.ValidatorSet.Hash(), hexBytes) - require.NoError(t, fetcher.UnsubscribeNewBlockEvent(ctx)) -} diff --git a/core/header_test.go b/core/header_test.go index 57f53d3661..ee5d10170e 100644 --- a/core/header_test.go +++ b/core/header_test.go @@ -10,6 +10,7 @@ import ( "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/header/headertest" + "github.com/celestiaorg/celestia-node/share" ) func TestMakeExtendedHeaderForEmptyBlock(t *testing.T) { @@ -30,22 +31,20 @@ func TestMakeExtendedHeaderForEmptyBlock(t *testing.T) { comm, val, err := fetcher.GetBlockInfo(ctx, &height) require.NoError(t, err) - eds, err := extendBlock(b.Data) + eds, err := extendBlock(b.Data, b.Header.Version.App) require.NoError(t, err) - headerExt, err := header.MakeExtendedHeader(ctx, &b.Header, comm, val, eds) + headerExt, err := header.MakeExtendedHeader(&b.Header, comm, val, eds) require.NoError(t, err) - assert.Equal(t, header.EmptyDAH(), *headerExt.DAH) + assert.Equal(t, share.EmptyRoot(), headerExt.DAH) } func TestMismatchedDataHash_ComputedRoot(t *testing.T) { header := headertest.RandExtendedHeader(t) - header.DataHash = rand.Bytes(32) - panicFn := func() { - header.Validate() //nolint:errcheck - } - assert.Panics(t, panicFn) + err := header.Validate() + assert.Contains(t, err.Error(), "mismatch between data hash commitment from"+ + " core header and computed data root") } diff --git a/core/listener.go b/core/listener.go index 24d83cda12..10255fc4cc 100644 --- a/core/listener.go +++ b/core/listener.go @@ -12,13 +12,20 @@ import ( "go.opentelemetry.io/otel/attribute" libhead "github.com/celestiaorg/go-header" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/ipld" "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" ) -var tracer = otel.Tracer("core/listener") +var ( + tracer = otel.Tracer("core/listener") + retrySubscriptionDelay = 5 * time.Second + + errInvalidSubscription = errors.New("invalid subscription") +) // Listener is responsible for listening to Core for // new block events and converting new Core blocks into @@ -36,9 +43,12 @@ type Listener struct { headerBroadcaster libhead.Broadcaster[*header.ExtendedHeader] hashBroadcaster shrexsub.BroadcastFn - listenerTimeout time.Duration + metrics *listenerMetrics - cancel context.CancelFunc + chainID string + + listenerTimeout time.Duration + cancel context.CancelFunc } func NewListener( @@ -48,15 +58,34 @@ func NewListener( construct header.ConstructFn, store *eds.Store, blocktime time.Duration, -) *Listener { + opts ...Option, +) (*Listener, error) { + p := new(params) + for _, opt := range opts { + opt(p) + } + + var ( + metrics *listenerMetrics + err error + ) + if p.metrics { + metrics, err = newListenerMetrics() + if err != nil { + return nil, err + } + } + return &Listener{ fetcher: fetcher, headerBroadcaster: bcast, hashBroadcaster: hashBroadcaster, construct: construct, store: store, - listenerTimeout: 2 * blocktime, - } + listenerTimeout: 5 * blocktime, + metrics: metrics, + chainID: p.chainID, + }, nil } // Start kicks off the Listener listener loop. @@ -80,7 +109,7 @@ func (cl *Listener) Start(context.Context) error { func (cl *Listener) Stop(context.Context) error { cl.cancel() cl.cancel = nil - return nil + return cl.metrics.Close() } // runSubscriber runs a subscriber to receive event data of new signed blocks. It will attempt to @@ -92,18 +121,38 @@ func (cl *Listener) runSubscriber(ctx context.Context, sub <-chan types.EventDat // listener stopped because external context was canceled return } - log.Warnw("listener: subscriber error, resubscribing...", "err", err) + if errors.Is(err, errInvalidSubscription) { + // stop node if there is a critical issue with the block subscription + log.Fatalf("listener: %v", err) + } - err = cl.fetcher.UnsubscribeNewBlockEvent(ctx) - if err != nil { - log.Errorw("listener: unsubscribe error", "err", err) + log.Warnw("listener: subscriber error, resubscribing...", "err", err) + sub = cl.resubscribe(ctx) + if sub == nil { return } + } +} - sub, err = cl.fetcher.SubscribeNewBlockEvent(ctx) - if err != nil { - log.Errorw("listener: resubscribe error", "err", err) - return +func (cl *Listener) resubscribe(ctx context.Context) <-chan types.EventDataSignedBlock { + err := cl.fetcher.UnsubscribeNewBlockEvent(ctx) + if err != nil { + log.Warnw("listener: unsubscribe", "err", err) + } + + ticker := time.NewTicker(retrySubscriptionDelay) + defer ticker.Stop() + for { + sub, err := cl.fetcher.SubscribeNewBlockEvent(ctx) + if err == nil { + return sub + } + log.Errorw("listener: resubscribe", "err", err) + + select { + case <-ctx.Done(): + return nil + case <-ticker.C: } } } @@ -122,7 +171,14 @@ func (cl *Listener) listen(ctx context.Context, sub <-chan types.EventDataSigned return errors.New("underlying subscription was closed") } + if cl.chainID != "" && b.Header.ChainID != cl.chainID { + log.Errorf("listener: received block with unexpected chain ID: expected %s,"+ + " received %s", cl.chainID, b.Header.ChainID) + return errInvalidSubscription + } + log.Debugw("listener: new block from core", "height", b.Header.Height) + err := cl.handleNewSignedBlock(ctx, b) if err != nil { log.Errorw("listener: handling new block msg", @@ -136,6 +192,7 @@ func (cl *Listener) listen(ctx context.Context, sub <-chan types.EventDataSigned } timeout.Reset(cl.listenerTimeout) case <-timeout.C: + cl.metrics.subscriptionStuck(ctx) return errors.New("underlying subscription is stuck") case <-ctx.Done(): return ctx.Err() @@ -150,17 +207,22 @@ func (cl *Listener) handleNewSignedBlock(ctx context.Context, b types.EventDataS attribute.Int64("height", b.Header.Height), ) // extend block data - eds, err := extendBlock(b.Data) + adder := ipld.NewProofsAdder(int(b.Data.SquareSize)) + defer adder.Purge() + + eds, err := extendBlock(b.Data, b.Header.Version.App, nmt.NodeVisitor(adder.VisitFn())) if err != nil { return fmt.Errorf("extending block data: %w", err) } + // generate extended header - eh, err := cl.construct(ctx, &b.Header, &b.Commit, &b.ValidatorSet, eds) + eh, err := cl.construct(&b.Header, &b.Commit, &b.ValidatorSet, eds) if err != nil { - return fmt.Errorf("making extended header: %w", err) + panic(fmt.Errorf("making extended header: %w", err)) } // attempt to store block data if not empty + ctx = ipld.CtxWithProofsAdder(ctx, adder) err = storeEDS(ctx, b.Header.DataHash.Bytes(), eds, cl.store) if err != nil { return fmt.Errorf("storing EDS: %w", err) @@ -175,7 +237,7 @@ func (cl *Listener) handleNewSignedBlock(ctx context.Context, b types.EventDataS if !syncing { err = cl.hashBroadcaster(ctx, shrexsub.Notification{ DataHash: eh.DataHash.Bytes(), - Height: uint64(eh.Height()), + Height: eh.Height(), }) if err != nil && !errors.Is(err, context.Canceled) { log.Errorw("listener: broadcasting data hash", diff --git a/core/listener_metrics.go b/core/listener_metrics.go new file mode 100644 index 0000000000..f17903a91a --- /dev/null +++ b/core/listener_metrics.go @@ -0,0 +1,81 @@ +package core + +import ( + "context" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" + + "github.com/celestiaorg/celestia-node/libs/utils" +) + +var meter = otel.Meter("core") + +type listenerMetrics struct { + lastTimeSubscriptionStuck time.Time + lastTimeSubscriptionStuckInst metric.Int64ObservableGauge + lastTimeSubscriptionStuckReg metric.Registration + + subscriptionStuckInst metric.Int64Counter +} + +func newListenerMetrics() (*listenerMetrics, error) { + m := new(listenerMetrics) + + var err error + m.subscriptionStuckInst, err = meter.Int64Counter( + "core_listener_subscription_stuck_count", + metric.WithDescription("number of times core listener block subscription has been stuck/retried"), + ) + if err != nil { + return nil, err + } + + m.lastTimeSubscriptionStuckInst, err = meter.Int64ObservableGauge( + "core_listener_last_time_subscription_stuck_timestamp", + metric.WithDescription("last time the listener subscription was stuck"), + ) + if err != nil { + return nil, err + } + m.lastTimeSubscriptionStuckReg, err = meter.RegisterCallback( + m.observeLastTimeStuckCallback, + m.lastTimeSubscriptionStuckInst, + ) + if err != nil { + return nil, err + } + + return m, nil +} + +func (m *listenerMetrics) observe(ctx context.Context, observeFn func(ctx context.Context)) { + if m == nil { + return + } + + ctx = utils.ResetContextOnError(ctx) + + observeFn(ctx) +} + +func (m *listenerMetrics) subscriptionStuck(ctx context.Context) { + m.observe(ctx, func(ctx context.Context) { + m.subscriptionStuckInst.Add(ctx, 1) + m.lastTimeSubscriptionStuck = time.Now() + }) +} + +func (m *listenerMetrics) observeLastTimeStuckCallback(_ context.Context, obs metric.Observer) error { + obs.ObserveInt64(m.lastTimeSubscriptionStuckInst, m.lastTimeSubscriptionStuck.Unix()) + return nil +} + +func (m *listenerMetrics) Close() error { + if m == nil { + return nil + } + + return m.lastTimeSubscriptionStuckReg.Unregister() +} diff --git a/core/listener_no_race_test.go b/core/listener_no_race_test.go new file mode 100644 index 0000000000..eac12785ee --- /dev/null +++ b/core/listener_no_race_test.go @@ -0,0 +1,71 @@ +//go:build !race + +package core + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-node/share" +) + +// TestListenerWithNonEmptyBlocks ensures that non-empty blocks are actually +// stored to eds.Store. +func TestListenerWithNonEmptyBlocks(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + t.Cleanup(cancel) + + // create mocknet with two pubsub endpoints + ps0, _ := createMocknetWithTwoPubsubEndpoints(ctx, t) + + // create one block to store as Head in local store and then unsubscribe from block events + cfg := DefaultTestConfig() + cfg.ChainID = networkID + fetcher, cctx := createCoreFetcher(t, cfg) + eds := createEdsPubSub(ctx, t) + + store := createStore(t) + err := store.Start(ctx) + require.NoError(t, err) + t.Cleanup(func() { + err = store.Stop(ctx) + require.NoError(t, err) + }) + + // create Listener and start listening + cl := createListener(ctx, t, fetcher, ps0, eds, store, networkID) + err = cl.Start(ctx) + require.NoError(t, err) + + // listen for eds hashes broadcasted through eds-sub and ensure store has + // already stored them + sub, err := eds.Subscribe() + require.NoError(t, err) + t.Cleanup(sub.Cancel) + + empty := share.EmptyRoot() + // TODO extract 16 + for i := 0; i < 16; i++ { + _, err := cctx.FillBlock(16, cfg.Accounts, flags.BroadcastBlock) + require.NoError(t, err) + msg, err := sub.Next(ctx) + require.NoError(t, err) + + if bytes.Equal(empty.Hash(), msg.DataHash) { + continue + } + + has, err := store.Has(ctx, msg.DataHash) + require.NoError(t, err) + require.True(t, has) + } + + err = cl.Stop(ctx) + require.NoError(t, err) + require.Nil(t, cl.cancel) +} diff --git a/core/listener_test.go b/core/listener_test.go index 7d4b12310a..b3ed11e571 100644 --- a/core/listener_test.go +++ b/core/listener_test.go @@ -1,12 +1,10 @@ package core import ( - "bytes" "context" "testing" "time" - "github.com/cosmos/cosmos-sdk/client/flags" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/event" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" @@ -30,9 +28,14 @@ func TestListener(t *testing.T) { // create mocknet with two pubsub endpoints ps0, ps1 := createMocknetWithTwoPubsubEndpoints(ctx, t) - subscriber := p2p.NewSubscriber[*header.ExtendedHeader](ps1, header.MsgID, networkID) - err := subscriber.AddValidator(func(context.Context, *header.ExtendedHeader) pubsub.ValidationResult { - return pubsub.ValidationAccept + subscriber, err := p2p.NewSubscriber[*header.ExtendedHeader]( + ps1, + header.MsgID, + p2p.WithSubscriberNetworkID(networkID), + ) + require.NoError(t, err) + err = subscriber.SetVerifier(func(context.Context, *header.ExtendedHeader) error { + return nil }) require.NoError(t, err) require.NoError(t, subscriber.Start(ctx)) @@ -41,10 +44,14 @@ func TestListener(t *testing.T) { t.Cleanup(subs.Cancel) // create one block to store as Head in local store and then unsubscribe from block events - fetcher, _ := createCoreFetcher(t, DefaultTestConfig()) + cfg := DefaultTestConfig() + cfg.ChainID = networkID + fetcher, _ := createCoreFetcher(t, cfg) + eds := createEdsPubSub(ctx, t) + // create Listener and start listening - cl := createListener(ctx, t, fetcher, ps0, eds, createStore(t)) + cl := createListener(ctx, t, fetcher, ps0, eds, createStore(t), networkID) err = cl.Start(ctx) require.NoError(t, err) @@ -63,9 +70,7 @@ func TestListener(t *testing.T) { require.Nil(t, cl.cancel) } -// TestListenerWithNonEmptyBlocks ensures that non-empty blocks are actually -// stored to eds.Store. -func TestListenerWithNonEmptyBlocks(t *testing.T) { +func TestListenerWithWrongChainRPC(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) t.Cleanup(cancel) @@ -74,7 +79,8 @@ func TestListenerWithNonEmptyBlocks(t *testing.T) { // create one block to store as Head in local store and then unsubscribe from block events cfg := DefaultTestConfig() - fetcher, cctx := createCoreFetcher(t, cfg) + cfg.ChainID = networkID + fetcher, _ := createCoreFetcher(t, cfg) eds := createEdsPubSub(ctx, t) store := createStore(t) @@ -86,36 +92,12 @@ func TestListenerWithNonEmptyBlocks(t *testing.T) { }) // create Listener and start listening - cl := createListener(ctx, t, fetcher, ps0, eds, store) - err = cl.Start(ctx) - require.NoError(t, err) - - // listen for eds hashes broadcasted through eds-sub and ensure store has - // already stored them - sub, err := eds.Subscribe() + cl := createListener(ctx, t, fetcher, ps0, eds, store, "wrong-chain-rpc") + sub, err := cl.fetcher.SubscribeNewBlockEvent(ctx) require.NoError(t, err) - t.Cleanup(sub.Cancel) - - empty := header.EmptyDAH() - // TODO extract 16 - for i := 0; i < 16; i++ { - _, err := cctx.FillBlock(16, cfg.Accounts, flags.BroadcastBlock) - require.NoError(t, err) - msg, err := sub.Next(ctx) - require.NoError(t, err) - - if bytes.Equal(empty.Hash(), msg.DataHash) { - continue - } - has, err := store.Has(ctx, msg.DataHash) - require.NoError(t, err) - require.True(t, has) - } - - err = cl.Stop(ctx) - require.NoError(t, err) - require.Nil(t, cl.cancel) + err = cl.listen(ctx, sub) + assert.ErrorIs(t, err, errInvalidSubscription) } func createMocknetWithTwoPubsubEndpoints(ctx context.Context, t *testing.T) (*pubsub.PubSub, *pubsub.PubSub) { @@ -160,15 +142,25 @@ func createListener( ps *pubsub.PubSub, edsSub *shrexsub.PubSub, store *eds.Store, + chainID string, ) *Listener { - p2pSub := p2p.NewSubscriber[*header.ExtendedHeader](ps, header.MsgID, networkID) - err := p2pSub.Start(ctx) + p2pSub, err := p2p.NewSubscriber[*header.ExtendedHeader](ps, header.MsgID, p2p.WithSubscriberNetworkID(networkID)) + require.NoError(t, err) + + err = p2pSub.Start(ctx) + require.NoError(t, err) + err = p2pSub.SetVerifier(func(ctx context.Context, msg *header.ExtendedHeader) error { + return nil + }) require.NoError(t, err) t.Cleanup(func() { require.NoError(t, p2pSub.Stop(ctx)) }) - return NewListener(p2pSub, fetcher, edsSub.Broadcast, header.MakeExtendedHeader, store, nodep2p.BlockTime) + listener, err := NewListener(p2pSub, fetcher, edsSub.Broadcast, header.MakeExtendedHeader, + store, nodep2p.BlockTime, WithChainID(nodep2p.Network(chainID))) + require.NoError(t, err) + return listener } func createEdsPubSub(ctx context.Context, t *testing.T) *shrexsub.PubSub { diff --git a/core/option.go b/core/option.go new file mode 100644 index 0000000000..6916ced4d8 --- /dev/null +++ b/core/option.go @@ -0,0 +1,25 @@ +package core + +import "github.com/celestiaorg/celestia-node/nodebuilder/p2p" + +type Option func(*params) + +type params struct { + metrics bool + + chainID string +} + +// WithMetrics is a functional option that enables metrics +// inside the core package. +func WithMetrics() Option { + return func(p *params) { + p.metrics = true + } +} + +func WithChainID(id p2p.Network) Option { + return func(p *params) { + p.chainID = id.String() + } +} diff --git a/core/testing.go b/core/testing.go index 88154dc7d9..8d29ce9bbc 100644 --- a/core/testing.go +++ b/core/testing.go @@ -1,30 +1,18 @@ package core import ( - "fmt" "net" "net/url" "testing" + "time" - appconfig "github.com/cosmos/cosmos-sdk/server/config" "github.com/stretchr/testify/require" tmconfig "github.com/tendermint/tendermint/config" tmrand "github.com/tendermint/tendermint/libs/rand" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - "github.com/celestiaorg/celestia-app/testutil/testnode" + "github.com/celestiaorg/celestia-app/test/util/testnode" ) -// TestConfig encompasses all the configs required to run test Tendermint + Celestia App tandem. -type TestConfig struct { - ConsensusParams *tmproto.ConsensusParams - Tendermint *tmconfig.Config - App *appconfig.Config - - Accounts []string - SuppressLogs bool -} - // DefaultTestConfig returns the default testing configuration for Tendermint + Celestia App tandem. // // It fetches free ports from OS and sets them into configs, s.t. @@ -32,17 +20,8 @@ type TestConfig struct { // multiple tests nodes in parallel. // // Additionally, it instructs Tendermint + Celestia App tandem to setup 10 funded accounts. -func DefaultTestConfig() *TestConfig { - conCfg := testnode.DefaultParams() - - tnCfg := testnode.DefaultTendermintConfig() - tnCfg.RPC.ListenAddress = fmt.Sprintf("tcp://127.0.0.1:%d", getFreePort()) - tnCfg.RPC.GRPCListenAddress = fmt.Sprintf("tcp://127.0.0.1:%d", getFreePort()) - tnCfg.P2P.ListenAddress = fmt.Sprintf("tcp://127.0.0.1:%d", getFreePort()) - - appCfg := testnode.DefaultAppConfig() - appCfg.GRPC.Address = fmt.Sprintf("127.0.0.1:%d", getFreePort()) - appCfg.API.Address = fmt.Sprintf("tcp://127.0.0.1:%d", getFreePort()) +func DefaultTestConfig() *testnode.Config { + cfg := testnode.DefaultConfig() // instructs creating funded accounts // 10 usually is enough for testing @@ -51,13 +30,13 @@ func DefaultTestConfig() *TestConfig { accounts[i] = tmrand.Str(9) } - return &TestConfig{ - ConsensusParams: conCfg, - Tendermint: tnCfg, - App: appCfg, - Accounts: accounts, - SuppressLogs: true, - } + cfg.TmConfig.Consensus.TimeoutCommit = time.Millisecond * 200 + + cfg = cfg. + WithAccounts(accounts). + WithSupressLogs(true) + + return cfg } // StartTestNode simply starts Tendermint and Celestia App tandem with default testing @@ -67,41 +46,13 @@ func StartTestNode(t *testing.T) testnode.Context { } // StartTestNodeWithConfig starts Tendermint and Celestia App tandem with custom configuration. -func StartTestNodeWithConfig(t *testing.T, cfg *TestConfig) testnode.Context { - state, kr, err := testnode.DefaultGenesisState(cfg.Accounts...) - require.NoError(t, err) - - tmNode, app, cctx, err := testnode.New( - t, - cfg.ConsensusParams, - cfg.Tendermint, - cfg.SuppressLogs, - state, - kr, - "private", - nil, - ) - require.NoError(t, err) - - cctx, cleanupCoreNode, err := testnode.StartNode(tmNode, cctx) - require.NoError(t, err) - t.Cleanup(func() { - err := cleanupCoreNode() - require.NoError(t, err) - }) - - cctx, cleanupGRPCServer, err := StartGRPCServer(app, cfg.App, cctx) - require.NoError(t, err) - t.Cleanup(func() { - err := cleanupGRPCServer() - require.NoError(t, err) - }) - +func StartTestNodeWithConfig(t *testing.T, cfg *testnode.Config) testnode.Context { + cctx, _, _ := testnode.NewNetwork(t, cfg) // we want to test over remote http client, // so we are as close to the real environment as possible // however, it might be useful to use local tendermint client // if you need to debug something inside of it - ip, port, err := getEndpoint(cfg.Tendermint) + ip, port, err := getEndpoint(cfg.TmConfig) require.NoError(t, err) client, err := NewRemote(ip, port) require.NoError(t, err) @@ -117,18 +68,6 @@ func StartTestNodeWithConfig(t *testing.T, cfg *TestConfig) testnode.Context { return cctx } -func getFreePort() int { - a, err := net.ResolveTCPAddr("tcp", "localhost:0") - if err == nil { - var l *net.TCPListener - if l, err = net.ListenTCP("tcp", a); err == nil { - defer l.Close() - return l.Addr().(*net.TCPAddr).Port - } - } - panic("while getting free port: " + err.Error()) -} - func getEndpoint(cfg *tmconfig.Config) (string, string, error) { url, err := url.Parse(cfg.RPC.ListenAddress) if err != nil { diff --git a/core/testing_grpc.go b/core/testing_grpc.go index 2306b61723..d831bc0724 100644 --- a/core/testing_grpc.go +++ b/core/testing_grpc.go @@ -14,7 +14,7 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" - "github.com/celestiaorg/celestia-app/testutil/testnode" + "github.com/celestiaorg/celestia-app/test/util/testnode" ) /* diff --git a/das/checkpoint.go b/das/checkpoint.go index a38eca828c..bb023a19da 100644 --- a/das/checkpoint.go +++ b/das/checkpoint.go @@ -23,15 +23,15 @@ type workerCheckpoint struct { func newCheckpoint(stats SamplingStats) checkpoint { workers := make([]workerCheckpoint, 0, len(stats.Workers)) for _, w := range stats.Workers { - // no need to store retry jobs, since they will resume from failed heights map - if w.JobType == retryJob { - continue + // no need to resume recent jobs after restart. On the other hand, retry jobs will resume from + // failed heights map. it leaves only catchup jobs to be stored and resumed + if w.JobType == catchupJob { + workers = append(workers, workerCheckpoint{ + From: w.Curr, + To: w.To, + JobType: w.JobType, + }) } - workers = append(workers, workerCheckpoint{ - From: w.Curr, - To: w.To, - JobType: w.JobType, - }) } return checkpoint{ SampleFrom: stats.CatchupHead + 1, diff --git a/das/coordinator.go b/das/coordinator.go index 2184dce2c8..852a40d24d 100644 --- a/das/coordinator.go +++ b/das/coordinator.go @@ -81,7 +81,9 @@ func (sc *samplingCoordinator) run(ctx context.Context, cp checkpoint) { select { case head := <-sc.updHeadCh: if sc.state.isNewHead(head.Height()) { - sc.runWorker(ctx, sc.state.recentJob(head)) + if !sc.recentJobsLimitReached() { + sc.runWorker(ctx, sc.state.recentJob(head)) + } sc.state.updateHead(head.Height()) // run worker without concurrency limit restrictions to reduced delay sc.metrics.observeNewHead(ctx) @@ -146,3 +148,8 @@ func (sc *samplingCoordinator) getCheckpoint(ctx context.Context) (checkpoint, e func (sc *samplingCoordinator) concurrencyLimitReached() bool { return len(sc.state.inProgress) >= sc.concurrencyLimit } + +// recentJobsLimitReached indicates whether concurrency limit for recent jobs has been reached +func (sc *samplingCoordinator) recentJobsLimitReached() bool { + return len(sc.state.inProgress) >= 2*sc.concurrencyLimit +} diff --git a/das/coordinator_test.go b/das/coordinator_test.go index e7e2d5ceb2..1ae54c470f 100644 --- a/das/coordinator_test.go +++ b/das/coordinator_test.go @@ -13,6 +13,7 @@ import ( "github.com/tendermint/tendermint/types" "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" ) @@ -366,7 +367,7 @@ func (m *mockSampler) sample(ctx context.Context, h *header.ExtendedHeader) erro m.lock.Lock() defer m.lock.Unlock() - height := uint64(h.Height()) + height := h.Height() m.done[height]++ if len(m.done) > int(m.NetworkHead-m.SampleFrom) && !m.isFinished { @@ -431,7 +432,7 @@ func (m *mockSampler) discover(ctx context.Context, newHeight uint64, emit liste emit(ctx, &header.ExtendedHeader{ Commit: &types.Commit{}, RawHeader: header.RawHeader{Height: int64(newHeight)}, - DAH: &header.DataAvailabilityHeader{RowsRoots: make([][]byte, 0)}, + DAH: &share.Root{RowRoots: make([][]byte, 0)}, }) } @@ -503,7 +504,7 @@ func (o *checkOrder) middleWare(out sampleFn) sampleFn { if len(o.queue) > 0 { // check last item in queue to be same as input - if o.queue[0] != uint64(h.Height()) { + if o.queue[0] != h.Height() { defer o.lock.Unlock() return fmt.Errorf("expected height: %v,got: %v", o.queue[0], h.Height()) } @@ -573,7 +574,7 @@ func (l *lock) releaseAll(except ...uint64) { func (l *lock) middleWare(out sampleFn) sampleFn { return func(ctx context.Context, h *header.ExtendedHeader) error { l.m.Lock() - ch, blocked := l.blockList[uint64(h.Height())] + ch, blocked := l.blockList[h.Height()] l.m.Unlock() if !blocked { return out(ctx, h) @@ -589,7 +590,7 @@ func (l *lock) middleWare(out sampleFn) sampleFn { } func onceMiddleWare(out sampleFn) sampleFn { - db := make(map[int64]int) + db := make(map[uint64]int) m := sync.Mutex{} return func(ctx context.Context, h *header.ExtendedHeader) error { m.Lock() diff --git a/das/daser.go b/das/daser.go index 8b7e2a145c..7d569f7e0b 100644 --- a/das/daser.go +++ b/das/daser.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "sync/atomic" + "time" "github.com/ipfs/go-datastore" logging "github.com/ipfs/go-log/v2" @@ -25,7 +26,7 @@ type DASer struct { params Parameters da share.Availability - bcast fraud.Broadcaster + bcast fraud.Broadcaster[*header.ExtendedHeader] hsub libhead.Subscriber[*header.ExtendedHeader] // listens for new headers in the network getter libhead.Getter[*header.ExtendedHeader] // retrieves past headers @@ -47,7 +48,7 @@ func NewDASer( hsub libhead.Subscriber[*header.ExtendedHeader], getter libhead.Getter[*header.ExtendedHeader], dstore datastore.Datastore, - bcast fraud.Broadcaster, + bcast fraud.Broadcaster[*header.ExtendedHeader], shrexBroadcast shrexsub.BroadcastFn, options ...Option, ) (*DASer, error) { @@ -99,7 +100,7 @@ func (d *DASer) Start(ctx context.Context) error { // attempt to get head info. No need to handle error, later DASer // will be able to find new head from subscriber after it is started if h, err := d.getter.Head(ctx); err == nil { - cp.NetworkHead = uint64(h.Height()) + cp.NetworkHead = h.Height() } } log.Info("starting DASer from checkpoint: ", cp.String()) @@ -137,7 +138,7 @@ func (d *DASer) Stop(ctx context.Context) error { // save updated checkpoint after sampler and all workers are shut down if err = d.store.store(ctx, newCheckpoint(d.sampler.state.unsafeStats())); err != nil { - log.Errorw("storing checkpoint to disk", "Err", err) + log.Errorw("storing checkpoint to disk", "err", err) } if err = d.store.wait(ctx); err != nil { @@ -147,12 +148,20 @@ func (d *DASer) Stop(ctx context.Context) error { } func (d *DASer) sample(ctx context.Context, h *header.ExtendedHeader) error { - err := d.da.SharesAvailable(ctx, h.DAH) + // short-circuit if pruning is enabled and the header is outside the + // availability window + if !d.isWithinSamplingWindow(h) { + log.Debugw("skipping header outside sampling window", "height", h.Height(), + "time", h.Time()) + return nil + } + + err := d.da.SharesAvailable(ctx, h) if err != nil { var byzantineErr *byzantine.ErrByzantine if errors.As(err, &byzantineErr) { log.Warn("Propagating proof...") - sendErr := d.bcast.Broadcast(ctx, byzantine.CreateBadEncodingProof(h.Hash(), uint64(h.Height()), byzantineErr)) + sendErr := d.bcast.Broadcast(ctx, byzantine.CreateBadEncodingProof(h.Hash(), h.Height(), byzantineErr)) if sendErr != nil { log.Errorw("fraud proof propagating failed", "err", sendErr) } @@ -162,6 +171,14 @@ func (d *DASer) sample(ctx context.Context, h *header.ExtendedHeader) error { return nil } +func (d *DASer) isWithinSamplingWindow(eh *header.ExtendedHeader) bool { + // if sampling window is not set, then all headers are within the window + if d.params.SamplingWindow == 0 { + return true + } + return time.Since(eh.Time()) <= d.params.SamplingWindow +} + // SamplingStats returns the current statistics over the DA sampling process. func (d *DASer) SamplingStats(ctx context.Context) (SamplingStats, error) { return d.sampler.stats(ctx) diff --git a/das/daser_test.go b/das/daser_test.go index 2f1c494309..9eec6392cc 100644 --- a/das/daser_test.go +++ b/das/daser_test.go @@ -2,14 +2,14 @@ package das import ( "context" + "strconv" "testing" "time" "github.com/golang/mock/gomock" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "github.com/ipfs/go-datastore" ds_sync "github.com/ipfs/go-datastore/sync" - mdutils "github.com/ipfs/go-merkledag/test" pubsub "github.com/libp2p/go-libp2p-pubsub" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/stretchr/testify/assert" @@ -23,6 +23,7 @@ import ( "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/header/headertest" + headerfraud "github.com/celestiaorg/celestia-node/header/headertest/fraud" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/availability/full" "github.com/celestiaorg/celestia-node/share/availability/light" @@ -30,6 +31,7 @@ import ( availability_test "github.com/celestiaorg/celestia-node/share/availability/test" "github.com/celestiaorg/celestia-node/share/eds/byzantine" "github.com/celestiaorg/celestia-node/share/getters" + "github.com/celestiaorg/celestia-node/share/ipld" ) var timeout = time.Second * 15 @@ -38,7 +40,7 @@ var timeout = time.Second * 15 // the DASer checkpoint is updated to network head. func TestDASerLifecycle(t *testing.T) { ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() avail := light.TestAvailability(getters.NewIPLDGetter(bServ)) // 15 headers from the past and 15 future headers mockGet, sub, mockService := createDASerSubcomponents(t, bServ, 15, 15) @@ -78,7 +80,7 @@ func TestDASerLifecycle(t *testing.T) { func TestDASer_Restart(t *testing.T) { ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() avail := light.TestAvailability(getters.NewIPLDGetter(bServ)) // 15 headers from the past and 15 future headers mockGet, sub, mockService := createDASerSubcomponents(t, bServ, 15, 15) @@ -146,7 +148,7 @@ func TestDASer_stopsAfter_BEFP(t *testing.T) { t.Cleanup(cancel) ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() // create mock network net, err := mocknet.FullMeshLinked(1) require.NoError(t, err) @@ -154,26 +156,42 @@ func TestDASer_stopsAfter_BEFP(t *testing.T) { ps, err := pubsub.NewGossipSub(ctx, net.Hosts()[0], pubsub.WithMessageSignaturePolicy(pubsub.StrictNoSign)) require.NoError(t, err) - avail := full.TestAvailability(getters.NewIPLDGetter(bServ)) + avail := full.TestAvailability(t, getters.NewIPLDGetter(bServ)) // 15 headers from the past and 15 future headers mockGet, sub, _ := createDASerSubcomponents(t, bServ, 15, 15) // create fraud service and break one header - getter := func(ctx context.Context, height uint64) (libhead.Header, error) { + getter := func(ctx context.Context, height uint64) (*header.ExtendedHeader, error) { return mockGet.GetByHeight(ctx, height) } - f := fraudserv.NewProofService(ps, net.Hosts()[0], getter, ds, false, "private") - require.NoError(t, f.Start(ctx)) - mockGet.headers[1], _ = headertest.CreateFraudExtHeader(t, mockGet.headers[1], bServ) + unmarshaler := fraud.MultiUnmarshaler[*header.ExtendedHeader]{ + Unmarshalers: map[fraud.ProofType]func([]byte) (fraud.Proof[*header.ExtendedHeader], error){ + byzantine.BadEncoding: func(data []byte) (fraud.Proof[*header.ExtendedHeader], error) { + befp := &byzantine.BadEncodingProof{} + return befp, befp.UnmarshalBinary(data) + }, + }, + } + + fserv := fraudserv.NewProofService[*header.ExtendedHeader](ps, + net.Hosts()[0], + getter, + unmarshaler, + ds, + false, + "private", + ) + require.NoError(t, fserv.Start(ctx)) + mockGet.headers[1] = headerfraud.CreateFraudExtHeader(t, mockGet.headers[1], bServ) newCtx := context.Background() // create and start DASer - daser, err := NewDASer(avail, sub, mockGet, ds, f, newBroadcastMock(1)) + daser, err := NewDASer(avail, sub, mockGet, ds, fserv, newBroadcastMock(1)) require.NoError(t, err) resultCh := make(chan error) - go fraud.OnProof(newCtx, f, byzantine.BadEncoding, - func(fraud.Proof) { + go fraud.OnProof[*header.ExtendedHeader](newCtx, fserv, byzantine.BadEncoding, + func(fraud.Proof[*header.ExtendedHeader]) { resultCh <- daser.Stop(newCtx) }) @@ -197,7 +215,7 @@ func TestDASerSampleTimeout(t *testing.T) { avail := mocks.NewMockAvailability(gomock.NewController(t)) doneCh := make(chan struct{}) avail.EXPECT().SharesAvailable(gomock.Any(), gomock.Any()).DoAndReturn( - func(sampleCtx context.Context, h *share.Root) error { + func(sampleCtx context.Context, h *header.ExtendedHeader) error { select { case <-sampleCtx.Done(): close(doneCh) @@ -210,10 +228,11 @@ func TestDASerSampleTimeout(t *testing.T) { ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) sub := new(headertest.Subscriber) - f := new(fraudtest.DummyService) + fserv := &fraudtest.DummyService[*header.ExtendedHeader]{} // create and start DASer - daser, err := NewDASer(avail, sub, getter, ds, f, newBroadcastMock(1), WithSampleTimeout(1)) + daser, err := NewDASer(avail, sub, getter, ds, fserv, newBroadcastMock(1), + WithSampleTimeout(1)) require.NoError(t, err) require.NoError(t, daser.Start(ctx)) @@ -226,6 +245,42 @@ func TestDASerSampleTimeout(t *testing.T) { } } +// TestDASer_SamplingWindow tests the sampling window determination +// for headers. +func TestDASer_SamplingWindow(t *testing.T) { + ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) + sub := new(headertest.Subscriber) + fserv := &fraudtest.DummyService[*header.ExtendedHeader]{} + getter := getterStub{} + avail := mocks.NewMockAvailability(gomock.NewController(t)) + + // create and start DASer + daser, err := NewDASer(avail, sub, getter, ds, fserv, newBroadcastMock(1), + WithSamplingWindow(time.Second)) + require.NoError(t, err) + + var tests = []struct { + timestamp time.Time + withinWindow bool + }{ + {timestamp: time.Now().Add(-(time.Second * 5)), withinWindow: false}, + {timestamp: time.Now().Add(-(time.Millisecond * 800)), withinWindow: true}, + {timestamp: time.Now().Add(-(time.Hour)), withinWindow: false}, + {timestamp: time.Now().Add(-(time.Hour * 24 * 30)), withinWindow: false}, + {timestamp: time.Now(), withinWindow: true}, + } + + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + eh := headertest.RandExtendedHeader(t) + eh.RawHeader.Time = tt.timestamp + + assert.Equal(t, tt.withinWindow, daser.isWithinSamplingWindow(eh)) + }) + } + +} + // createDASerSubcomponents takes numGetter (number of headers // to store in mockGetter) and numSub (number of headers to store // in the mock header.Subscriber), returning a newly instantiated @@ -235,9 +290,9 @@ func createDASerSubcomponents( bServ blockservice.BlockService, numGetter, numSub int, -) (*mockGetter, *headertest.Subscriber, *fraudtest.DummyService) { +) (*mockGetter, *headertest.Subscriber, *fraudtest.DummyService[*header.ExtendedHeader]) { mockGet, sub := createMockGetterAndSub(t, bServ, numGetter, numSub) - fraud := new(fraudtest.DummyService) + fraud := &fraudtest.DummyService[*header.ExtendedHeader]{} return mockGet, sub, fraud } @@ -274,9 +329,7 @@ func (m *mockGetter) fillSubWithHeaders( for i := startHeight; i < endHeight; i++ { dah := availability_test.RandFillBS(t, 16, bServ) - randHeader := headertest.RandExtendedHeader(t) - randHeader.DataHash = dah.Hash() - randHeader.DAH = dah + randHeader := headertest.RandExtendedHeaderWithRoot(t, dah) randHeader.RawHeader.Height = int64(i + 1) sub.Headers[index] = randHeader @@ -302,9 +355,7 @@ func (m *mockGetter) generateHeaders(t *testing.T, bServ blockservice.BlockServi for i := startHeight; i < endHeight; i++ { dah := availability_test.RandFillBS(t, 16, bServ) - randHeader := headertest.RandExtendedHeader(t) - randHeader.DataHash = dah.Hash() - randHeader.DAH = dah + randHeader := headertest.RandExtendedHeaderWithRoot(t, dah) randHeader.RawHeader.Height = int64(i + 1) m.headers[int64(i+1)] = randHeader @@ -313,7 +364,10 @@ func (m *mockGetter) generateHeaders(t *testing.T, bServ blockservice.BlockServi m.head = int64(startHeight + endHeight) } -func (m *mockGetter) Head(context.Context) (*header.ExtendedHeader, error) { +func (m *mockGetter) Head( + context.Context, + ...libhead.HeadOption[*header.ExtendedHeader], +) (*header.ExtendedHeader, error) { return m.headers[m.head], nil } @@ -345,7 +399,7 @@ type benchGetterStub struct { func newBenchGetter() benchGetterStub { return benchGetterStub{header: &header.ExtendedHeader{ - DAH: &header.DataAvailabilityHeader{RowsRoots: make([][]byte, 0)}}} + DAH: &share.Root{RowRoots: make([][]byte, 0)}}} } func (m benchGetterStub) GetByHeight(context.Context, uint64) (*header.ExtendedHeader, error) { @@ -354,7 +408,10 @@ func (m benchGetterStub) GetByHeight(context.Context, uint64) (*header.ExtendedH type getterStub struct{} -func (m getterStub) Head(context.Context) (*header.ExtendedHeader, error) { +func (m getterStub) Head( + context.Context, + ...libhead.HeadOption[*header.ExtendedHeader], +) (*header.ExtendedHeader, error) { return &header.ExtendedHeader{RawHeader: header.RawHeader{Height: 1}}, nil } @@ -362,14 +419,10 @@ func (m getterStub) GetByHeight(_ context.Context, height uint64) (*header.Exten return &header.ExtendedHeader{ Commit: &types.Commit{}, RawHeader: header.RawHeader{Height: int64(height)}, - DAH: &header.DataAvailabilityHeader{RowsRoots: make([][]byte, 0)}}, nil -} - -func (m getterStub) GetRangeByHeight(context.Context, uint64, uint64) ([]*header.ExtendedHeader, error) { - return nil, nil + DAH: &share.Root{RowRoots: make([][]byte, 0)}}, nil } -func (m getterStub) GetVerifiedRange( +func (m getterStub) GetRangeByHeight( context.Context, *header.ExtendedHeader, uint64, diff --git a/das/metrics.go b/das/metrics.go index fd72fe5e4e..6454e9d138 100644 --- a/das/metrics.go +++ b/das/metrics.go @@ -6,13 +6,12 @@ import ( "sync/atomic" "time" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" - "go.opentelemetry.io/otel/metric/instrument/syncfloat64" - "go.opentelemetry.io/otel/metric/instrument/syncint64" + "go.opentelemetry.io/otel/metric" "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/libs/utils" ) const ( @@ -21,74 +20,69 @@ const ( failedLabel = "failed" ) -var ( - meter = global.MeterProvider().Meter("das") -) +var meter = otel.Meter("das") type metrics struct { - sampled syncint64.Counter - sampleTime syncfloat64.Histogram - getHeaderTime syncfloat64.Histogram - newHead syncint64.Counter + sampled metric.Int64Counter + sampleTime metric.Float64Histogram + getHeaderTime metric.Float64Histogram + newHead metric.Int64Counter lastSampledTS uint64 } func (d *DASer) InitMetrics() error { - sampled, err := meter.SyncInt64().Counter("das_sampled_headers_counter", - instrument.WithDescription("sampled headers counter")) + sampled, err := meter.Int64Counter("das_sampled_headers_counter", + metric.WithDescription("sampled headers counter")) if err != nil { return err } - sampleTime, err := meter.SyncFloat64().Histogram("das_sample_time_hist", - instrument.WithDescription("duration of sampling a single header")) + sampleTime, err := meter.Float64Histogram("das_sample_time_hist", + metric.WithDescription("duration of sampling a single header")) if err != nil { return err } - getHeaderTime, err := meter.SyncFloat64().Histogram("das_get_header_time_hist", - instrument.WithDescription("duration of getting header from header store")) + getHeaderTime, err := meter.Float64Histogram("das_get_header_time_hist", + metric.WithDescription("duration of getting header from header store")) if err != nil { return err } - newHead, err := meter.SyncInt64().Counter("das_head_updated_counter", - instrument.WithDescription("amount of times DAS'er advanced network head")) + newHead, err := meter.Int64Counter("das_head_updated_counter", + metric.WithDescription("amount of times DAS'er advanced network head")) if err != nil { return err } - lastSampledTS, err := meter.AsyncInt64().Gauge("das_latest_sampled_ts", - instrument.WithDescription("latest sampled timestamp")) + lastSampledTS, err := meter.Int64ObservableGauge("das_latest_sampled_ts", + metric.WithDescription("latest sampled timestamp")) if err != nil { return err } - busyWorkers, err := meter.AsyncInt64().Gauge("das_busy_workers_amount", - instrument.WithDescription("number of active parallel workers in DAS'er")) + busyWorkers, err := meter.Int64ObservableGauge("das_busy_workers_amount", + metric.WithDescription("number of active parallel workers in DAS'er")) if err != nil { return err } - networkHead, err := meter.AsyncInt64().Gauge("das_network_head", - instrument.WithDescription("most recent network head")) + networkHead, err := meter.Int64ObservableGauge("das_network_head", + metric.WithDescription("most recent network head")) if err != nil { return err } - sampledChainHead, err := meter.AsyncInt64().Gauge("das_sampled_chain_head", - instrument.WithDescription("height of the sampled chain - all previous headers have been successfully sampled")) + sampledChainHead, err := meter.Int64ObservableGauge("das_sampled_chain_head", + metric.WithDescription("height of the sampled chain - all previous headers have been successfully sampled")) if err != nil { return err } - totalSampled, err := meter. - AsyncInt64(). - Gauge( - "das_total_sampled_headers", - instrument.WithDescription("total sampled headers gauge"), - ) + totalSampled, err := meter.Int64ObservableGauge("das_total_sampled_headers", + metric.WithDescription("total sampled headers gauge"), + ) if err != nil { return err } @@ -100,36 +94,38 @@ func (d *DASer) InitMetrics() error { newHead: newHead, } - err = meter.RegisterCallback( - []instrument.Asynchronous{ - lastSampledTS, - busyWorkers, - networkHead, - sampledChainHead, - totalSampled, - }, - func(ctx context.Context) { - stats, err := d.sampler.stats(ctx) - if err != nil { - log.Errorf("observing stats: %s", err.Error()) - } - - for jobType, amount := range stats.workersByJobType() { - busyWorkers.Observe(ctx, amount, - attribute.String(jobTypeLabel, string(jobType))) - } - - networkHead.Observe(ctx, int64(stats.NetworkHead)) - sampledChainHead.Observe(ctx, int64(stats.SampledChainHead)) - - if ts := atomic.LoadUint64(&d.sampler.metrics.lastSampledTS); ts != 0 { - lastSampledTS.Observe(ctx, int64(ts)) - } - - totalSampled.Observe(ctx, int64(stats.totalSampled())) - }, - ) + callback := func(ctx context.Context, observer metric.Observer) error { + stats, err := d.sampler.stats(ctx) + if err != nil { + log.Errorf("observing stats: %s", err.Error()) + return err + } + + for jobType, amount := range stats.workersByJobType() { + observer.ObserveInt64(busyWorkers, amount, + metric.WithAttributes( + attribute.String(jobTypeLabel, string(jobType)), + )) + } + + observer.ObserveInt64(networkHead, int64(stats.NetworkHead)) + observer.ObserveInt64(sampledChainHead, int64(stats.SampledChainHead)) + + if ts := atomic.LoadUint64(&d.sampler.metrics.lastSampledTS); ts != 0 { + observer.ObserveInt64(lastSampledTS, int64(ts)) + } + + observer.ObserveInt64(totalSampled, int64(stats.totalSampled())) + return nil + } + _, err = meter.RegisterCallback(callback, + lastSampledTS, + busyWorkers, + networkHead, + sampledChainHead, + totalSampled, + ) if err != nil { return fmt.Errorf("registering metrics callback: %w", err) } @@ -149,20 +145,22 @@ func (m *metrics) observeSample( if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } + + ctx = utils.ResetContextOnError(ctx) + m.sampleTime.Record(ctx, sampleTime.Seconds(), - attribute.Bool(failedLabel, err != nil), - attribute.Int(headerWidthLabel, len(h.DAH.RowsRoots)), - attribute.String(jobTypeLabel, string(jobType)), - ) + metric.WithAttributes( + attribute.Bool(failedLabel, err != nil), + attribute.Int(headerWidthLabel, len(h.DAH.RowRoots)), + attribute.String(jobTypeLabel, string(jobType)), + )) m.sampled.Add(ctx, 1, - attribute.Bool(failedLabel, err != nil), - attribute.Int(headerWidthLabel, len(h.DAH.RowsRoots)), - attribute.String(jobTypeLabel, string(jobType)), - ) + metric.WithAttributes( + attribute.Bool(failedLabel, err != nil), + attribute.Int(headerWidthLabel, len(h.DAH.RowRoots)), + attribute.String(jobTypeLabel, string(jobType)), + )) atomic.StoreUint64(&m.lastSampledTS, uint64(time.Now().UTC().Unix())) } @@ -172,9 +170,7 @@ func (m *metrics) observeGetHeader(ctx context.Context, d time.Duration) { if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } + ctx = utils.ResetContextOnError(ctx) m.getHeaderTime.Record(ctx, d.Seconds()) } @@ -183,8 +179,6 @@ func (m *metrics) observeNewHead(ctx context.Context) { if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } + ctx = utils.ResetContextOnError(ctx) m.newHead.Add(ctx, 1) } diff --git a/das/options.go b/das/options.go index 665fc5a7d6..c2f2b2fedb 100644 --- a/das/options.go +++ b/das/options.go @@ -40,6 +40,11 @@ type Parameters struct { // divided between parallel workers. SampleTimeout should be adjusted proportionally to // ConcurrencyLimit. SampleTimeout time.Duration + + // SamplingWindow determines the time window that headers should fall into + // in order to be sampled. If set to 0, the sampling window will include + // all headers. + SamplingWindow time.Duration } // DefaultParameters returns the default configuration values for the daser parameters @@ -52,7 +57,8 @@ func DefaultParameters() Parameters { ConcurrencyLimit: concurrencyLimit, BackgroundStoreInterval: 10 * time.Minute, SampleFrom: 1, - // SampleTimeout = block time * max amount of catchup workers + // SampleTimeout = approximate block time (with a bit of wiggle room) * max amount of catchup + // workers SampleTimeout: 15 * time.Second * time.Duration(concurrencyLimit), } } @@ -147,10 +153,18 @@ func WithSampleFrom(sampleFrom uint64) Option { } } -// WithSampleFrom is a functional option to configure the daser's `SampleTimeout` parameter +// WithSampleTimeout is a functional option to configure the daser's `SampleTimeout` parameter // Refer to WithSamplingRange documentation to see an example of how to use this func WithSampleTimeout(sampleTimeout time.Duration) Option { return func(d *DASer) { d.params.SampleTimeout = sampleTimeout } } + +// WithSamplingWindow is a functional option to configure the DASer's +// `SamplingWindow` parameter. +func WithSamplingWindow(samplingWindow time.Duration) Option { + return func(d *DASer) { + d.params.SamplingWindow = samplingWindow + } +} diff --git a/das/state.go b/das/state.go index 6af0b7d8d8..bd3a018a40 100644 --- a/das/state.go +++ b/das/state.go @@ -132,30 +132,29 @@ func (s *coordinatorState) handleRetryResult(res result) { } } -func (s *coordinatorState) isNewHead(newHead int64) bool { +func (s *coordinatorState) isNewHead(newHead uint64) bool { // seen this header before - if uint64(newHead) <= s.networkHead { + if newHead <= s.networkHead { log.Warnf("received head height: %v, which is lower or the same as previously known: %v", newHead, s.networkHead) return false } return true } -func (s *coordinatorState) updateHead(newHead int64) { +func (s *coordinatorState) updateHead(newHead uint64) { if s.networkHead == s.sampleFrom { log.Infow("found first header, starting sampling") } - s.networkHead = uint64(newHead) + s.networkHead = newHead log.Debugw("updated head", "from_height", s.networkHead, "to_height", newHead) s.checkDone() } // recentJob creates a job to process a recent header. func (s *coordinatorState) recentJob(header *header.ExtendedHeader) job { - height := uint64(header.Height()) // move next, to prevent catchup job from processing same height - if s.next == height { + if s.next == header.Height() { s.next++ } s.nextJobID++ @@ -163,8 +162,8 @@ func (s *coordinatorState) recentJob(header *header.ExtendedHeader) job { id: s.nextJobID, jobType: recentJob, header: header, - from: height, - to: height, + from: header.Height(), + to: header.Height(), } } diff --git a/das/stats.go b/das/stats.go index 5799370f91..dda6be6cc0 100644 --- a/das/stats.go +++ b/das/stats.go @@ -34,7 +34,10 @@ type WorkerStats struct { func (s SamplingStats) totalSampled() uint64 { var inProgress uint64 for _, w := range s.Workers { - inProgress += w.To - w.Curr + 1 + // don't count recent jobs, since heights they are working on are after catchup head + if w.JobType != recentJob { + inProgress += w.To - w.Curr + 1 + } } return s.CatchupHead - inProgress - uint64(len(s.Failed)) } diff --git a/das/worker.go b/das/worker.go index 06585e49b0..f2e8c4d821 100644 --- a/das/worker.go +++ b/das/worker.go @@ -120,7 +120,7 @@ func (w *worker) sample(ctx context.Context, timeout time.Duration, height uint6 "type", w.state.jobType, "height", h.Height(), "hash", h.Hash(), - "square width", len(h.DAH.RowsRoots), + "square width", len(h.DAH.RowRoots), "data root", h.DAH.String(), "err", err, "finished (s)", time.Since(start), @@ -135,7 +135,7 @@ func (w *worker) sample(ctx context.Context, timeout time.Duration, height uint6 if w.state.job.jobType == recentJob { err = w.broadcast(ctx, shrexsub.Notification{ DataHash: h.DataHash.Bytes(), - Height: uint64(h.Height()), + Height: h.Height(), }) if err != nil { log.Warn("failed to broadcast availability message", @@ -150,7 +150,7 @@ func (w *worker) sample(ctx context.Context, timeout time.Duration, height uint6 "type", w.state.jobType, "height", h.Height(), "hash", h.Hash(), - "square width", len(h.DAH.RowsRoots), + "square width", len(h.DAH.RowRoots), "data root", h.DAH.String(), "finished (s)", time.Since(start), ) @@ -179,7 +179,7 @@ func (w *worker) getHeader(ctx context.Context, height uint64) (*header.Extended "got header from header store", "height", h.Height(), "hash", h.Hash(), - "square width", len(h.DAH.RowsRoots), + "square width", len(h.DAH.RowRoots), "data root", h.DAH.String(), "finished (s)", time.Since(start), ) diff --git a/docker/telemetry/docker-compose.yml b/docker/telemetry/docker-compose.yml new file mode 100644 index 0000000000..9a95551b0c --- /dev/null +++ b/docker/telemetry/docker-compose.yml @@ -0,0 +1,89 @@ +--- +version: '3.8' + +services: + loki: + container_name: loki + image: grafana/loki:2.6.1 + expose: + - 3100 + ports: + - "3100:3100" + restart: unless-stopped + volumes: + - loki-data:/loki + + promtail: + container_name: promtail + image: grafana/promtail:latest + volumes: + # custom config will read logs from the containers of + # this project + - ${PWD}/promtail:/etc/promtail + # to read container labels and logs + - /var/run/docker.sock:/var/run/docker.sock + - /var/lib/docker/containers:/var/lib/docker/containers:ro + depends_on: + - loki + + prometheus: + container_name: prometheus + image: prom/prometheus + ports: + - 9000:9090 + volumes: + - ${PWD}/prometheus:/etc/prometheus + - prometheus-data:/prometheus + # yamllint disable-line rule:line-length + command: --web.enable-lifecycle --config.file=/etc/prometheus/prometheus.yml + extra_hosts: + - "host.docker.internal:host-gateway" + + otel-collector: + container_name: otel-collector + image: otel/opentelemetry-collector + command: ["--config=/root/otel-collector/config.yml"] + volumes: + - ${PWD}/otel-collector:/root/otel-collector/ + ports: + - "8888:8888" # Prometheus metrics exposed by the collector + - "8889:8889" # Prometheus exporter metrics + - "55681:55681" + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + - "4318:4318" # OTLP http receiver + + jaeger: + container_name: jaeger + image: jaegertracing/all-in-one:latest + ports: + - "16686:16686" + - "14268:14268" + - "14250:14250" + environment: + - COLLECTOR_OTLP_ENABLED=true + - LOG_LEVEL=debug + + grafana: + container_name: grafana + image: grafana/grafana:latest + user: "0" + ports: + - 3001:3000 + restart: unless-stopped + volumes: + - ${PWD}/grafana/:/etc/grafana/provisioning/ + - ${PWD}/grafana/:/var/lib/grafana/dashboards/ + - grafana-data:/var/lib/grafana + + pyroscope: + image: "pyroscope/pyroscope:latest" + ports: + - "4040:4040" + command: + - "server" + +volumes: + prometheus-data: + loki-data: + grafana-data: diff --git a/docker/telemetry/grafana/datasources/config.yml b/docker/telemetry/grafana/datasources/config.yml new file mode 100644 index 0000000000..a9ad009548 --- /dev/null +++ b/docker/telemetry/grafana/datasources/config.yml @@ -0,0 +1,14 @@ +--- +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + jsonData: + maxLines: 1000 diff --git a/docker/telemetry/loki/config.yml b/docker/telemetry/loki/config.yml new file mode 100644 index 0000000000..467188f09b --- /dev/null +++ b/docker/telemetry/loki/config.yml @@ -0,0 +1,11 @@ +--- +auth_enabled: true + +http_prefix: + +server: + http_listen_address: 0.0.0.0 + grpc_listen_address: 0.0.0.0 + http_listen_port: 3100 + grpc_listen_port: 9095 + log_level: info diff --git a/docker/telemetry/otel-collector/config.yml b/docker/telemetry/otel-collector/config.yml new file mode 100644 index 0000000000..c74c8ca93c --- /dev/null +++ b/docker/telemetry/otel-collector/config.yml @@ -0,0 +1,32 @@ +--- +extensions: + health_check: + +receivers: + otlp: + protocols: + grpc: + # endpoint: "0.0.0.0:4317" + http: + # endpoint: "0.0.0.0:4318" + +exporters: + prometheus: + endpoint: "otel-collector:8889" + send_timestamps: true + metric_expiration: 1800m + jaeger: + endpoint: "jaeger:14250" + tls: + insecure: true + +service: + extensions: [health_check] + pipelines: + metrics: + receivers: [otlp] + exporters: [prometheus] + traces: + receivers: [otlp] + processors: [] + exporters: [jaeger] diff --git a/docker/telemetry/prometheus/prometheus.yml b/docker/telemetry/prometheus/prometheus.yml new file mode 100644 index 0000000000..7cc7395305 --- /dev/null +++ b/docker/telemetry/prometheus/prometheus.yml @@ -0,0 +1,25 @@ +--- +global: + scrape_interval: 15s + scrape_timeout: 10s + evaluation_interval: 15s + +scrape_configs: + - job_name: 'collector' + metrics_path: /metrics + honor_timestamps: true + scrape_interval: 15s + scrape_timeout: 10s + scheme: http + static_configs: + - targets: + - 'otel-collector:8889' + - job_name: 'p2p-metrics' + metrics_path: /metrics + honor_timestamps: true + scrape_interval: 15s + scrape_timeout: 10s + scheme: http + static_configs: + - targets: + - 'host.docker.internal:8890' diff --git a/docker/telemetry/promtail/config.yml b/docker/telemetry/promtail/config.yml new file mode 100644 index 0000000000..95267000a9 --- /dev/null +++ b/docker/telemetry/promtail/config.yml @@ -0,0 +1,29 @@ +# https://grafana.com/docs/loki/latest/clients/promtail/configuration/ +# https://docs.docker.com/engine/api/v1.41/#operation/ContainerList +--- +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + - job_name: flog_scrape + docker_sd_configs: + - host: unix:///var/run/docker.sock + refresh_interval: 5s + filters: + - name: label + values: ["logging=promtail"] + relabel_configs: + - source_labels: ['__meta_docker_container_name'] + regex: '/(.*)' + target_label: 'container' + - source_labels: ['__meta_docker_container_log_stream'] + target_label: 'logstream' + - source_labels: ['__meta_docker_container_label_logging_jobname'] + target_label: 'job' diff --git a/docs/adr/adr-009-public-api.md b/docs/adr/adr-009-public-api.md index 76bb1c5ab9..be9e8827eb 100644 --- a/docs/adr/adr-009-public-api.md +++ b/docs/adr/adr-009-public-api.md @@ -100,10 +100,10 @@ GetByHeight(ctx context.Context, height uint64) (*header.ExtendedHeader, error) // WaitForHeight blocks until the header at the given height has been processed // by the node's header store or until context deadline is exceeded. WaitForHeight(ctx context.Context, height uint64) (*header.ExtendedHeader, error) -// GetVerifiedRangeByHeight returns the given range [from:to) of ExtendedHeaders +// GetRangeByHeight returns the given range (from:to) of ExtendedHeaders // from the node's header store and verifies that the returned headers are // adjacent to each other. -GetVerifiedRangeByHeight(ctx context.Context, from, to uint64) ([]*ExtendedHeader, error) +GetRangeByHeight(ctx context.Context, from, to uint64) ([]*ExtendedHeader, error) // Subscribe creates long-living Subscription for newly validated // ExtendedHeaders. Multiple Subscriptions can be created. Subscribe(context.Context) (<-chan *header.ExtendedHeader, error) diff --git a/go.mod b/go.mod index 431a78f519..9f4754f981 100644 --- a/go.mod +++ b/go.mod @@ -1,95 +1,97 @@ module github.com/celestiaorg/celestia-node -go 1.20 +go 1.22 -replace github.com/ipfs/go-verifcid => github.com/celestiaorg/go-verifcid v0.0.1-lazypatch +toolchain go1.22.0 require ( - cosmossdk.io/math v1.0.0-beta.3 - github.com/BurntSushi/toml v1.2.1 - github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 - github.com/benbjohnson/clock v1.3.0 - github.com/celestiaorg/celestia-app v0.13.3 - github.com/celestiaorg/go-fraud v0.1.0 - github.com/celestiaorg/go-header v0.2.7 + cosmossdk.io/errors v1.0.1 + cosmossdk.io/math v1.2.0 + github.com/BurntSushi/toml v1.3.2 + github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b + github.com/benbjohnson/clock v1.3.5 + github.com/celestiaorg/celestia-app v1.4.0 + github.com/celestiaorg/go-fraud v0.2.0 + github.com/celestiaorg/go-header v0.5.3 github.com/celestiaorg/go-libp2p-messenger v0.2.0 - github.com/celestiaorg/nmt v0.15.0 - github.com/celestiaorg/rsmt2d v0.8.0 - github.com/cosmos/cosmos-sdk v0.46.7 + github.com/celestiaorg/nmt v0.20.0 + github.com/celestiaorg/rsmt2d v0.11.0 + github.com/cosmos/cosmos-sdk v0.46.14 github.com/cosmos/cosmos-sdk/api v0.1.0 github.com/cristalhq/jwt v1.2.0 - github.com/dgraph-io/badger/v2 v2.2007.4 + github.com/dgraph-io/badger/v4 v4.2.1-0.20240106094458-1c417aa3799c github.com/etclabscore/go-openrpc-reflect v0.0.37 github.com/filecoin-project/dagstore v0.5.6 - github.com/filecoin-project/go-jsonrpc v0.1.9 + github.com/filecoin-project/go-jsonrpc v0.3.1 github.com/gammazero/workerpool v1.1.3 github.com/gogo/protobuf v1.3.3 github.com/golang/mock v1.6.0 - github.com/gorilla/mux v1.8.0 - github.com/hashicorp/go-retryablehttp v0.7.2 - github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d - github.com/imdario/mergo v0.3.15 + github.com/gorilla/mux v1.8.1 + github.com/grafana/otel-profiling-go v0.5.1 + github.com/grafana/pyroscope-go v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.5 + github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/imdario/mergo v0.3.16 + github.com/ipfs/boxo v0.16.0 + github.com/ipfs/go-block-format v0.2.0 github.com/ipfs/go-blockservice v0.5.0 - github.com/ipfs/go-cid v0.3.2 + github.com/ipfs/go-cid v0.4.1 github.com/ipfs/go-datastore v0.6.0 - github.com/ipfs/go-ds-badger2 v0.1.3 - github.com/ipfs/go-ipfs-blockstore v1.2.0 - github.com/ipfs/go-ipfs-exchange-interface v0.2.0 - github.com/ipfs/go-ipfs-exchange-offline v0.3.0 - github.com/ipfs/go-ipfs-routing v0.3.0 - github.com/ipfs/go-ipld-cbor v0.0.6 - github.com/ipfs/go-ipld-format v0.4.0 - github.com/ipfs/go-libipfs v0.6.0 + github.com/ipfs/go-ds-badger4 v0.1.5 + github.com/ipfs/go-ipld-cbor v0.1.0 + github.com/ipfs/go-ipld-format v0.6.0 github.com/ipfs/go-log/v2 v2.5.1 - github.com/ipfs/go-merkledag v0.10.0 - github.com/ipld/go-car v0.6.0 - github.com/libp2p/go-libp2p v0.26.3 - github.com/libp2p/go-libp2p-kad-dht v0.21.0 - github.com/libp2p/go-libp2p-pubsub v0.9.3 + github.com/ipld/go-car v0.6.2 + github.com/libp2p/go-libp2p v0.32.2 + github.com/libp2p/go-libp2p-kad-dht v0.25.2 + github.com/libp2p/go-libp2p-pubsub v0.10.0 github.com/libp2p/go-libp2p-record v0.2.0 - github.com/libp2p/go-libp2p-routing-helpers v0.6.1 - github.com/minio/sha256-simd v1.0.0 + github.com/libp2p/go-libp2p-routing-helpers v0.7.3 github.com/mitchellh/go-homedir v1.1.0 github.com/multiformats/go-base32 v0.1.0 - github.com/multiformats/go-multiaddr v0.8.0 + github.com/multiformats/go-multiaddr v0.12.2 github.com/multiformats/go-multiaddr-dns v0.3.1 - github.com/multiformats/go-multihash v0.2.2-0.20221030163302-608669da49b6 + github.com/multiformats/go-multihash v0.2.3 github.com/open-rpc/meta-schema v0.0.0-20201029221707-1b72ef2ea333 - github.com/prometheus/client_golang v1.14.0 - github.com/pyroscope-io/client v0.7.0 - github.com/pyroscope-io/otel-profiling-go v0.4.0 - github.com/spf13/cobra v1.6.1 + github.com/prometheus/client_golang v1.18.0 + github.com/rollkit/go-da v0.4.0 + github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 - github.com/stretchr/testify v1.8.1 - github.com/tendermint/tendermint v0.35.4 - go.opentelemetry.io/otel v1.13.0 - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.34.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.2 - go.opentelemetry.io/otel/metric v0.34.0 - go.opentelemetry.io/otel/sdk v1.11.2 - go.opentelemetry.io/otel/sdk/metric v0.34.0 - go.opentelemetry.io/otel/trace v1.13.0 - go.opentelemetry.io/proto/otlp v0.19.0 - go.uber.org/fx v1.19.2 - go.uber.org/zap v1.24.0 - golang.org/x/crypto v0.7.0 - golang.org/x/sync v0.1.0 - golang.org/x/text v0.8.0 - google.golang.org/grpc v1.53.0 - google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 + github.com/stretchr/testify v1.8.4 + github.com/tendermint/tendermint v0.34.29 + go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0 + go.opentelemetry.io/otel v1.22.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 + go.opentelemetry.io/otel/metric v1.22.0 + go.opentelemetry.io/otel/sdk v1.22.0 + go.opentelemetry.io/otel/sdk/metric v1.22.0 + go.opentelemetry.io/otel/trace v1.22.0 + go.opentelemetry.io/proto/otlp v1.0.0 + go.uber.org/fx v1.20.1 + go.uber.org/zap v1.26.0 + golang.org/x/crypto v0.18.0 + golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e + golang.org/x/sync v0.6.0 + golang.org/x/text v0.14.0 + google.golang.org/grpc v1.60.1 + google.golang.org/protobuf v1.32.0 ) require ( - cloud.google.com/go v0.107.0 // indirect - cloud.google.com/go/compute v1.15.1 // indirect + cloud.google.com/go v0.110.10 // indirect + cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v0.8.0 // indirect - cloud.google.com/go/storage v1.27.0 // indirect - cosmossdk.io/errors v1.0.0-beta.7 // indirect + cloud.google.com/go/iam v1.1.5 // indirect + cloud.google.com/go/storage v1.30.1 // indirect filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/99designs/keyring v1.2.1 // indirect - github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect + github.com/ChainSafe/go-schnorrkel v1.0.0 // indirect + github.com/DataDog/zstd v1.5.0 // indirect + github.com/Jorropo/jsync v1.0.1 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/StackExchange/wmi v1.2.1 // indirect @@ -99,80 +101,96 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bgentry/speakeasy v0.1.0 // indirect - github.com/btcsuite/btcd v0.22.1 // indirect + github.com/bits-and-blooms/bitset v1.7.0 // indirect github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect github.com/celestiaorg/merkletree v0.0.0-20210714075610-a84dc3ddbbe4 // indirect - github.com/celestiaorg/quantum-gravity-bridge v1.3.0 // indirect - github.com/cenkalti/backoff/v4 v4.2.0 // indirect + github.com/celestiaorg/quantum-gravity-bridge/v2 v2.1.2 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chzyer/readline v1.5.0 // indirect + github.com/chzyer/readline v1.5.1 // indirect github.com/cockroachdb/apd/v2 v2.0.2 // indirect + github.com/cockroachdb/errors v1.11.1 // indirect + github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect + github.com/cockroachdb/pebble v1.1.0 // indirect + github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/coinbase/rosetta-sdk-go v0.7.9 // indirect - github.com/confio/ics23/go v0.9.0 // indirect - github.com/containerd/cgroups v1.0.4 // indirect + github.com/cometbft/cometbft-db v0.11.0 // indirect + github.com/confio/ics23/go v0.9.1 // indirect + github.com/consensys/bavard v0.1.13 // indirect + github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/cosmos/btcutil v1.0.5 // indirect github.com/cosmos/cosmos-proto v1.0.0-alpha8 // indirect github.com/cosmos/go-bip39 v1.0.0 // indirect - github.com/cosmos/gogoproto v1.4.2 // indirect + github.com/cosmos/gogoproto v1.4.11 // indirect github.com/cosmos/gorocksdb v1.2.0 // indirect - github.com/cosmos/iavl v0.19.4 // indirect - github.com/cosmos/ibc-go/v6 v6.1.0 // indirect - github.com/cosmos/ledger-cosmos-go v0.12.1 // indirect + github.com/cosmos/iavl v0.19.6 // indirect + github.com/cosmos/ibc-go/v6 v6.2.0 // indirect + github.com/cosmos/ledger-cosmos-go v0.13.2 // indirect + github.com/crate-crypto/go-kzg-4844 v0.3.0 // indirect github.com/creachadair/taskgroup v0.3.2 // indirect github.com/cskr/pubsub v1.0.2 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect - github.com/deckarep/golang-set v1.8.0 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect + github.com/deckarep/golang-set/v2 v2.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect + github.com/deepmap/oapi-codegen v1.8.2 // indirect github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect + github.com/dgraph-io/badger/v2 v2.2007.4 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac // indirect - github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/dvsekhvalnov/jose2go v1.6.0 // indirect github.com/elastic/gosigar v0.14.2 // indirect github.com/etclabscore/go-jsonschema-walk v0.0.6 // indirect - github.com/ethereum/go-ethereum v1.10.26 // indirect + github.com/ethereum/c-kzg-4844 v0.4.0 // indirect + github.com/ethereum/go-ethereum v1.13.2 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect github.com/flynn/noise v1.0.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gammazero/deque v0.2.0 // indirect + github.com/getsentry/sentry-go v0.18.0 // indirect github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect - github.com/go-logfmt/logfmt v0.5.1 // indirect - github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.19.4 // indirect github.com/go-openapi/spec v0.19.11 // indirect github.com/go-openapi/swag v0.19.11 // indirect - github.com/go-stack/stack v1.8.0 // indirect - github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect + github.com/go-stack/stack v1.8.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/gogo/gateway v1.1.0 // indirect - github.com/golang/glog v1.0.0 // indirect + github.com/golang/glog v1.1.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect - github.com/google/btree v1.0.1 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/btree v1.1.2 // indirect + github.com/google/flatbuffers v1.12.1 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gopacket v1.1.19 // indirect github.com/google/orderedcode v0.0.1 // indirect - github.com/google/pprof v0.0.0-20221203041831-ce31453925ec // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect - github.com/googleapis/gax-go/v2 v2.7.0 // indirect + github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b // indirect + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.5.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/gorilla/websocket v1.5.0 // indirect + github.com/grafana/pyroscope-go/godeltaprof v0.1.6 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/gtank/merlin v0.1.1 // indirect github.com/gtank/ristretto255 v0.1.2 // indirect @@ -183,153 +201,161 @@ require ( github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect + github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/hashicorp/golang-lru/arc/v2 v2.0.5 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 // indirect - github.com/huin/goupnp v1.0.3 // indirect + github.com/holiman/uint256 v1.2.3 // indirect + github.com/huin/goupnp v1.3.0 // indirect github.com/iancoleman/orderedmap v0.1.0 // indirect github.com/improbable-eng/grpc-web v0.15.0 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/influxdata/influxdb-client-go/v2 v2.12.2 // indirect + github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 // indirect github.com/ipfs/bbloom v0.0.4 // indirect - github.com/ipfs/go-bitswap v0.12.0 // indirect - github.com/ipfs/go-block-format v0.1.1 // indirect + github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect github.com/ipfs/go-ipfs-delay v0.0.1 // indirect - github.com/ipfs/go-ipfs-ds-help v1.1.0 // indirect + github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect github.com/ipfs/go-ipfs-pq v0.0.3 // indirect - github.com/ipfs/go-ipfs-util v0.0.2 // indirect - github.com/ipfs/go-ipld-legacy v0.1.1 // indirect - github.com/ipfs/go-ipns v0.3.0 // indirect + github.com/ipfs/go-ipfs-util v0.0.3 // indirect + github.com/ipfs/go-ipld-legacy v0.2.1 // indirect github.com/ipfs/go-log v1.0.5 // indirect + github.com/ipfs/go-merkledag v0.11.0 // indirect github.com/ipfs/go-metrics-interface v0.0.1 // indirect github.com/ipfs/go-peertaskqueue v0.8.1 // indirect github.com/ipfs/go-verifcid v0.0.2 // indirect - github.com/ipld/go-car/v2 v2.5.1 // indirect + github.com/ipld/go-car/v2 v2.11.0 // indirect github.com/ipld/go-codec-dagpb v1.6.0 // indirect - github.com/ipld/go-ipld-prime v0.20.0 // indirect + github.com/ipld/go-ipld-prime v0.21.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/jbenet/goprocess v0.1.4 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.15.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.3 // indirect - github.com/klauspost/reedsolomon v1.11.1 // indirect - github.com/koron/go-ssdp v0.0.3 // indirect - github.com/lib/pq v1.10.6 // indirect + github.com/klauspost/compress v1.17.3 // indirect + github.com/klauspost/cpuid/v2 v2.2.6 // indirect + github.com/klauspost/reedsolomon v1.11.8 // indirect + github.com/koron/go-ssdp v0.0.4 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/lib/pq v1.10.7 // indirect github.com/libp2p/go-buffer-pool v0.1.0 // indirect github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-flow-metrics v0.1.0 // indirect - github.com/libp2p/go-libp2p-asn-util v0.2.0 // indirect - github.com/libp2p/go-libp2p-kbucket v0.5.0 // indirect + github.com/libp2p/go-libp2p-asn-util v0.3.0 // indirect + github.com/libp2p/go-libp2p-kbucket v0.6.3 // indirect github.com/libp2p/go-msgio v0.3.0 // indirect - github.com/libp2p/go-nat v0.1.0 // indirect + github.com/libp2p/go-nat v0.2.0 // indirect github.com/libp2p/go-netroute v0.2.1 // indirect - github.com/libp2p/go-reuseport v0.2.0 // indirect - github.com/libp2p/go-yamux/v4 v4.0.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/mailru/easyjson v0.7.6 // indirect + github.com/libp2p/go-reuseport v0.4.0 // indirect + github.com/libp2p/go-yamux/v4 v4.0.1 // indirect + github.com/linxGnu/grocksdb v1.8.12 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.17 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/miekg/dns v1.1.50 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/miekg/dns v1.1.56 // indirect github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect github.com/minio/highwayhash v1.0.2 // indirect + github.com/minio/sha256-simd v1.0.1 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/mmcloughlin/addchain v0.4.0 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/multiformats/go-multiaddr-fmt v0.1.0 // indirect - github.com/multiformats/go-multibase v0.1.1 // indirect - github.com/multiformats/go-multicodec v0.8.0 // indirect - github.com/multiformats/go-multistream v0.4.1 // indirect + github.com/multiformats/go-multibase v0.2.0 // indirect + github.com/multiformats/go-multicodec v0.9.0 // indirect + github.com/multiformats/go-multistream v0.5.0 // indirect github.com/multiformats/go-varint v0.0.7 // indirect - github.com/onsi/ginkgo/v2 v2.5.1 // indirect - github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect + github.com/onsi/ginkgo/v2 v2.13.0 // indirect + github.com/opencontainers/runtime-spec v1.1.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 // indirect - github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect + github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/polydawn/refmt v0.89.0 // indirect - github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect - github.com/pyroscope-io/godeltaprof v0.1.0 // indirect + github.com/prometheus/client_model v0.5.0 // indirect + github.com/prometheus/common v0.45.0 // indirect + github.com/prometheus/procfs v0.12.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/qtls-go1-19 v0.2.1 // indirect - github.com/quic-go/qtls-go1-20 v0.1.1 // indirect - github.com/quic-go/quic-go v0.33.0 // indirect - github.com/quic-go/webtransport-go v0.5.2 // indirect + github.com/quic-go/qtls-go1-20 v0.3.4 // indirect + github.com/quic-go/quic-go v0.39.4 // indirect + github.com/quic-go/webtransport-go v0.6.0 // indirect github.com/rakyll/statik v0.1.7 // indirect github.com/raulk/go-watchdog v1.3.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/regen-network/cosmos-proto v0.3.1 // indirect - github.com/rivo/uniseg v0.4.2 // indirect - github.com/rjeczalik/notify v0.9.1 // indirect - github.com/rs/cors v1.8.2 // indirect - github.com/rs/zerolog v1.27.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/rs/cors v1.8.3 // indirect + github.com/rs/zerolog v1.31.0 // indirect github.com/sasha-s/go-deadlock v0.3.1 // indirect github.com/shirou/gopsutil v3.21.6+incompatible // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect - github.com/spf13/afero v1.9.2 // indirect + github.com/spf13/afero v1.9.3 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/viper v1.14.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect - github.com/tendermint/btcd v0.1.1 // indirect - github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 // indirect + github.com/spf13/viper v1.15.0 // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/supranational/blst v0.3.11 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect github.com/tendermint/tm-db v0.6.7 // indirect - github.com/tklauser/go-sysconf v0.3.10 // indirect - github.com/tklauser/numcpus v0.4.0 // indirect + github.com/tidwall/btree v1.5.0 // indirect + github.com/tklauser/go-sysconf v0.3.12 // indirect + github.com/tklauser/numcpus v0.6.1 // indirect github.com/ulikunitz/xz v0.5.10 // indirect - github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 // indirect github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 // indirect - github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa // indirect + github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25 // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect - github.com/zondax/hid v0.9.1 // indirect - github.com/zondax/ledger-go v0.14.0 // indirect - go.etcd.io/bbolt v1.3.6 // indirect + github.com/zondax/hid v0.9.2 // indirect + github.com/zondax/ledger-go v0.14.3 // indirect + go.etcd.io/bbolt v1.3.8 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.2 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.34.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.2 // indirect - go.uber.org/atomic v1.10.0 // indirect - go.uber.org/dig v1.16.1 // indirect + go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.15.1 // indirect + go.uber.org/atomic v1.11.0 // indirect + go.uber.org/dig v1.17.1 // indirect + go.uber.org/mock v0.3.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/exp v0.0.0-20230129154200-a960b3787bd2 // indirect - golang.org/x/mod v0.8.0 // indirect - golang.org/x/net v0.8.0 // indirect - golang.org/x/oauth2 v0.4.0 // indirect - golang.org/x/sys v0.6.0 // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/tools v0.6.0 // indirect + golang.org/x/mod v0.14.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/oauth2 v0.13.0 // indirect + golang.org/x/sys v0.16.0 // indirect + golang.org/x/term v0.16.0 // indirect + golang.org/x/tools v0.17.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.103.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect + gonum.org/v1/gonum v0.13.0 // indirect + google.golang.org/api v0.149.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - lukechampine.com/blake3 v1.1.7 // indirect + lukechampine.com/blake3 v1.2.1 // indirect nhooyr.io/websocket v1.8.7 // indirect + rsc.io/tmplfunc v0.0.3 // indirect sigs.k8s.io/yaml v1.3.0 // indirect ) replace ( - github.com/cosmos/cosmos-sdk => github.com/celestiaorg/cosmos-sdk v1.8.0-sdk-v0.46.7 - github.com/filecoin-project/dagstore => github.com/celestiaorg/dagstore v0.0.0-20230413141458-735ab09a15d6 + github.com/celestiaorg/celestia-app => github.com/faddat/celestia-app v1.6.1-0.20240222001605-69472d965497 + github.com/cosmos/cosmos-sdk => github.com/celestiaorg/cosmos-sdk v1.18.3-sdk-v0.46.14 + github.com/filecoin-project/dagstore => github.com/celestiaorg/dagstore v0.0.0-20230824094345-537c012aa403 github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1 - github.com/tendermint/tendermint => github.com/celestiaorg/celestia-core v1.15.0-tm-v0.34.23 + // broken goleveldb needs to be replaced for the cosmos-sdk and celestia-app + github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 + github.com/tendermint/tendermint => github.com/faddat/celestia-core v1.29.0-tm-v0.34.29.0.20240222093945-cd36a4de3a4d ) diff --git a/go.sum b/go.sum index a7feaf83ab..5635e8b0ea 100644 --- a/go.sum +++ b/go.sum @@ -36,8 +36,8 @@ cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w9 cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.107.0 h1:qkj22L7bgkl6vIeZDlOY2po43Mx/TIa2Wsa7VR+PEww= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= @@ -75,8 +75,8 @@ cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.15.1 h1:7UGq3QknM33pw5xATlpzeoomNxsacIVvTqTTvbfajmE= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= @@ -116,13 +116,12 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97 cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= @@ -179,8 +178,9 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= +cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= @@ -193,10 +193,10 @@ cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuW cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= collectd.org v0.3.0/go.mod h1:A/8DzQBkF6abtvrT2j/AU/4tiBgJWYyh0y/oB/4MlWE= -cosmossdk.io/errors v1.0.0-beta.7 h1:gypHW76pTQGVnHKo6QBkb4yFOJjC+sUGRc5Al3Odj1w= -cosmossdk.io/errors v1.0.0-beta.7/go.mod h1:mz6FQMJRku4bY7aqS/Gwfcmr/ue91roMEKAmDUDpBfE= -cosmossdk.io/math v1.0.0-beta.3 h1:TbZxSopz2LqjJ7aXYfn7nJSb8vNaBklW6BLpcei1qwM= -cosmossdk.io/math v1.0.0-beta.3/go.mod h1:3LYasri3Zna4XpbrTNdKsWmD5fHHkaNAod/mNT9XdE4= +cosmossdk.io/errors v1.0.1 h1:bzu+Kcr0kS/1DuPBtUFdWjzLqyUuCiyHjyJB6srBV/0= +cosmossdk.io/errors v1.0.1/go.mod h1:MeelVSZThMi4bEakzhhhE/CKqVv3nOJDA25bIqRDu/U= +cosmossdk.io/math v1.2.0 h1:8gudhTkkD3NxOP2YyyJIYYmt6dQ55ZfJkDOaxXpy7Ig= +cosmossdk.io/math v1.2.0/go.mod h1:l2Gnda87F0su8a/7FEKJfFdJrM0JZRXQaohlgJeyQh0= dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20201218220906-28db891af037/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= @@ -214,28 +214,34 @@ github.com/99designs/keyring v1.2.1 h1:tYLp1ULvO7i3fI5vE21ReQuj99QFSs7lGm0xWyJo8 github.com/99designs/keyring v1.2.1/go.mod h1:fc+wB5KTk9wQ9sDx0kFXB3A0MaeGHM9AwRStKOQ5vOA= github.com/AndreasBriese/bbloom v0.0.0-20180913140656-343706a395b7/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/AndreasBriese/bbloom v0.0.0-20190306092124-e2d15f34fcf9/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= +github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M= github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM= github.com/Azure/azure-sdk-for-go/sdk/internal v0.8.3/go.mod h1:KLF4gFr6DcKFZwSuH8w8yEK6DpFl3LP5rhdvAb7Yz5I= github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.3.0/go.mod h1:tPaiy8S5bQ+S5sOiDlINkp7+Ef339+Nz5L5XO+cnOHo= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d h1:nalkkPQcITbvhmL4+C4cKA87NW0tfm3Kl9VXRoPywFg= -github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d/go.mod h1:URdX5+vg25ts3aCh8H5IFZybJYKWhJHYMTnf+ULtoC4= +github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= +github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= -github.com/DataDog/zstd v1.4.1/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/DataDog/zstd v1.5.0 h1:+K/VEwIAaPcHiMtQvpLD4lqW7f0Gk3xdYZmI1hD+CXo= github.com/DataDog/zstd v1.5.0/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/Jorropo/jsync v1.0.1 h1:6HgRolFZnsdfzRUj+ImB9og1JYOxQoReSywkHOGSaUU= +github.com/Jorropo/jsync v1.0.1/go.mod h1:jCOZj3vrBCri3bSU3ErUYvevKlnbssrXeCivybS5ABQ= github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/Kubuxu/go-os-helper v0.0.1/go.mod h1:N8B+I7vPCT80IcP58r50u4+gEEcsZETFUpAzWW2ep1Y= github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA= -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= @@ -247,6 +253,7 @@ github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMx github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/Stebalien/go-bitfield v0.0.1/go.mod h1:GNjFpasyUVkHMsfEOk8EFLJ9syQ6SI+XWrX9Wf2XH0s= github.com/VictoriaMetrics/fastcache v1.6.0 h1:C/3Oi3EiBCqufydp1neRZkqcwmEiuRT9c3fqvvgKm5o= github.com/VictoriaMetrics/fastcache v1.6.0/go.mod h1:0qHz5QP0GMX4pfmMA/zt5RgfNuXJrTP0zS7DqpHGGTw= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= @@ -255,16 +262,19 @@ github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSa github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= github.com/Zilliqa/gozilliqa-sdk v1.2.1-0.20201201074141-dd0ecada1be6/go.mod h1:eSYp2T6f0apnuW8TzhV3f6Aff2SE8Dwio++U4ha4yEM= github.com/adlio/schema v1.3.3 h1:oBJn8I02PyTB466pZO1UZEn1TV5XLlifBSyMrmHl/1I= +github.com/adlio/schema v1.3.3/go.mod h1:1EsRssiv9/Ce2CMzq5DoL7RiMshhuigQxrR4DMV9fHg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= -github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 h1:T3+cD5fYvuH36h7EZq+TDpm+d8a6FSD4pQsbmuGGQ8o= github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= +github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b h1:doCpXjVwui6HUN+xgNsNS3SZ0/jUZ68Eb+mJRNOZfog= +github.com/alecthomas/jsonschema v0.0.0-20220216202328-9eeeec9d044b/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -294,8 +304,9 @@ github.com/aws/aws-sdk-go-v2/service/sso v1.1.1/go.mod h1:SuZJxklHxLAXgLTc1iFXbE github.com/aws/aws-sdk-go-v2/service/sts v1.1.1/go.mod h1:Wi0EBZwiz/K44YliU0EKxqTCJGUfYTWXrrBwkq736bM= github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB9LgIw= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +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 v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -304,12 +315,15 @@ github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1U github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo= +github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/btcsuite/btcd v0.0.0-20190213025234-306aecffea32/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8= github.com/btcsuite/btcd v0.0.0-20190315201642-aa6e0f35703c/go.mod h1:DrZx5ec/dmnfpw9KyYoQyYo7d0KEvTkk/5M/vbZjAr8= github.com/btcsuite/btcd v0.0.0-20190523000118-16327141da8c/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= +github.com/btcsuite/btcd v0.0.0-20190605094302-a0d1e3e36d50/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.21.0-beta/go.mod h1:ZSWyehm27aAuS9bvkATT+Xte3hjHZ+MRgMY/8NJ7K94= @@ -318,8 +332,11 @@ github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c= github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= github.com/btcsuite/btcd/btcec/v2 v2.1.2/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.1.3 h1:xfbtw8lwpp0G6NwSHb+UE67ryTFHJAiNuipusjXSohQ= +github.com/btcsuite/btcd/btcutil v1.1.3/go.mod h1:UR7dsSJzJUfMmFiiLlIrMq1lS9jh9EdCV7FStZSnpi0= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= @@ -327,7 +344,6 @@ github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufo github.com/btcsuite/btcutil v0.0.0-20190207003914-4c204d697803/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v1.0.2/go.mod h1:j9HUFwoQRsZL3V4n+qG+CUnEGHOarIxfC3Le2Yhbcts= -github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce h1:YtWJF7RHm2pYCvA5t0RPmAaLUhREsKuKd+SLhxFbFeQ= github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= @@ -336,39 +352,35 @@ github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/bufbuild/protocompile v0.5.1 h1:mixz5lJX4Hiz4FpqFREJHIXLfaLBntfaJv1h+/jS+Qg= +github.com/bufbuild/protocompile v0.5.1/go.mod h1:G5iLmavmF4NsYtpZFvE3B/zFch2GIY8+wjsYLR/lc40= github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/c-bata/go-prompt v0.2.2/go.mod h1:VzqtzE2ksDBcdln8G7mk2RX9QyGjH+OVqOCSiVIqS34= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= -github.com/celestiaorg/celestia-app v0.13.3 h1:lJQLBzKALrKe0lLe3Gf8Si7+CF0eqRuNhjIjj7pM2fk= -github.com/celestiaorg/celestia-app v0.13.3/go.mod h1:OcPBfWDyowJgoEQ89NB2LgLOm9LSwloCgCzdZKjmi78= -github.com/celestiaorg/celestia-core v1.15.0-tm-v0.34.23 h1:BHvn41IHOtvHeX1VZqO/xBFIHj93llcw9ZQfNxyVRlI= -github.com/celestiaorg/celestia-core v1.15.0-tm-v0.34.23/go.mod h1:nL+vkAMKy/A8wWemWqMwBy4pOGWYYbboAVTEe3N5gIU= -github.com/celestiaorg/cosmos-sdk v1.8.0-sdk-v0.46.7 h1:EADZy33ufskVIy6Rj6jbi3SOVCeYYo26zUi7iYx+QR0= -github.com/celestiaorg/cosmos-sdk v1.8.0-sdk-v0.46.7/go.mod h1:vg3Eza9adJJ5Mdx6boz5MpZsZcTZyrfTVYZHyi2zLm4= -github.com/celestiaorg/dagstore v0.0.0-20230413141458-735ab09a15d6 h1:/yCwMCoOPcYCiG18u8/1pv5eXF04xczoQO3sR0bKsgM= -github.com/celestiaorg/dagstore v0.0.0-20230413141458-735ab09a15d6/go.mod h1:ta/DlqIH10bvhwqJIw51Nq3QU4XVMp6pz3f0Deve9fM= -github.com/celestiaorg/go-fraud v0.1.0 h1:v6mZvlmf2J5ELZfPnrtmmOvKbaYIUs/erDWPO8NbZyY= -github.com/celestiaorg/go-fraud v0.1.0/go.mod h1:yoNM35cKMAkt5Mi/Qx3Wi9bnPilLi8n6RpHZVglTUDs= -github.com/celestiaorg/go-header v0.2.7 h1:r0X9Dl7lqBkQpwG3ekQHC61n/QdwO6epuIxDkQ4YX4o= -github.com/celestiaorg/go-header v0.2.7/go.mod h1:i9OpY70+PJ1xPw1IgMfF0Pk6vBD6VWPmjY3bgubJBcU= +github.com/celestiaorg/cosmos-sdk v1.18.3-sdk-v0.46.14 h1:+Te28r5Zp4Vp69f82kcON9/BIF8f1BNXb0go2+achuc= +github.com/celestiaorg/cosmos-sdk v1.18.3-sdk-v0.46.14/go.mod h1:Og5KKgoBEiQlI6u56lDLG191pfknIdXctFn3COWLQP8= +github.com/celestiaorg/dagstore v0.0.0-20230824094345-537c012aa403 h1:Lj73O3S+KJx5/hgZ+IeOLEIoLsAveJN/7/ZtQQtPSVw= +github.com/celestiaorg/dagstore v0.0.0-20230824094345-537c012aa403/go.mod h1:cCGM1UoMvyTk8k62mkc+ReVu8iHBCtSBAAL4wYU7KEI= +github.com/celestiaorg/go-fraud v0.2.0 h1:aaq2JiW0gTnhEdac3l51UCqSyJ4+VjFGTTpN83V4q7I= +github.com/celestiaorg/go-fraud v0.2.0/go.mod h1:lNY1i4K6kUeeE60Z2VK8WXd+qXb8KRzfBhvwPkK6aUc= +github.com/celestiaorg/go-header v0.5.3 h1:8CcflT6aIlcQXKNWcMekoBNs3EU50mEmDp17gbn1pP4= +github.com/celestiaorg/go-header v0.5.3/go.mod h1:7BVR6myjRfACbqW1de6s8OjuK66XzHm8MpFNYr0G+nU= github.com/celestiaorg/go-libp2p-messenger v0.2.0 h1:/0MuPDcFamQMbw9xTZ73yImqgTO3jHV7wKHvWD/Irao= github.com/celestiaorg/go-libp2p-messenger v0.2.0/go.mod h1:s9PIhMi7ApOauIsfBcQwbr7m+HBzmVfDIS+QLdgzDSo= -github.com/celestiaorg/go-verifcid v0.0.1-lazypatch h1:9TSe3w1cmJmbWlweCwCTIZkan7jV8M+KwglXpdD+UG8= -github.com/celestiaorg/go-verifcid v0.0.1-lazypatch/go.mod h1:kXPYu0XqTNUKWA1h3M95UHjUqBzDwXVVt/RXZDjKJmQ= github.com/celestiaorg/merkletree v0.0.0-20210714075610-a84dc3ddbbe4 h1:CJdIpo8n5MFP2MwK0gSRcOVlDlFdQJO1p+FqdxYzmvc= github.com/celestiaorg/merkletree v0.0.0-20210714075610-a84dc3ddbbe4/go.mod h1:fzuHnhzj1pUygGz+1ZkB3uQbEUL4htqCGJ4Qs2LwMZA= -github.com/celestiaorg/nmt v0.15.0 h1:ID9QlMIeP6WK/iiGcfnYLu2qqVIq0UYe/dc3TVPt6EA= -github.com/celestiaorg/nmt v0.15.0/go.mod h1:GfwIvQPhUakn1modWxJ+rv8dUjJzuXg5H+MLFM1o7nY= -github.com/celestiaorg/quantum-gravity-bridge v1.3.0 h1:9zPIp7w1FWfkPnn16y3S4FpFLnQtS7rm81CUVcHEts0= -github.com/celestiaorg/quantum-gravity-bridge v1.3.0/go.mod h1:6WOajINTDEUXpSj5UZzod16UZ96ZVB/rFNKyM+Mt1gI= -github.com/celestiaorg/rsmt2d v0.8.0 h1:ZUxTCELZCM9zMGKNF3cT+rUqMddXMeiuyleSJPZ3Wn4= -github.com/celestiaorg/rsmt2d v0.8.0/go.mod h1:hhlsTi6G3+X5jOP/8Lb/d7i5y2XNFmnyMddYbFSmrgo= +github.com/celestiaorg/nmt v0.20.0 h1:9i7ultZ8Wv5ytt8ZRaxKQ5KOOMo4A2K2T/aPGjIlSas= +github.com/celestiaorg/nmt v0.20.0/go.mod h1:Oz15Ub6YPez9uJV0heoU4WpFctxazuIhKyUtaYNio7E= +github.com/celestiaorg/quantum-gravity-bridge/v2 v2.1.2 h1:Q8nr5SAtDW5gocrBwqwDJcSS/JedqU58WwQA2SP+nXw= +github.com/celestiaorg/quantum-gravity-bridge/v2 v2.1.2/go.mod h1:s/LzLUw0WeYPJ6qdk4q46jKLOq7rc9Z5Mdrxtfpcigw= +github.com/celestiaorg/rsmt2d v0.11.0 h1:lcto/637WyTEZR3dLRoNvyuExfnUbxvdvKi3qz/2V4k= +github.com/celestiaorg/rsmt2d v0.11.0/go.mod h1:6Y580I3gVr0+OVFfW6m2JTwnCCmvW3WfbwSLfuT+HCA= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= -github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= -github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= @@ -381,14 +393,17 @@ github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.0 h1:+eqR0HfOetur4tgnC8ftU5imRnhi4te+BadWS95c5AM= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= +github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= +github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.0 h1:lSwwFrbNviGePhkewF1az4oLmcwqCZijQ2/Wi3BGHAI= github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v0.0.0-20210722231415-061457976a23 h1:dZ0/VyGgQdVGAss6Ju0dt5P0QltE0SFY5Woh6hbIfiQ= github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= +github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= github.com/cilium/ebpf v0.4.0/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= @@ -408,22 +423,42 @@ github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWH github.com/cockroachdb/apd/v2 v2.0.2 h1:weh8u7Cneje73dDh+2tEVLUvyBc89iwepWCD8b8034E= github.com/cockroachdb/apd/v2 v2.0.2/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw= github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +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.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= +github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= +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.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4= +github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= +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/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= github.com/codegangsta/cli v1.20.0/go.mod h1:/qJNoX69yVSKu5o4jLyXAENLRyk1uhi7zkbQ3slBdOA= github.com/coinbase/kryptology v1.8.0/go.mod h1:RYXOAPdzOGUe3qlSFkMGn58i3xUA8hmxYHksuq+8ciI= github.com/coinbase/rosetta-sdk-go v0.7.9 h1:lqllBjMnazTjIqYrOGv8h8jxjg9+hJazIGZr9ZvoCcA= github.com/coinbase/rosetta-sdk-go v0.7.9/go.mod h1:0/knutI7XGVqXmmH4OQD8OckFrbQ8yMsUZTG7FXCR2M= -github.com/confio/ics23/go v0.9.0 h1:cWs+wdbS2KRPZezoaaj+qBleXgUk5WOQFMP3CQFGTr4= -github.com/confio/ics23/go v0.9.0/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= +github.com/cometbft/cometbft-db v0.11.0 h1:M3Lscmpogx5NTbb1EGyGDaFRdsoLWrUWimFEyf7jej8= +github.com/cometbft/cometbft-db v0.11.0/go.mod h1:GDPJAC/iFHNjmZZPN8V8C1yr/eyityhi2W1hz2MGKSc= +github.com/confio/ics23/go v0.9.1 h1:3MV46eeWwO3xCauKyAtuAdJYMyPnnchW4iLr2bTw6/U= +github.com/confio/ics23/go v0.9.1/go.mod h1:4LPZ2NYqnYIVRklaozjNR1FScgDJ2s5Xrp+e/mYVRak= github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/3mZuaj6Sj+PqrmIquiOKy397AKGThQPaGzNXAQ= github.com/consensys/bavard v0.1.8-0.20210915155054-088da2f7f54a/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= +github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= +github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= github.com/consensys/gnark-crypto v0.5.3/go.mod h1:hOdPlWQV1gDLp7faZVeg8Y0iEPFaOUnCc4XeCCk96p0= +github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= +github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= github.com/containerd/cgroups v1.0.3/go.mod h1:/ofk34relqNjSGyqPrmEULrO4Sc8LJhvJmWbUCUKqj8= -github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= github.com/containerd/cgroups v1.0.4/go.mod h1:nLNQtsF7Sl2HxNebu77i1R0oDlhiTG+kO4JTrUzo6IA= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -433,7 +468,7 @@ github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.4.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -447,22 +482,26 @@ github.com/cosmos/cosmos-sdk/api v0.1.0/go.mod h1:CupqQBskAOiTXO1XDZ/wrtWzN/wTxU github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= -github.com/cosmos/gogoproto v1.4.2 h1:UeGRcmFW41l0G0MiefWhkPEVEwvu78SZsHBvI78dAYw= -github.com/cosmos/gogoproto v1.4.2/go.mod h1:cLxOsn1ljAHSV527CHOtaIP91kK6cCrZETRBrkzItWU= +github.com/cosmos/gogoproto v1.4.11 h1:LZcMHrx4FjUgrqQSWeaGC1v/TeuVFqSLa43CC6aWR2g= +github.com/cosmos/gogoproto v1.4.11/go.mod h1:/g39Mh8m17X8Q/GDEs5zYTSNaNnInBSohtaxzQnYq1Y= github.com/cosmos/gorocksdb v1.2.0 h1:d0l3jJG8M4hBouIZq0mDUHZ+zjOx044J3nGRskwTb4Y= github.com/cosmos/gorocksdb v1.2.0/go.mod h1:aaKvKItm514hKfNJpUJXnnOWeBnk2GL4+Qw9NHizILw= -github.com/cosmos/iavl v0.19.4 h1:t82sN+Y0WeqxDLJRSpNd8YFX5URIrT+p8n6oJbJ2Dok= -github.com/cosmos/iavl v0.19.4/go.mod h1:X9PKD3J0iFxdmgNLa7b2LYWdsGd90ToV5cAONApkEPw= -github.com/cosmos/ibc-go/v6 v6.1.0 h1:o7oXws2vKkKfOFzJI+oNylRn44PCNt5wzHd/zKQKbvQ= -github.com/cosmos/ibc-go/v6 v6.1.0/go.mod h1:CY3zh2HLfetRiW8LY6kVHMATe90Wj/UOoY8T6cuB0is= -github.com/cosmos/ledger-cosmos-go v0.12.1 h1:sMBxza5p/rNK/06nBSNmsI/WDqI0pVJFVNihy1Y984w= -github.com/cosmos/ledger-cosmos-go v0.12.1/go.mod h1:dhO6kj+Y+AHIOgAe4L9HL/6NDdyyth4q238I9yFpD2g= +github.com/cosmos/iavl v0.19.6 h1:XY78yEeNPrEYyNCKlqr9chrwoeSDJ0bV2VjocTk//OU= +github.com/cosmos/iavl v0.19.6/go.mod h1:X9PKD3J0iFxdmgNLa7b2LYWdsGd90ToV5cAONApkEPw= +github.com/cosmos/ibc-go/v6 v6.2.0 h1:HKS5WNxQrlmjowHb73J9LqlNJfvTnvkbhXZ9QzNTU7Q= +github.com/cosmos/ibc-go/v6 v6.2.0/go.mod h1:+S3sxcNwOhgraYDJAhIFDg5ipXHaUnJrg7tOQqGyWlc= +github.com/cosmos/ledger-cosmos-go v0.13.2 h1:aY0KZSmUwNKbBm9OvbIjvf7Ozz2YzzpAbgvN2C8x2T0= +github.com/cosmos/ledger-cosmos-go v0.13.2/go.mod h1:HENcEP+VtahZFw38HZ3+LS3Iv5XV6svsnkk9vdJtLr8= github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3 h1:HVTnpeuvF6Owjd5mniCL8DEXo7uYXdQEmOP4FJbV5tg= +github.com/crackcomm/go-gitignore v0.0.0-20170627025303-887ab5e44cc3/go.mod h1:p1d6YEZWvFzEh4KLyvBcVSnrfNDDvK2zfK/4x2v/4pE= +github.com/crate-crypto/go-kzg-4844 v0.3.0 h1:UBlWE0CgyFqqzTI+IFyCzA7A3Zw4iip6uzRv5NIXG0A= +github.com/crate-crypto/go-kzg-4844 v0.3.0/go.mod h1:SBP7ikXEgDnUPONgm33HtuDZEDtWa3L4QtN1ocJSEQ4= github.com/creachadair/taskgroup v0.3.2 h1:zlfutDS+5XG40AOxcHDSThxKzns8Tnr9jnr6VqkYlkM= github.com/creachadair/taskgroup v0.3.2/go.mod h1:wieWwecHVzsidg2CsUnFinW1faVN4+kq+TDlRJQ0Wbk= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= @@ -482,15 +521,19 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davidlazar/go-crypto v0.0.0-20170701192655-dcfb0a7ac018/go.mod h1:rQYf4tfk5sSwFsnDg3qYaBxSjsD9S8+59vW0dKUgme4= 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/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= +github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= +github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= github.com/deepmap/oapi-codegen v1.8.2/go.mod h1:YLgSKSDv/bZQB7N4ws6luhozi3cEdRktEqrX88CvjIw= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= @@ -498,15 +541,17 @@ github.com/dgraph-io/badger v1.5.5-0.20190226225317-8115aed38f8f/go.mod h1:VZxzA github.com/dgraph-io/badger v1.6.0-rc1/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgraph-io/badger v1.6.0/go.mod h1:zwt7syl517jmP8s94KqSxTlM6IMsdhYy6psNgSztDR4= github.com/dgraph-io/badger v1.6.1/go.mod h1:FRmFw3uxvcpa8zG3Rxs0th+hCLIuaQg8HlNV5bjgnuU= +github.com/dgraph-io/badger v1.6.2 h1:mNw0qs90GVgGGWylh0umH5iag1j6n/PeJtNvL6KY/x8= github.com/dgraph-io/badger v1.6.2/go.mod h1:JW2yswe3V058sS0kZ2h/AXeDSqFjxnZcRrVH//y2UQE= -github.com/dgraph-io/badger/v2 v2.2007.3/go.mod h1:26P/7fbL4kUZVEVKLAKXkBXKOydDmM2p1e+NhhnBCAE= github.com/dgraph-io/badger/v2 v2.2007.4 h1:TRWBQg8UrlUhaFdco01nO2uXwzKS7zd+HVdwV/GHc4o= github.com/dgraph-io/badger/v2 v2.2007.4/go.mod h1:vSw/ax2qojzbN6eXHIx6KPKtCSHJN/Uz0X0VPruTIhk= +github.com/dgraph-io/badger/v4 v4.2.1-0.20240106094458-1c417aa3799c h1:Z9rm0wkQBM+VF7vpyrbKnCcSbww0PKygLoptTpkX3d4= +github.com/dgraph-io/badger/v4 v4.2.1-0.20240106094458-1c417aa3799c/go.mod h1:T/uWAYxrXdaXw64ihI++9RMbKTCpKd/yE9+saARew7k= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3-0.20200630154024-f66de99634de/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= -github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgraph-io/ristretto v0.1.1/go.mod h1:S1GPSBCYCIhmVNfcth17y2zZtQT6wzkzgwUve0VDWWA= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-bitstream v0.0.0-20180413035011-3522498ce2c8/go.mod h1:VMaSuZ+SZcx/wljOQKvp5srsbCiKDEb6K2wC4+PiBmQ= github.com/dgryski/go-farm v0.0.0-20190104051053-3adb47b1fb0f/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= @@ -519,6 +564,7 @@ github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/ github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/docker/docker v1.4.2-0.20180625184442-8e610b2b55bf/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= @@ -526,15 +572,14 @@ github.com/dop251/goja v0.0.0-20211011172007-d99e4b8cbf48/go.mod h1:R9ET47fwRVRP github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac h1:opbrjaN/L8gg6Xh5D04Tem+8xVcz6ajZlGCs49mQgyg= -github.com/dustin/go-humanize v1.0.1-0.20200219035652-afde56e7acac/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= -github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +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/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= +github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eclipse/paho.mqtt.golang v1.2.0/go.mod h1:H9keYFcgq3Qr5OUJm/JZI/i6U7joQ8SYLhZwfeOo6Ts= -github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.2 h1:Dg80n8cr90OZ7x+bAax/QjoW/XqTI11RmA79ZwIm9/4= @@ -554,38 +599,56 @@ github.com/etclabscore/go-jsonschema-walk v0.0.6 h1:DrNzoKWKd8f8XB5nFGBY00IcjakR github.com/etclabscore/go-jsonschema-walk v0.0.6/go.mod h1:VdfDY72AFAiUhy0ZXEaWSpveGjMT5JcDIm903NGqFwQ= github.com/etclabscore/go-openrpc-reflect v0.0.37 h1:IH0e7JqIvR9OhbbFWi/BHIkXrqbR3Zyia3RJ733eT6c= github.com/etclabscore/go-openrpc-reflect v0.0.37/go.mod h1:0404Ky3igAasAOpyj1eESjstTyneBAIk5PgJFbK4s5E= +github.com/ethereum/c-kzg-4844 v0.4.0 h1:3MS1s4JtA868KpJxroZoepdV0ZKBp3u/O5HcZ7R3nlY= +github.com/ethereum/c-kzg-4844 v0.4.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= github.com/ethereum/go-ethereum v1.10.17/go.mod h1:Lt5WzjM07XlXc95YzrhosmR4J9Ahd6X2wyEV2SvGhk0= -github.com/ethereum/go-ethereum v1.10.26 h1:i/7d9RBBwiXCEuyduBQzJw/mKmnvzsN14jqBmytw72s= -github.com/ethereum/go-ethereum v1.10.26/go.mod h1:EYFyF19u3ezGLD4RqOkLq+ZCXzYbLoNDdZlMt7kyKFg= +github.com/ethereum/go-ethereum v1.13.2 h1:g9mCpfPWqCA1OL4e6C98PeVttb0HadfBRuKTGvMnOvw= +github.com/ethereum/go-ethereum v1.13.2/go.mod h1:gkQ5Ygi64ZBh9M/4iXY1R8WqoNCx1Ey0CkYn2BD4/fw= github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c h1:8ISkoahWXwZR41ois5lSJBSVw4D0OV19Ht/JSTzvSv0= +github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64= github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A= +github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg= github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 h1:7HZCaLC5+BZpmbhCOZJ293Lz68O7PYrF2EzeiFMwCLk= +github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0= +github.com/faddat/celestia-app v1.6.1-0.20240222001605-69472d965497 h1:xLSiIAn4PDy9D3bxf+70JrRKJyiEB3AzuwBwZp+Tmbs= +github.com/faddat/celestia-app v1.6.1-0.20240222001605-69472d965497/go.mod h1:zWPznuIdfKWCmfQt27sW29ahZME88fWr7982NSs+A98= +github.com/faddat/celestia-core v1.29.0-tm-v0.34.29.0.20240222093945-cd36a4de3a4d h1:JLYT2ynpFu9mo8ISsxi1fK0zF0wVEKRDBVaIbNphsJM= +github.com/faddat/celestia-core v1.29.0-tm-v0.34.29.0.20240222093945-cd36a4de3a4d/go.mod h1:0f44crIBHrgfMMB7D+Q0wCnOGETDqx1kIXOhM8ox3hE= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= +github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/filecoin-project/go-jsonrpc v0.1.9 h1:HRWLxo7HAWzI3xZGeFG4LZJoYpms+Q+8kwmMTLnyS3A= -github.com/filecoin-project/go-jsonrpc v0.1.9/go.mod h1:XBBpuKIMaXIIzeqzO1iucq4GvbF8CxmXRFoezRh+Cx4= +github.com/filecoin-project/go-jsonrpc v0.3.1 h1:qwvAUc5VwAkooquKJmfz9R2+F8znhiqcNHYjEp/NM10= +github.com/filecoin-project/go-jsonrpc v0.3.1/go.mod h1:jBSvPTl8V1N7gSTuCR4bis8wnQnIjHbRPpROol6iQKM= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/flynn/noise v0.0.0-20180327030543-2492fe189ae6/go.mod h1:1i71OnUq3iUe1ma7Lr6yG6/rjvM3emb6yoL7xLFzcVQ= github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk= github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY= github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.2/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/gabriel-vasile/mimetype v1.4.1/go.mod h1:05Vi0w3Y9c/lNvJOdmIwvrrAhX3rYhfQQCaf9VJcv7M= github.com/gammazero/deque v0.2.0 h1:SkieyNB4bg2/uZZLxvya0Pq6diUlwx7m2TeT7GAIWaA= github.com/gammazero/deque v0.2.0/go.mod h1:LFroj8x4cMYCukHJDbxFCkT+r9AndaJnFMuZDV34tuU= github.com/gammazero/workerpool v1.1.3 h1:WixN4xzukFoN0XSeXF6puqEqFTl2mECI9S6W44HWy9Q= @@ -594,17 +657,22 @@ github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqG github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww= github.com/getkin/kin-openapi v0.53.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= github.com/getkin/kin-openapi v0.61.0/go.mod h1:7Yn5whZr5kJi6t+kShccXS8ae1APpYTW6yheSwk8Yi4= +github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= +github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= +github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8= +github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE= github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24= github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= github.com/go-chi/chi/v5 v5.0.0/go.mod h1:BBug9lr0cqtdAhsu6R4AAdvufI0/XBzAQSsUqJpoZOs= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +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-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -620,12 +688,15 @@ github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBj github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8= @@ -649,32 +720,44 @@ github.com/go-openapi/swag v0.19.8/go.mod h1:ao+8BpOPyKdpQz3AOJfbeEVpLmWAvlT1IfT github.com/go-openapi/swag v0.19.11 h1:RFTu/dlFySpyVvJDfp/7674JY4SDglYWKztbiIGFpmc= github.com/go-openapi/swag v0.19.11/go.mod h1:Uc0gKkdR+ojzsEpjh39QChyu92vPgIr72POcgHMAgSY= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= +github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= +github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk= +github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/gateway v1.1.0 h1:u0SuhL9+Il+UbjM9VIE3ntfRujKbvVpFvNB4HbjeVQ0= github.com/gogo/gateway v1.1.0/go.mod h1:S7rR8FRQyG3QFESeSv4l2WnsyzlCLG0CzBbUUo/mbic= @@ -684,8 +767,8 @@ github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/geo v0.0.0-20190916061304-5b978397cfec/go.mod h1:QZ0nwyI2jOfgRAoBvP+ab5aRr7c9x7lhGEJrKvBwjWI= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= +github.com/golang/glog v1.1.2/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -721,19 +804,22 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= +github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/flatbuffers v1.11.0/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +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= @@ -748,14 +834,16 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v0.0.0-20170612174753-24818f796faf/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.1.1-0.20200604201612-c04b05f3adfa/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= @@ -763,8 +851,9 @@ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPg github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/orderedcode v0.0.1 h1:UzfcAexk9Vhv8+9pNOgRu41f16lHq725vPwnSeiG/Us= github.com/google/orderedcode v0.0.1/go.mod h1:iVyU4/qPKHY5h/wSd6rZZCDcLJNxiWO6dvsYES2Sb20= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -779,22 +868,29 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20221203041831-ce31453925ec h1:fR20TYVVwhK4O7r7y+McjRYyaTH6/vjwJOajE+XhlzM= github.com/google/pprof v0.0.0-20221203041831-ce31453925ec/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b h1:RMpPgZTSApbPf7xaVel+QkoGPRLFLrwFO89uDUHEGf0= +github.com/google/pprof v0.0.0-20231023181126-ff6d637d2a7b/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0 h1:y8Yozv7SZtlU//QXbezB6QkpuE6jMD2/gfzk4AftXjs= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= @@ -806,25 +902,33 @@ github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99 github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c h1:7lF+Vz0LqiRidnzC1Oq86fpX1q/iEv2KJdrCtttYjT4= +github.com/gopherjs/gopherjs v0.0.0-20190430165422-3e4dfb77656c/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8= +github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls= +github.com/grafana/pyroscope-go v1.1.1 h1:PQoUU9oWtO3ve/fgIiklYuGilvsm8qaGhlY4Vw6MAcQ= +github.com/grafana/pyroscope-go v1.1.1/go.mod h1:Mw26jU7jsL/KStNSGGuuVYdUq7Qghem5P8aXYXSXG88= +github.com/grafana/pyroscope-go/godeltaprof v0.1.6 h1:nEdZ8louGAplSvIJi1HVp7kWvFvdiiYg3COLlTwJiFo= +github.com/grafana/pyroscope-go/godeltaprof v0.1.6/go.mod h1:Tk376Nbldo4Cha9RgiU7ik8WKFkNpfds98aUzS8omLE= github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= @@ -837,8 +941,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 h1:BZHcxBETFHIdVyhyEfOvn/RdU/QGdLI4y34qQGjGWO0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= @@ -863,6 +967,7 @@ github.com/hashicorp/go-getter v1.7.0 h1:bzrYP+qu/gMrL1au7/aDvkoOVGUJpeKBgbqRHAC github.com/hashicorp/go-getter v1.7.0/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= +github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -871,16 +976,17 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= -github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0= -github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= +github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= +github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= @@ -888,10 +994,14 @@ github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d h1:dg1dEPuWpEqDnvIw251EVy4zlP8gWbsGj4BsUKCRpYs= github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= +github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= +github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= @@ -900,53 +1010,77 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3 h1:aSVUgRRRtOrZOC1fYmY9gV0e9z/Iu+xNVSASWjsuyGU= github.com/hdevalence/ed25519consensus v0.0.0-20220222234857-c00d1f31bab3/go.mod h1:5PC6ZNPde8bBqU/ewGZig35+UIZtw9Ytxez8/q5ZyFE= +github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw= +github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= -github.com/holiman/uint256 v1.2.0 h1:gpSYcPLWGv4sG43I2mVLiDZCNDh/EpGjSk8tmtxitHM= github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= +github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= +github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/huin/goupnp v1.0.0/go.mod h1:n9v9KO1tAxYH82qOn+UTIFQDmx5n1Zxd/ClZDMX7Bnc= github.com/huin/goupnp v1.0.3-0.20220313090229-ca81a64b4204/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= -github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= github.com/huin/goutil v0.0.0-20170803182201-1ca381bf3150/go.mod h1:PpLOETDnJ0o3iZrZfqZzyLl6l7F3c6L1oWn7OICBi6o= github.com/iancoleman/orderedmap v0.0.0-20190318233801-ac98e3ecb4b0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/iancoleman/orderedmap v0.1.0 h1:2orAxZBJsvimgEBmMWfXaFlzSG2fbQil5qzP3F6cCkg= github.com/iancoleman/orderedmap v0.1.0/go.mod h1:N0Wam8K1arqPXNWjMo21EXnBPOPp36vB07FNRdD2geA= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428/go.mod h1:uhpZMVGznybq1itEKXj6RYw9I71qK4kH+OGMjRC4KEo= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/flux v0.65.1/go.mod h1:J754/zds0vvpfwuq7Gc2wRdVwEodfpCFM7mYlOw2LqY= github.com/influxdata/influxdb v1.8.3/go.mod h1:JugdFhsvvI8gadxOI6noqNeeBHvWNTbfYGtiAn+2jhI= github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8= +github.com/influxdata/influxdb-client-go/v2 v2.12.2 h1:uYABKdrEKlYm+++qfKdbgaHKBPmoWR5wpbmj6MBB/2g= +github.com/influxdata/influxdb-client-go/v2 v2.12.2/go.mod h1:YteV91FiQxRdccyJ2cHvj2f/5sq4y4Njqu1fQzsQCOU= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/influxdata/influxql v1.1.1-0.20200828144457-65d3ef77d385/go.mod h1:gHp9y86a/pxhjJ+zMjNXiQAA197Xk9wLxaz+fGG+kWk= github.com/influxdata/line-protocol v0.0.0-20180522152040-32c6aa80de5e/go.mod h1:4kt73NQhadE3daL3WhR5EJ/J2ocX0PZzwxQ0gXJ7oFE= github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097 h1:vilfsDSy7TDxedi9gyBkMvAirat/oRcL0lFdJBf6tdM= github.com/influxdata/line-protocol v0.0.0-20210311194329-9aa0e372d097/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= github.com/influxdata/promql/v2 v2.12.0/go.mod h1:fxOPu+DY0bqCTCECchSRtWfc+0X19ybifQhZoQNF5D8= github.com/influxdata/roaring v0.4.13-0.20180809181101-fc520f41fab6/go.mod h1:bSgUQ7q5ZLSO+bKBGqJiCBGAl+9DxyW63zLTujjUlOE= github.com/influxdata/tdigest v0.0.0-20181121200506-bf2b5ad3c0a9/go.mod h1:Js0mqiSBE6Ffsg94weZZ2c+v/ciT8QRHFOap7EKDrR0= github.com/influxdata/usage-client v0.0.0-20160829180054-6d3895376368/go.mod h1:Wbbw6tYNvwa5dlB6304Sd+82Z3f7PmVZHVKU637d4po= +github.com/ipfs/bbloom v0.0.1/go.mod h1:oqo8CVWsJFMOZqTglBG4wydCE4IQA/G2/SEofB0rjUI= github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= -github.com/ipfs/go-bitfield v1.0.0 h1:y/XHm2GEmD9wKngheWNNCNL0pzrWXZwCdQGv1ikXknQ= +github.com/ipfs/boxo v0.16.0 h1:A9dUmef5a+mEFki6kbyG7el5gl65CiUBzrDeZxzTWKY= +github.com/ipfs/boxo v0.16.0/go.mod h1:jAgpNQn7T7BnibUeReXcKU9Ha1xmYNyOlwVEl193ow0= +github.com/ipfs/go-bitfield v1.0.0/go.mod h1:N/UiujQy+K+ceU1EF5EkVd1TNqevLrCQMIcAEPrdtus= +github.com/ipfs/go-bitfield v1.1.0 h1:fh7FIo8bSwaJEh6DdTWbCeZ1eqOaOkKFI74SCnsWbGA= +github.com/ipfs/go-bitfield v1.1.0/go.mod h1:paqf1wjq/D2BBmzfTVFlJQ9IlFOZpg422HL0HqsGWHU= +github.com/ipfs/go-bitswap v0.1.0/go.mod h1:FFJEf18E9izuCqUtHxbWEvq+reg7o4CW5wSAE1wsxj0= +github.com/ipfs/go-bitswap v0.1.2/go.mod h1:qxSWS4NXGs7jQ6zQvoPY3+NmOfHHG47mhkiLzBpJQIs= +github.com/ipfs/go-bitswap v0.1.8/go.mod h1:TOWoxllhccevbWFUR2N7B1MTSVVge1s6XSMiCSA4MzM= +github.com/ipfs/go-bitswap v0.3.4/go.mod h1:4T7fvNv/LmOys+21tnLzGKncMeeXUYUd1nUiJ2teMvI= github.com/ipfs/go-bitswap v0.5.1/go.mod h1:P+ckC87ri1xFLvk74NlXdP0Kj9RmWAh4+H78sC6Qopo= -github.com/ipfs/go-bitswap v0.12.0 h1:ClbLaufwv8SRQK0sBhl4wDVqJoZGAGMVxdjQy5CTt6c= -github.com/ipfs/go-bitswap v0.12.0/go.mod h1:Iwjkd6+vaDjVIa6b6ogmZgs+b5U3EkIFEX79kQ4DjnI= +github.com/ipfs/go-bitswap v0.6.0/go.mod h1:Hj3ZXdOC5wBJvENtdqsixmzzRukqd8EHLxZLZc3mzRA= +github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= +github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= +github.com/ipfs/go-block-format v0.0.1/go.mod h1:DK/YYcsSUIVAFNwo/KZCdIIbpN0ROH/baNLgayt4pFc= github.com/ipfs/go-block-format v0.0.2/go.mod h1:AWR46JfpcObNfg3ok2JHDUfdiHRgWhJgCQF+KIgOPJY= github.com/ipfs/go-block-format v0.0.3/go.mod h1:4LmD4ZUw0mhO+JSKdpWwrzATiEfM7WWgQ8H5l6P8MVk= -github.com/ipfs/go-block-format v0.1.1 h1:129vSO3zwbsYADcyQWcOYiuCpAqt462SFfqFHdFJhhI= github.com/ipfs/go-block-format v0.1.1/go.mod h1:+McEIT+g52p+zz5xGAABGSOKrzmrdX97bc0USBdWPUs= +github.com/ipfs/go-block-format v0.1.2/go.mod h1:mACVcrxarQKstUU3Yf/RdwbC4DzPV6++rO2a3d+a/KE= +github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= +github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= +github.com/ipfs/go-blockservice v0.1.0/go.mod h1:hzmMScl1kXHg3M2BjTymbVPjv627N7sYcvYaKbop39M= +github.com/ipfs/go-blockservice v0.1.4/go.mod h1:OTZhFpkgY48kNzbgyvcexW9cHrpjBYIjSR0KoDOFOLU= github.com/ipfs/go-blockservice v0.2.1/go.mod h1:k6SiwmgyYgs4M/qt+ww6amPeUH9EISLRBnvUurKJhi8= +github.com/ipfs/go-blockservice v0.3.0/go.mod h1:P5ppi8IHDC7O+pA0AlGTF09jruB2h+oP3wVVaZl8sfk= github.com/ipfs/go-blockservice v0.5.0 h1:B2mwhhhVQl2ntW2EIpaWPwSCxSuqr5fFA93Ms4bYLEY= github.com/ipfs/go-blockservice v0.5.0/go.mod h1:W6brZ5k20AehbmERplmERn8o2Ni3ZZubvAxaIUeaT6w= github.com/ipfs/go-cid v0.0.1/go.mod h1:GHWU/WuQdMPmIosc4Yn1bcCT7dSeX4lBafM7iqUPQvM= @@ -957,10 +1091,18 @@ github.com/ipfs/go-cid v0.0.5/go.mod h1:plgt+Y5MnOey4vO4UlUazGqdbEXuFYitED67Fexh github.com/ipfs/go-cid v0.0.6/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= github.com/ipfs/go-cid v0.0.7/go.mod h1:6Ux9z5e+HpkQdckYoX1PG/6xqKspzlEIR5SDmgqgC/I= github.com/ipfs/go-cid v0.1.0/go.mod h1:rH5/Xv83Rfy8Rw6xG+id3DYAMUVmem1MowoKwdXmN2o= -github.com/ipfs/go-cid v0.3.2 h1:OGgOd+JCFM+y1DjWPmVH+2/4POtpDzwcr7VgnB7mZXc= +github.com/ipfs/go-cid v0.2.0/go.mod h1:P+HXFDF4CVhaVayiEb4wkAy7zBHxBwsJyt0Y5U6MLro= +github.com/ipfs/go-cid v0.3.0/go.mod h1:P+HXFDF4CVhaVayiEb4wkAy7zBHxBwsJyt0Y5U6MLro= github.com/ipfs/go-cid v0.3.2/go.mod h1:gQ8pKqT/sUxGY+tIwy1RPpAojYu7jAyCp5Tz1svoupw= +github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= +github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= +github.com/ipfs/go-cidutil v0.1.0 h1:RW5hO7Vcf16dplUU60Hs0AKDkQAVPVplr7lk97CFL+Q= +github.com/ipfs/go-cidutil v0.1.0/go.mod h1:e7OEVBMIv9JaOxt9zaGEmAoSlXW9jdFZ5lP/0PwcfpA= github.com/ipfs/go-datastore v0.0.1/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE= +github.com/ipfs/go-datastore v0.0.5/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE= +github.com/ipfs/go-datastore v0.1.0/go.mod h1:d4KVXhMt913cLBEI/PXAy6ko+W7e9AhyAKBGh803qeE= github.com/ipfs/go-datastore v0.1.1/go.mod h1:w38XXW9kVFNp57Zj5knbKWM2T+KOZCGDRVNdgPHtbHw= +github.com/ipfs/go-datastore v0.3.1/go.mod h1:w38XXW9kVFNp57Zj5knbKWM2T+KOZCGDRVNdgPHtbHw= github.com/ipfs/go-datastore v0.4.0/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA= github.com/ipfs/go-datastore v0.4.1/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA= github.com/ipfs/go-datastore v0.4.4/go.mod h1:SX/xMIKoCszPqp+z9JhPYCmoOoXTvaa13XEbGtsFUhA= @@ -973,57 +1115,97 @@ github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= github.com/ipfs/go-ds-badger v0.0.2/go.mod h1:Y3QpeSFWQf6MopLTiZD+VT6IC1yZqaGmjvRcKeSGij8= github.com/ipfs/go-ds-badger v0.0.5/go.mod h1:g5AuuCGmr7efyzQhLL8MzwqcauPojGPUaHzfGTzuE3s= +github.com/ipfs/go-ds-badger v0.0.7/go.mod h1:qt0/fWzZDoPW6jpQeqUjR5kBfhDNB65jd9YlmAvpQBk= github.com/ipfs/go-ds-badger v0.2.1/go.mod h1:Tx7l3aTph3FMFrRS838dcSJh+jjA7cX9DrGVwx/NOwE= github.com/ipfs/go-ds-badger v0.2.3/go.mod h1:pEYw0rgg3FIrywKKnL+Snr+w/LjJZVMTBRn4FS6UHUk= +github.com/ipfs/go-ds-badger v0.3.0 h1:xREL3V0EH9S219kFFueOYJJTcjgNSZ2HY1iSvN7U1Ro= github.com/ipfs/go-ds-badger v0.3.0/go.mod h1:1ke6mXNqeV8K3y5Ak2bAA0osoTfmxUdupVCGm4QUIek= -github.com/ipfs/go-ds-badger2 v0.1.3 h1:Zo9JicXJ1DmXTN4KOw7oPXkspZ0AWHcAFCP1tQKnegg= -github.com/ipfs/go-ds-badger2 v0.1.3/go.mod h1:TPhhljfrgewjbtuL/tczP8dNrBYwwk+SdPYbms/NO9w= +github.com/ipfs/go-ds-badger4 v0.1.5 h1:MwrTsIUJIqH/ChuDdUOzxwxMxHx/Li1ECoSCKsCUxiA= +github.com/ipfs/go-ds-badger4 v0.1.5/go.mod h1:LUU2FbhNdmhAbJmMeoahVRbe4GsduAODSJHWJJh2Vo4= github.com/ipfs/go-ds-leveldb v0.0.1/go.mod h1:feO8V3kubwsEF22n0YRQCffeb79OOYIykR4L04tMOYc= +github.com/ipfs/go-ds-leveldb v0.1.0/go.mod h1:hqAW8y4bwX5LWcCtku2rFNX3vjDZCy5LZCg+cSZvYb8= github.com/ipfs/go-ds-leveldb v0.4.1/go.mod h1:jpbku/YqBSsBc1qgME8BkWS4AxzF2cEu1Ii2r79Hh9s= github.com/ipfs/go-ds-leveldb v0.4.2/go.mod h1:jpbku/YqBSsBc1qgME8BkWS4AxzF2cEu1Ii2r79Hh9s= github.com/ipfs/go-ds-leveldb v0.5.0 h1:s++MEBbD3ZKc9/8/njrn4flZLnCuY9I79v94gBUNumo= github.com/ipfs/go-ds-leveldb v0.5.0/go.mod h1:d3XG9RUDzQ6V4SHi8+Xgj9j1XuEk1z82lquxrVbml/Q= +github.com/ipfs/go-fetcher v1.5.0/go.mod h1:5pDZ0393oRF/fHiLmtFZtpMNBQfHOYNPtryWedVuSWE= +github.com/ipfs/go-fetcher v1.6.1/go.mod h1:27d/xMV8bodjVs9pugh/RCjjK2OZ68UgAMspMdingNo= +github.com/ipfs/go-ipfs-blockstore v0.0.1/go.mod h1:d3WClOmRQKFnJ0Jz/jj/zmksX0ma1gROTlovZKBmN08= +github.com/ipfs/go-ipfs-blockstore v0.1.0/go.mod h1:5aD0AvHPi7mZc6Ci1WCAhiBQu2IsfTduLl+422H6Rqw= +github.com/ipfs/go-ipfs-blockstore v0.1.4/go.mod h1:Jxm3XMVjh6R17WvxFEiyKBLUGr86HgIYJW/D/MwqeYQ= github.com/ipfs/go-ipfs-blockstore v0.2.1/go.mod h1:jGesd8EtCM3/zPgx+qr0/feTXGUeRai6adgwC+Q+JvE= github.com/ipfs/go-ipfs-blockstore v1.1.2/go.mod h1:w51tNR9y5+QXB0wkNcHt4O2aSZjTdqaEWaQdSxEyUOY= -github.com/ipfs/go-ipfs-blockstore v1.2.0 h1:n3WTeJ4LdICWs/0VSfjHrlqpPpl6MZ+ySd3j8qz0ykw= github.com/ipfs/go-ipfs-blockstore v1.2.0/go.mod h1:eh8eTFLiINYNSNawfZOC7HOxNTxpB1PFuA5E1m/7exE= +github.com/ipfs/go-ipfs-blockstore v1.3.0/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= +github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= +github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= -github.com/ipfs/go-ipfs-chunker v0.0.1 h1:cHUUxKFQ99pozdahi+uSC/3Y6HeRpi9oTeUHbE27SEw= +github.com/ipfs/go-ipfs-chunker v0.0.1/go.mod h1:tWewYK0we3+rMbOh7pPFGDyypCtvGcBFymgY4rSDLAw= +github.com/ipfs/go-ipfs-chunker v0.0.5 h1:ojCf7HV/m+uS2vhUGWcogIIxiO5ubl5O57Q7NapWLY8= +github.com/ipfs/go-ipfs-chunker v0.0.5/go.mod h1:jhgdF8vxRHycr00k13FM8Y0E+6BoalYeobXmUyTreP8= github.com/ipfs/go-ipfs-delay v0.0.0-20181109222059-70721b86a9a8/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= +github.com/ipfs/go-ipfs-ds-help v0.0.1/go.mod h1:gtP9xRaZXqIQRh1HRpp595KbBEdgqWFxefeVKOV8sxo= github.com/ipfs/go-ipfs-ds-help v0.1.1/go.mod h1:SbBafGJuGsPI/QL3j9Fc5YPLeAu+SzOkI0gFwAg+mOs= -github.com/ipfs/go-ipfs-ds-help v1.1.0 h1:yLE2w9RAsl31LtfMt91tRZcrx+e61O5mDxFRR994w4Q= github.com/ipfs/go-ipfs-ds-help v1.1.0/go.mod h1:YR5+6EaebOhfcqVCyqemItCLthrpVNot+rsOU/5IatU= +github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= +github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= +github.com/ipfs/go-ipfs-exchange-interface v0.0.1/go.mod h1:c8MwfHjtQjPoDyiy9cFquVtVHkO9b9Ob3FG91qJnWCM= github.com/ipfs/go-ipfs-exchange-interface v0.1.0/go.mod h1:ych7WPlyHqFvCi/uQI48zLZuAWVP5iTQPXEfVaw5WEI= -github.com/ipfs/go-ipfs-exchange-interface v0.2.0 h1:8lMSJmKogZYNo2jjhUs0izT+dck05pqUw4mWNW9Pw6Y= github.com/ipfs/go-ipfs-exchange-interface v0.2.0/go.mod h1:z6+RhJuDQbqKguVyslSOuVDhqF9JtTrO3eptSAiW2/Y= +github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s= +github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E= +github.com/ipfs/go-ipfs-exchange-offline v0.0.1/go.mod h1:WhHSFCVYX36H/anEKQboAzpUws3x7UeEGkzQc3iNkM0= github.com/ipfs/go-ipfs-exchange-offline v0.1.1/go.mod h1:vTiBRIbzSwDD0OWm+i3xeT0mO7jG2cbJYatp3HPk5XY= +github.com/ipfs/go-ipfs-exchange-offline v0.2.0/go.mod h1:HjwBeW0dvZvfOMwDP0TSKXIHf2s+ksdP4E3MLDRtLKY= github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= +github.com/ipfs/go-ipfs-files v0.0.3/go.mod h1:INEFm0LL2LWXBhNJ2PMIIb2w45hpXgPjNoE7yA8Y1d4= +github.com/ipfs/go-ipfs-files v0.0.8/go.mod h1:wiN/jSG8FKyk7N0WyctKSvq3ljIa2NNTiZB55kpTdOs= +github.com/ipfs/go-ipfs-files v0.3.0 h1:fallckyc5PYjuMEitPNrjRfpwl7YFt69heCOUhsbGxQ= +github.com/ipfs/go-ipfs-files v0.3.0/go.mod h1:xAUtYMwB+iu/dtf6+muHNSFQCJG2dSiStR2P6sn9tIM= +github.com/ipfs/go-ipfs-keystore v0.1.0/go.mod h1:LvLw7Qhnb0RlMOfCzK6OmyWxICip6lQ06CCmdbee75U= +github.com/ipfs/go-ipfs-posinfo v0.0.1 h1:Esoxj+1JgSjX0+ylc0hUmJCOv6V2vFoZiETLR6OtpRs= +github.com/ipfs/go-ipfs-posinfo v0.0.1/go.mod h1:SwyeVP+jCwiDu0C313l/8jg6ZxM0qqtlt2a0vILTc1A= +github.com/ipfs/go-ipfs-pq v0.0.1/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY= github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY= github.com/ipfs/go-ipfs-pq v0.0.3 h1:YpoHVJB+jzK15mr/xsWC574tyDLkezVrDNeaalQBsTE= github.com/ipfs/go-ipfs-pq v0.0.3/go.mod h1:btNw5hsHBpRcSSgZtiNm/SLj5gYIZ18AKtv3kERkRb4= +github.com/ipfs/go-ipfs-redirects-file v0.1.1/go.mod h1:tAwRjCV0RjLTjH8DR/AU7VYvfQECg+lpUy2Mdzv7gyk= +github.com/ipfs/go-ipfs-routing v0.1.0/go.mod h1:hYoUkJLyAUKhF58tysKpids8RNDPO42BVMgK5dNsoqY= github.com/ipfs/go-ipfs-routing v0.2.1/go.mod h1:xiNNiwgjmLqPS1cimvAw6EyB9rkVDbiocA4yY+wRNLM= github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc= github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo= github.com/ipfs/go-ipfs-util v0.0.1/go.mod h1:spsl5z8KUnrve+73pOhSVZND1SIxPW5RyBCNzQxlJBc= -github.com/ipfs/go-ipfs-util v0.0.2 h1:59Sswnk1MFaiq+VcaknX7aYEyGyGDAA73ilhEK2POp8= github.com/ipfs/go-ipfs-util v0.0.2/go.mod h1:CbPtkWJzjLdEcezDns2XYaehFVNXG9zrdrtMecczcsQ= +github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= +github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= +github.com/ipfs/go-ipld-cbor v0.0.2/go.mod h1:wTBtrQZA3SoFKMVkp6cn6HMRteIB1VsmHA0AQFOn7Nc= +github.com/ipfs/go-ipld-cbor v0.0.3/go.mod h1:wTBtrQZA3SoFKMVkp6cn6HMRteIB1VsmHA0AQFOn7Nc= github.com/ipfs/go-ipld-cbor v0.0.5/go.mod h1:BkCduEx3XBCO6t2Sfo5BaHzuok7hbhdMm9Oh8B2Ftq4= -github.com/ipfs/go-ipld-cbor v0.0.6 h1:pYuWHyvSpIsOOLw4Jy7NbBkCyzLDcl64Bf/LZW7eBQ0= github.com/ipfs/go-ipld-cbor v0.0.6/go.mod h1:ssdxxaLJPXH7OjF5V4NSjBbcfh+evoR4ukuru0oPXMA= +github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= +github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= github.com/ipfs/go-ipld-format v0.0.1/go.mod h1:kyJtbkDALmFHv3QR6et67i35QzO3S0dCDnkOJhcZkms= +github.com/ipfs/go-ipld-format v0.0.2/go.mod h1:4B6+FM2u9OJ9zCV+kSbgFAZlOrv1Hqbf0INGQgiKf9k= github.com/ipfs/go-ipld-format v0.2.0/go.mod h1:3l3C1uKoadTPbeNfrDi+xMInYKlx2Cvg1BuydPSdzQs= github.com/ipfs/go-ipld-format v0.3.0/go.mod h1:co/SdBE8h99968X0hViiw1MNlh6fvxxnHpvVLnH7jSM= -github.com/ipfs/go-ipld-format v0.4.0 h1:yqJSaJftjmjc9jEOFYlpkwOLVKv68OD27jFLlSghBlQ= +github.com/ipfs/go-ipld-format v0.3.1/go.mod h1:co/SdBE8h99968X0hViiw1MNlh6fvxxnHpvVLnH7jSM= github.com/ipfs/go-ipld-format v0.4.0/go.mod h1:co/SdBE8h99968X0hViiw1MNlh6fvxxnHpvVLnH7jSM= +github.com/ipfs/go-ipld-format v0.5.0/go.mod h1:ImdZqJQaEouMjCvqCe0ORUS+uoBmf7Hf+EO/jh+nk3M= +github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= +github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= github.com/ipfs/go-ipld-legacy v0.1.0/go.mod h1:86f5P/srAmh9GcIcWQR9lfFLZPrIyyXQeVlOWeeWEuI= -github.com/ipfs/go-ipld-legacy v0.1.1 h1:BvD8PEuqwBHLTKqlGFTHSwrwFOMkVESEvwIYwR2cdcc= github.com/ipfs/go-ipld-legacy v0.1.1/go.mod h1:8AyKFCjgRPsQFf15ZQgDB8Din4DML/fOmKZkkFkrIEg= -github.com/ipfs/go-ipns v0.3.0 h1:ai791nTgVo+zTuq2bLvEGmWP1M0A6kGTXUsgv/Yq67A= +github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= +github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= +github.com/ipfs/go-ipns v0.2.0/go.mod h1:3cLT2rbvgPZGkHJoPO1YMJeh6LtkxopCkKFcio/wE24= github.com/ipfs/go-ipns v0.3.0/go.mod h1:3cLT2rbvgPZGkHJoPO1YMJeh6LtkxopCkKFcio/wE24= +github.com/ipfs/go-libipfs v0.1.0/go.mod h1:qX0d9h+wu53PFtCTXxdXVBakd6ZCvGDdkZUKmdLMLx0= +github.com/ipfs/go-libipfs v0.3.0/go.mod h1:pSUHZ5qPJTAidsxe9bAeHp3KIiw2ODEW2a2kM3v+iXI= +github.com/ipfs/go-libipfs v0.4.0/go.mod h1:XsU2cP9jBhDrXoJDe0WxikB8XcVmD3k2MEZvB3dbYu8= github.com/ipfs/go-libipfs v0.6.0 h1:3FuckAJEm+zdHbHbf6lAyk0QUzc45LsFcGw102oBCZM= github.com/ipfs/go-libipfs v0.6.0/go.mod h1:UjjDIuehp2GzlNP0HEr5I9GfFT7zWgst+YfpUEIThtw= github.com/ipfs/go-log v0.0.1/go.mod h1:kL1d2/hzSpI0thNYjiKfjanbVNU+IIGA/WnNESY9leM= @@ -1035,37 +1217,78 @@ github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JP github.com/ipfs/go-log/v2 v2.0.2/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0= github.com/ipfs/go-log/v2 v2.0.3/go.mod h1:O7P1lJt27vWHhOwQmcFEvlmo49ry2VY2+JfBWFaa9+0= github.com/ipfs/go-log/v2 v2.0.5/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= -github.com/ipfs/go-log/v2 v2.0.8/go.mod h1:eZs4Xt4ZUJQFM3DlanGhy7TkwwawCZcSByscwkWG+dw= github.com/ipfs/go-log/v2 v2.1.1/go.mod h1:2v2nsGfZsvvAJz13SyFzf9ObaqwHiHxsPLEHntrv9KM= github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= github.com/ipfs/go-log/v2 v2.3.0/go.mod h1:QqGoj30OTpnKaG/LKTGTxoP2mmQtjVMEnK72gynbe/g= github.com/ipfs/go-log/v2 v2.5.0/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= +github.com/ipfs/go-merkledag v0.2.3/go.mod h1:SQiXrtSts3KGNmgOzMICy5c0POOpUNQLvB3ClKnBAlk= +github.com/ipfs/go-merkledag v0.3.2/go.mod h1:fvkZNNZixVW6cKSZ/JfLlON5OlgTXNdRLz0p6QG/I2M= github.com/ipfs/go-merkledag v0.5.1/go.mod h1:cLMZXx8J08idkp5+id62iVftUQV+HlYJ3PIhDfZsjA4= -github.com/ipfs/go-merkledag v0.10.0 h1:IUQhj/kzTZfam4e+LnaEpoiZ9vZF6ldimVlby+6OXL4= +github.com/ipfs/go-merkledag v0.6.0/go.mod h1:9HSEwRd5sV+lbykiYP+2NC/3o6MZbKNaa4hfNcH5iH0= +github.com/ipfs/go-merkledag v0.9.0/go.mod h1:bPHqkHt5OZ0p1n3iqPeDiw2jIBkjAytRjS3WSBwjq90= github.com/ipfs/go-merkledag v0.10.0/go.mod h1:zkVav8KiYlmbzUzNM6kENzkdP5+qR7+2mCwxkQ6GIj8= +github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY= +github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4= github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= +github.com/ipfs/go-namesys v0.7.0/go.mod h1:KYSZBVZG3VJC34EfqqJPG7T48aWgxseoMPAPA5gLyyQ= +github.com/ipfs/go-path v0.1.1/go.mod h1:vC8q4AKOtrjJz2NnllIrmr2ZbGlF5fW2OKKyhV9ggb0= +github.com/ipfs/go-path v0.3.0/go.mod h1:NOScsVgxfC/eIw4nz6OiGwK42PjaSJ4Y/ZFPn1Xe07I= +github.com/ipfs/go-peertaskqueue v0.1.0/go.mod h1:Jmk3IyCcfl1W3jTW3YpghSwSEC6IJ3Vzz/jUmWw8Z0U= +github.com/ipfs/go-peertaskqueue v0.1.1/go.mod h1:Jmk3IyCcfl1W3jTW3YpghSwSEC6IJ3Vzz/jUmWw8Z0U= +github.com/ipfs/go-peertaskqueue v0.2.0/go.mod h1:5/eNrBEbtSKWCG+kQK8K8fGNixoYUnr+P7jivavs9lY= github.com/ipfs/go-peertaskqueue v0.7.0/go.mod h1:M/akTIE/z1jGNXMU7kFB4TeSEFvj68ow0Rrb04donIU= +github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM= github.com/ipfs/go-peertaskqueue v0.8.1 h1:YhxAs1+wxb5jk7RvS0LHdyiILpNmRIRnZVztekOF0pg= github.com/ipfs/go-peertaskqueue v0.8.1/go.mod h1:Oxxd3eaK279FxeydSPPVGHzbwVeHjatZ2GA8XD+KbPU= -github.com/ipfs/go-unixfsnode v1.5.1 h1:JcR3t5C2nM1V7PMzhJ/Qmo19NkoFIKweDSZyDx+CjkI= -github.com/ipld/go-car v0.6.0 h1:d5QrGLnHAxiNLHor+DKGrLdqnM0dQJh2whfSXRDq6J0= -github.com/ipld/go-car v0.6.0/go.mod h1:tBrW1XZ3L2XipLxA69RnTVGW3rve6VX4TbaTYkq8aEA= +github.com/ipfs/go-unixfs v0.2.4/go.mod h1:SUdisfUjNoSDzzhGVxvCL9QO/nKdwXdr+gbMUdqcbYw= +github.com/ipfs/go-unixfs v0.3.1/go.mod h1:h4qfQYzghiIc8ZNFKiLMFWOTzrWIAtzYQ59W/pCFf1o= +github.com/ipfs/go-unixfs v0.4.3/go.mod h1:TSG7G1UuT+l4pNj91raXAPkX0BhJi3jST1FDTfQ5QyM= +github.com/ipfs/go-unixfs v0.4.4/go.mod h1:TSG7G1UuT+l4pNj91raXAPkX0BhJi3jST1FDTfQ5QyM= +github.com/ipfs/go-unixfs v0.4.5 h1:wj8JhxvV1G6CD7swACwSKYa+NgtdWC1RUit+gFnymDU= +github.com/ipfs/go-unixfs v0.4.5/go.mod h1:BIznJNvt/gEx/ooRMI4Us9K8+qeGO7vx1ohnbk8gjFg= +github.com/ipfs/go-unixfsnode v1.1.2/go.mod h1:5dcE2x03pyjHk4JjamXmunTMzz+VUtqvPwZjIEkfV6s= +github.com/ipfs/go-unixfsnode v1.4.0/go.mod h1:qc7YFFZ8tABc58p62HnIYbUMwj9chhUuFWmxSokfePo= +github.com/ipfs/go-unixfsnode v1.5.1/go.mod h1:ed79DaG9IEuZITJVQn4U6MZDftv6I3ygUBLPfhEbHvk= +github.com/ipfs/go-unixfsnode v1.5.2/go.mod h1:NlOebRwYx8lMCNMdhAhEspYPBD3obp7TE0LvBqHY+ks= +github.com/ipfs/go-unixfsnode v1.7.1/go.mod h1:PVfoyZkX1B34qzT3vJO4nsLUpRCyhnMuHBznRcXirlk= +github.com/ipfs/go-unixfsnode v1.7.4 h1:iLvKyAVKUYOIAW2t4kDYqsT7VLGj31eXJE2aeqGfbwA= +github.com/ipfs/go-unixfsnode v1.7.4/go.mod h1:PVfoyZkX1B34qzT3vJO4nsLUpRCyhnMuHBznRcXirlk= +github.com/ipfs/go-verifcid v0.0.1/go.mod h1:5Hrva5KBeIog4A+UpqlaIU+DEstipcJYQQZc0g37pY0= +github.com/ipfs/go-verifcid v0.0.2 h1:XPnUv0XmdH+ZIhLGKg6U2vaPaRDXb9urMyNVCE7uvTs= +github.com/ipfs/go-verifcid v0.0.2/go.mod h1:40cD9x1y4OWnFXbLNJYRe7MpNvWlMn3LZAG5Wb4xnPU= +github.com/ipfs/interface-go-ipfs-core v0.9.0/go.mod h1:F3EcmDy53GFkF0H3iEJpfJC320fZ/4G60eftnItrrJ0= +github.com/ipfs/interface-go-ipfs-core v0.10.0/go.mod h1:F3EcmDy53GFkF0H3iEJpfJC320fZ/4G60eftnItrrJ0= +github.com/ipld/go-car v0.5.0/go.mod h1:ppiN5GWpjOZU9PgpAZ9HbZd9ZgSpwPMr48fGRJOWmvE= +github.com/ipld/go-car v0.6.2 h1:Hlnl3Awgnq8icK+ze3iRghk805lu8YNq3wlREDTF2qc= +github.com/ipld/go-car v0.6.2/go.mod h1:oEGXdwp6bmxJCZ+rARSkDliTeYnVzv3++eXajZ+Bmr8= github.com/ipld/go-car/v2 v2.1.1/go.mod h1:+2Yvf0Z3wzkv7NeI69i8tuZ+ft7jyjPYIWZzeVNeFcI= -github.com/ipld/go-car/v2 v2.5.1 h1:U2ux9JS23upEgrJScW8VQuxmE94560kYxj9CQUpcfmk= github.com/ipld/go-car/v2 v2.5.1/go.mod h1:jKjGOqoCj5zn6KjnabD6JbnCsMntqU2hLiU6baZVO3E= +github.com/ipld/go-car/v2 v2.8.0/go.mod h1:a+BnAxUqgr7wcWxW/lI6ctyEQ2v9gjBChPytwFMp2f4= +github.com/ipld/go-car/v2 v2.10.1/go.mod h1:sQEkXVM3csejlb1kCCb+vQ/pWBKX9QtvsrysMQjOgOg= +github.com/ipld/go-car/v2 v2.11.0 h1:lkAPwbbTFqbdfawgm+bfmFc8PjGC7D12VcaLXPCLNfM= +github.com/ipld/go-car/v2 v2.11.0/go.mod h1:aDszqev0zjtU8l96g4lwXHaU9bzArj56Y7eEN0q/xqA= github.com/ipld/go-codec-dagpb v1.3.0/go.mod h1:ga4JTU3abYApDC3pZ00BC2RSvC3qfBb9MSJkMLSwnhA= +github.com/ipld/go-codec-dagpb v1.3.1/go.mod h1:ErNNglIi5KMur/MfFE/svtgQthzVvf+43MrzLbpcIZY= +github.com/ipld/go-codec-dagpb v1.4.1/go.mod h1:XdXTO/TUD/ra9RcK/NfmwBfr1JpFxM2uRKaB9oe4LxE= +github.com/ipld/go-codec-dagpb v1.5.0/go.mod h1:0yRIutEFD8o1DGVqw4RSHh+BUTlJA9XWldxaaWR/o4g= github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= +github.com/ipld/go-ipld-prime v0.9.0/go.mod h1:KvBLMr4PX1gWptgkzRjVZCrLmSGcZCb/jioOQwCqZN8= github.com/ipld/go-ipld-prime v0.9.1-0.20210324083106-dc342a9917db/go.mod h1:KvBLMr4PX1gWptgkzRjVZCrLmSGcZCb/jioOQwCqZN8= github.com/ipld/go-ipld-prime v0.11.0/go.mod h1:+WIAkokurHmZ/KwzDOMUuoeJgaRQktHtEaLglS3ZeV8= github.com/ipld/go-ipld-prime v0.14.0/go.mod h1:9ASQLwUFLptCov6lIYc70GRB4V7UTyLD0IJtrDJe6ZM= -github.com/ipld/go-ipld-prime v0.20.0 h1:Ud3VwE9ClxpO2LkCYP7vWPc0Fo+dYdYzgxUJZ3uRG4g= +github.com/ipld/go-ipld-prime v0.16.0/go.mod h1:axSCuOCBPqrH+gvXr2w9uAOulJqBPhHPT2PjoiiU1qA= +github.com/ipld/go-ipld-prime v0.18.0/go.mod h1:735yXW548CKrLwVCYXzqx90p5deRJMVVxM9eJ4Qe+qE= +github.com/ipld/go-ipld-prime v0.19.0/go.mod h1:Q9j3BaVXwaA3o5JUDNvptDDr/x8+F7FG6XJ8WI3ILg4= github.com/ipld/go-ipld-prime v0.20.0/go.mod h1:PzqZ/ZR981eKbgdr3y2DJYeD/8bgMawdGVlJDE8kK+M= -github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20211210234204-ce2a1c70cd73 h1:TsyATB2ZRRQGTwafJdgEUQkmjOExRV0DNokcihZxbnQ= +github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= +github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20211210234204-ce2a1c70cd73/go.mod h1:2PJ0JgxyB08t0b2WKrcuqI3di0V+5n6RS/LTUJhkoxY= +github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd h1:gMlw/MhNr2Wtp5RwGdsW23cs+yCuj9k2ON7i9MiJlRo= +github.com/ipld/go-ipld-prime/storage/bsadapter v0.0.0-20230102063945-1a409dc236dd/go.mod h1:wZ8hH8UxeryOs4kJEJaiui/s00hDSbE37OKsL47g+Sw= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= @@ -1081,9 +1304,11 @@ github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e/go.mod h1:G1CVv03EnqU1wYL2dFwXxW2An0az9JTl/ZsqXQeBlkU= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jellydator/ttlcache/v2 v2.11.1/go.mod h1:RtE5Snf0/57e+2cLWFYWCCsLas2Hy3c5Z4n14XmSvTI= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jhump/protoreflect v1.12.1-0.20220721211354-060cc04fc18b h1:izTof8BKh/nE1wrKOrloNA5q4odOarjf+Xpe+4qow98= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -1125,26 +1350,35 @@ github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.1/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/klauspost/compress v1.15.10/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= -github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.17.3 h1:qkRjuerhUU1EmXLYGkSH6EZL+vPSxIrYjLNAK4slzwA= +github.com/klauspost/compress v1.17.3/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= -github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU= +github.com/klauspost/cpuid/v2 v2.1.0/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.1.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.1.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.1/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= +github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= -github.com/klauspost/reedsolomon v1.11.1 h1:0gCWQXOB8pVe1Y5SGozDA5t2qoVxX3prsV+qHgI/Fik= -github.com/klauspost/reedsolomon v1.11.1/go.mod h1:FXLZzlJIdfqEnQLdUKWNRuMZg747hZ4oYp2Ml60Lb/k= +github.com/klauspost/reedsolomon v1.11.8 h1:s8RpUW5TK4hjr+djiOpbZJB4ksx+TdYbRH7vHQpwPOY= +github.com/klauspost/reedsolomon v1.11.8/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/koron/go-ssdp v0.0.0-20180514024734-4a0ed625a78b/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= github.com/koron/go-ssdp v0.0.0-20191105050749-2e1c40ed0b5d/go.mod h1:5Ky9EC2xfoUKUor0Hjgi2BJhCSXJfMOFlmyYrVKGQMk= github.com/koron/go-ssdp v0.0.2/go.mod h1:XoLfkAiA2KeZsYh4DbHxD7h3nR2AZNqVQOa+LJuqPYs= -github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= +github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0= +github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -1152,20 +1386,24 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.3/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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.2.1/go.mod h1:AA49e0DZ8kk5jTOOCKNuPR6oTnBS0dYiM4FW1e6jwpg= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= +github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.6 h1:jbk+ZieJ0D7EVGJYpL9QTz7/YW6UHbmdnZWYyK5cdBs= -github.com/lib/pq v1.10.6/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/libp2p/go-addr-util v0.0.1/go.mod h1:4ac6O7n9rIAKB1dnd+s8IbbMXkt+oBpzX4/+RACcnlQ= github.com/libp2p/go-addr-util v0.0.2/go.mod h1:Ecd6Fb3yIuLzq4bD7VcywcVSBtefcAwnUISBM3WG15E= github.com/libp2p/go-addr-util v0.1.0/go.mod h1:6I3ZYuFr2O/9D+SoyM0zEw0EF3YkldtTX406BpdQMqw= @@ -1185,32 +1423,44 @@ github.com/libp2p/go-flow-metrics v0.0.1/go.mod h1:Iv1GH0sG8DtYN3SVJ2eG221wMiNpZ github.com/libp2p/go-flow-metrics v0.0.3/go.mod h1:HeoSNUrOJVK1jEpDqVEiUOIXqhbnS27omG0uWU5slZs= github.com/libp2p/go-flow-metrics v0.1.0 h1:0iPhMI8PskQwzh57jB9WxIuIOQ0r+15PChFGkx3Q3WM= github.com/libp2p/go-flow-metrics v0.1.0/go.mod h1:4Xi8MX8wj5aWNDAZttg6UPmc0ZrnFNsMtpsYUClFtro= +github.com/libp2p/go-libp2p v0.1.0/go.mod h1:6D/2OBauqLUoqcADOJpn9WbKqvaM07tDw68qHM0BxUM= +github.com/libp2p/go-libp2p v0.1.1/go.mod h1:I00BRo1UuUSdpuc8Q2mN7yDF/oTUTRAX6JWpTiK9Rp8= github.com/libp2p/go-libp2p v0.6.1/go.mod h1:CTFnWXogryAHjXAKEbOf1OWY+VeAP3lDMZkfEI5sT54= github.com/libp2p/go-libp2p v0.7.0/go.mod h1:hZJf8txWeCduQRDC/WSqBGMxaTHCOYHt2xSU1ivxn0k= github.com/libp2p/go-libp2p v0.7.4/go.mod h1:oXsBlTLF1q7pxr+9w6lqzS1ILpyHsaBPniVO7zIHGMw= github.com/libp2p/go-libp2p v0.8.1/go.mod h1:QRNH9pwdbEBpx5DTJYg+qxcVaDMAz3Ee/qDKwXujH5o= +github.com/libp2p/go-libp2p v0.13.0/go.mod h1:pM0beYdACRfHO1WcJlp65WXyG2A6NqYM+t2DTVAJxMo= github.com/libp2p/go-libp2p v0.14.3/go.mod h1:d12V4PdKbpL0T1/gsUNN8DfgMuRPDX8bS2QxCZlwRH0= github.com/libp2p/go-libp2p v0.19.0/go.mod h1:Ki9jJXLO2YqrTIFxofV7Twyd3INWPT97+r8hGt7XPjI= -github.com/libp2p/go-libp2p v0.26.3 h1:6g/psubqwdaBqNNoidbRKSTBEYgaOuKBhHl8Q5tO+PM= -github.com/libp2p/go-libp2p v0.26.3/go.mod h1:x75BN32YbwuY0Awm2Uix4d4KOz+/4piInkp4Wr3yOo8= +github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= +github.com/libp2p/go-libp2p v0.23.4/go.mod h1:s9DEa5NLR4g+LZS+md5uGU4emjMWFiqkZr6hBTY8UxI= +github.com/libp2p/go-libp2p v0.25.0/go.mod h1:vXHmFpcfl+xIGN4qW58Bw3a0/SKGAesr5/T4IuJHE3o= +github.com/libp2p/go-libp2p v0.25.1/go.mod h1:xnK9/1d9+jeQCVvi/f1g12KqtVi/jP/SijtKV1hML3g= +github.com/libp2p/go-libp2p v0.32.2 h1:s8GYN4YJzgUoyeYNPdW7JZeZ5Ee31iNaIBfGYMAY4FQ= +github.com/libp2p/go-libp2p v0.32.2/go.mod h1:E0LKe+diV/ZVJVnOJby8VC5xzHF0660osg71skcxJvk= github.com/libp2p/go-libp2p-asn-util v0.1.0/go.mod h1:wu+AnM9Ii2KgO5jMmS1rz9dvzTdj8BXqsPR9HR0XB7I= -github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= +github.com/libp2p/go-libp2p-asn-util v0.3.0 h1:gMDcMyYiZKkocGXDQ5nsUQyquC9+H+iLEQHwOCZ7s8s= +github.com/libp2p/go-libp2p-asn-util v0.3.0/go.mod h1:B1mcOrKUE35Xq/ASTmQ4tN3LNzVVaMNmq2NACuqyB9w= +github.com/libp2p/go-libp2p-autonat v0.1.0/go.mod h1:1tLf2yXxiE/oKGtDwPYWTSYG3PtvYlJmg7NeVtPRqH8= github.com/libp2p/go-libp2p-autonat v0.1.1/go.mod h1:OXqkeGOY2xJVWKAGV2inNF5aKN/djNA3fdpCWloIudE= github.com/libp2p/go-libp2p-autonat v0.2.0/go.mod h1:DX+9teU4pEEoZUqR1PiMlqliONQdNbfzE1C718tcViI= github.com/libp2p/go-libp2p-autonat v0.2.1/go.mod h1:MWtAhV5Ko1l6QBsHQNSuM6b1sRkXrpk0/LqCr+vCVxI= github.com/libp2p/go-libp2p-autonat v0.2.2/go.mod h1:HsM62HkqZmHR2k1xgX34WuWDzk/nBwNHoeyyT4IWV6A= +github.com/libp2p/go-libp2p-autonat v0.4.0/go.mod h1:YxaJlpr81FhdOv3W3BTconZPfhaYivRdf53g+S2wobk= github.com/libp2p/go-libp2p-autonat v0.4.2/go.mod h1:YxaJlpr81FhdOv3W3BTconZPfhaYivRdf53g+S2wobk= github.com/libp2p/go-libp2p-blankhost v0.1.1/go.mod h1:pf2fvdLJPsC1FsVrNP3DUUvMzUts2dsLLBEpo1vW1ro= github.com/libp2p/go-libp2p-blankhost v0.1.4/go.mod h1:oJF0saYsAXQCSfDq254GMNmLNz6ZTHTOvtF4ZydUvwU= github.com/libp2p/go-libp2p-blankhost v0.2.0/go.mod h1:eduNKXGTioTuQAUcZ5epXi9vMl+t4d8ugUBRQ4SqaNQ= github.com/libp2p/go-libp2p-blankhost v0.3.0/go.mod h1:urPC+7U01nCGgJ3ZsV8jdwTp6Ji9ID0dMTvq+aJ+nZU= +github.com/libp2p/go-libp2p-circuit v0.1.0/go.mod h1:Ahq4cY3V9VJcHcn1SBXjr78AbFkZeIRmfunbA7pmFh8= github.com/libp2p/go-libp2p-circuit v0.1.4/go.mod h1:CY67BrEjKNDhdTk8UgBX1Y/H5c3xkAcs3gnksxY7osU= github.com/libp2p/go-libp2p-circuit v0.2.1/go.mod h1:BXPwYDN5A8z4OEY9sOfr2DUQMLQvKt/6oku45YUmjIo= github.com/libp2p/go-libp2p-circuit v0.4.0/go.mod h1:t/ktoFIUzM6uLQ+o1G6NuBl2ANhBKN9Bc8jRIk31MoA= github.com/libp2p/go-libp2p-circuit v0.6.0/go.mod h1:kB8hY+zCpMeScyvFrKrGicRdid6vNXbunKE4rXATZ0M= github.com/libp2p/go-libp2p-core v0.0.1/go.mod h1:g/VxnTZ/1ygHxH3dKok7Vno1VfpvGcGip57wjTU4fco= github.com/libp2p/go-libp2p-core v0.0.2/go.mod h1:9dAcntw/n46XycV4RnlBq3BpgrmyUi9LuoTNdPrbUco= +github.com/libp2p/go-libp2p-core v0.0.3/go.mod h1:j+YQMNz9WNSkNezXOsahp9kwZBKBvxLpKD316QWSJXE= github.com/libp2p/go-libp2p-core v0.0.4/go.mod h1:jyuCQP356gzfCFtRKyvAbNkyeuxb7OlyhWZ3nls5d2I= github.com/libp2p/go-libp2p-core v0.2.0/go.mod h1:X0eyB0Gy93v0DZtSYbEM7RnMChm9Uv3j7yRXjO77xSI= github.com/libp2p/go-libp2p-core v0.2.2/go.mod h1:8fcwTbsG2B+lTgRJ1ICZtiM5GWCWZVoVrLaDRvIRng0= @@ -1236,14 +1486,20 @@ github.com/libp2p/go-libp2p-core v0.11.0/go.mod h1:ECdxehoYosLYHgDDFa2N4yE8Y7aQR github.com/libp2p/go-libp2p-core v0.12.0/go.mod h1:ECdxehoYosLYHgDDFa2N4yE8Y7aQRAMf0sX9mf2sbGg= github.com/libp2p/go-libp2p-core v0.14.0/go.mod h1:tLasfcVdTXnixsLB0QYaT1syJOhsbrhG7q6pGrHtBg8= github.com/libp2p/go-libp2p-core v0.15.1/go.mod h1:agSaboYM4hzB1cWekgVReqV5M4g5M+2eNNejV+1EEhs= +github.com/libp2p/go-libp2p-core v0.19.0/go.mod h1:AkA+FUKQfYt1FLNef5fOPlo/naAWjKy/RCjkcPjqzYg= github.com/libp2p/go-libp2p-crypto v0.1.0/go.mod h1:sPUokVISZiy+nNuTTH/TY+leRSxnFj/2GLjtOTW90hI= +github.com/libp2p/go-libp2p-discovery v0.1.0/go.mod h1:4F/x+aldVHjHDHuX85x1zWoFTGElt8HnoDzwkFZm29g= github.com/libp2p/go-libp2p-discovery v0.2.0/go.mod h1:s4VGaxYMbw4+4+tsoQTqh7wfxg97AEdo4GYBt6BadWg= github.com/libp2p/go-libp2p-discovery v0.3.0/go.mod h1:o03drFnz9BVAZdzC/QUQ+NeQOu38Fu7LJGEOK2gQltw= github.com/libp2p/go-libp2p-discovery v0.5.0/go.mod h1:+srtPIU9gDaBNu//UHvcdliKBIcr4SfDcm0/PfPJLug= -github.com/libp2p/go-libp2p-kad-dht v0.21.0 h1:J0Yd22VA+sk0CJRGMgtfHvLVIkZDyJ3AJGiljywIw5U= +github.com/libp2p/go-libp2p-kad-dht v0.19.0/go.mod h1:qPIXdiZsLczhV4/+4EO1jE8ae0YCW4ZOogc4WVIyTEU= github.com/libp2p/go-libp2p-kad-dht v0.21.0/go.mod h1:Bhm9diAFmc6qcWAr084bHNL159srVZRKADdp96Qqd1I= -github.com/libp2p/go-libp2p-kbucket v0.5.0 h1:g/7tVm8ACHDxH29BGrpsQlnNeu+6OF1A9bno/4/U1oA= +github.com/libp2p/go-libp2p-kad-dht v0.25.2 h1:FOIk9gHoe4YRWXTu8SY9Z1d0RILol0TrtApsMDPjAVQ= +github.com/libp2p/go-libp2p-kad-dht v0.25.2/go.mod h1:6za56ncRHYXX4Nc2vn8z7CZK0P4QiMcrn77acKLM2Oo= +github.com/libp2p/go-libp2p-kbucket v0.3.1/go.mod h1:oyjT5O7tS9CQurok++ERgc46YLwEpuGoFq9ubvoUOio= github.com/libp2p/go-libp2p-kbucket v0.5.0/go.mod h1:zGzGCpQd78b5BNTDGHNDLaTt9aDK/A02xeZp9QeFC4U= +github.com/libp2p/go-libp2p-kbucket v0.6.3 h1:p507271wWzpy2f1XxPzCQG9NiN6R6lHL9GiSErbQQo0= +github.com/libp2p/go-libp2p-kbucket v0.6.3/go.mod h1:RCseT7AH6eJWxxk2ol03xtP9pEHetYSPXOaJnOiD8i0= github.com/libp2p/go-libp2p-loggables v0.1.0/go.mod h1:EyumB2Y6PrYjr55Q3/tiJ/o3xoDasoRYM7nOzEpoa90= github.com/libp2p/go-libp2p-mplex v0.2.0/go.mod h1:Ejl9IyjvXJ0T9iqUTE1jpYATQ9NM3g+OtR+EMMODbKo= github.com/libp2p/go-libp2p-mplex v0.2.1/go.mod h1:SC99Rxs8Vuzrf/6WhmH41kNn13TiYdAWNYHrwImKLnE= @@ -1252,15 +1508,18 @@ github.com/libp2p/go-libp2p-mplex v0.2.3/go.mod h1:CK3p2+9qH9x+7ER/gWWDYJ3QW5ZxW github.com/libp2p/go-libp2p-mplex v0.4.0/go.mod h1:yCyWJE2sc6TBTnFpjvLuEJgTSw/u+MamvzILKdX7asw= github.com/libp2p/go-libp2p-mplex v0.4.1/go.mod h1:cmy+3GfqfM1PceHTLL7zQzAAYaryDu6iPSC+CIb094g= github.com/libp2p/go-libp2p-mplex v0.5.0/go.mod h1:eLImPJLkj3iG5t5lq68w3Vm5NAQ5BcKwrrb2VmOYb3M= +github.com/libp2p/go-libp2p-nat v0.0.4/go.mod h1:N9Js/zVtAXqaeT99cXgTV9e75KpnWCvVOiGzlcHmBbY= github.com/libp2p/go-libp2p-nat v0.0.5/go.mod h1:1qubaE5bTZMJE+E/uu2URroMbzdubFz1ChgiN79yKPE= github.com/libp2p/go-libp2p-nat v0.0.6/go.mod h1:iV59LVhB3IkFvS6S6sauVTSOrNEANnINbI/fkaLimiw= github.com/libp2p/go-libp2p-nat v0.1.0/go.mod h1:DQzAG+QbDYjN1/C3B6vXucLtz3u9rEonLVPtZVzQqks= github.com/libp2p/go-libp2p-netutil v0.1.0/go.mod h1:3Qv/aDqtMLTUyQeundkKsA+YCThNdbQD54k3TqjpbFU= +github.com/libp2p/go-libp2p-noise v0.1.1/go.mod h1:QDFLdKX7nluB7DEnlVPbz7xlLHdwHFA9HiohJRr3vwM= github.com/libp2p/go-libp2p-noise v0.2.0/go.mod h1:IEbYhBBzGyvdLBoxxULL/SGbJARhUeqlO8lVSREYu2Q= github.com/libp2p/go-libp2p-noise v0.4.0/go.mod h1:BzzY5pyzCYSyJbQy9oD8z5oP2idsafjt4/X42h9DjZU= github.com/libp2p/go-libp2p-peer v0.2.0/go.mod h1:RCffaCvUyW2CJmG2gAWVqwePwW7JMgxjsHm7+J5kjWY= github.com/libp2p/go-libp2p-peerstore v0.1.0/go.mod h1:2CeHkQsr8svp4fZ+Oi9ykN1HBb6u0MOvdJ7YIsmcwtY= github.com/libp2p/go-libp2p-peerstore v0.1.3/go.mod h1:BJ9sHlm59/80oSkpWgr1MyY1ciXAXV397W6h1GH/uKI= +github.com/libp2p/go-libp2p-peerstore v0.1.4/go.mod h1:+4BDbDiiKf4PzpANZDAT+knVdLxvqh7hXOujessqdzs= github.com/libp2p/go-libp2p-peerstore v0.2.0/go.mod h1:N2l3eVIeAitSg3Pi2ipSrJYnqhVnMNQZo9nkSCuAbnQ= github.com/libp2p/go-libp2p-peerstore v0.2.1/go.mod h1:NQxhNjWxf1d4w6PihR8btWIRjwRLBr4TYKfNgrUkOPA= github.com/libp2p/go-libp2p-peerstore v0.2.2/go.mod h1:NQxhNjWxf1d4w6PihR8btWIRjwRLBr4TYKfNgrUkOPA= @@ -1269,8 +1528,8 @@ github.com/libp2p/go-libp2p-peerstore v0.2.7/go.mod h1:ss/TWTgHZTMpsU/oKVVPQCGuD github.com/libp2p/go-libp2p-peerstore v0.4.0/go.mod h1:rDJUFyzEWPpXpEwywkcTYYzDHlwza8riYMaUzaN6hX0= github.com/libp2p/go-libp2p-peerstore v0.6.0/go.mod h1:DGEmKdXrcYpK9Jha3sS7MhqYdInxJy84bIPtSu65bKc= github.com/libp2p/go-libp2p-pnet v0.2.0/go.mod h1:Qqvq6JH/oMZGwqs3N1Fqhv8NVhrdYcO0BW4wssv21LA= -github.com/libp2p/go-libp2p-pubsub v0.9.3 h1:ihcz9oIBMaCK9kcx+yHWm3mLAFBMAUsM4ux42aikDxo= -github.com/libp2p/go-libp2p-pubsub v0.9.3/go.mod h1:RYA7aM9jIic5VV47WXu4GkcRxRhrdElWf8xtyli+Dzc= +github.com/libp2p/go-libp2p-pubsub v0.10.0 h1:wS0S5FlISavMaAbxyQn3dxMOe2eegMfswM471RuHJwA= +github.com/libp2p/go-libp2p-pubsub v0.10.0/go.mod h1:1OxbaT/pFRO5h+Dpze8hdHQ63R0ke55XTs6b6NwLLkw= github.com/libp2p/go-libp2p-quic-transport v0.10.0/go.mod h1:RfJbZ8IqXIhxBRm5hqUEJqjiiY8xmEuq3HUDS993MkA= github.com/libp2p/go-libp2p-quic-transport v0.13.0/go.mod h1:39/ZWJ1TW/jx1iFkKzzUg00W6tDJh73FC0xYudjr7Hc= github.com/libp2p/go-libp2p-quic-transport v0.16.0/go.mod h1:1BXjVMzr+w7EkPfiHkKnwsWjPjtfaNT0q8RS3tGDvEQ= @@ -1279,8 +1538,9 @@ github.com/libp2p/go-libp2p-record v0.1.0/go.mod h1:ujNc8iuE5dlKWVy6wuL6dd58t0n7 github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= github.com/libp2p/go-libp2p-resource-manager v0.2.1/go.mod h1:K+eCkiapf+ey/LADO4TaMpMTP9/Qde/uLlrnRqV4PLQ= -github.com/libp2p/go-libp2p-routing-helpers v0.6.1 h1:tI3rHOf/FDQsxC2pHBaOZiqPJ0MZYyzGAf4V45xla4U= -github.com/libp2p/go-libp2p-routing-helpers v0.6.1/go.mod h1:R289GUxUMzRXIbWGSuUUTPrlVJZ3Y/pPz495+qgXJX8= +github.com/libp2p/go-libp2p-routing-helpers v0.4.0/go.mod h1:dYEAgkVhqho3/YKxfOEGdFMIcWfAFNlZX8iAIihYA2E= +github.com/libp2p/go-libp2p-routing-helpers v0.7.3 h1:u1LGzAMVRK9Nqq5aYDVOiq/HaB93U9WWczBzGyAC5ZY= +github.com/libp2p/go-libp2p-routing-helpers v0.7.3/go.mod h1:cN4mJAD/7zfPKXBcs9ze31JGYAZgzdABEm+q/hkswb8= github.com/libp2p/go-libp2p-secio v0.1.0/go.mod h1:tMJo2w7h3+wN4pgU2LSYeiKPrfqBgkOsdiKK77hE7c8= github.com/libp2p/go-libp2p-secio v0.2.0/go.mod h1:2JdZepB8J5V9mBp79BmwsaPQhRPNN2NrnB2lKQcdy6g= github.com/libp2p/go-libp2p-secio v0.2.1/go.mod h1:cWtZpILJqkqrSkiYcDBh5lA3wbT2Q+hz3rJQq3iftD8= @@ -1290,6 +1550,7 @@ github.com/libp2p/go-libp2p-swarm v0.2.2/go.mod h1:fvmtQ0T1nErXym1/aa1uJEyN7JzaT github.com/libp2p/go-libp2p-swarm v0.2.3/go.mod h1:P2VO/EpxRyDxtChXz/VPVXyTnszHvokHKRhfkEgFKNM= github.com/libp2p/go-libp2p-swarm v0.2.8/go.mod h1:JQKMGSth4SMqonruY0a8yjlPVIkb0mdNSwckW7OYziM= github.com/libp2p/go-libp2p-swarm v0.3.0/go.mod h1:hdv95GWCTmzkgeJpP+GK/9D9puJegb7H57B5hWQR5Kk= +github.com/libp2p/go-libp2p-swarm v0.4.0/go.mod h1:XVFcO52VoLoo0eitSxNQWYq4D6sydGOweTOAjJNraCw= github.com/libp2p/go-libp2p-swarm v0.5.0/go.mod h1:sU9i6BoHE0Ve5SKz3y9WfKrh8dUat6JknzUehFx8xW4= github.com/libp2p/go-libp2p-swarm v0.8.0/go.mod h1:sOMp6dPuqco0r0GHTzfVheVBh6UEL0L1lXUZ5ot2Fvc= github.com/libp2p/go-libp2p-swarm v0.10.0/go.mod h1:71ceMcV6Rg/0rIQ97rsZWMzto1l9LnNquef+efcRbmA= @@ -1306,24 +1567,30 @@ github.com/libp2p/go-libp2p-testing v0.5.0/go.mod h1:QBk8fqIL1XNcno/l3/hhaIEn4aL github.com/libp2p/go-libp2p-testing v0.7.0/go.mod h1:OLbdn9DbgdMwv00v+tlp1l3oe2Cl+FAjoWIA2pa0X6E= github.com/libp2p/go-libp2p-testing v0.9.0/go.mod h1:Td7kbdkWqYTJYQGTwzlgXwaqldraIanyjuRiAbK/XQU= github.com/libp2p/go-libp2p-testing v0.9.2/go.mod h1:Td7kbdkWqYTJYQGTwzlgXwaqldraIanyjuRiAbK/XQU= +github.com/libp2p/go-libp2p-testing v0.11.0/go.mod h1:qG4sF27dfKFoK9KlVzK2y52LQKhp0VEmLjV5aDqr1Hg= 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-libp2p-tls v0.1.3/go.mod h1:wZfuewxOndz5RTnCAxFliGjvYSDA40sKitV4c50uI1M= github.com/libp2p/go-libp2p-tls v0.3.0/go.mod h1:fwF5X6PWGxm6IDRwF3V8AVCCj/hOd5oFlg+wo2FxJDY= github.com/libp2p/go-libp2p-tls v0.4.1/go.mod h1:EKCixHEysLNDlLUoKxv+3f/Lp90O2EXNjTr0UQDnrIw= github.com/libp2p/go-libp2p-transport-upgrader v0.1.1/go.mod h1:IEtA6or8JUbsV07qPW4r01GnTenLW4oi3lOPbUMGJJA= github.com/libp2p/go-libp2p-transport-upgrader v0.2.0/go.mod h1:mQcrHj4asu6ArfSoMuyojOdjx73Q47cYD7s5+gZOlns= github.com/libp2p/go-libp2p-transport-upgrader v0.3.0/go.mod h1:i+SKzbRnvXdVbU3D1dwydnTmKRPXiAR/fyvi1dXuL4o= +github.com/libp2p/go-libp2p-transport-upgrader v0.4.0/go.mod h1:J4ko0ObtZSmgn5BX5AmegP+dK3CSnU2lMCKsSq/EY0s= github.com/libp2p/go-libp2p-transport-upgrader v0.4.2/go.mod h1:NR8ne1VwfreD5VIWIU62Agt/J18ekORFU/j1i2y8zvk= github.com/libp2p/go-libp2p-transport-upgrader v0.5.0/go.mod h1:Rc+XODlB3yce7dvFV4q/RmyJGsFcCZRkeZMu/Zdg0mo= github.com/libp2p/go-libp2p-transport-upgrader v0.7.0/go.mod h1:GIR2aTRp1J5yjVlkUoFqMkdobfob6RnAwYg/RZPhrzg= github.com/libp2p/go-libp2p-transport-upgrader v0.7.1/go.mod h1:GIR2aTRp1J5yjVlkUoFqMkdobfob6RnAwYg/RZPhrzg= +github.com/libp2p/go-libp2p-xor v0.1.0/go.mod h1:LSTM5yRnjGZbWNTA/hRwq2gGFrvRIbQJscoIL/u6InY= github.com/libp2p/go-libp2p-yamux v0.2.0/go.mod h1:Db2gU+XfLpm6E4rG5uGCFX6uXA8MEXOxFcRoXUODaK8= +github.com/libp2p/go-libp2p-yamux v0.2.1/go.mod h1:1FBXiHDk1VyRM1C0aez2bCfHQ4vMZKkAQzZbkSQt5fI= github.com/libp2p/go-libp2p-yamux v0.2.2/go.mod h1:lIohaR0pT6mOt0AZ0L2dFze9hds9Req3OfS+B+dv4qw= github.com/libp2p/go-libp2p-yamux v0.2.5/go.mod h1:Zpgj6arbyQrmZ3wxSZxfBmbdnWtbZ48OpsfmQVTErwA= github.com/libp2p/go-libp2p-yamux v0.2.7/go.mod h1:X28ENrBMU/nm4I3Nx4sZ4dgjZ6VhLEn0XhIoZ5viCwU= github.com/libp2p/go-libp2p-yamux v0.2.8/go.mod h1:/t6tDqeuZf0INZMTgd0WxIRbtK2EzI2h7HbFm9eAKI4= github.com/libp2p/go-libp2p-yamux v0.4.0/go.mod h1:+DWDjtFMzoAwYLVkNZftoucn7PelNoy5nm3tZ3/Zw30= github.com/libp2p/go-libp2p-yamux v0.5.0/go.mod h1:AyR8k5EzyM2QN9Bbdg6X1SkVVuqLwTGf0L4DFq9g6po= +github.com/libp2p/go-libp2p-yamux v0.5.1/go.mod h1:dowuvDu8CRWmr0iqySMiSxK+W0iL5cMVO9S94Y6gkv4= github.com/libp2p/go-libp2p-yamux v0.5.4/go.mod h1:tfrXbyaTqqSU654GTvK3ocnSZL3BuHoeTSqhcel1wsE= github.com/libp2p/go-libp2p-yamux v0.8.0/go.mod h1:yTkPgN2ib8FHyU1ZcVD7aelzyAqXXwEPbyx+aSKm9h8= github.com/libp2p/go-libp2p-yamux v0.8.1/go.mod h1:rUozF8Jah2dL9LLGyBaBeTQeARdwhefMCTQVQt6QobE= @@ -1338,16 +1605,20 @@ github.com/libp2p/go-mplex v0.1.2/go.mod h1:Xgz2RDCi3co0LeZfgjm4OgUF15+sVR8SRcu3 github.com/libp2p/go-mplex v0.2.0/go.mod h1:0Oy/A9PQlwBytDRp4wSkFnzHYDKcpLot35JQ6msjvYQ= github.com/libp2p/go-mplex v0.3.0/go.mod h1:0Oy/A9PQlwBytDRp4wSkFnzHYDKcpLot35JQ6msjvYQ= github.com/libp2p/go-mplex v0.4.0/go.mod h1:y26Lx+wNVtMYMaPu300Cbot5LkEZ4tJaNYeHeT9dh6E= +github.com/libp2p/go-mplex v0.7.0/go.mod h1:rW8ThnRcYWft/Jb2jeORBmPd6xuG3dGxWN/W168L9EU= github.com/libp2p/go-msgio v0.0.2/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ= +github.com/libp2p/go-msgio v0.0.3/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ= github.com/libp2p/go-msgio v0.0.4/go.mod h1:63lBBgOTDKQL6EWazRMCwXsEeEeK9O2Cd+0+6OOuipQ= github.com/libp2p/go-msgio v0.0.6/go.mod h1:4ecVB6d9f4BDSL5fqvPiC4A3KivjWn+Venn/1ALLMWA= github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= 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-nat v0.0.3/go.mod h1:88nUEt0k0JD45Bk93NIwDqjlhiOwOoV36GchpcVc1yI= github.com/libp2p/go-nat v0.0.4/go.mod h1:Nmw50VAvKuk38jUBcmNh6p9lUJLoODbJRvYAa/+KSDo= github.com/libp2p/go-nat v0.0.5/go.mod h1:B7NxsVNPZmRLvMOwiEO1scOSyjA56zxYAGv1yQgRkEU= -github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= +github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk= +github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk= github.com/libp2p/go-netroute v0.1.2/go.mod h1:jZLDV+1PE8y5XxBySEBgbuVAXbhtuHSdmLPL2n9MKbk= github.com/libp2p/go-netroute v0.1.3/go.mod h1:jZLDV+1PE8y5XxBySEBgbuVAXbhtuHSdmLPL2n9MKbk= github.com/libp2p/go-netroute v0.1.5/go.mod h1:V1SR3AaECRkEQCoFFzYwVYWvYIEtlxx89+O3qcpCl4A= @@ -1360,11 +1631,13 @@ github.com/libp2p/go-openssl v0.0.3/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO github.com/libp2p/go-openssl v0.0.4/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc= github.com/libp2p/go-openssl v0.0.5/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc= github.com/libp2p/go-openssl v0.0.7/go.mod h1:unDrJpgy3oFr+rqXsarWifmJuNnJR4chtO1HmaZjggc= +github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= github.com/libp2p/go-reuseport v0.0.1/go.mod h1:jn6RmB1ufnQwl0Q1f+YxAj8isJgDCQzaaxIFYDhcYEA= github.com/libp2p/go-reuseport v0.0.2/go.mod h1:SPD+5RwGC7rcnzngoYC86GjPzjSywuQyMVAheVBD9nQ= github.com/libp2p/go-reuseport v0.1.0/go.mod h1:bQVn9hmfcTaoo0c9v5pBhOarsU1eNOBZdaAd2hzXRKU= -github.com/libp2p/go-reuseport v0.2.0 h1:18PRvIMlpY6ZK85nIAicSBuXXvrYoSw3dsBAR7zc560= github.com/libp2p/go-reuseport v0.2.0/go.mod h1:bvVho6eLMm6Bz5hmU0LYN3ixd3nPPvtIlaURZZgOY4k= +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-reuseport-transport v0.0.2/go.mod h1:YkbSDrvjUVDL6b8XqriyA20obEtsW9BLkuOUyQAOCbs= github.com/libp2p/go-reuseport-transport v0.0.3/go.mod h1:Spv+MPft1exxARzP2Sruj2Wb5JSyHNncjf1Oi2dEbzM= github.com/libp2p/go-reuseport-transport v0.0.4/go.mod h1:trPa7r/7TJK/d+0hdBLOCGvpQQVOU74OXbNCIMkufGw= @@ -1379,47 +1652,60 @@ github.com/libp2p/go-stream-muxer-multistream v0.4.0/go.mod h1:nb+dGViZleRP4XcyH github.com/libp2p/go-tcp-transport v0.1.0/go.mod h1:oJ8I5VXryj493DEJ7OsBieu8fcg2nHGctwtInJVpipc= github.com/libp2p/go-tcp-transport v0.1.1/go.mod h1:3HzGvLbx6etZjnFlERyakbaYPdfjg2pWP97dFZworkY= github.com/libp2p/go-tcp-transport v0.2.0/go.mod h1:vX2U0CnWimU4h0SGSEsg++AzvBcroCGYw28kh94oLe0= +github.com/libp2p/go-tcp-transport v0.2.1/go.mod h1:zskiJ70MEfWz2MKxvFB/Pv+tPIB1PpPUrHIWQ8aFw7M= github.com/libp2p/go-tcp-transport v0.2.3/go.mod h1:9dvr03yqrPyYGIEN6Dy5UvdJZjyPFvl1S/igQ5QD1SU= github.com/libp2p/go-tcp-transport v0.4.0/go.mod h1:0y52Rwrn4076xdJYu/51/qJIdxz+EWDAOG2S45sV3VI= github.com/libp2p/go-tcp-transport v0.5.0/go.mod h1:UPPL0DIjQqiWRwVAb+CEQlaAG0rp/mCqJfIhFcLHc4Y= github.com/libp2p/go-tcp-transport v0.5.1/go.mod h1:UPPL0DIjQqiWRwVAb+CEQlaAG0rp/mCqJfIhFcLHc4Y= +github.com/libp2p/go-testutil v0.1.0/go.mod h1:81b2n5HypcVyrCg/MJx4Wgfp/VHojytjVe/gLzZ2Ehc= +github.com/libp2p/go-ws-transport v0.1.0/go.mod h1:rjw1MG1LU9YDC6gzmwObkPd/Sqwhw7yT74kj3raBFuo= github.com/libp2p/go-ws-transport v0.2.0/go.mod h1:9BHJz/4Q5A9ludYWKoGCFC5gUElzlHoKzu0yY9p/klM= github.com/libp2p/go-ws-transport v0.3.0/go.mod h1:bpgTJmRZAvVHrgHybCVyqoBmyLQ1fiZuEaBYusP5zsk= github.com/libp2p/go-ws-transport v0.4.0/go.mod h1:EcIEKqf/7GDjth6ksuS/6p7R49V4CBY6/E7R/iyhYUA= github.com/libp2p/go-ws-transport v0.6.0/go.mod h1:dXqtI9e2JV9FtF1NOtWVZSKXh5zXvnuwPXfj8GPBbYU= github.com/libp2p/go-yamux v1.2.2/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow= +github.com/libp2p/go-yamux v1.2.3/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow= github.com/libp2p/go-yamux v1.3.0/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow= github.com/libp2p/go-yamux v1.3.3/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow= github.com/libp2p/go-yamux v1.3.5/go.mod h1:FGTiPvoV/3DVdgWpX+tM0OW3tsM+W5bSE3gZwqQTcow= github.com/libp2p/go-yamux v1.3.7/go.mod h1:fr7aVgmdNGJK+N1g+b6DW6VxzbRCjCOejR/hkmpooHE= github.com/libp2p/go-yamux v1.4.0/go.mod h1:fr7aVgmdNGJK+N1g+b6DW6VxzbRCjCOejR/hkmpooHE= github.com/libp2p/go-yamux v1.4.1/go.mod h1:fr7aVgmdNGJK+N1g+b6DW6VxzbRCjCOejR/hkmpooHE= +github.com/libp2p/go-yamux/v2 v2.0.0/go.mod h1:NVWira5+sVUIU6tu1JWvaRn1dRnG+cawOJiflsAM+7U= github.com/libp2p/go-yamux/v2 v2.2.0/go.mod h1:3So6P6TV6r75R9jiBpiIKgU/66lOarCZjqROGxzPpPQ= github.com/libp2p/go-yamux/v3 v3.0.1/go.mod h1:s2LsDhHbh+RfCsQoICSYt58U2f8ijtPANFD8BmE74Bo= github.com/libp2p/go-yamux/v3 v3.0.2/go.mod h1:s2LsDhHbh+RfCsQoICSYt58U2f8ijtPANFD8BmE74Bo= github.com/libp2p/go-yamux/v3 v3.1.1/go.mod h1:jeLEQgLXqE2YqX1ilAClIfCMDY+0uXQUKmmb/qp0gT4= -github.com/libp2p/go-yamux/v4 v4.0.0 h1:+Y80dV2Yx/kv7Y7JKu0LECyVdMXm1VUoko+VQ9rBfZQ= +github.com/libp2p/go-yamux/v3 v3.1.2/go.mod h1:jeLEQgLXqE2YqX1ilAClIfCMDY+0uXQUKmmb/qp0gT4= github.com/libp2p/go-yamux/v4 v4.0.0/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= +github.com/libp2p/go-yamux/v4 v4.0.1 h1:FfDR4S1wj6Bw2Pqbc8Uz7pCxeRBPbwsBbEdfwiCypkQ= +github.com/libp2p/go-yamux/v4 v4.0.1/go.mod h1:NWjl8ZTLOGlozrXSOZ/HlfG++39iKNnM5wwmtQP1YB4= github.com/libp2p/zeroconf/v2 v2.1.1/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= +github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/linxGnu/grocksdb v1.8.12 h1:1/pCztQUOa3BX/1gR3jSZDoaKFpeHFvQ1XrqZpSvZVo= +github.com/linxGnu/grocksdb v1.8.12/go.mod h1:xZCIb5Muw+nhbDK4Y5UJuOrin5MceOuiXkVUR7vp4WY= github.com/lucas-clemente/quic-go v0.19.3/go.mod h1:ADXpNbTQjq1hIzCpB+y/k5iz4n4z4IwqoLb94Kh5Hu8= github.com/lucas-clemente/quic-go v0.23.0/go.mod h1:paZuzjXCE5mj6sikVLMvqXk8lJV2AsqtJ6bDhjEfxx0= github.com/lucas-clemente/quic-go v0.25.0/go.mod h1:YtzP8bxRVCBlO77yRanE264+fY/T2U9ZlW1AaHOsMOg= github.com/lucas-clemente/quic-go v0.27.0/go.mod h1:AzgQoPda7N+3IqMMMkywBKggIFo2KT6pfnlrQ2QieeI= +github.com/lucas-clemente/quic-go v0.28.1/go.mod h1:oGz5DKK41cJt5+773+BSO9BXDsREY4HLf7+0odGAPO0= +github.com/lucas-clemente/quic-go v0.29.1/go.mod h1:CTcNfLYJS2UuRNB+zcNlgvkjBhxX6Hm3WUxxAQx2mgE= github.com/lucasjones/reggen v0.0.0-20180717132126-cdb49ff09d77/go.mod h1:5ELEyG+X8f+meRWHuqUOewBOhvHkl7M76pdGEansxW4= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.1/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= github.com/marten-seemann/qpack v0.2.1/go.mod h1:F7Gl5L1jIgN1D11ucXefiuJS9UMVP2opoCp2jDKb7wc= @@ -1430,10 +1716,15 @@ github.com/marten-seemann/qtls-go1-16 v0.1.4/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZE github.com/marten-seemann/qtls-go1-16 v0.1.5/go.mod h1:gNpI2Ol+lRS3WwSOtIUUtRwZEQMXjYK+dQSBFbethAk= github.com/marten-seemann/qtls-go1-17 v0.1.0/go.mod h1:fz4HIxByo+LlWcreM4CZOYNuz3taBQ8rN2X6FqvaWo8= github.com/marten-seemann/qtls-go1-17 v0.1.1/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= +github.com/marten-seemann/qtls-go1-17 v0.1.2/go.mod h1:C2ekUKcDdz9SDWxec1N/MvcXBpaX9l3Nx67XaR84L5s= github.com/marten-seemann/qtls-go1-18 v0.1.0-beta.1/go.mod h1:PUhIQk19LoFt2174H4+an8TYvWOGjb/hHwphBeaDHwI= github.com/marten-seemann/qtls-go1-18 v0.1.1/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/marten-seemann/qtls-go1-18 v0.1.2/go.mod h1:mJttiymBAByA49mhlNZZGrH5u1uXYZJ+RW28Py7f4m4= +github.com/marten-seemann/qtls-go1-19 v0.1.0-beta.1/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= +github.com/marten-seemann/qtls-go1-19 v0.1.0/go.mod h1:5HTDWtVudo/WFsHKRNuOhWlbdjrfs5JHrYb0wIJqGpI= 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/marten-seemann/webtransport-go v0.1.1/go.mod h1:kBEh5+RSvOA4troP1vyOVBWK4MIMzDICXVrvCPrYcrM= github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= @@ -1441,7 +1732,6 @@ github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= @@ -1453,28 +1743,35 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= +github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.12/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.28/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= +github.com/miekg/dns v1.1.31/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.48/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= +github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY= 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= @@ -1492,8 +1789,9 @@ github.com/minio/sha256-simd v0.0.0-20190328051042-05b4dd3047e5/go.mod h1:2FMWW+ github.com/minio/sha256-simd v0.1.0/go.mod h1:2FMWW+8GMoPweT6+pI63m9YE3Lmw4J71hV56Chs1E/U= github.com/minio/sha256-simd v0.1.1-0.20190913151208-6de447530771/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= -github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +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/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -1511,6 +1809,9 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= +github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= +github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1549,8 +1850,11 @@ github.com/multiformats/go-multiaddr v0.3.3/go.mod h1:lCKNGP1EQ1eZ35Za2wlqnabm9x github.com/multiformats/go-multiaddr v0.4.0/go.mod h1:YcpyLH8ZPudLxQlemYBPhSm0/oCXAT8Z4mzFpyoPyRc= github.com/multiformats/go-multiaddr v0.4.1/go.mod h1:3afI9HfVW8csiF8UZqtpYRiDyew8pRX7qLIGHu9FLuM= github.com/multiformats/go-multiaddr v0.5.0/go.mod h1:3KAxNkUqLTJ20AAwN4XVX4kZar+bR+gh4zgbfr3SNug= -github.com/multiformats/go-multiaddr v0.8.0 h1:aqjksEcqK+iD/Foe1RRFsGZh8+XFiGo7FgUCZlpv3LU= +github.com/multiformats/go-multiaddr v0.6.0/go.mod h1:F4IpaKZuPP360tOMn2Tpyu0At8w23aRyVqeK0DbFeGM= +github.com/multiformats/go-multiaddr v0.7.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs= github.com/multiformats/go-multiaddr v0.8.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs= +github.com/multiformats/go-multiaddr v0.12.2 h1:9G9sTY/wCYajKa9lyfWPmpZAwe6oV+Wb1zcmMS1HG24= +github.com/multiformats/go-multiaddr v0.12.2/go.mod h1:GKyaTYjZRdcUhyOetrxTk9z0cW+jA/YrnqTOvKgi44M= github.com/multiformats/go-multiaddr-dns v0.0.1/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q= github.com/multiformats/go-multiaddr-dns v0.0.2/go.mod h1:9kWcqw/Pj6FwxAwW38n/9403szc57zJPs45fmnznu3Q= github.com/multiformats/go-multiaddr-dns v0.2.0/go.mod h1:TJ5pr5bBO7Y1B18djPuRsVkduhQH2YqYSbxWJzYGdK0= @@ -1569,13 +1873,20 @@ github.com/multiformats/go-multiaddr-net v0.1.5/go.mod h1:ilNnaM9HbmVFqsb/qcNysj github.com/multiformats/go-multiaddr-net v0.2.0/go.mod h1:gGdH3UXny6U3cKKYCvpXI5rnK7YaOIEOPVDI9tsJbEA= github.com/multiformats/go-multibase v0.0.1/go.mod h1:bja2MqRZ3ggyXtZSEDKpl0uO/gviWFaSteVbWT51qgs= github.com/multiformats/go-multibase v0.0.3/go.mod h1:5+1R4eQrT3PkYZ24C3W2Ue2tPwIdYQD509ZjSb5y9Oc= -github.com/multiformats/go-multibase v0.1.1 h1:3ASCDsuLX8+j4kx58qnJ4YFq/JWTJpCyDW27ztsVTOI= github.com/multiformats/go-multibase v0.1.1/go.mod h1:ZEjHE+IsUrgp5mhlEAYjMtZwK1k4haNkcaPg9aoe1a8= +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.3.0/go.mod h1:qGGaQmioCDh+TeFOnxrbU0DaIPw8yFgAZgFG0V7p1qQ= github.com/multiformats/go-multicodec v0.3.1-0.20210902112759-1539a079fd61/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ= +github.com/multiformats/go-multicodec v0.3.1-0.20211210143421-a526f306ed2c/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ= github.com/multiformats/go-multicodec v0.4.1/go.mod h1:1Hj/eHRaVWSXiSNNfcEPcwZleTmdNP81xlxDLnWU9GQ= -github.com/multiformats/go-multicodec v0.8.0 h1:evBmgkbSQux+Ds2IgfhkO38Dl2GDtRW8/Rp6YiSHX/Q= +github.com/multiformats/go-multicodec v0.5.0/go.mod h1:DiY2HFaEp5EhEXb/iYzVAunmyX/aSFMxq2KMKfWEues= +github.com/multiformats/go-multicodec v0.6.0/go.mod h1:GUC8upxSBE4oG+q3kWZRw/+6yC1BqO550bjhWsJbZlw= +github.com/multiformats/go-multicodec v0.7.0/go.mod h1:GUC8upxSBE4oG+q3kWZRw/+6yC1BqO550bjhWsJbZlw= github.com/multiformats/go-multicodec v0.8.0/go.mod h1:GUC8upxSBE4oG+q3kWZRw/+6yC1BqO550bjhWsJbZlw= +github.com/multiformats/go-multicodec v0.8.1/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= github.com/multiformats/go-multihash v0.0.1/go.mod h1:w/5tugSrLEbWqlcgJabL3oHFKTwfvkofsjW2Qa1ct4U= github.com/multiformats/go-multihash v0.0.5/go.mod h1:lt/HCbqlQwlPBz7lv0sQCdtfcMtlJvakRUn/0Ual8po= github.com/multiformats/go-multihash v0.0.8/go.mod h1:YSLudS+Pi8NHE7o6tb3D8vrpKa63epEDmG8nTduyAew= @@ -1584,16 +1895,22 @@ github.com/multiformats/go-multihash v0.0.13/go.mod h1:VdAWLKTwram9oKAatUcLxBNUj github.com/multiformats/go-multihash v0.0.14/go.mod h1:VdAWLKTwram9oKAatUcLxBNUjdtcVwxObEQBtRfuyjc= github.com/multiformats/go-multihash v0.0.15/go.mod h1:D6aZrWNLFTV/ynMpKsNtB40mJzmCl4jb1alC0OvHiHg= github.com/multiformats/go-multihash v0.1.0/go.mod h1:RJlXsxt6vHGaia+S8We0ErjhojtKzPP2AH4+kYM7k84= -github.com/multiformats/go-multihash v0.2.2-0.20221030163302-608669da49b6 h1:qLF997Rz0X1WvdcZ2r5CUkLZ2rvdiXwG1JRSrJZEAuE= -github.com/multiformats/go-multihash v0.2.2-0.20221030163302-608669da49b6/go.mod h1:kaHxr8TfO1cxIR/tYxgZ7e59HraJq8arEQQR8E/YNvI= +github.com/multiformats/go-multihash v0.2.0/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= +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.1.0/go.mod h1:fJTiDfXJVmItycydCnNx4+wSzZ5NwG2FEVAI30fiovg= github.com/multiformats/go-multistream v0.1.1/go.mod h1:KmHZ40hzVxiaiwlj3MEbYgK9JFk2/9UktWZAF54Du38= +github.com/multiformats/go-multistream v0.2.0/go.mod h1:5GZPQZbkWOLOn3J2y4Y99vVW7vOfsAflxARk3x14o6k= github.com/multiformats/go-multistream v0.2.1/go.mod h1:5GZPQZbkWOLOn3J2y4Y99vVW7vOfsAflxARk3x14o6k= github.com/multiformats/go-multistream v0.2.2/go.mod h1:UIcnm7Zuo8HKG+HkWgfQsGL+/MIEhyTqbODbIUwSXKs= github.com/multiformats/go-multistream v0.3.0/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= github.com/multiformats/go-multistream v0.3.1/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= -github.com/multiformats/go-multistream v0.4.1 h1:rFy0Iiyn3YT0asivDUIR05leAdwZq3de4741sbiSdfo= +github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= +github.com/multiformats/go-multistream v0.4.0/go.mod h1:BS6ZSYcA4NwYEaIMeCtpJydp2Dc+fNRA6uJMSu/m8+4= github.com/multiformats/go-multistream v0.4.1/go.mod h1:Mz5eykRVAjJWckE2U78c6xqdtyNUEhKSM0Lwar2p77Q= +github.com/multiformats/go-multistream v0.5.0 h1:5htLSLl7lvJk3xx3qT/8Zm9J4K8vEOf/QGkvOGQAyiE= +github.com/multiformats/go-multistream v0.5.0/go.mod h1:n6tMZiwiP2wUsR8DgfDWw1dydlEqV3l6N3/GBsX6ILA= github.com/multiformats/go-varint v0.0.1/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.2/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/multiformats/go-varint v0.0.5/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= @@ -1637,8 +1954,15 @@ github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvw github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 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/ginkgo/v2 v2.5.1 h1:auzK7OI497k6x4OvWq+TKAcpcSAlod0doAH72oIN0Jw= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= github.com/onsi/ginkgo/v2 v2.5.1/go.mod h1:63DOGlLAH8+REH8jUGdL3YpCpu7JODesutUjdENfUAc= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -1646,16 +1970,26 @@ github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7J github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= -github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/open-rpc/meta-schema v0.0.0-20201029221707-1b72ef2ea333 h1:CznVS40zms0Dj5he4ERo+fRPtO0qxUk8lA8Xu3ddet0= github.com/open-rpc/meta-schema v0.0.0-20201029221707-1b72ef2ea333/go.mod h1:Ag6rSXkHIckQmjFBCweJEEt1mrTPBv8b9W4aU/NQWfI= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= +github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 h1:3snG66yBm59tKhhSPQrQ/0bCrv1LQbKt40LnUPiUxdc= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -1669,6 +2003,7 @@ github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJ github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= 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/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY= @@ -1678,21 +2013,23 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2D github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= +github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= +github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= github.com/peterh/liner v1.0.1-0.20180619022028-8c1271fcf47f/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= -github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ= github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= +github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08 h1:hDSdbBuw3Lefr6R18ax0tZ2BJeNB3NehB3trOwYBsdU= +github.com/petermattis/goid v0.0.0-20230317030725-371a4b8eda08/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= +github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -1703,6 +2040,7 @@ github.com/pkg/term v0.0.0-20180730021639-bffc007b7fd5/go.mod h1:eCbImbZ95eXtAUI github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/polydawn/refmt v0.0.0-20190221155625-df39d6c2d992/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= +github.com/polydawn/refmt v0.0.0-20190408063855-01bf1e26dd14/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/polydawn/refmt v0.0.0-20190807091052-3d65705ee9f1/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/polydawn/refmt v0.0.0-20201211092308-30ac6d18308e/go.mod h1:uIp+gprXxxrWSjjklXD+mN4wed/tMfjMMmN/9+JsA9o= github.com/polydawn/refmt v0.89.0 h1:ADJTApkvkeBZsN0tBTx8QjpD9JkmxbKp0cxfr9qszm4= @@ -1719,16 +2057,19 @@ github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66Id github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= +github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= @@ -1742,8 +2083,9 @@ github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16 github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.33.0/go.mod h1:gB3sOl7P0TvJabZpLY5uQMpUqRCPPCyRLCZYc7JZTNE= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= +github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= @@ -1754,26 +2096,23 @@ github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/prometheus/tsdb v0.7.1 h1:YZcsG11NqnK4czYLrWd9mpEuAJIHVQLwdrleYfszMAA= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= -github.com/pyroscope-io/client v0.7.0 h1:LWuuqPQ1oa6x7BnmUOuo/aGwdX85QGhWZUBYWWW3zdk= -github.com/pyroscope-io/client v0.7.0/go.mod h1:4h21iOU4pUOq0prKyDlvYRL+SCKsBc5wKiEtV+rJGqU= -github.com/pyroscope-io/godeltaprof v0.1.0 h1:UBqtjt0yZi4jTxqZmLAs34XG6ycS3vUTlhEUSq4NHLE= -github.com/pyroscope-io/godeltaprof v0.1.0/go.mod h1:psMITXp90+8pFenXkKIpNhrfmI9saQnPbba27VIaiQE= -github.com/pyroscope-io/otel-profiling-go v0.4.0 h1:Hk/rbUqOWoByoWy1tt4r5BX5xoKAvs5drr0511Ki8ic= -github.com/pyroscope-io/otel-profiling-go v0.4.0/go.mod h1:MXaofiWU7PgLP7eISUZJYVO4Z8WYMqpkYgeP4XrPLyg= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A= -github.com/quic-go/qtls-go1-19 v0.2.1/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= -github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk= -github.com/quic-go/qtls-go1-20 v0.1.1/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= -github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0= -github.com/quic-go/quic-go v0.33.0/go.mod h1:YMuhaAV9/jIu0XclDXwZPAsP/2Kgr5yMYhe9oxhhOFA= -github.com/quic-go/webtransport-go v0.5.2 h1:GA6Bl6oZY+g/flt00Pnu0XtivSD8vukOu3lYhJjnGEk= -github.com/quic-go/webtransport-go v0.5.2/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc= +github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI= +github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= +github.com/quic-go/qtls-go1-20 v0.3.4 h1:MfFAPULvst4yoMgY9QmtpYmfij/em7O8UUi+bNVm7Cg= +github.com/quic-go/qtls-go1-20 v0.3.4/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= +github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo= +github.com/quic-go/quic-go v0.39.4 h1:PelfiuG7wXEffUT2yceiqz5V6Pc0TA5ruOd1LcmFc1s= +github.com/quic-go/quic-go v0.39.4/go.mod h1:T09QsDQWjLiQ74ZmacDfqZmhY/NLnw5BC40MANNNZ1Q= +github.com/quic-go/webtransport-go v0.5.1/go.mod h1:OhmmgJIzTTqXK5xvtuX0oBpLV2GkLWNDA+UeTGJXErU= +github.com/quic-go/webtransport-go v0.6.0 h1:CvNsKqc4W2HljHJnoT+rMmbRJybShZ0YPFDD3NxaZLY= +github.com/quic-go/webtransport-go v0.6.0/go.mod h1:9KjU4AEBqEQidGHNDkZrb8CAa1abRaosM2yGOyiikEc= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= github.com/raulk/clock v1.1.0/go.mod h1:3MpVxdZ/ODBQDxbN+kzshf5OSZwPjtMDx6BBXBmOeY0= @@ -1788,29 +2127,33 @@ github.com/regen-network/cosmos-proto v0.3.1/go.mod h1:jO0sVX6a1B36nmE8C9xBFXpNw github.com/regen-network/protobuf v1.3.3-alpha.regen.1 h1:OHEc+q5iIAXpqiqFKeLpu5NwTIkVXUs48vFMwzqpqY4= github.com/regen-network/protobuf v1.3.3-alpha.regen.1/go.mod h1:2DjTFR1HhMQhiWC5sZ4OhQ3+NtdbZ6oBDKQwq5Ou+FI= github.com/retailnext/hllpp v1.0.1-0.20180308014038-101a6d2f8b52/go.mod h1:RDpi1RftBQPUCDRw6SmxeaREsAaRKnOclghuzp/WRzc= -github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= -github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rjeczalik/notify v0.9.1 h1:CLCKso/QK1snAlnhNR/CNvNiFU2saUtjV0bx3EwNeCE= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rollkit/go-da v0.4.0 h1:/s7ZrVq7DC2aK8UXIvB7rsXrZ2mVGRw7zrexcxRvhlw= +github.com/rollkit/go-da v0.4.0/go.mod h1:Kef0XI5ecEKd3TXzI8S+9knAUJnZg0svh2DuXoCsPlM= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= -github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo= +github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.21.0/go.mod h1:ZPhntP/xmq1nnND05hhpAh2QMhSsA4UN3MGZ6O2J3hM= -github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs= -github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.36.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sasha-s/go-deadlock v0.3.1 h1:sqv7fDNShgjcaxkO0JNcOAlr8B9+cV5Ey/OB71efZx0= github.com/sasha-s/go-deadlock v0.3.1/go.mod h1:F73l+cr82YSh10GxyRI6qZiCgK64VaZjwesgfQ1/iLM= @@ -1850,11 +2193,14 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v0.0.0-20190222223459-a17d461953aa/go.mod h1:2RVY1rIf+2J2o/IM9+vPq9RzmHDSseB7FoXiSNIUsoU= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= @@ -1869,15 +2215,15 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= +github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= -github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -1886,11 +2232,12 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= -github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= -github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/spf13/viper v1.15.0 h1:js3yy885G8xwJa6iOISGFwd+qlUo5AvyXb7CiihdtiU= +github.com/spf13/viper v1.15.0/go.mod h1:fFcTBJxvhhzSJiZy8n+PeW6t8l+KeT/uTARa0jHOQLA= github.com/src-d/envconfig v1.0.0/go.mod h1:Q9YQZ7BKITldTBnoxsE5gOeB5y66RyPXeue/R4aaNBc= -github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4 h1:Gb2Tyox57NRNuZ2d3rmvB3pcmbu7O1RS3m8WRx7ilrg= github.com/status-im/keycard-go v0.0.0-20190316090335-8537d3370df4/go.mod h1:RZLeN1LMWmRsyYjvAu+I6Dm9QmlDaIIt+Y+4Kd7Tp+Q= +github.com/status-im/keycard-go v0.2.0 h1:QDLFswOQu1r5jsycloeQh3bVU8n/NatHHaZobtDnDzA= +github.com/status-im/keycard-go v0.2.0/go.mod h1:wlp8ZLbsmrF6g6WjugPAx+IzoLrkdf9+mHxBEeo3Hbg= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -1909,52 +2256,60 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= +github.com/supranational/blst v0.3.11/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/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= -github.com/tendermint/btcd v0.1.1 h1:0VcxPfflS2zZ3RiOAHkBiFUcPvbtRj5O7zHmcJWHV7s= -github.com/tendermint/btcd v0.1.1/go.mod h1:DC6/m53jtQzr/NFmMNEu0rxf18/ktVoVtMrnDD5pN+U= -github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 h1:hqAk8riJvK4RMWx1aInLzndwxKalgi5rTqgfXxOxbEI= -github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15/go.mod h1:z4YtwM70uOnk8h0pjJYlj3zdYwi9l03By6iAIF5j/Pk= github.com/tendermint/go-amino v0.16.0 h1:GyhmgQKvqF82e2oZeuMSp9JTN0N09emoSZlb2lyGa2E= github.com/tendermint/go-amino v0.16.0/go.mod h1:TQU0M1i/ImAo+tYpZi73AU3V/dKeCoMC9Sphe2ZwGME= github.com/tendermint/tm-db v0.6.7 h1:fE00Cbl0jayAoqlExN6oyQJ7fR/ZtoVOmvPJ//+shu8= github.com/tendermint/tm-db v0.6.7/go.mod h1:byQDzFkZV1syXr/ReXS808NxA2xvyuuVgXOJ/088L6I= +github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= +github.com/tidwall/btree v1.5.0 h1:iV0yVY/frd7r6qGBXfEYs7DH0gTDgrKTrDjS7xt/IyQ= +github.com/tidwall/btree v1.5.0/go.mod h1:LGm8L/DZjPLmeWGjv5kFrY8dL4uVhMmzmmLYmsObdKE= github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= +github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= github.com/tklauser/go-sysconf v0.3.5/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= -github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw= -github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk= +github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= +github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= -github.com/tklauser/numcpus v0.4.0 h1:E53Dm1HjH1/R2/aoCtXtPgzmElmn51aOkhCFSuZq//o= -github.com/tklauser/numcpus v0.4.0/go.mod h1:1+UI3pD8NW14VMwdgJNJ1ESk2UnwhAnz5hMwiKKqXCQ= +github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ttacon/chalk v0.0.0-20160626202418-22c06c80ed31/go.mod h1:onvgF043R+lC5RZ8IT9rBXDaEDnpnw/Cl+HFiw+v/7Q= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= -github.com/tyler-smith/go-bip39 v1.0.2 h1:+t3w+KwLXO6154GNJY+qUtIxLTmFjfUmpguQT1OlOT8= github.com/tyler-smith/go-bip39 v1.0.2/go.mod h1:sJ5fKU0s6JVwZjjcUEX2zFOnvq0ASQ2K9Zr6cf67kNs= +github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= +github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= +github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= @@ -1963,33 +2318,43 @@ github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtX github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= -github.com/urfave/cli/v2 v2.10.2 h1:x3p8awjp/2arX+Nl/G2040AZpOCHS/eMJJ1/a+mye4Y= +github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= +github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= -github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3 h1:zMsHhfK9+Wdl1F7sIKLyx3wrOFofpb3rWFbA4HgcK5k= -github.com/vivint/infectious v0.0.0-20200605153912-25a574ae18a3/go.mod h1:R0Gbuw7ElaGSLOZUSwBm/GgVwMd30jWxBDdAyMOeTuc= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wangjia184/sortedset v0.0.0-20160527075905-f5d03557ba30/go.mod h1:YkocrP2K2tcw938x9gCOmT5G5eCD6jsTz0SZuyAqwIE= github.com/warpfork/go-testmark v0.3.0/go.mod h1:jhEf8FVxd+F17juRubpmut64NEG6I2rgkUhlcqqXwE0= -github.com/warpfork/go-testmark v0.11.0 h1:J6LnV8KpceDvo7spaNU4+DauH2n1x+6RaO2rJrmpQ9U= +github.com/warpfork/go-testmark v0.10.0/go.mod h1:jhEf8FVxd+F17juRubpmut64NEG6I2rgkUhlcqqXwE0= +github.com/warpfork/go-testmark v0.11.0/go.mod h1:jhEf8FVxd+F17juRubpmut64NEG6I2rgkUhlcqqXwE0= +github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= +github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= github.com/warpfork/go-wish v0.0.0-20180510122957-5ad1f5abf436/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/warpfork/go-wish v0.0.0-20190328234359-8b3e70f8e830/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/warpfork/go-wish v0.0.0-20200122115046-b9ea61034e4a/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= +github.com/whyrusleeping/base32 v0.0.0-20170828182744-c30ac30633cc/go.mod h1:r45hJU7yEoA81k6MWNhpMj/kms0n14dkzkxYHoB96UM= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= github.com/whyrusleeping/cbor-gen v0.0.0-20200123233031-1cdf64d27158/go.mod h1:Xj/M2wWU+QdTdRbu/L/1dIZY8/Wb2K9pAhtroQuxJJI= -github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa h1:EyA027ZAkuaCLoxVX4r1TZMPy1d31fM6hbfQ4OU4I5o= +github.com/whyrusleeping/cbor-gen v0.0.0-20221220214510-0333c149dec0/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/cbor-gen v0.0.0-20230126041949-52956bd4c9aa/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= +github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25 h1:yVYDLoN2gmB3OdBXFW8e1UwgVbmCvNlnAKhvHPaNARI= +github.com/whyrusleeping/cbor-gen v0.0.0-20230818171029-f91ae536ca25/go.mod h1:fgkXqYy7bV2cFeIEOkVTZS/WjXARfBqSH6Q2qHL33hQ= github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f h1:jQa4QT2UP9WYv2nzyawpKMOCl+Z/jW7djv2/J50lj9E= +github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f/go.mod h1:p9UJB6dDgdPgMJZs7UjUOdulKyRr9fqkS+6JKAInPy8= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 h1:EKhdznlJHPMoKr0XTrX+IlJs1LH3lyx2nfr1dOlZ79k= github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1/go.mod h1:8UvriyWtv5Q5EOgjHaSseUEdkQfvwFv1I/In/O2M9gc= github.com/whyrusleeping/go-logging v0.0.0-20170515211332-0457bb6b88fc/go.mod h1:bopw91TMyo8J3tvftk8xmU2kPmlrt4nScJQZU2hE5EM= github.com/whyrusleeping/go-logging v0.0.1/go.mod h1:lDPYj54zutzG1XYfHAhcc7oNXEburHQBn+Iqd4yS4vE= +github.com/whyrusleeping/go-notifier v0.0.0-20170827234753-097c5d47330f/go.mod h1:cZNvX9cFybI01GriPRMXDtczuvUhgbcYr9iCGaNlRv8= github.com/whyrusleeping/mafmt v1.2.8/go.mod h1:faQJFPbLSxzD9xpA02ttW/tS9vZykNvXwGvqIpk20FA= +github.com/whyrusleeping/mdns v0.0.0-20180901202407-ef14215e6b30/go.mod h1:j4l84WPFclQPj320J9gp0XwNKBb3U0zt5CBqjPp22G4= github.com/whyrusleeping/mdns v0.0.0-20190826153040-b9b60ed33aa9/go.mod h1:j4l84WPFclQPj320J9gp0XwNKBb3U0zt5CBqjPp22G4= github.com/whyrusleeping/multiaddr-filter v0.0.0-20160516205228-e903e4adabd7/go.mod h1:X2c0RVCI1eSUFI8eLcY3c0423ykwiUdxLJtkDvruhjI= github.com/willf/bitset v1.1.3/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= @@ -1998,6 +2363,7 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q github.com/xlab/treeprint v0.0.0-20180616005107-d6fb6747feb6/go.mod h1:ce1O1j6UtZfjr22oyGxGLbauSBp2YVXpARAosm7dHBg= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/ybbus/jsonrpc v2.1.2+incompatible/go.mod h1:XJrh1eMSzdIYFbM08flv0wp5G35eRniyeGut1z+LSiE= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -2006,17 +2372,18 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zondax/hid v0.9.1 h1:gQe66rtmyZ8VeGFcOpbuH3r7erYtNEAezCAYu8LdkJo= -github.com/zondax/hid v0.9.1/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= -github.com/zondax/ledger-go v0.14.0 h1:dlMC7aO8Wss1CxBq2I96kZ69Nh1ligzbs8UWOtq/AsA= -github.com/zondax/ledger-go v0.14.0/go.mod h1:fZ3Dqg6qcdXWSOJFKMG8GCTnD7slO/RL2feOQv8K320= +github.com/zondax/hid v0.9.2 h1:WCJFnEDMiqGF64nlZz28E9qLVZ0KSJ7xpc5DLEyma2U= +github.com/zondax/hid v0.9.2/go.mod h1:l5wttcP0jwtdLjqjMMWFVEE7d1zO0jvSPA9OPZxWpEM= +github.com/zondax/ledger-go v0.14.3 h1:wEpJt2CEcBJ428md/5MgSLsXLBos98sBOyxNmCjfUCw= +github.com/zondax/ledger-go v0.14.3/go.mod h1:IKKaoxupuB43g4NxeQmbLXv7T9AlQyie1UpHb342ycI= gitlab.com/NebulousLabs/errors v0.0.0-20171229012116-7ead97ef90b8/go.mod h1:ZkMZ0dpQyWwlENaeZVBiQRjhMEZvk6VTXquzl3FOFP8= gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975 h1:L/ENs/Ar1bFzUeKx6m3XjlmBgIUlykX9dzvp5k9NGxc= +gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975/go.mod h1:ZkMZ0dpQyWwlENaeZVBiQRjhMEZvk6VTXquzl3FOFP8= gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 h1:dizWJqTWjwyD8KGcMOwgrkqu1JIkofYgKkmDeNE7oAs= gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40/go.mod h1:rOnSnoRyxMI3fe/7KIbVcsHRGxe30OONv8dEgo+vCfA= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= -go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= @@ -2031,60 +2398,73 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0 h1:2JydY5UiDpqvj2p7sO9bgHuhTy4hgTZ0ymehdq/Ob0Q= +go.opentelemetry.io/contrib/instrumentation/runtime v0.45.0/go.mod h1:ch3a5QxOqVWxas4CzjCFFOOQe+7HgAXC/N1oVxS9DK4= go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo= -go.opentelemetry.io/otel v1.4.1/go.mod h1:StM6F/0fSwpd8dKWDCdRr7uRvEPYdW0hBSlbdTiUde4= -go.opentelemetry.io/otel v1.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y= +go.opentelemetry.io/otel v1.7.0/go.mod h1:5BdUoMIz5WEs0vt0CUEMtSSaTSHBBVwrhnz7+nrD5xk= go.opentelemetry.io/otel v1.13.0/go.mod h1:FH3RtdZCzRkJYFTCsAKDy9l/XYjMdNv6QrkFFB8DvVg= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.2 h1:htgM8vZIF8oPSCxa341e3IZ4yr/sKxgu8KZYllByiVY= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.11.2/go.mod h1:rqbht/LlhVBgn5+k3M5QK96K5Xb0DvXpMJ5SFQpY6uw= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.34.0 h1:kpskzLZ60cJ48SJ4uxWa6waBL+4kSV6nVK8rP+QM8Wg= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.34.0/go.mod h1:4+x3i62TEegDHuzNva0bMcAN8oUi5w4liGb1d/VgPYo= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.34.0 h1:t4Ajxj8JGjxkqoBtbkCOY2cDUl9RwiNE9LPQavooi9U= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.34.0/go.mod h1:WO7omosl4P7JoanH9NgInxDxEn2F2M5YinIh8EyeT8w= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.2 h1:fqR1kli93643au1RKo0Uma3d2aPQKT+WBKfTSBaKbOc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.2/go.mod h1:5Qn6qvgkMsLDX+sYK64rHb1FPhpn0UtxF+ouX1uhyJE= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.2 h1:Us8tbCmuN16zAnK5TC69AtODLycKbwnskQzaB6DfFhc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.11.2/go.mod h1:GZWSQQky8AgdJj50r1KJm8oiQiIPaAX7uZCFQX9GzC8= +go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0 h1:+RbSCde0ERway5FwKvXR3aRJIFeDu9rtwC6E7BC6uoM= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.45.0/go.mod h1:zcI8u2EJxbLPyoZ3SkVAAcQPgYb1TDRzW93xLFnsggU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0 h1:9M3+rhx7kZCIQQhQRYaZCdNu1V73tm4TvXs2ntl98C4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.22.0/go.mod h1:noq80iT8rrHP1SfybmPiRGc9dc5M8RPmGvtwo7Oo7tc= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0 h1:FyjCyI9jVEfqhUh2MoSkmolPjfh5fp2hnV0b0irxH4Q= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.22.0/go.mod h1:hYwym2nDEeZfG/motx0p7L7J1N1vyzIThemQsb4g2qY= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.15.1 h1:2PunuO5SbkN5MhCbuHCd3tC6qrcaj+uDAkX/qBU5BAs= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.15.1/go.mod h1:q8+Tha+5LThjeSU8BW93uUC5w5/+DnYHMKBMpRCsui0= go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU= -go.opentelemetry.io/otel/metric v0.34.0 h1:MCPoQxcg/26EuuJwpYN1mZTeCYAUGx8ABxfW07YkjP8= -go.opentelemetry.io/otel/metric v0.34.0/go.mod h1:ZFuI4yQGNCupurTXCwkeD/zHBt+C2bR7bw5JqUm/AP8= +go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw= go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc= -go.opentelemetry.io/otel/sdk v1.11.2 h1:GF4JoaEx7iihdMFu30sOyRx52HDHOkl9xQ8SMqNXUiU= -go.opentelemetry.io/otel/sdk v1.11.2/go.mod h1:wZ1WxImwpq+lVRo4vsmSOxdd+xwoUJ6rqyLc3SyX9aU= -go.opentelemetry.io/otel/sdk/metric v0.34.0 h1:7ElxfQpXCFZlRTvVRTkcUvK8Gt5DC8QzmzsLsO2gdzo= -go.opentelemetry.io/otel/sdk/metric v0.34.0/go.mod h1:l4r16BIqiqPy5rd14kkxllPy/fOI4tWo1jkpD9Z3ffQ= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/sdk/metric v1.22.0 h1:ARrRetm1HCVxq0cbnaZQlfwODYJHo3gFL8Z3tSmHBcI= +go.opentelemetry.io/otel/sdk/metric v1.22.0/go.mod h1:KjQGeMIDlBNEOo6HvjhxIec1p/69/kULDcp4gr0oLQQ= go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw= -go.opentelemetry.io/otel/trace v1.4.1/go.mod h1:iYEVbroFCNut9QkwEczV9vMRPHNKSSwYZjulEtsmhFc= -go.opentelemetry.io/otel/trace v1.13.0 h1:CBgRZ6ntv+Amuj1jDsMhZtlAPT6gbyIRdaIzFhfBSdY= +go.opentelemetry.io/otel/trace v1.7.0/go.mod h1:fzLSB9nqR2eXzxPXb2JW9IKE+ScyXA48yyE4TNvoHqU= go.opentelemetry.io/otel/trace v1.13.0/go.mod h1:muCvmmO9KKpvuXSf3KKAXXB2ygNYHQ+ZfI5X08d3tds= +go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/dig v1.16.1 h1:+alNIBsl0qfY0j6epRubp/9obgtrObRAc5aD+6jbWY8= -go.uber.org/dig v1.16.1/go.mod h1:557JTAUZT5bUK0SvCwikmLPPtdQhfvLYtO5tJgQSbnk= -go.uber.org/fx v1.19.2 h1:SyFgYQFr1Wl0AYstE8vyYIzP4bFz2URrScjwC4cwUvY= -go.uber.org/fx v1.19.2/go.mod h1:43G1VcqSzbIv77y00p1DRAsyZS8WdzuYdhZXmEUkMyQ= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM= +go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc= +go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE= +go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY= +go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk= +go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg= go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= +go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= +go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= @@ -2096,8 +2476,11 @@ go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.22.0/go.mod h1:H4siCOZOrAolnUPJEkfaSjDqyP+BDS0DdDWzwcgt3+U= +go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= +go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -2112,6 +2495,7 @@ golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190513172903-22d7a77e9e5f/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190618222545-ea8f1a30c443/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -2122,6 +2506,7 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200109152110-61a87790db17/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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= @@ -2136,9 +2521,16 @@ golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5 golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= +golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -2155,9 +2547,14 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20210615023648-acb5c1269671/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc= -golang.org/x/exp v0.0.0-20210714144626-1041f73d31d8/go.mod h1:DVyR6MI7P4kEQgvZJSj1fQGrWIi2RzIrfYWycwheUAc= -golang.org/x/exp v0.0.0-20230129154200-a960b3787bd2 h1:5sPMf9HJXrvBWIamTw+rTST0bZ3Mho2n1p58M0+W99c= +golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= +golang.org/x/exp v0.0.0-20220916125017-b168a2c6b86b/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/exp v0.0.0-20230129154200-a960b3787bd2/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e h1:723BNChdd0c2Wk6WOE320qGBiPtYx0F0Bbm1kriShfE= +golang.org/x/exp v0.0.0-20240110193028-0dcbfd608b1e/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -2187,10 +2584,14 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= +golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= +golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -2211,7 +2612,9 @@ golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -2268,11 +2671,20 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220812174116-3211cb980234/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220920183852-bf014ff85ad5/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220923203811-8be639271d50/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -2300,8 +2712,8 @@ golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= -golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= +golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= +golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= 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= @@ -2317,8 +2729,9 @@ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/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.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180810173357-98c5dad5d1a0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2335,6 +2748,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190302025703-b6889370fb10/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2342,8 +2756,10 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190524122548-abf6ff778158/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190526052359-791d8a0f4d09/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2385,7 +2801,6 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/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-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2422,8 +2837,8 @@ golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2433,8 +2848,12 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220405210540-1e041c57c461/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220429233432-b5fbb4746d32/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2446,15 +2865,30 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2464,16 +2898,21 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -2537,6 +2976,7 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -2544,10 +2984,13 @@ golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= 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= @@ -2560,6 +3003,8 @@ golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNq gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= +gonum.org/v1/gonum v0.13.0 h1:a0T3bh+7fhRyqeNbiC3qVHYmkiQgit3wnNan/2c0HMM= +gonum.org/v1/gonum v0.13.0/go.mod h1:/WPYRckkfWrhWefxyYTfrTtQR0KH4iyHNuzxqXAKyAU= gonum.org/v1/netlib v0.0.0-20181029234149-ec6d1f5cefe6/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= @@ -2615,8 +3060,8 @@ google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.103.0 h1:9yuVqlu2JCvcLg9p8S3fcFLZij8EPSyvODIY1rkMizQ= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= +google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= +google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -2625,8 +3070,9 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= @@ -2740,8 +3186,12 @@ google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqw google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3 h1:1hfbdAfFbkmpg41000wDVqr7jUpK/Yo+LPnIxxGzmkg= +google.golang.org/genproto v0.0.0-20231211222908-989df2bf70f3/go.mod h1:5RBcpGRxr25RbDzY5w+dmaqpSEvl8Gwl1x2CICf60ic= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f h1:2yNACc1O40tTnrsbk9Cv6oxiW8pxI/pXj0wRtdlYmgY= +google.golang.org/genproto/googleapis/api v0.0.0-20231120223509-83a465c0220f/go.mod h1:Uy9bTZJqmfrw2rIBxgGLnamc78euZULUBrLZ9XTITKI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= @@ -2778,7 +3228,6 @@ google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= @@ -2788,8 +3237,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= +google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= +google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= 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= @@ -2806,12 +3255,13 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8 h1:KR8+MyP7/qOlV+8Af01LtjL04bu7on42eVsxT4EyBQk= -google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/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= @@ -2823,7 +3273,8 @@ gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce h1:+JknDZhAj8YMt7GC73Ei8pv4MzjDUNPHgQWJdtMAaDU= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce/go.mod h1:5AcXVHNjg+BDxry382+8OKon8SEWiKktQR07RKPsv1c= gopkg.in/olebedev/go-duktape.v3 v3.0.0-20200619000410-60c24ae608a6/go.mod h1:uAJfkITjFhyEEuUfm7bsmCZRbW5WRq8s9EY8HZ6hCns= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= @@ -2844,6 +3295,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -2859,16 +3311,19 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= -lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= +lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= pgregory.net/rapid v0.5.3 h1:163N50IHFqr1phZens4FQOdPgfJscR7a562mjQqeo4M= +pgregory.net/rapid v0.5.3/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU= rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/header/header.go b/header/header.go index 25c3b0e5cf..01a84a2c0a 100644 --- a/header/header.go +++ b/header/header.go @@ -2,14 +2,16 @@ package header import ( "bytes" - "context" "encoding/json" + "errors" "fmt" "time" tmjson "github.com/tendermint/tendermint/libs/json" + "github.com/tendermint/tendermint/light" core "github.com/tendermint/tendermint/types" + "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/pkg/da" libhead "github.com/celestiaorg/go-header" "github.com/celestiaorg/rsmt2d" @@ -17,18 +19,12 @@ import ( // ConstructFn aliases a function that creates an ExtendedHeader. type ConstructFn = func( - context.Context, *core.Header, *core.Commit, *core.ValidatorSet, *rsmt2d.ExtendedDataSquare, ) (*ExtendedHeader, error) -type DataAvailabilityHeader = da.DataAvailabilityHeader - -// EmptyDAH provides DAH of the empty block. -var EmptyDAH = da.MinDataAvailabilityHeader - // RawHeader is an alias to core.Header. It is // "raw" because it is not yet wrapped to include // the DataAvailabilityHeader. @@ -39,47 +35,30 @@ type RawHeader = core.Header // block headers and perform Data Availability Sampling. type ExtendedHeader struct { RawHeader `json:"header"` - Commit *core.Commit `json:"commit"` - ValidatorSet *core.ValidatorSet `json:"validator_set"` - DAH *DataAvailabilityHeader `json:"dah"` -} - -func (eh *ExtendedHeader) New() libhead.Header { - return new(ExtendedHeader) -} - -func (eh *ExtendedHeader) IsZero() bool { - return eh == nil -} - -func (eh *ExtendedHeader) ChainID() string { - return eh.RawHeader.ChainID + Commit *core.Commit `json:"commit"` + ValidatorSet *core.ValidatorSet `json:"validator_set"` + DAH *da.DataAvailabilityHeader `json:"dah"` } -func (eh *ExtendedHeader) Height() int64 { - return eh.RawHeader.Height -} - -func (eh *ExtendedHeader) Time() time.Time { - return eh.RawHeader.Time -} - -var _ libhead.Header = &ExtendedHeader{} - // MakeExtendedHeader assembles new ExtendedHeader. func MakeExtendedHeader( - _ context.Context, h *core.Header, comm *core.Commit, vals *core.ValidatorSet, eds *rsmt2d.ExtendedDataSquare, ) (*ExtendedHeader, error) { - var dah DataAvailabilityHeader + var ( + dah da.DataAvailabilityHeader + err error + ) switch eds { case nil: - dah = EmptyDAH() + dah = da.MinDataAvailabilityHeader() default: - dah = da.NewDataAvailabilityHeader(eds) + dah, err = da.NewDataAvailabilityHeader(eds) + if err != nil { + return nil, err + } } eh := &ExtendedHeader{ @@ -88,14 +67,34 @@ func MakeExtendedHeader( Commit: comm, ValidatorSet: vals, } - return eh, eh.Validate() + return eh, nil +} + +func (eh *ExtendedHeader) New() *ExtendedHeader { + return new(ExtendedHeader) +} + +func (eh *ExtendedHeader) IsZero() bool { + return eh == nil +} + +func (eh *ExtendedHeader) ChainID() string { + return eh.RawHeader.ChainID +} + +func (eh *ExtendedHeader) Height() uint64 { + return uint64(eh.RawHeader.Height) +} + +func (eh *ExtendedHeader) Time() time.Time { + return eh.RawHeader.Time } // Hash returns Hash of the wrapped RawHeader. // NOTE: It purposely overrides Hash method of RawHeader to get it directly from Commit without // recomputing. func (eh *ExtendedHeader) Hash() libhead.Hash { - return libhead.Hash(eh.Commit.BlockID.Hash) + return eh.Commit.BlockID.Hash.Bytes() } // LastHeader returns the Hash of the last wrapped RawHeader. @@ -115,6 +114,11 @@ func (eh *ExtendedHeader) Validate() error { return fmt.Errorf("ValidateBasic error on RawHeader at height %d: %w", eh.Height(), err) } + if eh.RawHeader.Version.App != appconsts.LatestVersion { + return fmt.Errorf("app version mismatch, expected: %d, got %d", appconsts.LatestVersion, + eh.RawHeader.Version.App) + } + err = eh.Commit.ValidateBasic() if err != nil { return fmt.Errorf("ValidateBasic error on Commit at height %d: %w", eh.Height(), err) @@ -134,8 +138,8 @@ func (eh *ExtendedHeader) Validate() error { // ensure data root from raw header matches computed root if !bytes.Equal(eh.DAH.Hash(), eh.DataHash) { - panic(fmt.Sprintf("mismatch between data hash commitment from core header and computed data root "+ - "at height %d: data hash: %X, computed root: %X", eh.Height(), eh.DataHash, eh.DAH.Hash())) + return fmt.Errorf("mismatch between data hash commitment from core header and computed data root "+ + "at height %d: data hash: %X, computed root: %X", eh.Height(), eh.DataHash, eh.DAH.Hash()) } // Make sure the header is consistent with the commit. @@ -146,7 +150,8 @@ func (eh *ExtendedHeader) Validate() error { return fmt.Errorf("commit signs block %X, header is block %X", chash, hhash) } - if err := eh.ValidatorSet.VerifyCommitLight(eh.ChainID(), eh.Commit.BlockID, eh.Height(), eh.Commit); err != nil { + err = eh.ValidatorSet.VerifyCommitLight(eh.ChainID(), eh.Commit.BlockID, int64(eh.Height()), eh.Commit) + if err != nil { return fmt.Errorf("VerifyCommitLight error at height %d: %w", eh.Height(), err) } @@ -157,6 +162,50 @@ func (eh *ExtendedHeader) Validate() error { return nil } +var ( + ErrValidatorHashMismatch = errors.New("validator hash mismatch") + ErrLastHeaderHashMismatch = errors.New("last header hash mismatch") + ErrVerifyCommitLightTrustingFailed = errors.New("commit light trusting verification failed") +) + +// Verify validates given untrusted Header against trusted ExtendedHeader. +func (eh *ExtendedHeader) Verify(untrst *ExtendedHeader) error { + isAdjacent := eh.Height()+1 == untrst.Height() + if isAdjacent { + // Optimized verification for adjacent headers + // Check the validator hashes are the same + if !bytes.Equal(untrst.ValidatorsHash, eh.NextValidatorsHash) { + return &libhead.VerifyError{ + Reason: fmt.Errorf("%w: expected (%X), but got (%X)", + ErrValidatorHashMismatch, + eh.NextValidatorsHash, + untrst.ValidatorsHash, + ), + } + } + + if !bytes.Equal(untrst.LastHeader(), eh.Hash()) { + return &libhead.VerifyError{ + Reason: fmt.Errorf("%w: expected (%X), but got (%X)", + ErrLastHeaderHashMismatch, + eh.Hash(), + untrst.LastHeader(), + ), + } + } + + return nil + } + + if err := eh.ValidatorSet.VerifyCommitLightTrusting(eh.ChainID(), untrst.Commit, light.DefaultTrustLevel); err != nil { + return &libhead.VerifyError{ + Reason: fmt.Errorf("%w: %w", ErrVerifyCommitLightTrustingFailed, err), + SoftFailure: true, + } + } + return nil +} + // MarshalBinary marshals ExtendedHeader to binary. func (eh *ExtendedHeader) MarshalBinary() ([]byte, error) { return MarshalExtendedHeader(eh) @@ -228,3 +277,5 @@ func (eh *ExtendedHeader) UnmarshalJSON(data []byte) error { eh.RawHeader = *rawHeader return nil } + +var _ libhead.Header[*ExtendedHeader] = &ExtendedHeader{} diff --git a/header/headertest/fraud/testing.go b/header/headertest/fraud/testing.go new file mode 100644 index 0000000000..e2ff13a4e0 --- /dev/null +++ b/header/headertest/fraud/testing.go @@ -0,0 +1,101 @@ +package headerfraud + +import ( + "context" + "testing" + "time" + + "github.com/ipfs/boxo/blockservice" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/bytes" + tmproto "github.com/tendermint/tendermint/proto/tendermint/types" + "github.com/tendermint/tendermint/types" + + "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/nmt" + "github.com/celestiaorg/rsmt2d" + + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/header/headertest" + "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/ipld" +) + +// FraudMaker allows to produce an invalid header at the specified height in order to produce the +// BEFP. +type FraudMaker struct { + t *testing.T + + vals []types.PrivValidator + valSet *types.ValidatorSet + + // height of the invalid header + height int64 + + prevHash bytes.HexBytes +} + +func NewFraudMaker(t *testing.T, height int64, vals []types.PrivValidator, valSet *types.ValidatorSet) *FraudMaker { + return &FraudMaker{ + t: t, + vals: vals, + valSet: valSet, + height: height, + } +} + +func (f *FraudMaker) MakeExtendedHeader(odsSize int, edsStore *eds.Store) header.ConstructFn { + return func( + h *types.Header, + comm *types.Commit, + vals *types.ValidatorSet, + eds *rsmt2d.ExtendedDataSquare, + ) (*header.ExtendedHeader, error) { + if h.Height < f.height { + return header.MakeExtendedHeader(h, comm, vals, eds) + } + + hdr := *h + if h.Height == f.height { + adder := ipld.NewProofsAdder(odsSize) + square := edstest.RandByzantineEDS(f.t, odsSize, nmt.NodeVisitor(adder.VisitFn())) + dah, err := da.NewDataAvailabilityHeader(square) + require.NoError(f.t, err) + hdr.DataHash = dah.Hash() + + ctx := ipld.CtxWithProofsAdder(context.Background(), adder) + require.NoError(f.t, edsStore.Put(ctx, h.DataHash.Bytes(), square)) + + *eds = *square + } + if h.Height > f.height { + hdr.LastBlockID.Hash = f.prevHash + } + + blockID := comm.BlockID + blockID.Hash = hdr.Hash() + voteSet := types.NewVoteSet(hdr.ChainID, hdr.Height, 0, tmproto.PrecommitType, f.valSet) + commit, err := headertest.MakeCommit(blockID, hdr.Height, 0, voteSet, f.vals, time.Now()) + require.NoError(f.t, err) + + *h = hdr + *comm = *commit + f.prevHash = h.Hash() + return header.MakeExtendedHeader(h, comm, vals, eds) + } +} +func CreateFraudExtHeader( + t *testing.T, + eh *header.ExtendedHeader, + serv blockservice.BlockService, +) *header.ExtendedHeader { + square := edstest.RandByzantineEDS(t, len(eh.DAH.RowRoots)) + err := ipld.ImportEDS(context.Background(), square, serv) + require.NoError(t, err) + dah, err := da.NewDataAvailabilityHeader(square) + require.NoError(t, err) + eh.DAH = &dah + eh.RawHeader.DataHash = dah.Hash() + return eh +} diff --git a/header/headertest/testing.go b/header/headertest/testing.go index 76bda50db5..e97f7f7825 100644 --- a/header/headertest/testing.go +++ b/header/headertest/testing.go @@ -1,7 +1,6 @@ package headertest import ( - "context" "crypto/rand" "fmt" mrand "math/rand" @@ -9,8 +8,6 @@ import ( "testing" "time" - "github.com/ipfs/go-blockservice" - logging "github.com/ipfs/go-log/v2" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/libs/bytes" @@ -29,8 +26,6 @@ import ( "github.com/celestiaorg/celestia-node/share" ) -var log = logging.Logger("headertest") - // TestSuite provides everything you need to test chain of Headers. // If not, please don't hesitate to extend it for your case. type TestSuite struct { @@ -58,7 +53,7 @@ func NewTestSuite(t *testing.T, num int) *TestSuite { } func (s *TestSuite) genesis() *header.ExtendedHeader { - dah := header.EmptyDAH() + dah := share.EmptyRoot() gen := RandRawHeader(s.t) @@ -76,7 +71,7 @@ func (s *TestSuite) genesis() *header.ExtendedHeader { RawHeader: *gen, Commit: commit, ValidatorSet: s.valSet, - DAH: &dah, + DAH: dah, } require.NoError(s.t, eh.Validate()) return eh @@ -143,23 +138,23 @@ func (s *TestSuite) NextHeader() *header.ExtendedHeader { return s.head } - dah := da.MinDataAvailabilityHeader() + dah := share.EmptyRoot() height := s.Head().Height() + 1 rh := s.GenRawHeader(height, s.Head().Hash(), libhead.Hash(s.Head().Commit.Hash()), dah.Hash()) s.head = &header.ExtendedHeader{ RawHeader: *rh, Commit: s.Commit(rh), ValidatorSet: s.valSet, - DAH: &dah, + DAH: dah, } require.NoError(s.t, s.head.Validate()) return s.head } func (s *TestSuite) GenRawHeader( - height int64, lastHeader, lastCommit, dataHash libhead.Hash) *header.RawHeader { + height uint64, lastHeader, lastCommit, dataHash libhead.Hash) *header.RawHeader { rh := RandRawHeader(s.t) - rh.Height = height + rh.Height = int64(height) rh.Time = time.Now() rh.LastBlockID = types.BlockID{Hash: bytes.HexBytes(lastHeader)} rh.LastCommitHash = bytes.HexBytes(lastCommit) @@ -208,8 +203,8 @@ func (s *TestSuite) nextProposer() *types.Validator { } // RandExtendedHeader provides an ExtendedHeader fixture. -func RandExtendedHeader(t *testing.T) *header.ExtendedHeader { - dah := header.EmptyDAH() +func RandExtendedHeader(t testing.TB) *header.ExtendedHeader { + dah := share.EmptyRoot() rh := RandRawHeader(t) rh.DataHash = dah.Hash() @@ -226,10 +221,17 @@ func RandExtendedHeader(t *testing.T) *header.ExtendedHeader { RawHeader: *rh, Commit: commit, ValidatorSet: valSet, - DAH: &dah, + DAH: dah, } } +func RandExtendedHeaderWithRoot(t testing.TB, dah *da.DataAvailabilityHeader) *header.ExtendedHeader { + h := RandExtendedHeader(t) + h.DataHash = dah.Hash() + h.DAH = dah + return h +} + func RandValidatorSet(numValidators int, votingPower int64) (*types.ValidatorSet, []types.PrivValidator) { var ( valz = make([]*types.Validator, numValidators) @@ -262,7 +264,7 @@ func RandValidator(randPower bool, minPower int64) (*types.Validator, types.Priv } // RandRawHeader provides a RawHeader fixture. -func RandRawHeader(t *testing.T) *header.RawHeader { +func RandRawHeader(t testing.TB) *header.RawHeader { return &header.RawHeader{ Version: version.Consensus{Block: 11, App: 1}, ChainID: "test", @@ -282,7 +284,7 @@ func RandRawHeader(t *testing.T) *header.RawHeader { } // RandBlockID provides a BlockID fixture. -func RandBlockID(*testing.T) types.BlockID { +func RandBlockID(testing.TB) types.BlockID { bid := types.BlockID{ Hash: make([]byte, 32), PartSetHeader: types.PartSetHeader{ @@ -295,46 +297,30 @@ func RandBlockID(*testing.T) types.BlockID { return bid } -// FraudMaker creates a custom ConstructFn that breaks the block at the given height. -func FraudMaker(t *testing.T, faultHeight int64, bServ blockservice.BlockService) header.ConstructFn { - log.Warn("Corrupting block...", "height", faultHeight) - return func(ctx context.Context, - h *types.Header, - comm *types.Commit, - vals *types.ValidatorSet, - eds *rsmt2d.ExtendedDataSquare, - ) (*header.ExtendedHeader, error) { - if h.Height == faultHeight { - eh := &header.ExtendedHeader{ - RawHeader: *h, - Commit: comm, - ValidatorSet: vals, - } - - eh, dataSq := CreateFraudExtHeader(t, eh, bServ) - if eds != nil { - *eds = *dataSq - } - return eh, nil - } - return header.MakeExtendedHeader(ctx, h, comm, vals, eds) - } -} +func ExtendedHeaderFromEDS(t testing.TB, height uint64, eds *rsmt2d.ExtendedDataSquare) *header.ExtendedHeader { + valSet, vals := RandValidatorSet(10, 10) + gen := RandRawHeader(t) + dah, err := share.NewRoot(eds) + require.NoError(t, err) -func CreateFraudExtHeader( - t *testing.T, - eh *header.ExtendedHeader, - dag blockservice.BlockService, -) (*header.ExtendedHeader, *rsmt2d.ExtendedDataSquare) { - extended := share.RandEDS(t, 2) - shares := share.ExtractEDS(extended) - copy(shares[0][share.NamespaceSize:], shares[1][share.NamespaceSize:]) - extended, err := share.ImportShares(context.Background(), shares, dag) + gen.DataHash = dah.Hash() + gen.ValidatorsHash = valSet.Hash() + gen.NextValidatorsHash = valSet.Hash() + gen.Height = int64(height) + blockID := RandBlockID(t) + blockID.Hash = gen.Hash() + voteSet := types.NewVoteSet(gen.ChainID, gen.Height, 0, tmproto.PrecommitType, valSet) + commit, err := MakeCommit(blockID, gen.Height, 0, voteSet, vals, time.Now()) require.NoError(t, err) - dah := da.NewDataAvailabilityHeader(extended) - eh.DAH = &dah - eh.RawHeader.DataHash = dah.Hash() - return eh, extended + + eh := &header.ExtendedHeader{ + RawHeader: *gen, + Commit: commit, + ValidatorSet: valSet, + DAH: dah, + } + require.NoError(t, eh.Validate()) + return eh } type Subscriber struct { diff --git a/header/headertest/verify_test.go b/header/headertest/verify_test.go index 78da779a54..82795ca5ff 100644 --- a/header/headertest/verify_test.go +++ b/header/headertest/verify_test.go @@ -1,95 +1,63 @@ package headertest import ( + "errors" "strconv" "testing" - "time" "github.com/stretchr/testify/assert" tmrand "github.com/tendermint/tendermint/libs/rand" - libhead "github.com/celestiaorg/go-header" + "github.com/celestiaorg/celestia-node/header" ) func TestVerify(t *testing.T) { h := NewTestSuite(t, 2).GenExtendedHeaders(3) trusted, untrustedAdj, untrustedNonAdj := h[0], h[1], h[2] tests := []struct { - prepare func() libhead.Header - err bool + prepare func() *header.ExtendedHeader + err error }{ { - prepare: func() libhead.Header { return untrustedAdj }, - err: false, + prepare: func() *header.ExtendedHeader { return untrustedAdj }, + err: nil, }, { - prepare: func() libhead.Header { + prepare: func() *header.ExtendedHeader { return untrustedNonAdj }, - err: false, + err: nil, }, { - prepare: func() libhead.Header { + prepare: func() *header.ExtendedHeader { untrusted := *untrustedAdj untrusted.ValidatorsHash = tmrand.Bytes(32) return &untrusted }, - err: true, + err: header.ErrValidatorHashMismatch, }, { - prepare: func() libhead.Header { - untrusted := *untrustedNonAdj - untrusted.Commit = NewTestSuite(t, 2).Commit(RandRawHeader(t)) - return &untrusted - }, - err: true, - }, - { - prepare: func() libhead.Header { + prepare: func() *header.ExtendedHeader { untrusted := *untrustedAdj untrusted.RawHeader.LastBlockID.Hash = tmrand.Bytes(32) return &untrusted }, - err: true, - }, - { - prepare: func() libhead.Header { - untrustedAdj.RawHeader.Time = untrustedAdj.RawHeader.Time.Add(time.Minute) - return untrustedAdj - }, - err: true, + err: header.ErrLastHeaderHashMismatch, }, { - prepare: func() libhead.Header { - untrustedAdj.RawHeader.Time = untrustedAdj.RawHeader.Time.Truncate(time.Hour) - return untrustedAdj - }, - err: true, - }, - { - prepare: func() libhead.Header { - untrustedAdj.RawHeader.ChainID = "toaster" - return untrustedAdj - }, - err: true, - }, - { - prepare: func() libhead.Header { - untrustedAdj.RawHeader.Height++ - return untrustedAdj + prepare: func() *header.ExtendedHeader { + untrusted := *untrustedNonAdj + untrusted.Commit = NewTestSuite(t, 2).Commit(RandRawHeader(t)) + return &untrusted }, - err: true, + err: header.ErrVerifyCommitLightTrustingFailed, }, } for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { err := trusted.Verify(test.prepare()) - if test.err { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } + assert.ErrorIs(t, errors.Unwrap(err), test.err) }) } } diff --git a/header/serde.go b/header/serde.go index f4763e3b3b..a511a1352b 100644 --- a/header/serde.go +++ b/header/serde.go @@ -61,7 +61,7 @@ func UnmarshalExtendedHeader(data []byte) (*ExtendedHeader, error) { return nil, err } - return out, out.Validate() + return out, nil } func ExtendedHeaderToProto(eh *ExtendedHeader) (*header_pb.ExtendedHeader, error) { diff --git a/header/verify.go b/header/verify.go deleted file mode 100644 index 18e7f91ea6..0000000000 --- a/header/verify.go +++ /dev/null @@ -1,84 +0,0 @@ -package header - -import ( - "bytes" - "fmt" - "time" - - "github.com/tendermint/tendermint/light" - - libhead "github.com/celestiaorg/go-header" -) - -// Verify validates given untrusted Header against trusted ExtendedHeader. -func (eh *ExtendedHeader) Verify(untrusted libhead.Header) error { - untrst, ok := untrusted.(*ExtendedHeader) - if !ok { - // if the header of the type was given, something very wrong happens - panic(fmt.Sprintf("invalid header type: expected %T, got %T", eh, untrusted)) - } - - if err := eh.verify(untrst); err != nil { - return &libhead.VerifyError{Reason: err} - } - - isAdjacent := eh.Height()+1 == untrst.Height() - if isAdjacent { - // Optimized verification for adjacent headers - // Check the validator hashes are the same - if !bytes.Equal(untrst.ValidatorsHash, eh.NextValidatorsHash) { - return &libhead.VerifyError{ - Reason: fmt.Errorf("expected old header next validators (%X) to match those from new header (%X)", - eh.NextValidatorsHash, - untrst.ValidatorsHash, - ), - } - } - - if !bytes.Equal(untrst.LastHeader(), eh.Hash()) { - return &libhead.VerifyError{ - Reason: fmt.Errorf("expected new header to point to last header hash (%X), but got %X)", - eh.Hash(), - untrst.LastHeader(), - ), - } - } - - return nil - } - - // Ensure that untrusted commit has enough of trusted commit's power. - err := eh.ValidatorSet.VerifyCommitLightTrusting(eh.ChainID(), untrst.Commit, light.DefaultTrustLevel) - if err != nil { - return &libhead.VerifyError{Reason: err} - } - - return nil -} - -// clockDrift defines how much new header's time can drift into -// the future relative to the now time during verification. -var clockDrift = 10 * time.Second - -// verify performs basic verification of untrusted header. -func (eh *ExtendedHeader) verify(untrst libhead.Header) error { - if untrst.Height() <= eh.Height() { - return fmt.Errorf("untrusted header height(%d) <= current trusted header(%d)", untrst.Height(), eh.Height()) - } - - if untrst.ChainID() != eh.ChainID() { - return fmt.Errorf("untrusted header has different chain %s, not %s", untrst.ChainID(), eh.ChainID()) - } - - if !untrst.Time().After(eh.Time()) { - return fmt.Errorf("untrusted header time(%v) must be after current trusted header(%v)", untrst.Time(), eh.Time()) - } - - now := time.Now() - if !untrst.Time().Before(now.Add(clockDrift)) { - return fmt.Errorf( - "new untrusted header has a time from the future %v (now: %v, clockDrift: %v)", untrst.Time(), now, clockDrift) - } - - return nil -} diff --git a/libs/authtoken/authtoken.go b/libs/authtoken/authtoken.go new file mode 100644 index 0000000000..3d6645c972 --- /dev/null +++ b/libs/authtoken/authtoken.go @@ -0,0 +1,36 @@ +package authtoken + +import ( + "encoding/json" + + "github.com/cristalhq/jwt" + "github.com/filecoin-project/go-jsonrpc/auth" + + "github.com/celestiaorg/celestia-node/api/rpc/perms" +) + +// ExtractSignedPermissions returns the permissions granted to the token by the passed signer. +// If the token isn't signed by the signer, it will not pass verification. +func ExtractSignedPermissions(signer jwt.Signer, token string) ([]auth.Permission, error) { + tk, err := jwt.ParseAndVerifyString(token, signer) + if err != nil { + return nil, err + } + p := new(perms.JWTPayload) + err = json.Unmarshal(tk.RawClaims(), p) + if err != nil { + return nil, err + } + return p.Allow, nil +} + +// NewSignedJWT returns a signed JWT token with the passed permissions and signer. +func NewSignedJWT(signer jwt.Signer, permissions []auth.Permission) (string, error) { + token, err := jwt.NewTokenBuilder(signer).Build(&perms.JWTPayload{ + Allow: permissions, + }) + if err != nil { + return "", err + } + return token.InsecureString(), nil +} diff --git a/libs/edssser/edssser.go b/libs/edssser/edssser.go new file mode 100644 index 0000000000..34712b785a --- /dev/null +++ b/libs/edssser/edssser.go @@ -0,0 +1,173 @@ +package edssser + +import ( + "context" + "errors" + "fmt" + "os" + "sync" + "testing" + "time" + + "github.com/ipfs/go-datastore" + + "github.com/celestiaorg/celestia-app/pkg/da" + + "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/edstest" +) + +type Config struct { + EDSSize int + EDSWrites int + EnableLog bool + LogFilePath string + StatLogFreq int + OpTimeout time.Duration +} + +// EDSsser stand for EDS Store Stresser. +type EDSsser struct { + config Config + datastore datastore.Batching + edsstoreMu sync.Mutex + edsstore *eds.Store + + statsFileMu sync.Mutex + statsFile *os.File +} + +func NewEDSsser(path string, datastore datastore.Batching, cfg Config) (*EDSsser, error) { + storeCfg := eds.DefaultParameters() + edsstore, err := eds.NewStore(storeCfg, path, datastore) + if err != nil { + return nil, err + } + + return &EDSsser{ + config: cfg, + datastore: datastore, + edsstore: edsstore, + }, nil +} + +func (ss *EDSsser) Run(ctx context.Context) (stats Stats, err error) { + ss.edsstoreMu.Lock() + defer ss.edsstoreMu.Unlock() + + err = ss.edsstore.Start(ctx) + if err != nil { + return stats, err + } + defer func() { + err = errors.Join(err, ss.edsstore.Stop(ctx)) + }() + + edsHashes, err := ss.edsstore.List() + if err != nil { + return stats, err + } + fmt.Printf("recovered %d EDSes\n\n", len(edsHashes)) + + t := &testing.T{} + for toWrite := ss.config.EDSWrites - len(edsHashes); ctx.Err() == nil && toWrite > 0; toWrite-- { + took, err := ss.put(ctx, t) + + stats.TotalWritten++ + stats.TotalTime += took + if took < stats.MinTime || stats.MinTime == 0 { + stats.MinTime = took + } else if took > stats.MaxTime { + stats.MaxTime = took + } + + if ss.config.EnableLog { + if stats.TotalWritten%ss.config.StatLogFreq == 0 { + stats := stats.Finalize() + fmt.Println(stats) + go func() { + err := ss.dumpStat(stats) + if err != nil { + fmt.Printf("error dumping stats: %s\n", err.Error()) + } + }() + } + if err != nil { + fmt.Printf("ERROR put: %s, took: %v, at: %v\n", err.Error(), took, time.Now()) + continue + } + if took > ss.config.OpTimeout/2 { + fmt.Println("long put", "size", ss.config.EDSSize, "took", took, "at", time.Now()) + continue + } + + fmt.Println("square written", "size", ss.config.EDSSize, "took", took, "at", time.Now()) + } + } + return stats, nil +} + +func (ss *EDSsser) dumpStat(stats Stats) (err error) { + ss.statsFileMu.Lock() + defer ss.statsFileMu.Unlock() + + ss.statsFile, err = os.Create(ss.config.LogFilePath + "/edssser_stats.txt") + if err != nil { + return err + } + + _, err = ss.statsFile.Write([]byte(stats.String())) + if err != nil { + return err + } + + return ss.statsFile.Close() +} + +type Stats struct { + TotalWritten int + TotalTime, MinTime, MaxTime, AvgTime time.Duration + // Deviation ? +} + +func (stats Stats) Finalize() Stats { + if stats.TotalTime != 0 { + stats.AvgTime = stats.TotalTime / time.Duration(stats.TotalWritten) + } + return stats +} + +func (stats Stats) String() string { + return fmt.Sprintf(` +TotalWritten %d +TotalWritingTime %v +MaxTime %s +MinTime %s +AvgTime %s +`, + stats.TotalWritten, + stats.TotalTime, + stats.MaxTime, + stats.MinTime, + stats.AvgTime, + ) +} + +func (ss *EDSsser) put(ctx context.Context, t *testing.T) (time.Duration, error) { + ctx, cancel := context.WithTimeout(ctx, ss.config.OpTimeout) + if ss.config.OpTimeout == 0 { + ctx, cancel = context.WithCancel(ctx) + } + defer cancel() + + // divide by 2 to get ODS size as expected by RandEDS + square := edstest.RandEDS(t, ss.config.EDSSize/2) + dah, err := da.NewDataAvailabilityHeader(square) + if err != nil { + return 0, err + } + + now := time.Now() + err = ss.edsstore.Put(ctx, dah.Hash(), square) + return time.Since(now), err +} diff --git a/libs/pidstore/pidstore.go b/libs/pidstore/pidstore.go new file mode 100644 index 0000000000..17241aa4a9 --- /dev/null +++ b/libs/pidstore/pidstore.go @@ -0,0 +1,97 @@ +package pidstore + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/namespace" + logging "github.com/ipfs/go-log/v2" + "github.com/libp2p/go-libp2p/core/peer" +) + +var ( + storePrefix = datastore.NewKey("pidstore") + peersKey = datastore.NewKey("peers") + + log = logging.Logger("pidstore") +) + +// PeerIDStore is used to store/load peers to/from disk. +type PeerIDStore struct { + ds datastore.Datastore +} + +// NewPeerIDStore creates a new peer ID store backed by the given datastore. +func NewPeerIDStore(ctx context.Context, ds datastore.Datastore) (*PeerIDStore, error) { + pidstore := &PeerIDStore{ + ds: namespace.Wrap(ds, storePrefix), + } + + // check if pidstore is already initialized, and if not, + // initialize the pidstore + exists, err := pidstore.ds.Has(ctx, peersKey) + if err != nil { + return nil, err + } + if !exists { + return pidstore, pidstore.Put(ctx, []peer.ID{}) + } + + // if pidstore exists, ensure its contents are uncorrupted + _, err = pidstore.Load(ctx) + if err != nil { + log.Warn("pidstore: corrupted pidstore detected, resetting...", "err", err) + return pidstore, pidstore.reset(ctx) + } + + return pidstore, nil +} + +// Load loads the peers from datastore and returns them. +func (p *PeerIDStore) Load(ctx context.Context) ([]peer.ID, error) { + log.Debug("Loading peers") + + bin, err := p.ds.Get(ctx, peersKey) + if err != nil { + return nil, fmt.Errorf("pidstore: loading peers from datastore: %w", err) + } + + var peers []peer.ID + err = json.Unmarshal(bin, &peers) + if err != nil { + return nil, fmt.Errorf("pidstore: unmarshalling peer IDs: %w", err) + } + + log.Infow("Loaded peers from disk", "amount", len(peers)) + return peers, nil +} + +// Put persists the given peer IDs to the datastore. +func (p *PeerIDStore) Put(ctx context.Context, peers []peer.ID) error { + log.Debugw("Persisting peers to disk", "amount", len(peers)) + + bin, err := json.Marshal(peers) + if err != nil { + return fmt.Errorf("pidstore: marshal peerlist: %w", err) + } + + if err = p.ds.Put(ctx, peersKey, bin); err != nil { + return fmt.Errorf("pidstore: error writing to datastore: %w", err) + } + + log.Infow("Persisted peers successfully", "amount", len(peers)) + return nil +} + +// reset resets the pidstore in case of corruption. +func (p *PeerIDStore) reset(ctx context.Context) error { + log.Warn("pidstore: resetting the pidstore...") + err := p.ds.Delete(ctx, peersKey) + if err != nil { + return fmt.Errorf("pidstore: error resetting datastore: %w", err) + } + + return p.Put(ctx, []peer.ID{}) +} diff --git a/libs/pidstore/pidstore_test.go b/libs/pidstore/pidstore_test.go new file mode 100644 index 0000000000..4a35783db3 --- /dev/null +++ b/libs/pidstore/pidstore_test.go @@ -0,0 +1,93 @@ +package pidstore + +import ( + "context" + "crypto/rand" + "crypto/rsa" + "testing" + "time" + + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/namespace" + "github.com/ipfs/go-datastore/sync" + "github.com/libp2p/go-libp2p/core/crypto" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPutLoad(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer t.Cleanup(cancel) + + ds := sync.MutexWrap(datastore.NewMapDatastore()) + + t.Run("uninitialized-pidstore", func(t *testing.T) { + testPutLoad(ctx, ds, t) + }) + t.Run("initialized-pidstore", func(t *testing.T) { + testPutLoad(ctx, ds, t) + }) +} + +// TestCorruptedPidstore tests whether a pidstore can detect +// corruption and reset itself on construction. +func TestCorruptedPidstore(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer t.Cleanup(cancel) + + ds := sync.MutexWrap(datastore.NewMapDatastore()) + + // intentionally corrupt the store + wrappedDS := namespace.Wrap(ds, storePrefix) + err := wrappedDS.Put(ctx, peersKey, []byte("corrupted")) + require.NoError(t, err) + + pidstore, err := NewPeerIDStore(ctx, ds) + require.NoError(t, err) + + got, err := pidstore.Load(ctx) + require.NoError(t, err) + assert.Equal(t, []peer.ID{}, got) +} + +func testPutLoad(ctx context.Context, ds datastore.Datastore, t *testing.T) { + peerstore, err := NewPeerIDStore(ctx, ds) + require.NoError(t, err) + + ids, err := generateRandomPeerList(10) + require.NoError(t, err) + + err = peerstore.Put(ctx, ids) + require.NoError(t, err) + + retrievedPeerlist, err := peerstore.Load(ctx) + require.NoError(t, err) + + assert.Equal(t, len(ids), len(retrievedPeerlist)) + assert.Equal(t, ids, retrievedPeerlist) +} + +func generateRandomPeerList(length int) ([]peer.ID, error) { + peerlist := make([]peer.ID, length) + for i := range peerlist { + key, err := rsa.GenerateKey(rand.Reader, 2096) + if err != nil { + return nil, err + } + + _, pubkey, err := crypto.KeyPairFromStdKey(key) + if err != nil { + return nil, err + } + + peerID, err := peer.IDFromPublicKey(pubkey) + if err != nil { + return nil, err + } + + peerlist[i] = peerID + } + + return peerlist, nil +} diff --git a/libs/utils/address.go b/libs/utils/address.go index 5971bf5a98..c20d11ad06 100644 --- a/libs/utils/address.go +++ b/libs/utils/address.go @@ -1,11 +1,14 @@ package utils import ( + "errors" "fmt" "net" "strings" ) +var ErrInvalidIP = errors.New("invalid IP address or hostname given") + // SanitizeAddr trims leading protocol scheme and port from the given // IP address or hostname if present. func SanitizeAddr(addr string) (string, error) { @@ -16,7 +19,7 @@ func SanitizeAddr(addr string) (string, error) { addr = strings.TrimSuffix(addr, "/") addr = strings.Split(addr, ":")[0] if addr == "" { - return "", fmt.Errorf("invalid IP address or hostname given: %s", original) + return "", fmt.Errorf("%w: %s", ErrInvalidIP, original) } return addr, nil } @@ -29,12 +32,14 @@ func ValidateAddr(addr string) (string, error) { return addr, err } - if ip := net.ParseIP(addr); ip == nil { - _, err = net.LookupHost(addr) - if err != nil { - return addr, fmt.Errorf("could not resolve hostname or ip: %w", err) - } + ip := net.ParseIP(addr) + if ip != nil { + return addr, nil } - return addr, nil + resolved, err := net.ResolveIPAddr("ip4", addr) + if err != nil { + return addr, err + } + return resolved.String(), nil } diff --git a/libs/utils/address_test.go b/libs/utils/address_test.go index c914a7d853..48a7747a4a 100644 --- a/libs/utils/address_test.go +++ b/libs/utils/address_test.go @@ -1,6 +1,7 @@ package utils import ( + "net" "testing" "github.com/stretchr/testify/require" @@ -10,40 +11,65 @@ func TestSanitizeAddr(t *testing.T) { var tests = []struct { addr string want string + err error }{ // Testcase: trims protocol prefix {addr: "http://celestia.org", want: "celestia.org"}, + // Testcase: protocol prefix trimmed already + {addr: "celestia.org", want: "celestia.org"}, // Testcase: trims protocol prefix, and trims port and trailing slash suffix {addr: "tcp://192.168.42.42:5050/", want: "192.168.42.42"}, + // Testcase: invariant ip + {addr: "192.168.42.42", want: "192.168.42.42"}, + // Testcase: empty addr + {addr: "", want: "", err: ErrInvalidIP}, } for _, tt := range tests { t.Run(tt.addr, func(t *testing.T) { got, err := SanitizeAddr(tt.addr) - require.NoError(t, err) require.Equal(t, tt.want, got) + require.ErrorIs(t, err, tt.err) }) } } func TestValidateAddr(t *testing.T) { + type want struct { + addr string + unresolved bool + } var tests = []struct { addr string - want string + want want }{ // Testcase: ip is valid - {addr: "192.168.42.42:5050", want: "192.168.42.42"}, - // Testcase: hostname is valid - {addr: "https://celestia.org", want: "celestia.org"}, + {addr: "192.168.42.42:5050", want: want{addr: "192.168.42.42"}}, + // Testcase: ip is valid, no port + {addr: "192.168.42.42", want: want{addr: "192.168.42.42"}}, // Testcase: resolves localhost - {addr: "http://localhost:8080/", want: "localhost"}, + {addr: "http://localhost:8080/", want: want{unresolved: true}}, + // Testcase: hostname is valid + {addr: "https://celestia.org", want: want{unresolved: true}}, + // Testcase: hostname is valid, but no schema + {addr: "celestia.org", want: want{unresolved: true}}, } for _, tt := range tests { t.Run(tt.addr, func(t *testing.T) { got, err := ValidateAddr(tt.addr) require.NoError(t, err) - require.Equal(t, tt.want, got) + + // validate that returned value is ip + if ip := net.ParseIP(got); ip == nil { + t.Fatalf("empty ip") + } + + if tt.want.unresolved { + // unresolved addr has no addr to compare with + return + } + require.Equal(t, tt.want.addr, got) }) } } diff --git a/libs/utils/fs.go b/libs/utils/fs.go index d67e9a1eaa..4ad8b6443e 100644 --- a/libs/utils/fs.go +++ b/libs/utils/fs.go @@ -1,6 +1,8 @@ package utils -import "os" +import ( + "os" +) // Exists checks whether file or directory exists under the given 'path' on the system. func Exists(path string) bool { diff --git a/libs/utils/resetctx.go b/libs/utils/resetctx.go new file mode 100644 index 0000000000..a108cc27b4 --- /dev/null +++ b/libs/utils/resetctx.go @@ -0,0 +1,14 @@ +package utils + +import ( + "context" +) + +// ResetContextOnError returns a fresh context if the given context has an error. +func ResetContextOnError(ctx context.Context) context.Context { + if ctx.Err() != nil { + ctx = context.Background() + } + + return ctx +} diff --git a/libs/utils/square.go b/libs/utils/square.go index ce2663fd81..68d7fc5ce7 100644 --- a/libs/utils/square.go +++ b/libs/utils/square.go @@ -1,6 +1,8 @@ package utils -import "math" +import ( + "math" +) // SquareSize returns the size of the square based on the given amount of shares. func SquareSize(lenShares int) uint64 { diff --git a/logs/logs.go b/logs/logs.go index 4ae1cddd3f..5cb9ed16c6 100644 --- a/logs/logs.go +++ b/logs/logs.go @@ -1,6 +1,8 @@ package logs -import logging "github.com/ipfs/go-log/v2" +import ( + logging "github.com/ipfs/go-log/v2" +) func SetAllLoggers(level logging.LogLevel) { logging.SetAllLoggers(level) @@ -11,6 +13,7 @@ func SetAllLoggers(level logging.LogLevel) { _ = logging.SetLogLevel("dht", "ERROR") _ = logging.SetLogLevel("swarm2", "WARN") _ = logging.SetLogLevel("bitswap", "WARN") + _ = logging.SetLogLevel("bitswap-client", "WARN") _ = logging.SetLogLevel("connmgr", "WARN") _ = logging.SetLogLevel("nat", "INFO") _ = logging.SetLogLevel("dht/RtRefreshManager", "FATAL") @@ -21,6 +24,8 @@ func SetAllLoggers(level logging.LogLevel) { _ = logging.SetLogLevel("net/identify", "ERROR") _ = logging.SetLogLevel("shrex/nd", "WARN") _ = logging.SetLogLevel("shrex/eds", "WARN") + _ = logging.SetLogLevel("dagstore", "WARN") + _ = logging.SetLogLevel("dagstore/upgrader", "WARN") _ = logging.SetLogLevel("fx", "FATAL") } diff --git a/nodebuilder/blob/blob.go b/nodebuilder/blob/blob.go new file mode 100644 index 0000000000..f87105541a --- /dev/null +++ b/nodebuilder/blob/blob.go @@ -0,0 +1,75 @@ +package blob + +import ( + "context" + + "github.com/celestiaorg/celestia-node/blob" + "github.com/celestiaorg/celestia-node/share" +) + +var _ Module = (*API)(nil) + +// Module defines the API related to interacting with the blobs +// +//go:generate mockgen -destination=mocks/api.go -package=mocks . Module +type Module interface { + // Submit sends Blobs and reports the height in which they were included. + // Allows sending multiple Blobs atomically synchronously. + // Uses default wallet registered on the Node. + Submit(_ context.Context, _ []*blob.Blob, _ blob.GasPrice) (height uint64, _ error) + // Get retrieves the blob by commitment under the given namespace and height. + Get(_ context.Context, height uint64, _ share.Namespace, _ blob.Commitment) (*blob.Blob, error) + // GetAll returns all blobs at the given height under the given namespaces. + GetAll(_ context.Context, height uint64, _ []share.Namespace) ([]*blob.Blob, error) + // GetProof retrieves proofs in the given namespaces at the given height by commitment. + GetProof(_ context.Context, height uint64, _ share.Namespace, _ blob.Commitment) (*blob.Proof, error) + // Included checks whether a blob's given commitment(Merkle subtree root) is included at + // given height and under the namespace. + Included(_ context.Context, height uint64, _ share.Namespace, _ *blob.Proof, _ blob.Commitment) (bool, error) +} + +type API struct { + Internal struct { + Submit func(context.Context, []*blob.Blob, blob.GasPrice) (uint64, error) `perm:"write"` + Get func(context.Context, uint64, share.Namespace, blob.Commitment) (*blob.Blob, error) `perm:"read"` + GetAll func(context.Context, uint64, []share.Namespace) ([]*blob.Blob, error) `perm:"read"` + GetProof func(context.Context, uint64, share.Namespace, blob.Commitment) (*blob.Proof, error) `perm:"read"` + Included func(context.Context, uint64, share.Namespace, *blob.Proof, blob.Commitment) (bool, error) `perm:"read"` + } +} + +func (api *API) Submit(ctx context.Context, blobs []*blob.Blob, gasPrice blob.GasPrice) (uint64, error) { + return api.Internal.Submit(ctx, blobs, gasPrice) +} + +func (api *API) Get( + ctx context.Context, + height uint64, + namespace share.Namespace, + commitment blob.Commitment, +) (*blob.Blob, error) { + return api.Internal.Get(ctx, height, namespace, commitment) +} + +func (api *API) GetAll(ctx context.Context, height uint64, namespaces []share.Namespace) ([]*blob.Blob, error) { + return api.Internal.GetAll(ctx, height, namespaces) +} + +func (api *API) GetProof( + ctx context.Context, + height uint64, + namespace share.Namespace, + commitment blob.Commitment, +) (*blob.Proof, error) { + return api.Internal.GetProof(ctx, height, namespace, commitment) +} + +func (api *API) Included( + ctx context.Context, + height uint64, + namespace share.Namespace, + proof *blob.Proof, + commitment blob.Commitment, +) (bool, error) { + return api.Internal.Included(ctx, height, namespace, proof, commitment) +} diff --git a/nodebuilder/blob/cmd/blob.go b/nodebuilder/blob/cmd/blob.go new file mode 100644 index 0000000000..7862eaa204 --- /dev/null +++ b/nodebuilder/blob/cmd/blob.go @@ -0,0 +1,222 @@ +package cmd + +import ( + "encoding/base64" + "fmt" + "reflect" + "strconv" + + "github.com/spf13/cobra" + + "github.com/celestiaorg/celestia-node/blob" + cmdnode "github.com/celestiaorg/celestia-node/cmd" + "github.com/celestiaorg/celestia-node/share" +) + +var ( + base64Flag bool + + gasPrice float64 +) + +func init() { + Cmd.AddCommand(getCmd, getAllCmd, submitCmd, getProofCmd) + + getCmd.PersistentFlags().BoolVar( + &base64Flag, + "base64", + false, + "printed blob's data a base64 string", + ) + + getAllCmd.PersistentFlags().BoolVar( + &base64Flag, + "base64", + false, + "printed blob's data as a base64 string", + ) + + submitCmd.PersistentFlags().Float64Var( + &gasPrice, + "gas.price", + float64(blob.DefaultGasPrice()), + "specifies gas price (in utia) for blob submission.\n"+ + "Gas price will be set to default (0.002) if no value is passed", + ) +} + +var Cmd = &cobra.Command{ + Use: "blob [command]", + Short: "Allows to interact with the Blob Service via JSON-RPC", + Args: cobra.NoArgs, + PersistentPreRunE: cmdnode.InitClient, +} + +var getCmd = &cobra.Command{ + Use: "get [height] [namespace] [commitment]", + Args: cobra.ExactArgs(3), + Short: "Returns the blob for the given namespace by commitment at a particular height.", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + height, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a height:%v", err) + } + + namespace, err := cmdnode.ParseV0Namespace(args[1]) + if err != nil { + return fmt.Errorf("error parsing a namespace:%v", err) + } + + commitment, err := base64.StdEncoding.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("error parsing a commitment:%v", err) + } + + blob, err := client.Blob.Get(cmd.Context(), height, namespace, commitment) + + formatter := formatData + if base64Flag || err != nil { + formatter = nil + } + return cmdnode.PrintOutput(blob, err, formatter) + }, +} + +var getAllCmd = &cobra.Command{ + Use: "get-all [height] [namespace]", + Args: cobra.ExactArgs(2), + Short: "Returns all blobs for the given namespace at a particular height.", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + height, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a height:%v", err) + } + + namespace, err := cmdnode.ParseV0Namespace(args[1]) + if err != nil { + return fmt.Errorf("error parsing a namespace:%v", err) + } + + blobs, err := client.Blob.GetAll(cmd.Context(), height, []share.Namespace{namespace}) + formatter := formatData + if base64Flag || err != nil { + formatter = nil + } + return cmdnode.PrintOutput(blobs, err, formatter) + }, +} + +var submitCmd = &cobra.Command{ + Use: "submit [namespace] [blobData]", + Args: cobra.ExactArgs(2), + Short: "Submit the blob at the given namespace.\n" + + "Note:\n" + + "* only one blob is allowed to submit through the RPC.\n" + + "* fee and gas limit params will be calculated automatically.\n", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + namespace, err := cmdnode.ParseV0Namespace(args[0]) + if err != nil { + return fmt.Errorf("error parsing a namespace:%v", err) + } + + parsedBlob, err := blob.NewBlobV0(namespace, []byte(args[1])) + if err != nil { + return fmt.Errorf("error creating a blob:%v", err) + } + + height, err := client.Blob.Submit( + cmd.Context(), + []*blob.Blob{parsedBlob}, + blob.GasPrice(gasPrice), + ) + + response := struct { + Height uint64 `json:"height"` + Commitment blob.Commitment `json:"commitment"` + }{ + Height: height, + Commitment: parsedBlob.Commitment, + } + return cmdnode.PrintOutput(response, err, nil) + }, +} + +var getProofCmd = &cobra.Command{ + Use: "get-proof [height] [namespace] [commitment]", + Args: cobra.ExactArgs(3), + Short: "Retrieves the blob in the given namespaces at the given height by commitment and returns its Proof.", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + height, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a height:%v", err) + } + + namespace, err := cmdnode.ParseV0Namespace(args[1]) + if err != nil { + return fmt.Errorf("error parsing a namespace:%v", err) + } + + commitment, err := base64.StdEncoding.DecodeString(args[2]) + if err != nil { + return fmt.Errorf("error parsing a commitment:%v", err) + } + + proof, err := client.Blob.GetProof(cmd.Context(), height, namespace, commitment) + return cmdnode.PrintOutput(proof, err, nil) + }, +} + +func formatData(data interface{}) interface{} { + type tempBlob struct { + Namespace []byte `json:"namespace"` + Data string `json:"data"` + ShareVersion uint32 `json:"share_version"` + Commitment []byte `json:"commitment"` + } + + if reflect.TypeOf(data).Kind() == reflect.Slice { + blobs := data.([]*blob.Blob) + result := make([]tempBlob, len(blobs)) + for i, b := range blobs { + result[i] = tempBlob{ + Namespace: b.Namespace(), + Data: string(b.Data), + ShareVersion: b.ShareVersion, + Commitment: b.Commitment, + } + } + return result + } + + b := data.(*blob.Blob) + return tempBlob{ + Namespace: b.Namespace(), + Data: string(b.Data), + ShareVersion: b.ShareVersion, + Commitment: b.Commitment, + } +} diff --git a/nodebuilder/blob/mocks/api.go b/nodebuilder/blob/mocks/api.go new file mode 100644 index 0000000000..0898e70459 --- /dev/null +++ b/nodebuilder/blob/mocks/api.go @@ -0,0 +1,112 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/celestiaorg/celestia-node/nodebuilder/blob (interfaces: Module) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + blob "github.com/celestiaorg/celestia-node/blob" + share "github.com/celestiaorg/celestia-node/share" + gomock "github.com/golang/mock/gomock" +) + +// MockModule is a mock of Module interface. +type MockModule struct { + ctrl *gomock.Controller + recorder *MockModuleMockRecorder +} + +// MockModuleMockRecorder is the mock recorder for MockModule. +type MockModuleMockRecorder struct { + mock *MockModule +} + +// NewMockModule creates a new mock instance. +func NewMockModule(ctrl *gomock.Controller) *MockModule { + mock := &MockModule{ctrl: ctrl} + mock.recorder = &MockModuleMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockModule) EXPECT() *MockModuleMockRecorder { + return m.recorder +} + +// Get mocks base method. +func (m *MockModule) Get(arg0 context.Context, arg1 uint64, arg2 share.Namespace, arg3 blob.Commitment) (*blob.Blob, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*blob.Blob) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockModuleMockRecorder) Get(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockModule)(nil).Get), arg0, arg1, arg2, arg3) +} + +// GetAll mocks base method. +func (m *MockModule) GetAll(arg0 context.Context, arg1 uint64, arg2 []share.Namespace) ([]*blob.Blob, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAll", arg0, arg1, arg2) + ret0, _ := ret[0].([]*blob.Blob) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAll indicates an expected call of GetAll. +func (mr *MockModuleMockRecorder) GetAll(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAll", reflect.TypeOf((*MockModule)(nil).GetAll), arg0, arg1, arg2) +} + +// GetProof mocks base method. +func (m *MockModule) GetProof(arg0 context.Context, arg1 uint64, arg2 share.Namespace, arg3 blob.Commitment) (*blob.Proof, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProof", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(*blob.Proof) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProof indicates an expected call of GetProof. +func (mr *MockModuleMockRecorder) GetProof(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProof", reflect.TypeOf((*MockModule)(nil).GetProof), arg0, arg1, arg2, arg3) +} + +// Included mocks base method. +func (m *MockModule) Included(arg0 context.Context, arg1 uint64, arg2 share.Namespace, arg3 *blob.Proof, arg4 blob.Commitment) (bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Included", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Included indicates an expected call of Included. +func (mr *MockModuleMockRecorder) Included(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Included", reflect.TypeOf((*MockModule)(nil).Included), arg0, arg1, arg2, arg3, arg4) +} + +// Submit mocks base method. +func (m *MockModule) Submit(arg0 context.Context, arg1 []*blob.Blob, arg2 blob.GasPrice) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Submit", arg0, arg1, arg2) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Submit indicates an expected call of Submit. +func (mr *MockModuleMockRecorder) Submit(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Submit", reflect.TypeOf((*MockModule)(nil).Submit), arg0, arg1, arg2) +} diff --git a/nodebuilder/blob/module.go b/nodebuilder/blob/module.go new file mode 100644 index 0000000000..76e7677725 --- /dev/null +++ b/nodebuilder/blob/module.go @@ -0,0 +1,28 @@ +package blob + +import ( + "context" + + "go.uber.org/fx" + + "github.com/celestiaorg/celestia-node/blob" + "github.com/celestiaorg/celestia-node/header" + headerService "github.com/celestiaorg/celestia-node/nodebuilder/header" + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/state" +) + +func ConstructModule() fx.Option { + return fx.Module("blob", + fx.Provide( + func(service headerService.Module) func(context.Context, uint64) (*header.ExtendedHeader, error) { + return service.GetByHeight + }), + fx.Provide(func( + state *state.CoreAccessor, + sGetter share.Getter, + getByHeightFn func(context.Context, uint64) (*header.ExtendedHeader, error), + ) Module { + return blob.NewService(state, sGetter, getByHeightFn) + })) +} diff --git a/nodebuilder/config.go b/nodebuilder/config.go index 3607aa593e..41f24d6d3d 100644 --- a/nodebuilder/config.go +++ b/nodebuilder/config.go @@ -25,6 +25,7 @@ type ConfigLoader func() (*Config, error) // Config is main configuration structure for a Node. // It combines configuration units for all Node subsystems. type Config struct { + Node node.Config Core core.Config State state.Config P2P p2p.Config @@ -39,6 +40,7 @@ type Config struct { // NOTE: Currently, configs are identical, but this will change. func DefaultConfig(tp node.Type) *Config { commonConfig := &Config{ + Node: node.DefaultConfig(tp), Core: core.DefaultConfig(), State: state.DefaultConfig(), P2P: p2p.DefaultConfig(tp), @@ -112,7 +114,7 @@ func removeConfig(path string) error { func UpdateConfig(tp node.Type, path string) (err error) { path, err = storePath(path) if err != nil { - return + return err } flock, err := fslock.Lock(lockPath(path)) @@ -120,7 +122,7 @@ func UpdateConfig(tp node.Type, path string) (err error) { if err == fslock.ErrLocked { err = ErrOpened } - return + return err } defer flock.Unlock() //nolint: errcheck @@ -129,18 +131,18 @@ func UpdateConfig(tp node.Type, path string) (err error) { cfgPath := configPath(path) cfg, err := LoadConfig(cfgPath) if err != nil { - return + return err } cfg, err = updateConfig(cfg, newCfg) if err != nil { - return + return err } // save the updated config err = removeConfig(cfgPath) if err != nil { - return + return err } return SaveConfig(cfgPath, cfg) } diff --git a/nodebuilder/config_test.go b/nodebuilder/config_test.go index db9af3a64d..e7b64b0aed 100644 --- a/nodebuilder/config_test.go +++ b/nodebuilder/config_test.go @@ -69,7 +69,7 @@ var outdatedConfig = ` KeyringBackend = "test" [P2P] - ListenAddresses = ["/ip4/0.0.0.0/udp/2121/quic-v1", "/ip6/::/udp/2121/quic-v1", "/ip4/0.0.0.0/tcp/2121", + ListenAddresses = ["/ip4/0.0.0.0/udp/2121/quic-v1", "/ip6/::/udp/2121/quic-v1", "/ip4/0.0.0.0/tcp/2121", "/ip6/::/tcp/2121"] AnnounceAddresses = [] NoAnnounceAddresses = ["/ip4/0.0.0.0/udp/2121/quic-v1", "/ip4/127.0.0.1/udp/2121/quic-v1", "/ip6/::/udp/2121/quic-v1", @@ -91,7 +91,7 @@ var outdatedConfig = ` [Gateway] Address = "0.0.0.0" Port = "26659" - Enabled = true + Enabled = true [Share] PeersLimit = 5 diff --git a/nodebuilder/core/config.go b/nodebuilder/core/config.go index c0261c7c86..bb5eea5b83 100644 --- a/nodebuilder/core/config.go +++ b/nodebuilder/core/config.go @@ -7,6 +7,8 @@ import ( "github.com/celestiaorg/celestia-node/libs/utils" ) +var MetricsEnabled bool + // Config combines all configuration fields for managing the relationship with a Core node. type Config struct { IP string @@ -18,14 +20,18 @@ type Config struct { // node's connection to a Celestia-Core endpoint. func DefaultConfig() Config { return Config{ - IP: "0.0.0.0", - RPCPort: "0", - GRPCPort: "0", + IP: "", + RPCPort: "26657", + GRPCPort: "9090", } } // Validate performs basic validation of the config. func (cfg *Config) Validate() error { + if !cfg.IsEndpointConfigured() { + return nil + } + ip, err := utils.ValidateAddr(cfg.IP) if err != nil { return err @@ -41,3 +47,9 @@ func (cfg *Config) Validate() error { } return nil } + +// IsEndpointConfigured returns whether a core endpoint has been set +// on the config (true if set). +func (cfg *Config) IsEndpointConfigured() bool { + return cfg.IP != "" +} diff --git a/nodebuilder/core/flags.go b/nodebuilder/core/flags.go index b98c7acd07..9cbed9b277 100644 --- a/nodebuilder/core/flags.go +++ b/nodebuilder/core/flags.go @@ -21,7 +21,8 @@ func Flags() *flag.FlagSet { coreFlag, "", "Indicates node to connect to the given core node. "+ - "Example: , 127.0.0.1. Assumes RPC port 26657 and gRPC port 9090 as default unless otherwise specified.", + "Example: , 127.0.0.1. , subdomain.domain.tld "+ + "Assumes RPC port 26657 and gRPC port 9090 as default unless otherwise specified.", ) flags.String( coreRPCFlag, @@ -55,5 +56,5 @@ func ParseFlags( cfg.IP = coreIP cfg.RPCPort = rpc cfg.GRPCPort = grpc - return nil + return cfg.Validate() } diff --git a/nodebuilder/core/module.go b/nodebuilder/core/module.go index 02863eae7e..7c5c9e6bfd 100644 --- a/nodebuilder/core/module.go +++ b/nodebuilder/core/module.go @@ -35,7 +35,20 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option return fx.Module("core", baseComponents, fx.Provide(core.NewBlockFetcher), - fxutil.ProvideAs(core.NewExchange, new(libhead.Exchange[*header.ExtendedHeader])), + fxutil.ProvideAs( + func( + fetcher *core.BlockFetcher, + store *eds.Store, + construct header.ConstructFn, + ) (*core.Exchange, error) { + var opts []core.Option + if MetricsEnabled { + opts = append(opts, core.WithMetrics()) + } + + return core.NewExchange(fetcher, store, construct, opts...) + }, + new(libhead.Exchange[*header.ExtendedHeader])), fx.Invoke(fx.Annotate( func( bcast libhead.Broadcaster[*header.ExtendedHeader], @@ -43,8 +56,14 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option pubsub *shrexsub.PubSub, construct header.ConstructFn, store *eds.Store, - ) *core.Listener { - return core.NewListener(bcast, fetcher, pubsub.Broadcast, construct, store, p2p.BlockTime) + chainID p2p.Network, + ) (*core.Listener, error) { + opts := []core.Option{core.WithChainID(chainID)} + if MetricsEnabled { + opts = append(opts, core.WithMetrics()) + } + + return core.NewListener(bcast, fetcher, pubsub.Broadcast, construct, store, p2p.BlockTime, opts...) }, fx.OnStart(func(ctx context.Context, listener *core.Listener) error { return listener.Start(ctx) diff --git a/nodebuilder/da/da.go b/nodebuilder/da/da.go new file mode 100644 index 0000000000..0d604d769f --- /dev/null +++ b/nodebuilder/da/da.go @@ -0,0 +1,54 @@ +package da + +import ( + "context" + + "github.com/rollkit/go-da" +) + +//go:generate mockgen -destination=mocks/api.go -package=mocks . Module +type Module interface { + da.DA +} + +// API is a wrapper around Module for the RPC. +// TODO(@distractedm1nd): These structs need to be autogenerated. +type API struct { + Internal struct { + MaxBlobSize func(ctx context.Context) (uint64, error) `perm:"read"` + Get func(ctx context.Context, ids []da.ID, ns da.Namespace) ([]da.Blob, error) `perm:"read"` + GetIDs func(ctx context.Context, height uint64, ns da.Namespace) ([]da.ID, error) `perm:"read"` + GetProofs func(ctx context.Context, ids []da.ID, ns da.Namespace) ([]da.Proof, error) `perm:"read"` + Commit func(ctx context.Context, blobs []da.Blob, ns da.Namespace) ([]da.Commitment, error) `perm:"read"` + Validate func(context.Context, []da.ID, []da.Proof, da.Namespace) ([]bool, error) `perm:"read"` + Submit func(context.Context, []da.Blob, float64, da.Namespace) ([]da.ID, error) `perm:"write"` + } +} + +func (api *API) MaxBlobSize(ctx context.Context) (uint64, error) { + return api.Internal.MaxBlobSize(ctx) +} + +func (api *API) Get(ctx context.Context, ids []da.ID, ns da.Namespace) ([]da.Blob, error) { + return api.Internal.Get(ctx, ids, ns) +} + +func (api *API) GetIDs(ctx context.Context, height uint64, ns da.Namespace) ([]da.ID, error) { + return api.Internal.GetIDs(ctx, height, ns) +} + +func (api *API) GetProofs(ctx context.Context, ids []da.ID, ns da.Namespace) ([]da.Proof, error) { + return api.Internal.GetProofs(ctx, ids, ns) +} + +func (api *API) Commit(ctx context.Context, blobs []da.Blob, ns da.Namespace) ([]da.Commitment, error) { + return api.Internal.Commit(ctx, blobs, ns) +} + +func (api *API) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, ns da.Namespace) ([]bool, error) { + return api.Internal.Validate(ctx, ids, proofs, ns) +} + +func (api *API) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64, ns da.Namespace) ([]da.ID, error) { + return api.Internal.Submit(ctx, blobs, gasPrice, ns) +} diff --git a/nodebuilder/da/mocks/api.go b/nodebuilder/da/mocks/api.go new file mode 100644 index 0000000000..5895240906 --- /dev/null +++ b/nodebuilder/da/mocks/api.go @@ -0,0 +1,140 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/celestiaorg/celestia-node/nodebuilder/da (interfaces: Module) + +// Package mocks is a generated GoMock package. +package mocks + +import ( + context "context" + reflect "reflect" + + gomock "github.com/golang/mock/gomock" +) + +// MockModule is a mock of Module interface. +type MockModule struct { + ctrl *gomock.Controller + recorder *MockModuleMockRecorder +} + +// MockModuleMockRecorder is the mock recorder for MockModule. +type MockModuleMockRecorder struct { + mock *MockModule +} + +// NewMockModule creates a new mock instance. +func NewMockModule(ctrl *gomock.Controller) *MockModule { + mock := &MockModule{ctrl: ctrl} + mock.recorder = &MockModuleMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockModule) EXPECT() *MockModuleMockRecorder { + return m.recorder +} + +// Commit mocks base method. +func (m *MockModule) Commit(arg0 context.Context, arg1 [][]byte, arg2 []byte) ([][]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Commit", arg0, arg1, arg2) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Commit indicates an expected call of Commit. +func (mr *MockModuleMockRecorder) Commit(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Commit", reflect.TypeOf((*MockModule)(nil).Commit), arg0, arg1, arg2) +} + +// Get mocks base method. +func (m *MockModule) Get(arg0 context.Context, arg1 [][]byte, arg2 []byte) ([][]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", arg0, arg1, arg2) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockModuleMockRecorder) Get(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockModule)(nil).Get), arg0, arg1, arg2) +} + +// GetIDs mocks base method. +func (m *MockModule) GetIDs(arg0 context.Context, arg1 uint64, arg2 []byte) ([][]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetIDs", arg0, arg1, arg2) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetIDs indicates an expected call of GetIDs. +func (mr *MockModuleMockRecorder) GetIDs(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIDs", reflect.TypeOf((*MockModule)(nil).GetIDs), arg0, arg1, arg2) +} + +// GetProofs mocks base method. +func (m *MockModule) GetProofs(arg0 context.Context, arg1 [][]byte, arg2 []byte) ([][]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetProofs", arg0, arg1, arg2) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetProofs indicates an expected call of GetProofs. +func (mr *MockModuleMockRecorder) GetProofs(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetProofs", reflect.TypeOf((*MockModule)(nil).GetProofs), arg0, arg1, arg2) +} + +// MaxBlobSize mocks base method. +func (m *MockModule) MaxBlobSize(arg0 context.Context) (uint64, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MaxBlobSize", arg0) + ret0, _ := ret[0].(uint64) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MaxBlobSize indicates an expected call of MaxBlobSize. +func (mr *MockModuleMockRecorder) MaxBlobSize(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxBlobSize", reflect.TypeOf((*MockModule)(nil).MaxBlobSize), arg0) +} + +// Submit mocks base method. +func (m *MockModule) Submit(arg0 context.Context, arg1 [][]byte, arg2 float64, arg3 []byte) ([][]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Submit", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([][]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Submit indicates an expected call of Submit. +func (mr *MockModuleMockRecorder) Submit(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Submit", reflect.TypeOf((*MockModule)(nil).Submit), arg0, arg1, arg2, arg3) +} + +// Validate mocks base method. +func (m *MockModule) Validate(arg0 context.Context, arg1, arg2 [][]byte, arg3 []byte) ([]bool, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Validate", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]bool) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Validate indicates an expected call of Validate. +func (mr *MockModuleMockRecorder) Validate(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Validate", reflect.TypeOf((*MockModule)(nil).Validate), arg0, arg1, arg2, arg3) +} diff --git a/nodebuilder/da/module.go b/nodebuilder/da/module.go new file mode 100644 index 0000000000..b119d11076 --- /dev/null +++ b/nodebuilder/da/module.go @@ -0,0 +1,14 @@ +package da + +import ( + "go.uber.org/fx" +) + +func ConstructModule() fx.Option { + return fx.Module("da", + fx.Provide(NewService), + fx.Provide(func(serv *Service) Module { + return serv + }), + ) +} diff --git a/nodebuilder/da/service.go b/nodebuilder/da/service.go new file mode 100644 index 0000000000..b775e10396 --- /dev/null +++ b/nodebuilder/da/service.go @@ -0,0 +1,188 @@ +package da + +import ( + "context" + "encoding/binary" + "fmt" + "strings" + + logging "github.com/ipfs/go-log/v2" + "github.com/rollkit/go-da" + + "github.com/celestiaorg/celestia-app/pkg/appconsts" + + "github.com/celestiaorg/celestia-node/blob" + nodeblob "github.com/celestiaorg/celestia-node/nodebuilder/blob" + "github.com/celestiaorg/celestia-node/share" +) + +var _ da.DA = (*Service)(nil) + +var log = logging.Logger("go-da") + +// heightLen is a length (in bytes) of serialized height. +// +// This is 8 as uint64 consist of 8 bytes. +const heightLen = 8 + +type Service struct { + blobServ nodeblob.Module +} + +func NewService(blobMod nodeblob.Module) *Service { + return &Service{ + blobServ: blobMod, + } +} + +// MaxBlobSize returns the max blob size +func (s *Service) MaxBlobSize(context.Context) (uint64, error) { + return appconsts.DefaultMaxBytes, nil +} + +// Get returns Blob for each given ID, or an error. +func (s *Service) Get(ctx context.Context, ids []da.ID, ns da.Namespace) ([]da.Blob, error) { + blobs := make([]da.Blob, 0, len(ids)) + for _, id := range ids { + height, commitment := SplitID(id) + log.Debugw("getting blob", "height", height, "commitment", commitment, "namespace", share.Namespace(ns)) + currentBlob, err := s.blobServ.Get(ctx, height, ns, commitment) + log.Debugw("got blob", "height", height, "commitment", commitment, "namespace", share.Namespace(ns)) + if err != nil { + return nil, err + } + blobs = append(blobs, currentBlob.Data) + } + return blobs, nil +} + +// GetIDs returns IDs of all Blobs located in DA at given height. +func (s *Service) GetIDs(ctx context.Context, height uint64, namespace da.Namespace) ([]da.ID, error) { + var ids []da.ID //nolint:prealloc + log.Debugw("getting ids", "height", height, "namespace", share.Namespace(namespace)) + blobs, err := s.blobServ.GetAll(ctx, height, []share.Namespace{namespace}) + log.Debugw("got ids", "height", height, "namespace", share.Namespace(namespace)) + if err != nil { + if strings.Contains(err.Error(), blob.ErrBlobNotFound.Error()) { + return nil, nil + } + return nil, err + } + for _, b := range blobs { + ids = append(ids, MakeID(height, b.Commitment)) + } + return ids, nil +} + +// GetProofs returns inclusion Proofs for all Blobs located in DA at given height. +func (s *Service) GetProofs(ctx context.Context, ids []da.ID, namespace da.Namespace) ([]da.Proof, error) { + proofs := make([]da.Proof, len(ids)) + for i, id := range ids { + height, commitment := SplitID(id) + proof, err := s.blobServ.GetProof(ctx, height, namespace, commitment) + if err != nil { + return nil, err + } + proofs[i], err = proof.MarshalJSON() + if err != nil { + return nil, err + } + } + return proofs, nil +} + +// Commit creates a Commitment for each given Blob. +func (s *Service) Commit(_ context.Context, daBlobs []da.Blob, namespace da.Namespace) ([]da.Commitment, error) { + _, commitments, err := s.blobsAndCommitments(daBlobs, namespace) + return commitments, err +} + +// Submit submits the Blobs to Data Availability layer. +func (s *Service) Submit( + ctx context.Context, + daBlobs []da.Blob, + gasPrice float64, + namespace da.Namespace, +) ([]da.ID, error) { + blobs, _, err := s.blobsAndCommitments(daBlobs, namespace) + if err != nil { + return nil, err + } + + height, err := s.blobServ.Submit(ctx, blobs, blob.GasPrice(gasPrice)) + if err != nil { + log.Error("failed to submit blobs", "height", height, "gas price", gasPrice) + return nil, err + } + log.Info("successfully submitted blobs", "height", height, "gas price", gasPrice) + ids := make([]da.ID, len(blobs)) + for i, blob := range blobs { + ids[i] = MakeID(height, blob.Commitment) + } + return ids, nil +} + +// blobsAndCommitments converts []da.Blob to []*blob.Blob and generates corresponding +// []da.Commitment +func (s *Service) blobsAndCommitments( + daBlobs []da.Blob, namespace da.Namespace, +) ([]*blob.Blob, []da.Commitment, error) { + blobs := make([]*blob.Blob, 0, len(daBlobs)) + commitments := make([]da.Commitment, 0, len(daBlobs)) + for _, daBlob := range daBlobs { + b, err := blob.NewBlobV0(namespace, daBlob) + if err != nil { + return nil, nil, err + } + blobs = append(blobs, b) + + commitments = append(commitments, b.Commitment) + } + return blobs, commitments, nil +} + +// Validate validates Commitments against the corresponding Proofs. This should be possible without +// retrieving the Blobs. +func (s *Service) Validate( + ctx context.Context, + ids []da.ID, + daProofs []da.Proof, + namespace da.Namespace, +) ([]bool, error) { + included := make([]bool, len(ids)) + proofs := make([]*blob.Proof, len(ids)) + for i, daProof := range daProofs { + blobProof := &blob.Proof{} + err := blobProof.UnmarshalJSON(daProof) + if err != nil { + return nil, err + } + proofs[i] = blobProof + } + for i, id := range ids { + height, commitment := SplitID(id) + // TODO(tzdybal): for some reason, if proof doesn't match commitment, API returns (false, "blob: + // invalid proof") but analysis of the code in celestia-node implies this should never happen - + // maybe it's caused by openrpc? there is no way of gently handling errors here, but returned + // value is fine for us + fmt.Println("proof", proofs[i] == nil, "commitment", commitment == nil) + isIncluded, _ := s.blobServ.Included(ctx, height, namespace, proofs[i], commitment) + included = append(included, isIncluded) + } + return included, nil +} + +func MakeID(height uint64, commitment da.Commitment) da.ID { + id := make([]byte, heightLen+len(commitment)) + binary.LittleEndian.PutUint64(id, height) + copy(id[heightLen:], commitment) + return id +} + +func SplitID(id da.ID) (uint64, da.Commitment) { + if len(id) <= heightLen { + return 0, nil + } + commitment := id[heightLen:] + return binary.LittleEndian.Uint64(id[:heightLen]), commitment +} diff --git a/nodebuilder/das/cmd/das.go b/nodebuilder/das/cmd/das.go new file mode 100644 index 0000000000..7512861ac3 --- /dev/null +++ b/nodebuilder/das/cmd/das.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "github.com/spf13/cobra" + + cmdnode "github.com/celestiaorg/celestia-node/cmd" +) + +func init() { + Cmd.AddCommand(samplingStatsCmd) +} + +var Cmd = &cobra.Command{ + Use: "das [command]", + Short: "Allows to interact with the Daser via JSON-RPC", + Args: cobra.NoArgs, + PersistentPreRunE: cmdnode.InitClient, +} + +var samplingStatsCmd = &cobra.Command{ + Use: "sampling-stats", + Short: "Returns the current statistics over the DA sampling process", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + stats, err := client.DAS.SamplingStats(cmd.Context()) + return cmdnode.PrintOutput(stats, err, nil) + }, +} diff --git a/nodebuilder/das/constructors.go b/nodebuilder/das/constructors.go index 18f6962f40..973aca5679 100644 --- a/nodebuilder/das/constructors.go +++ b/nodebuilder/das/constructors.go @@ -3,6 +3,7 @@ package das import ( "context" "fmt" + "time" "github.com/ipfs/go-datastore" @@ -12,6 +13,7 @@ import ( "github.com/celestiaorg/celestia-node/das" "github.com/celestiaorg/celestia-node/header" modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud" + "github.com/celestiaorg/celestia-node/pruner" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds/byzantine" "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" @@ -42,16 +44,19 @@ func newDASer( hsub libhead.Subscriber[*header.ExtendedHeader], store libhead.Store[*header.ExtendedHeader], batching datastore.Batching, - fraudServ fraud.Service, + fraudServ fraud.Service[*header.ExtendedHeader], bFn shrexsub.BroadcastFn, + availWindow pruner.AvailabilityWindow, options ...das.Option, -) (*das.DASer, *modfraud.ServiceBreaker[*das.DASer], error) { +) (*das.DASer, *modfraud.ServiceBreaker[*das.DASer, *header.ExtendedHeader], error) { + options = append(options, das.WithSamplingWindow(time.Duration(availWindow))) + ds, err := das.NewDASer(da, hsub, store, batching, fraudServ, bFn, options...) if err != nil { return nil, nil, err } - return ds, &modfraud.ServiceBreaker[*das.DASer]{ + return ds, &modfraud.ServiceBreaker[*das.DASer, *header.ExtendedHeader]{ Service: ds, FraudServ: fraudServ, FraudType: byzantine.BadEncoding, diff --git a/nodebuilder/das/mocks/api.go b/nodebuilder/das/mocks/api.go index 68ffaf3c8c..c4046e90e8 100644 --- a/nodebuilder/das/mocks/api.go +++ b/nodebuilder/das/mocks/api.go @@ -8,9 +8,8 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - das "github.com/celestiaorg/celestia-node/das" + gomock "github.com/golang/mock/gomock" ) // MockModule is a mock of Module interface. diff --git a/nodebuilder/das/module.go b/nodebuilder/das/module.go index 61c935fd40..0545fffc27 100644 --- a/nodebuilder/das/module.go +++ b/nodebuilder/das/module.go @@ -6,6 +6,7 @@ import ( "go.uber.org/fx" "github.com/celestiaorg/celestia-node/das" + "github.com/celestiaorg/celestia-node/header" modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud" "github.com/celestiaorg/celestia-node/nodebuilder/node" ) @@ -37,14 +38,14 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { switch tp { case node.Light, node.Full: return fx.Module( - "daser", + "das", baseComponents, fx.Provide(fx.Annotate( newDASer, - fx.OnStart(func(ctx context.Context, breaker *modfraud.ServiceBreaker[*das.DASer]) error { + fx.OnStart(func(ctx context.Context, breaker *modfraud.ServiceBreaker[*das.DASer, *header.ExtendedHeader]) error { return breaker.Start(ctx) }), - fx.OnStop(func(ctx context.Context, breaker *modfraud.ServiceBreaker[*das.DASer]) error { + fx.OnStop(func(ctx context.Context, breaker *modfraud.ServiceBreaker[*das.DASer, *header.ExtendedHeader]) error { return breaker.Stop(ctx) }), )), @@ -55,7 +56,7 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { ) case node.Bridge: return fx.Module( - "daser", + "das", baseComponents, fx.Provide(newDaserStub), ) diff --git a/nodebuilder/default_services.go b/nodebuilder/default_services.go index 03954c726a..430f6ba66b 100644 --- a/nodebuilder/default_services.go +++ b/nodebuilder/default_services.go @@ -1,6 +1,7 @@ package nodebuilder import ( + "github.com/celestiaorg/celestia-node/nodebuilder/blob" "github.com/celestiaorg/celestia-node/nodebuilder/das" "github.com/celestiaorg/celestia-node/nodebuilder/fraud" "github.com/celestiaorg/celestia-node/nodebuilder/header" @@ -17,7 +18,8 @@ var PackageToAPI = map[string]interface{}{ "state": &state.API{}, "share": &share.API{}, "header": &header.API{}, - "daser": &das.API{}, + "das": &das.API{}, "p2p": &p2p.API{}, + "blob": &blob.API{}, "node": &node.API{}, } diff --git a/nodebuilder/fraud/constructors.go b/nodebuilder/fraud/constructors.go index a70ee3e3d4..eee85d4139 100644 --- a/nodebuilder/fraud/constructors.go +++ b/nodebuilder/fraud/constructors.go @@ -1,8 +1,6 @@ package fraud import ( - "context" - "github.com/ipfs/go-datastore" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/host" @@ -16,32 +14,46 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder/p2p" ) -func newFraudService(syncerEnabled bool) func( - fx.Lifecycle, - *pubsub.PubSub, - host.Host, - libhead.Store[*header.ExtendedHeader], - datastore.Batching, - p2p.Network, -) (Module, fraud.Service, error) { - return func( - lc fx.Lifecycle, - sub *pubsub.PubSub, - host host.Host, - hstore libhead.Store[*header.ExtendedHeader], - ds datastore.Batching, - network p2p.Network, - ) (Module, fraud.Service, error) { - getter := func(ctx context.Context, height uint64) (libhead.Header, error) { - return hstore.GetByHeight(ctx, height) - } - pservice := fraudserv.NewProofService(sub, host, getter, ds, syncerEnabled, network.String()) - lc.Append(fx.Hook{ - OnStart: pservice.Start, - OnStop: pservice.Stop, - }) - return &Service{ - Service: pservice, - }, pservice, nil - } +func fraudUnmarshaler() fraud.ProofUnmarshaler[*header.ExtendedHeader] { + return defaultProofUnmarshaler +} + +func newFraudServiceWithSync( + lc fx.Lifecycle, + sub *pubsub.PubSub, + host host.Host, + hstore libhead.Store[*header.ExtendedHeader], + registry fraud.ProofUnmarshaler[*header.ExtendedHeader], + ds datastore.Batching, + network p2p.Network, +) (Module, fraud.Service[*header.ExtendedHeader], error) { + syncerEnabled := true + pservice := fraudserv.NewProofService(sub, host, hstore.GetByHeight, registry, ds, syncerEnabled, network.String()) + lc.Append(fx.Hook{ + OnStart: pservice.Start, + OnStop: pservice.Stop, + }) + return &module{ + Service: pservice, + }, pservice, nil +} + +func newFraudServiceWithoutSync( + lc fx.Lifecycle, + sub *pubsub.PubSub, + host host.Host, + hstore libhead.Store[*header.ExtendedHeader], + registry fraud.ProofUnmarshaler[*header.ExtendedHeader], + ds datastore.Batching, + network p2p.Network, +) (Module, fraud.Service[*header.ExtendedHeader], error) { + syncerEnabled := false + pservice := fraudserv.NewProofService(sub, host, hstore.GetByHeight, registry, ds, syncerEnabled, network.String()) + lc.Append(fx.Hook{ + OnStart: pservice.Start, + OnStop: pservice.Stop, + }) + return &module{ + Service: pservice, + }, pservice, nil } diff --git a/nodebuilder/fraud/fraud.go b/nodebuilder/fraud/fraud.go index 8d10d34e88..178b0527a1 100644 --- a/nodebuilder/fraud/fraud.go +++ b/nodebuilder/fraud/fraud.go @@ -2,8 +2,12 @@ package fraud import ( "context" + "encoding/json" + "errors" "github.com/celestiaorg/go-fraud" + + "github.com/celestiaorg/celestia-node/header" ) var _ Module = (*API)(nil) @@ -14,7 +18,7 @@ var _ Module = (*API)(nil) //go:generate mockgen -destination=mocks/api.go -package=mocks . Module type Module interface { // Subscribe allows to subscribe on a Proof pub sub topic by its type. - Subscribe(context.Context, fraud.ProofType) (<-chan Proof, error) + Subscribe(context.Context, fraud.ProofType) (<-chan *Proof, error) // Get fetches fraud proofs from the disk by its type. Get(context.Context, fraud.ProofType) ([]Proof, error) } @@ -23,15 +27,95 @@ type Module interface { // TODO(@distractedm1nd): These structs need to be autogenerated. type API struct { Internal struct { - Subscribe func(context.Context, fraud.ProofType) (<-chan Proof, error) `perm:"public"` - Get func(context.Context, fraud.ProofType) ([]Proof, error) `perm:"public"` + Subscribe func(context.Context, fraud.ProofType) (<-chan *Proof, error) `perm:"read"` + Get func(context.Context, fraud.ProofType) ([]Proof, error) `perm:"read"` } } -func (api *API) Subscribe(ctx context.Context, proofType fraud.ProofType) (<-chan Proof, error) { +func (api *API) Subscribe(ctx context.Context, proofType fraud.ProofType) (<-chan *Proof, error) { return api.Internal.Subscribe(ctx, proofType) } func (api *API) Get(ctx context.Context, proofType fraud.ProofType) ([]Proof, error) { return api.Internal.Get(ctx, proofType) } + +var _ Module = (*module)(nil) + +// module is an implementation of Module that uses fraud.module as a backend. It is used to +// provide fraud proofs as a non-interface type to the API, and wrap fraud.Subscriber with a +// channel of Proofs. +type module struct { + fraud.Service[*header.ExtendedHeader] +} + +func (s *module) Subscribe(ctx context.Context, proofType fraud.ProofType) (<-chan *Proof, error) { + subscription, err := s.Service.Subscribe(proofType) + if err != nil { + return nil, err + } + proofs := make(chan *Proof) + go func() { + defer close(proofs) + defer subscription.Cancel() + for { + proof, err := subscription.Proof(ctx) + if err != nil { + if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) { + log.Errorw("fetching proof from subscription", "err", err) + } + return + } + select { + case <-ctx.Done(): + return + case proofs <- &Proof{Proof: proof}: + } + } + }() + return proofs, nil +} + +func (s *module) Get(ctx context.Context, proofType fraud.ProofType) ([]Proof, error) { + originalProofs, err := s.Service.Get(ctx, proofType) + if err != nil { + return nil, err + } + proofs := make([]Proof, len(originalProofs)) + for i, originalProof := range originalProofs { + proofs[i].Proof = originalProof + } + return proofs, nil +} + +// Proof embeds the fraud.Proof interface type to provide a concrete type for JSON serialization. +type Proof struct { + fraud.Proof[*header.ExtendedHeader] +} + +type fraudProofJSON struct { + ProofType fraud.ProofType `json:"proof_type"` + Data []byte `json:"data"` +} + +func (f *Proof) UnmarshalJSON(data []byte) error { + var fp fraudProofJSON + err := json.Unmarshal(data, &fp) + if err != nil { + return err + } + f.Proof, err = defaultProofUnmarshaler.Unmarshal(fp.ProofType, fp.Data) + return err +} + +func (f *Proof) MarshalJSON() ([]byte, error) { + marshaledProof, err := f.MarshalBinary() + if err != nil { + return nil, err + } + fraudProof := &fraudProofJSON{ + ProofType: f.Type(), + Data: marshaledProof, + } + return json.Marshal(fraudProof) +} diff --git a/nodebuilder/fraud/lifecycle.go b/nodebuilder/fraud/lifecycle.go index 24ed402f5d..50f4e1035b 100644 --- a/nodebuilder/fraud/lifecycle.go +++ b/nodebuilder/fraud/lifecycle.go @@ -2,11 +2,13 @@ package fraud import ( "context" + "errors" "fmt" "github.com/ipfs/go-datastore" "github.com/celestiaorg/go-fraud" + libhead "github.com/celestiaorg/go-header" ) // service defines minimal interface with service lifecycle methods @@ -18,26 +20,30 @@ type service interface { // ServiceBreaker wraps any service with fraud proof subscription of a specific type. // If proof happens the service is Stopped automatically. // TODO(@Wondertan): Support multiple fraud types. -type ServiceBreaker[S service] struct { +type ServiceBreaker[S service, H libhead.Header[H]] struct { Service S FraudType fraud.ProofType - FraudServ fraud.Service + FraudServ fraud.Service[H] ctx context.Context cancel context.CancelFunc - sub fraud.Subscription + sub fraud.Subscription[H] } // Start starts the inner service if there are no fraud proofs stored. // Subscribes for fraud and stops the service whenever necessary. -func (breaker *ServiceBreaker[S]) Start(ctx context.Context) error { +func (breaker *ServiceBreaker[S, H]) Start(ctx context.Context) error { + if breaker == nil { + return nil + } + proofs, err := breaker.FraudServ.Get(ctx, breaker.FraudType) - switch err { + switch { default: return fmt.Errorf("getting proof(%s): %w", breaker.FraudType, err) - case nil: - return &fraud.ErrFraudExists{Proof: proofs} - case datastore.ErrNotFound: + case err == nil: + return &fraud.ErrFraudExists[H]{Proof: proofs} + case errors.Is(err, datastore.ErrNotFound): } err = breaker.Service.Start(ctx) @@ -56,24 +62,28 @@ func (breaker *ServiceBreaker[S]) Start(ctx context.Context) error { } // Stop stops the service and cancels subscription. -func (breaker *ServiceBreaker[S]) Stop(ctx context.Context) error { +func (breaker *ServiceBreaker[S, H]) Stop(ctx context.Context) error { + if breaker == nil { + return nil + } + if breaker.ctx.Err() != nil { // short circuit if the service was already stopped return nil } breaker.sub.Cancel() - breaker.cancel() + defer breaker.cancel() return breaker.Service.Stop(ctx) } -func (breaker *ServiceBreaker[S]) awaitProof() { +func (breaker *ServiceBreaker[S, H]) awaitProof() { _, err := breaker.sub.Proof(breaker.ctx) if err != nil { return } - if err := breaker.Stop(breaker.ctx); err != nil && err != context.Canceled { + if err := breaker.Stop(breaker.ctx); err != nil && !errors.Is(err, context.Canceled) { log.Errorw("stopping service: %s", err.Error()) } } diff --git a/nodebuilder/fraud/mocks/api.go b/nodebuilder/fraud/mocks/api.go index 5ede6f27c5..fcc7a58231 100644 --- a/nodebuilder/fraud/mocks/api.go +++ b/nodebuilder/fraud/mocks/api.go @@ -8,10 +8,9 @@ import ( context "context" reflect "reflect" + fraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud" + fraud0 "github.com/celestiaorg/go-fraud" gomock "github.com/golang/mock/gomock" - - fraud0 "github.com/celestiaorg/celestia-node/nodebuilder/fraud" - "github.com/celestiaorg/go-fraud" ) // MockModule is a mock of Module interface. @@ -38,10 +37,10 @@ func (m *MockModule) EXPECT() *MockModuleMockRecorder { } // Get mocks base method. -func (m *MockModule) Get(arg0 context.Context, arg1 fraud.ProofType) ([]fraud0.Proof, error) { +func (m *MockModule) Get(arg0 context.Context, arg1 fraud0.ProofType) ([]fraud.Proof, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Get", arg0, arg1) - ret0, _ := ret[0].([]fraud0.Proof) + ret0, _ := ret[0].([]fraud.Proof) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -53,10 +52,10 @@ func (mr *MockModuleMockRecorder) Get(arg0, arg1 interface{}) *gomock.Call { } // Subscribe mocks base method. -func (m *MockModule) Subscribe(arg0 context.Context, arg1 fraud.ProofType) (<-chan fraud0.Proof, error) { +func (m *MockModule) Subscribe(arg0 context.Context, arg1 fraud0.ProofType) (<-chan *fraud.Proof, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Subscribe", arg0, arg1) - ret0, _ := ret[0].(<-chan fraud0.Proof) + ret0, _ := ret[0].(<-chan *fraud.Proof) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/nodebuilder/fraud/module.go b/nodebuilder/fraud/module.go index 718b702f84..bf353f63c6 100644 --- a/nodebuilder/fraud/module.go +++ b/nodebuilder/fraud/module.go @@ -6,27 +6,31 @@ import ( "github.com/celestiaorg/go-fraud" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/nodebuilder/node" ) var log = logging.Logger("module/fraud") func ConstructModule(tp node.Type) fx.Option { - baseComponent := fx.Provide(func(serv fraud.Service) fraud.Getter { - return serv - }) + baseComponent := fx.Options( + fx.Provide(fraudUnmarshaler), + fx.Provide(func(serv fraud.Service[*header.ExtendedHeader]) fraud.Getter[*header.ExtendedHeader] { + return serv + }), + ) switch tp { case node.Light: return fx.Module( "fraud", baseComponent, - fx.Provide(newFraudService(true)), + fx.Provide(newFraudServiceWithSync), ) case node.Full, node.Bridge: return fx.Module( "fraud", baseComponent, - fx.Provide(newFraudService(false)), + fx.Provide(newFraudServiceWithoutSync), ) default: panic("invalid node type") diff --git a/nodebuilder/fraud/service.go b/nodebuilder/fraud/service.go deleted file mode 100644 index 0337c375ef..0000000000 --- a/nodebuilder/fraud/service.go +++ /dev/null @@ -1,87 +0,0 @@ -package fraud - -import ( - "context" - "encoding/json" - - "github.com/celestiaorg/go-fraud" -) - -var _ Module = (*Service)(nil) - -// Service is an implementation of Module that uses fraud.Service as a backend. It is used to -// provide fraud proofs as a non-interface type to the API, and wrap fraud.Subscriber with a -// channel of Proofs. -type Service struct { - fraud.Service -} - -func (s *Service) Subscribe(ctx context.Context, proofType fraud.ProofType) (<-chan Proof, error) { - subscription, err := s.Service.Subscribe(proofType) - if err != nil { - return nil, err - } - proofs := make(chan Proof) - go func() { - defer close(proofs) - for { - proof, err := subscription.Proof(ctx) - if err != nil { - if err != context.DeadlineExceeded && err != context.Canceled { - log.Errorw("fetching proof from subscription", "err", err) - } - return - } - select { - case <-ctx.Done(): - return - case proofs <- Proof{Proof: proof}: - } - } - }() - return proofs, nil -} - -func (s *Service) Get(ctx context.Context, proofType fraud.ProofType) ([]Proof, error) { - originalProofs, err := s.Service.Get(ctx, proofType) - if err != nil { - return nil, err - } - proofs := make([]Proof, len(originalProofs)) - for i, originalProof := range originalProofs { - proofs[i].Proof = originalProof - } - return proofs, nil -} - -// Proof embeds the fraud.Proof interface type to provide a concrete type for JSON serialization. -type Proof struct { - fraud.Proof -} - -type fraudProofJSON struct { - ProofType fraud.ProofType `json:"proof_type"` - Data []byte `json:"data"` -} - -func (f *Proof) UnmarshalJSON(data []byte) error { - var fp fraudProofJSON - err := json.Unmarshal(data, &fp) - if err != nil { - return err - } - f.Proof, err = fraud.Unmarshal(fp.ProofType, fp.Data) - return err -} - -func (f *Proof) MarshalJSON() ([]byte, error) { - marshaledProof, err := f.MarshalBinary() - if err != nil { - return nil, err - } - fraudProof := &fraudProofJSON{ - ProofType: f.Type(), - Data: marshaledProof, - } - return json.Marshal(fraudProof) -} diff --git a/nodebuilder/fraud/unmarshaler.go b/nodebuilder/fraud/unmarshaler.go new file mode 100644 index 0000000000..d5e0461f01 --- /dev/null +++ b/nodebuilder/fraud/unmarshaler.go @@ -0,0 +1,32 @@ +package fraud + +import ( + "github.com/celestiaorg/go-fraud" + + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/share/eds/byzantine" +) + +var defaultProofUnmarshaler proofRegistry + +type proofRegistry struct{} + +func (pr proofRegistry) List() []fraud.ProofType { + return []fraud.ProofType{ + byzantine.BadEncoding, + } +} + +func (pr proofRegistry) Unmarshal(proofType fraud.ProofType, data []byte) (fraud.Proof[*header.ExtendedHeader], error) { + switch proofType { + case byzantine.BadEncoding: + befp := &byzantine.BadEncodingProof{} + err := befp.UnmarshalBinary(data) + if err != nil { + return nil, err + } + return befp, nil + default: + return nil, &fraud.ErrNoUnmarshaler{ProofType: proofType} + } +} diff --git a/nodebuilder/gateway/config.go b/nodebuilder/gateway/config.go index 903a27489a..c49a4749a3 100644 --- a/nodebuilder/gateway/config.go +++ b/nodebuilder/gateway/config.go @@ -15,9 +15,9 @@ type Config struct { func DefaultConfig() Config { return Config{ - Address: "0.0.0.0", + Address: defaultBindAddress, // do NOT expose the same port as celestia-core by default so that both can run on the same machine - Port: "26659", + Port: defaultPort, Enabled: false, } } diff --git a/nodebuilder/gateway/config_test.go b/nodebuilder/gateway/config_test.go new file mode 100644 index 0000000000..9ef3f1e310 --- /dev/null +++ b/nodebuilder/gateway/config_test.go @@ -0,0 +1,18 @@ +package gateway + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestDefaultConfig tests that the default gateway config is correct. +func TestDefaultConfig(t *testing.T) { + expected := Config{ + Address: defaultBindAddress, + Port: defaultPort, + Enabled: false, + } + + assert.Equal(t, expected, DefaultConfig()) +} diff --git a/nodebuilder/gateway/defaults.go b/nodebuilder/gateway/defaults.go new file mode 100644 index 0000000000..e6c48d5d4e --- /dev/null +++ b/nodebuilder/gateway/defaults.go @@ -0,0 +1,6 @@ +package gateway + +const ( + defaultBindAddress = "localhost" + defaultPort = "26659" +) diff --git a/nodebuilder/gateway/defaults_test.go b/nodebuilder/gateway/defaults_test.go new file mode 100644 index 0000000000..c504f8cca4 --- /dev/null +++ b/nodebuilder/gateway/defaults_test.go @@ -0,0 +1,12 @@ +package gateway + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestServerDefaultConstants(t *testing.T) { + assert.Equal(t, "localhost", defaultBindAddress) + assert.Equal(t, "26659", defaultPort) +} diff --git a/nodebuilder/gateway/flags.go b/nodebuilder/gateway/flags.go index cd13e47162..6da4a66f03 100644 --- a/nodebuilder/gateway/flags.go +++ b/nodebuilder/gateway/flags.go @@ -1,6 +1,8 @@ package gateway import ( + "fmt" + "github.com/spf13/cobra" flag "github.com/spf13/pflag" ) @@ -23,12 +25,12 @@ func Flags() *flag.FlagSet { flags.String( addrFlag, "", - "Set a custom gateway listen address (default: localhost)", + fmt.Sprintf("Set a custom gateway listen address (default: %s)", defaultBindAddress), ) flags.String( portFlag, "", - "Set a custom gateway port (default: 26659)", + fmt.Sprintf("Set a custom gateway port (default: %s)", defaultPort), ) return flags diff --git a/nodebuilder/gateway/flags_test.go b/nodebuilder/gateway/flags_test.go new file mode 100644 index 0000000000..5f55ac77f2 --- /dev/null +++ b/nodebuilder/gateway/flags_test.go @@ -0,0 +1,95 @@ +package gateway + +import ( + "fmt" + "strconv" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFlags(t *testing.T) { + flags := Flags() + + enabled := flags.Lookup(enabledFlag) + require.NotNil(t, enabled) + assert.Equal(t, "false", enabled.Value.String()) + assert.Equal(t, "Enables the REST gateway", enabled.Usage) + + addr := flags.Lookup(addrFlag) + require.NotNil(t, addr) + assert.Equal(t, "", addr.Value.String()) + assert.Equal(t, fmt.Sprintf("Set a custom gateway listen address (default: %s)", defaultBindAddress), addr.Usage) + + port := flags.Lookup(portFlag) + require.NotNil(t, port) + assert.Equal(t, "", port.Value.String()) + assert.Equal(t, fmt.Sprintf("Set a custom gateway port (default: %s)", defaultPort), port.Usage) +} + +func TestParseFlags(t *testing.T) { + tests := []struct { + name string + enabledFlag bool + addrFlag string + portFlag string + expectedCfg *Config + }{ + { + name: "Enabled flag is true", + enabledFlag: true, + addrFlag: "127.0.0.1", + portFlag: "8080", + expectedCfg: &Config{ + Enabled: true, + Address: "127.0.0.1", + Port: "8080", + }, + }, + { + name: "Enabled flag is false", + enabledFlag: false, + addrFlag: "127.0.0.1", + portFlag: "8080", + expectedCfg: &Config{ + Enabled: false, + Address: "127.0.0.1", + Port: "8080", + }, + }, + { + name: "Enabled flag is false and address/port flags are not changed", + enabledFlag: false, + addrFlag: "", + portFlag: "", + expectedCfg: &Config{ + Enabled: false, + Address: "", + Port: "", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := &cobra.Command{} + cfg := &Config{} + + cmd.Flags().AddFlagSet(Flags()) + + err := cmd.Flags().Set(enabledFlag, strconv.FormatBool(test.enabledFlag)) + assert.NoError(t, err) + err = cmd.Flags().Set(addrFlag, test.addrFlag) + assert.NoError(t, err) + err = cmd.Flags().Set(portFlag, test.portFlag) + assert.NoError(t, err) + + ParseFlags(cmd, cfg) + assert.Equal(t, test.expectedCfg.Enabled, cfg.Enabled) + assert.Equal(t, test.expectedCfg.Address, cfg.Address) + assert.Equal(t, test.expectedCfg.Port, cfg.Port) + }) + } +} diff --git a/nodebuilder/gateway/module.go b/nodebuilder/gateway/module.go index b3070e01a6..4cdf325dc0 100644 --- a/nodebuilder/gateway/module.go +++ b/nodebuilder/gateway/module.go @@ -21,8 +21,6 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { if !cfg.Enabled { return fx.Options() } - // NOTE @distractedm1nd @renaynay: Remove whenever/if we decide to add auth to gateway - log.Warn("Gateway is enabled, however gateway endpoints are not authenticated. Use with caution!") baseComponents := fx.Options( fx.Supply(cfg), diff --git a/nodebuilder/header/cmd/header.go b/nodebuilder/header/cmd/header.go new file mode 100644 index 0000000000..b3bba1eb32 --- /dev/null +++ b/nodebuilder/header/cmd/header.go @@ -0,0 +1,117 @@ +package cmd + +import ( + "encoding/hex" + "fmt" + "strconv" + + "github.com/spf13/cobra" + + cmdnode "github.com/celestiaorg/celestia-node/cmd" +) + +func init() { + Cmd.AddCommand( + localHeadCmd, + networkHeadCmd, + getByHashCmd, + getByHeightCmd, + syncStateCmd, + ) +} + +var Cmd = &cobra.Command{ + Use: "header [command]", + Short: "Allows interaction with the Header Module via JSON-RPC", + Args: cobra.NoArgs, + PersistentPreRunE: cmdnode.InitClient, +} + +var localHeadCmd = &cobra.Command{ + Use: "local-head", + Short: "Returns the ExtendedHeader from the chain head.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + header, err := client.Header.LocalHead(cmd.Context()) + return cmdnode.PrintOutput(header, err, nil) + }, +} + +var networkHeadCmd = &cobra.Command{ + Use: "network-head", + Short: "Provides the Syncer's view of the current network head.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + header, err := client.Header.NetworkHead(cmd.Context()) + return cmdnode.PrintOutput(header, err, nil) + }, +} + +var getByHashCmd = &cobra.Command{ + Use: "get-by-hash", + Short: "Returns the header of the given hash from the node's header store.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + hash, err := hex.DecodeString(args[0]) + if err != nil { + return fmt.Errorf("error decoding a hash: expected a hex encoded string:%v", err) + } + header, err := client.Header.GetByHash(cmd.Context(), hash) + return cmdnode.PrintOutput(header, err, nil) + }, +} + +var getByHeightCmd = &cobra.Command{ + Use: "get-by-height", + Short: "Returns the ExtendedHeader at the given height if it is currently available.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + height, err := strconv.ParseUint(args[0], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a height:%v", err) + } + + header, err := client.Header.GetByHeight(cmd.Context(), height) + return cmdnode.PrintOutput(header, err, nil) + }, +} + +var syncStateCmd = &cobra.Command{ + Use: "sync-state", + Short: "Returns the current state of the header Syncer.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + header, err := client.Header.SyncState(cmd.Context()) + return cmdnode.PrintOutput(header, err, nil) + }, +} diff --git a/nodebuilder/header/config.go b/nodebuilder/header/config.go index 238fb39afd..3fb7162ae0 100644 --- a/nodebuilder/header/config.go +++ b/nodebuilder/header/config.go @@ -16,6 +16,9 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder/p2p" ) +// MetricsEnabled will be set during runtime if metrics are enabled on the node. +var MetricsEnabled = false + // Config contains configuration parameters for header retrieval and management. type Config struct { // TrustedHash is the Block/Header hash that Nodes use as starting point for header synchronization. @@ -40,13 +43,18 @@ func DefaultConfig(tp node.Type) Config { Store: store.DefaultParameters(), Syncer: sync.DefaultParameters(), Server: p2p_exchange.DefaultServerParameters(), + Client: p2p_exchange.DefaultClientParameters(), } switch tp { case node.Bridge: return cfg - case node.Light, node.Full: - cfg.Client = p2p_exchange.DefaultClientParameters() + case node.Full: + return cfg + case node.Light: + cfg.Store.StoreCacheSize = 512 + cfg.Store.IndexCacheSize = 2048 + cfg.Store.WriteBatchSize = 512 return cfg default: panic("header: invalid node type") diff --git a/nodebuilder/header/constructors.go b/nodebuilder/header/constructors.go index 7d70f0f5a8..a78d609d8e 100644 --- a/nodebuilder/header/constructors.go +++ b/nodebuilder/header/constructors.go @@ -3,6 +3,7 @@ package header import ( "context" + "github.com/ipfs/go-datastore" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/core/peerstore" @@ -15,21 +16,21 @@ import ( "github.com/celestiaorg/go-header/store" "github.com/celestiaorg/go-header/sync" - "github.com/celestiaorg/celestia-node/header" modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud" modp2p "github.com/celestiaorg/celestia-node/nodebuilder/p2p" "github.com/celestiaorg/celestia-node/share/eds/byzantine" ) // newP2PExchange constructs a new Exchange for headers. -func newP2PExchange( +func newP2PExchange[H libhead.Header[H]]( lc fx.Lifecycle, + cfg Config, bpeers modp2p.Bootstrappers, network modp2p.Network, host host.Host, conngater *conngater.BasicConnectionGater, - cfg Config, -) (libhead.Exchange[*header.ExtendedHeader], error) { + pidstore p2p.PeerIDStore, +) (libhead.Exchange[H], error) { peers, err := cfg.trustedPeers(bpeers) if err != nil { return nil, err @@ -39,11 +40,18 @@ func newP2PExchange( ids[index] = peer.ID host.Peerstore().AddAddrs(peer.ID, peer.Addrs, peerstore.PermanentAddrTTL) } - exchange, err := p2p.NewExchange[*header.ExtendedHeader](host, ids, conngater, + + opts := []p2p.Option[p2p.ClientParameters]{ p2p.WithParams(cfg.Client), p2p.WithNetworkID[p2p.ClientParameters](network.String()), p2p.WithChainID(network.String()), - ) + p2p.WithPeerIDStore[p2p.ClientParameters](pidstore), + } + if MetricsEnabled { + opts = append(opts, p2p.WithMetrics[p2p.ClientParameters]()) + } + + exchange, err := p2p.NewExchange[H](host, ids, conngater, opts...) if err != nil { return nil, err } @@ -56,44 +64,51 @@ func newP2PExchange( }, }) return exchange, nil - } // newSyncer constructs new Syncer for headers. -func newSyncer( - ex libhead.Exchange[*header.ExtendedHeader], - fservice libfraud.Service, - store InitStore, - sub libhead.Subscriber[*header.ExtendedHeader], +func newSyncer[H libhead.Header[H]]( + ex libhead.Exchange[H], + fservice libfraud.Service[H], + store libhead.Store[H], + sub libhead.Subscriber[H], cfg Config, -) (*sync.Syncer[*header.ExtendedHeader], *modfraud.ServiceBreaker[*sync.Syncer[*header.ExtendedHeader]], error) { - syncer, err := sync.NewSyncer[*header.ExtendedHeader](ex, store, sub, - sync.WithParams(cfg.Syncer), - sync.WithBlockTime(modp2p.BlockTime), - ) +) (*sync.Syncer[H], *modfraud.ServiceBreaker[*sync.Syncer[H], H], error) { + opts := []sync.Option{sync.WithParams(cfg.Syncer), sync.WithBlockTime(modp2p.BlockTime)} + if MetricsEnabled { + opts = append(opts, sync.WithMetrics()) + } + + syncer, err := sync.NewSyncer[H](ex, store, sub, opts...) if err != nil { return nil, nil, err } - return syncer, &modfraud.ServiceBreaker[*sync.Syncer[*header.ExtendedHeader]]{ + return syncer, &modfraud.ServiceBreaker[*sync.Syncer[H], H]{ Service: syncer, FraudType: byzantine.BadEncoding, FraudServ: fservice, }, nil } -// InitStore is a type representing initialized header store. -// NOTE: It is needed to ensure that Store is always initialized before Syncer is started. -type InitStore libhead.Store[*header.ExtendedHeader] - // newInitStore constructs an initialized store -func newInitStore( +func newInitStore[H libhead.Header[H]]( lc fx.Lifecycle, cfg Config, net modp2p.Network, - s libhead.Store[*header.ExtendedHeader], - ex libhead.Exchange[*header.ExtendedHeader], -) (InitStore, error) { + ds datastore.Batching, + ex libhead.Exchange[H], +) (libhead.Store[H], error) { + opts := []store.Option{store.WithParams(cfg.Store)} + if MetricsEnabled { + opts = append(opts, store.WithMetrics()) + } + + s, err := store.NewStore[H](ds, opts...) + if err != nil { + return nil, err + } + trustedHash, err := cfg.trustedHash(net) if err != nil { return nil, err @@ -101,7 +116,14 @@ func newInitStore( lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { - return store.Init(ctx, s, ex, trustedHash) + err = store.Init[H](ctx, s, ex, trustedHash) + if err != nil { + return err + } + return s.Start(ctx) + }, + OnStop: func(ctx context.Context) error { + return s.Stop(ctx) }, }) diff --git a/nodebuilder/header/header.go b/nodebuilder/header/header.go index d7eb7b5b46..f807796eb6 100644 --- a/nodebuilder/header/header.go +++ b/nodebuilder/header/header.go @@ -19,10 +19,10 @@ type Module interface { // GetByHash returns the header of the given hash from the node's header store. GetByHash(ctx context.Context, hash libhead.Hash) (*header.ExtendedHeader, error) - // GetVerifiedRangeByHeight returns the given range [from:to) of ExtendedHeaders + // GetRangeByHeight returns the given range (from:to) of ExtendedHeaders // from the node's header store and verifies that the returned headers are // adjacent to each other. - GetVerifiedRangeByHeight( + GetRangeByHeight( ctx context.Context, from *header.ExtendedHeader, to uint64, @@ -53,18 +53,18 @@ type API struct { GetByHash func( ctx context.Context, hash libhead.Hash, - ) (*header.ExtendedHeader, error) `perm:"public"` - GetVerifiedRangeByHeight func( + ) (*header.ExtendedHeader, error) `perm:"read"` + GetRangeByHeight func( context.Context, *header.ExtendedHeader, uint64, - ) ([]*header.ExtendedHeader, error) `perm:"public"` - GetByHeight func(context.Context, uint64) (*header.ExtendedHeader, error) `perm:"public"` + ) ([]*header.ExtendedHeader, error) `perm:"read"` + GetByHeight func(context.Context, uint64) (*header.ExtendedHeader, error) `perm:"read"` WaitForHeight func(context.Context, uint64) (*header.ExtendedHeader, error) `perm:"read"` SyncState func(ctx context.Context) (sync.State, error) `perm:"read"` SyncWait func(ctx context.Context) error `perm:"read"` - NetworkHead func(ctx context.Context) (*header.ExtendedHeader, error) `perm:"public"` - Subscribe func(ctx context.Context) (<-chan *header.ExtendedHeader, error) `perm:"public"` + NetworkHead func(ctx context.Context) (*header.ExtendedHeader, error) `perm:"read"` + Subscribe func(ctx context.Context) (<-chan *header.ExtendedHeader, error) `perm:"read"` } } @@ -72,12 +72,12 @@ func (api *API) GetByHash(ctx context.Context, hash libhead.Hash) (*header.Exten return api.Internal.GetByHash(ctx, hash) } -func (api *API) GetVerifiedRangeByHeight( +func (api *API) GetRangeByHeight( ctx context.Context, from *header.ExtendedHeader, to uint64, ) ([]*header.ExtendedHeader, error) { - return api.Internal.GetVerifiedRangeByHeight(ctx, from, to) + return api.Internal.GetRangeByHeight(ctx, from, to) } func (api *API) GetByHeight(ctx context.Context, u uint64) (*header.ExtendedHeader, error) { diff --git a/nodebuilder/header/mocks/api.go b/nodebuilder/header/mocks/api.go index 538169c6be..b0d2b961d9 100644 --- a/nodebuilder/header/mocks/api.go +++ b/nodebuilder/header/mocks/api.go @@ -8,11 +8,10 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - header "github.com/celestiaorg/celestia-node/header" header0 "github.com/celestiaorg/go-header" sync "github.com/celestiaorg/go-header/sync" + gomock "github.com/golang/mock/gomock" ) // MockModule is a mock of Module interface. @@ -68,19 +67,19 @@ func (mr *MockModuleMockRecorder) GetByHeight(arg0, arg1 interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetByHeight", reflect.TypeOf((*MockModule)(nil).GetByHeight), arg0, arg1) } -// GetVerifiedRangeByHeight mocks base method. -func (m *MockModule) GetVerifiedRangeByHeight(arg0 context.Context, arg1 *header.ExtendedHeader, arg2 uint64) ([]*header.ExtendedHeader, error) { +// GetRangeByHeight mocks base method. +func (m *MockModule) GetRangeByHeight(arg0 context.Context, arg1 *header.ExtendedHeader, arg2 uint64) ([]*header.ExtendedHeader, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetVerifiedRangeByHeight", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetRangeByHeight", arg0, arg1, arg2) ret0, _ := ret[0].([]*header.ExtendedHeader) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetVerifiedRangeByHeight indicates an expected call of GetVerifiedRangeByHeight. -func (mr *MockModuleMockRecorder) GetVerifiedRangeByHeight(arg0, arg1, arg2 interface{}) *gomock.Call { +// GetRangeByHeight indicates an expected call of GetRangeByHeight. +func (mr *MockModuleMockRecorder) GetRangeByHeight(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVerifiedRangeByHeight", reflect.TypeOf((*MockModule)(nil).GetVerifiedRangeByHeight), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRangeByHeight", reflect.TypeOf((*MockModule)(nil).GetRangeByHeight), arg0, arg1, arg2) } // LocalHead mocks base method. @@ -156,3 +155,18 @@ func (mr *MockModuleMockRecorder) SyncWait(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SyncWait", reflect.TypeOf((*MockModule)(nil).SyncWait), arg0) } + +// WaitForHeight mocks base method. +func (m *MockModule) WaitForHeight(arg0 context.Context, arg1 uint64) (*header.ExtendedHeader, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WaitForHeight", arg0, arg1) + ret0, _ := ret[0].(*header.ExtendedHeader) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// WaitForHeight indicates an expected call of WaitForHeight. +func (mr *MockModuleMockRecorder) WaitForHeight(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitForHeight", reflect.TypeOf((*MockModule)(nil).WaitForHeight), arg0, arg1) +} diff --git a/nodebuilder/header/module.go b/nodebuilder/header/module.go index 77e7c5eb99..4be25f7125 100644 --- a/nodebuilder/header/module.go +++ b/nodebuilder/header/module.go @@ -11,10 +11,10 @@ import ( libhead "github.com/celestiaorg/go-header" "github.com/celestiaorg/go-header/p2p" - "github.com/celestiaorg/go-header/store" "github.com/celestiaorg/go-header/sync" "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/libs/pidstore" modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud" "github.com/celestiaorg/celestia-node/nodebuilder/node" modp2p "github.com/celestiaorg/celestia-node/nodebuilder/p2p" @@ -22,7 +22,7 @@ import ( var log = logging.Logger("module/header") -func ConstructModule(tp node.Type, cfg *Config) fx.Option { +func ConstructModule[H libhead.Header[H]](tp node.Type, cfg *Config) fx.Option { // sanitize config values before constructing module cfgErr := cfg.Validate(tp) @@ -30,62 +30,61 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { fx.Supply(*cfg), fx.Error(cfgErr), fx.Provide(newHeaderService), - fx.Provide(fx.Annotate( - func(ds datastore.Batching) (libhead.Store[*header.ExtendedHeader], error) { - return store.NewStore[*header.ExtendedHeader](ds, store.WithParams(cfg.Store)) - }, - fx.OnStart(func(ctx context.Context, store libhead.Store[*header.ExtendedHeader]) error { - return store.Start(ctx) - }), - fx.OnStop(func(ctx context.Context, store libhead.Store[*header.ExtendedHeader]) error { - return store.Stop(ctx) - }), - )), - fx.Provide(newInitStore), - fx.Provide(func(subscriber *p2p.Subscriber[*header.ExtendedHeader]) libhead.Subscriber[*header.ExtendedHeader] { + fx.Provide(newInitStore[H]), + fx.Provide(func(subscriber *p2p.Subscriber[H]) libhead.Subscriber[H] { return subscriber }), fx.Provide(fx.Annotate( - newSyncer, + newSyncer[H], fx.OnStart(func( ctx context.Context, - breaker *modfraud.ServiceBreaker[*sync.Syncer[*header.ExtendedHeader]], + breaker *modfraud.ServiceBreaker[*sync.Syncer[H], H], ) error { return breaker.Start(ctx) }), fx.OnStop(func( ctx context.Context, - breaker *modfraud.ServiceBreaker[*sync.Syncer[*header.ExtendedHeader]], + breaker *modfraud.ServiceBreaker[*sync.Syncer[H], H], ) error { return breaker.Stop(ctx) }), )), fx.Provide(fx.Annotate( - func(ps *pubsub.PubSub, network modp2p.Network) *p2p.Subscriber[*header.ExtendedHeader] { - return p2p.NewSubscriber[*header.ExtendedHeader](ps, header.MsgID, network.String()) + func(ps *pubsub.PubSub, network modp2p.Network) (*p2p.Subscriber[H], error) { + opts := []p2p.SubscriberOption{p2p.WithSubscriberNetworkID(network.String())} + if MetricsEnabled { + opts = append(opts, p2p.WithSubscriberMetrics()) + } + return p2p.NewSubscriber[H](ps, header.MsgID, opts...) }, - fx.OnStart(func(ctx context.Context, sub *p2p.Subscriber[*header.ExtendedHeader]) error { + fx.OnStart(func(ctx context.Context, sub *p2p.Subscriber[H]) error { return sub.Start(ctx) }), - fx.OnStop(func(ctx context.Context, sub *p2p.Subscriber[*header.ExtendedHeader]) error { + fx.OnStop(func(ctx context.Context, sub *p2p.Subscriber[H]) error { return sub.Stop(ctx) }), )), fx.Provide(fx.Annotate( func( + cfg Config, host host.Host, - store libhead.Store[*header.ExtendedHeader], + store libhead.Store[H], network modp2p.Network, - ) (*p2p.ExchangeServer[*header.ExtendedHeader], error) { - return p2p.NewExchangeServer[*header.ExtendedHeader](host, store, + ) (*p2p.ExchangeServer[H], error) { + opts := []p2p.Option[p2p.ServerParameters]{ p2p.WithParams(cfg.Server), p2p.WithNetworkID[p2p.ServerParameters](network.String()), - ) + } + if MetricsEnabled { + opts = append(opts, p2p.WithMetrics[p2p.ServerParameters]()) + } + + return p2p.NewExchangeServer[H](host, store, opts...) }, - fx.OnStart(func(ctx context.Context, server *p2p.ExchangeServer[*header.ExtendedHeader]) error { + fx.OnStart(func(ctx context.Context, server *p2p.ExchangeServer[H]) error { return server.Start(ctx) }), - fx.OnStop(func(ctx context.Context, server *p2p.ExchangeServer[*header.ExtendedHeader]) error { + fx.OnStop(func(ctx context.Context, server *p2p.ExchangeServer[H]) error { return server.Stop(ctx) }), )), @@ -96,13 +95,16 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { return fx.Module( "header", baseComponents, - fx.Provide(newP2PExchange), + fx.Provide(newP2PExchange[H]), + fx.Provide(func(ctx context.Context, ds datastore.Batching) (p2p.PeerIDStore, error) { + return pidstore.NewPeerIDStore(ctx, ds) + }), ) case node.Bridge: return fx.Module( "header", baseComponents, - fx.Provide(func(subscriber *p2p.Subscriber[*header.ExtendedHeader]) libhead.Broadcaster[*header.ExtendedHeader] { + fx.Provide(func(subscriber *p2p.Subscriber[H]) libhead.Broadcaster[H] { return subscriber }), fx.Supply(header.MakeExtendedHeader), diff --git a/nodebuilder/header/module_test.go b/nodebuilder/header/module_test.go index 89293e4ab4..23ca41050f 100644 --- a/nodebuilder/header/module_test.go +++ b/nodebuilder/header/module_test.go @@ -20,6 +20,7 @@ import ( "github.com/celestiaorg/go-header/sync" "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/libs/pidstore" "github.com/celestiaorg/celestia-node/nodebuilder/node" modp2p "github.com/celestiaorg/celestia-node/nodebuilder/p2p" ) @@ -35,10 +36,15 @@ func TestConstructModule_StoreParams(t *testing.T) { app := fxtest.New(t, fx.Supply(modp2p.Private), - fx.Provide(func() datastore.Batching { - return datastore.NewMapDatastore() + fx.Supply(modp2p.Bootstrappers{}), + fx.Provide(context.Background), + fx.Provide(libp2p.New), + fx.Provide(conngater.NewBasicConnectionGater), + fx.Provide(func() (datastore.Batching, datastore.Datastore) { + ds := datastore.NewMapDatastore() + return ds, ds }), - ConstructModule(node.Light, &cfg), + ConstructModule[*header.ExtendedHeader](node.Light, &cfg), fx.Invoke( func(s libhead.Store[*header.ExtendedHeader]) { ss := s.(*store.Store[*header.ExtendedHeader]) @@ -72,10 +78,10 @@ func TestConstructModule_SyncerParams(t *testing.T) { fx.Provide(func() datastore.Batching { return datastore.NewMapDatastore() }), - fx.Provide(func() fraud.Service { + fx.Provide(func() fraud.Service[*header.ExtendedHeader] { return nil }), - ConstructModule(node.Light, &cfg), + ConstructModule[*header.ExtendedHeader](node.Light, &cfg), fx.Invoke(func(s *sync.Syncer[*header.ExtendedHeader]) { syncer = s }), @@ -94,13 +100,15 @@ func TestConstructModule_ExchangeParams(t *testing.T) { var exchangeServer *p2p.ExchangeServer[*header.ExtendedHeader] app := fxtest.New(t, + fx.Provide(pidstore.NewPeerIDStore), + fx.Provide(context.Background), fx.Supply(modp2p.Private), fx.Supply(modp2p.Bootstrappers{}), fx.Provide(libp2p.New), fx.Provide(func() datastore.Batching { return datastore.NewMapDatastore() }), - ConstructModule(node.Light, &cfg), + ConstructModule[*header.ExtendedHeader](node.Light, &cfg), fx.Provide(func(b datastore.Batching) (*conngater.BasicConnectionGater, error) { return conngater.NewBasicConnectionGater(b) }), diff --git a/nodebuilder/header/opts.go b/nodebuilder/header/opts.go deleted file mode 100644 index decedcad85..0000000000 --- a/nodebuilder/header/opts.go +++ /dev/null @@ -1,28 +0,0 @@ -package header - -import ( - libhead "github.com/celestiaorg/go-header" - "github.com/celestiaorg/go-header/p2p" - "github.com/celestiaorg/go-header/sync" - - "github.com/celestiaorg/celestia-node/header" -) - -// WithMetrics provides sets `MetricsEnabled` to true on ClientParameters for the header exchange -func WithMetrics( - store libhead.Store[*header.ExtendedHeader], - ex libhead.Exchange[*header.ExtendedHeader], - sync *sync.Syncer[*header.ExtendedHeader], -) error { - if p2pex, ok := ex.(*p2p.Exchange[*header.ExtendedHeader]); ok { - if err := p2pex.InitMetrics(); err != nil { - return err - } - } - - if err := sync.InitMetrics(); err != nil { - return err - } - - return libhead.WithMetrics[*header.ExtendedHeader](store) -} diff --git a/nodebuilder/header/service.go b/nodebuilder/header/service.go index e6d7d46b8f..072ef070c6 100644 --- a/nodebuilder/header/service.go +++ b/nodebuilder/header/service.go @@ -37,7 +37,8 @@ func newHeaderService( sub libhead.Subscriber[*header.ExtendedHeader], p2pServer *p2p.ExchangeServer[*header.ExtendedHeader], ex libhead.Exchange[*header.ExtendedHeader], - store libhead.Store[*header.ExtendedHeader]) Module { + store libhead.Store[*header.ExtendedHeader], +) Module { return &Service{ syncer: syncer, sub: sub, @@ -51,12 +52,12 @@ func (s *Service) GetByHash(ctx context.Context, hash libhead.Hash) (*header.Ext return s.store.Get(ctx, hash) } -func (s *Service) GetVerifiedRangeByHeight( +func (s *Service) GetRangeByHeight( ctx context.Context, from *header.ExtendedHeader, to uint64, ) ([]*header.ExtendedHeader, error) { - return s.store.GetVerifiedRange(ctx, from, to) + return s.store.GetRangeByHeight(ctx, from, to) } func (s *Service) GetByHeight(ctx context.Context, height uint64) (*header.ExtendedHeader, error) { @@ -64,9 +65,9 @@ func (s *Service) GetByHeight(ctx context.Context, height uint64) (*header.Exten switch { case err != nil: return nil, err - case uint64(head.Height()) == height: + case head.Height() == height: return head, nil - case uint64(head.Height()) < height: + case head.Height()+1 < height: return nil, fmt.Errorf("header: given height is from the future: "+ "networkHeight: %d, requestedHeight: %d", head.Height(), height) } @@ -77,10 +78,10 @@ func (s *Service) GetByHeight(ctx context.Context, height uint64) (*header.Exten switch { case err != nil: return nil, err - case uint64(head.Height()) == height: + case head.Height() == height: return head, nil // `+1` allows for one header network lag, e.g. user request header that is milliseconds away - case uint64(head.Height())+1 < height: + case head.Height()+1 < height: return nil, fmt.Errorf("header: syncing in progress: "+ "localHeadHeight: %d, requestedHeight: %d", head.Height(), height) default: diff --git a/nodebuilder/header/service_test.go b/nodebuilder/header/service_test.go index 6493d3d51d..14d5ada87d 100644 --- a/nodebuilder/header/service_test.go +++ b/nodebuilder/header/service_test.go @@ -25,9 +25,9 @@ func TestGetByHeightHandlesError(t *testing.T) { }) } -type errorSyncer[H libhead.Header] struct{} +type errorSyncer[H libhead.Header[H]] struct{} -func (d *errorSyncer[H]) Head(context.Context) (H, error) { +func (d *errorSyncer[H]) Head(context.Context, ...libhead.HeadOption[H]) (H, error) { var zero H return zero, fmt.Errorf("dummy error") } diff --git a/nodebuilder/init.go b/nodebuilder/init.go index 2cabfc8abd..0593d88560 100644 --- a/nodebuilder/init.go +++ b/nodebuilder/init.go @@ -18,6 +18,9 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder/state" ) +// PrintKeyringInfo whether to print keyring information during init. +var PrintKeyringInfo = true + // Init initializes the Node FileSystem Store for the given Node Type 'tp' in the directory under // 'path'. func Init(cfg Config, path string, tp node.Type) error { @@ -213,8 +216,10 @@ func generateKeys(cfg Config, ksPath string) error { if err != nil { return err } - fmt.Printf("\nNAME: %s\nADDRESS: %s\nMNEMONIC (save this somewhere safe!!!): \n%s\n\n", - keyInfo.Name, addr.String(), mn) + if PrintKeyringInfo { + fmt.Printf("\nNAME: %s\nADDRESS: %s\nMNEMONIC (save this somewhere safe!!!): \n%s\n\n", + keyInfo.Name, addr.String(), mn) + } return nil } diff --git a/nodebuilder/module.go b/nodebuilder/module.go index 3f3b08e68b..ad287b1ac8 100644 --- a/nodebuilder/module.go +++ b/nodebuilder/module.go @@ -5,14 +5,18 @@ import ( "go.uber.org/fx" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/libs/fxutil" + "github.com/celestiaorg/celestia-node/nodebuilder/blob" "github.com/celestiaorg/celestia-node/nodebuilder/core" + "github.com/celestiaorg/celestia-node/nodebuilder/da" "github.com/celestiaorg/celestia-node/nodebuilder/das" "github.com/celestiaorg/celestia-node/nodebuilder/fraud" "github.com/celestiaorg/celestia-node/nodebuilder/gateway" - "github.com/celestiaorg/celestia-node/nodebuilder/header" + modhead "github.com/celestiaorg/celestia-node/nodebuilder/header" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/p2p" + "github.com/celestiaorg/celestia-node/nodebuilder/prune" "github.com/celestiaorg/celestia-node/nodebuilder/rpc" "github.com/celestiaorg/celestia-node/nodebuilder/share" "github.com/celestiaorg/celestia-node/nodebuilder/state" @@ -44,15 +48,18 @@ func ConstructModule(tp node.Type, network p2p.Network, cfg *Config, store Store fx.Supply(signer), // modules provided by the node p2p.ConstructModule(tp, &cfg.P2P), - state.ConstructModule(tp, &cfg.State), - header.ConstructModule(tp, &cfg.Header), + state.ConstructModule(tp, &cfg.State, &cfg.Core), + modhead.ConstructModule[*header.ExtendedHeader](tp, &cfg.Header), share.ConstructModule(tp, &cfg.Share), - rpc.ConstructModule(tp, &cfg.RPC), gateway.ConstructModule(tp, &cfg.Gateway), core.ConstructModule(tp, &cfg.Core), das.ConstructModule(tp, &cfg.DASer), fraud.ConstructModule(tp), + blob.ConstructModule(), + da.ConstructModule(), node.ConstructModule(tp), + prune.ConstructModule(tp), + rpc.ConstructModule(tp, &cfg.RPC), ) return fx.Module( diff --git a/nodebuilder/node.go b/nodebuilder/node.go index 90caac5dc9..b16a376cc1 100644 --- a/nodebuilder/node.go +++ b/nodebuilder/node.go @@ -5,10 +5,10 @@ import ( "errors" "fmt" "strings" - "time" - "github.com/ipfs/go-blockservice" - exchange "github.com/ipfs/go-ipfs-exchange-interface" + "github.com/cristalhq/jwt" + "github.com/ipfs/boxo/blockservice" + "github.com/ipfs/boxo/exchange" logging "github.com/ipfs/go-log/v2" pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/host" @@ -21,6 +21,8 @@ import ( "github.com/celestiaorg/celestia-node/api/gateway" "github.com/celestiaorg/celestia-node/api/rpc" + "github.com/celestiaorg/celestia-node/nodebuilder/blob" + "github.com/celestiaorg/celestia-node/nodebuilder/da" "github.com/celestiaorg/celestia-node/nodebuilder/das" "github.com/celestiaorg/celestia-node/nodebuilder/fraud" "github.com/celestiaorg/celestia-node/nodebuilder/header" @@ -30,8 +32,6 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder/state" ) -var Timeout = time.Minute * 2 - var ( log = logging.Logger("node") fxLog = logging.Logger("fx") @@ -50,6 +50,7 @@ type Node struct { Network p2p.Network Bootstrappers p2p.Bootstrappers Config *Config + AdminSigner jwt.Signer // rpc components RPCServer *rpc.Server // not optional @@ -68,7 +69,10 @@ type Node struct { HeaderServ header.Module // not optional StateServ state.Module // not optional FraudServ fraud.Module // not optional + BlobServ blob.Module // not optional DASer das.Module // not optional + AdminServ node.Module // not optional + DAMod da.Module // not optional // start and stop control ref internal fx.App lifecycle funcs to be called from Start and Stop start, stop lifecycleFunc @@ -93,20 +97,25 @@ func NewWithConfig(tp node.Type, network p2p.Network, store Store, cfg *Config, // Start launches the Node and all its components and services. func (n *Node) Start(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, Timeout) + to := n.Config.Node.StartupTimeout + ctx, cancel := context.WithTimeout(ctx, to) defer cancel() err := n.start(ctx) if err != nil { log.Debugf("error starting %s Node: %s", n.Type, err) if errors.Is(err, context.DeadlineExceeded) { - return fmt.Errorf("node: failed to start within timeout(%s): %w", Timeout, err) + return fmt.Errorf("node: failed to start within timeout(%s): %w", to, err) } return fmt.Errorf("node: failed to start: %w", err) } - log.Infof("\n\n/_____/ /_____/ /_____/ /_____/ /_____/ \n\nStarted celestia DA node \nnode "+ - "type: %s\nnetwork: %s\n\n/_____/ /_____/ /_____/ /_____/ /_____/ \n", strings.ToLower(n.Type.String()), + log.Infof("\n\n/_____/ /_____/ /_____/ /_____/ /_____/ \n\n"+ + "Started celestia DA node \n"+ + "node version: %s\nnode type: %s\nnetwork: %s\n\n"+ + "/_____/ /_____/ /_____/ /_____/ /_____/ \n", + node.GetBuildInfo().SemanticVersion, + strings.ToLower(n.Type.String()), n.Network) addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(n.Host)) @@ -138,14 +147,15 @@ func (n *Node) Run(ctx context.Context) error { // Canceling the given context earlier 'ctx' unblocks the Stop and aborts graceful shutdown forcing // remaining Modules/Services to close immediately. func (n *Node) Stop(ctx context.Context) error { - ctx, cancel := context.WithTimeout(ctx, Timeout) + to := n.Config.Node.ShutdownTimeout + ctx, cancel := context.WithTimeout(ctx, to) defer cancel() err := n.stop(ctx) if err != nil { log.Debugf("error stopping %s Node: %s", n.Type, err) if errors.Is(err, context.DeadlineExceeded) { - return fmt.Errorf("node: failed to stop within timeout(%s): %w", Timeout, err) + return fmt.Errorf("node: failed to stop within timeout(%s): %w", to, err) } return fmt.Errorf("node: failed to stop: %w", err) } diff --git a/nodebuilder/node/admin.go b/nodebuilder/node/admin.go index 4607c4d33f..f71af66081 100644 --- a/nodebuilder/node/admin.go +++ b/nodebuilder/node/admin.go @@ -2,21 +2,25 @@ package node import ( "context" - "fmt" + "github.com/cristalhq/jwt" "github.com/filecoin-project/go-jsonrpc/auth" logging "github.com/ipfs/go-log/v2" + + "github.com/celestiaorg/celestia-node/libs/authtoken" ) -const APIVersion = "v0.1.0" +const APIVersion = "v0.11.0" type module struct { - tp Type + tp Type + signer jwt.Signer } -func newModule(tp Type) Module { +func newModule(tp Type, signer jwt.Signer) Module { return &module{ - tp: tp, + tp: tp, + signer: signer, } } @@ -34,14 +38,22 @@ func (m *module) Info(context.Context) (Info, error) { }, nil } +func (m *module) Ready(context.Context) (bool, error) { + // Because the node uses FX to provide the RPC last, all services' lifecycles have been started by + // the point this endpoint is available. It is not 100% guaranteed at this point that all services + // are fully ready, but it is very high probability and all endpoints are available at this point + // regardless. + return true, nil +} + func (m *module) LogLevelSet(_ context.Context, name, level string) error { return logging.SetLogLevel(name, level) } -func (m *module) AuthVerify(context.Context, string) ([]auth.Permission, error) { - return []auth.Permission{}, fmt.Errorf("not implemented") +func (m *module) AuthVerify(_ context.Context, token string) ([]auth.Permission, error) { + return authtoken.ExtractSignedPermissions(m.signer, token) } -func (m *module) AuthNew(context.Context, []auth.Permission) ([]byte, error) { - return nil, fmt.Errorf("not implemented") +func (m *module) AuthNew(_ context.Context, permissions []auth.Permission) (string, error) { + return authtoken.NewSignedJWT(m.signer, permissions) } diff --git a/nodebuilder/node/buildInfo.go b/nodebuilder/node/buildInfo.go index 5f5bdde28e..53d8554d4d 100644 --- a/nodebuilder/node/buildInfo.go +++ b/nodebuilder/node/buildInfo.go @@ -1,9 +1,35 @@ package node -// BuildInfo stores all necessary information for the current build. +import ( + "fmt" + "runtime" +) + +var ( + buildTime string + lastCommit string + semanticVersion string + + systemVersion = fmt.Sprintf("%s/%s", runtime.GOARCH, runtime.GOOS) + golangVersion = runtime.Version() +) + +// BuildInfo represents all necessary information about current build. type BuildInfo struct { + BuildTime string LastCommit string SemanticVersion string SystemVersion string GolangVersion string } + +// GetBuildInfo returns information about current build. +func GetBuildInfo() *BuildInfo { + return &BuildInfo{ + buildTime, + lastCommit, + semanticVersion, + systemVersion, + golangVersion, + } +} diff --git a/nodebuilder/node/cmd/node.go b/nodebuilder/node/cmd/node.go new file mode 100644 index 0000000000..a65727fb03 --- /dev/null +++ b/nodebuilder/node/cmd/node.go @@ -0,0 +1,105 @@ +package cmd + +import ( + "errors" + "strings" + + "github.com/filecoin-project/go-jsonrpc/auth" + "github.com/spf13/cobra" + + cmdnode "github.com/celestiaorg/celestia-node/cmd" +) + +func init() { + Cmd.AddCommand(nodeInfoCmd, logCmd, verifyCmd, authCmd) +} + +var Cmd = &cobra.Command{ + Use: "node [command]", + Short: "Allows administrating running node.", + Args: cobra.NoArgs, + PersistentPreRunE: cmdnode.InitClient, +} + +var nodeInfoCmd = &cobra.Command{ + Use: "info", + Args: cobra.NoArgs, + Short: "Returns administrative information about the node.", + RunE: func(c *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(c.Context()) + if err != nil { + return err + } + defer client.Close() + + info, err := client.Node.Info(c.Context()) + return cmdnode.PrintOutput(info, err, nil) + }, +} + +var logCmd = &cobra.Command{ + Use: "log-level", + Args: cobra.MinimumNArgs(1), + Short: "Sets log level for module.", + Long: "Allows to set log level for module to in format :" + + "`DEBUG, INFO, WARN, ERROR, DPANIC, PANIC, FATAL and their lower-case forms`.\n" + + "To set all modules to a particular level `*:` should be passed", + RunE: func(c *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(c.Context()) + if err != nil { + return err + } + defer client.Close() + + for _, ll := range args { + params := strings.Split(ll, ":") + if len(params) != 2 { + return errors.New("cmd: log-level arg must be in form :," + + "e.g. pubsub:debug") + } + + if err := client.Node.LogLevelSet(c.Context(), params[0], params[1]); err != nil { + return err + } + } + return nil + }, +} + +var verifyCmd = &cobra.Command{ + Use: "permissions", + Args: cobra.ExactArgs(1), + Short: "Returns the permissions assigned to the given token.", + + RunE: func(c *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(c.Context()) + if err != nil { + return err + } + defer client.Close() + + perms, err := client.Node.AuthVerify(c.Context(), args[0]) + return cmdnode.PrintOutput(perms, err, nil) + }, +} + +var authCmd = &cobra.Command{ + Use: "set-permissions", + Args: cobra.MinimumNArgs(1), + Short: "Signs and returns a new token with the given permissions.", + RunE: func(c *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(c.Context()) + if err != nil { + return err + } + defer client.Close() + + perms := make([]auth.Permission, len(args)) + for i, p := range args { + perms[i] = (auth.Permission)(p) + } + + result, err := client.Node.AuthNew(c.Context(), perms) + return cmdnode.PrintOutput(result, err, nil) + }, +} diff --git a/nodebuilder/node/config.go b/nodebuilder/node/config.go new file mode 100644 index 0000000000..e44fe2f014 --- /dev/null +++ b/nodebuilder/node/config.go @@ -0,0 +1,38 @@ +package node + +import ( + "fmt" + "time" +) + +var defaultLifecycleTimeout = time.Minute * 2 + +type Config struct { + StartupTimeout time.Duration + ShutdownTimeout time.Duration +} + +// DefaultConfig returns the default node configuration for a given node type. +func DefaultConfig(tp Type) Config { + var timeout time.Duration + switch tp { + case Light: + timeout = time.Second * 20 + default: + timeout = defaultLifecycleTimeout + } + return Config{ + StartupTimeout: timeout, + ShutdownTimeout: timeout, + } +} + +func (c *Config) Validate() error { + if c.StartupTimeout == 0 { + return fmt.Errorf("invalid startup timeout: %v", c.StartupTimeout) + } + if c.ShutdownTimeout == 0 { + return fmt.Errorf("invalid shutdown timeout: %v", c.ShutdownTimeout) + } + return nil +} diff --git a/nodebuilder/node/metrics.go b/nodebuilder/node/metrics.go index 625e8425e8..07c9a5fc0f 100644 --- a/nodebuilder/node/metrics.go +++ b/nodebuilder/node/metrics.go @@ -4,11 +4,12 @@ import ( "context" "time" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" ) -var meter = global.MeterProvider().Meter("node") +var meter = otel.Meter("node") var ( timeStarted time.Time @@ -17,37 +18,55 @@ var ( // WithMetrics registers node metrics. func WithMetrics() error { - nodeStartTS, err := meter. - AsyncFloat64(). - Gauge( - "node_start_ts", - instrument.WithDescription("timestamp when the node was started"), - ) + nodeStartTS, err := meter.Int64ObservableGauge( + "node_start_ts", + metric.WithDescription("timestamp when the node was started"), + ) if err != nil { return err } - totalNodeRunTime, err := meter. - AsyncFloat64(). - Counter( - "node_runtime_counter_in_seconds", - instrument.WithDescription("total time the node has been running"), - ) + totalNodeRunTime, err := meter.Float64ObservableCounter( + "node_runtime_counter_in_seconds", + metric.WithDescription("total time the node has been running"), + ) if err != nil { return err } - return meter.RegisterCallback( - []instrument.Asynchronous{nodeStartTS, totalNodeRunTime}, - func(ctx context.Context) { - if !nodeStarted { - // Observe node start timestamp - timeStarted = time.Now() - nodeStartTS.Observe(ctx, float64(timeStarted.Unix())) - nodeStarted = true - } - - totalNodeRunTime.Observe(ctx, time.Since(timeStarted).Seconds()) - }, + buildInfoGauge, err := meter.Float64ObservableGauge( + "build_info", + metric.WithDescription("Celestia Node build information"), ) + if err != nil { + return err + } + + callback := func(ctx context.Context, observer metric.Observer) error { + if !nodeStarted { + // Observe node start timestamp + timeStarted = time.Now() + observer.ObserveInt64(nodeStartTS, timeStarted.Unix()) + nodeStarted = true + } + + observer.ObserveFloat64(totalNodeRunTime, time.Since(timeStarted).Seconds()) + + // Observe build info with labels + labels := metric.WithAttributes( + attribute.String("build_time", buildTime), + attribute.String("last_commit", lastCommit), + attribute.String("semantic_version", semanticVersion), + attribute.String("system_version", systemVersion), + attribute.String("golang_version", golangVersion), + ) + + observer.ObserveFloat64(buildInfoGauge, 1, labels) + + return nil + } + + _, err = meter.RegisterCallback(callback, nodeStartTS, totalNodeRunTime, buildInfoGauge) + + return err } diff --git a/nodebuilder/node/mocks/api.go b/nodebuilder/node/mocks/api.go index 39d500bc04..d8789a771c 100644 --- a/nodebuilder/node/mocks/api.go +++ b/nodebuilder/node/mocks/api.go @@ -8,10 +8,9 @@ import ( context "context" reflect "reflect" + node "github.com/celestiaorg/celestia-node/nodebuilder/node" auth "github.com/filecoin-project/go-jsonrpc/auth" gomock "github.com/golang/mock/gomock" - - node "github.com/celestiaorg/celestia-node/nodebuilder/node" ) // MockModule is a mock of Module interface. @@ -38,10 +37,10 @@ func (m *MockModule) EXPECT() *MockModuleMockRecorder { } // AuthNew mocks base method. -func (m *MockModule) AuthNew(arg0 context.Context, arg1 []auth.Permission) ([]byte, error) { +func (m *MockModule) AuthNew(arg0 context.Context, arg1 []auth.Permission) (string, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AuthNew", arg0, arg1) - ret0, _ := ret[0].([]byte) + ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/nodebuilder/node/module.go b/nodebuilder/node/module.go index e7ae88182e..5abfad8e5f 100644 --- a/nodebuilder/node/module.go +++ b/nodebuilder/node/module.go @@ -9,7 +9,7 @@ func ConstructModule(tp Type) fx.Option { return fx.Module( "node", fx.Provide(func(secret jwt.Signer) Module { - return newModule(tp) + return newModule(tp, secret) }), fx.Provide(secret), ) diff --git a/nodebuilder/node/node.go b/nodebuilder/node/node.go index c33f73ef58..b2bc7dac31 100644 --- a/nodebuilder/node/node.go +++ b/nodebuilder/node/node.go @@ -14,13 +14,16 @@ type Module interface { // Info returns administrative information about the node. Info(context.Context) (Info, error) + // Ready returns true once the node's RPC is ready to accept requests. + Ready(context.Context) (bool, error) + // LogLevelSet sets the given component log level to the given level. LogLevelSet(ctx context.Context, name, level string) error // AuthVerify returns the permissions assigned to the given token. AuthVerify(ctx context.Context, token string) ([]auth.Permission, error) // AuthNew signs and returns a new token with the given permissions. - AuthNew(ctx context.Context, perms []auth.Permission) ([]byte, error) + AuthNew(ctx context.Context, perms []auth.Permission) (string, error) } var _ Module = (*API)(nil) @@ -28,9 +31,10 @@ var _ Module = (*API)(nil) type API struct { Internal struct { Info func(context.Context) (Info, error) `perm:"admin"` + Ready func(context.Context) (bool, error) `perm:"read"` LogLevelSet func(ctx context.Context, name, level string) error `perm:"admin"` AuthVerify func(ctx context.Context, token string) ([]auth.Permission, error) `perm:"admin"` - AuthNew func(ctx context.Context, perms []auth.Permission) ([]byte, error) `perm:"admin"` + AuthNew func(ctx context.Context, perms []auth.Permission) (string, error) `perm:"admin"` } } @@ -38,6 +42,10 @@ func (api *API) Info(ctx context.Context) (Info, error) { return api.Internal.Info(ctx) } +func (api *API) Ready(ctx context.Context) (bool, error) { + return api.Internal.Ready(ctx) +} + func (api *API) LogLevelSet(ctx context.Context, name, level string) error { return api.Internal.LogLevelSet(ctx, name, level) } @@ -46,6 +54,6 @@ func (api *API) AuthVerify(ctx context.Context, token string) ([]auth.Permission return api.Internal.AuthVerify(ctx, token) } -func (api *API) AuthNew(ctx context.Context, perms []auth.Permission) ([]byte, error) { +func (api *API) AuthNew(ctx context.Context, perms []auth.Permission) (string, error) { return api.Internal.AuthNew(ctx, perms) } diff --git a/nodebuilder/node_light_test.go b/nodebuilder/node_light_test.go index a7a70d0622..7138a23c9e 100644 --- a/nodebuilder/node_light_test.go +++ b/nodebuilder/node_light_test.go @@ -1,6 +1,7 @@ package nodebuilder import ( + "context" "crypto/rand" "testing" @@ -11,6 +12,7 @@ import ( nodebuilder "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/p2p" + "github.com/celestiaorg/celestia-node/nodebuilder/state" ) func TestNewLightWithP2PKey(t *testing.T) { @@ -44,3 +46,11 @@ func TestLight_WithNetwork(t *testing.T) { require.NotNil(t, node) assert.Equal(t, p2p.Private, node.Network) } + +// TestLight_WithStubbedCoreAccessor ensures that a node started without +// a core connection will return a stubbed StateModule. +func TestLight_WithStubbedCoreAccessor(t *testing.T) { + node := TestNode(t, nodebuilder.Light) + _, err := node.StateServ.Balance(context.Background()) + assert.ErrorIs(t, state.ErrNoStateAccess, err) +} diff --git a/nodebuilder/node_test.go b/nodebuilder/node_test.go index 11b27b076a..41eff32fab 100644 --- a/nodebuilder/node_test.go +++ b/nodebuilder/node_test.go @@ -1,3 +1,5 @@ +//go:build !race + package nodebuilder import ( @@ -13,6 +15,7 @@ import ( collectormetricpb "go.opentelemetry.io/proto/otlp/collector/metrics/v1" "google.golang.org/protobuf/proto" + "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/share" ) @@ -34,6 +37,7 @@ func TestLifecycle(t *testing.T) { require.NotNil(t, node.Host) require.NotNil(t, node.HeaderServ) require.NotNil(t, node.StateServ) + require.NotNil(t, node.AdminServ) require.Equal(t, tt.tp, node.Type) ctx, cancel := context.WithCancel(context.Background()) @@ -42,14 +46,8 @@ func TestLifecycle(t *testing.T) { err := node.Start(ctx) require.NoError(t, err) - // ensure the state service is running - require.False(t, node.StateServ.IsStopped(ctx)) - err = node.Stop(ctx) require.NoError(t, err) - - // ensure the state service is stopped - require.True(t, node.StateServ.IsStopped(ctx)) }) } } @@ -80,7 +78,6 @@ func TestLifecycle_WithMetrics(t *testing.T) { otlpmetrichttp.WithInsecure(), }, tt.tp, - node.BuildInfo{}, ), ) require.NotNil(t, node) @@ -96,14 +93,8 @@ func TestLifecycle_WithMetrics(t *testing.T) { err := node.Start(ctx) require.NoError(t, err) - // ensure the state service is running - require.False(t, node.StateServ.IsStopped(ctx)) - err = node.Stop(ctx) require.NoError(t, err) - - // ensure the state service is stopped - require.True(t, node.StateServ.IsStopped(ctx)) }) } } @@ -136,6 +127,9 @@ func StartMockOtelCollectorHTTPServer(t *testing.T) (string, func()) { } func TestEmptyBlockExists(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + var test = []struct { tp node.Type }{ @@ -148,15 +142,13 @@ func TestEmptyBlockExists(t *testing.T) { for i, tt := range test { t.Run(strconv.Itoa(i), func(t *testing.T) { node := TestNode(t, tt.tp) - - ctx, cancel := context.WithTimeout(context.Background(), Timeout) - defer cancel() - err := node.Start(ctx) require.NoError(t, err) // ensure an empty block exists in store - err = node.ShareServ.SharesAvailable(ctx, share.EmptyRoot()) + + eh := headertest.RandExtendedHeaderWithRoot(t, share.EmptyRoot()) + err = node.ShareServ.SharesAvailable(ctx, eh) require.NoError(t, err) err = node.Stop(ctx) diff --git a/nodebuilder/p2p/bitswap.go b/nodebuilder/p2p/bitswap.go index 3a99dd2c7d..014435071a 100644 --- a/nodebuilder/p2p/bitswap.go +++ b/nodebuilder/p2p/bitswap.go @@ -2,13 +2,15 @@ package p2p import ( "context" + "errors" "fmt" + "github.com/ipfs/boxo/bitswap/client" + "github.com/ipfs/boxo/bitswap/network" + "github.com/ipfs/boxo/bitswap/server" + "github.com/ipfs/boxo/blockstore" + "github.com/ipfs/boxo/exchange" "github.com/ipfs/go-datastore" - blockstore "github.com/ipfs/go-ipfs-blockstore" - exchange "github.com/ipfs/go-ipfs-exchange-interface" - "github.com/ipfs/go-libipfs/bitswap" - "github.com/ipfs/go-libipfs/bitswap/network" routinghelpers "github.com/libp2p/go-libp2p-routing-helpers" hst "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/protocol" @@ -28,17 +30,38 @@ const ( // dataExchange provides a constructor for IPFS block's DataExchange over BitSwap. func dataExchange(params bitSwapParams) exchange.Interface { - prefix := protocol.ID(fmt.Sprintf("/celestia/%s", params.Net)) - return bitswap.New( + prefix := protocolID(params.Net) + net := network.NewFromIpfsHost(params.Host, &routinghelpers.Null{}, network.Prefix(prefix)) + srvr := server.New( params.Ctx, - network.NewFromIpfsHost(params.Host, &routinghelpers.Null{}, network.Prefix(prefix)), + net, params.Bs, - bitswap.ProvideEnabled(false), - // NOTE: These below ar required for our protocol to work reliably. - // See https://github.com/celestiaorg/celestia-node/issues/732 - bitswap.SetSendDontHaves(false), - bitswap.SetSimulateDontHavesOnTimeout(false), + server.ProvideEnabled(false), // we don't provide blocks over DHT + // NOTE: These below are required for our protocol to work reliably. + // // See https://github.com/celestiaorg/celestia-node/issues/732 + server.SetSendDontHaves(false), ) + + clnt := client.New( + params.Ctx, + net, + params.Bs, + client.WithBlockReceivedNotifier(srvr), + client.SetSimulateDontHavesOnTimeout(false), + client.WithoutDuplicatedBlockStats(), + ) + net.Start(srvr, clnt) // starting with hook does not work + + params.Lifecycle.Append(fx.Hook{ + OnStop: func(ctx context.Context) (err error) { + err = errors.Join(err, clnt.Close()) + err = errors.Join(err, srvr.Close()) + net.Stop() + return err + }, + }) + + return clnt } func blockstoreFromDatastore(ctx context.Context, ds datastore.Batching) (blockstore.Blockstore, error) { @@ -48,7 +71,7 @@ func blockstoreFromDatastore(ctx context.Context, ds datastore.Batching) (blocks blockstore.CacheOpts{ HasBloomFilterSize: defaultBloomFilterSize, HasBloomFilterHashes: defaultBloomFilterHashes, - HasARCCacheSize: defaultARCCacheSize, + HasTwoQueueCacheSize: defaultARCCacheSize, }, ) } @@ -58,7 +81,7 @@ func blockstoreFromEDSStore(ctx context.Context, store *eds.Store) (blockstore.B ctx, store.Blockstore(), blockstore.CacheOpts{ - HasARCCacheSize: defaultARCCacheSize, + HasTwoQueueCacheSize: defaultARCCacheSize, }, ) } @@ -66,8 +89,13 @@ func blockstoreFromEDSStore(ctx context.Context, store *eds.Store) (blockstore.B type bitSwapParams struct { fx.In - Ctx context.Context - Net Network - Host hst.Host - Bs blockstore.Blockstore + Lifecycle fx.Lifecycle + Ctx context.Context + Net Network + Host hst.Host + Bs blockstore.Blockstore +} + +func protocolID(network Network) protocol.ID { + return protocol.ID(fmt.Sprintf("/celestia/%s", network)) } diff --git a/nodebuilder/p2p/bootstrap.go b/nodebuilder/p2p/bootstrap.go index 0ceefa0453..8e1856f6fb 100644 --- a/nodebuilder/p2p/bootstrap.go +++ b/nodebuilder/p2p/bootstrap.go @@ -37,21 +37,28 @@ func bootstrappersFor(net Network) ([]string, error) { // NOTE: Every time we add a new long-running network, its bootstrap peers have to be added here. var bootstrapList = map[Network][]string{ + Mainnet: { + "/dns4/da-bridge-1.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWSqZaLcn5Guypo2mrHr297YPJnV8KMEMXNjs3qAS8msw8", + "/dns4/da-bridge-2.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWQpuTFELgsUypqp9N4a1rKBccmrmQVY8Em9yhqppTJcXf", + "/dns4/da-bridge-3.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWSGa4huD6ts816navn7KFYiStBiy5LrBQH1HuEahk4TzQ", + "/dns4/da-bridge-4.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWHBXCmXaUNat6ooynXG837JXPsZpSTeSzZx6DpgNatMmR", + "/dns4/da-bridge-5.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWDGTBK1a2Ru1qmnnRwP6Dmc44Zpsxi3xbgFk7ATEPfmEU", + "/dns4/da-bridge-6.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWLTUFyf3QEGqYkHWQS2yCtuUcL78vnKBdXU5gABM1YDeH", + "/dns4/da-full-1.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWKZCMcwGCYbL18iuw3YVpAZoyb1VBGbx9Kapsjw3soZgr", + "/dns4/da-full-2.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWE3fmRtHgfk9DCuQFfY3H3JYEnTU3xZozv1Xmo8KWrWbK", + "/dns4/da-full-3.celestia-bootstrap.net/tcp/2121/p2p/12D3KooWK6Ftsd4XsWCsQZgZPNhTrE5urwmkoo5P61tGvnKmNVyv", + }, Arabica: { - "/dns4/limani.celestia-devops.dev/tcp/2121/p2p/12D3KooWDgG69kXfmSiHjUErN2ahpUC1SXpSfB2urrqMZ6aWC8NS", - "/dns4/marsellesa.celestia-devops.dev/tcp/2121/p2p/12D3KooWHr2wqFAsMXnPzpFsgxmePgXb8BqpkePebwUgLyZc95bd", - "/dns4/parainem.celestia-devops.dev/tcp/2121/p2p/12D3KooWHX8xpwg8qkP7kLKmKGtgZvmsopvgxc6Fwtu665QC7G8q", - "/dns4/kaarina.celestia-devops.dev/tcp/2121/p2p/12D3KooWN6fzdt4sG5QfWRPn4kwCQBdkt7TDNQkWsUymAwKrmvUs", + "/dns4/da-bridge-1.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWGqwzdEqM54Dce6LXzfFr97Bnhvm6rN7KM7MFwdomfm4S", + "/dns4/da-bridge-2.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWCMGM5eZWVfCN9ZLAViGfLUWAfXP5pCm78NFKb9jpBtua", + "/dns4/da-bridge-3.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWEWuqrjULANpukDFGVoHW3RoeUU53Ec9t9v5cwW3MkVdQ", + "/dns4/da-bridge-4.celestia-arabica-11.com/tcp/2121/p2p/12D3KooWLT1ysSrD7XWSBjh7tU1HQanF5M64dHV6AuM6cYEJxMPk", }, Mocha: { - "/dns4/andromeda.celestia-devops.dev/tcp/2121/p2p/12D3KooWKvPXtV1yaQ6e3BRNUHa5Phh8daBwBi3KkGaSSkUPys6D", - "/dns4/libra.celestia-devops.dev/tcp/2121/p2p/12D3KooWK5aDotDcLsabBmWDazehQLMsDkRyARm1k7f1zGAXqbt4", - "/dns4/norma.celestia-devops.dev/tcp/2121/p2p/12D3KooWHYczJDVNfYVkLcNHPTDKCeiVvRhg8Q9JU3bE3m9eEVyY", - }, - BlockspaceRace: { - "/dns4/bootstr-incent-3.celestia.tools/tcp/2121/p2p/12D3KooWNzdKcHagtvvr6qtjcPTAdCN6ZBiBLH8FBHbihxqu4GZx", - "/dns4/bootstr-incent-2.celestia.tools/tcp/2121/p2p/12D3KooWNJZyWeCsrKxKrxsNM1RVL2Edp77svvt7Cosa63TggC9m", - "/dns4/bootstr-incent-1.celestia.tools/tcp/2121/p2p/12D3KooWBtxdBzToQwnS4ySGpph9PtGmmjEyATkgX3PfhAo4xmf7", + "/dns4/da-bridge-mocha-4.celestia-mocha.com/tcp/2121/p2p/12D3KooWCBAbQbJSpCpCGKzqz3rAN4ixYbc63K68zJg9aisuAajg", + "/dns4/da-bridge-mocha-4-2.celestia-mocha.com/tcp/2121/p2p/12D3KooWK6wJkScGQniymdWtBwBuU36n6BRXp9rCDDUD6P5gJr3G", + "/dns4/da-full-1-mocha-4.celestia-mocha.com/tcp/2121/p2p/12D3KooWCUHPLqQXZzpTx1x3TAsdn3vYmTNDhzg66yG8hqoxGGN8", + "/dns4/da-full-2-mocha-4.celestia-mocha.com/tcp/2121/p2p/12D3KooWR6SHsXPkkvhCRn6vp1RqSefgaT1X1nMNvrVjU2o3GoYy", }, Private: {}, } diff --git a/nodebuilder/p2p/cmd/p2p.go b/nodebuilder/p2p/cmd/p2p.go new file mode 100644 index 0000000000..8b44802947 --- /dev/null +++ b/nodebuilder/p2p/cmd/p2p.go @@ -0,0 +1,576 @@ +package cmd + +import ( + "github.com/libp2p/go-libp2p/core/metrics" + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + ma2 "github.com/multiformats/go-multiaddr" + "github.com/spf13/cobra" + + cmdnode "github.com/celestiaorg/celestia-node/cmd" +) + +type peerInfo struct { + ID string `json:"id"` + PeerAddr []string `json:"peer_addr"` +} + +func init() { + Cmd.AddCommand(infoCmd, + peersCmd, + peerInfoCmd, + connectCmd, + closePeerCmd, + connectednessCmd, + natStatusCmd, + blockPeerCmd, + unblockPeerCmd, + blockedPeersCmd, + protectCmd, + unprotectCmd, + protectedCmd, + bandwidthStatsCmd, + peerBandwidthCmd, + bandwidthForProtocolCmd, + pubsubPeersCmd, + ) +} + +var Cmd = &cobra.Command{ + Use: "p2p [command]", + Short: "Allows interaction with the P2P Module via JSON-RPC", + Args: cobra.NoArgs, + PersistentPreRunE: cmdnode.InitClient, +} + +var infoCmd = &cobra.Command{ + Use: "info", + Short: "Gets the node's peer info (peer id and multiaddresses)", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + info, err := client.P2P.Info(cmd.Context()) + + formatter := func(data interface{}) interface{} { + peerAdd := data.(peer.AddrInfo) + ma := make([]string, len(info.Addrs)) + for i := range peerAdd.Addrs { + ma[i] = peerAdd.Addrs[i].String() + } + + return peerInfo{ + ID: peerAdd.ID.String(), + PeerAddr: ma, + } + } + return cmdnode.PrintOutput(info, err, formatter) + }, +} + +var peersCmd = &cobra.Command{ + Use: "peers", + Short: "Lists the peers we are connected to", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + result, err := client.P2P.Peers(cmd.Context()) + peers := make([]string, len(result)) + for i, peer := range result { + peers[i] = peer.String() + } + + formatter := func(data interface{}) interface{} { + conPeers := data.([]string) + return struct { + Peers []string `json:"peers"` + }{ + Peers: conPeers, + } + } + return cmdnode.PrintOutput(peers, err, formatter) + }, +} + +var peerInfoCmd = &cobra.Command{ + Use: "peer-info [param]", + Short: "Gets PeerInfo for a given peer", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + info, err := client.P2P.PeerInfo(cmd.Context(), pid) + formatter := func(data interface{}) interface{} { + peerAdd := data.(peer.AddrInfo) + ma := make([]string, len(info.Addrs)) + for i := range peerAdd.Addrs { + ma[i] = peerAdd.Addrs[i].String() + } + + return peerInfo{ + ID: peerAdd.ID.String(), + PeerAddr: ma, + } + } + return cmdnode.PrintOutput(info, err, formatter) + }, +} + +var connectCmd = &cobra.Command{ + Use: "connect [peer.ID, address]", + Short: "Establishes a connection with the given peer", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + ma, err := ma2.NewMultiaddr(args[1]) + if err != nil { + return err + } + + peerInfo := peer.AddrInfo{ + ID: pid, + Addrs: []ma2.Multiaddr{ma}, + } + + err = client.P2P.Connect(cmd.Context(), peerInfo) + if err != nil { + return cmdnode.PrintOutput(nil, err, nil) + } + return connectednessCmd.RunE(cmd, args) + }, +} + +var closePeerCmd = &cobra.Command{ + Use: "close-peer [peer.ID]", + Short: "Closes the connection with the given peer", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + err = client.P2P.ClosePeer(cmd.Context(), pid) + if err != nil { + return cmdnode.PrintOutput(nil, err, nil) + } + return connectednessCmd.RunE(cmd, args) + }, +} + +var connectednessCmd = &cobra.Command{ + Use: "connectedness [peer.ID]", + Short: "Checks the connection state between current and given peers", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + con, err := client.P2P.Connectedness(cmd.Context(), pid) + + formatter := func(data interface{}) interface{} { + conn := data.(network.Connectedness) + return struct { + ConnectionState string `json:"connection_state"` + }{ + ConnectionState: conn.String(), + } + } + return cmdnode.PrintOutput(con, err, formatter) + }, +} + +var natStatusCmd = &cobra.Command{ + Use: "nat-status", + Short: "Gets the current NAT status", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + r, err := client.P2P.NATStatus(cmd.Context()) + + formatter := func(data interface{}) interface{} { + rr := data.(network.Reachability) + return struct { + Reachability string `json:"reachability"` + }{ + Reachability: rr.String(), + } + } + return cmdnode.PrintOutput(r, err, formatter) + }, +} + +var blockPeerCmd = &cobra.Command{ + Use: "block-peer [peer.ID]", + Short: "Blocks the given peer", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + err = client.P2P.BlockPeer(cmd.Context(), pid) + + formatter := func(data interface{}) interface{} { + err, ok := data.(error) + blocked := false + if !ok { + blocked = true + } + return struct { + Blocked bool `json:"blocked"` + Peer string `json:"peer"` + Reason error `json:"reason,omitempty"` + }{ + Blocked: blocked, + Peer: args[0], + Reason: err, + } + } + return cmdnode.PrintOutput(err, nil, formatter) + }, +} + +var unblockPeerCmd = &cobra.Command{ + Use: "unblock-peer [peer.ID]", + Short: "Unblocks the given peer", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + err = client.P2P.UnblockPeer(cmd.Context(), pid) + + formatter := func(data interface{}) interface{} { + err, ok := data.(error) + unblocked := false + if !ok { + unblocked = true + } + + return struct { + Unblocked bool `json:"unblocked"` + Peer string `json:"peer"` + Reason error `json:"reason,omitempty"` + }{ + Unblocked: unblocked, + Peer: args[0], + Reason: err, + } + } + return cmdnode.PrintOutput(err, nil, formatter) + }, +} + +var blockedPeersCmd = &cobra.Command{ + Use: "blocked-peers", + Short: "Lists the node's blocked peers", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + list, err := client.P2P.ListBlockedPeers(cmd.Context()) + + pids := make([]string, len(list)) + for i, peer := range list { + pids[i] = peer.String() + } + + formatter := func(data interface{}) interface{} { + peers := data.([]string) + return struct { + Peers []string `json:"peers"` + }{ + Peers: peers, + } + } + return cmdnode.PrintOutput(pids, err, formatter) + }, +} + +var protectCmd = &cobra.Command{ + Use: "protect [peer.ID, tag]", + Short: "Protects the given peer from being pruned by the given tag", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + err = client.P2P.Protect(cmd.Context(), pid, args[1]) + + formatter := func(data interface{}) interface{} { + err, ok := data.(error) + protected := false + if !ok { + protected = true + } + return struct { + Protected bool `json:"protected"` + Peer string `json:"peer"` + Reason error `json:"reason,omitempty"` + }{ + Protected: protected, + Peer: args[0], + Reason: err, + } + } + return cmdnode.PrintOutput(err, nil, formatter) + }, +} + +var unprotectCmd = &cobra.Command{ + Use: "unprotect [peer.ID, tag]", + Short: "Removes protection from the given peer.", + Long: "Removes a protection that may have been placed on a peer, under the specified tag." + + "The return value indicates whether the peer continues to be protected after this call, by way of a different tag", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + _, err = client.P2P.Unprotect(cmd.Context(), pid, args[1]) + + formatter := func(data interface{}) interface{} { + err, ok := data.(error) + unprotected := false + if !ok { + unprotected = true + } + return struct { + Unprotected bool `json:"unprotected"` + Peer string `json:"peer"` + Reason error `json:"reason,omitempty"` + }{ + Unprotected: unprotected, + Peer: args[0], + Reason: err, + } + } + return cmdnode.PrintOutput(err, nil, formatter) + }, +} + +var protectedCmd = &cobra.Command{ + Use: "protected [peer.ID, tag]", + Short: "Ensures that a given peer is protected under a specific tag", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + result, err := client.P2P.IsProtected(cmd.Context(), pid, args[1]) + return cmdnode.PrintOutput(result, err, nil) + }, +} + +type bandwidthStats struct { + TotalIn int64 `json:"total_in"` + TotalOut int64 `json:"total_out"` + RateIn float64 `json:"rate_in"` + RateOut float64 `json:"rate_out"` +} + +var bandwidthStatsCmd = &cobra.Command{ + Use: "bandwidth-stats", + Short: "Provides metrics for current peer.", + Long: "Get stats struct with bandwidth metrics for all data sent/" + + "received by the local peer, regardless of protocol or remote peer IDs", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + result, err := client.P2P.BandwidthStats(cmd.Context()) + + formatter := func(data interface{}) interface{} { + stats := data.(metrics.Stats) + return bandwidthStats{ + TotalIn: stats.TotalIn, + TotalOut: stats.TotalOut, + RateIn: stats.RateIn, + RateOut: stats.RateOut, + } + } + return cmdnode.PrintOutput(result, err, formatter) + }, +} + +var peerBandwidthCmd = &cobra.Command{ + Use: "peer-bandwidth [peer.ID]", + Short: "Gets stats struct with bandwidth metrics associated with the given peer.ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + pid, err := peer.Decode(args[0]) + if err != nil { + return err + } + + result, err := client.P2P.BandwidthForPeer(cmd.Context(), pid) + + formatter := func(data interface{}) interface{} { + stats := data.(metrics.Stats) + return bandwidthStats{ + TotalIn: stats.TotalIn, + TotalOut: stats.TotalOut, + RateIn: stats.RateIn, + RateOut: stats.RateOut, + } + } + return cmdnode.PrintOutput(result, err, formatter) + }, +} + +var bandwidthForProtocolCmd = &cobra.Command{ + Use: "protocol-bandwidth [protocol.ID]", + Short: "Gets stats struct with bandwidth metrics associated with the given protocol.ID", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + result, err := client.P2P.BandwidthForProtocol(cmd.Context(), protocol.ID(args[0])) + + formatter := func(data interface{}) interface{} { + stats := data.(metrics.Stats) + return bandwidthStats{ + TotalIn: stats.TotalIn, + TotalOut: stats.TotalOut, + RateIn: stats.RateIn, + RateOut: stats.RateOut, + } + } + return cmdnode.PrintOutput(result, err, formatter) + }, +} + +var pubsubPeersCmd = &cobra.Command{ + Use: "pubsub-peers [topic]", + Short: "Lists the peers we are connected to in the given topic", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + result, err := client.P2P.PubSubPeers(cmd.Context(), args[0]) + peers := make([]string, len(result)) + + for i, peer := range result { + peers[i] = peer.String() + } + + formatter := func(data interface{}) interface{} { + conPeers := data.([]string) + return struct { + Peers []string `json:"peers"` + }{ + Peers: conPeers, + } + } + return cmdnode.PrintOutput(peers, err, formatter) + }, +} diff --git a/nodebuilder/p2p/config.go b/nodebuilder/p2p/config.go index 589945ae9f..99f3fe8879 100644 --- a/nodebuilder/p2p/config.go +++ b/nodebuilder/p2p/config.go @@ -40,6 +40,8 @@ type Config struct { func DefaultConfig(tp node.Type) Config { return Config{ ListenAddresses: []string{ + "/ip4/0.0.0.0/udp/2121/quic-v1/webtransport", + "/ip6/::/udp/2121/quic-v1/webtransport", "/ip4/0.0.0.0/udp/2121/quic-v1", "/ip6/::/udp/2121/quic-v1", "/ip4/0.0.0.0/tcp/2121", @@ -47,6 +49,9 @@ func DefaultConfig(tp node.Type) Config { }, AnnounceAddresses: []string{}, NoAnnounceAddresses: []string{ + "/ip4/127.0.0.1/udp/2121/quic-v1/webtransport", + "/ip4/0.0.0.0/udp/2121/quic-v1/webtransport", + "/ip6/::/udp/2121/quic-v1/webtransport", "/ip4/0.0.0.0/udp/2121/quic-v1", "/ip4/127.0.0.1/udp/2121/quic-v1", "/ip6/::/udp/2121/quic-v1", diff --git a/nodebuilder/p2p/flags.go b/nodebuilder/p2p/flags.go index 0faf15e3ca..8e7c0f8bc0 100644 --- a/nodebuilder/p2p/flags.go +++ b/nodebuilder/p2p/flags.go @@ -32,10 +32,11 @@ Peers must bidirectionally point to each other. (Format: multiformats.io/multiad ) flags.String( networkFlag, - "", - "The name of the network to connect to, e.g. "+ - listProvidedNetworks()+ - ". Must be passed on both init and start to take effect.", + DefaultNetwork.String(), + fmt.Sprintf("The name of the network to connect to, e.g. %s. Must be passed on "+ + "both init and start to take effect. Assumes mainnet (%s) unless otherwise specified.", + listProvidedNetworks(), + DefaultNetwork.String()), ) return flags @@ -67,22 +68,23 @@ func ParseFlags( // ParseNetwork tries to parse the network from the flags and environment, // and returns either the parsed network or the build's default network func ParseNetwork(cmd *cobra.Command) (Network, error) { - parsed := cmd.Flag(networkFlag).Value.String() - // no network set through the flags, so check if there is an override in the env - if parsed == "" { - envNetwork, err := parseNetworkFromEnv() - // no network found in env, so use the default network - if envNetwork == "" { - return DefaultNetwork, err - } + if envNetwork, err := parseNetworkFromEnv(); envNetwork != "" { return envNetwork, err } - // check if user provided the actual network value - // or an alias - if net, err := Network(parsed).Validate(); err == nil { - return net, nil + parsed := cmd.Flag(networkFlag).Value.String() + switch parsed { + case "": + return "", fmt.Errorf("no network provided, allowed values: %s", listProvidedNetworks()) + + case DefaultNetwork.String(): + return DefaultNetwork, nil + + default: + if net, err := Network(parsed).Validate(); err == nil { + return net, nil + } + return "", fmt.Errorf("invalid network specified: %s, allowed values: %s", parsed, listProvidedNetworks()) } - return "", fmt.Errorf("invalid network specified: %s", parsed) } // parseNetworkFromEnv tries to parse the network from the environment. diff --git a/nodebuilder/p2p/flags_test.go b/nodebuilder/p2p/flags_test.go index bec49f6074..cfbb5fed5d 100644 --- a/nodebuilder/p2p/flags_test.go +++ b/nodebuilder/p2p/flags_test.go @@ -69,3 +69,59 @@ func createCmdWithNetworkFlag() *cobra.Command { cmd.Flags().AddFlagSet(flags) return cmd } + +// Set empty network flag and ensure error returned +func TestParseNetwork_emptyFlag(t *testing.T) { + cmd := createCmdWithNetworkFlag() + + err := cmd.Flags().Set(networkFlag, "") + require.NoError(t, err) + + _, err = ParseNetwork(cmd) + assert.Error(t, err) + +} + +// Set empty network flag and ensure error returned +func TestParseNetwork_emptyEnvEmptyFlag(t *testing.T) { + + t.Setenv(EnvCustomNetwork, "") + + cmd := createCmdWithNetworkFlag() + err := cmd.Flags().Set(networkFlag, "") + require.NoError(t, err) + + _, err = ParseNetwork(cmd) + require.Error(t, err) + +} + +// Env overrides empty flag to take precedence +func TestParseNetwork_envOverridesEmptyFlag(t *testing.T) { + + t.Setenv(EnvCustomNetwork, "custom-network") + + cmd := createCmdWithNetworkFlag() + err := cmd.Flags().Set(networkFlag, "") + require.NoError(t, err) + + network, err := ParseNetwork(cmd) + require.NoError(t, err) + assert.Equal(t, Network("custom-network"), network) + +} + +// Explicitly set flag but env should still override +func TestParseNetwork_envOverridesFlag(t *testing.T) { + + t.Setenv(EnvCustomNetwork, "custom-network") + + cmd := createCmdWithNetworkFlag() + err := cmd.Flags().Set(networkFlag, string(Mocha)) + require.NoError(t, err) + + network, err := ParseNetwork(cmd) + require.NoError(t, err) + assert.Equal(t, Network("custom-network"), network) + +} diff --git a/nodebuilder/p2p/genesis.go b/nodebuilder/p2p/genesis.go index 670699b292..e35ca9bf29 100644 --- a/nodebuilder/p2p/genesis.go +++ b/nodebuilder/p2p/genesis.go @@ -23,8 +23,8 @@ func GenesisFor(net Network) (string, error) { // NOTE: Every time we add a new long-running network, its genesis hash has to be added here. var genesisList = map[Network]string{ - Arabica: "EE310062CBB13CE98CBC7EAD3F6A827F0E4A86043FDEB2DA42048821877FE45C", - Mocha: "8038B21032C941372ED601699857043C12E5CC7D5945DCEEA4567D11B5712526", - BlockspaceRace: "1A8491A72F73929680DAA6C93E3B593579261B2E76536BFA4F5B97D6FE76E088", - Private: "", + Mainnet: "6BE39EFD10BA412A9DB5288488303F5DD32CF386707A5BEF33617F4C43301872", + Arabica: "27122593765E07329BC348E8D16E92DCB4C75B34CCCB35C640FD7A4484D4C711", + Mocha: "B93BBE20A0FBFDF955811B6420F8433904664D45DB4BF51022BE4200C1A1680D", + Private: "", } diff --git a/nodebuilder/p2p/ipld.go b/nodebuilder/p2p/ipld.go deleted file mode 100644 index 6278538825..0000000000 --- a/nodebuilder/p2p/ipld.go +++ /dev/null @@ -1,12 +0,0 @@ -package p2p - -import ( - "github.com/ipfs/go-blockservice" - blockstore "github.com/ipfs/go-ipfs-blockstore" - exchange "github.com/ipfs/go-ipfs-exchange-interface" -) - -// blockService constructs IPFS's BlockService for fetching arbitrary Merkle structures. -func blockService(bs blockstore.Blockstore, ex exchange.Interface) blockservice.BlockService { - return blockservice.New(bs, ex) -} diff --git a/nodebuilder/p2p/metrics.go b/nodebuilder/p2p/metrics.go index 3e6caf08ca..095c30d9b7 100644 --- a/nodebuilder/p2p/metrics.go +++ b/nodebuilder/p2p/metrics.go @@ -6,7 +6,6 @@ import ( "net/http" "time" - rcmgrObs "github.com/libp2p/go-libp2p/p2p/host/resource-manager/obs" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/fx" @@ -28,8 +27,6 @@ const ( // prometheusMetrics option sets up native libp2p metrics up func prometheusMetrics(lifecycle fx.Lifecycle, registerer prometheus.Registerer) error { - rcmgrObs.MustRegisterWith(registerer) - registry := registerer.(*prometheus.Registry) mux := http.NewServeMux() diff --git a/nodebuilder/p2p/misc.go b/nodebuilder/p2p/misc.go index 9b7d8d8108..0d842e0601 100644 --- a/nodebuilder/p2p/misc.go +++ b/nodebuilder/p2p/misc.go @@ -1,12 +1,13 @@ package p2p import ( + "context" "time" "github.com/ipfs/go-datastore" connmgri "github.com/libp2p/go-libp2p/core/connmgr" "github.com/libp2p/go-libp2p/core/peerstore" - "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoremem" + "github.com/libp2p/go-libp2p/p2p/host/peerstore/pstoreds" //nolint:staticcheck "github.com/libp2p/go-libp2p/p2p/net/conngater" "github.com/libp2p/go-libp2p/p2p/net/connmgr" @@ -71,7 +72,7 @@ func connectionGater(ds datastore.Batching) (*conngater.BasicConnectionGater, er return conngater.NewBasicConnectionGater(ds) } -// peerStore constructs a PeerStore. -func peerStore() (peerstore.Peerstore, error) { - return pstoremem.NewPeerstore() +// peerStore constructs an on-disk PeerStore. +func peerStore(ctx context.Context, ds datastore.Batching) (peerstore.Peerstore, error) { + return pstoreds.NewPeerstore(ctx, ds, pstoreds.DefaultOpts()) } diff --git a/nodebuilder/p2p/module.go b/nodebuilder/p2p/module.go index e7f60956c7..1ddff02173 100644 --- a/nodebuilder/p2p/module.go +++ b/nodebuilder/p2p/module.go @@ -6,6 +6,7 @@ import ( "go.uber.org/fx" "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/share/ipld" ) var log = logging.Logger("module/p2p") @@ -27,7 +28,7 @@ func ConstructModule(tp node.Type, cfg *Config) fx.Option { fx.Provide(routedHost), fx.Provide(pubSub), fx.Provide(dataExchange), - fx.Provide(blockService), + fx.Provide(ipld.NewBlockservice), fx.Provide(peerRouting), fx.Provide(contentRouting), fx.Provide(addrsFactory(cfg.AnnounceAddresses, cfg.NoAnnounceAddresses)), diff --git a/nodebuilder/p2p/network.go b/nodebuilder/p2p/network.go index 025ba3bddf..53893eff7c 100644 --- a/nodebuilder/p2p/network.go +++ b/nodebuilder/p2p/network.go @@ -10,18 +10,18 @@ import ( // NOTE: Every time we add a new long-running network, it has to be added here. const ( // DefaultNetwork is the default network of the current build. - DefaultNetwork = Mocha + DefaultNetwork = Mainnet // Arabica testnet. See: celestiaorg/networks. - Arabica Network = "arabica-6" + Arabica Network = "arabica-11" // Mocha testnet. See: celestiaorg/networks. - Mocha Network = "mocha" - // BlockspaceRace testnet. See: https://docs.celestia.org/nodes/blockspace-race/. - BlockspaceRace Network = "blockspacerace-0" + Mocha Network = "mocha-4" // Private can be used to set up any private network, including local testing setups. Private Network = "private" + // Celestia mainnet. See: celestiaorg/networks. + Mainnet Network = "celestia" // BlockTime is a network block time. // TODO @renaynay @Wondertan (#790) - BlockTime = time.Second * 15 + BlockTime = time.Second * 10 ) // Network is a type definition for DA network run by Celestia Node. @@ -52,20 +52,20 @@ func (n Network) String() string { // networksList is a strict list of all known long-standing networks. var networksList = map[Network]struct{}{ - Arabica: {}, - Mocha: {}, - BlockspaceRace: {}, - Private: {}, + Mainnet: {}, + Arabica: {}, + Mocha: {}, + Private: {}, } // networkAliases is a strict list of all known long-standing networks // mapped from the string representation of their *alias* (rather than // their actual value) to the Network. var networkAliases = map[string]Network{ - "arabica": Arabica, - "mocha": Mocha, - "blockspacerace": BlockspaceRace, - "private": Private, + "mainnet": Mainnet, + "arabica": Arabica, + "mocha": Mocha, + "private": Private, } // listProvidedNetworks provides a string listing all known long-standing networks for things like diff --git a/nodebuilder/p2p/opts.go b/nodebuilder/p2p/opts.go index 9501dfe8e1..8e5d714a64 100644 --- a/nodebuilder/p2p/opts.go +++ b/nodebuilder/p2p/opts.go @@ -3,6 +3,7 @@ package p2p import ( "encoding/hex" + "github.com/ipfs/go-blockservice" "github.com/libp2p/go-libp2p/core/crypto" hst "github.com/libp2p/go-libp2p/core/host" "go.uber.org/fx" @@ -34,3 +35,8 @@ func WithP2PKeyStr(key string) fx.Option { func WithHost(hst hst.Host) fx.Option { return fxutil.ReplaceAs(hst, new(HostBase)) } + +// WithBlockService allows to replace the default BlockService. +func WithBlockService(bServ blockservice.BlockService) fx.Option { + return fxutil.ReplaceAs(bServ, new(blockservice.BlockService)) +} diff --git a/nodebuilder/p2p/pubsub.go b/nodebuilder/p2p/pubsub.go index 0061ab9eea..13d812e3ce 100644 --- a/nodebuilder/p2p/pubsub.go +++ b/nodebuilder/p2p/pubsub.go @@ -18,6 +18,8 @@ import ( "github.com/celestiaorg/go-fraud" "github.com/celestiaorg/go-fraud/fraudserv" headp2p "github.com/celestiaorg/go-header/p2p" + + "github.com/celestiaorg/celestia-node/header" ) func init() { @@ -66,7 +68,7 @@ func pubSub(cfg Config, params pubSubParams) (*pubsub.PubSub, error) { // * https://github.com/libp2p/specs/blob/master/pubsub/gossipsub/gossipsub-v1.1.md#peer-scoring // * lotus // * prysm - topicScores := topicScoreParams(params.Network) + topicScores := topicScoreParams(params) peerScores, err := peerScoreParams(params.Bootstrappers, cfg) if err != nil { return nil, err @@ -105,15 +107,16 @@ type pubSubParams struct { Host hst.Host Bootstrappers Bootstrappers Network Network + Unmarshaler fraud.ProofUnmarshaler[*header.ExtendedHeader] } -func topicScoreParams(network Network) map[string]*pubsub.TopicScoreParams { +func topicScoreParams(params pubSubParams) map[string]*pubsub.TopicScoreParams { mp := map[string]*pubsub.TopicScoreParams{ - headp2p.PubsubTopicID(network.String()): &headp2p.GossibSubScore, + headp2p.PubsubTopicID(params.Network.String()): &headp2p.GossibSubScore, } - for _, pt := range fraud.Registered() { - mp[fraudserv.PubsubTopicID(pt.String(), network.String())] = &fraudserv.GossibSubScore + for _, pt := range params.Unmarshaler.List() { + mp[fraudserv.PubsubTopicID(pt.String(), params.Network.String())] = &fraudserv.GossibSubScore } return mp diff --git a/nodebuilder/p2p/resources.go b/nodebuilder/p2p/resources.go index 371747463a..6e24e1e542 100644 --- a/nodebuilder/p2p/resources.go +++ b/nodebuilder/p2p/resources.go @@ -6,7 +6,6 @@ import ( "github.com/libp2p/go-libp2p" "github.com/libp2p/go-libp2p/core/network" rcmgr "github.com/libp2p/go-libp2p/p2p/host/resource-manager" - rcmgrObs "github.com/libp2p/go-libp2p/p2p/host/resource-manager/obs" ma "github.com/multiformats/go-multiaddr" madns "github.com/multiformats/go-multiaddr-dns" "go.uber.org/fx" @@ -60,7 +59,7 @@ func allowList(ctx context.Context, cfg Config, bootstrappers Bootstrappers) (rc } func traceReporter() rcmgr.Option { - str, err := rcmgrObs.NewStatsTraceReporter() + str, err := rcmgr.NewStatsTraceReporter() if err != nil { panic(err) // err is always nil as per sources } diff --git a/nodebuilder/prune/module.go b/nodebuilder/prune/module.go new file mode 100644 index 0000000000..2141b74bf1 --- /dev/null +++ b/nodebuilder/prune/module.go @@ -0,0 +1,47 @@ +package prune + +import ( + "context" + + "go.uber.org/fx" + + "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/pruner" + "github.com/celestiaorg/celestia-node/pruner/archival" + "github.com/celestiaorg/celestia-node/pruner/light" +) + +func ConstructModule(tp node.Type) fx.Option { + baseComponents := fx.Options( + fx.Provide(fx.Annotate( + pruner.NewService, + fx.OnStart(func(ctx context.Context, p *pruner.Service) error { + return p.Start(ctx) + }), + fx.OnStop(func(ctx context.Context, p *pruner.Service) error { + return p.Stop(ctx) + }), + )), + ) + + switch tp { + case node.Full, node.Bridge: + return fx.Module("prune", + baseComponents, + fx.Provide(func() pruner.Pruner { + return archival.NewPruner() + }), + fx.Supply(archival.Window), + ) + case node.Light: + return fx.Module("prune", + baseComponents, + fx.Provide(func() pruner.Pruner { + return light.NewPruner() + }), + fx.Supply(light.Window), + ) + default: + panic("unknown node type") + } +} diff --git a/nodebuilder/rpc/config.go b/nodebuilder/rpc/config.go index 306dd562e3..d6031082a8 100644 --- a/nodebuilder/rpc/config.go +++ b/nodebuilder/rpc/config.go @@ -8,15 +8,17 @@ import ( ) type Config struct { - Address string - Port string + Address string + Port string + SkipAuth bool } func DefaultConfig() Config { return Config{ - Address: "0.0.0.0", + Address: defaultBindAddress, // do NOT expose the same port as celestia-core by default so that both can run on the same machine - Port: "26658", + Port: defaultPort, + SkipAuth: false, } } diff --git a/nodebuilder/rpc/config_test.go b/nodebuilder/rpc/config_test.go new file mode 100644 index 0000000000..1c78a1a19f --- /dev/null +++ b/nodebuilder/rpc/config_test.go @@ -0,0 +1,59 @@ +package rpc + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestDefaultConfig tests that the default gateway config is correct. +func TestDefaultConfig(t *testing.T) { + expected := Config{ + Address: defaultBindAddress, + Port: defaultPort, + } + + assert.Equal(t, expected, DefaultConfig()) +} + +func TestConfigValidate(t *testing.T) { + tests := []struct { + name string + cfg Config + err bool + }{ + { + name: "valid config", + cfg: Config{ + Address: "127.0.0.1", + Port: "8080", + }, + err: false, + }, + { + name: "invalid address", + cfg: Config{ + Address: "invalid", + Port: "8080", + }, + err: true, + }, + { + name: "invalid port", + cfg: Config{ + Address: "127.0.0.1", + Port: "invalid", + }, + err: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.cfg.Validate() + if (err != nil) != tt.err { + t.Errorf("Config.Validate() error = %v, err %v", err, tt.err) + } + }) + } +} diff --git a/nodebuilder/rpc/constructors.go b/nodebuilder/rpc/constructors.go index 3d5c368042..43a8055207 100644 --- a/nodebuilder/rpc/constructors.go +++ b/nodebuilder/rpc/constructors.go @@ -4,6 +4,8 @@ import ( "github.com/cristalhq/jwt" "github.com/celestiaorg/celestia-node/api/rpc" + "github.com/celestiaorg/celestia-node/nodebuilder/blob" + "github.com/celestiaorg/celestia-node/nodebuilder/da" "github.com/celestiaorg/celestia-node/nodebuilder/das" "github.com/celestiaorg/celestia-node/nodebuilder/fraud" "github.com/celestiaorg/celestia-node/nodebuilder/header" @@ -22,17 +24,21 @@ func registerEndpoints( daserMod das.Module, p2pMod p2p.Module, nodeMod node.Module, + blobMod blob.Module, + daMod da.Module, serv *rpc.Server, ) { - serv.RegisterAuthedService("fraud", fraudMod, &fraud.API{}) - serv.RegisterAuthedService("das", daserMod, &das.API{}) - serv.RegisterAuthedService("header", headerMod, &header.API{}) - serv.RegisterAuthedService("state", stateMod, &state.API{}) - serv.RegisterAuthedService("share", shareMod, &share.API{}) - serv.RegisterAuthedService("p2p", p2pMod, &p2p.API{}) - serv.RegisterAuthedService("node", nodeMod, &node.API{}) + serv.RegisterService("fraud", fraudMod, &fraud.API{}) + serv.RegisterService("das", daserMod, &das.API{}) + serv.RegisterService("header", headerMod, &header.API{}) + serv.RegisterService("state", stateMod, &state.API{}) + serv.RegisterService("share", shareMod, &share.API{}) + serv.RegisterService("p2p", p2pMod, &p2p.API{}) + serv.RegisterService("node", nodeMod, &node.API{}) + serv.RegisterService("blob", blobMod, &blob.API{}) + serv.RegisterService("da", daMod, &da.API{}) } func server(cfg *Config, auth jwt.Signer) *rpc.Server { - return rpc.NewServer(cfg.Address, cfg.Port, auth) + return rpc.NewServer(cfg.Address, cfg.Port, cfg.SkipAuth, auth) } diff --git a/nodebuilder/rpc/defaults.go b/nodebuilder/rpc/defaults.go new file mode 100644 index 0000000000..55e51a7c9b --- /dev/null +++ b/nodebuilder/rpc/defaults.go @@ -0,0 +1,6 @@ +package rpc + +const ( + defaultBindAddress = "localhost" + defaultPort = "26658" +) diff --git a/nodebuilder/rpc/defaults_test.go b/nodebuilder/rpc/defaults_test.go new file mode 100644 index 0000000000..74d9c98cfc --- /dev/null +++ b/nodebuilder/rpc/defaults_test.go @@ -0,0 +1,12 @@ +package rpc + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestServerDefaultConstants(t *testing.T) { + assert.Equal(t, "localhost", defaultBindAddress) + assert.Equal(t, "26658", defaultPort) +} diff --git a/nodebuilder/rpc/flags.go b/nodebuilder/rpc/flags.go index 167dbc803a..d37014004d 100644 --- a/nodebuilder/rpc/flags.go +++ b/nodebuilder/rpc/flags.go @@ -1,13 +1,18 @@ package rpc import ( + "fmt" + + logging "github.com/ipfs/go-log/v2" "github.com/spf13/cobra" flag "github.com/spf13/pflag" ) var ( + log = logging.Logger("rpc") addrFlag = "rpc.addr" portFlag = "rpc.port" + authFlag = "rpc.skip-auth" ) // Flags gives a set of hardcoded node/rpc package flags. @@ -17,12 +22,17 @@ func Flags() *flag.FlagSet { flags.String( addrFlag, "", - "Set a custom RPC listen address (default: localhost)", + fmt.Sprintf("Set a custom RPC listen address (default: %s)", defaultBindAddress), ) flags.String( portFlag, "", - "Set a custom RPC port (default: 26658)", + fmt.Sprintf("Set a custom RPC port (default: %s)", defaultPort), + ) + flags.Bool( + authFlag, + false, + "Skips authentication for RPC requests", ) return flags @@ -38,4 +48,12 @@ func ParseFlags(cmd *cobra.Command, cfg *Config) { if port != "" { cfg.Port = port } + ok, err := cmd.Flags().GetBool(authFlag) + if err != nil { + panic(err) + } + if ok { + log.Warn("RPC authentication is disabled") + cfg.SkipAuth = true + } } diff --git a/nodebuilder/rpc/flags_test.go b/nodebuilder/rpc/flags_test.go new file mode 100644 index 0000000000..1370995833 --- /dev/null +++ b/nodebuilder/rpc/flags_test.go @@ -0,0 +1,95 @@ +package rpc + +import ( + "fmt" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFlags(t *testing.T) { + flags := Flags() + + // Test addrFlag + addr := flags.Lookup(addrFlag) + require.NotNil(t, addr) + assert.Equal(t, "", addr.Value.String()) + assert.Equal(t, fmt.Sprintf("Set a custom RPC listen address (default: %s)", defaultBindAddress), addr.Usage) + + // Test portFlag + port := flags.Lookup(portFlag) + require.NotNil(t, port) + assert.Equal(t, "", port.Value.String()) + assert.Equal(t, fmt.Sprintf("Set a custom RPC port (default: %s)", defaultPort), port.Usage) +} + +// TestParseFlags tests the ParseFlags function in rpc/flags.go +func TestParseFlags(t *testing.T) { + tests := []struct { + name string + addrFlag string + portFlag string + expected *Config + }{ + { + name: "addrFlag is set", + addrFlag: "127.0.0.1:8080", + portFlag: "", + expected: &Config{ + Address: "127.0.0.1:8080", + Port: "", + }, + }, + { + name: "portFlag is set", + addrFlag: "", + portFlag: "9090", + expected: &Config{ + Address: "", + Port: "9090", + }, + }, + { + name: "both addrFlag and portFlag are set", + addrFlag: "192.168.0.1:1234", + portFlag: "5678", + expected: &Config{ + Address: "192.168.0.1:1234", + Port: "5678", + }, + }, + { + name: "neither addrFlag nor portFlag are set", + addrFlag: "", + portFlag: "", + expected: &Config{ + Address: "", + Port: "", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := &cobra.Command{} + cfg := &Config{} + + cmd.Flags().AddFlagSet(Flags()) + + err := cmd.Flags().Set(addrFlag, test.addrFlag) + if err != nil { + t.Errorf(err.Error()) + } + err = cmd.Flags().Set(portFlag, test.portFlag) + if err != nil { + t.Errorf(err.Error()) + } + + ParseFlags(cmd, cfg) + assert.Equal(t, test.expected.Address, cfg.Address) + assert.Equal(t, test.expected.Port, cfg.Port) + }) + } +} diff --git a/nodebuilder/settings.go b/nodebuilder/settings.go index bea3c78ad2..6dc95df285 100644 --- a/nodebuilder/settings.go +++ b/nodebuilder/settings.go @@ -5,25 +5,36 @@ import ( "fmt" "time" + otelpyroscope "github.com/grafana/otel-profiling-go" + pyroscope "github.com/grafana/pyroscope-go" + logging "github.com/ipfs/go-log/v2" "github.com/libp2p/go-libp2p/core/peer" - "github.com/pyroscope-io/client/pyroscope" + "go.opentelemetry.io/contrib/instrumentation/runtime" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" + sdk "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" + tracesdk "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.11.0" + "go.opentelemetry.io/otel/trace" "go.uber.org/fx" "github.com/celestiaorg/go-fraud" + "github.com/celestiaorg/celestia-node/header" + modcore "github.com/celestiaorg/celestia-node/nodebuilder/core" "github.com/celestiaorg/celestia-node/nodebuilder/das" - modheader "github.com/celestiaorg/celestia-node/nodebuilder/header" + modhead "github.com/celestiaorg/celestia-node/nodebuilder/header" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/p2p" "github.com/celestiaorg/celestia-node/nodebuilder/share" "github.com/celestiaorg/celestia-node/state" ) +const defaultMetricsCollectInterval = 10 * time.Second + // WithNetwork specifies the Network to which the Node should connect to. // WARNING: Use this option with caution and never run the Node with different networks over the // same persisted Store. @@ -41,6 +52,7 @@ func WithPyroscope(endpoint string, nodeType node.Type) fx.Option { return fx.Options( fx.Invoke(func(peerID peer.ID) error { _, err := pyroscope.Start(pyroscope.Config{ + UploadRate: 15 * time.Second, ApplicationName: "celestia.da-node", ServerAddress: endpoint, Tags: map[string]string{ @@ -54,6 +66,7 @@ func WithPyroscope(endpoint string, nodeType node.Type) fx.Option { pyroscope.ProfileAllocSpace, pyroscope.ProfileInuseObjects, pyroscope.ProfileInuseSpace, + pyroscope.ProfileGoroutines, }, }) return err @@ -62,15 +75,23 @@ func WithPyroscope(endpoint string, nodeType node.Type) fx.Option { } // WithMetrics enables metrics exporting for the node. -func WithMetrics(metricOpts []otlpmetrichttp.Option, nodeType node.Type, buildInfo node.BuildInfo) fx.Option { +func WithMetrics(metricOpts []otlpmetrichttp.Option, nodeType node.Type) fx.Option { + // TODO @renaynay: this will be refactored when there is more granular + // control over which module to enable metrics for + modhead.MetricsEnabled = true + modcore.MetricsEnabled = true + baseComponents := fx.Options( fx.Supply(metricOpts), - fx.Supply(buildInfo), fx.Invoke(initializeMetrics), - fx.Invoke(state.WithMetrics), - fx.Invoke(fraud.WithMetrics), + fx.Invoke(func(ca *state.CoreAccessor) { + if ca == nil { + return + } + state.WithMetrics(ca) + }), + fx.Invoke(fraud.WithMetrics[*header.ExtendedHeader]), fx.Invoke(node.WithMetrics), - fx.Invoke(modheader.WithMetrics), fx.Invoke(share.WithDiscoveryMetrics), ) @@ -86,6 +107,7 @@ func WithMetrics(metricOpts []otlpmetrichttp.Option, nodeType node.Type, buildIn case node.Full: opts = fx.Options( baseComponents, + fx.Invoke(share.WithStoreMetrics), fx.Invoke(share.WithShrexServerMetrics), samplingMetrics, ) @@ -97,6 +119,7 @@ func WithMetrics(metricOpts []otlpmetrichttp.Option, nodeType node.Type, buildIn case node.Bridge: opts = fx.Options( baseComponents, + fx.Invoke(share.WithStoreMetrics), fx.Invoke(share.WithShrexServerMetrics), ) default: @@ -105,13 +128,55 @@ func WithMetrics(metricOpts []otlpmetrichttp.Option, nodeType node.Type, buildIn return opts } +func WithTraces(opts []otlptracehttp.Option, pyroOpts []otelpyroscope.Option) fx.Option { + options := fx.Options( + fx.Supply(opts), + fx.Supply(pyroOpts), + fx.Invoke(initializeTraces), + ) + return options +} + +func initializeTraces( + ctx context.Context, + nodeType node.Type, + peerID peer.ID, + network p2p.Network, + opts []otlptracehttp.Option, + pyroOpts []otelpyroscope.Option, +) error { + client := otlptracehttp.NewClient(opts...) + exporter, err := otlptrace.New(ctx, client) + if err != nil { + return fmt.Errorf("creating OTLP trace exporter: %w", err) + } + + var tp trace.TracerProvider + tp = tracesdk.NewTracerProvider( + tracesdk.WithSampler(tracesdk.AlwaysSample()), + // Always be sure to batch in production. + tracesdk.WithBatcher(exporter), + // Record information about this application in a Resource. + tracesdk.WithResource(resource.NewWithAttributes( + semconv.SchemaURL, + semconv.ServiceNamespaceKey.String(nodeType.String()), + semconv.ServiceNameKey.String(fmt.Sprintf("%s/%s", network.String(), peerID.String()))), + )) + + if len(pyroOpts) > 0 { + tp = otelpyroscope.NewTracerProvider(tp, pyroOpts...) + } + otel.SetTracerProvider(tp) + return nil +} + // initializeMetrics initializes the global meter provider. func initializeMetrics( ctx context.Context, lc fx.Lifecycle, peerID peer.ID, nodeType node.Type, - buildInfo node.BuildInfo, + network p2p.Network, opts []otlpmetrichttp.Option, ) error { exp, err := otlpmetrichttp.New(ctx, opts...) @@ -119,19 +184,43 @@ func initializeMetrics( return err } - provider := metric.NewMeterProvider( - metric.WithReader(metric.NewPeriodicReader(exp, metric.WithTimeout(2*time.Second))), - metric.WithResource(resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceNamespaceKey.String(fmt.Sprintf("Celestia-%s", nodeType.String())), - semconv.ServiceNameKey.String(fmt.Sprintf("semver-%s", buildInfo.SemanticVersion)), - semconv.ServiceInstanceIDKey.String(peerID.String()), - ))) + provider := sdk.NewMeterProvider( + sdk.WithReader( + sdk.NewPeriodicReader(exp, + sdk.WithTimeout(defaultMetricsCollectInterval), + sdk.WithInterval(defaultMetricsCollectInterval))), + sdk.WithResource( + resource.NewWithAttributes( + semconv.SchemaURL, + // ServiceNamespaceKey and ServiceNameKey will be concatenated into single attribute with key: + // "job" and value: "%service.namespace%/%service.name%" + semconv.ServiceNamespaceKey.String(network.String()), + semconv.ServiceNameKey.String(nodeType.String()), + // ServiceInstanceIDKey will be exported with key: "instance" + semconv.ServiceInstanceIDKey.String(peerID.String()), + ))) + + err = runtime.Start( + runtime.WithMinimumReadMemStatsInterval(defaultMetricsCollectInterval), + runtime.WithMeterProvider(provider)) + if err != nil { + return fmt.Errorf("start runtime metrics: %w", err) + } + lc.Append(fx.Hook{ OnStop: func(ctx context.Context) error { return provider.Shutdown(ctx) }, }) - global.SetMeterProvider(provider) + otel.SetMeterProvider(provider) + otel.SetErrorHandler(&loggingErrorHandler{}) return nil } + +var metricsLogger = logging.Logger("otlp") + +type loggingErrorHandler struct{} + +func (loggingErrorHandler) Handle(err error) { + metricsLogger.Error(err) +} diff --git a/nodebuilder/share/cmd/share.go b/nodebuilder/share/cmd/share.go new file mode 100644 index 0000000000..b890f2d4c0 --- /dev/null +++ b/nodebuilder/share/cmd/share.go @@ -0,0 +1,179 @@ +package cmd + +import ( + "context" + "encoding/hex" + "fmt" + "strconv" + + "github.com/spf13/cobra" + + rpc "github.com/celestiaorg/celestia-node/api/rpc/client" + cmdnode "github.com/celestiaorg/celestia-node/cmd" + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/share" +) + +func init() { + Cmd.AddCommand( + sharesAvailableCmd, + getSharesByNamespaceCmd, + getShare, + getEDS, + ) +} + +var Cmd = &cobra.Command{ + Use: "share [command]", + Short: "Allows interaction with the Share Module via JSON-RPC", + Args: cobra.NoArgs, + PersistentPreRunE: cmdnode.InitClient, +} + +var sharesAvailableCmd = &cobra.Command{ + Use: "available", + Short: "Subjectively validates if Shares committed to the given Root are available on the Network.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + eh, err := getExtendedHeaderFromCmdArg(cmd.Context(), client, args[0]) + + if err != nil { + return err + } + + err = client.Share.SharesAvailable(cmd.Context(), eh) + formatter := func(data interface{}) interface{} { + err, ok := data.(error) + available := false + if !ok { + available = true + } + return struct { + Available bool `json:"available"` + Hash []byte `json:"dah_hash"` + Reason error `json:"reason,omitempty"` + }{ + Available: available, + Hash: []byte(args[0]), + Reason: err, + } + } + return cmdnode.PrintOutput(err, nil, formatter) + }, +} + +var getSharesByNamespaceCmd = &cobra.Command{ + Use: "get-by-namespace (height | hash) namespace", + Short: "Gets all shares from an EDS within the given namespace.", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + eh, err := getExtendedHeaderFromCmdArg(cmd.Context(), client, args[0]) + + if err != nil { + return err + } + + ns, err := cmdnode.ParseV0Namespace(args[1]) + if err != nil { + return err + } + + shares, err := client.Share.GetSharesByNamespace(cmd.Context(), eh, ns) + return cmdnode.PrintOutput(shares, err, nil) + }, +} + +var getShare = &cobra.Command{ + Use: "get-share (height | hash) row col", + Short: "Gets a Share by coordinates in EDS.", + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + eh, err := getExtendedHeaderFromCmdArg(cmd.Context(), client, args[0]) + + if err != nil { + return err + } + + row, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return err + } + + col, err := strconv.ParseInt(args[2], 10, 64) + if err != nil { + return err + } + + s, err := client.Share.GetShare(cmd.Context(), eh, int(row), int(col)) + + formatter := func(data interface{}) interface{} { + sh, ok := data.(share.Share) + if !ok { + return data + } + + ns := hex.EncodeToString(share.GetNamespace(sh)) + + return struct { + Namespace string `json:"namespace"` + Data []byte `json:"data"` + }{ + Namespace: ns, + Data: share.GetData(sh), + } + } + return cmdnode.PrintOutput(s, err, formatter) + }, +} + +var getEDS = &cobra.Command{ + Use: "get-eds (height | hash)", + Short: "Gets the full EDS identified by the given block height", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + eh, err := getExtendedHeaderFromCmdArg(cmd.Context(), client, args[0]) + + if err != nil { + return err + } + + shares, err := client.Share.GetEDS(cmd.Context(), eh) + return cmdnode.PrintOutput(shares, err, nil) + }, +} + +func getExtendedHeaderFromCmdArg(ctx context.Context, client *rpc.Client, arg string) (*header.ExtendedHeader, error) { + hash, err := hex.DecodeString(arg) + if err == nil { + return client.Header.GetByHash(ctx, hash) + } + height, err := strconv.ParseUint(arg, 10, 64) + if err != nil { + return nil, fmt.Errorf("can't parse the height/hash argument: %w", err) + } + return client.Header.GetByHeight(ctx, height) +} diff --git a/nodebuilder/share/config.go b/nodebuilder/share/config.go index 7fd845a672..1d984b6dca 100644 --- a/nodebuilder/share/config.go +++ b/nodebuilder/share/config.go @@ -5,6 +5,7 @@ import ( "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/share/availability/light" + "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/p2p/discovery" "github.com/celestiaorg/celestia-node/share/p2p/peers" "github.com/celestiaorg/celestia-node/share/p2p/shrexeds" @@ -13,6 +14,9 @@ import ( // TODO: some params are pointers and other are not, Let's fix this. type Config struct { + // EDSStoreParams sets eds store configuration parameters + EDSStoreParams *eds.Parameters + UseShareExchange bool // ShrExEDSParams sets shrexeds client and server configuration parameters ShrExEDSParams *shrexeds.Parameters @@ -22,11 +26,12 @@ type Config struct { PeerManagerParams peers.Parameters LightAvailability light.Parameters `toml:",omitempty"` - Discovery discovery.Parameters + Discovery *discovery.Parameters } func DefaultConfig(tp node.Type) Config { cfg := Config{ + EDSStoreParams: eds.DefaultParameters(), Discovery: discovery.DefaultParameters(), ShrExEDSParams: shrexeds.DefaultParameters(), ShrExNDParams: shrexnd.DefaultParameters(), diff --git a/nodebuilder/share/constructors.go b/nodebuilder/share/constructors.go index 1913b3d576..aa2ac5bec1 100644 --- a/nodebuilder/share/constructors.go +++ b/nodebuilder/share/constructors.go @@ -5,45 +5,43 @@ import ( "errors" "github.com/filecoin-project/dagstore" - "github.com/ipfs/go-datastore" + "github.com/ipfs/boxo/blockservice" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/routing" routingdisc "github.com/libp2p/go-libp2p/p2p/discovery/routing" - "go.uber.org/fx" "github.com/celestiaorg/celestia-app/pkg/da" "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/availability/cache" - "github.com/celestiaorg/celestia-node/share/availability/light" "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/getters" + "github.com/celestiaorg/celestia-node/share/ipld" disc "github.com/celestiaorg/celestia-node/share/p2p/discovery" + "github.com/celestiaorg/celestia-node/share/p2p/peers" ) -func newDiscovery(cfg Config) func(routing.ContentRouting, host.Host) *disc.Discovery { +const ( + // fullNodesTag is the tag used to identify full nodes in the discovery service. + fullNodesTag = "full" +) + +func newDiscovery(cfg *disc.Parameters, +) func(routing.ContentRouting, host.Host, *peers.Manager) (*disc.Discovery, error) { return func( r routing.ContentRouting, h host.Host, - ) *disc.Discovery { + manager *peers.Manager, + ) (*disc.Discovery, error) { return disc.NewDiscovery( + cfg, h, routingdisc.NewRoutingDiscovery(r), - disc.WithPeersLimit(cfg.Discovery.PeersLimit), - disc.WithAdvertiseInterval(cfg.Discovery.AdvertiseInterval), + fullNodesTag, + disc.WithOnPeersUpdate(manager.UpdateFullNodePool), ) } } -// cacheAvailability wraps light availability with a cache for result sampling. -func cacheAvailability(lc fx.Lifecycle, ds datastore.Batching, avail *light.ShareAvailability) share.Availability { - ca := cache.NewShareAvailability(avail, ds) - lc.Append(fx.Hook{ - OnStop: ca.Close, - }) - return ca -} - func newModule(getter share.Getter, avail share.Availability) Module { return &module{getter, avail} } @@ -51,15 +49,27 @@ func newModule(getter share.Getter, avail share.Availability) Module { // ensureEmptyCARExists adds an empty EDS to the provided EDS store. func ensureEmptyCARExists(ctx context.Context, store *eds.Store) error { emptyEDS := share.EmptyExtendedDataSquare() - emptyDAH := da.NewDataAvailabilityHeader(emptyEDS) + emptyDAH, err := da.NewDataAvailabilityHeader(emptyEDS) + if err != nil { + return err + } - err := store.Put(ctx, emptyDAH.Hash(), emptyEDS) + err = store.Put(ctx, emptyDAH.Hash(), emptyEDS) if errors.Is(err, dagstore.ErrShardExists) { return nil } return err } +// ensureEmptyEDSInBS checks if the given DAG contains an empty block data square. +// If it does not, it stores an empty block. This optimization exists to prevent +// redundant storing of empty block data so that it is only stored once and returned +// upon request for a block with an empty data square. +func ensureEmptyEDSInBS(ctx context.Context, bServ blockservice.BlockService) error { + _, err := ipld.AddShares(ctx, share.EmptyBlockShares(), bServ) + return err +} + func lightGetter( shrexGetter *getters.ShrexGetter, ipldGetter *getters.IPLDGetter, @@ -73,8 +83,24 @@ func lightGetter( return getters.NewCascadeGetter(cascade) } +// ShrexGetter is added to bridge nodes for the case that a shard is removed +// after detected shard corruption. This ensures the block is fetched and stored +// by shrex the next time the data is retrieved (meaning shard recovery is +// manual after corruption is detected). +func bridgeGetter( + storeGetter *getters.StoreGetter, + shrexGetter *getters.ShrexGetter, + cfg Config, +) share.Getter { + var cascade []share.Getter + cascade = append(cascade, storeGetter) + if cfg.UseShareExchange { + cascade = append(cascade, shrexGetter) + } + return getters.NewCascadeGetter(cascade) +} + func fullGetter( - store *eds.Store, storeGetter *getters.StoreGetter, shrexGetter *getters.ShrexGetter, ipldGetter *getters.IPLDGetter, @@ -83,8 +109,8 @@ func fullGetter( var cascade []share.Getter cascade = append(cascade, storeGetter) if cfg.UseShareExchange { - cascade = append(cascade, getters.NewTeeGetter(shrexGetter, store)) + cascade = append(cascade, shrexGetter) } - cascade = append(cascade, getters.NewTeeGetter(ipldGetter, store)) + cascade = append(cascade, ipldGetter) return getters.NewCascadeGetter(cascade) } diff --git a/nodebuilder/share/mocks/api.go b/nodebuilder/share/mocks/api.go index 1b26273c0f..4e21cecae0 100644 --- a/nodebuilder/share/mocks/api.go +++ b/nodebuilder/share/mocks/api.go @@ -8,12 +8,10 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - - da "github.com/celestiaorg/celestia-app/pkg/da" + header "github.com/celestiaorg/celestia-node/header" share "github.com/celestiaorg/celestia-node/share" - namespace "github.com/celestiaorg/nmt/namespace" rsmt2d "github.com/celestiaorg/rsmt2d" + gomock "github.com/golang/mock/gomock" ) // MockModule is a mock of Module interface. @@ -40,7 +38,7 @@ func (m *MockModule) EXPECT() *MockModuleMockRecorder { } // GetEDS mocks base method. -func (m *MockModule) GetEDS(arg0 context.Context, arg1 *da.DataAvailabilityHeader) (*rsmt2d.ExtendedDataSquare, error) { +func (m *MockModule) GetEDS(arg0 context.Context, arg1 *header.ExtendedHeader) (*rsmt2d.ExtendedDataSquare, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetEDS", arg0, arg1) ret0, _ := ret[0].(*rsmt2d.ExtendedDataSquare) @@ -55,7 +53,7 @@ func (mr *MockModuleMockRecorder) GetEDS(arg0, arg1 interface{}) *gomock.Call { } // GetShare mocks base method. -func (m *MockModule) GetShare(arg0 context.Context, arg1 *da.DataAvailabilityHeader, arg2, arg3 int) ([]byte, error) { +func (m *MockModule) GetShare(arg0 context.Context, arg1 *header.ExtendedHeader, arg2, arg3 int) ([]byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetShare", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]byte) @@ -70,7 +68,7 @@ func (mr *MockModuleMockRecorder) GetShare(arg0, arg1, arg2, arg3 interface{}) * } // GetSharesByNamespace mocks base method. -func (m *MockModule) GetSharesByNamespace(arg0 context.Context, arg1 *da.DataAvailabilityHeader, arg2 namespace.ID) (share.NamespacedShares, error) { +func (m *MockModule) GetSharesByNamespace(arg0 context.Context, arg1 *header.ExtendedHeader, arg2 share.Namespace) (share.NamespacedShares, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSharesByNamespace", arg0, arg1, arg2) ret0, _ := ret[0].(share.NamespacedShares) @@ -84,22 +82,8 @@ func (mr *MockModuleMockRecorder) GetSharesByNamespace(arg0, arg1, arg2 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSharesByNamespace", reflect.TypeOf((*MockModule)(nil).GetSharesByNamespace), arg0, arg1, arg2) } -// ProbabilityOfAvailability mocks base method. -func (m *MockModule) ProbabilityOfAvailability(arg0 context.Context) float64 { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProbabilityOfAvailability", arg0) - ret0, _ := ret[0].(float64) - return ret0 -} - -// ProbabilityOfAvailability indicates an expected call of ProbabilityOfAvailability. -func (mr *MockModuleMockRecorder) ProbabilityOfAvailability(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProbabilityOfAvailability", reflect.TypeOf((*MockModule)(nil).ProbabilityOfAvailability), arg0) -} - // SharesAvailable mocks base method. -func (m *MockModule) SharesAvailable(arg0 context.Context, arg1 *da.DataAvailabilityHeader) error { +func (m *MockModule) SharesAvailable(arg0 context.Context, arg1 *header.ExtendedHeader) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SharesAvailable", arg0, arg1) ret0, _ := ret[0].(error) diff --git a/nodebuilder/share/module.go b/nodebuilder/share/module.go index 6dfb155bb0..3fa55b2d35 100644 --- a/nodebuilder/share/module.go +++ b/nodebuilder/share/module.go @@ -5,9 +5,13 @@ import ( "github.com/ipfs/go-datastore" "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/p2p/net/conngater" "go.uber.org/fx" - "github.com/celestiaorg/celestia-node/libs/fxutil" + libhead "github.com/celestiaorg/go-header" + "github.com/celestiaorg/go-header/sync" + + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/nodebuilder/node" modp2p "github.com/celestiaorg/celestia-node/nodebuilder/p2p" "github.com/celestiaorg/celestia-node/share" @@ -33,7 +37,7 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option fx.Provide(newModule), fx.Invoke(func(disc *disc.Discovery) {}), fx.Provide(fx.Annotate( - newDiscovery(*cfg), + newDiscovery(cfg.Discovery), fx.OnStart(func(ctx context.Context, d *disc.Discovery) error { return d.Start(ctx) }), @@ -48,6 +52,33 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option ), ) + shrexGetterComponents := fx.Options( + fx.Provide(func() peers.Parameters { + return cfg.PeerManagerParams + }), + fx.Provide( + func(host host.Host, network modp2p.Network) (*shrexnd.Client, error) { + cfg.ShrExNDParams.WithNetworkID(network.String()) + return shrexnd.NewClient(cfg.ShrExNDParams, host) + }, + ), + fx.Provide( + func(host host.Host, network modp2p.Network) (*shrexeds.Client, error) { + cfg.ShrExEDSParams.WithNetworkID(network.String()) + return shrexeds.NewClient(cfg.ShrExEDSParams, host) + }, + ), + fx.Provide(fx.Annotate( + getters.NewShrexGetter, + fx.OnStart(func(ctx context.Context, getter *getters.ShrexGetter) error { + return getter.Start(ctx) + }), + fx.OnStop(func(ctx context.Context, getter *getters.ShrexGetter) error { + return getter.Stop(ctx) + }), + )), + ) + bridgeAndFullComponents := fx.Options( fx.Provide(getters.NewStoreGetter), fx.Invoke(func(edsSrv *shrexeds.Server, ndSrc *shrexnd.Server) {}), @@ -67,11 +98,10 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option func( host host.Host, store *eds.Store, - getter *getters.StoreGetter, network modp2p.Network, ) (*shrexnd.Server, error) { cfg.ShrExNDParams.WithNetworkID(network.String()) - return shrexnd.NewServer(cfg.ShrExNDParams, host, store, getter) + return shrexnd.NewServer(cfg.ShrExNDParams, host, store) }, fx.OnStart(func(ctx context.Context, server *shrexnd.Server) error { return server.Start(ctx) @@ -82,7 +112,7 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option )), fx.Provide(fx.Annotate( func(path node.StorePath, ds datastore.Batching) (*eds.Store, error) { - return eds.NewStore(string(path), ds) + return eds.NewStore(cfg.EDSStoreParams, string(path), ds) }, fx.OnStart(func(ctx context.Context, store *eds.Store) error { err := store.Start(ctx) @@ -112,32 +142,26 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option }), ) - shrexGetterComponents := fx.Options( - fx.Provide(func() peers.Parameters { - return cfg.PeerManagerParams - }), - fx.Provide(peers.NewManager), - fx.Provide( - func(host host.Host, network modp2p.Network) (*shrexnd.Client, error) { - cfg.ShrExNDParams.WithNetworkID(network.String()) - return shrexnd.NewClient(cfg.ShrExNDParams, host) - }, - ), + peerManagerWithShrexPools := fx.Options( fx.Provide( - func(host host.Host, network modp2p.Network) (*shrexeds.Client, error) { - cfg.ShrExEDSParams.WithNetworkID(network.String()) - return shrexeds.NewClient(cfg.ShrExEDSParams, host) + func( + params peers.Parameters, + host host.Host, + connGater *conngater.BasicConnectionGater, + shrexSub *shrexsub.PubSub, + headerSub libhead.Subscriber[*header.ExtendedHeader], + // we must ensure Syncer is started before PeerManager + // so that Syncer registers header validator before PeerManager subscribes to headers + _ *sync.Syncer[*header.ExtendedHeader], + ) (*peers.Manager, error) { + return peers.NewManager( + params, + host, + connGater, + peers.WithShrexSubPools(shrexSub, headerSub), + ) }, ), - fx.Provide(fx.Annotate( - getters.NewShrexGetter, - fx.OnStart(func(ctx context.Context, getter *getters.ShrexGetter) error { - return getter.Start(ctx) - }), - fx.OnStop(func(ctx context.Context, getter *getters.ShrexGetter) error { - return getter.Stop(ctx) - }), - )), ) switch tp { @@ -145,10 +169,10 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option return fx.Module( "share", baseComponents, + fx.Provide(peers.NewManager), bridgeAndFullComponents, - fxutil.ProvideAs(func(getter *getters.StoreGetter) share.Getter { - return getter - }), + shrexGetterComponents, + fx.Provide(bridgeGetter), fx.Invoke(func(lc fx.Lifecycle, sub *shrexsub.PubSub) error { lc.Append(fx.Hook{ OnStart: sub.Start, @@ -160,6 +184,7 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option case node.Full: return fx.Module( "share", + peerManagerWithShrexPools, baseComponents, bridgeAndFullComponents, shrexGetterComponents, @@ -175,8 +200,9 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option light.WithSampleAmount(cfg.LightAvailability.SampleAmount), } }), + peerManagerWithShrexPools, shrexGetterComponents, - fx.Invoke(share.EnsureEmptySquareExists), + fx.Invoke(ensureEmptyEDSInBS), fx.Provide(getters.NewIPLDGetter), fx.Provide(lightGetter), // shrexsub broadcaster stub for daser @@ -185,10 +211,15 @@ func ConstructModule(tp node.Type, cfg *Config, options ...fx.Option) fx.Option return nil } }), - fx.Provide(light.NewShareAvailability), - // cacheAvailability's lifecycle continues to use a fx hook, - // since the LC requires a cacheAvailability but the constructor returns a share.Availability - fx.Provide(cacheAvailability), + fx.Provide(fx.Annotate( + light.NewShareAvailability, + fx.OnStop(func(ctx context.Context, la *light.ShareAvailability) error { + return la.Close(ctx) + }), + )), + fx.Provide(func(avail *light.ShareAvailability) share.Availability { + return avail + }), ) default: panic("invalid node type") diff --git a/nodebuilder/share/opts.go b/nodebuilder/share/opts.go index 20ba0ce58c..e236847f41 100644 --- a/nodebuilder/share/opts.go +++ b/nodebuilder/share/opts.go @@ -1,6 +1,7 @@ package share import ( + "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/getters" disc "github.com/celestiaorg/celestia-node/share/p2p/discovery" "github.com/celestiaorg/celestia-node/share/p2p/peers" @@ -41,3 +42,7 @@ func WithShrexServerMetrics(edsServer *shrexeds.Server, ndServer *shrexnd.Server func WithShrexGetterMetrics(sg *getters.ShrexGetter) error { return sg.WithMetrics() } + +func WithStoreMetrics(s *eds.Store) error { + return s.WithMetrics() +} diff --git a/nodebuilder/share/share.go b/nodebuilder/share/share.go index 0c703f9a15..a8e1e1c895 100644 --- a/nodebuilder/share/share.go +++ b/nodebuilder/share/share.go @@ -3,9 +3,9 @@ package share import ( "context" - "github.com/celestiaorg/nmt/namespace" "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share" ) @@ -28,66 +28,60 @@ var _ Module = (*API)(nil) // //go:generate mockgen -destination=mocks/api.go -package=mocks . Module type Module interface { - // SharesAvailable subjectively validates if Shares committed to the given Root are available on - // the Network. - SharesAvailable(context.Context, *share.Root) error - // ProbabilityOfAvailability calculates the probability of the data square - // being available based on the number of samples collected. - ProbabilityOfAvailability(context.Context) float64 + // SharesAvailable subjectively validates if Shares committed to the given + // ExtendedHeader are available on the Network. + SharesAvailable(context.Context, *header.ExtendedHeader) error // GetShare gets a Share by coordinates in EDS. - GetShare(ctx context.Context, dah *share.Root, row, col int) (share.Share, error) - // GetEDS gets the full EDS identified by the given root. - GetEDS(ctx context.Context, root *share.Root) (*rsmt2d.ExtendedDataSquare, error) + GetShare(ctx context.Context, header *header.ExtendedHeader, row, col int) (share.Share, error) + // GetEDS gets the full EDS identified by the given extended header. + GetEDS(ctx context.Context, header *header.ExtendedHeader) (*rsmt2d.ExtendedDataSquare, error) // GetSharesByNamespace gets all shares from an EDS within the given namespace. // Shares are returned in a row-by-row order if the namespace spans multiple rows. - GetSharesByNamespace(ctx context.Context, root *share.Root, namespace namespace.ID) (share.NamespacedShares, error) + GetSharesByNamespace( + ctx context.Context, header *header.ExtendedHeader, namespace share.Namespace, + ) (share.NamespacedShares, error) } // API is a wrapper around Module for the RPC. // TODO(@distractedm1nd): These structs need to be autogenerated. type API struct { Internal struct { - SharesAvailable func(context.Context, *share.Root) error `perm:"public"` - ProbabilityOfAvailability func(context.Context) float64 `perm:"public"` - GetShare func( + SharesAvailable func(context.Context, *header.ExtendedHeader) error `perm:"read"` + GetShare func( ctx context.Context, - dah *share.Root, + header *header.ExtendedHeader, row, col int, - ) (share.Share, error) `perm:"public"` + ) (share.Share, error) `perm:"read"` GetEDS func( ctx context.Context, - root *share.Root, - ) (*rsmt2d.ExtendedDataSquare, error) `perm:"public"` + header *header.ExtendedHeader, + ) (*rsmt2d.ExtendedDataSquare, error) `perm:"read"` GetSharesByNamespace func( ctx context.Context, - root *share.Root, - namespace namespace.ID, - ) (share.NamespacedShares, error) `perm:"public"` + header *header.ExtendedHeader, + namespace share.Namespace, + ) (share.NamespacedShares, error) `perm:"read"` } } -func (api *API) SharesAvailable(ctx context.Context, root *share.Root) error { - return api.Internal.SharesAvailable(ctx, root) +func (api *API) SharesAvailable(ctx context.Context, header *header.ExtendedHeader) error { + return api.Internal.SharesAvailable(ctx, header) } -func (api *API) ProbabilityOfAvailability(ctx context.Context) float64 { - return api.Internal.ProbabilityOfAvailability(ctx) +func (api *API) GetShare(ctx context.Context, header *header.ExtendedHeader, row, col int) (share.Share, error) { + return api.Internal.GetShare(ctx, header, row, col) } -func (api *API) GetShare(ctx context.Context, dah *share.Root, row, col int) (share.Share, error) { - return api.Internal.GetShare(ctx, dah, row, col) -} - -func (api *API) GetEDS(ctx context.Context, root *share.Root) (*rsmt2d.ExtendedDataSquare, error) { - return api.Internal.GetEDS(ctx, root) +func (api *API) GetEDS(ctx context.Context, header *header.ExtendedHeader) (*rsmt2d.ExtendedDataSquare, error) { + return api.Internal.GetEDS(ctx, header) } func (api *API) GetSharesByNamespace( ctx context.Context, - root *share.Root, - namespace namespace.ID, + header *header.ExtendedHeader, + namespace share.Namespace, ) (share.NamespacedShares, error) { - return api.Internal.GetSharesByNamespace(ctx, root, namespace) + return api.Internal.GetSharesByNamespace(ctx, header, namespace) } type module struct { @@ -95,6 +89,6 @@ type module struct { share.Availability } -func (m module) SharesAvailable(ctx context.Context, root *share.Root) error { - return m.Availability.SharesAvailable(ctx, root) +func (m module) SharesAvailable(ctx context.Context, header *header.ExtendedHeader) error { + return m.Availability.SharesAvailable(ctx, header) } diff --git a/nodebuilder/share/share_test.go b/nodebuilder/share/share_test.go index 388fd9af07..db170709db 100644 --- a/nodebuilder/share/share_test.go +++ b/nodebuilder/share/share_test.go @@ -9,8 +9,6 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/celestiaorg/celestia-app/pkg/da" - "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" ) @@ -19,15 +17,15 @@ func Test_EmptyCARExists(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - tmpDir := t.TempDir() ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - edsStore, err := eds.NewStore(tmpDir, ds) + edsStore, err := eds.NewStore(eds.DefaultParameters(), t.TempDir(), ds) require.NoError(t, err) err = edsStore.Start(ctx) require.NoError(t, err) eds := share.EmptyExtendedDataSquare() - dah := da.NewDataAvailabilityHeader(eds) + dah, err := share.NewRoot(eds) + require.NoError(t, err) // add empty EDS to store err = ensureEmptyCARExists(ctx, edsStore) diff --git a/nodebuilder/state/cmd/state.go b/nodebuilder/state/cmd/state.go new file mode 100644 index 0000000000..d35c4a1b4f --- /dev/null +++ b/nodebuilder/state/cmd/state.go @@ -0,0 +1,412 @@ +package cmd + +import ( + "encoding/hex" + "fmt" + "strconv" + + "cosmossdk.io/math" + "github.com/spf13/cobra" + + cmdnode "github.com/celestiaorg/celestia-node/cmd" + "github.com/celestiaorg/celestia-node/state" +) + +func init() { + Cmd.AddCommand( + accountAddressCmd, + balanceCmd, + balanceForAddressCmd, + transferCmd, + submitTxCmd, + cancelUnbondingDelegationCmd, + beginRedelegateCmd, + undelegateCmd, + delegateCmd, + queryDelegationCmd, + queryUnbondingCmd, + queryRedelegationCmd, + ) +} + +var Cmd = &cobra.Command{ + Use: "state [command]", + Short: "Allows interaction with the State Module via JSON-RPC", + Args: cobra.NoArgs, + PersistentPreRunE: cmdnode.InitClient, +} + +var accountAddressCmd = &cobra.Command{ + Use: "account-address", + Short: "Retrieves the address of the node's account/signer.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + address, err := client.State.AccountAddress(cmd.Context()) + return cmdnode.PrintOutput(address, err, nil) + }, +} + +var balanceCmd = &cobra.Command{ + Use: "balance", + Short: "Retrieves the Celestia coin balance for the node's account/signer and verifies it against " + + "the corresponding block's AppHash.", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + balance, err := client.State.Balance(cmd.Context()) + return cmdnode.PrintOutput(balance, err, nil) + }, +} + +var balanceForAddressCmd = &cobra.Command{ + Use: "balance-for-address [address]", + Short: "Retrieves the Celestia coin balance for the given address and verifies the returned balance against " + + "the corresponding block's AppHash.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + addr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + balance, err := client.State.BalanceForAddress(cmd.Context(), addr) + return cmdnode.PrintOutput(balance, err, nil) + }, +} + +var transferCmd = &cobra.Command{ + Use: "transfer [address] [amount] [fee] [gasLimit]", + Short: "Sends the given amount of coins from default wallet of the node to the given account address.", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + addr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + amount, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("error parsing an amount:%v", err) + } + fee, err := strconv.ParseInt(args[2], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a fee:%v", err) + } + gasLimit, err := strconv.ParseUint(args[3], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a gas limit:%v", err) + } + + txResponse, err := client.State.Transfer( + cmd.Context(), + addr.Address.(state.AccAddress), + math.NewInt(amount), + math.NewInt(fee), gasLimit, + ) + return cmdnode.PrintOutput(txResponse, err, nil) + }, +} + +var submitTxCmd = &cobra.Command{ + Use: "submit-tx [tx]", + Short: "Submits the given transaction/message to the Celestia network and blocks until the tx is included in a block.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + decoded, err := hex.DecodeString(args[0]) + if err != nil { + return fmt.Errorf("failed to decode tx: %v", err) + } + txResponse, err := client.State.SubmitTx( + cmd.Context(), + decoded, + ) + return cmdnode.PrintOutput(txResponse, err, nil) + }, +} + +var cancelUnbondingDelegationCmd = &cobra.Command{ + Use: "cancel-unbonding-delegation [address] [amount] [height] [fee] [gasLimit]", + Short: "Cancels a user's pending undelegation from a validator.", + Args: cobra.ExactArgs(5), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + addr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + amount, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("error parsing an amount:%v", err) + } + + height, err := strconv.ParseInt(args[2], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a fee:%v", err) + } + + fee, err := strconv.ParseInt(args[3], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a fee:%v", err) + } + + gasLimit, err := strconv.ParseUint(args[4], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a gas limit:%v", err) + } + + txResponse, err := client.State.CancelUnbondingDelegation( + cmd.Context(), + addr.Address.(state.ValAddress), + math.NewInt(amount), + math.NewInt(height), + math.NewInt(fee), + gasLimit, + ) + return cmdnode.PrintOutput(txResponse, err, nil) + }, +} + +var beginRedelegateCmd = &cobra.Command{ + Use: "begin-redelegate [srcAddress] [dstAddress] [amount] [fee] [gasLimit]", + Short: "Sends a user's delegated tokens to a new validator for redelegation", + Args: cobra.ExactArgs(5), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + srcAddr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + dstAddr, err := parseAddressFromString(args[1]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + amount, err := strconv.ParseInt(args[2], 10, 64) + if err != nil { + return fmt.Errorf("error parsing an amount:%v", err) + } + + fee, err := strconv.ParseInt(args[3], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a fee:%v", err) + } + gasLimit, err := strconv.ParseUint(args[4], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a gas limit:%v", err) + } + + txResponse, err := client.State.BeginRedelegate( + cmd.Context(), + srcAddr.Address.(state.ValAddress), + dstAddr.Address.(state.ValAddress), + math.NewInt(amount), + math.NewInt(fee), + gasLimit, + ) + return cmdnode.PrintOutput(txResponse, err, nil) + }, +} + +var undelegateCmd = &cobra.Command{ + Use: "undelegate [valAddress] [amount] [fee] [gasLimit]", + Short: "Undelegates a user's delegated tokens, unbonding them from the current validator.", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + addr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + amount, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("error parsing an amount:%v", err) + } + fee, err := strconv.ParseInt(args[2], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a fee:%v", err) + } + gasLimit, err := strconv.ParseUint(args[3], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a gas limit:%v", err) + } + + txResponse, err := client.State.Undelegate( + cmd.Context(), + addr.Address.(state.ValAddress), + math.NewInt(amount), + math.NewInt(fee), + gasLimit, + ) + return cmdnode.PrintOutput(txResponse, err, nil) + }, +} + +var delegateCmd = &cobra.Command{ + Use: "delegate [valAddress] [amount] [fee] [gasLimit]", + Short: "Sends a user's liquid tokens to a validator for delegation.", + Args: cobra.ExactArgs(4), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + addr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + amount, err := strconv.ParseInt(args[1], 10, 64) + if err != nil { + return fmt.Errorf("error parsing an amount:%v", err) + } + + fee, err := strconv.ParseInt(args[2], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a fee:%v", err) + } + + gasLimit, err := strconv.ParseUint(args[3], 10, 64) + if err != nil { + return fmt.Errorf("error parsing a gas limit:%v", err) + } + + txResponse, err := client.State.Delegate( + cmd.Context(), + addr.Address.(state.ValAddress), + math.NewInt(amount), + math.NewInt(fee), + gasLimit, + ) + return cmdnode.PrintOutput(txResponse, err, nil) + }, +} + +var queryDelegationCmd = &cobra.Command{ + Use: "get-delegation [valAddress]", + Short: "Retrieves the delegation information between a delegator and a validator.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + addr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + balance, err := client.State.QueryDelegation(cmd.Context(), addr.Address.(state.ValAddress)) + return cmdnode.PrintOutput(balance, err, nil) + }, +} + +var queryUnbondingCmd = &cobra.Command{ + Use: "get-unbonding [valAddress]", + Short: "Retrieves the unbonding status between a delegator and a validator.", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + addr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing an address:%v", err) + } + + response, err := client.State.QueryUnbonding(cmd.Context(), addr.Address.(state.ValAddress)) + return cmdnode.PrintOutput(response, err, nil) + }, +} + +var queryRedelegationCmd = &cobra.Command{ + Use: "get-redelegations [srcAddress] [dstAddress]", + Short: "Retrieves the status of the redelegations between a delegator and a validator.", + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + client, err := cmdnode.ParseClientFromCtx(cmd.Context()) + if err != nil { + return err + } + defer client.Close() + + srcAddr, err := parseAddressFromString(args[0]) + if err != nil { + return fmt.Errorf("error parsing a src address:%v", err) + } + + dstAddr, err := parseAddressFromString(args[1]) + if err != nil { + return fmt.Errorf("error parsing a dst address:%v", err) + } + + response, err := client.State.QueryRedelegations( + cmd.Context(), + srcAddr.Address.(state.ValAddress), + dstAddr.Address.(state.ValAddress), + ) + return cmdnode.PrintOutput(response, err, nil) + }, +} + +func parseAddressFromString(addrStr string) (state.Address, error) { + var address state.Address + err := address.UnmarshalJSON([]byte(addrStr)) + if err != nil { + return address, err + } + return address, nil +} diff --git a/nodebuilder/state/config.go b/nodebuilder/state/config.go index e6db813e06..f42e646b76 100644 --- a/nodebuilder/state/config.go +++ b/nodebuilder/state/config.go @@ -1,6 +1,8 @@ package state -import "github.com/cosmos/cosmos-sdk/crypto/keyring" +import ( + "github.com/cosmos/cosmos-sdk/crypto/keyring" +) var defaultKeyringBackend = keyring.BackendTest diff --git a/nodebuilder/state/core.go b/nodebuilder/state/core.go index a3eb7f7b6d..f8f8508540 100644 --- a/nodebuilder/state/core.go +++ b/nodebuilder/state/core.go @@ -18,11 +18,11 @@ func coreAccessor( corecfg core.Config, signer *apptypes.KeyringSigner, sync *sync.Syncer[*header.ExtendedHeader], - fraudServ libfraud.Service, -) (*state.CoreAccessor, *modfraud.ServiceBreaker[*state.CoreAccessor]) { + fraudServ libfraud.Service[*header.ExtendedHeader], +) (*state.CoreAccessor, Module, *modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]) { ca := state.NewCoreAccessor(signer, sync, corecfg.IP, corecfg.RPCPort, corecfg.GRPCPort) - return ca, &modfraud.ServiceBreaker[*state.CoreAccessor]{ + return ca, ca, &modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]{ Service: ca, FraudType: byzantine.BadEncoding, FraudServ: fraudServ, diff --git a/nodebuilder/state/mocks/api.go b/nodebuilder/state/mocks/api.go index 3658c7ebf8..1861a86e66 100644 --- a/nodebuilder/state/mocks/api.go +++ b/nodebuilder/state/mocks/api.go @@ -9,12 +9,12 @@ import ( reflect "reflect" math "cosmossdk.io/math" + blob "github.com/celestiaorg/celestia-node/blob" + state "github.com/celestiaorg/celestia-node/state" types "github.com/cosmos/cosmos-sdk/types" types0 "github.com/cosmos/cosmos-sdk/x/staking/types" gomock "github.com/golang/mock/gomock" types1 "github.com/tendermint/tendermint/types" - - namespace "github.com/celestiaorg/nmt/namespace" ) // MockModule is a mock of Module interface. @@ -41,10 +41,10 @@ func (m *MockModule) EXPECT() *MockModuleMockRecorder { } // AccountAddress mocks base method. -func (m *MockModule) AccountAddress(arg0 context.Context) (types.Address, error) { +func (m *MockModule) AccountAddress(arg0 context.Context) (state.Address, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "AccountAddress", arg0) - ret0, _ := ret[0].(types.Address) + ret0, _ := ret[0].(state.Address) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -71,7 +71,7 @@ func (mr *MockModuleMockRecorder) Balance(arg0 interface{}) *gomock.Call { } // BalanceForAddress mocks base method. -func (m *MockModule) BalanceForAddress(arg0 context.Context, arg1 types.Address) (*types.Coin, error) { +func (m *MockModule) BalanceForAddress(arg0 context.Context, arg1 state.Address) (*types.Coin, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "BalanceForAddress", arg0, arg1) ret0, _ := ret[0].(*types.Coin) @@ -130,20 +130,6 @@ func (mr *MockModuleMockRecorder) Delegate(arg0, arg1, arg2, arg3, arg4 interfac return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delegate", reflect.TypeOf((*MockModule)(nil).Delegate), arg0, arg1, arg2, arg3, arg4) } -// IsStopped mocks base method. -func (m *MockModule) IsStopped(arg0 context.Context) bool { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsStopped", arg0) - ret0, _ := ret[0].(bool) - return ret0 -} - -// IsStopped indicates an expected call of IsStopped. -func (mr *MockModuleMockRecorder) IsStopped(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsStopped", reflect.TypeOf((*MockModule)(nil).IsStopped), arg0) -} - // QueryDelegation mocks base method. func (m *MockModule) QueryDelegation(arg0 context.Context, arg1 types.ValAddress) (*types0.QueryDelegationResponse, error) { m.ctrl.T.Helper() @@ -189,19 +175,19 @@ func (mr *MockModuleMockRecorder) QueryUnbonding(arg0, arg1 interface{}) *gomock return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryUnbonding", reflect.TypeOf((*MockModule)(nil).QueryUnbonding), arg0, arg1) } -// SubmitPayForData mocks base method. -func (m *MockModule) SubmitPayForBlob(arg0 context.Context, arg1 namespace.ID, arg2 []byte, arg3 math.Int, arg4 uint64) (*types.TxResponse, error) { +// SubmitPayForBlob mocks base method. +func (m *MockModule) SubmitPayForBlob(arg0 context.Context, arg1 math.Int, arg2 uint64, arg3 []*blob.Blob) (*types.TxResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "SubmitPayForBlob", arg0, arg1, arg2, arg3, arg4) + ret := m.ctrl.Call(m, "SubmitPayForBlob", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(*types.TxResponse) ret1, _ := ret[1].(error) return ret0, ret1 } -// SubmitPayForData indicates an expected call of SubmitPayForData. -func (mr *MockModuleMockRecorder) SubmitPayForData(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +// SubmitPayForBlob indicates an expected call of SubmitPayForBlob. +func (mr *MockModuleMockRecorder) SubmitPayForBlob(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitPayForBlob", reflect.TypeOf((*MockModule)(nil).SubmitPayForBlob), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SubmitPayForBlob", reflect.TypeOf((*MockModule)(nil).SubmitPayForBlob), arg0, arg1, arg2, arg3) } // SubmitTx mocks base method. diff --git a/nodebuilder/state/module.go b/nodebuilder/state/module.go index 24305dabe1..733419a918 100644 --- a/nodebuilder/state/module.go +++ b/nodebuilder/state/module.go @@ -6,6 +6,9 @@ import ( logging "github.com/ipfs/go-log/v2" "go.uber.org/fx" + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/libs/fxutil" + "github.com/celestiaorg/celestia-node/nodebuilder/core" modfraud "github.com/celestiaorg/celestia-node/nodebuilder/fraud" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/state" @@ -15,25 +18,26 @@ var log = logging.Logger("module/state") // ConstructModule provides all components necessary to construct the // state service. -func ConstructModule(tp node.Type, cfg *Config) fx.Option { +func ConstructModule(tp node.Type, cfg *Config, coreCfg *core.Config) fx.Option { // sanitize config values before constructing module cfgErr := cfg.Validate() baseComponents := fx.Options( fx.Supply(*cfg), fx.Error(cfgErr), - fx.Provide(fx.Annotate( + fxutil.ProvideIf(coreCfg.IsEndpointConfigured(), fx.Annotate( coreAccessor, - fx.OnStart(func(ctx context.Context, breaker *modfraud.ServiceBreaker[*state.CoreAccessor]) error { + fx.OnStart(func(ctx context.Context, + breaker *modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]) error { return breaker.Start(ctx) }), - fx.OnStop(func(ctx context.Context, breaker *modfraud.ServiceBreaker[*state.CoreAccessor]) error { + fx.OnStop(func(ctx context.Context, + breaker *modfraud.ServiceBreaker[*state.CoreAccessor, *header.ExtendedHeader]) error { return breaker.Stop(ctx) }), )), - // the module is needed for the handler - fx.Provide(func(ca *state.CoreAccessor) Module { - return ca + fxutil.ProvideIf(!coreCfg.IsEndpointConfigured(), func() (*state.CoreAccessor, Module) { + return nil, &stubbedStateModule{} }), ) diff --git a/nodebuilder/state/state.go b/nodebuilder/state/state.go index 07f61e5952..52a2317445 100644 --- a/nodebuilder/state/state.go +++ b/nodebuilder/state/state.go @@ -5,8 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/x/staking/types" - "github.com/celestiaorg/nmt/namespace" - + "github.com/celestiaorg/celestia-node/blob" "github.com/celestiaorg/celestia-node/state" ) @@ -17,9 +16,8 @@ var _ Module = (*API)(nil) // messages to the Celestia network. // //go:generate mockgen -destination=mocks/api.go -package=mocks . Module +//nolint:dupl type Module interface { - // IsStopped checks if the Module's context has been stopped - IsStopped(ctx context.Context) bool // AccountAddress retrieves the address of the node's account/signer AccountAddress(ctx context.Context) (state.Address, error) @@ -36,7 +34,9 @@ type Module interface { // Transfer sends the given amount of coins from default wallet of the node to the given account // address. - Transfer(ctx context.Context, to state.AccAddress, amount, fee state.Int, gasLimit uint64) (*state.TxResponse, error) + Transfer( + ctx context.Context, to state.AccAddress, amount, fee state.Int, gasLimit uint64, + ) (*state.TxResponse, error) // SubmitTx submits the given transaction/message to the // Celestia network and blocks until the tx is included in // a block. @@ -44,10 +44,9 @@ type Module interface { // SubmitPayForBlob builds, signs and submits a PayForBlob transaction. SubmitPayForBlob( ctx context.Context, - nID namespace.ID, - data []byte, fee state.Int, gasLim uint64, + blobs []*blob.Blob, ) (*state.TxResponse, error) // CancelUnbondingDelegation cancels a user's pending undelegation from a validator. @@ -97,12 +96,13 @@ type Module interface { // API is a wrapper around Module for the RPC. // TODO(@distractedm1nd): These structs need to be autogenerated. +// +//nolint:dupl type API struct { Internal struct { AccountAddress func(ctx context.Context) (state.Address, error) `perm:"read"` - IsStopped func(ctx context.Context) bool `perm:"public"` Balance func(ctx context.Context) (*state.Balance, error) `perm:"read"` - BalanceForAddress func(ctx context.Context, addr state.Address) (*state.Balance, error) `perm:"public"` + BalanceForAddress func(ctx context.Context, addr state.Address) (*state.Balance, error) `perm:"read"` Transfer func( ctx context.Context, to state.AccAddress, @@ -110,13 +110,12 @@ type API struct { fee state.Int, gasLimit uint64, ) (*state.TxResponse, error) `perm:"write"` - SubmitTx func(ctx context.Context, tx state.Tx) (*state.TxResponse, error) `perm:"write"` + SubmitTx func(ctx context.Context, tx state.Tx) (*state.TxResponse, error) `perm:"read"` SubmitPayForBlob func( ctx context.Context, - nID namespace.ID, - data []byte, fee state.Int, gasLim uint64, + blobs []*blob.Blob, ) (*state.TxResponse, error) `perm:"write"` CancelUnbondingDelegation func( ctx context.Context, @@ -151,16 +150,16 @@ type API struct { QueryDelegation func( ctx context.Context, valAddr state.ValAddress, - ) (*types.QueryDelegationResponse, error) `perm:"public"` + ) (*types.QueryDelegationResponse, error) `perm:"read"` QueryUnbonding func( ctx context.Context, valAddr state.ValAddress, - ) (*types.QueryUnbondingDelegationResponse, error) `perm:"public"` + ) (*types.QueryUnbondingDelegationResponse, error) `perm:"read"` QueryRedelegations func( ctx context.Context, srcValAddr, dstValAddr state.ValAddress, - ) (*types.QueryRedelegationsResponse, error) `perm:"public"` + ) (*types.QueryRedelegationsResponse, error) `perm:"read"` } } @@ -168,10 +167,6 @@ func (api *API) AccountAddress(ctx context.Context) (state.Address, error) { return api.Internal.AccountAddress(ctx) } -func (api *API) IsStopped(ctx context.Context) bool { - return api.Internal.IsStopped(ctx) -} - func (api *API) BalanceForAddress(ctx context.Context, addr state.Address) (*state.Balance, error) { return api.Internal.BalanceForAddress(ctx, addr) } @@ -192,12 +187,11 @@ func (api *API) SubmitTx(ctx context.Context, tx state.Tx) (*state.TxResponse, e func (api *API) SubmitPayForBlob( ctx context.Context, - nID namespace.ID, - data []byte, fee state.Int, gasLim uint64, + blobs []*blob.Blob, ) (*state.TxResponse, error) { - return api.Internal.SubmitPayForBlob(ctx, nID, data, fee, gasLim) + return api.Internal.SubmitPayForBlob(ctx, fee, gasLim, blobs) } func (api *API) CancelUnbondingDelegation( diff --git a/nodebuilder/state/stub.go b/nodebuilder/state/stub.go new file mode 100644 index 0000000000..30a431aba5 --- /dev/null +++ b/nodebuilder/state/stub.go @@ -0,0 +1,112 @@ +package state + +import ( + "context" + "errors" + + "github.com/cosmos/cosmos-sdk/x/staking/types" + + "github.com/celestiaorg/celestia-node/blob" + "github.com/celestiaorg/celestia-node/state" +) + +var ErrNoStateAccess = errors.New("node is running without state access. run with --core.ip to resolve") + +// stubbedStateModule provides a stub for the state module to return +// errors when state endpoints are accessed without a running connection +// to a core endpoint. +type stubbedStateModule struct{} + +func (s stubbedStateModule) AccountAddress(context.Context) (state.Address, error) { + return state.Address{}, ErrNoStateAccess +} + +func (s stubbedStateModule) Balance(context.Context) (*state.Balance, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) BalanceForAddress( + context.Context, + state.Address, +) (*state.Balance, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) Transfer( + _ context.Context, + _ state.AccAddress, + _, _ state.Int, + _ uint64, +) (*state.TxResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) SubmitTx(context.Context, state.Tx) (*state.TxResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) SubmitPayForBlob( + context.Context, + state.Int, + uint64, + []*blob.Blob, +) (*state.TxResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) CancelUnbondingDelegation( + _ context.Context, + _ state.ValAddress, + _, _, _ state.Int, + _ uint64, +) (*state.TxResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) BeginRedelegate( + _ context.Context, + _, _ state.ValAddress, + _, _ state.Int, + _ uint64, +) (*state.TxResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) Undelegate( + _ context.Context, + _ state.ValAddress, + _, _ state.Int, + _ uint64, +) (*state.TxResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) Delegate( + _ context.Context, + _ state.ValAddress, + _, _ state.Int, + _ uint64, +) (*state.TxResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) QueryDelegation( + context.Context, + state.ValAddress, +) (*types.QueryDelegationResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) QueryUnbonding( + context.Context, + state.ValAddress, +) (*types.QueryUnbondingDelegationResponse, error) { + return nil, ErrNoStateAccess +} + +func (s stubbedStateModule) QueryRedelegations( + _ context.Context, + _, _ state.ValAddress, +) (*types.QueryRedelegationsResponse, error) { + return nil, ErrNoStateAccess +} diff --git a/nodebuilder/store.go b/nodebuilder/store.go index 046dd41bbd..7f67a9e782 100644 --- a/nodebuilder/store.go +++ b/nodebuilder/store.go @@ -4,16 +4,19 @@ import ( "errors" "fmt" "path/filepath" + "runtime" "sync" + "time" "github.com/cosmos/cosmos-sdk/crypto/keyring" - "github.com/dgraph-io/badger/v2/options" + "github.com/dgraph-io/badger/v4/options" "github.com/ipfs/go-datastore" - dsbadger "github.com/ipfs/go-ds-badger2" + dsbadger "github.com/ipfs/go-ds-badger4" "github.com/mitchellh/go-homedir" "github.com/celestiaorg/celestia-node/libs/fslock" "github.com/celestiaorg/celestia-node/libs/keystore" + "github.com/celestiaorg/celestia-node/share" ) var ( @@ -65,7 +68,7 @@ func OpenStore(path string, ring keyring.Keyring) (Store, error) { ok := IsInit(path) if !ok { - flock.Unlock() //nolint: errcheck + flock.Unlock() //nolint:errcheck return nil, ErrNotInited } @@ -104,69 +107,45 @@ func (f *fsStore) PutConfig(cfg *Config) error { } func (f *fsStore) Keystore() (_ keystore.Keystore, err error) { - f.lock.RLock() - defer f.lock.RUnlock() if f.keys == nil { return nil, fmt.Errorf("node: no Keystore found") } return f.keys, nil } -func (f *fsStore) Datastore() (_ datastore.Batching, err error) { - f.lock.RLock() +func (f *fsStore) Datastore() (datastore.Batching, error) { + f.dataMu.Lock() + defer f.dataMu.Unlock() if f.data != nil { - f.lock.RUnlock() return f.data, nil } - f.lock.RUnlock() - - f.lock.Lock() - defer f.lock.Unlock() - - opts := dsbadger.DefaultOptions // this should be copied - - // Badger sets ValueThreshold to 1K by default and this makes shares being stored in LSM tree - // instead of the value log, so we change the value to be lower than share size, - // so shares are store in value log. For value log and LSM definitions - opts.ValueThreshold = 128 - // We always write unique values to Badger transaction so there is no need to detect conflicts. - opts.DetectConflicts = false - // Use MemoryMap for better performance - opts.ValueLogLoadingMode = options.MemoryMap - opts.TableLoadingMode = options.MemoryMap - // Truncate set to true will truncate corrupted data on start if there is any. - // If we don't truncate, the node will refuse to start and will beg for recovering, etc. - // If we truncate, the node will start with any uncorrupted data and reliably sync again what was - // corrupted in most cases. - opts.Truncate = true - // MaxTableSize defines in memory and on disk size of LSM tree - // Bigger values constantly takes more RAM - // TODO(@Wondertan): Make configurable with more conservative defaults for Light Node - opts.MaxTableSize = 64 << 20 - - f.data, err = dsbadger.NewDatastore(dataPath(f.path), &opts) + + cfg := constraintBadgerConfig() + ds, err := dsbadger.NewDatastore(dataPath(f.path), cfg) if err != nil { return nil, fmt.Errorf("node: can't open Badger Datastore: %w", err) } - return f.data, nil + f.data = ds + return ds, nil } func (f *fsStore) Close() (err error) { err = errors.Join(err, f.dirLock.Unlock()) + f.dataMu.Lock() if f.data != nil { err = errors.Join(err, f.data.Close()) } + f.dataMu.Unlock() return } type fsStore struct { path string - data datastore.Batching - keys keystore.Keystore - - lock sync.RWMutex // protects all the fields + dataMu sync.Mutex + data datastore.Batching + keys keystore.Keystore dirLock *fslock.Locker // protects directory } @@ -203,3 +182,67 @@ func indexPath(base string) string { func dataPath(base string) string { return filepath.Join(base, "data") } + +// constraintBadgerConfig returns BadgerDB configuration optimized for low memory usage and more frequent +// compaction which prevents memory spikes. +// This is particularly important for LNs with restricted memory resources. +// +// With the following configuration, a LN uses up to 300iB of RAM during initial sync/sampling +// and up to 200MiB during normal operation. (on 4 core CPU, 8GiB RAM droplet) +// +// With the following configuration and "-tags=jemalloc", a LN uses no more than 180MiB during initial +// sync/sampling and up to 100MiB during normal operation. (same hardware spec) +// NOTE: To enable jemalloc, build celestia-node with "-tags=jemalloc" flag, which configures Badger to +// use jemalloc instead of Go's default allocator. +// +// TODO(@Wondertan): Consider alternative less constraint configuration for FN/BN +// TODO(@Wondertan): Consider dynamic memory allocation based on available RAM +func constraintBadgerConfig() *dsbadger.Options { + opts := dsbadger.DefaultOptions // this must be copied + // ValueLog: + // 2mib default => share.Size - makes sure headers and samples are stored in value log + // This *tremendously* reduces the amount of memory used by the node, up to 10 times less during + // compaction + opts.ValueThreshold = share.Size + // make sure we don't have any limits for stored headers + opts.ValueLogMaxEntries = 100000000 + // run value log GC more often to spread the work over time + opts.GcInterval = time.Minute * 1 + // default 0.5 => 0.125 - makes sure value log GC is more aggressive on reclaiming disk space + opts.GcDiscardRatio = 0.125 + + // badger stores checksum for every value, but doesn't verify it by default + // enabling this option may allow us to see detect corrupted data + opts.ChecksumVerificationMode = options.OnBlockRead + opts.VerifyValueChecksum = true + // default 64mib => 0 - disable block cache + // most of our component maintain their own caches, so this is not needed + opts.BlockCacheSize = 0 + // not much gain as it compresses the LSM only as well compression requires block cache + opts.Compression = options.None + + // MemTables: + // default 64mib => 16mib - decreases memory usage and makes compaction more often + opts.MemTableSize = 16 << 20 + // default 5 => 3 + opts.NumMemtables = 3 + // default 5 => 3 + opts.NumLevelZeroTables = 3 + // default 15 => 5 - this prevents memory growth on CPU constraint systems by blocking all writers + opts.NumLevelZeroTablesStall = 5 + + // Compaction: + // Dynamic compactor allocation + compactors := runtime.NumCPU() / 2 + if compactors < 2 { + compactors = 2 // can't be less than 2 + } + if compactors > opts.MaxLevels { // ensure there is no more compactors than db table levels + compactors = opts.MaxLevels + } + opts.NumCompactors = compactors + // makes sure badger is always compacted on shutdown + opts.CompactL0OnClose = true + + return &opts +} diff --git a/nodebuilder/store_test.go b/nodebuilder/store_test.go index 512d45bb70..51bd89c5a7 100644 --- a/nodebuilder/store_test.go +++ b/nodebuilder/store_test.go @@ -1,13 +1,27 @@ +//go:build !race + package nodebuilder import ( + "context" "strconv" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/celestia-app/pkg/wrapper" + "github.com/celestiaorg/nmt" + "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share/sharetest" ) func TestRepo(t *testing.T) { @@ -50,3 +64,123 @@ func TestRepo(t *testing.T) { }) } } + +func BenchmarkStore(b *testing.B) { + ctx, cancel := context.WithCancel(context.Background()) + b.Cleanup(cancel) + + // BenchmarkStore/bench_read_128-10 14 78970661 ns/op (~70ms) + b.Run("bench put 128", func(b *testing.B) { + dir := b.TempDir() + err := Init(*DefaultConfig(node.Full), dir, node.Full) + require.NoError(b, err) + + store := newStore(ctx, b, eds.DefaultParameters(), dir) + size := 128 + b.Run("enabled eds proof caching", func(b *testing.B) { + b.StopTimer() + b.ResetTimer() + for i := 0; i < b.N; i++ { + adder := ipld.NewProofsAdder(size * 2) + shares := sharetest.RandShares(b, size*size) + eds, err := rsmt2d.ComputeExtendedDataSquare( + shares, + share.DefaultRSMT2DCodec(), + wrapper.NewConstructor(uint64(size), + nmt.NodeVisitor(adder.VisitFn())), + ) + require.NoError(b, err) + dah, err := da.NewDataAvailabilityHeader(eds) + require.NoError(b, err) + ctx := ipld.CtxWithProofsAdder(ctx, adder) + + b.StartTimer() + err = store.edsStore.Put(ctx, dah.Hash(), eds) + b.StopTimer() + require.NoError(b, err) + } + }) + + b.Run("disabled eds proof caching", func(b *testing.B) { + b.ResetTimer() + b.StopTimer() + for i := 0; i < b.N; i++ { + eds := edstest.RandEDS(b, size) + dah, err := da.NewDataAvailabilityHeader(eds) + require.NoError(b, err) + + b.StartTimer() + err = store.edsStore.Put(ctx, dah.Hash(), eds) + b.StopTimer() + require.NoError(b, err) + } + }) + }) +} + +func TestStoreRestart(t *testing.T) { + const ( + blocks = 5 + size = 32 + ) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + dir := t.TempDir() + err := Init(*DefaultConfig(node.Full), dir, node.Full) + require.NoError(t, err) + + store := newStore(ctx, t, eds.DefaultParameters(), dir) + + hashes := make([][]byte, blocks) + for i := range hashes { + edss := edstest.RandEDS(t, size) + require.NoError(t, err) + dah, err := da.NewDataAvailabilityHeader(edss) + require.NoError(t, err) + err = store.edsStore.Put(ctx, dah.Hash(), edss) + require.NoError(t, err) + + // store hashes for read loop later + hashes[i] = dah.Hash() + } + + // restart store + store.stop(ctx, t) + store = newStore(ctx, t, eds.DefaultParameters(), dir) + + for _, h := range hashes { + edsReader, err := store.edsStore.GetCAR(ctx, h) + require.NoError(t, err) + odsReader, err := eds.ODSReader(edsReader) + require.NoError(t, err) + _, err = eds.ReadEDS(ctx, odsReader, h) + require.NoError(t, err) + require.NoError(t, edsReader.Close()) + } +} + +type store struct { + s Store + edsStore *eds.Store +} + +func newStore(ctx context.Context, t require.TestingT, params *eds.Parameters, dir string) store { + s, err := OpenStore(dir, nil) + require.NoError(t, err) + ds, err := s.Datastore() + require.NoError(t, err) + edsStore, err := eds.NewStore(params, dir, ds) + require.NoError(t, err) + err = edsStore.Start(ctx) + require.NoError(t, err) + return store{ + s: s, + edsStore: edsStore, + } +} + +func (s *store) stop(ctx context.Context, t *testing.T) { + require.NoError(t, s.edsStore.Stop(ctx)) + require.NoError(t, s.s.Close()) +} diff --git a/nodebuilder/testing.go b/nodebuilder/testing.go index 6cb40a2b6c..8d49772aef 100644 --- a/nodebuilder/testing.go +++ b/nodebuilder/testing.go @@ -9,11 +9,12 @@ import ( "go.uber.org/fx" apptypes "github.com/celestiaorg/celestia-app/x/blob/types" + libhead "github.com/celestiaorg/go-header" "github.com/celestiaorg/celestia-node/core" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/libs/fxutil" - "github.com/celestiaorg/celestia-node/nodebuilder/header" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/p2p" "github.com/celestiaorg/celestia-node/nodebuilder/state" @@ -47,7 +48,7 @@ func TestNodeWithConfig(t *testing.T, tp node.Type, cfg *Config, opts ...fx.Opti // temp dir for the eds store FIXME: Should be in mem fx.Replace(node.StorePath(t.TempDir())), // avoid requesting trustedPeer during initialization - fxutil.ReplaceAs(headertest.NewStore(t), new(header.InitStore)), + fxutil.ReplaceAs(headertest.NewStore(t), new(libhead.Store[*header.ExtendedHeader])), ) // in fact, we don't need core.Client in tests, but Bridge requires is a valid one diff --git a/nodebuilder/tests/README.md b/nodebuilder/tests/README.md index 176ee2ba21..dd2040ab42 100644 --- a/nodebuilder/tests/README.md +++ b/nodebuilder/tests/README.md @@ -1,6 +1,6 @@ # Swamp: In-Memory Test Tool -Swamp is a testing tool that creates an environment for deploying `celestia-node` and testing instances against each other. +Swamp is our integration testing tool that creates an environment for deploying `celestia-node` and testing instances against each other. While the swamp takes care of setting up networking and initial configuration of node types, the user can focus on tailoring test scenarios. ## Usage @@ -38,7 +38,7 @@ require.NoError(t, err) light := sw.NewLightClient(node.WithTrustedPeer(addrs[0].String())) ``` -## Concenptual overview +## Conceptual overview Each of the test scenario requires flexibility in network topology. The user can define the necessary amount of each type of node and be able to control each of them. diff --git a/nodebuilder/tests/api_test.go b/nodebuilder/tests/api_test.go index 27a10a5592..a3b99a750b 100644 --- a/nodebuilder/tests/api_test.go +++ b/nodebuilder/tests/api_test.go @@ -1,18 +1,127 @@ +//go:build api || integration + package tests import ( "context" "testing" + "time" + "github.com/filecoin-project/go-jsonrpc/auth" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/require" + "github.com/celestiaorg/celestia-node/api/rpc/client" + "github.com/celestiaorg/celestia-node/blob" + "github.com/celestiaorg/celestia-node/blob/blobtest" "github.com/celestiaorg/celestia-node/nodebuilder" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" ) +const ( + btime = time.Millisecond * 300 +) + +func TestNodeModule(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) + t.Cleanup(cancel) + + sw := swamp.NewSwamp(t, swamp.WithBlockTime(time.Second)) + // start a bridge node + bridge := sw.NewBridgeNode() + err := bridge.Start(ctx) + require.NoError(t, err) + + bridgeAddr := "http://" + bridge.RPCServer.ListenAddr() + + writePerms := []auth.Permission{"public", "read", "write"} + adminPerms := []auth.Permission{"public", "read", "write", "admin"} + jwt, err := bridge.AdminServ.AuthNew(ctx, adminPerms) + require.NoError(t, err) + + client, err := client.NewClient(ctx, bridgeAddr, jwt) + require.NoError(t, err) + + info, err := client.Node.Info(ctx) + require.NoError(t, err) + require.Equal(t, info.APIVersion, node.APIVersion) + + ready, err := client.Node.Ready(ctx) + require.NoError(t, err) + require.True(t, ready) + + perms, err := client.Node.AuthVerify(ctx, jwt) + require.NoError(t, err) + require.Equal(t, perms, adminPerms) + + writeJWT, err := client.Node.AuthNew(ctx, writePerms) + require.NoError(t, err) + + perms, err = client.Node.AuthVerify(ctx, writeJWT) + require.NoError(t, err) + require.Equal(t, perms, writePerms) + +} + +func TestGetByHeight(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) + t.Cleanup(cancel) + + sw := swamp.NewSwamp(t, swamp.WithBlockTime(time.Second)) + + // start a bridge node + bridge := sw.NewBridgeNode() + err := bridge.Start(ctx) + require.NoError(t, err) + + rpcClient := getAdminClient(ctx, bridge, t) + + // let a few blocks be produced + _, err = rpcClient.Header.WaitForHeight(ctx, 3) + require.NoError(t, err) + + networkHead, err := rpcClient.Header.NetworkHead(ctx) + require.NoError(t, err) + _, err = rpcClient.Header.GetByHeight(ctx, networkHead.Height()+1) + require.Nil(t, err, "Requesting syncer.Head()+1 shouldn't return an error") + + networkHead, err = rpcClient.Header.NetworkHead(ctx) + require.NoError(t, err) + _, err = rpcClient.Header.GetByHeight(ctx, networkHead.Height()+2) + require.ErrorContains(t, err, "given height is from the future") +} + +// TestBlobRPC ensures that blobs can be submitted via rpc +func TestBlobRPC(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) + t.Cleanup(cancel) + + sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) + + // start a bridge node + bridge := sw.NewBridgeNode() + err := bridge.Start(ctx) + require.NoError(t, err) + + rpcClient := getAdminClient(ctx, bridge, t) + + appBlobs, err := blobtest.GenerateV0Blobs([]int{8}, false) + require.NoError(t, err) + + newBlob, err := blob.NewBlob( + appBlobs[0].ShareVersion, + append([]byte{appBlobs[0].NamespaceVersion}, appBlobs[0].NamespaceID...), + appBlobs[0].Data, + ) + require.NoError(t, err) + + height, err := rpcClient.Blob.Submit(ctx, []*blob.Blob{newBlob}, blob.DefaultGasPrice()) + require.NoError(t, err) + require.True(t, height != 0) +} + // TestHeaderSubscription ensures that the header subscription over RPC works // as intended and gets canceled successfully after rpc context cancellation. func TestHeaderSubscription(t *testing.T) { @@ -36,9 +145,11 @@ func TestHeaderSubscription(t *testing.T) { err = light.Start(ctx) require.NoError(t, err) + lightClient := getAdminClient(ctx, light, t) + // subscribe to headers via the light node's RPC header subscription subctx, subcancel := context.WithCancel(ctx) - sub, err := light.HeaderServ.Subscribe(subctx) + sub, err := lightClient.Header.Subscribe(subctx) require.NoError(t, err) // listen for 5 headers for i := 0; i < 5; i++ { diff --git a/nodebuilder/tests/blob_test.go b/nodebuilder/tests/blob_test.go new file mode 100644 index 0000000000..d0aeefd568 --- /dev/null +++ b/nodebuilder/tests/blob_test.go @@ -0,0 +1,207 @@ +//go:build blob || integration + +package tests + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-node/blob" + "github.com/celestiaorg/celestia-node/blob/blobtest" + "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" + "github.com/celestiaorg/celestia-node/share" +) + +func TestBlobModule(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) + t.Cleanup(cancel) + sw := swamp.NewSwamp(t, swamp.WithBlockTime(time.Second*1)) + + appBlobs0, err := blobtest.GenerateV0Blobs([]int{8, 4}, true) + require.NoError(t, err) + appBlobs1, err := blobtest.GenerateV0Blobs([]int{4}, false) + require.NoError(t, err) + blobs := make([]*blob.Blob, 0, len(appBlobs0)+len(appBlobs1)) + + for _, b := range append(appBlobs0, appBlobs1...) { + blob, err := blob.NewBlob(b.ShareVersion, append([]byte{b.NamespaceVersion}, b.NamespaceID...), b.Data) + require.NoError(t, err) + blobs = append(blobs, blob) + } + + require.NoError(t, err) + bridge := sw.NewBridgeNode() + require.NoError(t, bridge.Start(ctx)) + + addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) + require.NoError(t, err) + + fullCfg := sw.DefaultTestConfig(node.Full) + fullCfg.Header.TrustedPeers = append(fullCfg.Header.TrustedPeers, addrs[0].String()) + fullNode := sw.NewNodeWithConfig(node.Full, fullCfg) + require.NoError(t, fullNode.Start(ctx)) + + addrsFull, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(fullNode.Host)) + require.NoError(t, err) + + lightCfg := sw.DefaultTestConfig(node.Light) + lightCfg.Header.TrustedPeers = append(lightCfg.Header.TrustedPeers, addrsFull[0].String()) + lightNode := sw.NewNodeWithConfig(node.Light, lightCfg) + require.NoError(t, lightNode.Start(ctx)) + + fullClient := getAdminClient(ctx, fullNode, t) + lightClient := getAdminClient(ctx, lightNode, t) + + height, err := fullClient.Blob.Submit(ctx, blobs, blob.DefaultGasPrice()) + require.NoError(t, err) + + _, err = fullClient.Header.WaitForHeight(ctx, height) + require.NoError(t, err) + _, err = lightClient.Header.WaitForHeight(ctx, height) + require.NoError(t, err) + + var test = []struct { + name string + doFn func(t *testing.T) + }{ + { + name: "Get", + doFn: func(t *testing.T) { + // https://github.com/celestiaorg/celestia-node/issues/2915 + time.Sleep(time.Second) + blob1, err := fullClient.Blob.Get(ctx, height, blobs[0].Namespace(), blobs[0].Commitment) + require.NoError(t, err) + require.Equal(t, blobs[0], blob1) + }, + }, + { + name: "GetAll", + doFn: func(t *testing.T) { + // https://github.com/celestiaorg/celestia-node/issues/2915 + time.Sleep(time.Second) + newBlobs, err := fullClient.Blob.GetAll(ctx, height, []share.Namespace{blobs[0].Namespace()}) + require.NoError(t, err) + require.Len(t, newBlobs, len(appBlobs0)) + require.True(t, bytes.Equal(blobs[0].Commitment, newBlobs[0].Commitment)) + require.True(t, bytes.Equal(blobs[1].Commitment, newBlobs[1].Commitment)) + }, + }, + { + name: "Included", + doFn: func(t *testing.T) { + // https://github.com/celestiaorg/celestia-node/issues/2915 + time.Sleep(time.Second) + proof, err := fullClient.Blob.GetProof(ctx, height, blobs[0].Namespace(), blobs[0].Commitment) + require.NoError(t, err) + + included, err := lightClient.Blob.Included( + ctx, + height, + blobs[0].Namespace(), + proof, + blobs[0].Commitment, + ) + require.NoError(t, err) + require.True(t, included) + }, + }, + { + name: "Not Found", + doFn: func(t *testing.T) { + appBlob, err := blobtest.GenerateV0Blobs([]int{4}, false) + require.NoError(t, err) + newBlob, err := blob.NewBlob( + appBlob[0].ShareVersion, + append([]byte{appBlob[0].NamespaceVersion}, appBlob[0].NamespaceID...), + appBlob[0].Data, + ) + require.NoError(t, err) + + b, err := fullClient.Blob.Get(ctx, height, newBlob.Namespace(), newBlob.Commitment) + assert.Nil(t, b) + require.Error(t, err) + require.ErrorContains(t, err, blob.ErrBlobNotFound.Error()) + }, + }, + { + name: "Submit equal blobs", + doFn: func(t *testing.T) { + appBlob, err := blobtest.GenerateV0Blobs([]int{8, 4}, true) + require.NoError(t, err) + b, err := blob.NewBlob( + appBlob[0].ShareVersion, + append([]byte{appBlob[0].NamespaceVersion}, appBlob[0].NamespaceID...), + appBlob[0].Data, + ) + require.NoError(t, err) + + height, err := fullClient.Blob.Submit(ctx, []*blob.Blob{b, b}, blob.DefaultGasPrice()) + require.NoError(t, err) + + _, err = fullClient.Header.WaitForHeight(ctx, height) + require.NoError(t, err) + + b0, err := fullClient.Blob.Get(ctx, height, b.Namespace(), b.Commitment) + require.NoError(t, err) + require.Equal(t, b, b0) + + // give some time to store the data, + // otherwise the test will hang on the IPLD level. + // https://github.com/celestiaorg/celestia-node/issues/2915 + time.Sleep(time.Second) + + proof, err := fullClient.Blob.GetProof(ctx, height, b.Namespace(), b.Commitment) + require.NoError(t, err) + + included, err := fullClient.Blob.Included(ctx, height, b.Namespace(), proof, b.Commitment) + require.NoError(t, err) + require.True(t, included) + }, + }, + { + // This test allows to check that the blob won't be + // deduplicated if it will be sent multiple times in + // different pfbs. + name: "Submit the same blob in different pfb", + doFn: func(t *testing.T) { + h, err := fullClient.Blob.Submit(ctx, []*blob.Blob{blobs[0]}, blob.DefaultGasPrice()) + require.NoError(t, err) + + _, err = fullClient.Header.WaitForHeight(ctx, h) + require.NoError(t, err) + + b0, err := fullClient.Blob.Get(ctx, h, blobs[0].Namespace(), blobs[0].Commitment) + require.NoError(t, err) + require.Equal(t, blobs[0], b0) + + // give some time to store the data, + // otherwise the test will hang on the IPLD level. + // https://github.com/celestiaorg/celestia-node/issues/2915 + time.Sleep(time.Second) + + proof, err := fullClient.Blob.GetProof(ctx, h, blobs[0].Namespace(), blobs[0].Commitment) + require.NoError(t, err) + + included, err := fullClient.Blob.Included(ctx, h, blobs[0].Namespace(), proof, blobs[0].Commitment) + require.NoError(t, err) + require.True(t, included) + + }, + }, + } + + for _, tt := range test { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tt.doFn(t) + }) + } +} diff --git a/nodebuilder/tests/da_test.go b/nodebuilder/tests/da_test.go new file mode 100644 index 0000000000..2925898e1e --- /dev/null +++ b/nodebuilder/tests/da_test.go @@ -0,0 +1,144 @@ +//go:build blob || integration + +package tests + +import ( + "bytes" + "context" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-app/pkg/appconsts" + + "github.com/celestiaorg/celestia-node/blob" + "github.com/celestiaorg/celestia-node/blob/blobtest" + "github.com/celestiaorg/celestia-node/nodebuilder/da" + "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" + "github.com/celestiaorg/celestia-node/share" +) + +func TestDaModule(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 25*time.Second) + t.Cleanup(cancel) + sw := swamp.NewSwamp(t, swamp.WithBlockTime(time.Second)) + + namespace, err := share.NewBlobNamespaceV0([]byte("namespace")) + require.NoError(t, err) + + appBlobs0, err := blobtest.GenerateV0Blobs([]int{8, 4}, true) + require.NoError(t, err) + appBlobs1, err := blobtest.GenerateV0Blobs([]int{4}, false) + require.NoError(t, err) + blobs := make([]*blob.Blob, 0, len(appBlobs0)+len(appBlobs1)) + daBlobs := make([][]byte, 0, len(appBlobs0)+len(appBlobs1)) + + for _, b := range append(appBlobs0, appBlobs1...) { + blob, err := blob.NewBlob(b.ShareVersion, namespace, b.Data) + require.NoError(t, err) + blobs = append(blobs, blob) + daBlobs = append(daBlobs, blob.Data) + } + + require.NoError(t, err) + bridge := sw.NewBridgeNode() + require.NoError(t, bridge.Start(ctx)) + + addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) + require.NoError(t, err) + + fullCfg := sw.DefaultTestConfig(node.Full) + fullCfg.Header.TrustedPeers = append(fullCfg.Header.TrustedPeers, addrs[0].String()) + fullNode := sw.NewNodeWithConfig(node.Full, fullCfg) + require.NoError(t, fullNode.Start(ctx)) + + addrsFull, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(fullNode.Host)) + require.NoError(t, err) + + lightCfg := sw.DefaultTestConfig(node.Light) + lightCfg.Header.TrustedPeers = append(lightCfg.Header.TrustedPeers, addrsFull[0].String()) + lightNode := sw.NewNodeWithConfig(node.Light, lightCfg) + require.NoError(t, lightNode.Start(ctx)) + + fullClient := getAdminClient(ctx, fullNode, t) + lightClient := getAdminClient(ctx, lightNode, t) + + ids, err := fullClient.DA.Submit(ctx, daBlobs, -1, namespace) + require.NoError(t, err) + + var test = []struct { + name string + doFn func(t *testing.T) + }{ + { + name: "MaxBlobSize", + doFn: func(t *testing.T) { + mbs, err := fullClient.DA.MaxBlobSize(ctx) + require.NoError(t, err) + require.Equal(t, mbs, uint64(appconsts.DefaultMaxBytes)) + }, + }, + { + name: "GetProofs + Validate", + doFn: func(t *testing.T) { + t.Skip() + h, _ := da.SplitID(ids[0]) + lightClient.Header.WaitForHeight(ctx, h) + proofs, err := lightClient.DA.GetProofs(ctx, ids, namespace) + require.NoError(t, err) + require.NotEmpty(t, proofs) + valid, err := fullClient.DA.Validate(ctx, ids, proofs, namespace) + require.NoError(t, err) + for _, v := range valid { + require.True(t, v) + } + }, + }, + { + name: "GetIDs", + doFn: func(t *testing.T) { + height, _ := da.SplitID(ids[0]) + ids2, err := fullClient.DA.GetIDs(ctx, height, namespace) + require.NoError(t, err) + require.EqualValues(t, ids, ids2) + }, + }, + { + name: "Get", + doFn: func(t *testing.T) { + h, _ := da.SplitID(ids[0]) + lightClient.Header.WaitForHeight(ctx, h) + fetched, err := lightClient.DA.Get(ctx, ids, namespace) + require.NoError(t, err) + require.Len(t, fetched, len(ids)) + for i := range fetched { + require.True(t, bytes.Equal(fetched[i], daBlobs[i])) + } + }, + }, + { + name: "Commit", + doFn: func(t *testing.T) { + t.Skip() + fetched, err := fullClient.DA.Commit(ctx, ids, namespace) + require.NoError(t, err) + require.Len(t, fetched, len(ids)) + for i := range fetched { + _, commitment := da.SplitID(ids[i]) + require.EqualValues(t, fetched[i], commitment) + } + }, + }, + } + + for _, tt := range test { + tt := tt + t.Run(tt.name, func(t *testing.T) { + tt.doFn(t) + }) + } +} diff --git a/nodebuilder/tests/fraud_test.go b/nodebuilder/tests/fraud_test.go index 2022107cb7..6496cdbb53 100644 --- a/nodebuilder/tests/fraud_test.go +++ b/nodebuilder/tests/fraud_test.go @@ -1,3 +1,5 @@ +//go:build fraud || integration + package tests import ( @@ -5,16 +7,23 @@ import ( "testing" "time" - mdutils "github.com/ipfs/go-merkledag/test" + "github.com/ipfs/go-datastore" + ds_sync "github.com/ipfs/go-datastore/sync" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/types" + "go.uber.org/fx" + + "github.com/celestiaorg/go-fraud" - "github.com/celestiaorg/celestia-node/header/headertest" + "github.com/celestiaorg/celestia-node/header" + headerfraud "github.com/celestiaorg/celestia-node/header/headertest/fraud" "github.com/celestiaorg/celestia-node/nodebuilder" "github.com/celestiaorg/celestia-node/nodebuilder/core" "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" + "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/eds/byzantine" ) @@ -29,152 +38,140 @@ Steps: 4. Start a FN. 5. Subscribe to a fraud proof and wait when it will be received. 6. Check FN is not synced to 15. -Note: 15 is not available because DASer will be stopped before reaching this height due to receiving a fraud proof. +Note: 15 is not available because DASer/Syncer will be stopped +before reaching this height due to receiving a fraud proof. Another note: this test disables share exchange to speed up test results. +7. Spawn a Light Node(LN) in order to sync a BEFP. +8. Ensure that the BEFP was received. +9. Try to start a Full Node(FN) that contains a BEFP in its store. */ -func TestFraudProofBroadcasting(t *testing.T) { - t.Skip("requires BEFP generation on app side to work") +func TestFraudProofHandling(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) + t.Cleanup(cancel) const ( - blocks = 15 - bsize = 2 - btime = time.Millisecond * 300 + blocks = 15 + blockSize = 4 + blockTime = time.Second ) - sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) - ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) - t.Cleanup(cancel) - fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, blocks) + sw := swamp.NewSwamp(t, swamp.WithBlockTime(blockTime)) + fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, blockSize, blocks) + set, val := sw.Validators(t) + fMaker := headerfraud.NewFraudMaker(t, 10, []types.PrivValidator{val}, set) + + storeCfg := eds.DefaultParameters() + ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) + edsStore, err := eds.NewStore(storeCfg, t.TempDir(), ds) + require.NoError(t, err) + require.NoError(t, edsStore.Start(ctx)) + t.Cleanup(func() { + _ = edsStore.Stop(ctx) + }) + cfg := nodebuilder.DefaultConfig(node.Bridge) - cfg.Share.UseShareExchange = false + // 1. bridge := sw.NewNodeWithConfig( node.Bridge, cfg, - core.WithHeaderConstructFn(headertest.FraudMaker(t, 10, mdutils.Bserv())), + core.WithHeaderConstructFn(fMaker.MakeExtendedHeader(16, edsStore)), + fx.Replace(edsStore), ) - - err := bridge.Start(ctx) - require.NoError(t, err) - addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) + // 2. + err = bridge.Start(ctx) require.NoError(t, err) + // 3. cfg = nodebuilder.DefaultConfig(node.Full) - cfg.Share.UseShareExchange = false + addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) + require.NoError(t, err) cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) + cfg.Share.UseShareExchange = false store := nodebuilder.MockStore(t, cfg) full := sw.NewNodeWithStore(node.Full, store) + // 4. err = full.Start(ctx) require.NoError(t, err) - // subscribe to fraud proof before node starts helps - // to prevent flakiness when fraud proof is propagating before subscribing on it - subscr, err := full.FraudServ.Subscribe(ctx, byzantine.BadEncoding) + fullClient := getAdminClient(ctx, full, t) + + // 5. + subCtx, subCancel := context.WithCancel(ctx) + subscr, err := fullClient.Fraud.Subscribe(subCtx, byzantine.BadEncoding) require.NoError(t, err) + select { + case p := <-subscr: + require.Equal(t, 10, int(p.Height())) + t.Log("Caught the proof....") + subCancel() + case <-ctx.Done(): + subCancel() + t.Fatal("full node did not receive a fraud proof in time") + } - p := <-subscr - require.Equal(t, 10, int(p.Height())) + getCtx, getCancel := context.WithTimeout(ctx, time.Second) + proofs, err := fullClient.Fraud.Get(getCtx, byzantine.BadEncoding) + getCancel() + require.NoError(t, err) + require.Len(t, proofs, 1) + require.True(t, proofs[0].Type() == byzantine.BadEncoding) // This is an obscure way to check if the Syncer was stopped. // If we cannot get a height header within a timeframe it means the syncer was stopped // FIXME: Eventually, this should be a check on service registry managing and keeping // lifecycles of each Module. - syncCtx, syncCancel := context.WithTimeout(context.Background(), btime) - _, err = full.HeaderServ.WaitForHeight(syncCtx, 100) + // 6. + // random height after befp.height + height := uint64(15) + // initial timeout is set to 5 sec, as we are targeting the height=15, + // blockTime=1 sec, expected befp.height=10 + timeOut := blockTime * 5 + // during befp validation the node can still receive headers and it mostly depends on + // the operating system or hardware(e.g. on macOS tests is working 100% time with a single + // height=15, and on the Linux VM sometimes the last height is 17-18). So, lets give a chance for + // our befp validator to check the fraud proof and stop the syncer. + for height < 20 { + syncCtx, syncCancel := context.WithTimeout(context.Background(), timeOut) + _, err = full.HeaderServ.WaitForHeight(syncCtx, height) + syncCancel() + if err != nil { + break + } + timeOut = blockTime + height++ + } require.ErrorIs(t, err, context.DeadlineExceeded) - syncCancel() - - require.NoError(t, full.Stop(ctx)) - require.NoError(t, sw.RemoveNode(full, node.Full)) - - full = sw.NewNodeWithStore(node.Full, store) - - require.Error(t, full.Start(ctx)) - proofs, err := full.FraudServ.Get(ctx, byzantine.BadEncoding) - require.NoError(t, err) - require.NotNil(t, proofs) - require.NoError(t, <-fillDn) -} - -/* -Test-Case: Light node receives a fraud proof using Fraud Sync -Pre-Requisites: -- CoreClient is started by swamp. -Steps: -1. Create a Bridge Node(BN) with broken extended header at height 10. -2. Start a BN. -3. Create a Full Node(FN) with a connection to BN as a trusted peer. -4. Start a FN. -5. Subscribe to a fraud proof and wait when it will be received. -6. Start LN once a fraud proof is received and verified by FN. -7. Wait until LN will be connected to FN and fetch a fraud proof. -Note: this test disables share exchange to speed up test results. -*/ -func TestFraudProofSyncing(t *testing.T) { - t.Skip("requires BEFP generation on app side to work") - const ( - blocks = 15 - bsize = 2 - btime = time.Millisecond * 300 - ) - sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) - ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) - t.Cleanup(cancel) - - fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, blocks) - cfg := nodebuilder.DefaultConfig(node.Bridge) - cfg.Share.UseShareExchange = false - store := nodebuilder.MockStore(t, cfg) - bridge := sw.NewNodeWithStore( - node.Bridge, - store, - core.WithHeaderConstructFn(headertest.FraudMaker(t, 10, mdutils.Bserv())), - ) - - err := bridge.Start(ctx) - require.NoError(t, err) - addr := host.InfoFromHost(bridge.Host) - addrs, err := peer.AddrInfoToP2pAddrs(addr) - require.NoError(t, err) - - fullCfg := nodebuilder.DefaultConfig(node.Full) - fullCfg.Share.UseShareExchange = false - fullCfg.Header.TrustedPeers = append(fullCfg.Header.TrustedPeers, addrs[0].String()) - full := sw.NewNodeWithStore(node.Full, nodebuilder.MockStore(t, fullCfg)) - - lightCfg := nodebuilder.DefaultConfig(node.Light) - lightCfg.Header.TrustedPeers = append(lightCfg.Header.TrustedPeers, addrs[0].String()) - ln := sw.NewNodeWithStore(node.Light, nodebuilder.MockStore(t, lightCfg)) - require.NoError(t, full.Start(ctx)) - - subsFN, err := full.FraudServ.Subscribe(ctx, byzantine.BadEncoding) + // 7. + cfg = nodebuilder.DefaultConfig(node.Light) + cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) + lnStore := nodebuilder.MockStore(t, cfg) + light := sw.NewNodeWithStore(node.Light, lnStore) + require.NoError(t, light.Start(ctx)) + lightClient := getAdminClient(ctx, light, t) + + // 8. + subCtx, subCancel = context.WithCancel(ctx) + subscr, err = lightClient.Fraud.Subscribe(subCtx, byzantine.BadEncoding) require.NoError(t, err) - select { - case <-subsFN: + case p := <-subscr: + require.Equal(t, 10, int(p.Height())) + subCancel() case <-ctx.Done(): - t.Fatal("full node didn't get FP in time") + subCancel() + t.Fatal("light node did not receive a fraud proof in time") } - // start LN to enforce syncing logic, not the PubSub's broadcasting - err = ln.Start(ctx) - require.NoError(t, err) - - // internal subscription for the fraud proof is done in order to ensure that light node - // receives the BEFP. - subsLN, err := ln.FraudServ.Subscribe(ctx, byzantine.BadEncoding) - require.NoError(t, err) - - // ensure that the full and light node are connected to speed up test - // alternatively, they would discover each other - err = ln.Host.Connect(ctx, *host.InfoFromHost(full.Host)) - require.NoError(t, err) + // 9. + fN := sw.NewNodeWithStore(node.Full, store) + err = fN.Start(ctx) + var fpExist *fraud.ErrFraudExists[*header.ExtendedHeader] + require.ErrorAs(t, err, &fpExist) - select { - case <-subsLN: - case <-ctx.Done(): - t.Fatal("light node didn't get FP in time") - } + sw.StopNode(ctx, bridge) + sw.StopNode(ctx, full) + sw.StopNode(ctx, light) require.NoError(t, <-fillDn) } diff --git a/nodebuilder/tests/helpers_test.go b/nodebuilder/tests/helpers_test.go new file mode 100644 index 0000000000..978b66553d --- /dev/null +++ b/nodebuilder/tests/helpers_test.go @@ -0,0 +1,35 @@ +//nolint:unused +package tests + +import ( + "context" + "testing" + "time" + + "github.com/filecoin-project/go-jsonrpc/auth" + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-node/api/rpc/client" + "github.com/celestiaorg/celestia-node/libs/authtoken" + "github.com/celestiaorg/celestia-node/nodebuilder" +) + +func getAdminClient(ctx context.Context, nd *nodebuilder.Node, t *testing.T) *client.Client { + t.Helper() + + signer := nd.AdminSigner + listenAddr := "ws://" + nd.RPCServer.ListenAddr() + + jwt, err := authtoken.NewSignedJWT(signer, []auth.Permission{"public", "read", "write", "admin"}) + require.NoError(t, err) + + client, err := client.NewClient(ctx, listenAddr, jwt) + require.NoError(t, err) + + return client +} + +func setTimeInterval(cfg *nodebuilder.Config, interval time.Duration) { + cfg.P2P.RoutingTableRefreshPeriod = interval + cfg.Share.Discovery.AdvertiseInterval = interval +} diff --git a/nodebuilder/tests/nd_test.go b/nodebuilder/tests/nd_test.go new file mode 100644 index 0000000000..338aa6d0c1 --- /dev/null +++ b/nodebuilder/tests/nd_test.go @@ -0,0 +1,212 @@ +//go:build nd || integration + +package tests + +import ( + "context" + "testing" + "time" + + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/network" + "github.com/stretchr/testify/require" + "go.uber.org/fx" + + "github.com/celestiaorg/celestia-node/nodebuilder" + "github.com/celestiaorg/celestia-node/nodebuilder/node" + "github.com/celestiaorg/celestia-node/nodebuilder/p2p" + "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/getters" + "github.com/celestiaorg/celestia-node/share/p2p/shrexnd" +) + +func TestShrexNDFromLights(t *testing.T) { + const ( + blocks = 10 + btime = time.Millisecond * 300 + bsize = 16 + ) + + ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) + t.Cleanup(cancel) + + sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) + fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, blocks) + + bridge := sw.NewBridgeNode() + sw.SetBootstrapper(t, bridge) + + cfg := nodebuilder.DefaultConfig(node.Light) + cfg.Share.Discovery.PeersLimit = 1 + light := sw.NewNodeWithConfig(node.Light, cfg) + + err := bridge.Start(ctx) + require.NoError(t, err) + err = light.Start(ctx) + require.NoError(t, err) + + bridgeClient := getAdminClient(ctx, bridge, t) + lightClient := getAdminClient(ctx, light, t) + + // wait for chain to be filled + require.NoError(t, <-fillDn) + + // first 15 blocks are not filled with data + // + // TODO: we need to stop guessing + // the block that actually has transactions. We can get this data from the + // response returned by FillBlock. + for i := 16; i < blocks; i++ { + h, err := bridgeClient.Header.GetByHeight(ctx, uint64(i)) + require.NoError(t, err) + + reqCtx, cancel := context.WithTimeout(ctx, time.Second*5) + + // ensure to fetch random namespace (not the reserved namespace) + namespace := h.DAH.RowRoots[1][:share.NamespaceSize] + + expected, err := bridgeClient.Share.GetSharesByNamespace(reqCtx, h, namespace) + require.NoError(t, err) + got, err := lightClient.Share.GetSharesByNamespace(reqCtx, h, namespace) + require.NoError(t, err) + + require.True(t, len(got[0].Shares) > 0) + require.Equal(t, expected, got) + + cancel() + } +} + +func TestShrexNDFromLightsWithBadFulls(t *testing.T) { + const ( + blocks = 10 + btime = time.Millisecond * 300 + bsize = 16 + amountOfFulls = 5 + testTimeout = time.Second * 10 + ) + + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + t.Cleanup(cancel) + + sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) + fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, blocks) + + bridge := sw.NewBridgeNode() + sw.SetBootstrapper(t, bridge) + + // create full nodes with basic stream.reset handler + ndHandler := func(stream network.Stream) { + _ = stream.Reset() + } + fulls := make([]*nodebuilder.Node, 0, amountOfFulls) + for i := 0; i < amountOfFulls; i++ { + cfg := nodebuilder.DefaultConfig(node.Full) + setTimeInterval(cfg, testTimeout) + full := sw.NewNodeWithConfig(node.Full, cfg, replaceNDServer(cfg, ndHandler), replaceShareGetter()) + fulls = append(fulls, full) + } + + lnConfig := nodebuilder.DefaultConfig(node.Light) + lnConfig.Share.Discovery.PeersLimit = uint(amountOfFulls) + light := sw.NewNodeWithConfig(node.Light, lnConfig) + + // start all nodes + require.NoError(t, bridge.Start(ctx)) + require.NoError(t, startFullNodes(ctx, fulls...)) + require.NoError(t, light.Start(ctx)) + + bridgeClient := getAdminClient(ctx, bridge, t) + lightClient := getAdminClient(ctx, light, t) + + // wait for chain to fill up + require.NoError(t, <-fillDn) + + // first 2 blocks are not filled with data + for i := 3; i < blocks; i++ { + h, err := bridgeClient.Header.GetByHeight(ctx, uint64(i)) + require.NoError(t, err) + + if len(h.DAH.RowRoots) != bsize*2 { + // fill blocks does not always fill every block to the given block + // size - this check prevents trying to fetch shares for the parity + // namespace. + continue + } + + reqCtx, cancel := context.WithTimeout(ctx, time.Second*5) + + // ensure to fetch random namespace (not the reserved namespace) + namespace := h.DAH.RowRoots[1][:share.NamespaceSize] + + expected, err := bridgeClient.Share.GetSharesByNamespace(reqCtx, h, namespace) + require.NoError(t, err) + require.True(t, len(expected[0].Shares) > 0) + + // choose a random full to test + fN := fulls[len(fulls)/2] + fnClient := getAdminClient(ctx, fN, t) + gotFull, err := fnClient.Share.GetSharesByNamespace(reqCtx, h, namespace) + require.NoError(t, err) + require.True(t, len(gotFull[0].Shares) > 0) + + gotLight, err := lightClient.Share.GetSharesByNamespace(reqCtx, h, namespace) + require.NoError(t, err) + require.True(t, len(gotLight[0].Shares) > 0) + + require.Equal(t, expected, gotFull) + require.Equal(t, expected, gotLight) + + cancel() + } +} + +func startFullNodes(ctx context.Context, fulls ...*nodebuilder.Node) error { + for _, full := range fulls { + err := full.Start(ctx) + if err != nil { + return err + } + } + return nil +} + +func replaceNDServer(cfg *nodebuilder.Config, handler network.StreamHandler) fx.Option { + return fx.Decorate(fx.Annotate( + func( + host host.Host, + store *eds.Store, + network p2p.Network, + ) (*shrexnd.Server, error) { + cfg.Share.ShrExNDParams.WithNetworkID(network.String()) + return shrexnd.NewServer(cfg.Share.ShrExNDParams, host, store) + }, + fx.OnStart(func(ctx context.Context, server *shrexnd.Server) error { + // replace handler for server + server.SetHandler(handler) + return server.Start(ctx) + }), + fx.OnStop(func(ctx context.Context, server *shrexnd.Server) error { + return server.Start(ctx) + }), + )) +} + +func replaceShareGetter() fx.Option { + return fx.Decorate(fx.Annotate( + func( + host host.Host, + store *eds.Store, + storeGetter *getters.StoreGetter, + shrexGetter *getters.ShrexGetter, + network p2p.Network, + ) share.Getter { + cascade := make([]share.Getter, 0, 2) + cascade = append(cascade, storeGetter) + cascade = append(cascade, shrexGetter) + return getters.NewCascadeGetter(cascade) + }, + )) +} diff --git a/nodebuilder/tests/p2p_test.go b/nodebuilder/tests/p2p_test.go index 39c3c985a2..98e9fc15b4 100644 --- a/nodebuilder/tests/p2p_test.go +++ b/nodebuilder/tests/p2p_test.go @@ -1,3 +1,5 @@ +//go:build p2p || integration + package tests import ( @@ -5,7 +7,6 @@ import ( "testing" "time" - "github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/peer" @@ -18,22 +19,22 @@ import ( ) /* -Test-Case: Full/Light Nodes connection to Bridge as a Bootstapper +Test-Case: Full/Light Nodes connection to Bridge as a Bootstrapper Steps: 1. Create a Bridge Node(BN) 2. Start a BN -3. Create full/light nodes with bridge node as bootsrapped peer +3. Create full/light nodes with bridge node as bootstrap peer 4. Start full/light nodes 5. Check that nodes are connected to bridge */ -func TestUseBridgeNodeAsBootstraper(t *testing.T) { - sw := swamp.NewSwamp(t) - - bridge := sw.NewBridgeNode() - +func TestBridgeNodeAsBootstrapper(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) t.Cleanup(cancel) + sw := swamp.NewSwamp(t) + + // create and start BN + bridge := sw.NewBridgeNode() err := bridge.Start(ctx) require.NoError(t, err) @@ -41,39 +42,17 @@ func TestUseBridgeNodeAsBootstraper(t *testing.T) { full := sw.NewFullNode(nodebuilder.WithBootstrappers([]peer.AddrInfo{*addr})) light := sw.NewLightNode(nodebuilder.WithBootstrappers([]peer.AddrInfo{*addr})) - nodes := []*nodebuilder.Node{full, light} - for index := range nodes { - require.NoError(t, nodes[index].Start(ctx)) - assert.Equal(t, *addr, nodes[index].Bootstrappers[0]) - assert.True(t, nodes[index].Host.Network().Connectedness(addr.ID) == network.Connected) - } -} -/* -Test-Case: Add peer to blacklist -Steps: -1. Create a Full Node(BN) -2. Start a FN -3. Create a Light Node(LN) -5. Start a LN -6. Explicitly block FN id by LN -7. Check FN is allowed to dial with LN -8. Check LN is not allowed to dial with FN -*/ -func TestAddPeerToBlackList(t *testing.T) { - sw := swamp.NewSwamp(t) - full := sw.NewFullNode() - ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) - t.Cleanup(cancel) - require.NoError(t, full.Start(ctx)) - - addr := host.InfoFromHost(full.Host) - light := sw.NewLightNode() - require.NoError(t, light.Start(ctx)) - require.NoError(t, light.ConnGater.BlockPeer(addr.ID)) - - require.True(t, full.ConnGater.InterceptPeerDial(host.InfoFromHost(light.Host).ID)) - require.False(t, light.ConnGater.InterceptPeerDial(addr.ID)) + for _, nd := range []*nodebuilder.Node{full, light} { + // start node and ensure that BN is correctly set as bootstrapper + require.NoError(t, nd.Start(ctx)) + assert.Equal(t, *addr, nd.Bootstrappers[0]) + // ensure that node is actually connected to BN + client := getAdminClient(ctx, nd, t) + connectedenss, err := client.P2P.Connectedness(ctx, addr.ID) + require.NoError(t, err) + assert.Equal(t, connectedenss, network.Connected) + } } /* @@ -86,134 +65,139 @@ Steps: 5. Ensure that nodes are connected to bridge 6. Wait until light will find full node 7. Check that full and light nodes are connected to each other - 8. Stop FN and ensure that it's not connected to LN */ -func TestBootstrapNodesFromBridgeNode(t *testing.T) { +func TestFullDiscoveryViaBootstrapper(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) + t.Cleanup(cancel) + + const defaultTimeInterval = time.Second * 2 + sw := swamp.NewSwamp(t) + + // create and start a BN cfg := nodebuilder.DefaultConfig(node.Bridge) - const defaultTimeInterval = time.Second * 10 setTimeInterval(cfg, defaultTimeInterval) - bridge := sw.NewNodeWithConfig(node.Bridge, cfg) - - ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) - t.Cleanup(cancel) - err := bridge.Start(ctx) require.NoError(t, err) - bridgeAddr := host.InfoFromHost(bridge.Host) + // use BN as the bootstrapper + bootstrapper := host.InfoFromHost(bridge.Host) + + // create FN with BN as bootstrapper cfg = nodebuilder.DefaultConfig(node.Full) setTimeInterval(cfg, defaultTimeInterval) full := sw.NewNodeWithConfig( node.Full, cfg, - nodebuilder.WithBootstrappers([]peer.AddrInfo{*bridgeAddr}), + nodebuilder.WithBootstrappers([]peer.AddrInfo{*bootstrapper}), ) + // create LN with BN as bootstrapper cfg = nodebuilder.DefaultConfig(node.Light) setTimeInterval(cfg, defaultTimeInterval) - cfg.P2P.PeerExchange = true light := sw.NewNodeWithConfig( node.Light, cfg, - nodebuilder.WithBootstrappers([]peer.AddrInfo{*bridgeAddr}), + nodebuilder.WithBootstrappers([]peer.AddrInfo{*bootstrapper}), ) + + // start FN and LN and ensure they are both connected to BN as a bootstrapper nodes := []*nodebuilder.Node{full, light} - ch := make(chan struct{}) - sub, err := light.Host.EventBus().Subscribe(&event.EvtPeerConnectednessChanged{}) - require.NoError(t, err) - defer sub.Close() for index := range nodes { require.NoError(t, nodes[index].Start(ctx)) - assert.Equal(t, *bridgeAddr, nodes[index].Bootstrappers[0]) - assert.True(t, nodes[index].Host.Network().Connectedness(bridgeAddr.ID) == network.Connected) + assert.Equal(t, *bootstrapper, nodes[index].Bootstrappers[0]) + // ensure that node is actually connected to BN + client := getAdminClient(ctx, nodes[index], t) + connectedness, err := client.P2P.Connectedness(ctx, bootstrapper.ID) + require.NoError(t, err) + assert.Equal(t, connectedness, network.Connected) } - addrFull := host.InfoFromHost(full.Host) - go func() { - for e := range sub.Out() { - connStatus := e.(event.EvtPeerConnectednessChanged) - if connStatus.Peer == full.Host.ID() && connStatus.Connectedness == network.NotConnected { - ch <- struct{}{} - } + + for { + if ctx.Err() != nil { + t.Fatal(ctx.Err()) + } + // LN discovered FN successfully and is now connected + client := getAdminClient(ctx, light, t) + connectedness, err := client.P2P.Connectedness(ctx, host.InfoFromHost(full.Host).ID) + require.NoError(t, err) + if connectedness == network.Connected { + break } - }() - - // ensure that the light node is connected to the full node - assert.True(t, light.Host.Network().Connectedness(addrFull.ID) == network.Connected) - - sw.Disconnect(t, light.Host.ID(), full.Host.ID()) - require.NoError(t, full.Stop(ctx)) - select { - case <-ctx.Done(): - t.Fatal("peer was not disconnected") - case <-ch: - assert.True(t, light.Host.Network().Connectedness(addrFull.ID) == network.NotConnected) } } /* -Test-Case: Restart full node discovery after one node is disconnected +Test-Case: Full node discovery of disconnected full nodes Steps: 1. Create a Bridge Node(BN) 2. Start a BN -3. Create 2 full nodes with bridge node as bootstrapper peer and start them -4. Check that nodes are connected to each other -5. Create one more node with disabled discovery -6. Disconnect FNs from each other -7. Check that the last FN is connected to one of the nodes +3. Create 2 FNs with bridge node as bootstrapper peer and start them +4. Check that the FNs discover each other +5. Disconnect the FNs +6. Create one more node with discovery process disabled (however advertisement is still enabled) +7. Check that the FN with discovery disabled is still found by the other two FNs *NOTE*: this test will take some time because it relies on several cycles of peer discovery */ func TestRestartNodeDiscovery(t *testing.T) { - sw := swamp.NewSwamp(t) - cfg := nodebuilder.DefaultConfig(node.Bridge) - const defaultTimeInterval = time.Second * 2 - const fullNodes = 2 - - setTimeInterval(cfg, defaultTimeInterval) - cfg.Share.Discovery.PeersLimit = fullNodes - bridge := sw.NewNodeWithConfig(node.Bridge, cfg) - ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) t.Cleanup(cancel) + const ( + defaultTimeInterval = time.Second * 2 + numFulls = 2 + ) + + sw := swamp.NewSwamp(t) + + // create and start a BN as a bootstrapper + fullCfg := nodebuilder.DefaultConfig(node.Bridge) + setTimeInterval(fullCfg, defaultTimeInterval) + bridge := sw.NewNodeWithConfig(node.Bridge, fullCfg) err := bridge.Start(ctx) require.NoError(t, err) + bridgeAddr := host.InfoFromHost(bridge.Host) - nodes := make([]*nodebuilder.Node, fullNodes) - cfg = nodebuilder.DefaultConfig(node.Full) - setTimeInterval(cfg, defaultTimeInterval) - cfg.Share.Discovery.PeersLimit = fullNodes + fullCfg = nodebuilder.DefaultConfig(node.Full) + setTimeInterval(fullCfg, defaultTimeInterval) nodesConfig := nodebuilder.WithBootstrappers([]peer.AddrInfo{*bridgeAddr}) - for index := 0; index < fullNodes; index++ { - nodes[index] = sw.NewNodeWithConfig(node.Full, cfg, nodesConfig) - } - for index := 0; index < fullNodes; index++ { + // create two FNs and start them, ensuring they are connected to BN as + // bootstrapper + nodes := make([]*nodebuilder.Node, numFulls) + for index := 0; index < numFulls; index++ { + nodes[index] = sw.NewNodeWithConfig(node.Full, fullCfg, nodesConfig) require.NoError(t, nodes[index].Start(ctx)) - assert.True(t, nodes[index].Host.Network().Connectedness(bridgeAddr.ID) == network.Connected) + client := getAdminClient(ctx, nodes[index], t) + connectedness, err := client.P2P.Connectedness(ctx, bridgeAddr.ID) + require.NoError(t, err) + assert.Equal(t, connectedness, network.Connected) } - // ensure full nodes are connected to each other - require.True(t, nodes[0].Host.Network().Connectedness(nodes[1].Host.ID()) == network.Connected) + // ensure FNs are connected to each other + fullClient1 := getAdminClient(ctx, nodes[0], t) + fullClient2 := getAdminClient(ctx, nodes[1], t) - // create one more node with disabled discovery - cfg = nodebuilder.DefaultConfig(node.Full) - setTimeInterval(cfg, defaultTimeInterval) - cfg.Share.Discovery.PeersLimit = 0 - node := sw.NewNodeWithConfig(node.Full, cfg, nodesConfig) - connectSub, err := nodes[0].Host.EventBus().Subscribe(&event.EvtPeerConnectednessChanged{}) + connectedness, err := fullClient1.P2P.Connectedness(ctx, nodes[1].Host.ID()) require.NoError(t, err) - defer connectSub.Close() - sw.Disconnect(t, nodes[0].Host.ID(), nodes[1].Host.ID()) - require.NoError(t, node.Start(ctx)) + assert.Equal(t, connectedness, network.Connected) - // ensure that the last node is connected to one of the nodes - require.True(t, nodes[0].Host.Network().Connectedness(node.Host.ID()) == network.Connected) -} + // disconnect the FNs + sw.Disconnect(t, nodes[0], nodes[1]) + + // create and start one more FN with disabled discovery + disabledDiscoveryFN := sw.NewNodeWithConfig(node.Full, fullCfg, nodesConfig) + require.NoError(t, err) -func setTimeInterval(cfg *nodebuilder.Config, interval time.Duration) { - cfg.P2P.RoutingTableRefreshPeriod = interval - cfg.Share.Discovery.AdvertiseInterval = interval + // ensure that the FN with disabled discovery is discovered by both of the + // running FNs that have discovery enabled + connectedness, err = fullClient1.P2P.Connectedness(ctx, disabledDiscoveryFN.Host.ID()) + require.NoError(t, err) + assert.Equal(t, connectedness, network.Connected) + + connectedness, err = fullClient2.P2P.Connectedness(ctx, disabledDiscoveryFN.Host.ID()) + require.NoError(t, err) + assert.Equal(t, connectedness, network.Connected) } diff --git a/nodebuilder/tests/reconstruct_test.go b/nodebuilder/tests/reconstruct_test.go index a4e3296c2f..d047182669 100644 --- a/nodebuilder/tests/reconstruct_test.go +++ b/nodebuilder/tests/reconstruct_test.go @@ -1,7 +1,4 @@ -// Test with light nodes spawns more goroutines than in the race detectors budget, -// and thus we're disabling the race detector. -// TODO(@Wondertan): Remove this once we move to go1.19 with unlimited race detector -//go:build !race +//go:build reconstruction || integration package tests @@ -14,6 +11,7 @@ import ( "github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" + ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" @@ -52,6 +50,12 @@ func TestFullReconstructFromBridge(t *testing.T) { bridge := sw.NewBridgeNode() err := bridge.Start(ctx) require.NoError(t, err) + bridgeClient := getAdminClient(ctx, bridge, t) + + // TODO: This is required to avoid flakes coming from unfinished retry + // mechanism for the same peer in go-header + _, err = bridgeClient.Header.WaitForHeight(ctx, uint64(blocks)) + require.NoError(t, err) cfg := nodebuilder.DefaultConfig(node.Full) cfg.Share.UseShareExchange = false @@ -59,19 +63,178 @@ func TestFullReconstructFromBridge(t *testing.T) { full := sw.NewNodeWithConfig(node.Full, cfg) err = full.Start(ctx) require.NoError(t, err) + fullClient := getAdminClient(ctx, full, t) errg, bctx := errgroup.WithContext(ctx) for i := 1; i <= blocks+1; i++ { i := i errg.Go(func() error { - h, err := full.HeaderServ.WaitForHeight(bctx, uint64(i)) + h, err := fullClient.Header.WaitForHeight(bctx, uint64(i)) + if err != nil { + return err + } + + return fullClient.Share.SharesAvailable(bctx, h) + }) + } + require.NoError(t, <-fillDn) + require.NoError(t, errg.Wait()) +} + +/* +Test-Case: Full Node reconstructs blocks from each other, after unsuccessfully syncing the complete +block from LN subnetworks. Analog to TestShareAvailable_DisconnectedFullNodes. +*/ +func TestFullReconstructFromFulls(t *testing.T) { + if testing.Short() { + t.Skip() + } + + light.DefaultSampleAmount = 10 // s + const ( + blocks = 10 + btime = time.Millisecond * 300 + bsize = 8 // k + lnodes = 12 // c - total number of nodes on two subnetworks + ) + + ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) + t.Cleanup(cancel) + + sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) + fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, blocks) + + const defaultTimeInterval = time.Second * 5 + bridge := sw.NewBridgeNode() + + sw.SetBootstrapper(t, bridge) + require.NoError(t, bridge.Start(ctx)) + bridgeClient := getAdminClient(ctx, bridge, t) + + // TODO: This is required to avoid flakes coming from unfinished retry + // mechanism for the same peer in go-header + _, err := bridgeClient.Header.WaitForHeight(ctx, uint64(blocks)) + require.NoError(t, err) + + lights1 := make([]*nodebuilder.Node, lnodes/2) + lights2 := make([]*nodebuilder.Node, lnodes/2) + subs := make([]event.Subscription, lnodes) + errg, errCtx := errgroup.WithContext(ctx) + for i := 0; i < lnodes/2; i++ { + i := i + errg.Go(func() error { + lnConfig := nodebuilder.DefaultConfig(node.Light) + setTimeInterval(lnConfig, defaultTimeInterval) + light := sw.NewNodeWithConfig(node.Light, lnConfig) + sub, err := light.Host.EventBus().Subscribe(&event.EvtPeerIdentificationCompleted{}) + if err != nil { + return err + } + subs[i] = sub + lights1[i] = light + return light.Start(errCtx) + }) + errg.Go(func() error { + lnConfig := nodebuilder.DefaultConfig(node.Light) + setTimeInterval(lnConfig, defaultTimeInterval) + light := sw.NewNodeWithConfig(node.Light, lnConfig) + sub, err := light.Host.EventBus().Subscribe(&event.EvtPeerIdentificationCompleted{}) if err != nil { return err } + subs[(lnodes/2)+i] = sub + lights2[i] = light + return light.Start(errCtx) + }) + } - return full.ShareServ.SharesAvailable(bctx, h.DAH) + require.NoError(t, errg.Wait()) + + for i := 0; i < lnodes; i++ { + select { + case <-ctx.Done(): + t.Fatal("peer was not found") + case <-subs[i].Out(): + require.NoError(t, subs[i].Close()) + continue + } + } + + // Remove bootstrappers to prevent FNs from connecting to bridge + sw.Bootstrappers = []ma.Multiaddr{} + // Use light nodes from respective subnetworks as bootstrappers to prevent connection to bridge + lnBootstrapper1, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(lights1[0].Host)) + require.NoError(t, err) + lnBootstrapper2, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(lights2[0].Host)) + require.NoError(t, err) + + cfg := nodebuilder.DefaultConfig(node.Full) + setTimeInterval(cfg, defaultTimeInterval) + cfg.Share.UseShareExchange = false + cfg.Share.Discovery.PeersLimit = 0 + cfg.Header.TrustedPeers = []string{lnBootstrapper1[0].String()} + full1 := sw.NewNodeWithConfig(node.Full, cfg) + cfg.Header.TrustedPeers = []string{lnBootstrapper2[0].String()} + full2 := sw.NewNodeWithConfig(node.Full, cfg) + require.NoError(t, full1.Start(ctx)) + require.NoError(t, full2.Start(ctx)) + + fullClient1 := getAdminClient(ctx, full1, t) + fullClient2 := getAdminClient(ctx, full2, t) + + // Form topology + for i := 0; i < lnodes/2; i++ { + // Separate light nodes into two subnetworks + for j := 0; j < lnodes/2; j++ { + sw.Disconnect(t, lights1[i], lights2[j]) + if i != j { + sw.Connect(t, lights1[i], lights1[j]) + sw.Connect(t, lights2[i], lights2[j]) + } + } + + sw.Connect(t, full1, lights1[i]) + sw.Disconnect(t, full1, lights2[i]) + + sw.Connect(t, full2, lights2[i]) + sw.Disconnect(t, full2, lights1[i]) + } + + // Ensure the fulls are not connected to the bridge + sw.Disconnect(t, full1, full2) + sw.Disconnect(t, full1, bridge) + sw.Disconnect(t, full2, bridge) + + h, err := fullClient1.Header.WaitForHeight(ctx, uint64(10+blocks-1)) + require.NoError(t, err) + + // Ensure that the full nodes cannot reconstruct before being connected to each other + ctxErr, cancelErr := context.WithTimeout(ctx, time.Second*30) + errg, errCtx = errgroup.WithContext(ctxErr) + errg.Go(func() error { + return fullClient1.Share.SharesAvailable(errCtx, h) + }) + errg.Go(func() error { + return fullClient1.Share.SharesAvailable(errCtx, h) + }) + require.Error(t, errg.Wait()) + cancelErr() + + // Reconnect FNs + sw.Connect(t, full1, full2) + + errg, bctx := errgroup.WithContext(ctx) + for i := 10; i < blocks+11; i++ { + h, err := fullClient1.Header.WaitForHeight(bctx, uint64(i)) + require.NoError(t, err) + errg.Go(func() error { + return fullClient1.Share.SharesAvailable(bctx, h) + }) + errg.Go(func() error { + return fullClient2.Share.SharesAvailable(bctx, h) }) } + require.NoError(t, <-fillDn) require.NoError(t, errg.Wait()) } @@ -93,6 +256,10 @@ Steps: 9. Check that the FN can retrieve shares from 1 to 20 blocks */ func TestFullReconstructFromLights(t *testing.T) { + if testing.Short() { + t.Skip() + } + eds.RetrieveQuadrantTimeout = time.Millisecond * 100 light.DefaultSampleAmount = 20 const ( @@ -122,9 +289,16 @@ func TestFullReconstructFromLights(t *testing.T) { } bootstrapper := sw.NewNodeWithConfig(node.Full, cfg) require.NoError(t, bootstrapper.Start(ctx)) - require.NoError(t, bridge.Start(ctx)) bootstrapperAddr := host.InfoFromHost(bootstrapper.Host) + require.NoError(t, bridge.Start(ctx)) + bridgeClient := getAdminClient(ctx, bridge, t) + + // TODO: This is required to avoid flakes coming from unfinished retry + // mechanism for the same peer in go-header + _, err = bridgeClient.Header.WaitForHeight(ctx, uint64(blocks)) + require.NoError(t, err) + cfg = nodebuilder.DefaultConfig(node.Full) setTimeInterval(cfg, defaultTimeInterval) cfg.Share.UseShareExchange = false @@ -152,8 +326,11 @@ func TestFullReconstructFromLights(t *testing.T) { return light.Start(errCtx) }) } + require.NoError(t, errg.Wait()) require.NoError(t, full.Start(ctx)) + fullClient := getAdminClient(ctx, full, t) + for i := 0; i < lnodes; i++ { select { case <-ctx.Done(): @@ -167,12 +344,12 @@ func TestFullReconstructFromLights(t *testing.T) { for i := 1; i <= blocks+1; i++ { i := i errg.Go(func() error { - h, err := full.HeaderServ.WaitForHeight(bctx, uint64(i)) + h, err := fullClient.Header.WaitForHeight(bctx, uint64(i)) if err != nil { return err } - return full.ShareServ.SharesAvailable(bctx, h.DAH) + return fullClient.Share.SharesAvailable(bctx, h) }) } require.NoError(t, <-fillDn) diff --git a/nodebuilder/tests/swamp/config.go b/nodebuilder/tests/swamp/config.go index f14243d342..047baa9f59 100644 --- a/nodebuilder/tests/swamp/config.go +++ b/nodebuilder/tests/swamp/config.go @@ -3,35 +3,30 @@ package swamp import ( "time" + "github.com/celestiaorg/celestia-app/test/util/testnode" + "github.com/celestiaorg/celestia-node/core" ) -// Config struct represents a set of pre-requisite attributes from the test scenario -type Config struct { - *core.TestConfig -} - // DefaultConfig creates a celestia-app instance with a block time of around // 100ms -func DefaultConfig() *Config { +func DefaultConfig() *testnode.Config { cfg := core.DefaultTestConfig() - // timeout commits faster than this tend to be flakier - cfg.Tendermint.Consensus.TimeoutCommit = 200 * time.Millisecond - return &Config{ - cfg, - } + // timeout commit lower than this tend to be flakier + cfg.TmConfig.Consensus.TimeoutCommit = 200 * time.Millisecond + return cfg } // Option for modifying Swamp's Config. -type Option func(*Config) +type Option func(*testnode.Config) // WithBlockTime sets a custom interval for block creation. func WithBlockTime(t time.Duration) Option { - return func(c *Config) { + return func(c *testnode.Config) { // for empty block - c.Tendermint.Consensus.CreateEmptyBlocksInterval = t + c.TmConfig.Consensus.CreateEmptyBlocksInterval = t // for filled block - c.Tendermint.Consensus.TimeoutCommit = t - c.Tendermint.Consensus.SkipTimeoutCommit = false + c.TmConfig.Consensus.TimeoutCommit = t + c.TmConfig.Consensus.SkipTimeoutCommit = false } } diff --git a/nodebuilder/tests/swamp/swamp.go b/nodebuilder/tests/swamp/swamp.go index 58ff807a4d..9faf69744d 100644 --- a/nodebuilder/tests/swamp/swamp.go +++ b/nodebuilder/tests/swamp/swamp.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "fmt" "net" + "sync" "testing" "time" @@ -15,9 +16,12 @@ import ( mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/types" "go.uber.org/fx" + "golang.org/x/exp/maps" - "github.com/celestiaorg/celestia-app/testutil/testnode" + "github.com/celestiaorg/celestia-app/test/util/testnode" apptypes "github.com/celestiaorg/celestia-app/x/blob/types" libhead "github.com/celestiaorg/go-header" @@ -35,7 +39,7 @@ import ( var blackholeIP6 = net.ParseIP("100::") -// DefaultTestTimeout should be used as the default timout on all the Swamp tests. +// DefaultTestTimeout should be used as the default timeout on all the Swamp tests. // It's generously set to 5 minutes to give enough time for CI. const DefaultTestTimeout = time.Minute * 5 @@ -45,16 +49,18 @@ const DefaultTestTimeout = time.Minute * 5 // - Slices of created Bridge/Full/Light Nodes // - trustedHash taken from the CoreClient and shared between nodes type Swamp struct { - t *testing.T - Network mocknet.Mocknet - BridgeNodes []*nodebuilder.Node - FullNodes []*nodebuilder.Node - LightNodes []*nodebuilder.Node - comps *Config + t *testing.T + cfg *testnode.Config + + Network mocknet.Mocknet + Bootstrappers []ma.Multiaddr ClientContext testnode.Context Accounts []string + nodesMu sync.Mutex + nodes map[*nodebuilder.Node]struct{} + genesis *header.ExtendedHeader } @@ -69,37 +75,39 @@ func NewSwamp(t *testing.T, options ...Option) *Swamp { option(ic) } - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - // Now, we are making an assumption that consensus mechanism is already tested out // so, we are not creating bridge nodes with each one containing its own core client // instead we are assigning all created BNs to 1 Core from the swamp - cctx := core.StartTestNodeWithConfig(t, ic.TestConfig) + ic.WithChainID("private") + cctx := core.StartTestNodeWithConfig(t, ic) swp := &Swamp{ t: t, + cfg: ic, Network: mocknet.New(), ClientContext: cctx, - comps: ic, Accounts: ic.Accounts, + nodes: map[*nodebuilder.Node]struct{}{}, } - swp.t.Cleanup(func() { - swp.stopAllNodes(ctx, swp.BridgeNodes, swp.FullNodes, swp.LightNodes) - }) - - swp.setupGenesis(ctx) + swp.t.Cleanup(swp.cleanup) + swp.setupGenesis() return swp } -// stopAllNodes goes through all received slices of Nodes and stops one-by-one -// this eliminates a manual clean-up in the test-cases itself in the end -func (s *Swamp) stopAllNodes(ctx context.Context, allNodes ...[]*nodebuilder.Node) { - for _, nodes := range allNodes { - for _, node := range nodes { - require.NoError(s.t, node.Stop(ctx)) - } - } +// cleanup frees up all the resources +// including stop of all created nodes +func (s *Swamp) cleanup() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + require.NoError(s.t, s.Network.Close()) + + s.nodesMu.Lock() + defer s.nodesMu.Unlock() + maps.DeleteFunc(s.nodes, func(nd *nodebuilder.Node, _ struct{}) bool { + require.NoError(s.t, nd.Stop(ctx)) + return true + }) } // GetCoreBlockHashByHeight returns a tendermint block's hash by provided height @@ -158,18 +166,23 @@ func (s *Swamp) createPeer(ks keystore.Keystore) host.Host { // setupGenesis sets up genesis Header. // This is required to initialize and start correctly. -func (s *Swamp) setupGenesis(ctx context.Context) { +func (s *Swamp) setupGenesis() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + // ensure core has surpassed genesis block s.WaitTillHeight(ctx, 2) - store, err := eds.NewStore(s.t.TempDir(), ds_sync.MutexWrap(ds.NewMapDatastore())) + ds := ds_sync.MutexWrap(ds.NewMapDatastore()) + store, err := eds.NewStore(eds.DefaultParameters(), s.t.TempDir(), ds) require.NoError(s.t, err) - ex := core.NewExchange( + ex, err := core.NewExchange( core.NewBlockFetcher(s.ClientContext.Client), store, header.MakeExtendedHeader, ) + require.NoError(s.t, err) h, err := ex.GetByHeight(ctx, 1) require.NoError(s.t, err) @@ -180,7 +193,7 @@ func (s *Swamp) setupGenesis(ctx context.Context) { func (s *Swamp) DefaultTestConfig(tp node.Type) *nodebuilder.Config { cfg := nodebuilder.DefaultConfig(tp) - ip, port, err := net.SplitHostPort(s.comps.App.GRPC.Address) + ip, port, err := net.SplitHostPort(s.cfg.AppConfig.GRPC.Address) require.NoError(s.t, err) cfg.Core.IP = ip @@ -204,6 +217,10 @@ func (s *Swamp) NewFullNode(options ...fx.Option) *nodebuilder.Node { cfg.Header.TrustedPeers = []string{ "/ip4/1.2.3.4/tcp/12345/p2p/12D3KooWNaJ1y1Yio3fFJEXCZyd1Cat3jmrPdgkYCrHfKD3Ce21p", } + // add all bootstrappers in suite as trusted peers + for _, bootstrapper := range s.Bootstrappers { + cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, bootstrapper.String()) + } store := nodebuilder.MockStore(s.t, cfg) return s.NewNodeWithStore(node.Full, store, options...) @@ -216,6 +233,10 @@ func (s *Swamp) NewLightNode(options ...fx.Option) *nodebuilder.Node { cfg.Header.TrustedPeers = []string{ "/ip4/1.2.3.4/tcp/12345/p2p/12D3KooWNaJ1y1Yio3fFJEXCZyd1Cat3jmrPdgkYCrHfKD3Ce21p", } + // add all bootstrappers in suite as trusted peers + for _, bootstrapper := range s.Bootstrappers { + cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, bootstrapper.String()) + } store := nodebuilder.MockStore(s.t, cfg) @@ -224,40 +245,37 @@ func (s *Swamp) NewLightNode(options ...fx.Option) *nodebuilder.Node { func (s *Swamp) NewNodeWithConfig(nodeType node.Type, cfg *nodebuilder.Config, options ...fx.Option) *nodebuilder.Node { store := nodebuilder.MockStore(s.t, cfg) + // add all bootstrappers in suite as trusted peers + for _, bootstrapper := range s.Bootstrappers { + cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, bootstrapper.String()) + } return s.NewNodeWithStore(nodeType, store, options...) } // NewNodeWithStore creates a new instance of Node with predefined Store. -// Afterwards, the instance is stored in the swamp's Nodes' slice according to the -// node's type provided from the user. func (s *Swamp) NewNodeWithStore( - t node.Type, + tp node.Type, store nodebuilder.Store, options ...fx.Option, ) *nodebuilder.Node { - var n *nodebuilder.Node - signer := apptypes.NewKeyringSigner(s.ClientContext.Keyring, s.Accounts[0], s.ClientContext.ChainID) options = append(options, state.WithKeyringSigner(signer), ) - switch t { + switch tp { case node.Bridge: options = append(options, coremodule.WithClient(s.ClientContext.Client), ) - n = s.newNode(node.Bridge, store, options...) - s.BridgeNodes = append(s.BridgeNodes, n) - case node.Full: - n = s.newNode(node.Full, store, options...) - s.FullNodes = append(s.FullNodes, n) - case node.Light: - n = s.newNode(node.Light, store, options...) - s.LightNodes = append(s.LightNodes, n) + default: } - return n + nd := s.newNode(tp, store, options...) + s.nodesMu.Lock() + s.nodes[nd] = struct{}{} + s.nodesMu.Unlock() + return nd } func (s *Swamp) newNode(t node.Type, store nodebuilder.Store, options ...fx.Option) *nodebuilder.Node { @@ -284,50 +302,20 @@ func (s *Swamp) newNode(t node.Type, store nodebuilder.Store, options ...fx.Opti return node } -// RemoveNode removes a node from the swamp's node slice -// this allows reusage of the same var in the test scenario -// if the user needs to stop and start the same node -func (s *Swamp) RemoveNode(n *nodebuilder.Node, t node.Type) error { - var err error - switch t { - case node.Light: - s.LightNodes, err = s.remove(n, s.LightNodes) - return err - case node.Bridge: - s.BridgeNodes, err = s.remove(n, s.BridgeNodes) - return err - case node.Full: - s.FullNodes, err = s.remove(n, s.FullNodes) - return err - default: - return fmt.Errorf("no such type or node") - } -} - -func (s *Swamp) remove(rn *nodebuilder.Node, sn []*nodebuilder.Node) ([]*nodebuilder.Node, error) { - if len(sn) == 1 { - return nil, nil - } - - initSize := len(sn) - for i := 0; i < len(sn); i++ { - if sn[i] == rn { - sn = append(sn[:i], sn[i+1:]...) - i-- - } - } - - if initSize <= len(sn) { - return sn, fmt.Errorf("cannot delete the node") - } - return sn, nil +// StopNode stops the node and removes from Swamp. +// TODO(@Wondertan): For clean and symmetrical API, we may want to add StartNode. +func (s *Swamp) StopNode(ctx context.Context, nd *nodebuilder.Node) { + s.nodesMu.Lock() + delete(s.nodes, nd) + s.nodesMu.Unlock() + require.NoError(s.t, nd.Stop(ctx)) } // Connect allows to connect peers after hard disconnection. -func (s *Swamp) Connect(t *testing.T, peerA, peerB peer.ID) { - _, err := s.Network.LinkPeers(peerA, peerB) +func (s *Swamp) Connect(t *testing.T, peerA, peerB *nodebuilder.Node) { + _, err := s.Network.LinkPeers(peerA.Host.ID(), peerB.Host.ID()) require.NoError(t, err) - _, err = s.Network.ConnectPeers(peerA, peerB) + _, err = s.Network.ConnectPeers(peerA.Host.ID(), peerB.Host.ID()) require.NoError(t, err) } @@ -335,7 +323,32 @@ func (s *Swamp) Connect(t *testing.T, peerA, peerB peer.ID) { // re-establish it. Order is very important here. We have to unlink peers first, and only after // that call disconnect. This is hard disconnect and peers will not be able to reconnect. // In order to reconnect peers again, please use swamp.Connect -func (s *Swamp) Disconnect(t *testing.T, peerA, peerB peer.ID) { - require.NoError(t, s.Network.UnlinkPeers(peerA, peerB)) - require.NoError(t, s.Network.DisconnectPeers(peerA, peerB)) +func (s *Swamp) Disconnect(t *testing.T, peerA, peerB *nodebuilder.Node) { + require.NoError(t, s.Network.UnlinkPeers(peerA.Host.ID(), peerB.Host.ID())) + require.NoError(t, s.Network.DisconnectPeers(peerA.Host.ID(), peerB.Host.ID())) +} + +// SetBootstrapper sets the given bootstrappers as the "bootstrappers" for the +// Swamp test suite. Every new full or light node created on the suite afterwards +// will automatically add the suite's bootstrappers as trusted peers to their config. +// NOTE: Bridge nodes do not automatically add the bootstrappers as trusted peers. +// NOTE: Use `NewNodeWithStore` to avoid this automatic configuration. +func (s *Swamp) SetBootstrapper(t *testing.T, bootstrappers ...*nodebuilder.Node) { + for _, trusted := range bootstrappers { + addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(trusted.Host)) + require.NoError(t, err) + s.Bootstrappers = append(s.Bootstrappers, addrs[0]) + } +} + +// Validators retrieves keys from the app node in order to build the validators. +func (s *Swamp) Validators(t *testing.T) (*types.ValidatorSet, types.PrivValidator) { + privPath := s.cfg.TmConfig.PrivValidatorKeyFile() + statePath := s.cfg.TmConfig.PrivValidatorStateFile() + priv := privval.LoadFilePV(privPath, statePath) + key, err := priv.GetPubKey() + require.NoError(t, err) + validator := types.NewValidator(key, 100) + set := types.NewValidatorSet([]*types.Validator{validator}) + return set, priv } diff --git a/nodebuilder/tests/swamp/swamp_tx.go b/nodebuilder/tests/swamp/swamp_tx.go index 956c8cf7c4..656b2f341d 100644 --- a/nodebuilder/tests/swamp/swamp_tx.go +++ b/nodebuilder/tests/swamp/swamp_tx.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" - "github.com/celestiaorg/celestia-app/testutil/testnode" + "github.com/celestiaorg/celestia-app/test/util/testnode" ) // FillBlocks produces the given amount of contiguous blocks with customizable size. diff --git a/nodebuilder/tests/sync_test.go b/nodebuilder/tests/sync_test.go index 68f33bc61b..ec8386ea43 100644 --- a/nodebuilder/tests/sync_test.go +++ b/nodebuilder/tests/sync_test.go @@ -1,3 +1,5 @@ +//go:build sync || integration + package tests import ( @@ -5,73 +7,193 @@ import ( "testing" "time" - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/peer" + ma "github.com/multiformats/go-multiaddr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/celestiaorg/celestia-node/nodebuilder" - "github.com/celestiaorg/celestia-node/nodebuilder/node" "github.com/celestiaorg/celestia-node/nodebuilder/tests/swamp" ) // Common consts for tests producing filled blocks const ( - blocks = 20 - bsize = 16 - btime = time.Millisecond * 300 + numBlocks = 20 + bsize = 16 + sbtime = time.Millisecond * 300 ) /* -Test-Case: Sync a Light Node with a Bridge Node(includes DASing of non-empty blocks) +Test-Case: Header and block/sample sync against a Bridge Node of non-empty blocks. + Steps: 1. Create a Bridge Node(BN) 2. Start a BN 3. Check BN is synced to height 20 -4. Create a Light Node(LN) with a trusted peer + +Light node: +4. Create a Light Node (LN) with bridge as a trusted peer 5. Start a LN with a defined connection to the BN -6. Check LN is synced to height 30 +6. Check LN is header-synced to height 20 +7. Wait until LN has sampled height 20 +8. Wait for LN DASer to catch up to network head + +Full node: +4. Create a Full Node (FN) with bridge as a trusted peer +5. Start a FN with a defined connection to the BN +6. Check FN is header-synced to height 20 +7. Wait until FN has synced block at height 20 +8. Wait for FN DASer to catch up to network head */ -func TestSyncLightWithBridge(t *testing.T) { +func TestSyncAgainstBridge_NonEmptyChain(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) t.Cleanup(cancel) - sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) - fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, blocks) + sw := swamp.NewSwamp(t, swamp.WithBlockTime(sbtime)) + // wait for core network to fill 20 blocks + fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, numBlocks) + sw.WaitTillHeight(ctx, numBlocks) + // create a bridge node and set it as the bootstrapper for the suite bridge := sw.NewBridgeNode() - - sw.WaitTillHeight(ctx, 20) - + sw.SetBootstrapper(t, bridge) + // start bridge and wait for it to sync to 20 err := bridge.Start(ctx) require.NoError(t, err) + bridgeClient := getAdminClient(ctx, bridge, t) - h, err := bridge.HeaderServ.WaitForHeight(ctx, 20) + h, err := bridgeClient.Header.WaitForHeight(ctx, numBlocks) require.NoError(t, err) + require.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) + + t.Run("light sync against bridge", func(t *testing.T) { + // create a light node that is connected to the bridge node as + // a bootstrapper + light := sw.NewLightNode() + // start light node and wait for it to sync 20 blocks + err = light.Start(ctx) + require.NoError(t, err) + lightClient := getAdminClient(ctx, light, t) + h, err = lightClient.Header.WaitForHeight(ctx, numBlocks) + require.NoError(t, err) + assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) + + // check that the light node has also sampled over the block at height 20 + err = lightClient.Share.SharesAvailable(ctx, h) + assert.NoError(t, err) + + // wait until the entire chain (up to network head) has been sampled + err = lightClient.DAS.WaitCatchUp(ctx) + require.NoError(t, err) + }) + + t.Run("full sync against bridge", func(t *testing.T) { + // create a full node with bridge node as its bootstrapper + full := sw.NewFullNode() + // let full node sync 20 blocks + err = full.Start(ctx) + require.NoError(t, err) + fullClient := getAdminClient(ctx, full, t) + h, err = fullClient.Header.WaitForHeight(ctx, numBlocks) + require.NoError(t, err) + assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) + + // check to ensure the full node can sync the 20th block's data + err = fullClient.Share.SharesAvailable(ctx, h) + assert.NoError(t, err) + + // wait for full node to sync up the blocks from genesis -> network head. + err = fullClient.DAS.WaitCatchUp(ctx) + require.NoError(t, err) + }) + + // wait for the core block filling process to exit + select { + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case err := <-fillDn: + require.NoError(t, err) + } +} - require.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 20)) +/* +Test-Case: Header and block/sample sync against a Bridge Node of empty blocks. - addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) - require.NoError(t, err) +Steps: +1. Create a Bridge Node(BN) +2. Start a BN +3. Check BN is synced to height 20 - cfg := nodebuilder.DefaultConfig(node.Light) - cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - light := sw.NewNodeWithConfig(node.Light, cfg) +Light node: +4. Create a Light Node (LN) with bridge as a trusted peer +5. Start a LN with a defined connection to the BN +6. Check LN is header-synced to height 20 +7. Wait until LN has sampled height 20 +8. Wait for LN DASer to catch up to network head + +Full node: +4. Create a Full Node (FN) with bridge as a trusted peer +5. Start a FN with a defined connection to the BN +6. Check FN is header-synced to height 20 +7. Wait until FN has synced block at height 20 +8. Wait for FN DASer to catch up to network head +*/ +func TestSyncAgainstBridge_EmptyChain(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) + t.Cleanup(cancel) - err = light.Start(ctx) - require.NoError(t, err) + sw := swamp.NewSwamp(t, swamp.WithBlockTime(sbtime)) + sw.WaitTillHeight(ctx, numBlocks) - h, err = light.HeaderServ.WaitForHeight(ctx, 30) + // create bridge node and set it as the bootstrapper for the suite + bridge := sw.NewBridgeNode() + sw.SetBootstrapper(t, bridge) + // start bridge and wait for it to sync to 20 + err := bridge.Start(ctx) require.NoError(t, err) - - err = light.ShareServ.SharesAvailable(ctx, h.DAH) - assert.NoError(t, err) - - err = light.DASer.WaitCatchUp(ctx) + bridgeClient := getAdminClient(ctx, bridge, t) + h, err := bridgeClient.Header.WaitForHeight(ctx, numBlocks) require.NoError(t, err) - - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 30)) - require.NoError(t, <-fillDn) + require.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) + + t.Run("light sync against bridge", func(t *testing.T) { + // create a light node that is connected to the bridge node as + // a bootstrapper + light := sw.NewLightNode() + // start light node and wait for it to sync 20 blocks + err = light.Start(ctx) + require.NoError(t, err) + lightClient := getAdminClient(ctx, light, t) + h, err = lightClient.Header.WaitForHeight(ctx, numBlocks) + require.NoError(t, err) + assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) + + // check that the light node has also sampled over the block at height 20 + err = lightClient.Share.SharesAvailable(ctx, h) + assert.NoError(t, err) + + // wait until the entire chain (up to network head) has been sampled + err = lightClient.DAS.WaitCatchUp(ctx) + require.NoError(t, err) + }) + + t.Run("full sync against bridge", func(t *testing.T) { + // create a full node with bridge node as its bootstrapper + full := sw.NewFullNode() + // let full node sync 20 blocks + err = full.Start(ctx) + require.NoError(t, err) + fullClient := getAdminClient(ctx, full, t) + h, err = fullClient.Header.WaitForHeight(ctx, numBlocks) + require.NoError(t, err) + assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) + + // check to ensure the full node can sync the 20th block's data + err = fullClient.Share.SharesAvailable(ctx, h) + assert.NoError(t, err) + + // wait for full node to sync up the blocks from genesis -> network head. + err = fullClient.DAS.WaitCatchUp(ctx) + require.NoError(t, err) + }) } /* @@ -85,107 +207,63 @@ Steps: 3. Check BN is synced to height 20 4. Create a Light Node(LN) with a trusted peer 5. Start a LN with a defined connection to the BN -6. Check LN is synced to height 30 -7. Stop LN -8. Start LN +6. Check LN is synced to height 20 +7. Disconnect LN from BN for 3 seconds while BN continues broadcasting new blocks from core +8. Re-connect LN and let it sync up again 9. Check LN is synced to height 40 */ func TestSyncStartStopLightWithBridge(t *testing.T) { - sw := swamp.NewSwamp(t) - - bridge := sw.NewBridgeNode() + if testing.Short() { + t.Skip("skipping TestSyncStartStopLightWithBridge test in short mode.") + } ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) - t.Cleanup(cancel) + defer cancel() - sw.WaitTillHeight(ctx, 50) + sw := swamp.NewSwamp(t) + // wait for core network to fill 20 blocks + fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, numBlocks) + sw.WaitTillHeight(ctx, numBlocks) + // create bridge and set it as a bootstrapper + bridge := sw.NewBridgeNode() + sw.SetBootstrapper(t, bridge) + // and let bridge node sync up 20 blocks err := bridge.Start(ctx) require.NoError(t, err) - - h, err := bridge.HeaderServ.WaitForHeight(ctx, 20) + bridgeClient := getAdminClient(ctx, bridge, t) + h, err := bridgeClient.Header.WaitForHeight(ctx, numBlocks) require.NoError(t, err) + require.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) - require.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 20)) - - addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) + // create a light node and connect it to the bridge node as a bootstrapper + light := sw.NewLightNode() + // start light node and let it sync to 20 + err = light.Start(ctx) require.NoError(t, err) - - cfg := nodebuilder.DefaultConfig(node.Light) - cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - light := sw.NewNodeWithConfig(node.Light, cfg) - require.NoError(t, light.Start(ctx)) - - h, err = light.HeaderServ.WaitForHeight(ctx, 30) + lightClient := getAdminClient(ctx, light, t) + h, err = lightClient.Header.WaitForHeight(ctx, numBlocks) require.NoError(t, err) + require.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) - require.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 30)) + sw.StopNode(ctx, light) - require.NoError(t, light.Stop(ctx)) - require.NoError(t, sw.RemoveNode(light, node.Light)) - - cfg = nodebuilder.DefaultConfig(node.Light) - cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - light = sw.NewNodeWithConfig(node.Light, cfg) + light = sw.NewLightNode() require.NoError(t, light.Start(ctx)) - h, err = light.HeaderServ.WaitForHeight(ctx, 40) + // ensure when light node comes back up, it can sync the remainder of the chain it + // missed while sleeping + h, err = lightClient.Header.WaitForHeight(ctx, 40) require.NoError(t, err) - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 40)) -} - -/* -Test-Case: Sync a Full Node with a Bridge Node(includes DASing of non-empty blocks) -Steps: -1. Create a Bridge Node(BN) -2. Start a BN -3. Check BN is synced to height 20 -4. Create a Full Node(FN) with a connection to BN as a trusted peer -5. Start a FN -6. Check FN is synced to height 30 -*/ -func TestSyncFullWithBridge(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) - t.Cleanup(cancel) - sw := swamp.NewSwamp(t, swamp.WithBlockTime(btime)) - fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, blocks) - - bridge := sw.NewBridgeNode() - - sw.WaitTillHeight(ctx, 20) - - err := bridge.Start(ctx) - require.NoError(t, err) - - h, err := bridge.HeaderServ.WaitForHeight(ctx, 20) - require.NoError(t, err) - - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 20)) - - addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) - require.NoError(t, err) - - cfg := nodebuilder.DefaultConfig(node.Full) - cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - cfg.Share.UseShareExchange = false - full := sw.NewNodeWithConfig(node.Full, cfg) - require.NoError(t, full.Start(ctx)) - - h, err = full.HeaderServ.WaitForHeight(ctx, 30) - require.NoError(t, err) - - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 30)) - - err = full.ShareServ.SharesAvailable(ctx, h.DAH) - assert.NoError(t, err) - - err = full.DASer.WaitCatchUp(ctx) - require.NoError(t, err) - - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 30)) - require.NoError(t, <-fillDn) + // wait for the core block filling process to exit + select { + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case err := <-fillDn: + require.NoError(t, err) + } } /* @@ -199,59 +277,73 @@ Steps: 3. Check BN is synced to height 20 4. Create a Full Node(FN) with a connection to BN as a trusted peer 5. Start a FN -6. Check FN is synced to height 30 +6. Check FN is synced to network head 7. Create a Light Node(LN) with a connection to FN as a trusted peer -8. Start LN -9. Check LN is synced to height 50 +8. Ensure LN is NOT connected to BN and only connected to FN +9. Start LN +10. Check LN is synced to network head */ -func TestSyncLightWithFull(t *testing.T) { - sw := swamp.NewSwamp(t) - - bridge := sw.NewBridgeNode() - +func TestSyncLightAgainstFull(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) t.Cleanup(cancel) - sw.WaitTillHeight(ctx, 20) + sw := swamp.NewSwamp(t) + // wait for the core network to fill up 20 blocks + fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, numBlocks) + sw.WaitTillHeight(ctx, numBlocks) + // create bridge and set it as a bootstrapper + bridge := sw.NewBridgeNode() + sw.SetBootstrapper(t, bridge) + // start a bridge node and wait for it to sync up 20 blocks err := bridge.Start(ctx) require.NoError(t, err) - - h, err := bridge.HeaderServ.WaitForHeight(ctx, 20) + bridgeClient := getAdminClient(ctx, bridge, t) + h, err := bridgeClient.Header.WaitForHeight(ctx, numBlocks) require.NoError(t, err) + assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 20)) - - addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) + // create a FN with BN as a trusted peer + full := sw.NewFullNode() + // start FN and wait for it to sync up to head of BN + err = full.Start(ctx) require.NoError(t, err) - - cfg := nodebuilder.DefaultConfig(node.Full) - cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - full := sw.NewNodeWithConfig(node.Full, cfg) - require.NoError(t, full.Start(ctx)) - - h, err = full.HeaderServ.WaitForHeight(ctx, 30) + fullClient := getAdminClient(ctx, full, t) + bridgeHead, err := bridgeClient.Header.LocalHead(ctx) require.NoError(t, err) - - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 30)) - - addrs, err = peer.AddrInfoToP2pAddrs(host.InfoFromHost(full.Host)) + _, err = fullClient.Header.WaitForHeight(ctx, bridgeHead.Height()) require.NoError(t, err) + assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, numBlocks)) - cfg = nodebuilder.DefaultConfig(node.Light) - cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - light := sw.NewNodeWithConfig(node.Light, cfg) + // reset suite bootstrapper list and set full node as a bootstrapper for + // LN to connect to + sw.Bootstrappers = make([]ma.Multiaddr, 0) + sw.SetBootstrapper(t, full) + // create an LN with FN as a trusted peer + light := sw.NewLightNode() + + // ensure there is no direct connection between LN and BN so that + // LN relies only on FN for syncing err = sw.Network.UnlinkPeers(bridge.Host.ID(), light.Host.ID()) require.NoError(t, err) + // start LN and wait for it to sync up to network head against the head of the FN err = light.Start(ctx) require.NoError(t, err) - - h, err = light.HeaderServ.WaitForHeight(ctx, 50) + lightClient := getAdminClient(ctx, light, t) + fullHead, err := fullClient.Header.LocalHead(ctx) + require.NoError(t, err) + _, err = lightClient.Header.WaitForHeight(ctx, fullHead.Height()) require.NoError(t, err) - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 50)) + // wait for the core block filling process to exit + select { + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case err := <-fillDn: + require.NoError(t, err) + } } /* @@ -265,54 +357,57 @@ Steps: 3. Check BN is synced to height 20 4. Create a Full Node(FN) with a connection to BN as a trusted peer 5. Start a FN -6. Check FN is synced to height 30 -7. Create a Light Node(LN) with a connection to BN, FN as trusted peers +6. Check FN is synced to network head +7. Create a Light Node(LN) with a connection to BN and FN as trusted peers 8. Start LN -9. Check LN is synced to height 50 +9. Check LN is synced to network head. */ func TestSyncLightWithTrustedPeers(t *testing.T) { - sw := swamp.NewSwamp(t) - - bridge := sw.NewBridgeNode() - ctx, cancel := context.WithTimeout(context.Background(), swamp.DefaultTestTimeout) t.Cleanup(cancel) - sw.WaitTillHeight(ctx, 20) + sw := swamp.NewSwamp(t) + fillDn := swamp.FillBlocks(ctx, sw.ClientContext, sw.Accounts, bsize, numBlocks) + sw.WaitTillHeight(ctx, numBlocks) + // create a BN and set as a bootstrapper + bridge := sw.NewBridgeNode() + sw.SetBootstrapper(t, bridge) + // let it sync to network head err := bridge.Start(ctx) require.NoError(t, err) - - h, err := bridge.HeaderServ.WaitForHeight(ctx, 20) + bridgeClient := getAdminClient(ctx, bridge, t) + _, err = bridgeClient.Header.WaitForHeight(ctx, numBlocks) require.NoError(t, err) - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 20)) + // create a FN with BN as trusted peer + full := sw.NewFullNode() - addrs, err := peer.AddrInfoToP2pAddrs(host.InfoFromHost(bridge.Host)) + // let FN sync to network head + err = full.Start(ctx) require.NoError(t, err) - - cfg := nodebuilder.DefaultConfig(node.Full) - cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - full := sw.NewNodeWithConfig(node.Full, cfg) - require.NoError(t, full.Start(ctx)) - - h, err = full.HeaderServ.WaitForHeight(ctx, 30) + fullClient := getAdminClient(ctx, full, t) + err = fullClient.Header.SyncWait(ctx) require.NoError(t, err) - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 30)) + // add full node as a bootstrapper for the suite + sw.SetBootstrapper(t, full) - addrs, err = peer.AddrInfoToP2pAddrs(host.InfoFromHost(full.Host)) - require.NoError(t, err) - - cfg = nodebuilder.DefaultConfig(node.Light) - cfg.Header.TrustedPeers = append(cfg.Header.TrustedPeers, addrs[0].String()) - light := sw.NewNodeWithConfig(node.Light, cfg) + // create a LN with both FN and BN as trusted peers + light := sw.NewLightNode() + // let LN sync to network head err = light.Start(ctx) require.NoError(t, err) - - h, err = light.HeaderServ.WaitForHeight(ctx, 50) + lightClient := getAdminClient(ctx, light, t) + err = lightClient.Header.SyncWait(ctx) require.NoError(t, err) - assert.EqualValues(t, h.Commit.BlockID.Hash, sw.GetCoreBlockHashByHeight(ctx, 50)) + // wait for the core block filling process to exit + select { + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case err := <-fillDn: + require.NoError(t, err) + } } diff --git a/pruner/archival/pruner.go b/pruner/archival/pruner.go new file mode 100644 index 0000000000..7b1cb935f3 --- /dev/null +++ b/pruner/archival/pruner.go @@ -0,0 +1,20 @@ +package archival + +import ( + "context" + + "github.com/celestiaorg/celestia-node/header" +) + +// Pruner is a noop implementation of the pruner.Factory interface +// that allows archival nodes to sync and retain historical data +// that is out of the availability window. +type Pruner struct{} + +func NewPruner() *Pruner { + return &Pruner{} +} + +func (p *Pruner) Prune(context.Context, ...*header.ExtendedHeader) error { + return nil +} diff --git a/pruner/archival/window.go b/pruner/archival/window.go new file mode 100644 index 0000000000..b89a779816 --- /dev/null +++ b/pruner/archival/window.go @@ -0,0 +1,5 @@ +package archival + +import "github.com/celestiaorg/celestia-node/pruner" + +const Window = pruner.AvailabilityWindow(0) diff --git a/pruner/light/pruner.go b/pruner/light/pruner.go new file mode 100644 index 0000000000..513bfa2b66 --- /dev/null +++ b/pruner/light/pruner.go @@ -0,0 +1,17 @@ +package light + +import ( + "context" + + "github.com/celestiaorg/celestia-node/header" +) + +type Pruner struct{} + +func NewPruner() *Pruner { + return &Pruner{} +} + +func (p *Pruner) Prune(context.Context, ...*header.ExtendedHeader) error { + return nil +} diff --git a/pruner/light/window.go b/pruner/light/window.go new file mode 100644 index 0000000000..dc1a9e4444 --- /dev/null +++ b/pruner/light/window.go @@ -0,0 +1,11 @@ +package light + +import ( + "time" + + "github.com/celestiaorg/celestia-node/pruner" +) + +// Window is the availability window for light nodes in the Celestia +// network (30 days). +const Window = pruner.AvailabilityWindow(time.Second * 86400 * 30) diff --git a/pruner/pruner.go b/pruner/pruner.go new file mode 100644 index 0000000000..fae60e483c --- /dev/null +++ b/pruner/pruner.go @@ -0,0 +1,13 @@ +package pruner + +import ( + "context" + + "github.com/celestiaorg/celestia-node/header" +) + +// Pruner contains methods necessary to prune data +// from the node's datastore. +type Pruner interface { + Prune(context.Context, ...*header.ExtendedHeader) error +} diff --git a/pruner/service.go b/pruner/service.go new file mode 100644 index 0000000000..f67265977a --- /dev/null +++ b/pruner/service.go @@ -0,0 +1,25 @@ +package pruner + +import ( + "context" +) + +// Service handles the pruning routine for the node using the +// prune Pruner. +type Service struct { + pruner Pruner +} + +func NewService(p Pruner) *Service { + return &Service{ + pruner: p, + } +} + +func (s *Service) Start(context.Context) error { + return nil +} + +func (s *Service) Stop(context.Context) error { + return nil +} diff --git a/pruner/window.go b/pruner/window.go new file mode 100644 index 0000000000..0a86c535ce --- /dev/null +++ b/pruner/window.go @@ -0,0 +1,7 @@ +package pruner + +import ( + "time" +) + +type AvailabilityWindow time.Duration diff --git a/share/availability.go b/share/availability.go index 9538573114..f3511da450 100644 --- a/share/availability.go +++ b/share/availability.go @@ -4,7 +4,10 @@ import ( "context" "errors" - da "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/rsmt2d" + + "github.com/celestiaorg/celestia-node/header" ) // ErrNotAvailable is returned whenever DA sampling fails. @@ -14,13 +17,21 @@ var ErrNotAvailable = errors.New("share: data not available") // In practice, it is a commitment to all the Data in a square. type Root = da.DataAvailabilityHeader +// NewRoot generates Root(DataAvailabilityHeader) using the +// provided extended data square. +func NewRoot(eds *rsmt2d.ExtendedDataSquare) (*Root, error) { + dah, err := da.NewDataAvailabilityHeader(eds) + if err != nil { + return nil, err + } + return &dah, nil +} + // Availability defines interface for validation of Shares' availability. +// +//go:generate mockgen -destination=availability/mocks/availability.go -package=mocks . Availability type Availability interface { // SharesAvailable subjectively validates if Shares committed to the given Root are available on // the Network. - SharesAvailable(context.Context, *Root) error - // ProbabilityOfAvailability calculates the probability of the data square - // being available based on the number of samples collected. - // TODO(@Wondertan): Merge with SharesAvailable method, eventually - ProbabilityOfAvailability(context.Context) float64 + SharesAvailable(context.Context, *header.ExtendedHeader) error } diff --git a/share/availability/cache/availability.go b/share/availability/cache/availability.go deleted file mode 100644 index d6496f7ff8..0000000000 --- a/share/availability/cache/availability.go +++ /dev/null @@ -1,100 +0,0 @@ -package cache - -import ( - "bytes" - "context" - "sync" - - "github.com/ipfs/go-datastore" - "github.com/ipfs/go-datastore/autobatch" - "github.com/ipfs/go-datastore/namespace" - logging "github.com/ipfs/go-log/v2" - - "github.com/celestiaorg/celestia-app/pkg/da" - - "github.com/celestiaorg/celestia-node/share" -) - -var ( - log = logging.Logger("share/cache") - minRoot = da.MinDataAvailabilityHeader() - - cacheAvailabilityPrefix = datastore.NewKey("sampling_result") - writeBatchSize = 2048 -) - -// ShareAvailability wraps a given share.Availability (whether it's light or full) -// and stores the results of a successful sampling routine over a given Root's hash -// to disk. -type ShareAvailability struct { - avail share.Availability - - // TODO(@Wondertan): Once we come to parallelized DASer, this lock becomes a contention point - // Related to #483 - dsLk sync.RWMutex - ds *autobatch.Datastore -} - -// NewShareAvailability wraps the given share.Availability with an additional datastore -// for sampling result caching. -func NewShareAvailability( - avail share.Availability, - ds datastore.Batching, -) *ShareAvailability { - ds = namespace.Wrap(ds, cacheAvailabilityPrefix) - autoDS := autobatch.NewAutoBatching(ds, writeBatchSize) - - return &ShareAvailability{ - avail: avail, - ds: autoDS, - } -} - -// SharesAvailable will store, upon success, the hash of the given Root to disk. -func (ca *ShareAvailability) SharesAvailable(ctx context.Context, root *share.Root) error { - // short-circuit if the given root is minimum DAH of an empty data square - if isMinRoot(root) { - return nil - } - // do not sample over Root that has already been sampled - key := rootKey(root) - - ca.dsLk.RLock() - exists, err := ca.ds.Has(ctx, key) - ca.dsLk.RUnlock() - if err != nil || exists { - return err - } - - err = ca.avail.SharesAvailable(ctx, root) - if err != nil { - return err - } - - ca.dsLk.Lock() - err = ca.ds.Put(ctx, key, []byte{}) - ca.dsLk.Unlock() - if err != nil { - log.Errorw("storing root of successful SharesAvailable request to disk", "err", err) - } - return err -} - -func (ca *ShareAvailability) ProbabilityOfAvailability(ctx context.Context) float64 { - return ca.avail.ProbabilityOfAvailability(ctx) -} - -// Close flushes all queued writes to disk. -func (ca *ShareAvailability) Close(ctx context.Context) error { - return ca.ds.Flush(ctx) -} - -func rootKey(root *share.Root) datastore.Key { - return datastore.NewKey(root.String()) -} - -// isMinRoot returns whether the given root is a minimum (empty) -// DataAvailabilityHeader (DAH). -func isMinRoot(root *share.Root) bool { - return bytes.Equal(minRoot.Hash(), root.Hash()) -} diff --git a/share/availability/cache/availability_test.go b/share/availability/cache/availability_test.go deleted file mode 100644 index 47df434c06..0000000000 --- a/share/availability/cache/availability_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package cache - -import ( - "context" - "fmt" - "strconv" - "testing" - "time" - - "github.com/ipfs/go-datastore" - "github.com/ipfs/go-datastore/sync" - mdutils "github.com/ipfs/go-merkledag/test" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/celestiaorg/celestia-app/pkg/da" - - "github.com/celestiaorg/celestia-node/share" - availability_test "github.com/celestiaorg/celestia-node/share/availability/test" -) - -// TestCacheAvailability tests to ensure that the successful result of a -// sampling process is properly stored locally. -func TestCacheAvailability(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - fullLocalServ, dah0 := FullAvailabilityWithLocalRandSquare(t, 16) - lightLocalServ, dah1 := LightAvailabilityWithLocalRandSquare(t, 16) - - var tests = []struct { - avail share.Availability - root *share.Root - }{ - { - avail: fullLocalServ, - root: dah0, - }, - { - avail: lightLocalServ, - root: dah1, - }, - } - - for i, tt := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - ca := tt.avail.(*ShareAvailability) - // ensure the dah isn't yet in the cache - exists, err := ca.ds.Has(ctx, rootKey(tt.root)) - require.NoError(t, err) - assert.False(t, exists) - err = tt.avail.SharesAvailable(ctx, tt.root) - require.NoError(t, err) - // ensure the dah was stored properly - exists, err = ca.ds.Has(ctx, rootKey(tt.root)) - require.NoError(t, err) - assert.True(t, exists) - }) - } -} - -var invalidHeader = da.DataAvailabilityHeader{ - RowsRoots: [][]byte{{1, 2}}, -} - -// TestCacheAvailability_Failed tests to make sure a failed -// sampling process is not stored. -func TestCacheAvailability_Failed(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - - ca := NewShareAvailability(&dummyAvailability{}, sync.MutexWrap(datastore.NewMapDatastore())) - - err := ca.SharesAvailable(ctx, &invalidHeader) - require.Error(t, err) - // ensure the dah was NOT cached - exists, err := ca.ds.Has(ctx, rootKey(&invalidHeader)) - require.NoError(t, err) - assert.False(t, exists) -} - -// TestCacheAvailability_NoDuplicateSampling tests to ensure that -// if the Root was already sampled, it does not run a sampling routine -// again. -func TestCacheAvailability_NoDuplicateSampling(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - // create root to cache - root := availability_test.RandFillBS(t, 16, mdutils.Bserv()) - // wrap dummyAvailability with a datastore - ds := sync.MutexWrap(datastore.NewMapDatastore()) - ca := NewShareAvailability(&dummyAvailability{counter: 0}, ds) - // sample the root - err := ca.SharesAvailable(ctx, root) - require.NoError(t, err) - // ensure root was cached - exists, err := ca.ds.Has(ctx, rootKey(root)) - require.NoError(t, err) - assert.True(t, exists) - // call sampling routine over same root again and ensure no error is returned - // if an error was returned, that means duplicate sampling occurred - err = ca.SharesAvailable(ctx, root) - require.NoError(t, err) -} - -// TestCacheAvailability_MinRoot tests to make sure `SharesAvailable` will -// short circuit if the given root is a minimum DataAvailabilityHeader (minRoot). -func TestCacheAvailability_MinRoot(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - fullLocalAvail, _ := FullAvailabilityWithLocalRandSquare(t, 16) - minDAH := da.MinDataAvailabilityHeader() - - err := fullLocalAvail.SharesAvailable(ctx, &minDAH) - assert.NoError(t, err) -} - -type dummyAvailability struct { - counter int -} - -// SharesAvailable should only be called once, if called more than once, return -// error. -func (da *dummyAvailability) SharesAvailable(_ context.Context, root *share.Root) error { - if root == &invalidHeader { - return fmt.Errorf("invalid header") - } - if da.counter > 0 { - return fmt.Errorf("duplicate sampling process called") - } - da.counter++ - return nil -} - -func (da *dummyAvailability) ProbabilityOfAvailability(context.Context) float64 { - return 0 -} diff --git a/share/availability/cache/testing.go b/share/availability/cache/testing.go deleted file mode 100644 index 978d51b6b5..0000000000 --- a/share/availability/cache/testing.go +++ /dev/null @@ -1,39 +0,0 @@ -package cache - -import ( - "testing" - - ds "github.com/ipfs/go-datastore" - dssync "github.com/ipfs/go-datastore/sync" - mdutils "github.com/ipfs/go-merkledag/test" - - "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/availability/full" - "github.com/celestiaorg/celestia-node/share/availability/light" - availability_test "github.com/celestiaorg/celestia-node/share/availability/test" - "github.com/celestiaorg/celestia-node/share/getters" -) - -// LightAvailabilityWithLocalRandSquare wraps light.GetterWithRandSquare with cache availability -func LightAvailabilityWithLocalRandSquare(t *testing.T, n int) (share.Availability, *share.Root) { - bServ := mdutils.Bserv() - store := dssync.MutexWrap(ds.NewMapDatastore()) - getter := getters.NewIPLDGetter(bServ) - avail := NewShareAvailability( - light.TestAvailability(getter), - store, - ) - return avail, availability_test.RandFillBS(t, n, bServ) -} - -// FullAvailabilityWithLocalRandSquare wraps full.GetterWithRandSquare with cache availability -func FullAvailabilityWithLocalRandSquare(t *testing.T, n int) (share.Availability, *share.Root) { - bServ := mdutils.Bserv() - store := dssync.MutexWrap(ds.NewMapDatastore()) - getter := getters.NewIPLDGetter(bServ) - avail := NewShareAvailability( - full.TestAvailability(getter), - store, - ) - return avail, availability_test.RandFillBS(t, n, bServ) -} diff --git a/share/availability/full/availability.go b/share/availability/full/availability.go index cfa5bd0c39..4ea211cb1e 100644 --- a/share/availability/full/availability.go +++ b/share/availability/full/availability.go @@ -3,13 +3,16 @@ package full import ( "context" "errors" + "fmt" - ipldFormat "github.com/ipfs/go-ipld-format" + "github.com/filecoin-project/dagstore" logging "github.com/ipfs/go-log/v2" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/eds/byzantine" + "github.com/celestiaorg/celestia-node/share/ipld" "github.com/celestiaorg/celestia-node/share/p2p/discovery" ) @@ -54,35 +57,46 @@ func (fa *ShareAvailability) Stop(context.Context) error { // SharesAvailable reconstructs the data committed to the given Root by requesting // enough Shares from the network. -func (fa *ShareAvailability) SharesAvailable(ctx context.Context, root *share.Root) error { +func (fa *ShareAvailability) SharesAvailable(ctx context.Context, header *header.ExtendedHeader) error { + dah := header.DAH + // short-circuit if the given root is minimum DAH of an empty data square, to avoid datastore hit + if share.DataHash(dah.Hash()).IsEmptyRoot() { + return nil + } + // we assume the caller of this method has already performed basic validation on the // given dah/root. If for some reason this has not happened, the node should panic. - if err := root.ValidateBasic(); err != nil { + if err := dah.ValidateBasic(); err != nil { log.Errorw("Availability validation cannot be performed on a malformed DataAvailabilityHeader", "err", err) panic(err) } // a hack to avoid loading the whole EDS in mem if we store it already. - if fa.store != nil { - if ok, _ := fa.store.Has(ctx, root.Hash()); ok { - return nil - } + if ok, _ := fa.store.Has(ctx, dah.Hash()); ok { + return nil } - _, err := fa.getter.GetEDS(ctx, root) + adder := ipld.NewProofsAdder(len(dah.RowRoots)) + ctx = ipld.CtxWithProofsAdder(ctx, adder) + defer adder.Purge() + + eds, err := fa.getter.GetEDS(ctx, header) if err != nil { - log.Errorw("availability validation failed", "root", root.String(), "err", err.Error()) + if errors.Is(err, context.Canceled) { + return err + } + log.Errorw("availability validation failed", "root", dah.String(), "err", err.Error()) var byzantineErr *byzantine.ErrByzantine - if ipldFormat.IsNotFound(err) || errors.Is(err, context.DeadlineExceeded) && !errors.As(err, &byzantineErr) { + if errors.Is(err, share.ErrNotFound) || errors.Is(err, context.DeadlineExceeded) && !errors.As(err, &byzantineErr) { return share.ErrNotAvailable } - return err } - return err -} -func (fa *ShareAvailability) ProbabilityOfAvailability(context.Context) float64 { - return 1 + err = fa.store.Put(ctx, dah.Hash(), eds) + if err != nil && !errors.Is(err, dagstore.ErrShardExists) { + return fmt.Errorf("full availability: failed to store eds: %w", err) + } + return nil } diff --git a/share/availability/full/availability_test.go b/share/availability/full/availability_test.go index 48ae0534aa..8ac0648a87 100644 --- a/share/availability/full/availability_test.go +++ b/share/availability/full/availability_test.go @@ -4,9 +4,17 @@ import ( "context" "testing" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/celestiaorg/celestia-app/pkg/da" + + "github.com/celestiaorg/celestia-node/header/headertest" + "github.com/celestiaorg/celestia-node/share" availability_test "github.com/celestiaorg/celestia-node/share/availability/test" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/mocks" ) func TestShareAvailableOverMocknet_Full(t *testing.T) { @@ -15,10 +23,12 @@ func TestShareAvailableOverMocknet_Full(t *testing.T) { net := availability_test.NewTestDAGNet(ctx, t) _, root := RandNode(net, 32) + + eh := headertest.RandExtendedHeaderWithRoot(t, root) nd := Node(net) net.ConnectAll() - err := nd.SharesAvailable(ctx, root) + err := nd.SharesAvailable(ctx, eh) assert.NoError(t, err) } @@ -28,7 +38,45 @@ func TestSharesAvailable_Full(t *testing.T) { // RandServiceWithSquare creates a NewShareAvailability inside, so we can test it getter, dah := GetterWithRandSquare(t, 16) - avail := TestAvailability(getter) - err := avail.SharesAvailable(ctx, dah) + + eh := headertest.RandExtendedHeaderWithRoot(t, dah) + avail := TestAvailability(t, getter) + err := avail.SharesAvailable(ctx, eh) assert.NoError(t, err) } + +func TestSharesAvailable_StoresToEDSStore(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // RandServiceWithSquare creates a NewShareAvailability inside, so we can test it + getter, dah := GetterWithRandSquare(t, 16) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) + avail := TestAvailability(t, getter) + err := avail.SharesAvailable(ctx, eh) + assert.NoError(t, err) + + has, err := avail.store.Has(ctx, dah.Hash()) + assert.NoError(t, err) + assert.True(t, has) +} + +func TestSharesAvailable_Full_ErrNotAvailable(t *testing.T) { + ctrl := gomock.NewController(t) + getter := mocks.NewMockGetter(ctrl) + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + eds := edstest.RandEDS(t, 4) + dah, err := da.NewDataAvailabilityHeader(eds) + eh := headertest.RandExtendedHeaderWithRoot(t, &dah) + require.NoError(t, err) + avail := TestAvailability(t, getter) + + errors := []error{share.ErrNotFound, context.DeadlineExceeded} + for _, getterErr := range errors { + getter.EXPECT().GetEDS(gomock.Any(), gomock.Any()).Return(nil, getterErr) + err := avail.SharesAvailable(ctx, eh) + require.ErrorIs(t, err, share.ErrNotAvailable) + } +} diff --git a/share/availability/full/reconstruction_test.go b/share/availability/full/reconstruction_test.go index 03b998d49e..6ac5a3f31e 100644 --- a/share/availability/full/reconstruction_test.go +++ b/share/availability/full/reconstruction_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" + "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/availability/light" availability_test "github.com/celestiaorg/celestia-node/share/availability/test" @@ -37,7 +38,9 @@ func TestShareAvailable_OneFullNode(t *testing.T) { net := availability_test.NewTestDAGNet(ctx, t) source, root := RandNode(net, origSquareSize) // make a source node, a.k.a bridge - full := Node(net) // make a full availability service which reconstructs data + eh := headertest.RandExtendedHeader(t) + eh.DAH = root + full := Node(net) // make a full availability service which reconstructs data // ensure there is no connection between source and full nodes // so that full reconstructs from the light nodes only @@ -45,14 +48,14 @@ func TestShareAvailable_OneFullNode(t *testing.T) { errg, errCtx := errgroup.WithContext(ctx) errg.Go(func() error { - return full.SharesAvailable(errCtx, root) + return full.SharesAvailable(errCtx, eh) }) lights := make([]*availability_test.TestNode, lightNodes) for i := 0; i < len(lights); i++ { lights[i] = light.Node(net) go func(i int) { - err := lights[i].SharesAvailable(ctx, root) + err := lights[i].SharesAvailable(ctx, eh) if err != nil { t.Log("light errors:", err) } @@ -91,6 +94,8 @@ func TestShareAvailable_ConnectedFullNodes(t *testing.T) { net := availability_test.NewTestDAGNet(ctx, t) source, root := RandNode(net, origSquareSize) + eh := headertest.RandExtendedHeader(t) + eh.DAH = root // create two full nodes and ensure they are disconnected full1 := Node(net) @@ -106,10 +111,10 @@ func TestShareAvailable_ConnectedFullNodes(t *testing.T) { // start reconstruction for fulls errg, errCtx := errgroup.WithContext(ctx) errg.Go(func() error { - return full1.SharesAvailable(errCtx, root) + return full1.SharesAvailable(errCtx, eh) }) errg.Go(func() error { - return full2.SharesAvailable(errCtx, root) + return full2.SharesAvailable(errCtx, eh) }) // create light nodes and start sampling for them immediately @@ -119,7 +124,7 @@ func TestShareAvailable_ConnectedFullNodes(t *testing.T) { for i := 0; i < len(lights1); i++ { lights1[i] = light.Node(net) go func(i int) { - err := lights1[i].SharesAvailable(ctx, root) + err := lights1[i].SharesAvailable(ctx, eh) if err != nil { t.Log("light1 errors:", err) } @@ -127,7 +132,7 @@ func TestShareAvailable_ConnectedFullNodes(t *testing.T) { lights2[i] = light.Node(net) go func(i int) { - err := lights2[i].SharesAvailable(ctx, root) + err := lights2[i].SharesAvailable(ctx, eh) if err != nil { t.Log("light2 errors:", err) } @@ -181,34 +186,16 @@ func TestShareAvailable_DisconnectedFullNodes(t *testing.T) { light.DefaultSampleAmount = 20 // s const ( origSquareSize = 16 // k - lightNodes = 60 // c - total number of nodes on two subnetworks + lightNodes = 32 // c - total number of nodes on two subnetworks ) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*60) defer cancel() net := availability_test.NewTestDAGNet(ctx, t) source, root := RandNode(net, origSquareSize) - - // create two full nodes and ensure they are disconnected - full1 := Node(net) - full2 := Node(net) - net.Disconnect(full1.ID(), full2.ID()) - - // ensure fulls and source are not connected - // so that fulls take data from light nodes only - net.Disconnect(full1.ID(), source.ID()) - net.Disconnect(full2.ID(), source.ID()) - - // start reconstruction for fulls that should fail - ctxErr, cancelErr := context.WithTimeout(ctx, eds.RetrieveQuadrantTimeout*8) - errg, errCtx := errgroup.WithContext(ctxErr) - errg.Go(func() error { - return full1.SharesAvailable(errCtx, root) - }) - errg.Go(func() error { - return full2.SharesAvailable(errCtx, root) - }) + eh := headertest.RandExtendedHeader(t) + eh.DAH = root // create light nodes and start sampling for them immediately lights1, lights2 := make( @@ -221,7 +208,7 @@ func TestShareAvailable_DisconnectedFullNodes(t *testing.T) { lights1[i] = light.Node(net) go func(i int) { defer wg.Done() - err := lights1[i].SharesAvailable(ctx, root) + err := lights1[i].SharesAvailable(ctx, eh) if err != nil { t.Log("light1 errors:", err) } @@ -230,13 +217,23 @@ func TestShareAvailable_DisconnectedFullNodes(t *testing.T) { lights2[i] = light.Node(net) go func(i int) { defer wg.Done() - err := lights2[i].SharesAvailable(ctx, root) + err := lights2[i].SharesAvailable(ctx, eh) if err != nil { t.Log("light2 errors:", err) } }(i) } + // create two full nodes and ensure they are disconnected + full1 := Node(net) + full2 := Node(net) + net.Disconnect(full1.ID(), full2.ID()) + + // ensure fulls and source are not connected + // so that fulls take data from light nodes only + net.Disconnect(full1.ID(), source.ID()) + net.Disconnect(full2.ID(), source.ID()) + // shape topology for i := 0; i < len(lights1); i++ { // ensure lights1 are only connected to source and full1 @@ -249,6 +246,16 @@ func TestShareAvailable_DisconnectedFullNodes(t *testing.T) { net.Disconnect(lights2[i].ID(), full1.ID()) } + // start reconstruction for fulls that should fail + ctxErr, cancelErr := context.WithTimeout(ctx, time.Second*5) + errg, errCtx := errgroup.WithContext(ctxErr) + errg.Go(func() error { + return full1.SharesAvailable(errCtx, eh) + }) + errg.Go(func() error { + return full2.SharesAvailable(errCtx, eh) + }) + // check that any of the fulls cannot reconstruct on their own err := errg.Wait() require.ErrorIs(t, err, share.ErrNotAvailable) @@ -262,10 +269,14 @@ func TestShareAvailable_DisconnectedFullNodes(t *testing.T) { full2.ClearStorage() // they both should be able to reconstruct the block - err = full1.SharesAvailable(ctx, root) - require.NoError(t, err, share.ErrNotAvailable) - err = full2.SharesAvailable(ctx, root) - require.NoError(t, err, share.ErrNotAvailable) + errg, bctx := errgroup.WithContext(ctx) + errg.Go(func() error { + return full1.SharesAvailable(bctx, eh) + }) + errg.Go(func() error { + return full2.SharesAvailable(bctx, eh) + }) + require.NoError(t, errg.Wait()) // wait for all routines to finish before exit, in case there are any errors to log wg.Wait() } diff --git a/share/availability/full/testing.go b/share/availability/full/testing.go index 680eb7abaf..46e97581f2 100644 --- a/share/availability/full/testing.go +++ b/share/availability/full/testing.go @@ -1,23 +1,27 @@ package full import ( + "context" "testing" "time" - mdutils "github.com/ipfs/go-merkledag/test" + "github.com/ipfs/go-datastore" routinghelpers "github.com/libp2p/go-libp2p-routing-helpers" "github.com/libp2p/go-libp2p/p2p/discovery/routing" + "github.com/stretchr/testify/require" "github.com/celestiaorg/celestia-node/share" availability_test "github.com/celestiaorg/celestia-node/share/availability/test" + "github.com/celestiaorg/celestia-node/share/eds" "github.com/celestiaorg/celestia-node/share/getters" + "github.com/celestiaorg/celestia-node/share/ipld" "github.com/celestiaorg/celestia-node/share/p2p/discovery" ) // GetterWithRandSquare provides a share.Getter filled with 'n' NMT // trees of 'n' random shares, essentially storing a whole square. func GetterWithRandSquare(t *testing.T, n int) (share.Getter, *share.Root) { - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() getter := getters.NewIPLDGetter(bServ) return getter, availability_test.RandFillBS(t, n, bServ) } @@ -32,16 +36,29 @@ func RandNode(dn *availability_test.TestDagNet, squareSize int) (*availability_t func Node(dn *availability_test.TestDagNet) *availability_test.TestNode { nd := dn.NewTestNode() nd.Getter = getters.NewIPLDGetter(nd.BlockService) - nd.Availability = TestAvailability(nd.Getter) + nd.Availability = TestAvailability(dn.T, nd.Getter) return nd } -func TestAvailability(getter share.Getter) *ShareAvailability { - disc := discovery.NewDiscovery( +func TestAvailability(t *testing.T, getter share.Getter) *ShareAvailability { + params := discovery.DefaultParameters() + params.AdvertiseInterval = time.Second + params.PeersLimit = 10 + disc, err := discovery.NewDiscovery( + params, nil, routing.NewRoutingDiscovery(routinghelpers.Null{}), - discovery.WithAdvertiseInterval(time.Second), - discovery.WithPeersLimit(10), + "full", ) - return NewShareAvailability(nil, getter, disc) + require.NoError(t, err) + store, err := eds.NewStore(eds.DefaultParameters(), t.TempDir(), datastore.NewMapDatastore()) + require.NoError(t, err) + err = store.Start(context.Background()) + require.NoError(t, err) + + t.Cleanup(func() { + err = store.Stop(context.Background()) + require.NoError(t, err) + }) + return NewShareAvailability(store, getter, disc) } diff --git a/share/availability/light/availability.go b/share/availability/light/availability.go index 07a3e801d9..1d35542344 100644 --- a/share/availability/light/availability.go +++ b/share/availability/light/availability.go @@ -3,16 +3,24 @@ package light import ( "context" "errors" - "math" + "sync" + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/autobatch" + "github.com/ipfs/go-datastore/namespace" ipldFormat "github.com/ipfs/go-ipld-format" logging "github.com/ipfs/go-log/v2" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/getters" ) -var log = logging.Logger("share/light") +var ( + log = logging.Logger("share/light") + cacheAvailabilityPrefix = datastore.NewKey("sampling_result") + writeBatchSize = 2048 +) // ShareAvailability implements share.Availability using Data Availability Sampling technique. // It is light because it does not require the downloading of all the data to verify @@ -21,34 +29,63 @@ var log = logging.Logger("share/light") type ShareAvailability struct { getter share.Getter params Parameters + + // TODO(@Wondertan): Once we come to parallelized DASer, this lock becomes a contention point + // Related to #483 + // TODO: Striped locks? :D + dsLk sync.RWMutex + ds *autobatch.Datastore } // NewShareAvailability creates a new light Availability. func NewShareAvailability( getter share.Getter, + ds datastore.Batching, opts ...Option, ) *ShareAvailability { params := DefaultParameters() + ds = namespace.Wrap(ds, cacheAvailabilityPrefix) + autoDS := autobatch.NewAutoBatching(ds, writeBatchSize) for _, opt := range opts { opt(¶ms) } - return &ShareAvailability{getter, params} + return &ShareAvailability{ + getter: getter, + params: params, + ds: autoDS, + } } // SharesAvailable randomly samples `params.SampleAmount` amount of Shares committed to the given -// Root. This way SharesAvailable subjectively verifies that Shares are available. -func (la *ShareAvailability) SharesAvailable(ctx context.Context, dah *share.Root) error { - log.Debugw("Validate availability", "root", dah.String()) +// ExtendedHeader. This way SharesAvailable subjectively verifies that Shares are available. +func (la *ShareAvailability) SharesAvailable(ctx context.Context, header *header.ExtendedHeader) error { + dah := header.DAH + // short-circuit if the given root is minimum DAH of an empty data square + if share.DataHash(dah.Hash()).IsEmptyRoot() { + return nil + } + + // do not sample over Root that has already been sampled + key := rootKey(dah) + + la.dsLk.RLock() + exists, err := la.ds.Has(ctx, key) + la.dsLk.RUnlock() + if err != nil || exists { + return err + } + + log.Debugw("validate availability", "root", dah.String()) // We assume the caller of this method has already performed basic validation on the // given dah/root. If for some reason this has not happened, the node should panic. if err := dah.ValidateBasic(); err != nil { - log.Errorw("Availability validation cannot be performed on a malformed DataAvailabilityHeader", + log.Errorw("availability validation cannot be performed on a malformed DataAvailabilityHeader", "err", err) panic(err) } - samples, err := SampleSquare(len(dah.RowsRoots), int(la.params.SampleAmount)) + samples, err := SampleSquare(len(dah.RowRoots), int(la.params.SampleAmount)) if err != nil { return err } @@ -62,7 +99,7 @@ func (la *ShareAvailability) SharesAvailable(ctx context.Context, dah *share.Roo for _, s := range samples { go func(s Sample) { log.Debugw("fetching share", "root", dah.String(), "row", s.Row, "col", s.Col) - _, err := la.getter.GetShare(ctx, dah, s.Row, s.Col) + _, err := la.getter.GetShare(ctx, header, s.Row, s.Col) if err != nil { log.Debugw("error fetching share", "root", dah.String(), "row", s.Row, "col", s.Col) } @@ -84,25 +121,31 @@ func (la *ShareAvailability) SharesAvailable(ctx context.Context, dah *share.Roo } if err != nil { - if !errors.Is(err, context.Canceled) { - log.Errorw("availability validation failed", "root", dah.String(), "err", err.Error()) + if errors.Is(err, context.Canceled) { + return err } + log.Errorw("availability validation failed", "root", dah.String(), "err", err.Error()) if ipldFormat.IsNotFound(err) || errors.Is(err, context.DeadlineExceeded) { return share.ErrNotAvailable } - return err } } + la.dsLk.Lock() + err = la.ds.Put(ctx, key, []byte{}) + la.dsLk.Unlock() + if err != nil { + log.Errorw("storing root of successful SharesAvailable request to disk", "err", err) + } return nil } -// ProbabilityOfAvailability calculates the probability that the -// data square is available based on the amount of samples collected -// (params.SampleAmount). -// -// Formula: 1 - (0.75 ** amount of samples) -func (la *ShareAvailability) ProbabilityOfAvailability(context.Context) float64 { - return 1 - math.Pow(0.75, float64(la.params.SampleAmount)) +func rootKey(root *share.Root) datastore.Key { + return datastore.NewKey(root.String()) +} + +// Close flushes all queued writes to disk. +func (la *ShareAvailability) Close(ctx context.Context) error { + return la.ds.Flush(ctx) } diff --git a/share/availability/light/availability_test.go b/share/availability/light/availability_test.go index 146cb8554a..2ace654d50 100644 --- a/share/availability/light/availability_test.go +++ b/share/availability/light/availability_test.go @@ -1,29 +1,83 @@ package light import ( - "bytes" "context" - "crypto/rand" _ "embed" - "encoding/hex" - "encoding/json" - mrand "math/rand" "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - tmproto "github.com/tendermint/tendermint/proto/tendermint/types" - core "github.com/tendermint/tendermint/types" - "github.com/celestiaorg/celestia-app/pkg/da" - appshares "github.com/celestiaorg/celestia-app/pkg/shares" - - "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" availability_test "github.com/celestiaorg/celestia-node/share/availability/test" + "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share/sharetest" ) +func TestSharesAvailableCaches(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + getter, eh := GetterWithRandSquare(t, 16) + dah := eh.DAH + avail := TestAvailability(getter) + + // cache doesn't have dah yet + has, err := avail.ds.Has(ctx, rootKey(dah)) + assert.NoError(t, err) + assert.False(t, has) + + err = avail.SharesAvailable(ctx, eh) + assert.NoError(t, err) + + // is now cached + has, err = avail.ds.Has(ctx, rootKey(dah)) + assert.NoError(t, err) + assert.True(t, has) +} + +func TestSharesAvailableHitsCache(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + getter, _ := GetterWithRandSquare(t, 16) + avail := TestAvailability(getter) + + bServ := ipld.NewMemBlockservice() + dah := availability_test.RandFillBS(t, 16, bServ) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) + + // blockstore doesn't actually have the dah + err := avail.SharesAvailable(ctx, eh) + require.Error(t, err) + + // cache doesn't have dah yet, since it errored + has, err := avail.ds.Has(ctx, rootKey(dah)) + assert.NoError(t, err) + assert.False(t, has) + + err = avail.ds.Put(ctx, rootKey(dah), []byte{}) + require.NoError(t, err) + + // should hit cache after putting + err = avail.SharesAvailable(ctx, eh) + require.NoError(t, err) +} + +func TestSharesAvailableEmptyRoot(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + getter, _ := GetterWithRandSquare(t, 16) + avail := TestAvailability(getter) + + eh := headertest.RandExtendedHeaderWithRoot(t, share.EmptyRoot()) + err := avail.SharesAvailable(ctx, eh) + assert.NoError(t, err) +} + func TestSharesAvailable(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -38,10 +92,13 @@ func TestSharesAvailableFailed(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() + bServ := ipld.NewMemBlockservice() + dah := availability_test.RandFillBS(t, 16, bServ) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) + getter, _ := GetterWithRandSquare(t, 16) avail := TestAvailability(getter) - empty := header.EmptyDAH() - err := avail.SharesAvailable(ctx, &empty) + err := avail.SharesAvailable(ctx, eh) assert.Error(t, err) } @@ -51,10 +108,12 @@ func TestShareAvailableOverMocknet_Light(t *testing.T) { net := availability_test.NewTestDAGNet(ctx, t) _, root := RandNode(net, 16) + eh := headertest.RandExtendedHeader(t) + eh.DAH = root nd := Node(net) net.ConnectAll() - err := nd.SharesAvailable(ctx, root) + err := nd.SharesAvailable(ctx, eh) assert.NoError(t, err) } @@ -63,11 +122,11 @@ func TestGetShare(t *testing.T) { defer cancel() n := 16 - getter, dah := GetterWithRandSquare(t, n) + getter, eh := GetterWithRandSquare(t, n) for i := range make([]bool, n) { for j := range make([]bool, n) { - sh, err := getter.GetShare(ctx, dah, i, j) + sh, err := getter.GetShare(ctx, eh, i, j) assert.NotNil(t, sh) assert.NoError(t, err) } @@ -87,24 +146,26 @@ func TestService_GetSharesByNamespace(t *testing.T) { for _, tt := range tests { t.Run("size: "+strconv.Itoa(tt.squareSize), func(t *testing.T) { getter, bServ := EmptyGetter() - n := tt.squareSize * tt.squareSize - randShares := share.RandShares(t, n) - idx1 := (n - 1) / 2 - idx2 := n / 2 + totalShares := tt.squareSize * tt.squareSize + randShares := sharetest.RandShares(t, totalShares) + idx1 := (totalShares - 1) / 2 + idx2 := totalShares / 2 if tt.expectedShareCount > 1 { - // make it so that two rows have the same namespace ID - copy(randShares[idx2][:8], randShares[idx1][:8]) + // make it so that two rows have the same namespace + copy(share.GetNamespace(randShares[idx2]), share.GetNamespace(randShares[idx1])) } root := availability_test.FillBS(t, bServ, randShares) - randNID := randShares[idx1][:8] + eh := headertest.RandExtendedHeader(t) + eh.DAH = root + randNamespace := share.GetNamespace(randShares[idx1]) - shares, err := getter.GetSharesByNamespace(context.Background(), root, randNID) + shares, err := getter.GetSharesByNamespace(context.Background(), eh, randNamespace) require.NoError(t, err) - require.NoError(t, shares.Verify(root, randNID)) + require.NoError(t, shares.Verify(root, randNamespace)) flattened := shares.Flatten() assert.Len(t, flattened, tt.expectedShareCount) for _, value := range flattened { - assert.Equal(t, randNID, []byte(share.ID(value))) + assert.Equal(t, randNamespace, share.GetNamespace(value)) } if tt.expectedShareCount > 1 { // idx1 is always smaller than idx2 @@ -112,6 +173,23 @@ func TestService_GetSharesByNamespace(t *testing.T) { assert.Equal(t, randShares[idx2], flattened[1]) } }) + t.Run("last two rows of a 4x4 square that have the same namespace have valid NMT proofs", func(t *testing.T) { + squareSize := 4 + totalShares := squareSize * squareSize + getter, bServ := EmptyGetter() + randShares := sharetest.RandShares(t, totalShares) + lastNID := share.GetNamespace(randShares[totalShares-1]) + for i := totalShares / 2; i < totalShares; i++ { + copy(share.GetNamespace(randShares[i]), lastNID) + } + root := availability_test.FillBS(t, bServ, randShares) + eh := headertest.RandExtendedHeader(t) + eh.DAH = root + + shares, err := getter.GetSharesByNamespace(context.Background(), eh, lastNID) + require.NoError(t, err) + require.NoError(t, shares.Verify(root, lastNID)) + }) } } @@ -120,21 +198,23 @@ func TestGetShares(t *testing.T) { defer cancel() n := 16 - getter, dah := GetterWithRandSquare(t, n) + getter, eh := GetterWithRandSquare(t, n) - eds, err := getter.GetEDS(ctx, dah) + eds, err := getter.GetEDS(ctx, eh) + require.NoError(t, err) + gotDAH, err := share.NewRoot(eds) require.NoError(t, err) - gotDAH := da.NewDataAvailabilityHeader(eds) - require.True(t, dah.Equals(&gotDAH)) + require.True(t, eh.DAH.Equals(gotDAH)) } func TestService_GetSharesByNamespaceNotFound(t *testing.T) { - getter, root := GetterWithRandSquare(t, 1) - root.RowsRoots = nil + getter, eh := GetterWithRandSquare(t, 1) + eh.DAH.RowRoots = nil - _, err := getter.GetSharesByNamespace(context.Background(), root, []byte{1, 1, 1, 1, 1, 1, 1, 1}) - assert.ErrorIs(t, err, share.ErrNamespaceNotFound) + emptyShares, err := getter.GetSharesByNamespace(context.Background(), eh, sharetest.RandV0Namespace()) + require.NoError(t, err) + require.Empty(t, emptyShares.Flatten()) } func BenchmarkService_GetSharesByNamespace(b *testing.B) { @@ -149,179 +229,15 @@ func BenchmarkService_GetSharesByNamespace(b *testing.B) { for _, tt := range tests { b.Run(strconv.Itoa(tt.amountShares), func(b *testing.B) { t := &testing.T{} - getter, root := GetterWithRandSquare(t, tt.amountShares) - randNID := root.RowsRoots[(len(root.RowsRoots)-1)/2][:8] - root.RowsRoots[(len(root.RowsRoots) / 2)] = root.RowsRoots[(len(root.RowsRoots)-1)/2] + getter, eh := GetterWithRandSquare(t, tt.amountShares) + root := eh.DAH + randNamespace := root.RowRoots[(len(root.RowRoots)-1)/2][:share.NamespaceSize] + root.RowRoots[(len(root.RowRoots) / 2)] = root.RowRoots[(len(root.RowRoots)-1)/2] b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := getter.GetSharesByNamespace(context.Background(), root, randNID) + _, err := getter.GetSharesByNamespace(context.Background(), eh, randNamespace) require.NoError(t, err) } }) } } - -func TestSharesRoundTrip(t *testing.T) { - getter, store := EmptyGetter() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - var pb tmproto.Block - err := json.Unmarshal([]byte(sampleBlock), &pb) - require.NoError(t, err) - - b, err := core.BlockFromProto(&pb) - require.NoError(t, err) - - namespace, err := hex.DecodeString("00001337BEEF0000") - require.NoError(t, err) - namespaceBefore, err := hex.DecodeString("0000000000000123") - require.NoError(t, err) - namespaceAfter, err := hex.DecodeString("1234000000000123") - require.NoError(t, err) - - type testCase struct { - name string - messages [][]byte - namespaces [][]byte - } - - cases := []testCase{ - { - "original test case", - [][]byte{b.Data.Blobs[0].Data}, - [][]byte{namespace}}, - { - "one short message", - [][]byte{{1, 2, 3, 4}}, - [][]byte{namespace}}, - { - "one short before other namespace", - [][]byte{{1, 2, 3, 4}, {4, 5, 6, 7}}, - [][]byte{namespace, namespaceAfter}, - }, - { - "one short after other namespace", - [][]byte{{1, 2, 3, 4}, {4, 5, 6, 7}}, - [][]byte{namespaceBefore, namespace}, - }, - { - "two short messages", - [][]byte{{1, 2, 3, 4}, {4, 5, 6, 7}}, - [][]byte{namespace, namespace}, - }, - { - "two short messages before other namespace", - [][]byte{{1, 2, 3, 4}, {4, 5, 6, 7}, {7, 8, 9}}, - [][]byte{namespace, namespace, namespaceAfter}, - }, - { - "two short messages after other namespace", - [][]byte{{1, 2, 3, 4}, {4, 5, 6, 7}, {7, 8, 9}}, - [][]byte{namespaceBefore, namespace, namespace}, - }, - } - randBytes := func(n int) []byte { - bs := make([]byte, n) - _, _ = rand.Read(bs) - return bs - } - for i := 128; i < 4192; i += mrand.Intn(256) { - l := strconv.Itoa(i) - cases = append(cases, testCase{ - "one " + l + " bytes message", - [][]byte{randBytes(i)}, - [][]byte{namespace}, - }) - cases = append(cases, testCase{ - "one " + l + " bytes before other namespace", - [][]byte{randBytes(i), randBytes(1 + mrand.Intn(i))}, - [][]byte{namespace, namespaceAfter}, - }) - cases = append(cases, testCase{ - "one " + l + " bytes after other namespace", - [][]byte{randBytes(1 + mrand.Intn(i)), randBytes(i)}, - [][]byte{namespaceBefore, namespace}, - }) - cases = append(cases, testCase{ - "two " + l + " bytes messages", - [][]byte{randBytes(i), randBytes(i)}, - [][]byte{namespace, namespace}, - }) - cases = append(cases, testCase{ - "two " + l + " bytes messages before other namespace", - [][]byte{randBytes(i), randBytes(i), randBytes(1 + mrand.Intn(i))}, - [][]byte{namespace, namespace, namespaceAfter}, - }) - cases = append(cases, testCase{ - "two " + l + " bytes messages after other namespace", - [][]byte{randBytes(1 + mrand.Intn(i)), randBytes(i), randBytes(i)}, - [][]byte{namespaceBefore, namespace, namespace}, - }) - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - // prepare data - b.Data.Blobs = make([]core.Blob, len(tc.messages)) - b.SquareSize = 16 - var msgsInNamespace [][]byte - require.Equal(t, len(tc.namespaces), len(tc.messages)) - for i := range tc.messages { - b.Data.Blobs[i] = core.Blob{NamespaceID: tc.namespaces[i], Data: tc.messages[i]} - if bytes.Equal(tc.namespaces[i], namespace) { - msgsInNamespace = append(msgsInNamespace, tc.messages[i]) - } - } - - // TODO: set useShareIndexes to true. This requires updating the - // transaction data in this test to include share indexes. - shares, err := appshares.Split(b.Data, false) - if err != nil { - t.Fatal(err) - } - - // test round trip using only encoding, without IPLD - { - myShares := make([][]byte, 0) - for _, sh := range shares { - if bytes.Equal(namespace, sh[:8]) { - myShares = append(myShares, sh) - } - } - blobs, err := appshares.ParseBlobs(myShares) - require.NoError(t, err) - assert.Len(t, blobs, len(msgsInNamespace)) - for i := range blobs { - assert.Equal(t, msgsInNamespace[i], blobs[i].Data) - } - } - - // test full round trip - with IPLD + decoding shares - { - extSquare, err := share.AddShares(ctx, appshares.ToBytes(shares), store) - require.NoError(t, err) - - dah := da.NewDataAvailabilityHeader(extSquare) - shares, err := getter.GetSharesByNamespace(ctx, &dah, namespace) - require.NoError(t, err) - require.NoError(t, shares.Verify(&dah, namespace)) - require.NotEmpty(t, shares) - - blobs, err := appshares.ParseBlobs(shares.Flatten()) - require.NoError(t, err) - assert.Len(t, blobs, len(msgsInNamespace)) - for i := range blobs { - assert.Equal(t, namespace, []byte(blobs[i].NamespaceID)) - assert.Equal(t, msgsInNamespace[i], blobs[i].Data) - } - } - }) - } -} - -// this is a sample block -// -//go:embed "testdata/sample-block.json" -var sampleBlock string diff --git a/share/availability/light/testdata/sample-block.json b/share/availability/light/testdata/sample-block.json deleted file mode 100755 index 2bb8d35bfb..0000000000 --- a/share/availability/light/testdata/sample-block.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "header": { - "version": { - "block": 11 - }, - "chain_id": "7sUWGE", - "height": 12, - "time": "2023-01-19T01:41:20.109585974Z", - "last_block_id": { - "hash": "MfZsPUTgVOBRak+3xnKdwWEr2NQ1v095T4BYaWcdYTs=", - "part_set_header": { - "total": 1, - "hash": "/qyxxHNcuOyYBcaGw90PqAa2GdFErMHkkbH66BDI1hI=" - } - }, - "last_commit_hash": "sQXqALyCQ+tHFKOjfQhB3+lQuud0d3C2ohuOgrO6YX4=", - "data_hash": "Pu8CIJqgqCFYkaSvfeS5jvb6+ThXtJkXeRDZPnb1auM=", - "validators_hash": "X7yBLGyFWnZvyakBElaaaqyV0iCSNTINfm+puFxcX/g=", - "next_validators_hash": "X7yBLGyFWnZvyakBElaaaqyV0iCSNTINfm+puFxcX/g=", - "consensus_hash": "BICRvH3cKD93v7+R1zxE2ljD34qcvIZ0Bdi389qtoi8=", - "app_hash": "pqd8GP5/icLOzlysLtq9IupqfLmoxNVySLWNarJssbE=", - "last_results_hash": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", - "evidence_hash": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", - "proposer_address": "oFwwfI48D9wGpICbXtGZNlx6xOI=" - }, - "data": { - "txs": [ - "CpsCCnsKeQoTL2Jsb2IuTXNnUGF5Rm9yQmxvYhJiCi1jb3Ntb3Mxemdwamp4bWZhNm00ZjU5cTJsNjgzOGp1c2RjcmZ4eHc1ajI4ZG0SCF62zWQ6Pw9TGgLBGyIgo5vwRkbYLo3+Xm1IpV8weKHdFYfqkfDyt8a1vfthQQJCAQASWgpOCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohA64OVjUldXAMb1uSZqrBNuYaatsk+CMGyQKNzY2vM5T+EgQKAggBEggQgIDpg7HeFhpAM88quyAFPvk6/4fuiolAJf3SmOLNv471av8uir6yHaJPdsusvOUQ6gK+/gwoksZ2Lytk48k3Gf4F+tcJO06ZBxIBBBoESU5EWA==" - ], - "blobs": [ - { - "namespace_id": "XrbNZDo/D1M=", - "data": "Jh6q4+BLrF+bOReK6omOI45dajuFKbRSc7FQR/jY67EHnUdz67HWrjZRie7EoF37n+OHbhnI4X72BLNdKpXYVy7J5mtlihMznSVU9FyvUDTy2tKmN2Xwc7bqTbSPKscM5j6r9fpWsD1cycNBMe6ZifQm+qu3+XNDSPdOzG1t318ByRuTAn5ttHFVynZfW3FWYVGSWNzaJ1ECzktus14FLIPIv5hvpia+VEWiBerbP0Oij2ckZWDLFIJsLC142JfPi1iGillBGNOR/fJGYfeoT3W+p0LaUF6BLNS/IbnZS+TrivnUu1zuw9lx+HwwaqKsnNWQpvuytihTKMlebPb+Bmk7FaAJchnnVYbmQhAbWC44xIykgTTQnZZpac00KCrBzCOECyt0eIEBClIqHs7z1nKQiMifqjFrTx95fJ/5wr28iLeJl/5zTuirh5XQbcqpJnXsSWvPIY69VsLunr4/MVVLLre0v4uPD/sZTgNR0Q0qMWp0G1ja2yS8H0PtENOkslxjD2QA66fvQRgwgHLcDduAVR39tyRLcjK0ofnyD9EBm/q8NS/+kgcZkz/5nA+1JM2HjNFfuqwd1+l+pi2Y+11DbPJloTZyk18CewdZVhVclREb/0+ih1DDD2N/RAZG2Thkfx0vge4mwV8MSSDxnHDVLABSkjVGArj2JOwkbQeRWXMp94hZl/O9f1lRxqibcMhme/OsoaqxAErCulqbckaRiUjvrBnAyIM5Bpr13i3ZFxHxJTV7NkXEVbgKfeN3F9ewj4CGCT0Kcq/oWdzJ+RjV+fsQa9/RBLUPwdTfwPDH0FjMlBcuvMfU4Kx3LOHNjr+Gs+BoEf5AX0IRJu4C2KpxpFUl8QYQLK2q4+NKC3KHOcp810V+4auinkLFWMkK1sHruqcQ6sFwxiYQUEkmAavTQTEhJaK0fFTNImnAqPXzz8KB4oGcFnXgGBpsP7aMIVfuI+vY5H5p4Stv6Oa4V953KBOZbAMT3Bo5br7wmmeXa5BAWUSAg7xxLxJqTkx0ZC9tm6Z4UImLpL3g/ytnD5b4fdiJwpzbAx/q9zHolTPz9lK/vAt/MqejRqu5YqkQMO3TY5hNLUWUh6Ty3+XZybfS68xNE/hUpz1AhSKsKX9Mp32rQTauXGXT0BveV+IyNC11Am0pUjRX6vcf3CwN+LRTdNkAZLTEJTChFNSh7a3wEsqxJ1jpEgiSiWDfGcR0aDI5Q/9WXFUvpSlJ3KUHx/o+wmF6SpNmjDI3bq+PAe9/wgpWiciepxcytMTOj3p7TNmK9fCstLGvlaLQczrN0iZdQ93ZvFzA0pQjBSo/42johGZSI9Z/07+BEavhxPOT1zOUJcq9t3VFkLsyYM3BmwKRJ8VvV9aHnE+wKK55A3BB67s++mAkbWB0ImMNlybEUgLq4nm/NC86BZrxUIf98cgEEVEOXpLP9bYGxe+/qIJIlGzwkhk7Uwbzk+0S0IFJnuPSPV6CWfoMnWEsqY+zuti/0HQzms2hGeQsIeGG8lL6mqhorg7TAzShrLF5d55TiT2bY4UZJHxHXjY7CbLOxsQBA+/moo4u1l1Kk93CkNtLGMG5yQiTZsfejYIlpZ4fnPM50sxZG5cbm/MLTW9/yL/Xjp5tt4Rle/feK/CvXAg60oxqXQGq9BDxpsi+dRJ3Q8qyaM7fzIhtfHcGTzWxaqGyAD2XobcqdV3D1gOqzslUwu07NbR+JztroAt7hs5sbkPCSOoRC1LbaXEZ6FZu9j8DbJ2/lnaZTXiOjqLDI0cbQCXVul09nRBldILXCN7baPAXqr0RcW7pLiTEtYkmP+3Y1MU0Epz3xDUF/TteROV4C354ta4L5J7JfWBXEJ5JslWXWTbPddDAA9tdCXa55UhMWWRzLUB6pnpMZRMih85AXKAzsLJeuAPrUBUg4nuFmK/NUALe9jVrFuJsPwZB3ILIwwLvd2T4OZf5a9kH19JMEfWm+x6NcoESus83WmczGzOwjgIhe6mVHAuyzN1cTBoEg7PCRhOyhplRheGC6XqpfA2xvio0+y2WxVOrVvyrqGMTeocqB0NcrMtyZQSZTYteGHYD01uVWe8vbpr1ARpF+Diy9J7Cuud9OeRTtSiUU4LgRVkbVkgtcH24zYUrC8uBkkirOTD8HiEQVUbKm7UO/BXhZ3SGtv6kYISRdiQo0BSdQxwKmThGdF2zdj7OhdAzSXjY2RtrWMAsivd91PBLnDcQGGr+r9Va9CvkBmeB6VmdZ8IF/DUwrD87JLukz2KsTDJM6wVkq5bYYuWhKxloauwr0lvYAtLMQov78sAZm1I89py8GH3cZbja8ctZ0Wz0eaO0ziHBEy3DM0bVrfBwtpY1pyadwgfLVxRkmqMgTR3zaVVQdOkP0CwuR3FdNJJqNda5UZD72boY6vhJdZie4TXf06LBIt5Ptxjh5MxYJBn8hS2HONaMNxofHfb9O+zRYpWznpLSq41F91llLGteBequJCWjw8Skdsztfu/Mvcu+KeliY6VZ4fcT/xST73f7N0qV5ZvBIZ6v2wpf4pJrklyHVCdcdKG3jUUJ5DQNiDzQCA5Ocmjj5k7OTRrE7zBwcfFtVQcOFEncyzPC4c9xAANaeDNz5WNUcPTESvqRpzZxgETo4smZsJMTwCmGoyTm4qHZ3T92y08smZuZO3JuMqtyxRGM2IC3wsFQqrChHcVY7ND0SMc7X5LWVc2j7UwQ6UbnBd6grVfgd/bfHzvPGm31R0uF3//xPGs0iE3f/FVsJE0dScmS0iXx8c8bwamThzD9YBRCNJge7nJIF+eL1Kf+pqgwzfKX6eb8eUIXP2+EJNvGUBhr/BImeULf1edrtqjYWCDQWYn3ISLpKZcv0EVFU/Clv1EkU1wXmGEeTY9xOh3wevSwUAxJ0At5J4qjRFAlFuFD8dqb4PoxeIePaIEpI2SWn9hgnhtTeCWPcMK4HCqdUSUvJJ4ilc5378uG4WrO/ulxicPYAWrocsqlRHmNit0gKD32YwlRjq5W1m1eunr0SJc299kjFm+Jhg6lQsPcw8BhMnTipse3K5n6uiwa8cwpeHN6mraR+ZwFmZhCNTHsaYk2hbnFGCH4h6W7RT9QH01Xq0F8N130oegxOEsI4GWaJS5KrBa8SqCHtSI2O/4eISdv98tkwx84h50hCU4kQ2o3mEmrTYCfmY80MtjZdE+vX+iXKlmX1nZxPxxBb/loU/mKcXPvapeBx3hkrxjnFBKDDgg8NEDj8IU3SRSewVXGBivKbyk/zUZl+Ow49+636IzQNtH0RSac1ox5FqVOxidaQ04si5lCROJaLuuhfo1CqgYmN502KL3u6gLQdk9pvwoPOJ7iYEx63VwBy8205lHB38o7bO2FmzaSpnVsCOxcK32ExSY92l659n+Wrxjj0dSqDj8VaKiGnJn9FKRNZLGMzONuXgCLikM7ChiGHRO1AjnJmHnunsQcbYdcHhyPlRim19oX0JhmDxrcqhAZbD0a3PAL/MBs671MDQ4GL0blXIYBzSAxZgyEl4Zn8FMC4xeOeMChLJ/aaKmhJx8k+S6uUCQCXuHktXLGq0COjEIx5cuvYmS+hVgqWv+UrHj7VkYbLPaufFfKe1neR7pG8SK4nRSwa6KB5IXUfBATvT7PU4jiXEo2JPuDQUIh7v1uloDjOm9OdMLZc7Kfcz6RI0yTzlmCTjWXRgp9WzztZsldb1obgW7t2XMEgKBR8OOVKUKbeqo1nWUMKp4Tz5jQnos0ixufGJ5iac57rpvATGkIAPhIMNTj35dRInpn4qI21TpPpOTYRnJVL4e0l6JjPTvy+JzS6Qec9JqJy0HWEmQmyw9/+vCGWR50tFyfhtPZzW+OvpbSs+sF5Xd4J//SWL5Y1iC3XhnNa6Uc2buY3LyLGZxHPdjCJCZ/I+7gdz5xUNnvzFtfUoLM5qoRsyDzJmdfL3fFHq5eDA1MkDqSypkixT3mkLnNtN5nnGIsa/2wKLoBSGEwCjsQGLesVYRibJ236rI78yCslTqfgrEaVlBnIPFWKdnl9YlR12G3WlKjplqbCo3CMSUcfVOR+f1CLM+IOclEwDszPmvwuJc/p8sh+Xhsbu9bOTuYo7S4HV73+M6Ct6Y4WuJEw/xmTxlNjhMOJi/lZvygzYFT8vC2S2FA5HLthkQkwE+xMFKBS4i0/hq8Mwi/bqLShkD+N0nCbo+jT9SlN8rkEhTzSgu3aaStd33N0/56OYee4oscdGOD5Io7RH636aMQpq80X9irkb7E4jTAK3aGQJMGfwz8BtOJ9rDJ4yv3uJHmwlQhzIwwQENPOnOFipkbWBpalmnYr/WoxgoGwR/ZOHXtA1J/beDHtjLB6Y3P5KO5ud7LeX2QGu3b7BAc19UeW+XfED1chzUyz1O65149H+NQaw0vVajpwYaRuAkZ+6B1W5pLP+5k5Gyc6TTmQR+4EucAy1R9dh6p29S2pBNWvHzaxKK1i/R32wjxBRr6M1X2Ym+k4DyZIeFnqH0a/UKqStyP31Z/qNCa//Nbcx2XxM9RgUJk4LH2mZRmaKUkesxwmQ+WB+dNohq+7uxDmTOyjcKOClcgcaSlv62htt0RiqkYSW2+qji9My4fHMQpPtKXWkbWwhXmg7ZgtB3Hx2ogyV5HVR5gLJ6Ydaq5aYQVLEVCYdr0pVmfzgHlDCZwj36S0EFZaMGWB8vIfWgfjop+VE4=" - } - ], - "square_size": 4, - "hash": "Pu8CIJqgqCFYkaSvfeS5jvb6+ThXtJkXeRDZPnb1auM=" - }, - "evidence": { - "evidence": [] - }, - "last_commit": { - "height": 11, - "block_id": { - "hash": "MfZsPUTgVOBRak+3xnKdwWEr2NQ1v095T4BYaWcdYTs=", - "part_set_header": { - "total": 1, - "hash": "/qyxxHNcuOyYBcaGw90PqAa2GdFErMHkkbH66BDI1hI=" - } - }, - "signatures": [ - { - "block_id_flag": 2, - "validator_address": "oFwwfI48D9wGpICbXtGZNlx6xOI=", - "timestamp": "2023-01-19T01:41:20.109585974Z", - "signature": "mPRPdLbp8cQcZ2w6CmWlmE+4lVzX9YgxzxOBp7lYTT8jYmehJC62sC/Nt/X/ezVW7u9OCXZjGG2oIDYmabKsAA==" - } - ] - } -} \ No newline at end of file diff --git a/share/availability/light/testing.go b/share/availability/light/testing.go index 59163d6356..9efc9ff14a 100644 --- a/share/availability/light/testing.go +++ b/share/availability/light/testing.go @@ -3,26 +3,33 @@ package light import ( "testing" - "github.com/ipfs/go-blockservice" - mdutils "github.com/ipfs/go-merkledag/test" + "github.com/ipfs/boxo/blockservice" + "github.com/ipfs/go-datastore" + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" availability_test "github.com/celestiaorg/celestia-node/share/availability/test" "github.com/celestiaorg/celestia-node/share/getters" + "github.com/celestiaorg/celestia-node/share/ipld" ) // GetterWithRandSquare provides a share.Getter filled with 'n' NMT trees of 'n' random shares, // essentially storing a whole square. -func GetterWithRandSquare(t *testing.T, n int) (share.Getter, *share.Root) { - bServ := mdutils.Bserv() +func GetterWithRandSquare(t *testing.T, n int) (share.Getter, *header.ExtendedHeader) { + bServ := ipld.NewMemBlockservice() getter := getters.NewIPLDGetter(bServ) - return getter, availability_test.RandFillBS(t, n, bServ) + root := availability_test.RandFillBS(t, n, bServ) + eh := headertest.RandExtendedHeader(t) + eh.DAH = root + + return getter, eh } // EmptyGetter provides an unfilled share.Getter with corresponding blockservice.BlockService than // can be filled by the test. func EmptyGetter() (share.Getter, blockservice.BlockService) { - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() getter := getters.NewIPLDGetter(bServ) return getter, bServ } @@ -42,7 +49,8 @@ func Node(dn *availability_test.TestDagNet) *availability_test.TestNode { } func TestAvailability(getter share.Getter) *ShareAvailability { - return NewShareAvailability(getter) + ds := datastore.NewMapDatastore() + return NewShareAvailability(getter, ds) } func SubNetNode(sn *availability_test.SubNet) *availability_test.TestNode { diff --git a/share/availability/mocks/availability.go b/share/availability/mocks/availability.go index ff4b8e1328..fc68d3d2bc 100644 --- a/share/availability/mocks/availability.go +++ b/share/availability/mocks/availability.go @@ -8,9 +8,8 @@ import ( context "context" reflect "reflect" + header "github.com/celestiaorg/celestia-node/header" gomock "github.com/golang/mock/gomock" - - da "github.com/celestiaorg/celestia-app/pkg/da" ) // MockAvailability is a mock of Availability interface. @@ -36,22 +35,8 @@ func (m *MockAvailability) EXPECT() *MockAvailabilityMockRecorder { return m.recorder } -// ProbabilityOfAvailability mocks base method. -func (m *MockAvailability) ProbabilityOfAvailability(arg0 context.Context) float64 { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ProbabilityOfAvailability", arg0) - ret0, _ := ret[0].(float64) - return ret0 -} - -// ProbabilityOfAvailability indicates an expected call of ProbabilityOfAvailability. -func (mr *MockAvailabilityMockRecorder) ProbabilityOfAvailability(arg0 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProbabilityOfAvailability", reflect.TypeOf((*MockAvailability)(nil).ProbabilityOfAvailability), arg0) -} - // SharesAvailable mocks base method. -func (m *MockAvailability) SharesAvailable(arg0 context.Context, arg1 *da.DataAvailabilityHeader) error { +func (m *MockAvailability) SharesAvailable(arg0 context.Context, arg1 *header.ExtendedHeader) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "SharesAvailable", arg0, arg1) ret0, _ := ret[0].(error) diff --git a/share/availability/test/corrupt_data.go b/share/availability/test/corrupt_data.go index 1bb8bd243a..1ff553f8b3 100644 --- a/share/availability/test/corrupt_data.go +++ b/share/availability/test/corrupt_data.go @@ -7,11 +7,11 @@ import ( mrand "math/rand" "testing" + "github.com/ipfs/boxo/blockstore" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" ds "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" - blockstore "github.com/ipfs/go-ipfs-blockstore" - blocks "github.com/ipfs/go-libipfs/blocks" ) var _ blockstore.Blockstore = (*FraudulentBlockstore)(nil) @@ -63,7 +63,7 @@ func (fb FraudulentBlockstore) Has(context.Context, cid.Cid) (bool, error) { func (fb FraudulentBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) { key := cid.String() if fb.Attacking { - key = "corrupt" + key + key = "corrupt_get" + key } data, err := fb.Datastore.Get(ctx, ds.NewKey(key)) @@ -76,7 +76,7 @@ func (fb FraudulentBlockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Blo func (fb FraudulentBlockstore) GetSize(ctx context.Context, cid cid.Cid) (int, error) { key := cid.String() if fb.Attacking { - key = "corrupt" + key + key = "corrupt_size" + key } return fb.Datastore.GetSize(ctx, ds.NewKey(key)) diff --git a/share/availability/test/testing.go b/share/availability/test/testing.go index b2fb780bd4..64e8d23bb7 100644 --- a/share/availability/test/testing.go +++ b/share/availability/test/testing.go @@ -4,36 +4,37 @@ import ( "context" "testing" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/bitswap" + "github.com/ipfs/boxo/bitswap/network" + "github.com/ipfs/boxo/blockservice" + "github.com/ipfs/boxo/blockstore" + "github.com/ipfs/boxo/routing/offline" ds "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" - blockstore "github.com/ipfs/go-ipfs-blockstore" - "github.com/ipfs/go-ipfs-routing/offline" - "github.com/ipfs/go-libipfs/bitswap" - "github.com/ipfs/go-libipfs/bitswap/network" record "github.com/libp2p/go-libp2p-record" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/stretchr/testify/require" - "github.com/celestiaorg/celestia-app/pkg/da" - "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share/sharetest" ) // RandFillBS fills the given BlockService with a random block of a given size. func RandFillBS(t *testing.T, n int, bServ blockservice.BlockService) *share.Root { - shares := share.RandShares(t, n*n) + shares := sharetest.RandShares(t, n*n) return FillBS(t, bServ, shares) } // FillBS fills the given BlockService with the given shares. func FillBS(t *testing.T, bServ blockservice.BlockService, shares []share.Share) *share.Root { - eds, err := share.AddShares(context.TODO(), shares, bServ) + eds, err := ipld.AddShares(context.TODO(), shares, bServ) + require.NoError(t, err) + dah, err := share.NewRoot(eds) require.NoError(t, err) - dah := da.NewDataAvailabilityHeader(eds) - return &dah + return dah } type TestNode struct { @@ -91,7 +92,7 @@ func (dn *TestDagNet) NewTestNodeWithBlockstore(dstore ds.Datastore, bstore bloc ) nd := &TestNode{ net: dn, - BlockService: blockservice.New(bstore, bs), + BlockService: ipld.NewBlockservice(bstore, bs), Host: hst, } dn.nodes = append(dn.nodes, nd) diff --git a/share/doc.go b/share/doc.go index 7ef8c9f7bf..97229932a7 100644 --- a/share/doc.go +++ b/share/doc.go @@ -4,7 +4,7 @@ block data. Though this package contains several useful methods for getting specific shares and/or sampling them at random, a particularly useful method is GetSharesByNamespace which retrieves -all shares of block data of the given namespace.ID from the block associated with the given +all shares of block data of the given Namespace from the block associated with the given DataAvailabilityHeader (DAH, but referred to as Root within this package). This package also contains declaration of the Availability interface. Implementations of diff --git a/share/eds/accessor_cache.go b/share/eds/accessor_cache.go deleted file mode 100644 index eac79b946f..0000000000 --- a/share/eds/accessor_cache.go +++ /dev/null @@ -1,119 +0,0 @@ -package eds - -import ( - "errors" - "fmt" - "reflect" - "sync" - - "github.com/filecoin-project/dagstore" - "github.com/filecoin-project/dagstore/shard" - lru "github.com/hashicorp/golang-lru" -) - -var ( - defaultCacheSize = 128 - errCacheMiss = errors.New("accessor not found in blockstore cache") -) - -// accessorWithBlockstore is the value that we store in the blockstore cache -type accessorWithBlockstore struct { - sa *dagstore.ShardAccessor - // blockstore is stored separately because each access to the blockstore over the shard accessor - // reopens the underlying CAR. - bs dagstore.ReadBlockstore -} - -type blockstoreCache struct { - // stripedLocks prevents simultaneous RW access to the blockstore cache for a shard. Instead - // of using only one lock or one lock per key, we stripe the shard keys across 256 locks. 256 is - // chosen because it 0-255 is the range of values we get looking at the last byte of the key. - stripedLocks [256]sync.Mutex - // caches the blockstore for a given shard for shard read affinity i.e. - // further reads will likely be from the same shard. Maps (shard key -> blockstore). - cache *lru.Cache -} - -func newBlockstoreCache(cacheSize int) (*blockstoreCache, error) { - // instantiate the blockstore cache - bslru, err := lru.NewWithEvict(cacheSize, func(_ interface{}, val interface{}) { - // ensure we close the blockstore for a shard when it's evicted so dagstore can gc it. - abs, ok := val.(*accessorWithBlockstore) - if !ok { - panic(fmt.Sprintf( - "casting value from cache to accessorWithBlockstore: %s", - reflect.TypeOf(val), - )) - } - - if err := abs.sa.Close(); err != nil { - log.Errorf("couldn't close accessor after cache eviction: %s", err) - } - }) - if err != nil { - return nil, fmt.Errorf("failed to instantiate blockstore cache: %w", err) - } - return &blockstoreCache{cache: bslru}, nil -} - -// Get retrieves the blockstore for a given shard key from the cache. If the blockstore is not in -// the cache, it returns an errCacheMiss -func (bc *blockstoreCache) Get(shardContainingCid shard.Key) (*accessorWithBlockstore, error) { - lk := &bc.stripedLocks[shardKeyToStriped(shardContainingCid)] - lk.Lock() - defer lk.Unlock() - - return bc.unsafeGet(shardContainingCid) -} - -func (bc *blockstoreCache) unsafeGet(shardContainingCid shard.Key) (*accessorWithBlockstore, error) { - // We've already ensured that the given shard has the cid/multihash we are looking for. - val, ok := bc.cache.Get(shardContainingCid) - if !ok { - return nil, errCacheMiss - } - - accessor, ok := val.(*accessorWithBlockstore) - if !ok { - panic(fmt.Sprintf( - "casting value from cache to accessorWithBlockstore: %s", - reflect.TypeOf(val), - )) - } - return accessor, nil -} - -// Add adds a blockstore for a given shard key to the cache. -func (bc *blockstoreCache) Add( - shardContainingCid shard.Key, - accessor *dagstore.ShardAccessor, -) (*accessorWithBlockstore, error) { - lk := &bc.stripedLocks[shardKeyToStriped(shardContainingCid)] - lk.Lock() - defer lk.Unlock() - - return bc.unsafeAdd(shardContainingCid, accessor) -} - -func (bc *blockstoreCache) unsafeAdd( - shardContainingCid shard.Key, - accessor *dagstore.ShardAccessor, -) (*accessorWithBlockstore, error) { - blockStore, err := accessor.Blockstore() - if err != nil { - return nil, fmt.Errorf("failed to get blockstore from accessor: %w", err) - } - - newAccessor := &accessorWithBlockstore{ - bs: blockStore, - sa: accessor, - } - bc.cache.Add(shardContainingCid, newAccessor) - return newAccessor, nil -} - -// shardKeyToStriped returns the index of the lock to use for a given shard key. We use the last -// byte of the shard key as the pseudo-random index. -func shardKeyToStriped(sk shard.Key) byte { - return sk.String()[len(sk.String())-1] -} diff --git a/share/eds/adapters.go b/share/eds/adapters.go index 8d98f092d7..8bf2340d91 100644 --- a/share/eds/adapters.go +++ b/share/eds/adapters.go @@ -5,9 +5,9 @@ import ( "sync" "github.com/filecoin-project/dagstore" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" - "github.com/ipfs/go-libipfs/blocks" ) var _ blockservice.BlockGetter = (*BlockGetter)(nil) diff --git a/share/eds/adapters_test.go b/share/eds/adapters_test.go index 6ccd29e99d..70165b81c8 100644 --- a/share/eds/adapters_test.go +++ b/share/eds/adapters_test.go @@ -8,8 +8,8 @@ import ( "testing" "time" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" - "github.com/ipfs/go-libipfs/blocks" "github.com/stretchr/testify/require" "github.com/celestiaorg/celestia-node/share/ipld" diff --git a/share/eds/blockstore.go b/share/eds/blockstore.go index 3c63fa3945..e44601870e 100644 --- a/share/eds/blockstore.go +++ b/share/eds/blockstore.go @@ -5,17 +5,19 @@ import ( "errors" "fmt" - "github.com/filecoin-project/dagstore" + bstore "github.com/ipfs/boxo/blockstore" + "github.com/ipfs/boxo/datastore/dshelp" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" "github.com/ipfs/go-datastore" - bstore "github.com/ipfs/go-ipfs-blockstore" + "github.com/ipfs/go-datastore/namespace" ipld "github.com/ipfs/go-ipld-format" - blocks "github.com/ipfs/go-libipfs/blocks" ) var _ bstore.Blockstore = (*blockstore)(nil) var ( + blockstoreCacheKey = datastore.NewKey("bs-cache") errUnsupportedOperation = errors.New("unsupported operation") ) @@ -29,73 +31,104 @@ var ( // implementation to allow for the blockstore operations to be routed to the underlying stores. type blockstore struct { store *Store - cache *blockstoreCache + ds datastore.Batching } -func newBlockstore(store *Store, cache *blockstoreCache) *blockstore { +func newBlockstore(store *Store, ds datastore.Batching) *blockstore { return &blockstore{ store: store, - cache: cache, + ds: namespace.Wrap(ds, blockstoreCacheKey), } } func (bs *blockstore) Has(ctx context.Context, cid cid.Cid) (bool, error) { keys, err := bs.store.dgstr.ShardsContainingMultihash(ctx, cid.Hash()) - if errors.Is(err, ErrNotFound) { - return false, nil + if errors.Is(err, ErrNotFound) || errors.Is(err, ErrNotFoundInIndex) { + // key wasn't found in top level blockstore, but could be in datastore while being reconstructed + dsHas, dsErr := bs.ds.Has(ctx, dshelp.MultihashToDsKey(cid.Hash())) + if dsErr != nil { + return false, nil + } + return dsHas, nil } if err != nil { - return false, fmt.Errorf("failed to find shards containing multihash: %w", err) + return false, err } + return len(keys) > 0, nil } func (bs *blockstore) Get(ctx context.Context, cid cid.Cid) (blocks.Block, error) { blockstr, err := bs.getReadOnlyBlockstore(ctx, cid) - if errors.Is(err, ErrNotFound) { + if err == nil { + defer closeAndLog("blockstore", blockstr) + return blockstr.Get(ctx, cid) + } + + if errors.Is(err, ErrNotFound) || errors.Is(err, ErrNotFoundInIndex) { + k := dshelp.MultihashToDsKey(cid.Hash()) + blockData, err := bs.ds.Get(ctx, k) + if err == nil { + return blocks.NewBlockWithCid(blockData, cid) + } // nmt's GetNode expects an ipld.ErrNotFound when a cid is not found. return nil, ipld.ErrNotFound{Cid: cid} } - if err != nil { - log.Debugf("failed to get blockstore for cid %s: %s", cid, err) - return nil, err - } - return blockstr.Get(ctx, cid) + + log.Debugf("failed to get blockstore for cid %s: %s", cid, err) + return nil, err } func (bs *blockstore) GetSize(ctx context.Context, cid cid.Cid) (int, error) { blockstr, err := bs.getReadOnlyBlockstore(ctx, cid) - if errors.Is(err, ErrNotFound) { + if err == nil { + defer closeAndLog("blockstore", blockstr) + return blockstr.GetSize(ctx, cid) + } + + if errors.Is(err, ErrNotFound) || errors.Is(err, ErrNotFoundInIndex) { + k := dshelp.MultihashToDsKey(cid.Hash()) + size, err := bs.ds.GetSize(ctx, k) + if err == nil { + return size, nil + } // nmt's GetSize expects an ipld.ErrNotFound when a cid is not found. return 0, ipld.ErrNotFound{Cid: cid} } - if err != nil { - return 0, err - } - return blockstr.GetSize(ctx, cid) + + log.Debugf("failed to get size for cid %s: %s", cid, err) + return 0, err } -// DeleteBlock is a noop on the EDS blockstore that returns an errUnsupportedOperation when called. -func (bs *blockstore) DeleteBlock(context.Context, cid.Cid) error { - return errUnsupportedOperation +func (bs *blockstore) DeleteBlock(ctx context.Context, cid cid.Cid) error { + k := dshelp.MultihashToDsKey(cid.Hash()) + return bs.ds.Delete(ctx, k) } -// Put is a noop on the EDS blockstore, but it does not return an error because it is called by -// bitswap. For clarification, an implementation of Put does not make sense in this context because -// it is unclear which CAR file the block should be written to. -// -// TODO: throw errUnsupportedOperation after issue #1440 -func (bs *blockstore) Put(context.Context, blocks.Block) error { - return nil +func (bs *blockstore) Put(ctx context.Context, blk blocks.Block) error { + k := dshelp.MultihashToDsKey(blk.Cid().Hash()) + // note: we leave duplicate resolution to the underlying datastore + return bs.ds.Put(ctx, k, blk.RawData()) } -// PutMany is a noop on the EDS blockstore, but it does not return an error because it is called by -// bitswap. For clarification, an implementation of PutMany does not make sense in this context -// because it is unclear which CAR file the blocks should be written to. -// -// TODO: throw errUnsupportedOperation after issue #1440 -func (bs *blockstore) PutMany(context.Context, []blocks.Block) error { - return nil +func (bs *blockstore) PutMany(ctx context.Context, blocks []blocks.Block) error { + if len(blocks) == 1 { + // performance fast-path + return bs.Put(ctx, blocks[0]) + } + + t, err := bs.ds.Batch(ctx) + if err != nil { + return err + } + for _, b := range blocks { + k := dshelp.MultihashToDsKey(b.Cid().Hash()) + err = t.Put(ctx, k, b.RawData()) + if err != nil { + return err + } + } + return t.Commit(ctx) } // AllKeysChan is a noop on the EDS blockstore because the keys are not stored in a single CAR file. @@ -110,20 +143,26 @@ func (bs *blockstore) HashOnRead(bool) { } // getReadOnlyBlockstore finds the underlying blockstore of the shard that contains the given CID. -func (bs *blockstore) getReadOnlyBlockstore(ctx context.Context, cid cid.Cid) (dagstore.ReadBlockstore, error) { +func (bs *blockstore) getReadOnlyBlockstore(ctx context.Context, cid cid.Cid) (*BlockstoreCloser, error) { keys, err := bs.store.dgstr.ShardsContainingMultihash(ctx, cid.Hash()) - if errors.Is(err, datastore.ErrNotFound) { + if errors.Is(err, datastore.ErrNotFound) || errors.Is(err, ErrNotFoundInIndex) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("failed to find shards containing multihash: %w", err) } - // a share can exist in multiple EDSes, so just take the first one. + // check if either cache contains an accessor shardKey := keys[0] - accessor, err := bs.store.getCachedAccessor(ctx, shardKey) + accessor, err := bs.store.cache.Load().Get(shardKey) + if err == nil { + return blockstoreCloser(accessor) + } + + // load accessor to the blockstore cache and use it as blockstoreCloser + accessor, err = bs.store.cache.Load().Second().GetOrLoad(ctx, shardKey, bs.store.getAccessor) if err != nil { return nil, fmt.Errorf("failed to get accessor for shard %s: %w", shardKey, err) } - return accessor.bs, nil + return blockstoreCloser(accessor) } diff --git a/share/eds/blockstore_test.go b/share/eds/blockstore_test.go index 745797bf42..d9dbf7ed30 100644 --- a/share/eds/blockstore_test.go +++ b/share/eds/blockstore_test.go @@ -37,6 +37,9 @@ func TestBlockstore_Operations(t *testing.T) { topLevelBS := edsStore.Blockstore() carBS, err := edsStore.CARBlockstore(ctx, dah.Hash()) require.NoError(t, err) + defer func() { + require.NoError(t, carBS.Close()) + }() root, err := edsStore.GetDAH(ctx, dah.Hash()) require.NoError(t, err) diff --git a/share/eds/byzantine/bad_encoding.go b/share/eds/byzantine/bad_encoding.go index 9ca481d552..fbb6b592ea 100644 --- a/share/eds/byzantine/bad_encoding.go +++ b/share/eds/byzantine/bad_encoding.go @@ -5,24 +5,21 @@ import ( "errors" "fmt" - "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/pkg/wrapper" "github.com/celestiaorg/go-fraud" - libhead "github.com/celestiaorg/go-header" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/share" pb "github.com/celestiaorg/celestia-node/share/eds/byzantine/pb" "github.com/celestiaorg/celestia-node/share/ipld" ) const ( - BadEncoding fraud.ProofType = "badencoding" -) + version = "v0.1" -func init() { - fraud.Register(&BadEncodingProof{}) -} + BadEncoding fraud.ProofType = "badencoding" + version +) type BadEncodingProof struct { headerHash []byte @@ -44,8 +41,7 @@ func CreateBadEncodingProof( hash []byte, height uint64, errByzantine *ErrByzantine, -) fraud.Proof { - +) fraud.Proof[*header.ExtendedHeader] { return &BadEncodingProof{ headerHash: hash, BlockHeight: height, @@ -106,79 +102,153 @@ func (p *BadEncodingProof) UnmarshalBinary(data []byte) error { return nil } +var ( + errHeightMismatch = errors.New("height reported in proof does not match with the header's height") + errIncorrectIndex = errors.New("row/col index is more then the roots amount") + errIncorrectAmountOfShares = errors.New("incorrect amount of shares") + errIncorrectShare = errors.New("incorrect share received") + errNMTTreeRootsMatch = errors.New("recomputed root matches the DAH root") +) + +var ( + invalidProofPrefix = fmt.Sprintf("invalid %s proof", BadEncoding) +) + // Validate ensures that fraud proof is correct. // Validate checks that provided Merkle Proofs correspond to the shares, // rebuilds bad row or col from received shares, computes Merkle Root // and compares it with block's Merkle Root. -func (p *BadEncodingProof) Validate(hdr libhead.Header) error { - header, ok := hdr.(*header.ExtendedHeader) - if !ok { - panic(fmt.Sprintf("invalid header type: expected %T, got %T", header, hdr)) - } - if header.Height() != int64(p.BlockHeight) { - return errors.New("fraud: incorrect block height") +func (p *BadEncodingProof) Validate(hdr *header.ExtendedHeader) error { + if hdr.Height() != p.BlockHeight { + log.Debugf("%s: %s. expected block's height: %d, got: %d", + invalidProofPrefix, + errHeightMismatch, + hdr.Height(), + p.BlockHeight, + ) + return errHeightMismatch } - merkleRowRoots := header.DAH.RowsRoots - merkleColRoots := header.DAH.ColumnRoots - if len(merkleRowRoots) != len(merkleColRoots) { + + if len(hdr.DAH.RowRoots) != len(hdr.DAH.ColumnRoots) { // NOTE: This should never happen as callers of this method should not feed it with a // malformed extended header. panic(fmt.Sprintf( - "fraud: invalid extended header: length of row and column roots do not match. (rowRoots=%d) (colRoots=%d)", - len(merkleRowRoots), - len(merkleColRoots)), + "invalid extended header: length of row and column roots do not match. (rowRoots=%d) (colRoots=%d)", + len(hdr.DAH.RowRoots), + len(hdr.DAH.ColumnRoots)), ) } - if int(p.Index) >= len(merkleRowRoots) { - return fmt.Errorf("fraud: invalid proof: index out of bounds (%d >= %d)", int(p.Index), len(merkleRowRoots)) + + // merkleRoots are the roots against which we are going to check the inclusion of the received + // shares. Changing the order of the roots to prove the shares relative to the orthogonal axis, + // because inside the rsmt2d library rsmt2d.Row = 0 and rsmt2d.Col = 1 + merkleRoots := hdr.DAH.RowRoots + if p.Axis == rsmt2d.Row { + merkleRoots = hdr.DAH.ColumnRoots } - if len(merkleRowRoots) != len(p.Shares) { - return fmt.Errorf("fraud: invalid proof: incorrect number of shares %d != %d", len(p.Shares), len(merkleRowRoots)) + + if int(p.Index) >= len(merkleRoots) { + log.Debugf("%s:%s (%d >= %d)", + invalidProofPrefix, errIncorrectIndex, int(p.Index), len(merkleRoots), + ) + return errIncorrectIndex } - root := merkleRowRoots[p.Index] - if p.Axis == rsmt2d.Col { - root = merkleColRoots[p.Index] + if len(p.Shares) != len(merkleRoots) { + // Since p.Shares should contain all the shares from either a row or a + // column, it should exactly match the number of row roots. In this + // context, the number of row roots is the width of the extended data + // square. + log.Infof("%s: %s (%d >= %d)", + invalidProofPrefix, errIncorrectAmountOfShares, int(p.Index), len(merkleRoots), + ) + return errIncorrectAmountOfShares } - // verify that Merkle proofs correspond to particular shares. - shares := make([][]byte, len(merkleRowRoots)) - for index, share := range p.Shares { + odsWidth := uint64(len(merkleRoots) / 2) + amount := uint64(0) + for _, share := range p.Shares { if share == nil { continue } + amount++ + if amount == odsWidth { + break + } + } + + if amount < odsWidth { + log.Debugf("%s: %s. not enough shares provided to reconstruct row/col", + invalidProofPrefix, errIncorrectAmountOfShares) + return errIncorrectAmountOfShares + } + + // verify that Merkle proofs correspond to particular shares. + shares := make([][]byte, len(merkleRoots)) + for index, shr := range p.Shares { + if shr == nil { + continue + } // validate inclusion of the share into one of the DAHeader roots - if ok := share.Validate(ipld.MustCidFromNamespacedSha256(root)); !ok { - return fmt.Errorf("fraud: invalid proof: incorrect share received at index %d", index) + if ok := shr.Validate(ipld.MustCidFromNamespacedSha256(merkleRoots[index])); !ok { + log.Debugf("%s: %s at index %d", invalidProofPrefix, errIncorrectShare, index) + return errIncorrectShare } // NMTree commits the additional namespace while rsmt2d does not know about, so we trim it // this is ugliness from NMTWrapper that we have to embrace ¯\_(ツ)_/¯ - shares[index] = share.Share[ipld.NamespaceSize:] + shares[index] = share.GetData(shr.Share) } - odsWidth := uint64(len(merkleRowRoots) / 2) - codec := appconsts.DefaultCodec() + codec := share.DefaultRSMT2DCodec() - // rebuild a row or col. + // We can conclude that the proof is valid in case we proved the inclusion of `Shares` but + // the row/col can't be reconstructed, or the building of NMTree fails. rebuiltShares, err := codec.Decode(shares) if err != nil { - return err + log.Debugw("failed to decode shares at height", + "height", hdr.Height(), "err", err, + ) + return nil } + rebuiltExtendedShares, err := codec.Encode(rebuiltShares[0:odsWidth]) if err != nil { - return err + log.Debugw("failed to encode shares at height", + "height", hdr.Height(), "err", err, + ) + return nil } copy(rebuiltShares[odsWidth:], rebuiltExtendedShares) tree := wrapper.NewErasuredNamespacedMerkleTree(odsWidth, uint(p.Index)) for _, share := range rebuiltShares { - tree.Push(share) + err = tree.Push(share) + if err != nil { + log.Debugw("failed to build a tree from the reconstructed shares at height", + "height", hdr.Height(), "err", err, + ) + return nil + } } - // comparing rebuilt Merkle Root of bad row/col with respective Merkle Root of row/col from block. - if bytes.Equal(tree.Root(), root) { - return errors.New("fraud: invalid proof: recomputed Merkle root matches the DAH's row/column root") + expectedRoot, err := tree.Root() + if err != nil { + log.Debugw("failed to build a tree root at height", + "height", hdr.Height(), "err", err, + ) + return nil } + // root is a merkle root of the row/col where ErrByzantine occurred + root := hdr.DAH.RowRoots[p.Index] + if p.Axis == rsmt2d.Col { + root = hdr.DAH.ColumnRoots[p.Index] + } + + // comparing rebuilt Merkle Root of bad row/col with respective Merkle Root of row/col from block. + if bytes.Equal(expectedRoot, root) { + log.Debugf("invalid %s proof:%s", BadEncoding, errNMTTreeRootsMatch) + return errNMTTreeRootsMatch + } return nil } diff --git a/share/eds/byzantine/bad_encoding_test.go b/share/eds/byzantine/bad_encoding_test.go index 6604ea1e17..e42e3c287c 100644 --- a/share/eds/byzantine/bad_encoding_test.go +++ b/share/eds/byzantine/bad_encoding_test.go @@ -2,39 +2,178 @@ package byzantine import ( "context" + "crypto/sha256" + "hash" "testing" + "time" - mdutils "github.com/ipfs/go-merkledag/test" + "github.com/ipfs/boxo/blockservice" + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + mhcore "github.com/multiformats/go-multihash/core" "github.com/stretchr/testify/require" core "github.com/tendermint/tendermint/types" "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/celestia-app/test/util/malicious" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/edstest" "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share/sharetest" ) +func TestBEFP_Validate(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer t.Cleanup(cancel) + bServ := ipld.NewMemBlockservice() + + square := edstest.RandByzantineEDS(t, 16) + dah, err := da.NewDataAvailabilityHeader(square) + require.NoError(t, err) + err = ipld.ImportEDS(ctx, square, bServ) + require.NoError(t, err) + + var errRsmt2d *rsmt2d.ErrByzantineData + err = square.Repair(dah.RowRoots, dah.ColumnRoots) + require.ErrorAs(t, err, &errRsmt2d) + + byzantine := NewErrByzantine(ctx, bServ, &dah, errRsmt2d) + var errByz *ErrByzantine + require.ErrorAs(t, byzantine, &errByz) + + proof := CreateBadEncodingProof([]byte("hash"), 0, errByz) + befp, ok := proof.(*BadEncodingProof) + require.True(t, ok) + var test = []struct { + name string + prepareFn func() error + expectedResult func(error) + }{ + { + name: "valid BEFP", + prepareFn: func() error { + return proof.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.NoError(t, err) + }, + }, + { + name: "invalid BEFP for valid header", + prepareFn: func() error { + validSquare := edstest.RandEDS(t, 2) + validDah, err := da.NewDataAvailabilityHeader(validSquare) + require.NoError(t, err) + err = ipld.ImportEDS(ctx, validSquare, bServ) + require.NoError(t, err) + validShares := validSquare.Flattened() + errInvalidByz := NewErrByzantine(ctx, bServ, &validDah, + &rsmt2d.ErrByzantineData{ + Axis: rsmt2d.Row, + Index: 0, + Shares: validShares[0:4], + }, + ) + var errInvalid *ErrByzantine + require.ErrorAs(t, errInvalidByz, &errInvalid) + invalidBefp := CreateBadEncodingProof([]byte("hash"), 0, errInvalid) + return invalidBefp.Validate(&header.ExtendedHeader{DAH: &validDah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errNMTTreeRootsMatch) + }, + }, + { + name: "incorrect share with Proof", + prepareFn: func() error { + // break the first shareWithProof to test negative case + sh := sharetest.RandShares(t, 2) + nmtProof := nmt.NewInclusionProof(0, 1, nil, false) + befp.Shares[0] = &ShareWithProof{sh[0], &nmtProof} + return proof.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errIncorrectShare) + }, + }, + { + name: "invalid amount of shares", + prepareFn: func() error { + befp.Shares = befp.Shares[0 : len(befp.Shares)/2] + return proof.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errIncorrectAmountOfShares) + }, + }, + { + name: "not enough shares to recompute the root", + prepareFn: func() error { + befp.Shares[0] = nil + return proof.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errIncorrectAmountOfShares) + }, + }, + { + name: "index out of bounds", + prepareFn: func() error { + befp.Index = 100 + return proof.Validate(&header.ExtendedHeader{DAH: &dah}) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errIncorrectIndex) + }, + }, + { + name: "heights mismatch", + prepareFn: func() error { + return proof.Validate(&header.ExtendedHeader{ + RawHeader: core.Header{ + Height: 42, + }, + DAH: &dah, + }) + }, + expectedResult: func(err error) { + require.ErrorIs(t, err, errHeightMismatch) + }, + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + err = tt.prepareFn() + tt.expectedResult(err) + }) + } +} + // TestIncorrectBadEncodingFraudProof asserts that BEFP is not generated for the correct data func TestIncorrectBadEncodingFraudProof(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() squareSize := 8 - shares := share.RandShares(t, squareSize*squareSize) + shares := sharetest.RandShares(t, squareSize*squareSize) - eds, err := share.AddShares(ctx, shares, bServ) + eds, err := ipld.AddShares(ctx, shares, bServ) require.NoError(t, err) - dah := da.NewDataAvailabilityHeader(eds) + dah, err := share.NewRoot(eds) + require.NoError(t, err) // get an arbitrary row row := uint(squareSize / 2) rowShares := eds.Row(row) - rowRoot := dah.RowsRoots[row] + rowRoot := dah.RowRoots[row] shareProofs, err := GetProofsForShares(ctx, bServ, ipld.MustCidFromNamespacedSha256(rowRoot), rowShares) require.NoError(t, err) @@ -50,7 +189,7 @@ func TestIncorrectBadEncodingFraudProof(t *testing.T) { RawHeader: core.Header{ Height: 420, }, - DAH: &dah, + DAH: dah, Commit: &core.Commit{ BlockID: core.BlockID{ Hash: []byte("made up hash"), @@ -58,7 +197,114 @@ func TestIncorrectBadEncodingFraudProof(t *testing.T) { }, } - proof := CreateBadEncodingProof(h.Hash(), uint64(h.Height()), &fakeError) + proof := CreateBadEncodingProof(h.Hash(), h.Height(), &fakeError) err = proof.Validate(h) require.Error(t, err) } + +func TestBEFP_ValidateOutOfOrderShares(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + t.Cleanup(cancel) + + size := 4 + eds := edstest.RandEDS(t, size) + + shares := eds.Flattened() + shares[0], shares[4] = shares[4], shares[0] // corrupting eds + + bServ := newNamespacedBlockService() + batchAddr := ipld.NewNmtNodeAdder(ctx, bServ, ipld.MaxSizeBatchOption(size*2)) + + eds, err := rsmt2d.ImportExtendedDataSquare(shares, + share.DefaultRSMT2DCodec(), + malicious.NewConstructor(uint64(size), nmt.NodeVisitor(batchAddr.Visit)), + ) + require.NoError(t, err, "failure to recompute the extended data square") + + err = batchAddr.Commit() + require.NoError(t, err) + + dah, err := da.NewDataAvailabilityHeader(eds) + require.NoError(t, err) + + var errRsmt2d *rsmt2d.ErrByzantineData + err = eds.Repair(dah.RowRoots, dah.ColumnRoots) + require.ErrorAs(t, err, &errRsmt2d) + + byzantine := NewErrByzantine(ctx, bServ, &dah, errRsmt2d) + var errByz *ErrByzantine + require.ErrorAs(t, byzantine, &errByz) + + befp := CreateBadEncodingProof([]byte("hash"), 0, errByz) + err = befp.Validate(&header.ExtendedHeader{DAH: &dah}) + require.NoError(t, err) +} + +// namespacedBlockService wraps `BlockService` and extends the verification part +// to avoid returning blocks that has out of order namespaces. +type namespacedBlockService struct { + blockservice.BlockService + // the data structure that is used on the networking level, in order + // to verify the order of the namespaces + prefix *cid.Prefix +} + +func newNamespacedBlockService() *namespacedBlockService { + sha256NamespaceFlagged := uint64(0x7701) + // register the nmt hasher to validate the order of namespaces + mhcore.Register(sha256NamespaceFlagged, func() hash.Hash { + nh := nmt.NewNmtHasher(sha256.New(), share.NamespaceSize, true) + nh.Reset() + return nh + }) + + bs := &namespacedBlockService{} + bs.BlockService = ipld.NewMemBlockservice() + + bs.prefix = &cid.Prefix{ + Version: 1, + Codec: sha256NamespaceFlagged, + MhType: sha256NamespaceFlagged, + // equals to NmtHasher.Size() + MhLength: sha256.New().Size() + 2*share.NamespaceSize, + } + return bs +} + +func (n *namespacedBlockService) GetBlock(ctx context.Context, c cid.Cid) (blocks.Block, error) { + block, err := n.BlockService.GetBlock(ctx, c) + if err != nil { + return nil, err + } + + _, err = n.prefix.Sum(block.RawData()) + if err != nil { + return nil, err + } + return block, nil +} + +func (n *namespacedBlockService) GetBlocks(ctx context.Context, cids []cid.Cid) <-chan blocks.Block { + blockCh := n.BlockService.GetBlocks(ctx, cids) + resultCh := make(chan blocks.Block) + + go func() { + for { + select { + case <-ctx.Done(): + close(resultCh) + return + case block, ok := <-blockCh: + if !ok { + close(resultCh) + return + } + if _, err := n.prefix.Sum(block.RawData()); err != nil { + continue + } + resultCh <- block + } + } + }() + return resultCh +} diff --git a/share/eds/byzantine/byzantine.go b/share/eds/byzantine/byzantine.go index 67a6ee54c2..d20b56deed 100644 --- a/share/eds/byzantine/byzantine.go +++ b/share/eds/byzantine/byzantine.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "github.com/celestiaorg/celestia-app/pkg/da" "github.com/celestiaorg/rsmt2d" @@ -34,24 +34,47 @@ func NewErrByzantine( bGetter blockservice.BlockGetter, dah *da.DataAvailabilityHeader, errByz *rsmt2d.ErrByzantineData, -) *ErrByzantine { - root := [][][]byte{ - dah.RowsRoots, +) error { + // changing the order to collect proofs against an orthogonal axis + roots := [][][]byte{ dah.ColumnRoots, - }[errByz.Axis][errByz.Index] - sharesWithProof, err := GetProofsForShares( - ctx, - bGetter, - ipld.MustCidFromNamespacedSha256(root), - errByz.Shares, - ) - if err != nil { - // Fatal as rsmt2d proved that error is byzantine, - // but we cannot properly collect the proof, - // so verification will fail and thus services won't be stopped - // while we still have to stop them. - // TODO(@Wondertan): Find a better way to handle - log.Fatalw("getting proof for ErrByzantine", "err", err) + dah.RowRoots, + }[errByz.Axis] + + sharesWithProof := make([]*ShareWithProof, len(errByz.Shares)) + + type result struct { + share *ShareWithProof + index int + } + resultCh := make(chan *result) + for index, share := range errByz.Shares { + if share == nil { + continue + } + + index := index + go func() { + share, err := getProofsAt( + ctx, bGetter, + ipld.MustCidFromNamespacedSha256(roots[index]), + int(errByz.Index), len(errByz.Shares), + ) + if err != nil { + log.Warn("requesting proof failed", "root", roots[index], "err", err) + return + } + resultCh <- &result{share, index} + }() + } + + for i := 0; i < len(dah.RowRoots)/2; i++ { + select { + case t := <-resultCh: + sharesWithProof[t.index] = t.share + case <-ctx.Done(): + return ipld.ErrNodeNotFound + } } return &ErrByzantine{ diff --git a/share/eds/byzantine/pb/share.pb.go b/share/eds/byzantine/pb/share.pb.go index 33b9cdd1ab..4186eabc64 100644 --- a/share/eds/byzantine/pb/share.pb.go +++ b/share/eds/byzantine/pb/share.pb.go @@ -5,6 +5,7 @@ package share_eds_byzantine_pb import ( fmt "fmt" + pb "github.com/celestiaorg/nmt/pb" proto "github.com/gogo/protobuf/proto" io "io" math "math" @@ -47,84 +48,16 @@ func (Axis) EnumDescriptor() ([]byte, []int) { return fileDescriptor_d28ce8f160a920d1, []int{0} } -type MerkleProof struct { - Start int64 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` - End int64 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"` - Nodes [][]byte `protobuf:"bytes,3,rep,name=nodes,proto3" json:"nodes,omitempty"` - LeafHash []byte `protobuf:"bytes,4,opt,name=leaf_hash,json=leafHash,proto3" json:"leaf_hash,omitempty"` -} - -func (m *MerkleProof) Reset() { *m = MerkleProof{} } -func (m *MerkleProof) String() string { return proto.CompactTextString(m) } -func (*MerkleProof) ProtoMessage() {} -func (*MerkleProof) Descriptor() ([]byte, []int) { - return fileDescriptor_d28ce8f160a920d1, []int{0} -} -func (m *MerkleProof) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *MerkleProof) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_MerkleProof.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *MerkleProof) XXX_Merge(src proto.Message) { - xxx_messageInfo_MerkleProof.Merge(m, src) -} -func (m *MerkleProof) XXX_Size() int { - return m.Size() -} -func (m *MerkleProof) XXX_DiscardUnknown() { - xxx_messageInfo_MerkleProof.DiscardUnknown(m) -} - -var xxx_messageInfo_MerkleProof proto.InternalMessageInfo - -func (m *MerkleProof) GetStart() int64 { - if m != nil { - return m.Start - } - return 0 -} - -func (m *MerkleProof) GetEnd() int64 { - if m != nil { - return m.End - } - return 0 -} - -func (m *MerkleProof) GetNodes() [][]byte { - if m != nil { - return m.Nodes - } - return nil -} - -func (m *MerkleProof) GetLeafHash() []byte { - if m != nil { - return m.LeafHash - } - return nil -} - type Share struct { - Data []byte `protobuf:"bytes,1,opt,name=Data,proto3" json:"Data,omitempty"` - Proof *MerkleProof `protobuf:"bytes,2,opt,name=Proof,proto3" json:"Proof,omitempty"` + Data []byte `protobuf:"bytes,1,opt,name=Data,proto3" json:"Data,omitempty"` + Proof *pb.Proof `protobuf:"bytes,2,opt,name=Proof,proto3" json:"Proof,omitempty"` } func (m *Share) Reset() { *m = Share{} } func (m *Share) String() string { return proto.CompactTextString(m) } func (*Share) ProtoMessage() {} func (*Share) Descriptor() ([]byte, []int) { - return fileDescriptor_d28ce8f160a920d1, []int{1} + return fileDescriptor_d28ce8f160a920d1, []int{0} } func (m *Share) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -160,7 +93,7 @@ func (m *Share) GetData() []byte { return nil } -func (m *Share) GetProof() *MerkleProof { +func (m *Share) GetProof() *pb.Proof { if m != nil { return m.Proof } @@ -179,7 +112,7 @@ func (m *BadEncoding) Reset() { *m = BadEncoding{} } func (m *BadEncoding) String() string { return proto.CompactTextString(m) } func (*BadEncoding) ProtoMessage() {} func (*BadEncoding) Descriptor() ([]byte, []int) { - return fileDescriptor_d28ce8f160a920d1, []int{2} + return fileDescriptor_d28ce8f160a920d1, []int{1} } func (m *BadEncoding) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -245,7 +178,6 @@ func (m *BadEncoding) GetAxis() Axis { func init() { proto.RegisterEnum("share.eds.byzantine.pb.Axis", Axis_name, Axis_value) - proto.RegisterType((*MerkleProof)(nil), "share.eds.byzantine.pb.MerkleProof") proto.RegisterType((*Share)(nil), "share.eds.byzantine.pb.Share") proto.RegisterType((*BadEncoding)(nil), "share.eds.byzantine.pb.BadEncoding") } @@ -255,78 +187,26 @@ func init() { } var fileDescriptor_d28ce8f160a920d1 = []byte{ - // 347 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x91, 0x41, 0x4b, 0xf3, 0x30, - 0x1c, 0xc6, 0x9b, 0xb7, 0xed, 0xde, 0xf7, 0xfd, 0x77, 0xca, 0x08, 0x32, 0x02, 0x6a, 0x28, 0xf5, - 0x52, 0x3c, 0xb4, 0x32, 0xf1, 0xe0, 0xd1, 0xa9, 0x30, 0x41, 0x99, 0x44, 0xd0, 0xa3, 0xa4, 0x26, - 0x5b, 0x8b, 0x23, 0x1d, 0x4d, 0x0f, 0xd3, 0x4f, 0xe1, 0x87, 0xf2, 0xe0, 0x71, 0x47, 0x8f, 0xb2, - 0x7d, 0x11, 0x49, 0x3a, 0x64, 0x07, 0x77, 0xfb, 0x3f, 0x0f, 0x4f, 0xf2, 0xfc, 0xfe, 0x09, 0x44, - 0x3a, 0xe7, 0x95, 0x4c, 0xa5, 0xd0, 0x69, 0xf6, 0xf2, 0xca, 0x55, 0x5d, 0x28, 0x99, 0x4e, 0xb3, - 0xd4, 0xda, 0xc9, 0xb4, 0x2a, 0xeb, 0x12, 0x77, 0x1b, 0x21, 0x85, 0x4e, 0x7e, 0x32, 0xc9, 0x34, - 0x8b, 0x72, 0x08, 0x6e, 0x64, 0xf5, 0x3c, 0x91, 0xb7, 0x55, 0x59, 0x8e, 0xf0, 0x0e, 0xf8, 0xba, - 0xe6, 0x55, 0x4d, 0x50, 0x88, 0x62, 0x97, 0x35, 0x02, 0x77, 0xc0, 0x95, 0x4a, 0x90, 0x3f, 0xd6, - 0x33, 0xa3, 0xc9, 0xa9, 0x52, 0x48, 0x4d, 0xdc, 0xd0, 0x8d, 0xdb, 0xac, 0x11, 0x78, 0x17, 0xfe, - 0x4f, 0x24, 0x1f, 0x3d, 0xe6, 0x5c, 0xe7, 0xc4, 0x0b, 0x51, 0xdc, 0x66, 0xff, 0x8c, 0x31, 0xe0, - 0x3a, 0x8f, 0xee, 0xc1, 0xbf, 0x33, 0x0c, 0x18, 0x83, 0x77, 0xc1, 0x6b, 0x6e, 0x2b, 0xda, 0xcc, - 0xce, 0xf8, 0x14, 0x7c, 0x0b, 0x60, 0x3b, 0x82, 0xde, 0x41, 0xf2, 0x3b, 0x6e, 0xb2, 0xc6, 0xca, - 0x9a, 0x13, 0xd1, 0x3b, 0x82, 0xa0, 0xcf, 0xc5, 0xa5, 0x7a, 0x2a, 0x45, 0xa1, 0xc6, 0x98, 0x02, - 0x0c, 0x24, 0x17, 0xb2, 0x32, 0xad, 0xab, 0x92, 0x35, 0x07, 0x77, 0xa1, 0x35, 0x90, 0xc5, 0x38, - 0xaf, 0x6d, 0x97, 0xc7, 0x56, 0x0a, 0x9f, 0x40, 0xcb, 0xf2, 0x35, 0x3b, 0x05, 0xbd, 0xfd, 0x4d, - 0x0c, 0x36, 0xc5, 0x56, 0x61, 0xf3, 0x12, 0x57, 0x4a, 0xc8, 0x99, 0xdd, 0x77, 0x8b, 0x35, 0x02, - 0x1f, 0x81, 0x77, 0x36, 0x2b, 0x34, 0xf1, 0x43, 0x14, 0x6f, 0xf7, 0xf6, 0x36, 0x5d, 0xc5, 0x67, - 0x85, 0x66, 0x36, 0x79, 0x48, 0xc0, 0x33, 0x0a, 0xff, 0x05, 0x97, 0x0d, 0x1f, 0x3a, 0x8e, 0x19, - 0xce, 0x87, 0xd7, 0x1d, 0xd4, 0x27, 0x1f, 0x0b, 0x8a, 0xe6, 0x0b, 0x8a, 0xbe, 0x16, 0x14, 0xbd, - 0x2d, 0xa9, 0x33, 0x5f, 0x52, 0xe7, 0x73, 0x49, 0x9d, 0xac, 0x65, 0xff, 0xf6, 0xf8, 0x3b, 0x00, - 0x00, 0xff, 0xff, 0x0f, 0xa3, 0x91, 0xb4, 0x01, 0x02, 0x00, 0x00, -} - -func (m *MerkleProof) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *MerkleProof) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *MerkleProof) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.LeafHash) > 0 { - i -= len(m.LeafHash) - copy(dAtA[i:], m.LeafHash) - i = encodeVarintShare(dAtA, i, uint64(len(m.LeafHash))) - i-- - dAtA[i] = 0x22 - } - if len(m.Nodes) > 0 { - for iNdEx := len(m.Nodes) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.Nodes[iNdEx]) - copy(dAtA[i:], m.Nodes[iNdEx]) - i = encodeVarintShare(dAtA, i, uint64(len(m.Nodes[iNdEx]))) - i-- - dAtA[i] = 0x1a - } - } - if m.End != 0 { - i = encodeVarintShare(dAtA, i, uint64(m.End)) - i-- - dAtA[i] = 0x10 - } - if m.Start != 0 { - i = encodeVarintShare(dAtA, i, uint64(m.Start)) - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil + // 297 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x74, 0x90, 0xc1, 0x4a, 0xec, 0x30, + 0x14, 0x86, 0x9b, 0x3b, 0xed, 0x5c, 0x38, 0xa3, 0xe3, 0x10, 0x64, 0x08, 0xa2, 0xa1, 0x0c, 0x08, + 0xc5, 0x45, 0x2a, 0x15, 0x1f, 0xc0, 0xaa, 0x50, 0x41, 0x18, 0x89, 0x0b, 0xd7, 0x89, 0x89, 0xd3, + 0x6e, 0xda, 0xd2, 0x74, 0x51, 0x7d, 0x0a, 0x1f, 0xca, 0x85, 0xcb, 0x59, 0xba, 0x94, 0xf6, 0x45, + 0xa4, 0x69, 0x11, 0x17, 0xba, 0x3b, 0xdf, 0x9f, 0x2f, 0x3f, 0xc9, 0x81, 0x95, 0x49, 0x45, 0xa5, + 0x43, 0xad, 0x4c, 0x28, 0x9f, 0x5f, 0x44, 0x5e, 0x67, 0xb9, 0x0e, 0x4b, 0x19, 0xda, 0x98, 0x95, + 0x55, 0x51, 0x17, 0x78, 0x39, 0x80, 0x56, 0x86, 0x7d, 0x3b, 0xac, 0x94, 0x07, 0xf3, 0x52, 0x86, + 0x65, 0x55, 0x14, 0x4f, 0x83, 0xb7, 0x8a, 0xc1, 0xbb, 0xef, 0x4d, 0x8c, 0xc1, 0xbd, 0x12, 0xb5, + 0x20, 0xc8, 0x47, 0xc1, 0x0e, 0xb7, 0x33, 0x3e, 0x06, 0xef, 0xae, 0x77, 0xc9, 0x3f, 0x1f, 0x05, + 0xb3, 0x68, 0x8f, 0x8d, 0x37, 0x25, 0xb3, 0x31, 0x1f, 0x4e, 0x57, 0x6f, 0x08, 0x66, 0xb1, 0x50, + 0xd7, 0xf9, 0x63, 0xa1, 0xb2, 0x7c, 0x83, 0x29, 0x40, 0xa2, 0x85, 0xd2, 0x55, 0x22, 0x4c, 0x3a, + 0x16, 0xfe, 0x48, 0xf0, 0x12, 0xa6, 0x89, 0xce, 0x36, 0x69, 0x6d, 0x7b, 0x5d, 0x3e, 0x12, 0x3e, + 0x87, 0xa9, 0x7d, 0x8b, 0x21, 0x13, 0x7f, 0x12, 0xcc, 0xa2, 0x23, 0xf6, 0xfb, 0x27, 0x98, 0xb5, + 0xf8, 0x28, 0xe3, 0x7d, 0xf0, 0x6e, 0x72, 0xa5, 0x1b, 0xe2, 0xfa, 0x28, 0xd8, 0xe5, 0x03, 0xe0, + 0x53, 0x70, 0x2f, 0x9a, 0xcc, 0x10, 0xcf, 0x47, 0xc1, 0x3c, 0x3a, 0xfc, 0xab, 0x4a, 0x34, 0x99, + 0xe1, 0xd6, 0x3c, 0x21, 0xe0, 0xf6, 0x84, 0xff, 0xc3, 0x84, 0xaf, 0x1f, 0x16, 0x4e, 0x3f, 0x5c, + 0xae, 0x6f, 0x17, 0x28, 0x26, 0xef, 0x2d, 0x45, 0xdb, 0x96, 0xa2, 0xcf, 0x96, 0xa2, 0xd7, 0x8e, + 0x3a, 0xdb, 0x8e, 0x3a, 0x1f, 0x1d, 0x75, 0xe4, 0xd4, 0x6e, 0xf1, 0xec, 0x2b, 0x00, 0x00, 0xff, + 0xff, 0xb1, 0x96, 0xb9, 0xbe, 0x93, 0x01, 0x00, 0x00, } func (m *Share) Marshal() (dAtA []byte, err error) { @@ -441,31 +321,6 @@ func encodeVarintShare(dAtA []byte, offset int, v uint64) int { dAtA[offset] = uint8(v) return base } -func (m *MerkleProof) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Start != 0 { - n += 1 + sovShare(uint64(m.Start)) - } - if m.End != 0 { - n += 1 + sovShare(uint64(m.End)) - } - if len(m.Nodes) > 0 { - for _, b := range m.Nodes { - l = len(b) - n += 1 + l + sovShare(uint64(l)) - } - } - l = len(m.LeafHash) - if l > 0 { - n += 1 + l + sovShare(uint64(l)) - } - return n -} - func (m *Share) Size() (n int) { if m == nil { return 0 @@ -517,160 +372,6 @@ func sovShare(x uint64) (n int) { func sozShare(x uint64) (n int) { return sovShare(uint64((x << 1) ^ uint64((int64(x) >> 63)))) } -func (m *MerkleProof) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: MerkleProof: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: MerkleProof: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Start", wireType) - } - m.Start = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Start |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field End", wireType) - } - m.End = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.End |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Nodes", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthShare - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthShare - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Nodes = append(m.Nodes, make([]byte, postIndex-iNdEx)) - copy(m.Nodes[len(m.Nodes)-1], dAtA[iNdEx:postIndex]) - iNdEx = postIndex - case 4: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field LeafHash", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthShare - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthShare - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.LeafHash = append(m.LeafHash[:0], dAtA[iNdEx:postIndex]...) - if m.LeafHash == nil { - m.LeafHash = []byte{} - } - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipShare(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthShare - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} func (m *Share) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -764,7 +465,7 @@ func (m *Share) Unmarshal(dAtA []byte) error { return io.ErrUnexpectedEOF } if m.Proof == nil { - m.Proof = &MerkleProof{} + m.Proof = &pb.Proof{} } if err := m.Proof.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err diff --git a/share/eds/byzantine/pb/share.proto b/share/eds/byzantine/pb/share.proto index e08dffae45..33e3dae2c2 100644 --- a/share/eds/byzantine/pb/share.proto +++ b/share/eds/byzantine/pb/share.proto @@ -1,17 +1,11 @@ syntax = "proto3"; package share.eds.byzantine.pb; - -message MerkleProof { - int64 start = 1; - int64 end = 2; - repeated bytes nodes = 3; - bytes leaf_hash = 4; -} +import "pb/proof.proto"; message Share { bytes Data = 1; - MerkleProof Proof = 2; + proof.pb.Proof Proof = 2; } enum axis { diff --git a/share/eds/byzantine/share_proof.go b/share/eds/byzantine/share_proof.go index 86eaad54a8..98b58ebbec 100644 --- a/share/eds/byzantine/share_proof.go +++ b/share/eds/byzantine/share_proof.go @@ -2,13 +2,14 @@ package byzantine import ( "context" + "crypto/sha256" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" - "github.com/minio/sha256-simd" "github.com/celestiaorg/nmt" + nmt_pb "github.com/celestiaorg/nmt/pb" "github.com/celestiaorg/celestia-node/share" pb "github.com/celestiaorg/celestia-node/share/eds/byzantine/pb" @@ -45,8 +46,8 @@ func NewShareWithProof(index int, share share.Share, pathToLeaf []cid.Cid) *Shar func (s *ShareWithProof) Validate(root cid.Cid) bool { return s.Proof.VerifyInclusion( sha256.New(), // TODO(@Wondertan): This should be defined somewhere globally - share.ID(s.Share), - [][]byte{share.Data(s.Share)}, + share.GetNamespace(s.Share).ToNMT(), + [][]byte{share.GetData(s.Share)}, ipld.NamespacedSha256FromCID(root), ) } @@ -58,11 +59,12 @@ func (s *ShareWithProof) ShareWithProofToProto() *pb.Share { return &pb.Share{ Data: s.Share, - Proof: &pb.MerkleProof{ - Start: int64(s.Proof.Start()), - End: int64(s.Proof.End()), - Nodes: s.Proof.Nodes(), - LeafHash: s.Proof.LeafHash(), + Proof: &nmt_pb.Proof{ + Start: int64(s.Proof.Start()), + End: int64(s.Proof.End()), + Nodes: s.Proof.Nodes(), + LeafHash: s.Proof.LeafHash(), + IsMaxNamespaceIgnored: s.Proof.IsMaxNamespaceIDIgnored(), }, } } @@ -78,24 +80,38 @@ func GetProofsForShares( proofs := make([]*ShareWithProof, len(shares)) for index, share := range shares { if share != nil { - proof := make([]cid.Cid, 0) - // TODO(@vgonkivs): Combine GetLeafData and GetProof in one function as the are traversing the same - // tree. Add options that will control what data will be fetched. - s, err := ipld.GetLeaf(ctx, bGetter, root, index, len(shares)) + proof, err := getProofsAt(ctx, bGetter, root, index, len(shares)) if err != nil { return nil, err } - proof, err = ipld.GetProof(ctx, bGetter, root, proof, index, len(shares)) - if err != nil { - return nil, err - } - proofs[index] = NewShareWithProof(index, s.RawData(), proof) + proofs[index] = proof } } - return proofs, nil } +func getProofsAt( + ctx context.Context, + bGetter blockservice.BlockGetter, + root cid.Cid, + index, + total int, +) (*ShareWithProof, error) { + proof := make([]cid.Cid, 0) + // TODO(@vgonkivs): Combine GetLeafData and GetProof in one function as the are traversing the same + // tree. Add options that will control what data will be fetched. + node, err := ipld.GetLeaf(ctx, bGetter, root, index, total) + if err != nil { + return nil, err + } + + proof, err = ipld.GetProof(ctx, bGetter, root, proof, index, total) + if err != nil { + return nil, err + } + return NewShareWithProof(index, node.RawData(), proof), nil +} + func ProtoToShare(protoShares []*pb.Share) []*ShareWithProof { shares := make([]*ShareWithProof, len(protoShares)) for i, share := range protoShares { @@ -108,6 +124,11 @@ func ProtoToShare(protoShares []*pb.Share) []*ShareWithProof { return shares } -func ProtoToProof(protoProof *pb.MerkleProof) nmt.Proof { - return nmt.NewInclusionProof(int(protoProof.Start), int(protoProof.End), protoProof.Nodes, ipld.NMTIgnoreMaxNamespace) +func ProtoToProof(protoProof *nmt_pb.Proof) nmt.Proof { + return nmt.NewInclusionProof( + int(protoProof.Start), + int(protoProof.End), + protoProof.Nodes, + protoProof.IsMaxNamespaceIgnored, + ) } diff --git a/share/eds/byzantine/share_proof_test.go b/share/eds/byzantine/share_proof_test.go index 501f4b40d9..a9021d806d 100644 --- a/share/eds/byzantine/share_proof_test.go +++ b/share/eds/byzantine/share_proof_test.go @@ -7,13 +7,12 @@ import ( "time" "github.com/ipfs/go-cid" - mdutils "github.com/ipfs/go-merkledag/test" "github.com/stretchr/testify/require" "github.com/celestiaorg/celestia-app/pkg/da" - "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share/sharetest" ) func TestGetProof(t *testing.T) { @@ -21,17 +20,18 @@ func TestGetProof(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) defer cancel() - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() - shares := share.RandShares(t, width*width) - in, err := share.AddShares(ctx, shares, bServ) + shares := sharetest.RandShares(t, width*width) + in, err := ipld.AddShares(ctx, shares, bServ) require.NoError(t, err) - dah := da.NewDataAvailabilityHeader(in) + dah, err := da.NewDataAvailabilityHeader(in) + require.NoError(t, err) var tests = []struct { roots [][]byte }{ - {dah.RowsRoots}, + {dah.RowRoots}, {dah.ColumnRoots}, } @@ -57,13 +57,14 @@ func TestGetProofs(t *testing.T) { const width = 4 ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) defer cancel() - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() - shares := share.RandShares(t, width*width) - in, err := share.AddShares(ctx, shares, bServ) + shares := sharetest.RandShares(t, width*width) + in, err := ipld.AddShares(ctx, shares, bServ) require.NoError(t, err) - dah := da.NewDataAvailabilityHeader(in) + dah, err := da.NewDataAvailabilityHeader(in) + require.NoError(t, err) for _, root := range dah.ColumnRoots { rootCid := ipld.MustCidFromNamespacedSha256(root) data := make([][]byte, 0, in.Width()) diff --git a/share/eds/cache/accessor_cache.go b/share/eds/cache/accessor_cache.go new file mode 100644 index 0000000000..6f937818f8 --- /dev/null +++ b/share/eds/cache/accessor_cache.go @@ -0,0 +1,262 @@ +package cache + +import ( + "context" + "errors" + "fmt" + "io" + "sync" + "sync/atomic" + "time" + + "github.com/filecoin-project/dagstore" + "github.com/filecoin-project/dagstore/shard" + lru "github.com/hashicorp/golang-lru/v2" +) + +const defaultCloseTimeout = time.Minute + +var _ Cache = (*AccessorCache)(nil) + +// AccessorCache implements the Cache interface using an LRU cache backend. +type AccessorCache struct { + // The name is a prefix that will be used for cache metrics if they are enabled. + name string + // stripedLocks prevents simultaneous RW access to the blockstore cache for a shard. Instead + // of using only one lock or one lock per key, we stripe the shard keys across 256 locks. 256 is + // chosen because it 0-255 is the range of values we get looking at the last byte of the key. + stripedLocks [256]sync.Mutex + // Caches the blockstore for a given shard for shard read affinity, i.e., further reads will likely + // be from the same shard. Maps (shard key -> blockstore). + cache *lru.Cache[shard.Key, *accessorWithBlockstore] + + metrics *metrics +} + +// accessorWithBlockstore is the value that we store in the blockstore Cache. It implements the +// Accessor interface. +type accessorWithBlockstore struct { + sync.RWMutex + shardAccessor Accessor + // The blockstore is stored separately because each access to the blockstore over the shard + // accessor reopens the underlying CAR. + bs dagstore.ReadBlockstore + + done chan struct{} + refs atomic.Int32 + isClosed bool +} + +// Blockstore implements the Blockstore of the Accessor interface. It creates the blockstore on the +// first request and reuses the created instance for all subsequent requests. +func (s *accessorWithBlockstore) Blockstore() (dagstore.ReadBlockstore, error) { + s.Lock() + defer s.Unlock() + var err error + if s.bs == nil { + s.bs, err = s.shardAccessor.Blockstore() + } + return s.bs, err +} + +// Reader returns a new copy of the reader to read data. +func (s *accessorWithBlockstore) Reader() io.Reader { + return s.shardAccessor.Reader() +} + +func (s *accessorWithBlockstore) addRef() error { + s.Lock() + defer s.Unlock() + if s.isClosed { + // item is already closed and soon will be removed after all refs are released + return errCacheMiss + } + if s.refs.Add(1) == 1 { + // there were no refs previously and done channel was closed, reopen it by recreating + s.done = make(chan struct{}) + } + return nil +} + +func (s *accessorWithBlockstore) removeRef() { + s.Lock() + defer s.Unlock() + if s.refs.Add(-1) <= 0 { + close(s.done) + } +} + +func (s *accessorWithBlockstore) close() error { + s.Lock() + if s.isClosed { + s.Unlock() + // accessor will be closed by another goroutine + return nil + } + s.isClosed = true + done := s.done + s.Unlock() + + select { + case <-done: + case <-time.After(defaultCloseTimeout): + return fmt.Errorf("closing accessor, some readers didn't close the accessor within timeout,"+ + " amount left: %v", s.refs.Load()) + } + if err := s.shardAccessor.Close(); err != nil { + return fmt.Errorf("closing accessor: %w", err) + } + return nil +} + +func NewAccessorCache(name string, cacheSize int) (*AccessorCache, error) { + bc := &AccessorCache{ + name: name, + } + // Instantiate the blockstore Cache. + bslru, err := lru.NewWithEvict[shard.Key, *accessorWithBlockstore](cacheSize, bc.evictFn()) + if err != nil { + return nil, fmt.Errorf("failed to instantiate blockstore cache: %w", err) + } + bc.cache = bslru + return bc, nil +} + +// evictFn will be invoked when an item is evicted from the cache. +func (bc *AccessorCache) evictFn() func(shard.Key, *accessorWithBlockstore) { + return func(_ shard.Key, abs *accessorWithBlockstore) { + // we can release accessor from cache early, while it is being closed in parallel routine + go func() { + err := abs.close() + if err != nil { + bc.metrics.observeEvicted(true) + log.Errorf("couldn't close accessor after cache eviction: %s", err) + return + } + bc.metrics.observeEvicted(false) + }() + } +} + +// Get retrieves the Accessor for a given shard key from the Cache. If the Accessor is not in +// the Cache, it returns an errCacheMiss. +func (bc *AccessorCache) Get(key shard.Key) (Accessor, error) { + lk := &bc.stripedLocks[shardKeyToStriped(key)] + lk.Lock() + defer lk.Unlock() + + accessor, err := bc.get(key) + if err != nil { + bc.metrics.observeGet(false) + return nil, err + } + bc.metrics.observeGet(true) + return newRefCloser(accessor) +} + +func (bc *AccessorCache) get(key shard.Key) (*accessorWithBlockstore, error) { + abs, ok := bc.cache.Get(key) + if !ok { + return nil, errCacheMiss + } + return abs, nil +} + +// GetOrLoad attempts to get an item from the cache, and if not found, invokes +// the provided loader function to load it. +func (bc *AccessorCache) GetOrLoad( + ctx context.Context, + key shard.Key, + loader func(context.Context, shard.Key) (Accessor, error), +) (Accessor, error) { + lk := &bc.stripedLocks[shardKeyToStriped(key)] + lk.Lock() + defer lk.Unlock() + + abs, err := bc.get(key) + if err == nil { + // return accessor, only of it is not closed yet + accessorWithRef, err := newRefCloser(abs) + if err == nil { + bc.metrics.observeGet(true) + return accessorWithRef, nil + } + } + + // accessor not found in cache, so load new one using loader + accessor, err := loader(ctx, key) + if err != nil { + return nil, fmt.Errorf("unable to load accessor: %w", err) + } + + abs = &accessorWithBlockstore{ + shardAccessor: accessor, + } + + // Create a new accessor first to increment the reference count in it, so it cannot get evicted + // from the inner lru cache before it is used. + accessorWithRef, err := newRefCloser(abs) + if err != nil { + return nil, err + } + bc.cache.Add(key, abs) + return accessorWithRef, nil +} + +// Remove removes the Accessor for a given key from the cache. +func (bc *AccessorCache) Remove(key shard.Key) error { + lk := &bc.stripedLocks[shardKeyToStriped(key)] + lk.Lock() + accessor, err := bc.get(key) + lk.Unlock() + if errors.Is(err, errCacheMiss) { + // item is not in cache + return nil + } + if err = accessor.close(); err != nil { + return err + } + // The cache will call evictFn on removal, where accessor close will be called. + bc.cache.Remove(key) + return nil +} + +// EnableMetrics enables metrics for the cache. +func (bc *AccessorCache) EnableMetrics() error { + var err error + bc.metrics, err = newMetrics(bc) + return err +} + +// refCloser manages references to accessor from provided reader and removes the ref, when the +// Close is called +type refCloser struct { + *accessorWithBlockstore + closeFn func() +} + +// newRefCloser creates new refCloser +func newRefCloser(abs *accessorWithBlockstore) (*refCloser, error) { + if err := abs.addRef(); err != nil { + return nil, err + } + + var closeOnce sync.Once + return &refCloser{ + accessorWithBlockstore: abs, + closeFn: func() { + closeOnce.Do(abs.removeRef) + }, + }, nil +} + +func (c *refCloser) Close() error { + c.closeFn() + return nil +} + +// shardKeyToStriped returns the index of the lock to use for a given shard key. We use the last +// byte of the shard key as the pseudo-random index. +func shardKeyToStriped(sk shard.Key) byte { + return sk.String()[len(sk.String())-1] +} diff --git a/share/eds/cache/accessor_cache_test.go b/share/eds/cache/accessor_cache_test.go new file mode 100644 index 0000000000..347b251a88 --- /dev/null +++ b/share/eds/cache/accessor_cache_test.go @@ -0,0 +1,350 @@ +package cache + +import ( + "bytes" + "context" + "errors" + "io" + "sync" + "testing" + "time" + + "github.com/filecoin-project/dagstore" + "github.com/filecoin-project/dagstore/shard" + blocks "github.com/ipfs/go-block-format" + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/require" +) + +func TestAccessorCache(t *testing.T) { + t.Run("add / get item from cache", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + cache, err := NewAccessorCache("test", 1) + require.NoError(t, err) + + // add accessor to the cache + key := shard.KeyFromString("key") + mock := &mockAccessor{ + data: []byte("test_data"), + } + loaded, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock, nil + }) + require.NoError(t, err) + + // check if item exists + got, err := cache.Get(key) + require.NoError(t, err) + + l, err := io.ReadAll(loaded.Reader()) + require.NoError(t, err) + require.Equal(t, mock.data, l) + g, err := io.ReadAll(got.Reader()) + require.NoError(t, err) + require.Equal(t, mock.data, g) + }) + + t.Run("get blockstore from accessor", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + cache, err := NewAccessorCache("test", 1) + require.NoError(t, err) + + // add accessor to the cache + key := shard.KeyFromString("key") + mock := &mockAccessor{} + accessor, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock, nil + }) + require.NoError(t, err) + + // check if item exists + _, err = cache.Get(key) + require.NoError(t, err) + + // blockstore should be created only after first request + require.Equal(t, 0, mock.returnedBs) + + // try to get blockstore + _, err = accessor.Blockstore() + require.NoError(t, err) + + // second call to blockstore should return same blockstore + _, err = accessor.Blockstore() + require.NoError(t, err) + require.Equal(t, 1, mock.returnedBs) + }) + + t.Run("remove an item", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + cache, err := NewAccessorCache("test", 1) + require.NoError(t, err) + + // add accessor to the cache + key := shard.KeyFromString("key") + mock := &mockAccessor{} + ac, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock, nil + }) + require.NoError(t, err) + err = ac.Close() + require.NoError(t, err) + + err = cache.Remove(key) + require.NoError(t, err) + + // accessor should be closed on removal + mock.checkClosed(t, true) + + // check if item exists + _, err = cache.Get(key) + require.ErrorIs(t, err, errCacheMiss) + }) + + t.Run("successive reads should read the same data", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + cache, err := NewAccessorCache("test", 1) + require.NoError(t, err) + + // add accessor to the cache + key := shard.KeyFromString("key") + mock := &mockAccessor{data: []byte("test")} + accessor, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock, nil + }) + require.NoError(t, err) + + loaded, err := io.ReadAll(accessor.Reader()) + require.NoError(t, err) + require.Equal(t, mock.data, loaded) + + for i := 0; i < 2; i++ { + accessor, err = cache.Get(key) + require.NoError(t, err) + got, err := io.ReadAll(accessor.Reader()) + require.NoError(t, err) + require.Equal(t, mock.data, got) + } + }) + + t.Run("removed by eviction", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + cache, err := NewAccessorCache("test", 1) + require.NoError(t, err) + + // add accessor to the cache + key := shard.KeyFromString("key") + mock := &mockAccessor{} + ac1, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock, nil + }) + require.NoError(t, err) + err = ac1.Close() + require.NoError(t, err) + + // add second item + key2 := shard.KeyFromString("key2") + ac2, err := cache.GetOrLoad(ctx, key2, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock, nil + }) + require.NoError(t, err) + err = ac2.Close() + require.NoError(t, err) + + // accessor should be closed on removal by eviction + mock.checkClosed(t, true) + + // check if item evicted + _, err = cache.Get(key) + require.ErrorIs(t, err, errCacheMiss) + }) + + t.Run("close on accessor is not closing underlying accessor", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + cache, err := NewAccessorCache("test", 1) + require.NoError(t, err) + + // add accessor to the cache + key := shard.KeyFromString("key") + mock := &mockAccessor{} + _, err = cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock, nil + }) + require.NoError(t, err) + + // check if item exists + accessor, err := cache.Get(key) + require.NoError(t, err) + require.NotNil(t, accessor) + + // close on returned accessor should not close inner accessor + err = accessor.Close() + require.NoError(t, err) + + // check that close was not performed on inner accessor + mock.checkClosed(t, false) + }) + + t.Run("close on accessor should wait all readers to finish", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + cache, err := NewAccessorCache("test", 1) + require.NoError(t, err) + + // add accessor to the cache + key := shard.KeyFromString("key") + mock := &mockAccessor{} + accessor1, err := cache.GetOrLoad(ctx, key, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock, nil + }) + require.NoError(t, err) + + // create second readers + accessor2, err := cache.Get(key) + require.NoError(t, err) + + // initialize close + done := make(chan struct{}) + go func() { + err := cache.Remove(key) + require.NoError(t, err) + close(done) + }() + + // close on first reader and check that it is not enough to release the inner accessor + err = accessor1.Close() + require.NoError(t, err) + mock.checkClosed(t, false) + + // second close from same reader should not release accessor either + err = accessor1.Close() + require.NoError(t, err) + mock.checkClosed(t, false) + + // reads for item that is being evicted should result in errCacheMiss + _, err = cache.Get(key) + require.ErrorIs(t, err, errCacheMiss) + + // close second reader and wait for accessor to be closed + err = accessor2.Close() + require.NoError(t, err) + // wait until close is performed on accessor + select { + case <-done: + case <-ctx.Done(): + t.Fatal("timeout reached") + } + + // item will be removed + mock.checkClosed(t, true) + }) + + t.Run("slow reader should not block eviction", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + cache, err := NewAccessorCache("test", 1) + require.NoError(t, err) + + // add accessor to the cache + key1 := shard.KeyFromString("key1") + mock1 := &mockAccessor{} + accessor1, err := cache.GetOrLoad(ctx, key1, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock1, nil + }) + require.NoError(t, err) + + // add second accessor, to trigger eviction of the first one + key2 := shard.KeyFromString("key2") + mock2 := &mockAccessor{} + accessor2, err := cache.GetOrLoad(ctx, key2, func(ctx context.Context, key shard.Key) (Accessor, error) { + return mock2, nil + }) + require.NoError(t, err) + + // first accessor should be evicted from cache + _, err = cache.Get(key1) + require.ErrorIs(t, err, errCacheMiss) + + // first accessor should not be closed before all refs are released by Close() is calls. + mock1.checkClosed(t, false) + + // after Close() is called on first accessor, it is free to get closed + err = accessor1.Close() + require.NoError(t, err) + mock1.checkClosed(t, true) + + // after Close called on second accessor, it should stay in cache (not closed) + err = accessor2.Close() + require.NoError(t, err) + mock2.checkClosed(t, false) + }) +} + +type mockAccessor struct { + m sync.Mutex + data []byte + isClosed bool + returnedBs int +} + +func (m *mockAccessor) Reader() io.Reader { + m.m.Lock() + defer m.m.Unlock() + return bytes.NewBuffer(m.data) +} + +func (m *mockAccessor) Blockstore() (dagstore.ReadBlockstore, error) { + m.m.Lock() + defer m.m.Unlock() + if m.returnedBs > 0 { + return nil, errors.New("blockstore already returned") + } + m.returnedBs++ + return rbsMock{}, nil +} + +func (m *mockAccessor) Close() error { + m.m.Lock() + defer m.m.Unlock() + if m.isClosed { + return errors.New("already closed") + } + m.isClosed = true + return nil +} + +func (m *mockAccessor) checkClosed(t *testing.T, expected bool) { + // item will be removed in background, so give it some time to settle + time.Sleep(time.Millisecond * 100) + m.m.Lock() + defer m.m.Unlock() + require.Equal(t, expected, m.isClosed) +} + +// rbsMock is a dagstore.ReadBlockstore mock +type rbsMock struct{} + +func (r rbsMock) Has(context.Context, cid.Cid) (bool, error) { + panic("implement me") +} + +func (r rbsMock) Get(_ context.Context, _ cid.Cid) (blocks.Block, error) { + panic("implement me") +} + +func (r rbsMock) GetSize(context.Context, cid.Cid) (int, error) { + panic("implement me") +} + +func (r rbsMock) AllKeysChan(context.Context) (<-chan cid.Cid, error) { + panic("implement me") +} + +func (r rbsMock) HashOnRead(bool) { + panic("implement me") +} diff --git a/share/eds/cache/cache.go b/share/eds/cache/cache.go new file mode 100644 index 0000000000..13e207d7c0 --- /dev/null +++ b/share/eds/cache/cache.go @@ -0,0 +1,49 @@ +package cache + +import ( + "context" + "errors" + "io" + + "github.com/filecoin-project/dagstore" + "github.com/filecoin-project/dagstore/shard" + logging "github.com/ipfs/go-log/v2" + "go.opentelemetry.io/otel" +) + +var ( + log = logging.Logger("share/eds/cache") + meter = otel.Meter("eds_store_cache") +) + +var ( + errCacheMiss = errors.New("accessor not found in blockstore cache") +) + +// Cache is an interface that defines the basic Cache operations. +type Cache interface { + // Get retrieves an item from the Cache. + Get(shard.Key) (Accessor, error) + + // GetOrLoad attempts to get an item from the Cache and, if not found, invokes + // the provided loader function to load it into the Cache. + GetOrLoad( + ctx context.Context, + key shard.Key, + loader func(context.Context, shard.Key) (Accessor, error), + ) (Accessor, error) + + // Remove removes an item from Cache. + Remove(shard.Key) error + + // EnableMetrics enables metrics in Cache + EnableMetrics() error +} + +// Accessor is a interface type returned by cache, that allows to read raw data by reader or create +// readblockstore +type Accessor interface { + Blockstore() (dagstore.ReadBlockstore, error) + Reader() io.Reader + io.Closer +} diff --git a/share/eds/cache/doublecache.go b/share/eds/cache/doublecache.go new file mode 100644 index 0000000000..a63eadee9e --- /dev/null +++ b/share/eds/cache/doublecache.go @@ -0,0 +1,51 @@ +package cache + +import ( + "errors" + + "github.com/filecoin-project/dagstore/shard" +) + +// DoubleCache represents a Cache that looks into multiple caches one by one. +type DoubleCache struct { + first, second Cache +} + +// NewDoubleCache creates a new DoubleCache with the provided caches. +func NewDoubleCache(first, second Cache) *DoubleCache { + return &DoubleCache{ + first: first, + second: second, + } +} + +// Get looks for an item in all the caches one by one and returns the Cache found item. +func (mc *DoubleCache) Get(key shard.Key) (Accessor, error) { + ac, err := mc.first.Get(key) + if err == nil { + return ac, nil + } + return mc.second.Get(key) +} + +// Remove removes an item from all underlying caches +func (mc *DoubleCache) Remove(key shard.Key) error { + err1 := mc.first.Remove(key) + err2 := mc.second.Remove(key) + return errors.Join(err1, err2) +} + +func (mc *DoubleCache) First() Cache { + return mc.first +} + +func (mc *DoubleCache) Second() Cache { + return mc.second +} + +func (mc *DoubleCache) EnableMetrics() error { + if err := mc.first.EnableMetrics(); err != nil { + return err + } + return mc.second.EnableMetrics() +} diff --git a/share/eds/cache/metrics.go b/share/eds/cache/metrics.go new file mode 100644 index 0000000000..b2e3bec8d8 --- /dev/null +++ b/share/eds/cache/metrics.go @@ -0,0 +1,69 @@ +package cache + +import ( + "context" + + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" +) + +const ( + cacheFoundKey = "found" + failedKey = "failed" +) + +type metrics struct { + getCounter metric.Int64Counter + evictedCounter metric.Int64Counter +} + +func newMetrics(bc *AccessorCache) (*metrics, error) { + metricsPrefix := "eds_blockstore_cache_" + bc.name + + evictedCounter, err := meter.Int64Counter(metricsPrefix+"_evicted_counter", + metric.WithDescription("eds blockstore cache evicted event counter")) + if err != nil { + return nil, err + } + + getCounter, err := meter.Int64Counter(metricsPrefix+"_get_counter", + metric.WithDescription("eds blockstore cache evicted event counter")) + if err != nil { + return nil, err + } + + cacheSize, err := meter.Int64ObservableGauge(metricsPrefix+"_size", + metric.WithDescription("total amount of items in blockstore cache"), + ) + if err != nil { + return nil, err + } + + callback := func(ctx context.Context, observer metric.Observer) error { + observer.ObserveInt64(cacheSize, int64(bc.cache.Len())) + return nil + } + _, err = meter.RegisterCallback(callback, cacheSize) + + return &metrics{ + getCounter: getCounter, + evictedCounter: evictedCounter, + }, err +} + +func (m *metrics) observeEvicted(failed bool) { + if m == nil { + return + } + m.evictedCounter.Add(context.Background(), 1, + metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} + +func (m *metrics) observeGet(found bool) { + if m == nil { + return + } + m.getCounter.Add(context.Background(), 1, metric.WithAttributes( + attribute.Bool(cacheFoundKey, found))) +} diff --git a/share/eds/cache/noop.go b/share/eds/cache/noop.go new file mode 100644 index 0000000000..0a1a39ec7e --- /dev/null +++ b/share/eds/cache/noop.go @@ -0,0 +1,50 @@ +package cache + +import ( + "context" + "io" + + "github.com/filecoin-project/dagstore" + "github.com/filecoin-project/dagstore/shard" +) + +var _ Cache = (*NoopCache)(nil) + +// NoopCache implements noop version of Cache interface +type NoopCache struct{} + +func (n NoopCache) Get(shard.Key) (Accessor, error) { + return nil, errCacheMiss +} + +func (n NoopCache) GetOrLoad( + context.Context, shard.Key, + func(context.Context, shard.Key) (Accessor, error), +) (Accessor, error) { + return NoopAccessor{}, nil +} + +func (n NoopCache) Remove(shard.Key) error { + return nil +} + +func (n NoopCache) EnableMetrics() error { + return nil +} + +var _ Accessor = (*NoopAccessor)(nil) + +// NoopAccessor implements noop version of Accessor interface +type NoopAccessor struct{} + +func (n NoopAccessor) Blockstore() (dagstore.ReadBlockstore, error) { + return nil, nil +} + +func (n NoopAccessor) Reader() io.Reader { + return nil +} + +func (n NoopAccessor) Close() error { + return nil +} diff --git a/share/eds/eds.go b/share/eds/eds.go index b183731a18..e0433a1b6b 100644 --- a/share/eds/eds.go +++ b/share/eds/eds.go @@ -3,23 +3,16 @@ package eds import ( "bytes" "context" + "crypto/sha256" "errors" "fmt" "io" "math" - "github.com/ipfs/go-blockservice" "github.com/ipfs/go-cid" - ds "github.com/ipfs/go-datastore" - dssync "github.com/ipfs/go-datastore/sync" - bstore "github.com/ipfs/go-ipfs-blockstore" - format "github.com/ipfs/go-ipld-format" "github.com/ipld/go-car" "github.com/ipld/go-car/util" - "github.com/minio/sha256-simd" - "github.com/celestiaorg/celestia-app/pkg/appconsts" - "github.com/celestiaorg/celestia-app/pkg/da" "github.com/celestiaorg/celestia-app/pkg/wrapper" "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" @@ -31,15 +24,6 @@ import ( var ErrEmptySquare = errors.New("share: importing empty data") -// writingSession contains the components needed to write an EDS to a CARv1 file with our custom -// node order. -type writingSession struct { - eds *rsmt2d.ExtendedDataSquare - store bstore.Blockstore // caches inner nodes (proofs) while we walk the nmt tree. - hasher *nmt.Hasher - w io.Writer -} - // WriteEDS writes the entire EDS into the given io.Writer as CARv1 file. // This includes all shares in quadrant order, followed by all inner nodes of the NMT tree. // Order: [ Carv1Header | Q1 | Q2 | Q3 | Q4 | inner nodes ] @@ -50,77 +34,28 @@ func WriteEDS(ctx context.Context, eds *rsmt2d.ExtendedDataSquare, w io.Writer) utils.SetStatusAndEnd(span, err) }() - // 1. Reimport EDS. This is needed to traverse the NMT tree and cache the inner nodes (proofs) - writer, err := initializeWriter(ctx, eds, w) - if err != nil { - return fmt.Errorf("share: creating eds writer: %w", err) - } - - // 2. Creates and writes Carv1Header - // - Roots are the eds Row + Col roots - err = writer.writeHeader() + // Creates and writes Carv1Header. Roots are the eds Row + Col roots + err = writeHeader(eds, w) if err != nil { return fmt.Errorf("share: writing carv1 header: %w", err) } - - // 3. Iterates over shares in quadrant order via eds.GetCell - err = writer.writeQuadrants() + // Iterates over shares in quadrant order via eds.GetCell + err = writeQuadrants(eds, w) if err != nil { return fmt.Errorf("share: writing shares: %w", err) } - // 4. Iterates over in-memory blockstore and writes proofs to the CAR - err = writer.writeProofs(ctx) + // Iterates over proofs and writes them to the CAR + err = writeProofs(ctx, eds, w) if err != nil { return fmt.Errorf("share: writing proofs: %w", err) } return nil } -// initializeWriter reimports the EDS into an in-memory blockstore in order to cache the proofs. -func initializeWriter(ctx context.Context, eds *rsmt2d.ExtendedDataSquare, w io.Writer) (*writingSession, error) { - // we use an in-memory blockstore and an offline exchange - store := bstore.NewBlockstore(dssync.MutexWrap(ds.NewMapDatastore())) - bs := blockservice.New(store, nil) - // shares are extracted from the eds so that we can reimport them to traverse - shares := share.ExtractEDS(eds) - shareCount := len(shares) - if shareCount == 0 { - return nil, ErrEmptySquare - } - odsWidth := int(math.Sqrt(float64(shareCount)) / 2) - // (shareCount*2) - (odsWidth*4) is the amount of inner nodes visited - batchAdder := ipld.NewNmtNodeAdder(ctx, bs, format.MaxSizeBatchOption(innerNodeBatchSize(shareCount, odsWidth))) - // this adder ignores leaves, so that they are not added to the store we iterate through in - // writeProofs - eds, err := rsmt2d.ImportExtendedDataSquare( - shares, - share.DefaultRSMT2DCodec(), - wrapper.NewConstructor(uint64(odsWidth), - nmt.NodeVisitor(batchAdder.VisitInnerNodes)), - ) - if err != nil { - return nil, fmt.Errorf("recomputing data square: %w", err) - } - // compute roots - eds.RowRoots() - // commit the batch to DAG - err = batchAdder.Commit() - if err != nil { - return nil, fmt.Errorf("committing inner nodes to the dag: %w", err) - } - - return &writingSession{ - eds: eds, - store: store, - hasher: nmt.NewNmtHasher(sha256.New(), ipld.NamespaceSize, ipld.NMTIgnoreMaxNamespace), - w: w, - }, nil -} - // writeHeader creates a CarV1 header using the EDS's Row and Column roots as the list of DAG roots. -func (w *writingSession) writeHeader() error { - rootCids, err := rootsToCids(w.eds) +func writeHeader(eds *rsmt2d.ExtendedDataSquare, w io.Writer) error { + rootCids, err := rootsToCids(eds) if err != nil { return fmt.Errorf("getting root cids: %w", err) } @@ -128,14 +63,15 @@ func (w *writingSession) writeHeader() error { return car.WriteHeader(&car.CarHeader{ Roots: rootCids, Version: 1, - }, w.w) + }, w) } // writeQuadrants reorders the shares to quadrant order and writes them to the CARv1 file. -func (w *writingSession) writeQuadrants() error { - shares := quadrantOrder(w.eds) +func writeQuadrants(eds *rsmt2d.ExtendedDataSquare, w io.Writer) error { + hasher := nmt.NewNmtHasher(sha256.New(), share.NamespaceSize, ipld.NMTIgnoreMaxNamespace) + shares := quadrantOrder(eds) for _, share := range shares { - leaf, err := w.hasher.HashLeaf(share) + leaf, err := hasher.HashLeaf(share) if err != nil { return fmt.Errorf("hashing share: %w", err) } @@ -143,7 +79,7 @@ func (w *writingSession) writeQuadrants() error { if err != nil { return fmt.Errorf("getting cid from share: %w", err) } - err = util.LdWrite(w.w, cid.Bytes(), share) + err = util.LdWrite(w, cid.Bytes(), share) if err != nil { return fmt.Errorf("writing share to file: %w", err) } @@ -153,30 +89,15 @@ func (w *writingSession) writeQuadrants() error { // writeProofs iterates over the in-memory blockstore's keys and writes all inner nodes to the // CARv1 file. -func (w *writingSession) writeProofs(ctx context.Context) error { - // we only stored proofs to the store, so we can just iterate over them here without getting any - // leaves - proofs, err := w.store.AllKeysChan(ctx) +func writeProofs(ctx context.Context, eds *rsmt2d.ExtendedDataSquare, w io.Writer) error { + // check if proofs are collected by ipld.ProofsAdder in previous reconstructions of eds + proofs, err := getProofs(ctx, eds) if err != nil { - return fmt.Errorf("getting all keys from the blockstore: %w", err) + return fmt.Errorf("recomputing proofs: %w", err) } - for proofCid := range proofs { - block, err := w.store.Get(ctx, proofCid) - if err != nil { - return fmt.Errorf("getting proof from the blockstore: %w", err) - } - node := block.RawData() - left, right := node[:ipld.NmtHashSize], node[ipld.NmtHashSize:] - hash, err := w.hasher.HashNode(left, right) - if err != nil { - return fmt.Errorf("hashing node: %w", err) - } - cid, err := ipld.CidFromNamespacedSha256(hash) - if err != nil { - return fmt.Errorf("getting cid: %w", err) - } - err = util.LdWrite(w.w, cid.Bytes(), node) + for id, proof := range proofs { + err := util.LdWrite(w, id.Bytes(), proof) if err != nil { return fmt.Errorf("writing proof to the car: %w", err) } @@ -184,6 +105,43 @@ func (w *writingSession) writeProofs(ctx context.Context) error { return nil } +func getProofs(ctx context.Context, eds *rsmt2d.ExtendedDataSquare) (map[cid.Cid][]byte, error) { + // check if there are proofs collected by ipld.ProofsAdder in previous reconstruction of eds + if adder := ipld.ProofsAdderFromCtx(ctx); adder != nil { + defer adder.Purge() + return adder.Proofs(), nil + } + + // recompute proofs from eds + shares := eds.Flattened() + shareCount := len(shares) + if shareCount == 0 { + return nil, ErrEmptySquare + } + odsWidth := int(math.Sqrt(float64(shareCount)) / 2) + + // this adder ignores leaves, so that they are not added to the store we iterate through in + // writeProofs + adder := ipld.NewProofsAdder(odsWidth * 2) + defer adder.Purge() + + eds, err := rsmt2d.ImportExtendedDataSquare( + shares, + share.DefaultRSMT2DCodec(), + wrapper.NewConstructor(uint64(odsWidth), + nmt.NodeVisitor(adder.VisitFn())), + ) + if err != nil { + return nil, fmt.Errorf("recomputing data square: %w", err) + } + // compute roots + if _, err = eds.RowRoots(); err != nil { + return nil, fmt.Errorf("computing row roots: %w", err) + } + + return adder.Proofs(), nil +} + // quadrantOrder reorders the shares in the EDS to quadrant row-by-row order, prepending the // respective namespace to the shares. // e.g. [ Q1 R1 | Q1 R2 | Q1 R3 | Q1 R4 | Q2 R1 | Q2 R2 .... ] @@ -218,12 +176,13 @@ func getQuadrantCells(eds *rsmt2d.ExtendedDataSquare, i, j uint) [][]byte { // prependNamespace adds the namespace to the passed share if in the first quadrant, // otherwise it adds the ParitySharesNamespace to the beginning. -func prependNamespace(quadrant int, share []byte) []byte { +func prependNamespace(quadrant int, shr share.Share) []byte { + namespacedShare := make([]byte, 0, share.NamespaceSize+share.Size) switch quadrant { case 0: - return append(share[:ipld.NamespaceSize], share...) + return append(append(namespacedShare, share.GetNamespace(shr)...), shr...) case 1, 2, 3: - return append(appconsts.ParitySharesNamespaceID, share...) + return append(append(namespacedShare, share.ParitySharesNamespace...), shr...) default: panic("invalid quadrant") } @@ -231,8 +190,18 @@ func prependNamespace(quadrant int, share []byte) []byte { // rootsToCids converts the EDS's Row and Column roots to CIDs. func rootsToCids(eds *rsmt2d.ExtendedDataSquare) ([]cid.Cid, error) { - var err error - roots := append(eds.RowRoots(), eds.ColRoots()...) + rowRoots, err := eds.RowRoots() + if err != nil { + return nil, err + } + colRoots, err := eds.ColRoots() + if err != nil { + return nil, err + } + + roots := make([][]byte, 0, len(rowRoots)+len(colRoots)) + roots = append(roots, rowRoots...) + roots = append(roots, colRoots...) rootCids := make([]cid.Cid, len(roots)) for i, r := range roots { rootCids[i], err = ipld.CidFromNamespacedSha256(r) @@ -271,19 +240,29 @@ func ReadEDS(ctx context.Context, r io.Reader, root share.DataHash) (eds *rsmt2d } // the stored first quadrant shares are wrapped with the namespace twice. // we cut it off here, because it is added again while importing to the tree below - shares[i] = block.RawData()[ipld.NamespaceSize:] + shares[i] = share.GetData(block.RawData()) + } + + // use proofs adder if provided, to cache collected proofs while recomputing the eds + var opts []nmt.Option + visitor := ipld.ProofsAdderFromCtx(ctx).VisitFn() + if visitor != nil { + opts = append(opts, nmt.NodeVisitor(visitor)) } eds, err = rsmt2d.ComputeExtendedDataSquare( shares, share.DefaultRSMT2DCodec(), - wrapper.NewConstructor(uint64(odsWidth)), + wrapper.NewConstructor(uint64(odsWidth), opts...), ) if err != nil { return nil, fmt.Errorf("share: computing eds: %w", err) } - newDah := da.NewDataAvailabilityHeader(eds) + newDah, err := share.NewRoot(eds) + if err != nil { + return nil, err + } if !bytes.Equal(newDah.Hash(), root) { return nil, fmt.Errorf( "share: content integrity mismatch: imported root %s doesn't match expected root %s", @@ -293,9 +272,3 @@ func ReadEDS(ctx context.Context, r io.Reader, root share.DataHash) (eds *rsmt2d } return eds, nil } - -// innerNodeBatchSize calculates the total number of inner nodes in an EDS, -// to be flushed to the dagstore in a single write. -func innerNodeBatchSize(shareCount int, odsWidth int) int { - return (shareCount * 2) - (odsWidth * 4) -} diff --git a/share/eds/eds_test.go b/share/eds/eds_test.go index 5eac847ac2..ffb05343b9 100644 --- a/share/eds/eds_test.go +++ b/share/eds/eds_test.go @@ -9,18 +9,19 @@ import ( "os" "testing" + bstore "github.com/ipfs/boxo/blockstore" ds "github.com/ipfs/go-datastore" - bstore "github.com/ipfs/go-ipfs-blockstore" carv1 "github.com/ipld/go-car" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/rand" "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/pkg/da" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share/eds/edstest" ) //go:embed "testdata/example-root.json" @@ -30,26 +31,47 @@ var exampleRoot string var f embed.FS func TestQuadrantOrder(t *testing.T) { - // TODO: add more test cases - nID := []byte{0, 0, 0, 0, 0, 0, 0, 0} - parity := append(appconsts.ParitySharesNamespaceID, nID...) //nolint - doubleNID := append(nID, nID...) //nolint - result, _ := rsmt2d.ComputeExtendedDataSquare([][]byte{ - append(nID, 1), append(nID, 2), - append(nID, 3), append(nID, 4), - }, rsmt2d.NewRSGF8Codec(), rsmt2d.NewDefaultTree) - // {{1}, {2}, {7}, {13}}, - // {{3}, {4}, {13}, {31}}, - // {{5}, {14}, {19}, {41}}, - // {{9}, {26}, {47}, {69}}, - require.Equal(t, - [][]byte{ - append(doubleNID, 1), append(doubleNID, 2), append(doubleNID, 3), append(doubleNID, 4), - append(parity, 7), append(parity, 13), append(parity, 13), append(parity, 31), - append(parity, 5), append(parity, 14), append(parity, 9), append(parity, 26), - append(parity, 19), append(parity, 41), append(parity, 47), append(parity, 69), - }, quadrantOrder(result), - ) + testCases := []struct { + name string + squareSize int + }{ + {"smol", 2}, + {"still smol", 8}, + {"default mainnet", appconsts.DefaultGovMaxSquareSize}, + {"max", share.MaxSquareSize}, + } + + testShareSize := 64 + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + shares := make([][]byte, tc.squareSize*tc.squareSize) + + for i := 0; i < tc.squareSize*tc.squareSize; i++ { + shares[i] = rand.Bytes(testShareSize) + } + + eds, err := rsmt2d.ComputeExtendedDataSquare(shares, share.DefaultRSMT2DCodec(), rsmt2d.NewDefaultTree) + require.NoError(t, err) + + res := quadrantOrder(eds) + for _, s := range res { + require.Len(t, s, testShareSize+share.NamespaceSize) + } + + for q := 0; q < 4; q++ { + for i := 0; i < tc.squareSize; i++ { + for j := 0; j < tc.squareSize; j++ { + resIndex := q*tc.squareSize*tc.squareSize + i*tc.squareSize + j + edsRow := q/2*tc.squareSize + i + edsCol := (q%2)*tc.squareSize + j + + assert.Equal(t, res[resIndex], prependNamespace(q, eds.Row(uint(edsRow))[edsCol])) + } + } + } + }) + } } func TestWriteEDS(t *testing.T) { @@ -78,7 +100,7 @@ func TestWriteEDSStartsWithLeaves(t *testing.T) { block, err := reader.Next() require.NoError(t, err, "error getting first block") - require.Equal(t, block.RawData()[ipld.NamespaceSize:], eds.GetCell(0, 0)) + require.Equal(t, share.GetData(block.RawData()), eds.GetCell(0, 0)) } func TestWriteEDSIncludesRoots(t *testing.T) { @@ -114,44 +136,27 @@ func TestWriteEDSInQuadrantOrder(t *testing.T) { } } -// TestInnerNodeBatchSize verifies that the number of unique inner nodes is equal to ipld.BatchSize -// - shareCount. -func TestInnerNodeBatchSize(t *testing.T) { - tests := []struct { - name string - origWidth int - }{ - {"2", 2}, - {"4", 4}, - {"8", 8}, - {"16", 16}, - {"32", 32}, - // {"64", 64}, // test case too large for CI with race detector - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - extendedWidth := tt.origWidth * 2 - shareCount := extendedWidth * extendedWidth - assert.Equalf( - t, - innerNodeBatchSize(shareCount, tt.origWidth), - ipld.BatchSize(extendedWidth)-shareCount, - "batchSize(%v)", extendedWidth, - ) - }) - } -} - func TestReadWriteRoundtrip(t *testing.T) { eds := writeRandomEDS(t) - dah := da.NewDataAvailabilityHeader(eds) + dah, err := share.NewRoot(eds) + require.NoError(t, err) f := openWrittenEDS(t) defer f.Close() loaded, err := ReadEDS(context.Background(), f, dah.Hash()) require.NoError(t, err, "error reading EDS from file") - require.Equal(t, eds.RowRoots(), loaded.RowRoots()) - require.Equal(t, eds.ColRoots(), loaded.ColRoots()) + + rowRoots, err := eds.RowRoots() + require.NoError(t, err) + loadedRowRoots, err := loaded.RowRoots() + require.NoError(t, err) + require.Equal(t, rowRoots, loadedRowRoots) + + colRoots, err := eds.ColRoots() + require.NoError(t, err) + loadedColRoots, err := loaded.ColRoots() + require.NoError(t, err) + require.Equal(t, colRoots, loadedColRoots) } func TestReadEDS(t *testing.T) { @@ -164,17 +169,22 @@ func TestReadEDS(t *testing.T) { loaded, err := ReadEDS(context.Background(), f, dah.Hash()) require.NoError(t, err, "error reading EDS from file") - require.Equal(t, dah.RowsRoots, loaded.RowRoots()) - require.Equal(t, dah.ColumnRoots, loaded.ColRoots()) + rowRoots, err := loaded.RowRoots() + require.NoError(t, err) + require.Equal(t, dah.RowRoots, rowRoots) + colRoots, err := loaded.ColRoots() + require.NoError(t, err) + require.Equal(t, dah.ColumnRoots, colRoots) } func TestReadEDSContentIntegrityMismatch(t *testing.T) { writeRandomEDS(t) - dah := da.NewDataAvailabilityHeader(share.RandEDS(t, 4)) + dah, err := da.NewDataAvailabilityHeader(edstest.RandEDS(t, 4)) + require.NoError(t, err) f := openWrittenEDS(t) defer f.Close() - _, err := ReadEDS(context.Background(), f, dah.Hash()) + _, err = ReadEDS(context.Background(), f, dah.Hash()) require.ErrorContains(t, err, "share: content integrity mismatch: imported root") } @@ -185,8 +195,9 @@ func BenchmarkReadWriteEDS(b *testing.B) { ctx, cancel := context.WithCancel(context.Background()) b.Cleanup(cancel) for originalDataWidth := 4; originalDataWidth <= 64; originalDataWidth *= 2 { - eds := share.RandEDS(b, originalDataWidth) - dah := da.NewDataAvailabilityHeader(eds) + eds := edstest.RandEDS(b, originalDataWidth) + dah, err := share.NewRoot(eds) + require.NoError(b, err) b.Run(fmt.Sprintf("Writing %dx%d", originalDataWidth, originalDataWidth), func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { @@ -219,7 +230,7 @@ func writeRandomEDS(t *testing.T) *rsmt2d.ExtendedDataSquare { f, err := os.OpenFile("test.car", os.O_WRONLY|os.O_CREATE, 0600) require.NoError(t, err, "error opening file") - eds := share.RandEDS(t, 4) + eds := edstest.RandEDS(t, 4) err = WriteEDS(ctx, eds, f) require.NoError(t, err, "error writing EDS to file") f.Close() @@ -253,11 +264,12 @@ func createTestData(t *testing.T, testDir string) { //nolint:unused f, err := os.OpenFile("example.car", os.O_WRONLY|os.O_CREATE, 0600) require.NoError(t, err, "opening file") - eds := share.RandEDS(t, 4) + eds := edstest.RandEDS(t, 4) err = WriteEDS(ctx, eds, f) require.NoError(t, err, "writing EDS to file") f.Close() - dah := da.NewDataAvailabilityHeader(eds) + dah, err := share.NewRoot(eds) + require.NoError(t, err) header, err := json.MarshalIndent(dah, "", "") require.NoError(t, err, "marshaling example root") diff --git a/share/eds/edstest/testing.go b/share/eds/edstest/testing.go new file mode 100644 index 0000000000..bf5e664f90 --- /dev/null +++ b/share/eds/edstest/testing.go @@ -0,0 +1,48 @@ +package edstest + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-app/pkg/wrapper" + "github.com/celestiaorg/nmt" + "github.com/celestiaorg/rsmt2d" + + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/sharetest" +) + +func RandByzantineEDS(t *testing.T, size int, options ...nmt.Option) *rsmt2d.ExtendedDataSquare { + eds := RandEDS(t, size) + shares := eds.Flattened() + copy(share.GetData(shares[0]), share.GetData(shares[1])) // corrupting eds + eds, err := rsmt2d.ImportExtendedDataSquare(shares, + share.DefaultRSMT2DCodec(), + wrapper.NewConstructor(uint64(size), + options...)) + require.NoError(t, err, "failure to recompute the extended data square") + return eds +} + +// RandEDS generates EDS filled with the random data with the given size for original square. It +// uses require.TestingT to be able to take both a *testing.T and a *testing.B. +func RandEDS(t require.TestingT, size int) *rsmt2d.ExtendedDataSquare { + shares := sharetest.RandShares(t, size*size) + eds, err := rsmt2d.ComputeExtendedDataSquare(shares, share.DefaultRSMT2DCodec(), wrapper.NewConstructor(uint64(size))) + require.NoError(t, err, "failure to recompute the extended data square") + return eds +} + +func RandEDSWithNamespace( + t require.TestingT, + namespace share.Namespace, + size int, +) (*rsmt2d.ExtendedDataSquare, *share.Root) { + shares := sharetest.RandSharesWithNamespace(t, namespace, size*size) + eds, err := rsmt2d.ComputeExtendedDataSquare(shares, share.DefaultRSMT2DCodec(), wrapper.NewConstructor(uint64(size))) + require.NoError(t, err, "failure to recompute the extended data square") + dah, err := share.NewRoot(eds) + require.NoError(t, err) + return eds, dah +} diff --git a/share/eds/inverted_index.go b/share/eds/inverted_index.go index f917619676..f30d1c37db 100644 --- a/share/eds/inverted_index.go +++ b/share/eds/inverted_index.go @@ -2,16 +2,23 @@ package eds import ( "context" - "encoding/json" + "errors" "fmt" + "runtime" + "github.com/dgraph-io/badger/v4/options" "github.com/filecoin-project/dagstore/index" "github.com/filecoin-project/dagstore/shard" ds "github.com/ipfs/go-datastore" - "github.com/ipfs/go-datastore/namespace" + dsbadger "github.com/ipfs/go-ds-badger4" "github.com/multiformats/go-multihash" ) +const invertedIndexPath = "/inverted_index/" + +// ErrNotFoundInIndex is returned instead of ErrNotFound if the multihash doesn't exist in the index +var ErrNotFoundInIndex = fmt.Errorf("does not exist in index") + // simpleInvertedIndex is an inverted index that only stores a single shard key per multihash. Its // implementation is modified from the default upstream implementation in dagstore/index. type simpleInvertedIndex struct { @@ -21,10 +28,33 @@ type simpleInvertedIndex struct { // newSimpleInvertedIndex returns a new inverted index that only stores a single shard key per // multihash. This is because we use badger as a storage backend, so updates are expensive, and we // don't care which shard is used to serve a cid. -func newSimpleInvertedIndex(dts ds.Batching) *simpleInvertedIndex { - return &simpleInvertedIndex{ - ds: namespace.Wrap(dts, ds.NewKey("/inverted/index")), +func newSimpleInvertedIndex(storePath string) (*simpleInvertedIndex, error) { + opts := dsbadger.DefaultOptions // this should be copied + // turn off value log GC as we don't use value log + opts.GcInterval = 0 + // use minimum amount of NumLevelZeroTables to trigger L0 compaction faster + opts.NumLevelZeroTables = 1 + // MaxLevels = 8 will allow the db to grow to ~11.1 TiB + opts.MaxLevels = 8 + // inverted index stores unique hash keys, so we don't need to detect conflicts + opts.DetectConflicts = false + // we don't need compression for inverted index as it just hashes + opts.Compression = options.None + compactors := runtime.NumCPU() + if compactors < 2 { + compactors = 2 + } + if compactors > opts.MaxLevels { // ensure there is no more compactors than db table levels + compactors = opts.MaxLevels + } + opts.NumCompactors = compactors + + ds, err := dsbadger.NewDatastore(storePath+invertedIndexPath, &opts) + if err != nil { + return nil, fmt.Errorf("can't open Badger Datastore: %w", err) } + + return &simpleInvertedIndex{ds: ds}, nil } func (s *simpleInvertedIndex) AddMultihashesForShard( @@ -40,35 +70,20 @@ func (s *simpleInvertedIndex) AddMultihashesForShard( return fmt.Errorf("failed to create ds batch: %w", err) } - if err := mhIter.ForEach(func(mh multihash.Multihash) error { + err = mhIter.ForEach(func(mh multihash.Multihash) error { key := ds.NewKey(string(mh)) - ok, err := s.ds.Has(ctx, key) - if err != nil { - return fmt.Errorf("failed to check if value for multihash exists %s, err: %w", mh, err) + if err := batch.Put(ctx, key, []byte(sk.String())); err != nil { + return fmt.Errorf("failed to put mh=%s, err=%w", mh, err) } - - if !ok { - bz, err := json.Marshal(sk) - if err != nil { - return fmt.Errorf("failed to marshal shard key to bytes: %w", err) - } - if err := batch.Put(ctx, key, bz); err != nil { - return fmt.Errorf("failed to put mh=%s, err=%w", mh, err) - } - } - return nil - }); err != nil { + }) + if err != nil { return fmt.Errorf("failed to add index entry: %w", err) } if err := batch.Commit(ctx); err != nil { return fmt.Errorf("failed to commit batch: %w", err) } - - if err := s.ds.Sync(ctx, ds.Key{}); err != nil { - return fmt.Errorf("failed to sync puts: %w", err) - } return nil } @@ -76,13 +91,12 @@ func (s *simpleInvertedIndex) GetShardsForMultihash(ctx context.Context, mh mult key := ds.NewKey(string(mh)) sbz, err := s.ds.Get(ctx, key) if err != nil { - return nil, fmt.Errorf("failed to lookup index for mh %s, err: %w", mh, err) + return nil, errors.Join(ErrNotFoundInIndex, err) } - var shardKey shard.Key - if err := json.Unmarshal(sbz, &shardKey); err != nil { - return nil, fmt.Errorf("failed to unmarshal shard key for mh=%s, err=%w", mh, err) - } + return []shard.Key{shard.KeyFromString(string(sbz))}, nil +} - return []shard.Key{shardKey}, nil +func (s *simpleInvertedIndex) close() error { + return s.ds.Close() } diff --git a/share/eds/inverted_index_test.go b/share/eds/inverted_index_test.go index f228aa0d92..e83c2be267 100644 --- a/share/eds/inverted_index_test.go +++ b/share/eds/inverted_index_test.go @@ -5,8 +5,6 @@ import ( "testing" "github.com/filecoin-project/dagstore/shard" - "github.com/ipfs/go-datastore" - ds_sync "github.com/ipfs/go-datastore/sync" "github.com/multiformats/go-multihash" "github.com/stretchr/testify/require" ) @@ -37,20 +35,21 @@ func TestMultihashesForShard(t *testing.T) { } mi := &mockIterator{mhs: mhs} - ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - invertedIndex := newSimpleInvertedIndex(ds) + path := t.TempDir() + invertedIndex, err := newSimpleInvertedIndex(path) + require.NoError(t, err) // 1. Add all 3 multihashes to shard1 - err := invertedIndex.AddMultihashesForShard(ctx, mi, shard.KeyFromString("shard1")) + err = invertedIndex.AddMultihashesForShard(ctx, mi, shard.KeyFromString("shard1")) require.NoError(t, err) shardKeys, err := invertedIndex.GetShardsForMultihash(ctx, mhs[0]) require.NoError(t, err) require.Equal(t, []shard.Key{shard.KeyFromString("shard1")}, shardKeys) - // 2. Add mh1 to shard2, and ensure that mh1 still points to shard1 + // 2. Add mh1 to shard2, and ensure that mh1 no longer points to shard1 err = invertedIndex.AddMultihashesForShard(ctx, &mockIterator{mhs: mhs[:1]}, shard.KeyFromString("shard2")) require.NoError(t, err) shardKeys, err = invertedIndex.GetShardsForMultihash(ctx, mhs[0]) require.NoError(t, err) - require.Equal(t, []shard.Key{shard.KeyFromString("shard1")}, shardKeys) + require.Equal(t, []shard.Key{shard.KeyFromString("shard2")}, shardKeys) } diff --git a/share/eds/metrics.go b/share/eds/metrics.go new file mode 100644 index 0000000000..8d69a3ec41 --- /dev/null +++ b/share/eds/metrics.go @@ -0,0 +1,270 @@ +package eds + +import ( + "context" + "time" + + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/metric" + + "github.com/celestiaorg/celestia-node/libs/utils" +) + +const ( + failedKey = "failed" + sizeKey = "eds_size" + + putResultKey = "result" + putOK putResult = "ok" + putExists putResult = "exists" + putFailed putResult = "failed" + + opNameKey = "op" + longOpResultKey = "result" + longOpUnresolved longOpResult = "unresolved" + longOpOK longOpResult = "ok" + longOpFailed longOpResult = "failed" + + dagstoreShardStatusKey = "shard_status" +) + +var meter = otel.Meter("eds_store") + +type putResult string + +type longOpResult string + +type metrics struct { + putTime metric.Float64Histogram + getCARTime metric.Float64Histogram + getCARBlockstoreTime metric.Float64Histogram + getDAHTime metric.Float64Histogram + removeTime metric.Float64Histogram + getTime metric.Float64Histogram + hasTime metric.Float64Histogram + listTime metric.Float64Histogram + + shardFailureCount metric.Int64Counter + + longOpTime metric.Float64Histogram + gcTime metric.Float64Histogram +} + +func (s *Store) WithMetrics() error { + putTime, err := meter.Float64Histogram("eds_store_put_time_histogram", + metric.WithDescription("eds store put time histogram(s)")) + if err != nil { + return err + } + + getCARTime, err := meter.Float64Histogram("eds_store_get_car_time_histogram", + metric.WithDescription("eds store get car time histogram(s)")) + if err != nil { + return err + } + + getCARBlockstoreTime, err := meter.Float64Histogram("eds_store_get_car_blockstore_time_histogram", + metric.WithDescription("eds store get car blockstore time histogram(s)")) + if err != nil { + return err + } + + getDAHTime, err := meter.Float64Histogram("eds_store_get_dah_time_histogram", + metric.WithDescription("eds store get dah time histogram(s)")) + if err != nil { + return err + } + + removeTime, err := meter.Float64Histogram("eds_store_remove_time_histogram", + metric.WithDescription("eds store remove time histogram(s)")) + if err != nil { + return err + } + + getTime, err := meter.Float64Histogram("eds_store_get_time_histogram", + metric.WithDescription("eds store get time histogram(s)")) + if err != nil { + return err + } + + hasTime, err := meter.Float64Histogram("eds_store_has_time_histogram", + metric.WithDescription("eds store has time histogram(s)")) + if err != nil { + return err + } + + listTime, err := meter.Float64Histogram("eds_store_list_time_histogram", + metric.WithDescription("eds store list time histogram(s)")) + if err != nil { + return err + } + + shardFailureCount, err := meter.Int64Counter("eds_store_shard_failure_counter", + metric.WithDescription("eds store OpShardFail counter")) + if err != nil { + return err + } + + longOpTime, err := meter.Float64Histogram("eds_store_long_operation_time_histogram", + metric.WithDescription("eds store long operation time histogram(s)")) + if err != nil { + return err + } + + gcTime, err := meter.Float64Histogram("eds_store_gc_time", + metric.WithDescription("dagstore gc time histogram(s)")) + if err != nil { + return err + } + + dagStoreShards, err := meter.Int64ObservableGauge("eds_store_dagstore_shards", + metric.WithDescription("dagstore amount of shards by status")) + if err != nil { + return err + } + + if err = s.cache.Load().EnableMetrics(); err != nil { + return err + } + + callback := func(ctx context.Context, observer metric.Observer) error { + stats := s.dgstr.Stats() + for status, amount := range stats { + observer.ObserveInt64(dagStoreShards, int64(amount), + metric.WithAttributes( + attribute.String(dagstoreShardStatusKey, status.String()), + )) + } + return nil + } + + if _, err := meter.RegisterCallback(callback, dagStoreShards); err != nil { + return err + } + + s.metrics = &metrics{ + putTime: putTime, + getCARTime: getCARTime, + getCARBlockstoreTime: getCARBlockstoreTime, + getDAHTime: getDAHTime, + removeTime: removeTime, + getTime: getTime, + hasTime: hasTime, + listTime: listTime, + shardFailureCount: shardFailureCount, + longOpTime: longOpTime, + gcTime: gcTime, + } + return nil +} + +func (m *metrics) observeGCtime(ctx context.Context, dur time.Duration, failed bool) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + m.gcTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} + +func (m *metrics) observeShardFailure(ctx context.Context, shardKey string) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.shardFailureCount.Add(ctx, 1, metric.WithAttributes(attribute.String("shard_key", shardKey))) +} + +func (m *metrics) observePut(ctx context.Context, dur time.Duration, result putResult, size uint) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.putTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.String(putResultKey, string(result)), + attribute.Int(sizeKey, int(size)))) +} + +func (m *metrics) observeLongOp(ctx context.Context, opName string, dur time.Duration, result longOpResult) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.longOpTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.String(opNameKey, opName), + attribute.String(longOpResultKey, string(result)))) +} + +func (m *metrics) observeGetCAR(ctx context.Context, dur time.Duration, failed bool) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.getCARTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} + +func (m *metrics) observeCARBlockstore(ctx context.Context, dur time.Duration, failed bool) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.getCARBlockstoreTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} + +func (m *metrics) observeGetDAH(ctx context.Context, dur time.Duration, failed bool) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.getDAHTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} + +func (m *metrics) observeRemove(ctx context.Context, dur time.Duration, failed bool) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.removeTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} + +func (m *metrics) observeGet(ctx context.Context, dur time.Duration, failed bool) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.getTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} + +func (m *metrics) observeHas(ctx context.Context, dur time.Duration, failed bool) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.hasTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} + +func (m *metrics) observeList(ctx context.Context, dur time.Duration, failed bool) { + if m == nil { + return + } + ctx = utils.ResetContextOnError(ctx) + + m.listTime.Record(ctx, dur.Seconds(), metric.WithAttributes( + attribute.Bool(failedKey, failed))) +} diff --git a/share/eds/ods_test.go b/share/eds/ods_test.go index 75fad3022e..0f7c69e708 100644 --- a/share/eds/ods_test.go +++ b/share/eds/ods_test.go @@ -32,6 +32,9 @@ func TestODSReader(t *testing.T) { // get CAR reader from store r, err := edsStore.GetCAR(ctx, dah.Hash()) assert.NoError(t, err) + defer func() { + require.NoError(t, r.Close()) + }() // create ODSReader wrapper based on car reader to limit reads to ODS only odsR, err := ODSReader(r) @@ -52,7 +55,7 @@ func TestODSReader(t *testing.T) { assert.NoError(t, err) // check that original data from eds is same as data from reader - assert.Equal(t, original, block.RawData()[share.NamespaceSize:]) + assert.Equal(t, original, share.GetData(block.RawData())) } } @@ -81,6 +84,9 @@ func TestODSReaderReconstruction(t *testing.T) { // get CAR reader from store r, err := edsStore.GetCAR(ctx, dah.Hash()) assert.NoError(t, err) + defer func() { + require.NoError(t, r.Close()) + }() // create ODSReader wrapper based on car reader to limit reads to ODS only odsR, err := ODSReader(r) @@ -89,6 +95,16 @@ func TestODSReaderReconstruction(t *testing.T) { // reconstruct EDS from ODSReader loaded, err := ReadEDS(ctx, odsR, dah.Hash()) assert.NoError(t, err) - require.Equal(t, eds.RowRoots(), loaded.RowRoots()) - require.Equal(t, eds.ColRoots(), loaded.ColRoots()) + + rowRoots, err := eds.RowRoots() + require.NoError(t, err) + loadedRowRoots, err := loaded.RowRoots() + require.NoError(t, err) + require.Equal(t, rowRoots, loadedRowRoots) + + colRoots, err := eds.ColRoots() + require.NoError(t, err) + loadedColRoots, err := loaded.ColRoots() + require.NoError(t, err) + require.Equal(t, colRoots, loadedColRoots) } diff --git a/share/eds/retriever.go b/share/eds/retriever.go index 5d3c61cabb..c2966c3953 100644 --- a/share/eds/retriever.go +++ b/share/eds/retriever.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "time" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" "go.opentelemetry.io/otel" @@ -17,6 +17,7 @@ import ( "github.com/celestiaorg/celestia-app/pkg/da" "github.com/celestiaorg/celestia-app/pkg/wrapper" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/share" @@ -63,11 +64,10 @@ func (r *Retriever) Retrieve(ctx context.Context, dah *da.DataAvailabilityHeader ctx, span := tracer.Start(ctx, "retrieve-square") defer span.End() span.SetAttributes( - attribute.Int("size", len(dah.RowsRoots)), - attribute.String("data_hash", dah.String()), + attribute.Int("size", len(dah.RowRoots)), ) - log.Debugw("retrieving data square", "data_hash", dah.String(), "size", len(dah.RowsRoots)) + log.Debugw("retrieving data square", "data_hash", dah.String(), "size", len(dah.RowRoots)) ses, err := r.newSession(ctx, dah) if err != nil { return nil, err @@ -121,13 +121,21 @@ type retrievalSession struct { // newSession creates a new retrieval session and kicks off requesting process. func (r *Retriever) newSession(ctx context.Context, dah *da.DataAvailabilityHeader) (*retrievalSession, error) { - size := len(dah.RowsRoots) + size := len(dah.RowRoots) + treeFn := func(_ rsmt2d.Axis, index uint) rsmt2d.Tree { - tree := wrapper.NewErasuredNamespacedMerkleTree(uint64(size)/2, index) + // use proofs adder if provided, to cache collected proofs while recomputing the eds + var opts []nmt.Option + visitor := ipld.ProofsAdderFromCtx(ctx).VisitFn() + if visitor != nil { + opts = append(opts, nmt.NodeVisitor(visitor)) + } + + tree := wrapper.NewErasuredNamespacedMerkleTree(uint64(size)/2, index, opts...) return &tree } - square, err := rsmt2d.ImportExtendedDataSquare(make([][]byte, size*size), share.DefaultRSMT2DCodec(), treeFn) + square, err := rsmt2d.NewExtendedDataSquare(share.DefaultRSMT2DCodec(), treeFn, uint(size), share.Size) if err != nil { return nil, err } @@ -145,6 +153,7 @@ func (r *Retriever) newSession(ctx context.Context, dah *da.DataAvailabilityHead for i := range ses.squareCellsLks { ses.squareCellsLks[i] = make([]sync.Mutex, size) } + go ses.request(ctx) return ses, nil } @@ -169,12 +178,12 @@ func (rs *retrievalSession) Reconstruct(ctx context.Context) (*rsmt2d.ExtendedDa defer span.End() // and try to repair with what we have - err := rs.square.Repair(rs.dah.RowsRoots, rs.dah.ColumnRoots) + err := rs.square.Repair(rs.dah.RowRoots, rs.dah.ColumnRoots) if err != nil { span.RecordError(err) return nil, err } - log.Infow("data square reconstructed", "data_hash", rs.dah.String(), "size", len(rs.dah.RowsRoots)) + log.Infow("data square reconstructed", "data_hash", rs.dah.String(), "size", len(rs.dah.RowRoots)) close(rs.squareDn) return rs.square, nil } @@ -248,7 +257,6 @@ func (rs *retrievalSession) doRequest(ctx context.Context, q *quadrant) { nd, err := ipld.GetNode(ctx, rs.bget, root) if err != nil { rs.span.RecordError(err, trace.WithAttributes( - attribute.String("requesting-root", root.String()), attribute.Int("root-index", i), )) return @@ -256,7 +264,7 @@ func (rs *retrievalSession) doRequest(ctx context.Context, q *quadrant) { // and go get shares of left or the right side of the whole col/row axis // the left or the right side of the tree represent some portion of the quadrant // which we put into the rs.square share-by-share by calculating shares' indexes using q.index - share.GetShares(ctx, rs.bget, nd.Links()[q.x].Cid, size, func(j int, share share.Share) { + ipld.GetShares(ctx, rs.bget, nd.Links()[q.x].Cid, size, func(j int, share share.Share) { // NOTE: Each share can appear twice here, for a Row and Col, respectively. // These shares are always equal, and we allow only the first one to be written // in the square. @@ -283,10 +291,12 @@ func (rs *retrievalSession) doRequest(ctx context.Context, q *quadrant) { if rs.isReconstructed() { return } - if rs.square.GetCell(uint(x), uint(y)) != nil { + if err := rs.square.SetCell(uint(x), uint(y), share); err != nil { + // safe to ignore as: + // * share size already verified + // * the same share might come from either Row or Col return } - rs.square.SetCell(uint(x), uint(y), share) // if we have >= 1/4 of the square we can start trying to Reconstruct // TODO(@Wondertan): This is not an ideal way to know when to start // reconstruction and can cause idle reconstruction tries in some cases, diff --git a/share/eds/retriever_no_race_test.go b/share/eds/retriever_no_race_test.go new file mode 100644 index 0000000000..15c6aa2fc4 --- /dev/null +++ b/share/eds/retriever_no_race_test.go @@ -0,0 +1,55 @@ +// go:build !race + +package eds + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/celestia-app/pkg/wrapper" + "github.com/celestiaorg/nmt" + "github.com/celestiaorg/rsmt2d" + + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/byzantine" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/ipld" +) + +func TestRetriever_ByzantineError(t *testing.T) { + const width = 8 + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + bserv := ipld.NewMemBlockservice() + shares := edstest.RandEDS(t, width).Flattened() + _, err := ipld.ImportShares(ctx, shares, bserv) + require.NoError(t, err) + + // corrupt shares so that eds erasure coding does not match + copy(shares[14][share.NamespaceSize:], shares[15][share.NamespaceSize:]) + + // import corrupted eds + batchAdder := ipld.NewNmtNodeAdder(ctx, bserv, ipld.MaxSizeBatchOption(width*2)) + attackerEDS, err := rsmt2d.ImportExtendedDataSquare( + shares, + share.DefaultRSMT2DCodec(), + wrapper.NewConstructor(uint64(width), + nmt.NodeVisitor(batchAdder.Visit)), + ) + require.NoError(t, err) + err = batchAdder.Commit() + require.NoError(t, err) + + // ensure we rcv an error + dah, err := da.NewDataAvailabilityHeader(attackerEDS) + require.NoError(t, err) + r := NewRetriever(bserv) + _, err = r.Retrieve(ctx, &dah) + var errByz *byzantine.ErrByzantine + require.ErrorAs(t, err, &errByz) +} diff --git a/share/eds/retriever_quadrant.go b/share/eds/retriever_quadrant.go index 8b8037ce85..3d616e9cd4 100644 --- a/share/eds/retriever_quadrant.go +++ b/share/eds/retriever_quadrant.go @@ -52,7 +52,7 @@ type quadrant struct { func newQuadrants(dah *da.DataAvailabilityHeader) []*quadrant { // combine all the roots into one slice, so they can be easily accessible by index daRoots := [][][]byte{ - dah.RowsRoots, + dah.RowRoots, dah.ColumnRoots, } // create a quadrant slice for each source(row;col) diff --git a/share/eds/retriever_test.go b/share/eds/retriever_test.go index 1c98c133c4..95da345d17 100644 --- a/share/eds/retriever_test.go +++ b/share/eds/retriever_test.go @@ -3,31 +3,31 @@ package eds import ( "context" "errors" + "fmt" "testing" "time" - "github.com/ipfs/go-blockservice" - mdutils "github.com/ipfs/go-merkledag/test" + "github.com/ipfs/boxo/blockservice" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/celestiaorg/celestia-app/pkg/da" - "github.com/celestiaorg/celestia-app/pkg/wrapper" - "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds/byzantine" + "github.com/celestiaorg/celestia-node/share/eds/edstest" "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share/sharetest" ) func TestRetriever_Retrieve(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() r := NewRetriever(bServ) type test struct { @@ -48,55 +48,23 @@ func TestRetriever_Retrieve(t *testing.T) { tc := tc t.Run(tc.name, func(t *testing.T) { // generate EDS - shares := share.RandShares(t, tc.squareSize*tc.squareSize) - in, err := share.AddShares(ctx, shares, bServ) + shares := sharetest.RandShares(t, tc.squareSize*tc.squareSize) + in, err := ipld.AddShares(ctx, shares, bServ) require.NoError(t, err) // limit with timeout, specifically retrieval ctx, cancel := context.WithTimeout(ctx, time.Minute*5) // the timeout is big for the max size which is long defer cancel() - dah := da.NewDataAvailabilityHeader(in) + dah, err := da.NewDataAvailabilityHeader(in) + require.NoError(t, err) out, err := r.Retrieve(ctx, &dah) require.NoError(t, err) - assert.True(t, share.EqualEDS(in, out)) + assert.True(t, in.Equals(out)) }) } } -func TestRetriever_ByzantineError(t *testing.T) { - const width = 8 - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - bserv := mdutils.Bserv() - shares := share.ExtractEDS(share.RandEDS(t, width)) - _, err := share.ImportShares(ctx, shares, bserv) - require.NoError(t, err) - - // corrupt shares so that eds erasure coding does not match - copy(shares[14][8:], shares[15][8:]) - - // import corrupted eds - batchAdder := ipld.NewNmtNodeAdder(ctx, bserv, ipld.MaxSizeBatchOption(width*2)) - attackerEDS, err := rsmt2d.ImportExtendedDataSquare( - shares, - share.DefaultRSMT2DCodec(), - wrapper.NewConstructor(uint64(width), - nmt.NodeVisitor(batchAdder.Visit)), - ) - require.NoError(t, err) - err = batchAdder.Commit() - require.NoError(t, err) - - // ensure we rcv an error - dah := da.NewDataAvailabilityHeader(attackerEDS) - r := NewRetriever(bserv) - _, err = r.Retrieve(ctx, &dah) - var errByz *byzantine.ErrByzantine - require.ErrorAs(t, err, &errByz) -} - // TestRetriever_MultipleRandQuadrants asserts that reconstruction succeeds // when any three random quadrants requested. func TestRetriever_MultipleRandQuadrants(t *testing.T) { @@ -105,15 +73,16 @@ func TestRetriever_MultipleRandQuadrants(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() r := NewRetriever(bServ) // generate EDS - shares := share.RandShares(t, squareSize*squareSize) - in, err := share.AddShares(ctx, shares, bServ) + shares := sharetest.RandShares(t, squareSize*squareSize) + in, err := ipld.AddShares(ctx, shares, bServ) require.NoError(t, err) - dah := da.NewDataAvailabilityHeader(in) + dah, err := da.NewDataAvailabilityHeader(in) + require.NoError(t, err) ses, err := r.newSession(ctx, &dah) require.NoError(t, err) @@ -130,27 +99,116 @@ func TestRetriever_MultipleRandQuadrants(t *testing.T) { func TestFraudProofValidation(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer t.Cleanup(cancel) - bServ := mdutils.Bserv() + bServ := ipld.NewMemBlockservice() - var errByz *byzantine.ErrByzantine - faultHeader, err := generateByzantineError(ctx, t, bServ) - require.True(t, errors.As(err, &errByz)) + odsSize := []int{2, 4, 16, 32, 64, 128} + for _, size := range odsSize { + t.Run(fmt.Sprintf("ods size:%d", size), func(t *testing.T) { + var errByz *byzantine.ErrByzantine + faultHeader, err := generateByzantineError(ctx, t, size, bServ) + require.True(t, errors.As(err, &errByz)) - p := byzantine.CreateBadEncodingProof([]byte("hash"), uint64(faultHeader.Height()), errByz) - err = p.Validate(faultHeader) - require.NoError(t, err) + p := byzantine.CreateBadEncodingProof([]byte("hash"), faultHeader.Height(), errByz) + err = p.Validate(faultHeader) + require.NoError(t, err) + }) + } } func generateByzantineError( ctx context.Context, t *testing.T, + odsSize int, bServ blockservice.BlockService, ) (*header.ExtendedHeader, error) { - store := headertest.NewStore(t) - h, err := store.GetByHeight(ctx, 1) + eds := edstest.RandByzantineEDS(t, odsSize) + err := ipld.ImportEDS(ctx, eds, bServ) require.NoError(t, err) + h := headertest.ExtendedHeaderFromEDS(t, 1, eds) + _, err = NewRetriever(bServ).Retrieve(ctx, h.DAH) + + return h, err +} + +/* +BenchmarkBEFPValidation/ods_size:2 31273 38819 ns/op 68052 B/op 366 allocs/op +BenchmarkBEFPValidation/ods_size:4 14664 80439 ns/op 135892 B/op 894 allocs/op +BenchmarkBEFPValidation/ods_size:16 2850 386178 ns/op 587890 B/op 4945 allocs/op +BenchmarkBEFPValidation/ods_size:32 1399 874490 ns/op 1233399 B/op 11284 allocs/op +BenchmarkBEFPValidation/ods_size:64 619 2047540 ns/op 2578008 B/op 25364 allocs/op +BenchmarkBEFPValidation/ods_size:128 259 4934375 ns/op 5418406 B/op 56345 allocs/op +*/ +func BenchmarkBEFPValidation(b *testing.B) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer b.Cleanup(cancel) + bServ := ipld.NewMemBlockservice() + r := NewRetriever(bServ) + t := &testing.T{} + odsSize := []int{2, 4, 16, 32, 64, 128} + for _, size := range odsSize { + b.Run(fmt.Sprintf("ods size:%d", size), func(b *testing.B) { + b.ResetTimer() + b.StopTimer() + eds := edstest.RandByzantineEDS(t, size) + err := ipld.ImportEDS(ctx, eds, bServ) + require.NoError(t, err) + h := headertest.ExtendedHeaderFromEDS(t, 1, eds) + _, err = r.Retrieve(ctx, h.DAH) + var errByz *byzantine.ErrByzantine + require.ErrorAs(t, err, &errByz) + b.StartTimer() + + for i := 0; i < b.N; i++ { + b.ReportAllocs() + p := byzantine.CreateBadEncodingProof([]byte("hash"), h.Height(), errByz) + err = p.Validate(h) + require.NoError(b, err) + } + }) + } +} - faultHeader, _ := headertest.CreateFraudExtHeader(t, h, bServ) - _, err = NewRetriever(bServ).Retrieve(ctx, faultHeader.DAH) - return faultHeader, err +/* +BenchmarkNewErrByzantineData/ods_size:2 29605 38846 ns/op 49518 B/op 579 allocs/op +BenchmarkNewErrByzantineData/ods_size:4 11380 105302 ns/op 134967 B/op 1571 allocs/op +BenchmarkNewErrByzantineData/ods_size:16 1902 631086 ns/op 830199 B/op 9601 allocs/op +BenchmarkNewErrByzantineData/ods_size:32 756 1530985 ns/op 1985272 B/op 22901 allocs/op +BenchmarkNewErrByzantineData/ods_size:64 340 3445544 ns/op 4767053 B/op 54704 allocs/op +BenchmarkNewErrByzantineData/ods_size:128 132 8740678 ns/op 11991093 B/op 136584 allocs/op +*/ +func BenchmarkNewErrByzantineData(b *testing.B) { + odsSize := []int{2, 4, 16, 32, 64, 128} + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + bServ := ipld.NewMemBlockservice() + r := NewRetriever(bServ) + t := &testing.T{} + for _, size := range odsSize { + b.Run(fmt.Sprintf("ods size:%d", size), func(b *testing.B) { + b.StopTimer() + eds := edstest.RandByzantineEDS(t, size) + err := ipld.ImportEDS(ctx, eds, bServ) + require.NoError(t, err) + h := headertest.ExtendedHeaderFromEDS(t, 1, eds) + ses, err := r.newSession(ctx, h.DAH) + require.NoError(t, err) + + select { + case <-ctx.Done(): + b.Fatal(ctx.Err()) + case <-ses.Done(): + } + + _, err = ses.Reconstruct(ctx) + assert.NoError(t, err) + var errByz *rsmt2d.ErrByzantineData + require.ErrorAs(t, err, &errByz) + b.StartTimer() + + for i := 0; i < b.N; i++ { + err = byzantine.NewErrByzantine(ctx, bServ, h.DAH, errByz) + require.NotNil(t, err) + } + }) + } } diff --git a/share/eds/store.go b/share/eds/store.go index 8222d04dd6..816065909e 100644 --- a/share/eds/store.go +++ b/share/eds/store.go @@ -8,6 +8,7 @@ import ( "fmt" "io" "os" + "sync" "sync/atomic" "time" @@ -15,17 +16,17 @@ import ( "github.com/filecoin-project/dagstore/index" "github.com/filecoin-project/dagstore/mount" "github.com/filecoin-project/dagstore/shard" + bstore "github.com/ipfs/boxo/blockstore" "github.com/ipfs/go-datastore" - bstore "github.com/ipfs/go-ipfs-blockstore" carv1 "github.com/ipld/go-car" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" "github.com/celestiaorg/rsmt2d" - "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/cache" "github.com/celestiaorg/celestia-node/share/ipld" ) @@ -33,8 +34,6 @@ const ( blocksPath = "/blocks/" indexPath = "/index/" transientsPath = "/transients/" - - defaultGCInterval = time.Hour ) var ErrNotFound = errors.New("eds not found in store") @@ -49,65 +48,90 @@ type Store struct { dgstr *dagstore.DAGStore mounts *mount.Registry - cache *blockstoreCache - bs bstore.Blockstore + bs *blockstore + cache atomic.Pointer[cache.DoubleCache] - topIdx index.Inverted - carIdx index.FullIndexRepo + carIdx index.FullIndexRepo + invertedIdx *simpleInvertedIndex basepath string gcInterval time.Duration // lastGCResult is only stored on the store for testing purposes. lastGCResult atomic.Pointer[dagstore.GCResult] + + // stripedLocks is used to synchronize parallel operations + stripedLocks [256]sync.Mutex + shardFailures chan dagstore.ShardResult + + metrics *metrics } // NewStore creates a new EDS Store under the given basepath and datastore. -func NewStore(basepath string, ds datastore.Batching) (*Store, error) { - err := setupPath(basepath) +func NewStore(params *Parameters, basePath string, ds datastore.Batching) (*Store, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + err := setupPath(basePath) if err != nil { return nil, fmt.Errorf("failed to setup eds.Store directories: %w", err) } r := mount.NewRegistry() - err = r.Register("fs", &mount.FileMount{Path: basepath + blocksPath}) + err = r.Register("fs", &inMemoryOnceMount{}) + if err != nil { + return nil, fmt.Errorf("failed to register memory mount on the registry: %w", err) + } if err != nil { return nil, fmt.Errorf("failed to register FS mount on the registry: %w", err) } - fsRepo, err := index.NewFSRepo(basepath + indexPath) + fsRepo, err := index.NewFSRepo(basePath + indexPath) if err != nil { return nil, fmt.Errorf("failed to create index repository: %w", err) } - invertedRepo := newSimpleInvertedIndex(ds) + invertedIdx, err := newSimpleInvertedIndex(basePath) + if err != nil { + return nil, fmt.Errorf("failed to create index: %w", err) + } + + failureChan := make(chan dagstore.ShardResult) dagStore, err := dagstore.NewDAGStore( dagstore.Config{ - TransientsDir: basepath + transientsPath, + TransientsDir: basePath + transientsPath, IndexRepo: fsRepo, Datastore: ds, MountRegistry: r, - TopLevelIndex: invertedRepo, + TopLevelIndex: invertedIdx, + FailureCh: failureChan, }, ) if err != nil { return nil, fmt.Errorf("failed to create DAGStore: %w", err) } - cache, err := newBlockstoreCache(defaultCacheSize) + recentBlocksCache, err := cache.NewAccessorCache("recent", params.RecentBlocksCacheSize) + if err != nil { + return nil, fmt.Errorf("failed to create recent blocks cache: %w", err) + } + + blockstoreCache, err := cache.NewAccessorCache("blockstore", params.BlockstoreCacheSize) if err != nil { return nil, fmt.Errorf("failed to create blockstore cache: %w", err) } store := &Store{ - basepath: basepath, - dgstr: dagStore, - topIdx: invertedRepo, - carIdx: fsRepo, - gcInterval: defaultGCInterval, - mounts: r, - cache: cache, - } - store.bs = newBlockstore(store, cache) + basepath: basePath, + dgstr: dagStore, + carIdx: fsRepo, + invertedIdx: invertedIdx, + gcInterval: params.GCInterval, + mounts: r, + shardFailures: failureChan, + } + store.bs = newBlockstore(store, ds) + store.cache.Store(cache.NewDoubleCache(recentBlocksCache, blockstoreCache)) return store, nil } @@ -117,38 +141,64 @@ func (s *Store) Start(ctx context.Context) error { return err } // start Store only if DagStore succeeds - ctx, cancel := context.WithCancel(context.Background()) + runCtx, cancel := context.WithCancel(context.Background()) s.cancel = cancel - go s.gc(ctx) + // initialize empty gc result to avoid panic on access + s.lastGCResult.Store(&dagstore.GCResult{ + Shards: make(map[shard.Key]error), + }) + + if s.gcInterval != 0 { + go s.gc(runCtx) + } + + go s.watchForFailures(runCtx) return nil } // Stop stops the underlying DAGStore. func (s *Store) Stop(context.Context) error { defer s.cancel() + if err := s.invertedIdx.close(); err != nil { + return err + } return s.dgstr.Close() } // gc periodically removes all inactive or errored shards. func (s *Store) gc(ctx context.Context) { ticker := time.NewTicker(s.gcInterval) - // initialize empty gc result to avoid panic on access - s.lastGCResult.Store(&dagstore.GCResult{ - Shards: make(map[shard.Key]error), - }) for { select { case <-ctx.Done(): return case <-ticker.C: + tnow := time.Now() res, err := s.dgstr.GC(ctx) + s.metrics.observeGCtime(ctx, time.Since(tnow), err != nil) if err != nil { log.Errorf("garbage collecting dagstore: %v", err) return } s.lastGCResult.Store(res) } + } +} +func (s *Store) watchForFailures(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case res := <-s.shardFailures: + log.Errorw("removing shard after failure", "key", res.Key, "err", res.Error) + s.metrics.observeShardFailure(ctx, res.Key.String()) + k := share.MustDataHashFromString(res.Key.String()) + err := s.Remove(ctx, k) + if err != nil { + log.Errorw("failed to remove shard after failure", "key", res.Key, "err", err) + } + } } } @@ -157,52 +207,125 @@ func (s *Store) gc(ctx context.Context) { // The square is verified on the Exchange level, and Put only stores the square, trusting it. // The resulting file stores all the shares and NMT Merkle Proofs of the EDS. // Additionally, the file gets indexed s.t. store.Blockstore can access them. -func (s *Store) Put(ctx context.Context, root share.DataHash, square *rsmt2d.ExtendedDataSquare) (err error) { - // if root already exists, short-circuit - has, err := s.Has(ctx, root) - if err != nil { - return fmt.Errorf("failed to check if root already exists in index: %w", err) - } - if has { - return dagstore.ErrShardExists - } - +func (s *Store) Put(ctx context.Context, root share.DataHash, square *rsmt2d.ExtendedDataSquare) error { ctx, span := tracer.Start(ctx, "store/put", trace.WithAttributes( - attribute.String("root", root.String()), attribute.Int("width", int(square.Width())), )) - defer func() { - utils.SetStatusAndEnd(span, err) - }() + + tnow := time.Now() + err := s.put(ctx, root, square) + result := putOK + switch { + case errors.Is(err, dagstore.ErrShardExists): + result = putExists + case err != nil: + result = putFailed + } + utils.SetStatusAndEnd(span, err) + s.metrics.observePut(ctx, time.Since(tnow), result, square.Width()) + return err +} + +func (s *Store) put(ctx context.Context, root share.DataHash, square *rsmt2d.ExtendedDataSquare) (err error) { + lk := &s.stripedLocks[root[len(root)-1]] + lk.Lock() + defer lk.Unlock() + + // if root already exists, short-circuit + if has, _ := s.Has(ctx, root); has { + return dagstore.ErrShardExists + } key := root.String() f, err := os.OpenFile(s.basepath+blocksPath+key, os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return err } - defer f.Close() + defer closeAndLog("car file", f) - err = WriteEDS(ctx, square, f) + // save encoded eds into buffer + mount := &inMemoryOnceMount{ + // TODO: buffer could be pre-allocated with capacity calculated based on eds size. + buf: bytes.NewBuffer(nil), + FileMount: mount.FileMount{Path: s.basepath + blocksPath + key}, + } + err = WriteEDS(ctx, square, mount) if err != nil { return fmt.Errorf("failed to write EDS to file: %w", err) } + // write whole buffered mount data in one go to optimize i/o + if _, err = mount.WriteTo(f); err != nil { + return fmt.Errorf("failed to write EDS to file: %w", err) + } + ch := make(chan dagstore.ShardResult, 1) - err = s.dgstr.RegisterShard(ctx, shard.KeyFromString(key), &mount.FileMount{ - Path: s.basepath + blocksPath + key, - }, ch, dagstore.RegisterOpts{}) + err = s.dgstr.RegisterShard(ctx, shard.KeyFromString(key), mount, ch, dagstore.RegisterOpts{}) if err != nil { return fmt.Errorf("failed to initiate shard registration: %w", err) } + var result dagstore.ShardResult select { + case result = <-ch: case <-ctx.Done(): + // if the context finished before the result was received, track the result in a separate goroutine + go trackLateResult("put", ch, s.metrics, time.Minute*5) return ctx.Err() - case result := <-ch: + } + + if result.Error != nil { + return fmt.Errorf("failed to register shard: %w", result.Error) + } + + // the accessor returned in the result will be nil, so the shard needs to be acquired first to + // become available in the cache. It might take some time, and the result should not affect the put + // operation, so do it in a goroutine + // TODO: Ideally, only recent blocks should be put in the cache, but there is no way right now to + // check such a condition. + go func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + ac, err := s.cache.Load().First().GetOrLoad(ctx, result.Key, s.getAccessor) + if err != nil { + log.Warnw("unable to put accessor to recent blocks accessors cache", "err", err) + return + } + + // need to close returned accessor to remove the reader reference + if err := ac.Close(); err != nil { + log.Warnw("unable to close accessor after loading", "err", err) + } + }() + + return nil +} + +// waitForResult waits for a result from the res channel for a maximum duration specified by +// maxWait. If the result is not received within the specified duration, it logs an error +// indicating that the parent context has expired and the shard registration is stuck. If a result +// is received, it checks for any error and logs appropriate messages. +func trackLateResult(opName string, res <-chan dagstore.ShardResult, metrics *metrics, maxWait time.Duration) { + tnow := time.Now() + select { + case <-time.After(maxWait): + metrics.observeLongOp(context.Background(), opName, time.Since(tnow), longOpUnresolved) + log.Errorf("parent context is expired, while register shard is stuck for more than %v sec", time.Since(tnow)) + return + case result := <-res: + // don't observe if result was received right after launch of the func + if time.Since(tnow) < time.Second { + return + } if result.Error != nil { - return fmt.Errorf("failed to register shard: %w", result.Error) + metrics.observeLongOp(context.Background(), opName, time.Since(tnow), longOpFailed) + log.Errorf("failed to register shard after context expired: %v ago, err: %s", time.Since(tnow), result.Error) + return } - return nil + metrics.observeLongOp(context.Background(), opName, time.Since(tnow), longOpOK) + log.Warnf("parent context expired, but register shard finished with no error,"+ + " after context expired: %v ago", time.Since(tnow)) + return } } @@ -213,16 +336,30 @@ func (s *Store) Put(ctx context.Context, root share.DataHash, square *rsmt2d.Ext // // The shard is cached in the Store, so subsequent calls to GetCAR with the same root will use the // same reader. The cache is responsible for closing the underlying reader. -func (s *Store) GetCAR(ctx context.Context, root share.DataHash) (io.Reader, error) { - ctx, span := tracer.Start(ctx, "store/get-car", trace.WithAttributes(attribute.String("root", root.String()))) - defer span.End() +func (s *Store) GetCAR(ctx context.Context, root share.DataHash) (io.ReadCloser, error) { + ctx, span := tracer.Start(ctx, "store/get-car") + tnow := time.Now() + r, err := s.getCAR(ctx, root) + s.metrics.observeGetCAR(ctx, time.Since(tnow), err != nil) + utils.SetStatusAndEnd(span, err) + return r, err +} - key := root.String() - accessor, err := s.getCachedAccessor(ctx, shard.KeyFromString(key)) +func (s *Store) getCAR(ctx context.Context, root share.DataHash) (io.ReadCloser, error) { + key := shard.KeyFromString(root.String()) + accessor, err := s.cache.Load().Get(key) + if err == nil { + return newReadCloser(accessor), nil + } + // If the accessor is not found in the cache, create a new one from dagstore. We don't put the + // accessor in the cache here because getCAR is used by shrex-eds. There is a lower probability, + // compared to other cache put triggers, that the same block will be requested again soon. + shardAccessor, err := s.getAccessor(ctx, key) if err != nil { return nil, fmt.Errorf("failed to get accessor: %w", err) } - return accessor.sa.Reader(), nil + + return newReadCloser(shardAccessor), nil } // Blockstore returns an IPFS blockstore providing access to individual shares/nodes of all EDS @@ -240,27 +377,51 @@ func (s *Store) Blockstore() bstore.Blockstore { func (s *Store) CARBlockstore( ctx context.Context, root share.DataHash, -) (dagstore.ReadBlockstore, error) { +) (*BlockstoreCloser, error) { + ctx, span := tracer.Start(ctx, "store/car-blockstore") + tnow := time.Now() + cbs, err := s.carBlockstore(ctx, root) + s.metrics.observeCARBlockstore(ctx, time.Since(tnow), err != nil) + utils.SetStatusAndEnd(span, err) + return cbs, err +} + +func (s *Store) carBlockstore( + ctx context.Context, + root share.DataHash, +) (*BlockstoreCloser, error) { key := shard.KeyFromString(root.String()) - accessor, err := s.getCachedAccessor(ctx, key) + accessor, err := s.cache.Load().Get(key) + if err == nil { + return blockstoreCloser(accessor) + } + + // if the accessor is not found in the cache, create a new one from dagstore + sa, err := s.getAccessor(ctx, key) if err != nil { - return nil, fmt.Errorf("eds/store: failed to get accessor: %w", err) + return nil, fmt.Errorf("failed to get accessor: %w", err) } - return accessor.bs, nil + return blockstoreCloser(sa) } // GetDAH returns the DataAvailabilityHeader for the EDS identified by DataHash. func (s *Store) GetDAH(ctx context.Context, root share.DataHash) (*share.Root, error) { - ctx, span := tracer.Start(ctx, "store/get-dah", trace.WithAttributes(attribute.String("root", root.String()))) - defer span.End() + ctx, span := tracer.Start(ctx, "store/car-dah") + tnow := time.Now() + r, err := s.getDAH(ctx, root) + s.metrics.observeGetDAH(ctx, time.Since(tnow), err != nil) + utils.SetStatusAndEnd(span, err) + return r, err +} - key := shard.KeyFromString(root.String()) - accessor, err := s.getCachedAccessor(ctx, key) +func (s *Store) getDAH(ctx context.Context, root share.DataHash) (*share.Root, error) { + r, err := s.getCAR(ctx, root) if err != nil { - return nil, fmt.Errorf("eds/store: failed to get accessor: %w", err) + return nil, fmt.Errorf("eds/store: failed to get CAR file: %w", err) } + defer closeAndLog("car reader", r) - carHeader, err := carv1.ReadHeader(bufio.NewReader(accessor.sa.Reader())) + carHeader, err := carv1.ReadHeader(bufio.NewReader(r)) if err != nil { return nil, fmt.Errorf("eds/store: failed to read car header: %w", err) } @@ -273,19 +434,19 @@ func (s *Store) GetDAH(ctx context.Context, root share.DataHash) (*share.Root, e } // dahFromCARHeader returns the DataAvailabilityHeader stored in the CIDs of a CARv1 header. -func dahFromCARHeader(carHeader *carv1.CarHeader) *header.DataAvailabilityHeader { +func dahFromCARHeader(carHeader *carv1.CarHeader) *share.Root { rootCount := len(carHeader.Roots) rootBytes := make([][]byte, 0, rootCount) for _, root := range carHeader.Roots { rootBytes = append(rootBytes, ipld.NamespacedSha256FromCID(root)) } - return &header.DataAvailabilityHeader{ - RowsRoots: rootBytes[:rootCount/2], + return &share.Root{ + RowRoots: rootBytes[:rootCount/2], ColumnRoots: rootBytes[rootCount/2:], } } -func (s *Store) getAccessor(ctx context.Context, key shard.Key) (*dagstore.ShardAccessor, error) { +func (s *Store) getAccessor(ctx context.Context, key shard.Key) (cache.Accessor, error) { ch := make(chan dagstore.ShardResult, 1) err := s.dgstr.AcquireShard(ctx, key, ch, dagstore.AcquireOpts{}) if err != nil { @@ -302,42 +463,30 @@ func (s *Store) getAccessor(ctx context.Context, key shard.Key) (*dagstore.Shard } return res.Accessor, nil case <-ctx.Done(): + go trackLateResult("get_shard", ch, s.metrics, time.Minute) return nil, ctx.Err() } } -func (s *Store) getCachedAccessor(ctx context.Context, key shard.Key) (*accessorWithBlockstore, error) { - lk := &s.cache.stripedLocks[shardKeyToStriped(key)] - lk.Lock() - defer lk.Unlock() - - accessor, err := s.cache.unsafeGet(key) - if err != nil && err != errCacheMiss { - log.Errorf("unexpected error while reading key from bs cache %s: %s", key, err) - } - if accessor != nil { - return accessor, nil - } - - // wasn't found in cache, so acquire it and add to cache - shardAccessor, err := s.getAccessor(ctx, key) - if err != nil { - return nil, err - } - return s.cache.unsafeAdd(key, shardAccessor) -} - // Remove removes EDS from Store by the given share.Root hash and cleans up all // the indexing. -func (s *Store) Remove(ctx context.Context, root share.DataHash) (err error) { - ctx, span := tracer.Start(ctx, "store/remove", trace.WithAttributes(attribute.String("root", root.String()))) - defer func() { - utils.SetStatusAndEnd(span, err) - }() +func (s *Store) Remove(ctx context.Context, root share.DataHash) error { + ctx, span := tracer.Start(ctx, "store/remove") + tnow := time.Now() + err := s.remove(ctx, root) + s.metrics.observeRemove(ctx, time.Since(tnow), err != nil) + utils.SetStatusAndEnd(span, err) + return err +} - key := root.String() +func (s *Store) remove(ctx context.Context, root share.DataHash) (err error) { + key := shard.KeyFromString(root.String()) + // remove open links to accessor from cache + if err := s.cache.Load().Remove(key); err != nil { + log.Warnw("remove accessor from cache", "err", err) + } ch := make(chan dagstore.ShardResult, 1) - err = s.dgstr.DestroyShard(ctx, shard.KeyFromString(key), ch, dagstore.DestroyOpts{}) + err = s.dgstr.DestroyShard(ctx, key, ch, dagstore.DestroyOpts{}) if err != nil { return fmt.Errorf("failed to initiate shard destruction: %w", err) } @@ -348,10 +497,11 @@ func (s *Store) Remove(ctx context.Context, root share.DataHash) (err error) { return fmt.Errorf("failed to destroy shard: %w", result.Error) } case <-ctx.Done(): + go trackLateResult("remove", ch, s.metrics, time.Minute) return ctx.Err() } - dropped, err := s.carIdx.DropFullIndex(shard.KeyFromString(key)) + dropped, err := s.carIdx.DropFullIndex(key) if !dropped { log.Warnf("failed to drop index for %s", key) } @@ -359,7 +509,7 @@ func (s *Store) Remove(ctx context.Context, root share.DataHash) (err error) { return fmt.Errorf("failed to drop index for %s: %w", key, err) } - err = os.Remove(s.basepath + blocksPath + key) + err = os.Remove(s.basepath + blocksPath + root.String()) if err != nil { return fmt.Errorf("failed to remove CAR file: %w", err) } @@ -370,17 +520,28 @@ func (s *Store) Remove(ctx context.Context, root share.DataHash) (err error) { // // It reads only one quadrant(1/4) of the EDS and verifies the integrity of the stored data by // recomputing it. -func (s *Store) Get(ctx context.Context, root share.DataHash) (eds *rsmt2d.ExtendedDataSquare, err error) { - ctx, span := tracer.Start(ctx, "store/get", trace.WithAttributes(attribute.String("root", root.String()))) +func (s *Store) Get(ctx context.Context, root share.DataHash) (*rsmt2d.ExtendedDataSquare, error) { + ctx, span := tracer.Start(ctx, "store/get") + tnow := time.Now() + eds, err := s.get(ctx, root) + s.metrics.observeGet(ctx, time.Since(tnow), err != nil) + utils.SetStatusAndEnd(span, err) + return eds, err +} + +func (s *Store) get(ctx context.Context, root share.DataHash) (eds *rsmt2d.ExtendedDataSquare, err error) { + ctx, span := tracer.Start(ctx, "store/get") defer func() { utils.SetStatusAndEnd(span, err) }() - f, err := s.GetCAR(ctx, root) + r, err := s.getCAR(ctx, root) if err != nil { return nil, fmt.Errorf("failed to get CAR file: %w", err) } - eds, err = ReadEDS(ctx, f, root) + defer closeAndLog("car reader", r) + + eds, err = ReadEDS(ctx, r, root) if err != nil { return nil, fmt.Errorf("failed to read EDS from CAR file: %w", err) } @@ -388,10 +549,16 @@ func (s *Store) Get(ctx context.Context, root share.DataHash) (eds *rsmt2d.Exten } // Has checks if EDS exists by the given share.Root hash. -func (s *Store) Has(ctx context.Context, root share.DataHash) (bool, error) { - _, span := tracer.Start(ctx, "store/has", trace.WithAttributes(attribute.String("root", root.String()))) - defer span.End() +func (s *Store) Has(ctx context.Context, root share.DataHash) (has bool, err error) { + ctx, span := tracer.Start(ctx, "store/has") + tnow := time.Now() + eds, err := s.has(ctx, root) + s.metrics.observeHas(ctx, time.Since(tnow), err != nil) + utils.SetStatusAndEnd(span, err) + return eds, err +} +func (s *Store) has(_ context.Context, root share.DataHash) (bool, error) { key := root.String() info, err := s.dgstr.GetShardInfo(shard.KeyFromString(key)) switch err { @@ -404,6 +571,26 @@ func (s *Store) Has(ctx context.Context, root share.DataHash) (bool, error) { } } +// List lists all the registered EDSes. +func (s *Store) List() ([]share.DataHash, error) { + ctx, span := tracer.Start(context.Background(), "store/list") + tnow := time.Now() + hashes, err := s.list() + s.metrics.observeList(ctx, time.Since(tnow), err != nil) + utils.SetStatusAndEnd(span, err) + return hashes, err +} + +func (s *Store) list() ([]share.DataHash, error) { + shards := s.dgstr.AllShardsInfo() + hashes := make([]share.DataHash, 0, len(shards)) + for shrd := range shards { + hash := share.MustDataHashFromString(shrd.String()) + hashes = append(hashes, hash) + } + return hashes, nil +} + func setupPath(basepath string) error { err := os.MkdirAll(basepath+blocksPath, os.ModePerm) if err != nil { @@ -419,3 +606,39 @@ func setupPath(basepath string) error { } return nil } + +// inMemoryOnceMount is used to allow reading once from buffer before using main mount.Reader +type inMemoryOnceMount struct { + buf *bytes.Buffer + + readOnce atomic.Bool + mount.FileMount +} + +func (m *inMemoryOnceMount) Fetch(ctx context.Context) (mount.Reader, error) { + if m.buf != nil && !m.readOnce.Swap(true) { + reader := &inMemoryReader{Reader: bytes.NewReader(m.buf.Bytes())} + // release memory for gc, otherwise buffer will stick forever + m.buf = nil + return reader, nil + } + return m.FileMount.Fetch(ctx) +} + +func (m *inMemoryOnceMount) Write(b []byte) (int, error) { + return m.buf.Write(b) +} + +func (m *inMemoryOnceMount) WriteTo(w io.Writer) (int64, error) { + return io.Copy(w, bytes.NewReader(m.buf.Bytes())) +} + +// inMemoryReader extends bytes.Reader to implement mount.Reader interface +type inMemoryReader struct { + *bytes.Reader +} + +// Close allows inMemoryReader to satisfy mount.Reader interface +func (r *inMemoryReader) Close() error { + return nil +} diff --git a/share/eds/store_options.go b/share/eds/store_options.go new file mode 100644 index 0000000000..e5e6ffa73d --- /dev/null +++ b/share/eds/store_options.go @@ -0,0 +1,43 @@ +package eds + +import ( + "fmt" + "time" +) + +type Parameters struct { + // GC performs DAG store garbage collection by reclaiming transient files of + // shards that are currently available but inactive, or errored. + // We don't use transient files right now, so GC is turned off by default. + GCInterval time.Duration + + // RecentBlocksCacheSize is the size of the cache for recent blocks. + RecentBlocksCacheSize int + + // BlockstoreCacheSize is the size of the cache for blockstore requested accessors. + BlockstoreCacheSize int +} + +// DefaultParameters returns the default configuration values for the EDS store parameters. +func DefaultParameters() *Parameters { + return &Parameters{ + GCInterval: 0, + RecentBlocksCacheSize: 10, + BlockstoreCacheSize: 128, + } +} + +func (p *Parameters) Validate() error { + if p.GCInterval < 0 { + return fmt.Errorf("eds: GC interval cannot be negative") + } + + if p.RecentBlocksCacheSize < 1 { + return fmt.Errorf("eds: recent blocks cache size must be positive") + } + + if p.BlockstoreCacheSize < 1 { + return fmt.Errorf("eds: blockstore cache size must be positive") + } + return nil +} diff --git a/share/eds/store_test.go b/share/eds/store_test.go index 6c6d7d10b2..6bc6972bb4 100644 --- a/share/eds/store_test.go +++ b/share/eds/store_test.go @@ -2,13 +2,18 @@ package eds import ( "context" + "io" "os" + "sync" "testing" "time" + "github.com/filecoin-project/dagstore" "github.com/filecoin-project/dagstore/shard" + "github.com/ipfs/go-cid" "github.com/ipfs/go-datastore" ds_sync "github.com/ipfs/go-datastore/sync" + dsbadger "github.com/ipfs/go-ds-badger4" "github.com/ipld/go-car" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,6 +22,9 @@ import ( "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/cache" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/ipld" ) func TestEDSStore(t *testing.T) { @@ -69,6 +77,9 @@ func TestEDSStore(t *testing.T) { r, err := edsStore.GetCAR(ctx, dah.Hash()) assert.NoError(t, err) + defer func() { + require.NoError(t, r.Close()) + }() carReader, err := car.NewCarReader(r) assert.NoError(t, err) @@ -77,7 +88,7 @@ func TestEDSStore(t *testing.T) { original := eds.GetCell(uint(i), uint(j)) block, err := carReader.Next() assert.NoError(t, err) - assert.Equal(t, original, block.RawData()[share.NamespaceSize:]) + assert.Equal(t, original, share.GetData(block.RawData())) } } }) @@ -104,6 +115,9 @@ func TestEDSStore(t *testing.T) { _, err = os.Stat(edsStore.basepath + blocksPath + dah.String()) assert.NoError(t, err) + // accessor will be registered in cache async on put, so give it some time to settle + time.Sleep(time.Millisecond * 100) + err = edsStore.Remove(ctx, dah.Hash()) assert.NoError(t, err) @@ -121,6 +135,51 @@ func TestEDSStore(t *testing.T) { assert.ErrorContains(t, err, "no such file or directory") }) + t.Run("Remove after OpShardFail", func(t *testing.T) { + eds, dah := randomEDS(t) + + err = edsStore.Put(ctx, dah.Hash(), eds) + require.NoError(t, err) + + // assert that shard now exists + ok, err := edsStore.Has(ctx, dah.Hash()) + assert.NoError(t, err) + assert.True(t, ok) + + // assert that file now exists + path := edsStore.basepath + blocksPath + dah.String() + _, err = os.Stat(path) + assert.NoError(t, err) + + err = os.Remove(path) + assert.NoError(t, err) + + // accessor will be registered in cache async on put, so give it some time to settle + time.Sleep(time.Millisecond * 100) + + // remove non-failed accessor from cache + err = edsStore.cache.Load().Remove(shard.KeyFromString(dah.String())) + assert.NoError(t, err) + + _, err = edsStore.GetCAR(ctx, dah.Hash()) + assert.Error(t, err) + + ticker := time.NewTicker(time.Millisecond * 100) + defer ticker.Stop() + for { + select { + case <-ticker.C: + has, err := edsStore.Has(ctx, dah.Hash()) + if err == nil && !has { + // shard no longer exists after OpShardFail was detected from GetCAR call + return + } + case <-ctx.Done(): + t.Fatal("timeout waiting for shard to be removed") + } + } + }) + t.Run("Has", func(t *testing.T) { eds, dah := randomEDS(t) @@ -136,22 +195,59 @@ func TestEDSStore(t *testing.T) { assert.True(t, ok) }) - t.Run("BlockstoreCache", func(t *testing.T) { + t.Run("RecentBlocksCache", func(t *testing.T) { eds, dah := randomEDS(t) - err = edsStore.Put(ctx, dah.Hash(), eds) require.NoError(t, err) - // key isnt in cache yet, so get returns errCacheMiss - shardKey := shard.KeyFromString(dah.String()) - _, err = edsStore.cache.Get(shardKey) - assert.ErrorIs(t, err, errCacheMiss) + // accessor will be registered in cache async on put, so give it some time to settle + time.Sleep(time.Millisecond * 100) - // now get it, so that the key is in the cache - _, err = edsStore.CARBlockstore(ctx, dah.Hash()) + // check, that the key is in the cache after put + shardKey := shard.KeyFromString(dah.String()) + _, err = edsStore.cache.Load().Get(shardKey) assert.NoError(t, err) - _, err = edsStore.cache.Get(shardKey) - assert.NoError(t, err, errCacheMiss) + }) + + t.Run("List", func(t *testing.T) { + const amount = 10 + hashes := make([]share.DataHash, 0, amount) + for range make([]byte, amount) { + eds, dah := randomEDS(t) + err = edsStore.Put(ctx, dah.Hash(), eds) + require.NoError(t, err) + hashes = append(hashes, dah.Hash()) + } + + hashesOut, err := edsStore.List() + require.NoError(t, err) + for _, hash := range hashes { + assert.Contains(t, hashesOut, hash) + } + }) + + t.Run("Parallel put", func(t *testing.T) { + const amount = 20 + eds, dah := randomEDS(t) + + wg := sync.WaitGroup{} + for i := 1; i < amount; i++ { + wg.Add(1) + go func() { + defer wg.Done() + err := edsStore.Put(ctx, dah.Hash(), eds) + if err != nil { + require.ErrorIs(t, err, dagstore.ErrShardExists) + } + }() + } + wg.Wait() + + eds, err := edsStore.Get(ctx, dah.Hash()) + require.NoError(t, err) + newDah, err := da.NewDataAvailabilityHeader(eds) + require.NoError(t, err) + require.Equal(t, dah.Hash(), newDah.Hash()) }) } @@ -174,6 +270,15 @@ func TestEDSStore_GC(t *testing.T) { err = edsStore.Put(ctx, dah.Hash(), eds) require.NoError(t, err) + // accessor will be registered in cache async on put, so give it some time to settle + time.Sleep(time.Millisecond * 100) + + // remove links to the shard from cache + time.Sleep(time.Millisecond * 100) + key := shard.KeyFromString(share.DataHash(dah.Hash()).String()) + err = edsStore.cache.Load().Remove(key) + require.NoError(t, err) + // doesn't exist yet assert.NotContains(t, edsStore.lastGCResult.Load().Shards, shardKey) @@ -199,20 +304,43 @@ func Test_BlockstoreCache(t *testing.T) { err = edsStore.Start(ctx) require.NoError(t, err) + // store eds to the store with noopCache to allow clean cache after put + swap := edsStore.cache.Load() + edsStore.cache.Store(cache.NewDoubleCache(cache.NoopCache{}, cache.NoopCache{})) eds, dah := randomEDS(t) err = edsStore.Put(ctx, dah.Hash(), eds) require.NoError(t, err) - // key isnt in cache yet, so get returns errCacheMiss + // get any key from saved eds + bs, err := edsStore.carBlockstore(ctx, dah.Hash()) + require.NoError(t, err) + defer func() { + require.NoError(t, bs.Close()) + }() + keys, err := bs.AllKeysChan(ctx) + require.NoError(t, err) + var key cid.Cid + select { + case key = <-keys: + case <-ctx.Done(): + t.Fatal("context timeout") + } + + // swap back original cache + edsStore.cache.Store(swap) + + // key shouldn't be in cache yet, check for returned errCacheMiss shardKey := shard.KeyFromString(dah.String()) - _, err = edsStore.cache.Get(shardKey) - assert.ErrorIs(t, err, errCacheMiss) - - // now get it, so that the key is in the cache - _, err = edsStore.getCachedAccessor(ctx, shardKey) - assert.NoError(t, err) - _, err = edsStore.cache.Get(shardKey) - assert.NoError(t, err, errCacheMiss) + _, err = edsStore.cache.Load().Get(shardKey) + require.Error(t, err) + + // now get it from blockstore, to trigger storing to cache + _, err = edsStore.Blockstore().Get(ctx, key) + require.NoError(t, err) + + // should be no errCacheMiss anymore + _, err = edsStore.cache.Load().Get(shardKey) + require.NoError(t, err) } // Test_CachedAccessor verifies that the reader represented by a cached accessor can be read from @@ -230,39 +358,182 @@ func Test_CachedAccessor(t *testing.T) { err = edsStore.Put(ctx, dah.Hash(), eds) require.NoError(t, err) - shardKey := shard.KeyFromString(dah.String()) - // adds to cache - cachedAccessor, err := edsStore.getCachedAccessor(ctx, shardKey) - assert.NoError(t, err) - - // first read - carReader, err := car.NewCarReader(cachedAccessor.sa.Reader()) - assert.NoError(t, err) - firstBlock, err := carReader.Next() - assert.NoError(t, err) - - // second read - cachedAccessor, err = edsStore.getCachedAccessor(ctx, shardKey) - assert.NoError(t, err) - carReader, err = car.NewCarReader(cachedAccessor.sa.Reader()) - assert.NoError(t, err) - secondBlock, err := carReader.Next() - assert.NoError(t, err) - - assert.Equal(t, firstBlock, secondBlock) + // accessor will be registered in cache async on put, so give it some time to settle + time.Sleep(time.Millisecond * 100) + + // accessor should be in cache + _, err = edsStore.cache.Load().Get(shard.KeyFromString(dah.String())) + require.NoError(t, err) + + // first read from cached accessor + carReader, err := edsStore.getCAR(ctx, dah.Hash()) + require.NoError(t, err) + firstBlock, err := io.ReadAll(carReader) + require.NoError(t, err) + require.NoError(t, carReader.Close()) + + // second read from cached accessor + carReader, err = edsStore.getCAR(ctx, dah.Hash()) + require.NoError(t, err) + secondBlock, err := io.ReadAll(carReader) + require.NoError(t, err) + require.NoError(t, carReader.Close()) + + require.Equal(t, firstBlock, secondBlock) +} + +// Test_CachedAccessor verifies that the reader represented by a accessor obtained directly from +// dagstore can be read from multiple times, without exhausting the underlying reader. +func Test_NotCachedAccessor(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + edsStore, err := newStore(t) + require.NoError(t, err) + err = edsStore.Start(ctx) + require.NoError(t, err) + // replace cache with noopCache to + edsStore.cache.Store(cache.NewDoubleCache(cache.NoopCache{}, cache.NoopCache{})) + + eds, dah := randomEDS(t) + err = edsStore.Put(ctx, dah.Hash(), eds) + require.NoError(t, err) + + // accessor will be registered in cache async on put, so give it some time to settle + time.Sleep(time.Millisecond * 100) + + // accessor should not be in cache + _, err = edsStore.cache.Load().Get(shard.KeyFromString(dah.String())) + require.Error(t, err) + + // first read from direct accessor (not from cache) + carReader, err := edsStore.getCAR(ctx, dah.Hash()) + require.NoError(t, err) + firstBlock, err := io.ReadAll(carReader) + require.NoError(t, err) + require.NoError(t, carReader.Close()) + + // second read from direct accessor (not from cache) + carReader, err = edsStore.getCAR(ctx, dah.Hash()) + require.NoError(t, err) + secondBlock, err := io.ReadAll(carReader) + require.NoError(t, err) + require.NoError(t, carReader.Close()) + + require.Equal(t, firstBlock, secondBlock) +} + +func BenchmarkStore(b *testing.B) { + ctx, cancel := context.WithCancel(context.Background()) + b.Cleanup(cancel) + + ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) + edsStore, err := NewStore(DefaultParameters(), b.TempDir(), ds) + require.NoError(b, err) + err = edsStore.Start(ctx) + require.NoError(b, err) + + // BenchmarkStore/bench_put_128-10 10 3231859283 ns/op (~3sec) + b.Run("bench put 128", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + // pause the timer for initializing test data + b.StopTimer() + eds := edstest.RandEDS(b, 128) + dah, err := share.NewRoot(eds) + require.NoError(b, err) + b.StartTimer() + + err = edsStore.Put(ctx, dah.Hash(), eds) + require.NoError(b, err) + } + }) + + // BenchmarkStore/bench_read_128-10 14 78970661 ns/op (~70ms) + b.Run("bench read 128", func(b *testing.B) { + b.ResetTimer() + for i := 0; i < b.N; i++ { + // pause the timer for initializing test data + b.StopTimer() + eds := edstest.RandEDS(b, 128) + dah, err := share.NewRoot(eds) + require.NoError(b, err) + _ = edsStore.Put(ctx, dah.Hash(), eds) + b.StartTimer() + + _, err = edsStore.Get(ctx, dah.Hash()) + require.NoError(b, err) + } + }) +} + +// BenchmarkCacheEviction benchmarks the time it takes to load a block to the cache, when the +// cache size is set to 1. This forces cache eviction on every read. +// BenchmarkCacheEviction-10/128 384 3533586 ns/op (~3ms) +func BenchmarkCacheEviction(b *testing.B) { + const ( + blocks = 4 + size = 128 + ) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + b.Cleanup(cancel) + + dir := b.TempDir() + ds, err := dsbadger.NewDatastore(dir, &dsbadger.DefaultOptions) + require.NoError(b, err) + + newStore := func(params *Parameters) *Store { + edsStore, err := NewStore(params, dir, ds) + require.NoError(b, err) + err = edsStore.Start(ctx) + require.NoError(b, err) + return edsStore + } + edsStore := newStore(DefaultParameters()) + + // generate EDSs and store them + cids := make([]cid.Cid, blocks) + for i := range cids { + eds := edstest.RandEDS(b, size) + dah, err := da.NewDataAvailabilityHeader(eds) + require.NoError(b, err) + err = edsStore.Put(ctx, dah.Hash(), eds) + require.NoError(b, err) + + // store cids for read loop later + cids[i] = ipld.MustCidFromNamespacedSha256(dah.RowRoots[0]) + } + + // restart store to clear cache + require.NoError(b, edsStore.Stop(ctx)) + + // set BlockstoreCacheSize to 1 to force eviction on every read + params := DefaultParameters() + params.BlockstoreCacheSize = 1 + bstore := newStore(params).Blockstore() + + // start benchmark + b.ResetTimer() + for i := 0; i < b.N; i++ { + h := cids[i%blocks] + // every read will trigger eviction + _, err := bstore.Get(ctx, h) + require.NoError(b, err) + } } func newStore(t *testing.T) (*Store, error) { t.Helper() - tmpDir := t.TempDir() ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - return NewStore(tmpDir, ds) + return NewStore(DefaultParameters(), t.TempDir(), ds) } -func randomEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, share.Root) { - eds := share.RandEDS(t, 4) - dah := da.NewDataAvailabilityHeader(eds) +func randomEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, *share.Root) { + eds := edstest.RandEDS(t, 4) + dah, err := share.NewRoot(eds) + require.NoError(t, err) return eds, dah } diff --git a/share/eds/testdata/example-root.json b/share/eds/testdata/example-root.json index b687e3cbeb..999d6301b6 100644 --- a/share/eds/testdata/example-root.json +++ b/share/eds/testdata/example-root.json @@ -1,22 +1,22 @@ { "row_roots": [ -"A3I8MUKAXaEf7JDjUxllL5CGD0/uS8k2l3KpbN+u5fa3tGL1h9FSqZhCJFcZhzAT", -"IA74YwcC1tFMkQfD9sE2NO7hwDtjo37O1KiiptYPCgAQWQnv/G7bxu6mUJ2s3Ljk", -"ZskuRtu6YfK9pTqLAOsfLXbvjrvDD+80xoj/850Z4YGvmAaRH5vFXraZNWgEQNyr", -"w9Tz/Q7t/JfX7McKTWvNjhqi784y+nGCYXrQtu4jy90kx0irPJcVTXTQ7v6Xnjdf", -"/////////////////////1MCHZjf0ySoNlpt+h8Vb6QFm7BHYJXI14SMiNRgwA0K", -"/////////////////////2dOgrZG2ZT+KIrfjoPmTcGpqXdsdFZE6ZHcF0l+fL6i", -"/////////////////////7OJEeOYA6VsagglTnpIysAtyK3m3u4d8Zd66FuqX0+M", -"/////////////////////+hYZyJ10gx7Brp05g/MLtSV4z7SLLwGcMsXZI/Fgw0J" +"AAAAAAAAAAAAAAAAAAAAAAAAABPYEuDlO9Dz69oAAAAAAAAAAAAAAAAAAAAAAAAAMcklN0h38T4b/UBC/Cmr5YWmjmmxvi1e35vZBW14b8gDHBoTFVvY6H4J", +"AAAAAAAAAAAAAAAAAAAAAAAAADxyZecUZD41W5IAAAAAAAAAAAAAAAAAAAAAAAAAh8vQUZ38PaWyeUs7dQhphIuRIKiGaTr4KFwEhMRhejTd6/4NHdnKTDyY", +"AAAAAAAAAAAAAAAAAAAAAAAAAKDQatbQSwQ9uJsAAAAAAAAAAAAAAAAAAAAAAAAArtdqXCSsM1OlVCRZqqfZDnEO9eC5cwlgy5MQHb2g4NLr7nZYTruiOoz7", +"AAAAAAAAAAAAAAAAAAAAAAAAAMeUhM8LZBo9sWwAAAAAAAAAAAAAAAAAAAAAAAAA8PtvJpbDc4APKOK6MT1k61HuQXwauWw3nFWwr9pSljiYMv6jjjdLDF8o", +"/////////////////////////////////////////////////////////////////////////////xnHmhDh4Y8vfJrgewAcvLWpvI5XOyATj1IQDkCwvIEh", +"/////////////////////////////////////////////////////////////////////////////+qngp0AfoykfXwsMBukRtYxNA/bzW0+F3J7Q/+S1YZJ", +"/////////////////////////////////////////////////////////////////////////////4WNPrME/2MLrIZgAUoKaVx2GzJqDcYGrBg+sudPKUDy", +"/////////////////////////////////////////////////////////////////////////////6HdebpaHl7iTpLvmuPvtQNnkHfNOPyEhahxbVnIB2d1" ], "column_roots": [ -"A3I8MUKAXaHD1PP9Du38l5n9/iXzaDzWlVrLnsRE5zp+rEeyH2A0is+czwU/aHUn", -"D7uVznpBCw/S2nVDrzgGx9Ditcfzy/cL2Zdn+MBV31zeOgzCv+zXA0QP3SQRWXlb", -"FQDYNIesVPTUn9q9Ybq0VqDgprQbzKsMGrX+7kTlnbdOu434iOmx4P+vZVGjXAsm", -"H+yQ41MZZS/X7McKTWvNjoZId6AiHkxnGRsmYKuh4EvQCy+eEj19Q6Z5eQoMGWZL", -"/////////////////////1wS3guBpJRGNMLQMkEwgVOuXrTG3gUwJwI+nybYxTRF", -"/////////////////////1Eg48mqxsQejtwUIJkwrjEYCPvDYCRYm+dPXVitBCjl", -"/////////////////////zDz519rhl2FfQp6S3hR887dRSi6zN+Qs4PwIFdRqbxP", -"/////////////////////zARSBQBFxvGNpLt8I1qtbBOyEfFOXD4V/beDdU1kXPm" +"AAAAAAAAAAAAAAAAAAAAAAAAABPYEuDlO9Dz69oAAAAAAAAAAAAAAAAAAAAAAAAAx5SEzwtkGj2xbESyOeamsjGWUBQdAQoiSl+rMtNMo1wEtfGQnFS/g+K+", +"AAAAAAAAAAAAAAAAAAAAAAAAAC3uK6nhCxHTfBwAAAAAAAAAAAAAAAAAAAAAAAAA1fxnqHyO6qV39pcUQ8MuTfJ7RBhbSVWf0aamUP27KRY0II55oJoY6Ng6", +"AAAAAAAAAAAAAAAAAAAAAAAAAC6DkYeeBY/kKvAAAAAAAAAAAAAAAAAAAAAAAAAA47rxk8hoCnWGM+CX47TlYWBeE2unvRhA/j3EvHdxeL1rFRkaYfAd5eg7", +"AAAAAAAAAAAAAAAAAAAAAAAAADHJJTdId/E+G/0AAAAAAAAAAAAAAAAAAAAAAAAA8PtvJpbDc4APKAk5QPSH59HECE2sf/CDLKAZJjWo9DD4sLXJQ4jTZoH6", +"/////////////////////////////////////////////////////////////////////////////4lKCT3K11RnNIuLNfY+SfDZCYAE2iW0hjQHIVBpoN0q", +"/////////////////////////////////////////////////////////////////////////////1NpYcgayEVenbFeEO5LJ1j1/1sD+PvZWHDv+jqT1dLR", +"/////////////////////////////////////////////////////////////////////////////8FOWVuCU0rTzUW9tP2R47RmTBvwXX8ycKrMhgKEi1xa", +"/////////////////////////////////////////////////////////////////////////////7K5SoZ3HF5QgPvIXpKSr9eT4Xfiokc3PUMmXE4pBDTf" ] } \ No newline at end of file diff --git a/share/eds/testdata/example.car b/share/eds/testdata/example.car index 487bca4653..4d33c0ef33 100644 Binary files a/share/eds/testdata/example.car and b/share/eds/testdata/example.car differ diff --git a/share/eds/utils.go b/share/eds/utils.go new file mode 100644 index 0000000000..b897dd14b5 --- /dev/null +++ b/share/eds/utils.go @@ -0,0 +1,152 @@ +package eds + +import ( + "context" + "errors" + "fmt" + "io" + + "github.com/filecoin-project/dagstore" + "github.com/ipfs/boxo/blockservice" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" + "golang.org/x/sync/errgroup" + + "github.com/celestiaorg/celestia-node/libs/utils" + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/cache" + "github.com/celestiaorg/celestia-node/share/ipld" +) + +// readCloser is a helper struct, that combines io.Reader and io.Closer +type readCloser struct { + io.Reader + io.Closer +} + +// BlockstoreCloser represents a blockstore that can also be closed. It combines the functionality +// of a dagstore.ReadBlockstore with that of an io.Closer. +type BlockstoreCloser struct { + dagstore.ReadBlockstore + io.Closer +} + +func newReadCloser(ac cache.Accessor) io.ReadCloser { + return readCloser{ + ac.Reader(), + ac, + } +} + +// blockstoreCloser constructs new BlockstoreCloser from cache.Accessor +func blockstoreCloser(ac cache.Accessor) (*BlockstoreCloser, error) { + bs, err := ac.Blockstore() + if err != nil { + return nil, fmt.Errorf("eds/store: failed to get blockstore: %w", err) + } + return &BlockstoreCloser{ + ReadBlockstore: bs, + Closer: ac, + }, nil +} + +func closeAndLog(name string, closer io.Closer) { + if err := closer.Close(); err != nil { + log.Warnw("closing "+name, "err", err) + } +} + +// RetrieveNamespaceFromStore gets all EDS shares in the given namespace from +// the EDS store through the corresponding CAR-level blockstore. It is extracted +// from the store getter to make it available for reuse in the shrexnd server. +func RetrieveNamespaceFromStore( + ctx context.Context, + store *Store, + dah *share.Root, + namespace share.Namespace, +) (shares share.NamespacedShares, err error) { + if err = namespace.ValidateForData(); err != nil { + return nil, err + } + + bs, err := store.CARBlockstore(ctx, dah.Hash()) + if errors.Is(err, ErrNotFound) { + // convert error to satisfy getter interface contract + err = share.ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to retrieve blockstore from eds store: %w", err) + } + defer func() { + if err := bs.Close(); err != nil { + log.Warnw("closing blockstore", "err", err) + } + }() + + // wrap the read-only CAR blockstore in a getter + blockGetter := NewBlockGetter(bs) + shares, err = CollectSharesByNamespace(ctx, blockGetter, dah, namespace) + if errors.Is(err, ipld.ErrNodeNotFound) { + // IPLD node not found after the index pointed to this shard and the CAR + // blockstore has been opened successfully is a strong indicator of + // corruption. We remove the block on bridges and fulls and return + // share.ErrNotFound to ensure the data is retrieved by the next getter. + // Note that this recovery is manual and will only be restored by an RPC + // call to SharesAvailable that fetches the same datahash that was + // removed. + err = store.Remove(ctx, dah.Hash()) + if err != nil { + log.Errorf("failed to remove CAR from store after detected corruption: %w", err) + } + err = share.ErrNotFound + } + if err != nil { + return nil, fmt.Errorf("failed to retrieve shares by namespace from store: %w", err) + } + + return shares, nil +} + +// CollectSharesByNamespace collects NamespaceShares within the given namespace from share.Root. +func CollectSharesByNamespace( + ctx context.Context, + bg blockservice.BlockGetter, + root *share.Root, + namespace share.Namespace, +) (shares share.NamespacedShares, err error) { + ctx, span := tracer.Start(ctx, "collect-shares-by-namespace", trace.WithAttributes( + attribute.String("namespace", namespace.String()), + )) + defer func() { + utils.SetStatusAndEnd(span, err) + }() + + rootCIDs := ipld.FilterRootByNamespace(root, namespace) + if len(rootCIDs) == 0 { + return []share.NamespacedRow{}, nil + } + + errGroup, ctx := errgroup.WithContext(ctx) + shares = make([]share.NamespacedRow, len(rootCIDs)) + for i, rootCID := range rootCIDs { + // shadow loop variables, to ensure correct values are captured + i, rootCID := i, rootCID + errGroup.Go(func() error { + row, proof, err := ipld.GetSharesByNamespace(ctx, bg, rootCID, namespace, len(root.RowRoots)) + shares[i] = share.NamespacedRow{ + Shares: row, + Proof: proof, + } + if err != nil { + return fmt.Errorf("retrieving shares by namespace %s for row %x: %w", namespace.String(), rootCID, err) + } + return nil + }) + } + + if err := errGroup.Wait(); err != nil { + return nil, err + } + + return shares, nil +} diff --git a/share/empty.go b/share/empty.go index 6b5d7bb1e8..ef3d088e1d 100644 --- a/share/empty.go +++ b/share/empty.go @@ -2,11 +2,8 @@ package share import ( "bytes" - "context" "fmt" - "math" - - "github.com/ipfs/go-blockservice" + "sync" "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/pkg/da" @@ -14,52 +11,59 @@ import ( "github.com/celestiaorg/rsmt2d" ) +// EmptyRoot returns Root of the empty block EDS. +func EmptyRoot() *Root { + initEmpty() + return emptyBlockRoot +} + +// EmptyExtendedDataSquare returns the EDS of the empty block data square. +func EmptyExtendedDataSquare() *rsmt2d.ExtendedDataSquare { + initEmpty() + return emptyBlockEDS +} + +// EmptyBlockShares returns the shares of the empty block. +func EmptyBlockShares() []Share { + initEmpty() + return emptyBlockShares +} + var ( - emptyRoot *Root - emptyEDS *rsmt2d.ExtendedDataSquare + emptyMu sync.Mutex + emptyBlockRoot *Root + emptyBlockEDS *rsmt2d.ExtendedDataSquare + emptyBlockShares []Share ) -func init() { +// initEmpty enables lazy initialization for constant empty block data. +func initEmpty() { + emptyMu.Lock() + defer emptyMu.Unlock() + if emptyBlockRoot != nil { + return + } + // compute empty block EDS and DAH for it - shares := emptyDataSquare() - squareSize := uint64(math.Sqrt(float64(appconsts.DefaultMinSquareSize))) - eds, err := da.ExtendShares(squareSize, shares) + result := shares.TailPaddingShares(appconsts.MinShareCount) + emptyBlockShares = shares.ToBytes(result) + + eds, err := da.ExtendShares(emptyBlockShares) if err != nil { panic(fmt.Errorf("failed to create empty EDS: %w", err)) } - emptyEDS = eds + emptyBlockEDS = eds - dah := da.NewDataAvailabilityHeader(eds) + emptyBlockRoot, err = NewRoot(eds) + if err != nil { + panic(fmt.Errorf("failed to create empty DAH: %w", err)) + } minDAH := da.MinDataAvailabilityHeader() - if !bytes.Equal(minDAH.Hash(), dah.Hash()) { + if !bytes.Equal(minDAH.Hash(), emptyBlockRoot.Hash()) { panic(fmt.Sprintf("mismatch in calculated minimum DAH and minimum DAH from celestia-app, "+ - "expected %X, got %X", minDAH.Hash(), dah.Hash())) + "expected %s, got %s", minDAH.String(), emptyBlockRoot.String())) } - emptyRoot = &dah // precompute Hash, so it's cached internally to avoid potential races - emptyRoot.Hash() -} - -// EmptyRoot returns Root of an empty EDS. -func EmptyRoot() *Root { - return emptyRoot -} - -// EnsureEmptySquareExists checks if the given DAG contains an empty block data square. -// If it does not, it stores an empty block. This optimization exists to prevent -// redundant storing of empty block data so that it is only stored once and returned -// upon request for a block with an empty data square. Ref: header/constructors.go#L56 -func EnsureEmptySquareExists(ctx context.Context, bServ blockservice.BlockService) (*rsmt2d.ExtendedDataSquare, error) { - return AddShares(ctx, emptyDataSquare(), bServ) -} - -// EmptyExtendedDataSquare returns the EDS of the empty block data square. -func EmptyExtendedDataSquare() *rsmt2d.ExtendedDataSquare { - return emptyEDS -} - -// emptyDataSquare returns the minimum size data square filled with tail padding. -func emptyDataSquare() [][]byte { - return shares.ToBytes(shares.TailPaddingShares(appconsts.MinShareCount)) + emptyBlockRoot.Hash() } diff --git a/share/get_test.go b/share/get_test.go deleted file mode 100644 index 0ea83fd178..0000000000 --- a/share/get_test.go +++ /dev/null @@ -1,455 +0,0 @@ -package share - -import ( - "context" - mrand "math/rand" - "strconv" - "testing" - "time" - - "github.com/ipfs/go-blockservice" - "github.com/ipfs/go-cid" - ds "github.com/ipfs/go-datastore" - dssync "github.com/ipfs/go-datastore/sync" - blockstore "github.com/ipfs/go-ipfs-blockstore" - offline "github.com/ipfs/go-ipfs-exchange-offline" - mdutils "github.com/ipfs/go-merkledag/test" - "github.com/minio/sha256-simd" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/celestiaorg/celestia-app/pkg/wrapper" - "github.com/celestiaorg/nmt/namespace" - "github.com/celestiaorg/rsmt2d" - - "github.com/celestiaorg/celestia-node/libs/utils" - "github.com/celestiaorg/celestia-node/share/ipld" -) - -func TestGetShare(t *testing.T) { - const size = 8 - - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - defer cancel() - bServ := mdutils.Bserv() - - // generate random shares for the nmt - shares := RandShares(t, size*size) - eds, err := AddShares(ctx, shares, bServ) - require.NoError(t, err) - - for i, leaf := range shares { - row := i / size - pos := i - (size * row) - share, err := GetShare(ctx, bServ, ipld.MustCidFromNamespacedSha256(eds.RowRoots()[row]), pos, size*2) - require.NoError(t, err) - assert.Equal(t, leaf, share) - } -} - -func TestBlockRecovery(t *testing.T) { - originalSquareWidth := 8 - shareCount := originalSquareWidth * originalSquareWidth - extendedSquareWidth := 2 * originalSquareWidth - extendedShareCount := extendedSquareWidth * extendedSquareWidth - - // generate test data - quarterShares := RandShares(t, shareCount) - allShares := RandShares(t, shareCount) - - testCases := []struct { - name string - shares []Share - expectErr bool - errString string - d int // number of shares to delete - }{ - {"missing 1/2 shares", quarterShares, false, "", extendedShareCount / 2}, - {"missing 1/4 shares", quarterShares, false, "", extendedShareCount / 4}, - {"max missing data", quarterShares, false, "", (originalSquareWidth + 1) * (originalSquareWidth + 1)}, - {"missing all but one shares", allShares, true, "failed to solve data square", extendedShareCount - 1}, - } - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - squareSize := utils.SquareSize(len(tc.shares)) - - eds, err := rsmt2d.ComputeExtendedDataSquare(tc.shares, rsmt2d.NewRSGF8Codec(), wrapper.NewConstructor(squareSize)) - require.NoError(t, err) - - // calculate roots using the first complete square - rowRoots := eds.RowRoots() - colRoots := eds.ColRoots() - - flat := ExtractEDS(eds) - - // recover a partially complete square - rdata := removeRandShares(flat, tc.d) - eds, err = rsmt2d.ImportExtendedDataSquare( - rdata, - rsmt2d.NewRSGF8Codec(), - wrapper.NewConstructor(squareSize), - ) - require.NoError(t, err) - - err = eds.Repair(rowRoots, colRoots) - if tc.expectErr { - require.Error(t, err) - require.Contains(t, err.Error(), tc.errString) - return - } - assert.NoError(t, err) - - reds, err := rsmt2d.ImportExtendedDataSquare(rdata, rsmt2d.NewRSGF8Codec(), wrapper.NewConstructor(squareSize)) - require.NoError(t, err) - // check that the squares are equal - assert.Equal(t, ExtractEDS(eds), ExtractEDS(reds)) - }) - } -} - -func Test_ConvertEDStoShares(t *testing.T) { - squareWidth := 16 - shares := RandShares(t, squareWidth*squareWidth) - - // compute extended square - eds, err := rsmt2d.ComputeExtendedDataSquare( - shares, - rsmt2d.NewRSGF8Codec(), - wrapper.NewConstructor(uint64(squareWidth)), - ) - require.NoError(t, err) - - resshares := ExtractODS(eds) - require.Equal(t, shares, resshares) -} - -// removes d shares from data -func removeRandShares(data [][]byte, d int) [][]byte { - count := len(data) - // remove shares randomly - for i := 0; i < d; { - ind := mrand.Intn(count) - if len(data[ind]) == 0 { - continue - } - data[ind] = nil - i++ - } - return data -} - -func TestGetSharesByNamespace(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - bServ := mdutils.Bserv() - - var tests = []struct { - rawData []Share - }{ - {rawData: RandShares(t, 4)}, - {rawData: RandShares(t, 16)}, - } - - for i, tt := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - // choose random nID from rand shares - expected := tt.rawData[len(tt.rawData)/2] - nID := expected[:NamespaceSize] - - // change rawData to contain several shares with same nID - tt.rawData[(len(tt.rawData)/2)+1] = expected - // put raw data in BlockService - eds, err := AddShares(ctx, tt.rawData, bServ) - require.NoError(t, err) - - var shares []Share - for _, row := range eds.RowRoots() { - rcid := ipld.MustCidFromNamespacedSha256(row) - rowShares, _, err := GetSharesByNamespace(ctx, bServ, rcid, nID, len(eds.RowRoots())) - require.NoError(t, err) - - shares = append(shares, rowShares...) - } - - assert.Equal(t, 2, len(shares)) - for _, share := range shares { - assert.Equal(t, expected, share) - } - }) - } -} - -func TestCollectLeavesByNamespace_IncompleteData(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - bServ := mdutils.Bserv() - - shares := RandShares(t, 16) - - // set all shares to the same namespace id - nid := shares[0][:NamespaceSize] - - for i, nspace := range shares { - if i == len(shares) { - break - } - - copy(nspace[:NamespaceSize], nid) - } - - eds, err := AddShares(ctx, shares, bServ) - require.NoError(t, err) - - roots := eds.RowRoots() - - // remove the second share from the first row - rcid := ipld.MustCidFromNamespacedSha256(roots[0]) - node, err := ipld.GetNode(ctx, bServ, rcid) - require.NoError(t, err) - - // Left side of the tree contains the original shares - data, err := ipld.GetNode(ctx, bServ, node.Links()[0].Cid) - require.NoError(t, err) - - // Second share is the left side's right child - l, err := ipld.GetNode(ctx, bServ, data.Links()[0].Cid) - require.NoError(t, err) - r, err := ipld.GetNode(ctx, bServ, l.Links()[1].Cid) - require.NoError(t, err) - err = bServ.DeleteBlock(ctx, r.Cid()) - require.NoError(t, err) - - namespaceData := ipld.NewNamespaceData(len(shares), nid, ipld.WithLeaves()) - err = namespaceData.CollectLeavesByNamespace(ctx, bServ, rcid) - leaves := namespaceData.Leaves() - assert.Nil(t, leaves[1]) - assert.Equal(t, 4, len(leaves)) - require.Error(t, err) -} - -func TestCollectLeavesByNamespace_AbsentNamespaceId(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - bServ := mdutils.Bserv() - - shares := RandShares(t, 16) - - minNid := make([]byte, NamespaceSize) - midNid := make([]byte, NamespaceSize) - maxNid := make([]byte, NamespaceSize) - - numberOfShares := len(shares) - - copy(minNid, shares[0][:NamespaceSize]) - copy(maxNid, shares[numberOfShares-1][:NamespaceSize]) - copy(midNid, shares[numberOfShares/2][:NamespaceSize]) - - // create min nid missing data by replacing first namespace id with second - minNidMissingData := make([]Share, len(shares)) - copy(minNidMissingData, shares) - copy(minNidMissingData[0][:NamespaceSize], shares[1][:NamespaceSize]) - - // create max nid missing data by replacing last namespace id with second last - maxNidMissingData := make([]Share, len(shares)) - copy(maxNidMissingData, shares) - copy(maxNidMissingData[numberOfShares-1][:NamespaceSize], shares[numberOfShares-2][:NamespaceSize]) - - // create mid nid missing data by replacing middle namespace id with the one after - midNidMissingData := make([]Share, len(shares)) - copy(midNidMissingData, shares) - copy(midNidMissingData[numberOfShares/2][:NamespaceSize], shares[(numberOfShares/2)+1][:NamespaceSize]) - - var tests = []struct { - name string - data []Share - missingNid []byte - }{ - {name: "Namespace id less than the minimum namespace in data", data: minNidMissingData, missingNid: minNid}, - {name: "Namespace id greater than the maximum namespace in data", data: maxNidMissingData, missingNid: maxNid}, - {name: "Namespace id in range but still missing", data: midNidMissingData, missingNid: midNid}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - eds, err := AddShares(ctx, shares, bServ) - require.NoError(t, err) - assertNoRowContainsNID(t, bServ, eds, tt.missingNid) - }) - } -} - -func TestCollectLeavesByNamespace_MultipleRowsContainingSameNamespaceId(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - bServ := mdutils.Bserv() - - shares := RandShares(t, 16) - - // set all shares to the same namespace and data but the last one - nid := shares[0][:NamespaceSize] - commonNamespaceData := shares[0] - - for i, nspace := range shares { - if i == len(shares)-1 { - break - } - - copy(nspace, commonNamespaceData) - } - - eds, err := AddShares(ctx, shares, bServ) - require.NoError(t, err) - - for _, row := range eds.RowRoots() { - rcid := ipld.MustCidFromNamespacedSha256(row) - data := ipld.NewNamespaceData(len(shares), nid, ipld.WithLeaves()) - err := data.CollectLeavesByNamespace(ctx, bServ, rcid) - assert.Nil(t, err) - leaves := data.Leaves() - for _, node := range leaves { - // test that the data returned by collectLeavesByNamespace for nid - // matches the commonNamespaceData that was copied across almost all data - assert.Equal(t, commonNamespaceData, node.RawData()[NamespaceSize:]) - } - } -} - -func TestGetSharesWithProofsByNamespace(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - bServ := mdutils.Bserv() - - var tests = []struct { - rawData []Share - }{ - {rawData: RandShares(t, 4)}, - {rawData: RandShares(t, 16)}, - {rawData: RandShares(t, 64)}, - } - - for i, tt := range tests { - t.Run(strconv.Itoa(i), func(t *testing.T) { - rand := mrand.New(mrand.NewSource(time.Now().UnixNano())) - // choose random range in shares slice - from := rand.Intn(len(tt.rawData)) - to := rand.Intn(len(tt.rawData)) - - if to < from { - from, to = to, from - } - - expected := tt.rawData[from] - nID := expected[:NamespaceSize] - - // change rawData to contain several shares with same nID - for i := from; i <= to; i++ { - tt.rawData[i] = expected - } - - // put raw data in BlockService - eds, err := AddShares(ctx, tt.rawData, bServ) - require.NoError(t, err) - - var shares []Share - for _, row := range eds.RowRoots() { - rcid := ipld.MustCidFromNamespacedSha256(row) - rowShares, proof, err := GetSharesByNamespace(ctx, bServ, rcid, nID, len(eds.RowRoots())) - require.NoError(t, err) - if len(rowShares) > 0 { - require.NotNil(t, proof) - // append shares to check integrity later - shares = append(shares, rowShares...) - - // construct nodes from shares by prepending namespace - var leaves [][]byte - for _, sh := range rowShares { - leaves = append(leaves, append(sh[:NamespaceSize], sh...)) - } - - // verify namespace - verified := proof.VerifyNamespace( - sha256.New(), - nID, - leaves, - ipld.NamespacedSha256FromCID(rcid)) - require.True(t, verified) - - // verify inclusion - verified = proof.VerifyInclusion( - sha256.New(), - nID, - rowShares, - ipld.NamespacedSha256FromCID(rcid)) - require.True(t, verified) - } - } - - // validate shares - assert.Equal(t, to-from+1, len(shares)) - for _, share := range shares { - assert.Equal(t, expected, share) - } - }) - } -} - -func TestBatchSize(t *testing.T) { - tests := []struct { - name string - origWidth int - }{ - {"2", 2}, - {"4", 4}, - {"8", 8}, - {"16", 16}, - {"32", 32}, - // {"64", 64}, // test case too large for CI with race detector - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(tt.origWidth)) - defer cancel() - - bs := blockstore.NewBlockstore(dssync.MutexWrap(ds.NewMapDatastore())) - - eds := RandEDS(t, tt.origWidth) - _, err := AddShares(ctx, ExtractODS(eds), blockservice.New(bs, offline.Exchange(bs))) - require.NoError(t, err) - - out, err := bs.AllKeysChan(ctx) - require.NoError(t, err) - - var count int - for range out { - count++ - } - extendedWidth := tt.origWidth * 2 - assert.Equalf(t, count, ipld.BatchSize(extendedWidth), "batchSize(%v)", extendedWidth) - }) - } -} - -func assertNoRowContainsNID( - t *testing.T, - bServ blockservice.BlockService, - eds *rsmt2d.ExtendedDataSquare, - nID namespace.ID, -) { - rowRootCount := len(eds.RowRoots()) - // get all row root cids - rowRootCIDs := make([]cid.Cid, rowRootCount) - for i, rowRoot := range eds.RowRoots() { - rowRootCIDs[i] = ipld.MustCidFromNamespacedSha256(rowRoot) - } - - // for each row root cid check if the minNID exists - for _, rowCID := range rowRootCIDs { - data := ipld.NewNamespaceData(rowRootCount, nID, ipld.WithProofs()) - err := data.CollectLeavesByNamespace(context.Background(), bServ, rowCID) - leaves := data.Leaves() - assert.Nil(t, leaves) - assert.Nil(t, err) - } -} diff --git a/share/getter.go b/share/getter.go index 163741f925..3fcc93de33 100644 --- a/share/getter.go +++ b/share/getter.go @@ -2,22 +2,22 @@ package share import ( "context" + "crypto/sha256" "errors" "fmt" - "github.com/minio/sha256-simd" - "github.com/celestiaorg/nmt" - "github.com/celestiaorg/nmt/namespace" "github.com/celestiaorg/rsmt2d" + + "github.com/celestiaorg/celestia-node/header" ) var ( // ErrNotFound is used to indicate that requested data could not be found. ErrNotFound = errors.New("share: data not found") - // ErrNamespaceNotFound is returned by GetSharesByNamespace when data for requested root does - // not include any shares from the given namespace - ErrNamespaceNotFound = errors.New("share: namespace not found in data") + // ErrOutOfBounds is used to indicate that a passed row or column index is out of bounds of the + // square size. + ErrOutOfBounds = errors.New("share: row or column index is larger than square size") ) // Getter interface provides a set of accessors for shares by the Root. @@ -26,14 +26,17 @@ var ( //go:generate mockgen -destination=mocks/getter.go -package=mocks . Getter type Getter interface { // GetShare gets a Share by coordinates in EDS. - GetShare(ctx context.Context, root *Root, row, col int) (Share, error) + GetShare(ctx context.Context, header *header.ExtendedHeader, row, col int) (Share, error) - // GetEDS gets the full EDS identified by the given root. - GetEDS(context.Context, *Root) (*rsmt2d.ExtendedDataSquare, error) + // GetEDS gets the full EDS identified by the given extended header. + GetEDS(context.Context, *header.ExtendedHeader) (*rsmt2d.ExtendedDataSquare, error) // GetSharesByNamespace gets all shares from an EDS within the given namespace. // Shares are returned in a row-by-row order if the namespace spans multiple rows. - GetSharesByNamespace(context.Context, *Root, namespace.ID) (NamespacedShares, error) + // Inclusion of returned data could be verified using Verify method on NamespacedShares. + // If no shares are found for target namespace non-inclusion could be also verified by calling + // Verify method. + GetSharesByNamespace(context.Context, *header.ExtendedHeader, Namespace) (NamespacedShares, error) } // NamespacedShares represents all shares with proofs within a specific namespace of an EDS. @@ -50,15 +53,15 @@ func (ns NamespacedShares) Flatten() []Share { // NamespacedRow represents all shares with proofs within a specific namespace of a single EDS row. type NamespacedRow struct { - Shares []Share - Proof *nmt.Proof + Shares []Share `json:"shares"` + Proof *nmt.Proof `json:"proof"` } // Verify validates NamespacedShares by checking every row with nmt inclusion proof. -func (ns NamespacedShares) Verify(root *Root, nID namespace.ID) error { - originalRoots := make([][]byte, 0) - for _, row := range root.RowsRoots { - if !nID.Less(nmt.MinNamespace(row, nID.Size())) && nID.LessOrEqual(nmt.MaxNamespace(row, nID.Size())) { +func (ns NamespacedShares) Verify(root *Root, namespace Namespace) error { + var originalRoots [][]byte + for _, row := range root.RowRoots { + if !namespace.IsOutsideRange(row, row) { originalRoots = append(originalRoots, row) } } @@ -70,25 +73,26 @@ func (ns NamespacedShares) Verify(root *Root, nID namespace.ID) error { for i, row := range ns { // verify row data against row hash from original root - if !row.verify(originalRoots[i], nID) { - return fmt.Errorf("row verification failed: row %d doesn't match original root: %s", i, root.Hash()) + if !row.verify(originalRoots[i], namespace) { + return fmt.Errorf("row verification failed: row %d doesn't match original root: %s", i, root.String()) } } return nil } // verify validates the row using nmt inclusion proof. -func (row *NamespacedRow) verify(rowRoot []byte, nID namespace.ID) bool { +func (row *NamespacedRow) verify(rowRoot []byte, namespace Namespace) bool { // construct nmt leaves from shares by prepending namespace leaves := make([][]byte, 0, len(row.Shares)) - for _, sh := range row.Shares { - leaves = append(leaves, append(sh[:NamespaceSize], sh...)) + for _, shr := range row.Shares { + leaves = append(leaves, append(GetNamespace(shr), shr...)) } // verify namespace return row.Proof.VerifyNamespace( sha256.New(), - nID, + namespace.ToNMT(), leaves, - rowRoot) + rowRoot, + ) } diff --git a/share/getters/cascade.go b/share/getters/cascade.go index 3dcb2a472b..3875127580 100644 --- a/share/getters/cascade.go +++ b/share/getters/cascade.go @@ -7,11 +7,12 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/celestiaorg/nmt/namespace" "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/byzantine" ) var _ share.Getter = (*CascadeGetter)(nil) @@ -32,30 +33,37 @@ func NewCascadeGetter(getters []share.Getter) *CascadeGetter { } // GetShare gets a share from any of registered share.Getters in cascading order. -func (cg *CascadeGetter) GetShare(ctx context.Context, root *share.Root, row, col int) (share.Share, error) { +func (cg *CascadeGetter) GetShare( + ctx context.Context, header *header.ExtendedHeader, row, col int, +) (share.Share, error) { ctx, span := tracer.Start(ctx, "cascade/get-share", trace.WithAttributes( - attribute.String("root", root.String()), attribute.Int("row", row), attribute.Int("col", col), )) defer span.End() + upperBound := len(header.DAH.RowRoots) + if row >= upperBound || col >= upperBound { + err := share.ErrOutOfBounds + span.RecordError(err) + return nil, err + } get := func(ctx context.Context, get share.Getter) (share.Share, error) { - return get.GetShare(ctx, root, row, col) + return get.GetShare(ctx, header, row, col) } return cascadeGetters(ctx, cg.getters, get) } // GetEDS gets a full EDS from any of registered share.Getters in cascading order. -func (cg *CascadeGetter) GetEDS(ctx context.Context, root *share.Root) (*rsmt2d.ExtendedDataSquare, error) { - ctx, span := tracer.Start(ctx, "cascade/get-eds", trace.WithAttributes( - attribute.String("root", root.String()), - )) +func (cg *CascadeGetter) GetEDS( + ctx context.Context, header *header.ExtendedHeader, +) (*rsmt2d.ExtendedDataSquare, error) { + ctx, span := tracer.Start(ctx, "cascade/get-eds") defer span.End() get := func(ctx context.Context, get share.Getter) (*rsmt2d.ExtendedDataSquare, error) { - return get.GetEDS(ctx, root) + return get.GetEDS(ctx, header) } return cascadeGetters(ctx, cg.getters, get) @@ -65,17 +73,16 @@ func (cg *CascadeGetter) GetEDS(ctx context.Context, root *share.Root) (*rsmt2d. // order. func (cg *CascadeGetter) GetSharesByNamespace( ctx context.Context, - root *share.Root, - id namespace.ID, + header *header.ExtendedHeader, + namespace share.Namespace, ) (share.NamespacedShares, error) { ctx, span := tracer.Start(ctx, "cascade/get-shares-by-namespace", trace.WithAttributes( - attribute.String("root", root.String()), - attribute.String("nid", id.String()), + attribute.String("namespace", namespace.String()), )) defer span.End() get := func(ctx context.Context, get share.Getter) (share.NamespacedShares, error) { - return get.GetSharesByNamespace(ctx, root, id) + return get.GetSharesByNamespace(ctx, header, namespace) } return cascadeGetters(ctx, cg.getters, get) @@ -124,13 +131,22 @@ func cascadeGetters[V any]( if getErr == nil { return val, nil } - if errors.Is(share.ErrNamespaceNotFound, getErr) { - return zero, getErr + + if errors.Is(getErr, errOperationNotSupported) { + continue + } + + span.RecordError(getErr, trace.WithAttributes(attribute.Int("getter_idx", i))) + var byzantineErr *byzantine.ErrByzantine + if errors.As(getErr, &byzantineErr) { + // short circuit if byzantine error was detected (to be able to handle it correctly + // and create the BEFP) + return zero, byzantineErr } - if !errors.Is(getErr, errOperationNotSupported) { - err = errors.Join(err, getErr) - span.RecordError(getErr, trace.WithAttributes(attribute.Int("getter_idx", i))) + err = errors.Join(err, getErr) + if ctx.Err() != nil { + return zero, err } } return zero, err diff --git a/share/getters/cascade_test.go b/share/getters/cascade_test.go index e3a324c5e9..d2b44883a1 100644 --- a/share/getters/cascade_test.go +++ b/share/getters/cascade_test.go @@ -11,6 +11,7 @@ import ( "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/mocks" ) @@ -20,24 +21,24 @@ func TestCascadeGetter(t *testing.T) { t.Cleanup(cancel) const gettersN = 3 - roots := make([]*share.Root, gettersN) + headers := make([]*header.ExtendedHeader, gettersN) getters := make([]share.Getter, gettersN) - for i := range roots { - getters[i], roots[i] = TestGetter(t) + for i := range headers { + getters[i], headers[i] = TestGetter(t) } getter := NewCascadeGetter(getters) t.Run("GetShare", func(t *testing.T) { - for _, r := range roots { - sh, err := getter.GetShare(ctx, r, 0, 0) + for _, eh := range headers { + sh, err := getter.GetShare(ctx, eh, 0, 0) assert.NoError(t, err) assert.NotEmpty(t, sh) } }) t.Run("GetEDS", func(t *testing.T) { - for _, r := range roots { - sh, err := getter.GetEDS(ctx, r) + for _, eh := range headers { + sh, err := getter.GetEDS(ctx, eh) assert.NoError(t, err) assert.NotEmpty(t, sh) } @@ -52,14 +53,19 @@ func TestCascade(t *testing.T) { timeoutGetter := mocks.NewMockGetter(ctrl) immediateFailGetter := mocks.NewMockGetter(ctrl) successGetter := mocks.NewMockGetter(ctrl) + ctxGetter := mocks.NewMockGetter(ctrl) timeoutGetter.EXPECT().GetEDS(gomock.Any(), gomock.Any()). - DoAndReturn(func(ctx context.Context, _ *share.Root) (*rsmt2d.ExtendedDataSquare, error) { + DoAndReturn(func(ctx context.Context, _ *header.ExtendedHeader) (*rsmt2d.ExtendedDataSquare, error) { return nil, context.DeadlineExceeded }).AnyTimes() immediateFailGetter.EXPECT().GetEDS(gomock.Any(), gomock.Any()). Return(nil, errors.New("second getter fails immediately")).AnyTimes() successGetter.EXPECT().GetEDS(gomock.Any(), gomock.Any()). Return(nil, nil).AnyTimes() + ctxGetter.EXPECT().GetEDS(gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, _ *header.ExtendedHeader) (*rsmt2d.ExtendedDataSquare, error) { + return nil, ctx.Err() + }).AnyTimes() get := func(ctx context.Context, get share.Getter) (*rsmt2d.ExtendedDataSquare, error) { return get.GetEDS(ctx, nil) @@ -96,6 +102,15 @@ func TestCascade(t *testing.T) { assert.Equal(t, strings.Count(err.Error(), "\n"), 2) }) + t.Run("Context Canceled", func(t *testing.T) { + ctx, cancel := context.WithCancel(ctx) + cancel() + getters := []share.Getter{ctxGetter, ctxGetter, ctxGetter} + _, err := cascadeGetters(ctx, getters, get) + assert.Error(t, err) + assert.Equal(t, strings.Count(err.Error(), "\n"), 0) + }) + t.Run("Single", func(t *testing.T) { getters := []share.Getter{successGetter} _, err := cascadeGetters(ctx, getters, get) diff --git a/share/getters/getter_test.go b/share/getters/getter_test.go index 8751f49da0..7297766652 100644 --- a/share/getters/getter_test.go +++ b/share/getters/getter_test.go @@ -2,14 +2,15 @@ package getters import ( "context" + "os" + "sync" "testing" "time" - bsrv "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/exchange/offline" "github.com/ipfs/go-datastore" ds_sync "github.com/ipfs/go-datastore/sync" - offline "github.com/ipfs/go-ipfs-exchange-offline" - mdutils "github.com/ipfs/go-merkledag/test" + dsbadger "github.com/ipfs/go-ds-badger4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -17,72 +18,23 @@ import ( "github.com/celestiaorg/celestia-app/pkg/wrapper" "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share/sharetest" ) -func TestTeeGetter(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - t.Cleanup(cancel) - - tmpDir := t.TempDir() - ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - edsStore, err := eds.NewStore(tmpDir, ds) - require.NoError(t, err) - - err = edsStore.Start(ctx) - require.NoError(t, err) - - bServ := mdutils.Bserv() - ig := NewIPLDGetter(bServ) - tg := NewTeeGetter(ig, edsStore) - - t.Run("TeesToEDSStore", func(t *testing.T) { - eds, dah := randomEDS(t) - _, err := share.ImportShares(ctx, eds.Flattened(), bServ) - require.NoError(t, err) - - // eds store doesn't have the EDS yet - ok, err := edsStore.Has(ctx, dah.Hash()) - assert.False(t, ok) - assert.NoError(t, err) - - retrievedEDS, err := tg.GetEDS(ctx, &dah) - require.NoError(t, err) - require.True(t, share.EqualEDS(eds, retrievedEDS)) - - // eds store now has the EDS and it can be retrieved - ok, err = edsStore.Has(ctx, dah.Hash()) - assert.True(t, ok) - assert.NoError(t, err) - finalEDS, err := edsStore.Get(ctx, dah.Hash()) - assert.NoError(t, err) - require.True(t, share.EqualEDS(eds, finalEDS)) - }) - - t.Run("ShardAlreadyExistsDoesntError", func(t *testing.T) { - eds, dah := randomEDS(t) - _, err := share.ImportShares(ctx, eds.Flattened(), bServ) - require.NoError(t, err) - - retrievedEDS, err := tg.GetEDS(ctx, &dah) - require.NoError(t, err) - require.True(t, share.EqualEDS(eds, retrievedEDS)) - - // no error should be returned, even though the EDS identified by the DAH already exists - retrievedEDS, err = tg.GetEDS(ctx, &dah) - require.NoError(t, err) - require.True(t, share.EqualEDS(eds, retrievedEDS)) - }) -} - func TestStoreGetter(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) tmpDir := t.TempDir() + storeCfg := eds.DefaultParameters() ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - edsStore, err := eds.NewStore(tmpDir, ds) + edsStore, err := eds.NewStore(storeCfg, tmpDir, ds) require.NoError(t, err) err = edsStore.Start(ctx) @@ -91,59 +43,109 @@ func TestStoreGetter(t *testing.T) { sg := NewStoreGetter(edsStore) t.Run("GetShare", func(t *testing.T) { - eds, dah := randomEDS(t) - err = edsStore.Put(ctx, dah.Hash(), eds) + randEds, eh := randomEDS(t) + err = edsStore.Put(ctx, eh.DAH.Hash(), randEds) require.NoError(t, err) - squareSize := int(eds.Width()) + squareSize := int(randEds.Width()) for i := 0; i < squareSize; i++ { for j := 0; j < squareSize; j++ { - share, err := sg.GetShare(ctx, &dah, i, j) + share, err := sg.GetShare(ctx, eh, i, j) require.NoError(t, err) - assert.Equal(t, eds.GetCell(uint(i), uint(j)), share) + assert.Equal(t, randEds.GetCell(uint(i), uint(j)), share) } } + // doesn't panic on indexes too high + _, err := sg.GetShare(ctx, eh, squareSize, squareSize) + require.ErrorIs(t, err, share.ErrOutOfBounds) + // root not found - _, dah = randomEDS(t) - _, err = sg.GetShare(ctx, &dah, 0, 0) + _, eh = randomEDS(t) + _, err = sg.GetShare(ctx, eh, 0, 0) require.ErrorIs(t, err, share.ErrNotFound) }) t.Run("GetEDS", func(t *testing.T) { - eds, dah := randomEDS(t) - err = edsStore.Put(ctx, dah.Hash(), eds) + randEds, eh := randomEDS(t) + err = edsStore.Put(ctx, eh.DAH.Hash(), randEds) require.NoError(t, err) - retrievedEDS, err := sg.GetEDS(ctx, &dah) + retrievedEDS, err := sg.GetEDS(ctx, eh) require.NoError(t, err) - assert.True(t, share.EqualEDS(eds, retrievedEDS)) + assert.True(t, randEds.Equals(retrievedEDS)) // root not found - root := share.Root{} - _, err = sg.GetEDS(ctx, &root) + emptyRoot := da.MinDataAvailabilityHeader() + eh.DAH = &emptyRoot + _, err = sg.GetEDS(ctx, eh) require.ErrorIs(t, err, share.ErrNotFound) }) t.Run("GetSharesByNamespace", func(t *testing.T) { - eds, nID, dah := randomEDSWithDoubledNamespace(t, 4) - err = edsStore.Put(ctx, dah.Hash(), eds) + randEds, namespace, eh := randomEDSWithDoubledNamespace(t, 4) + err = edsStore.Put(ctx, eh.DAH.Hash(), randEds) require.NoError(t, err) - shares, err := sg.GetSharesByNamespace(ctx, &dah, nID) + shares, err := sg.GetSharesByNamespace(ctx, eh, namespace) require.NoError(t, err) - require.NoError(t, shares.Verify(&dah, nID)) + require.NoError(t, shares.Verify(eh.DAH, namespace)) assert.Len(t, shares.Flatten(), 2) - // nid not found - nID = make([]byte, 8) - _, err = sg.GetSharesByNamespace(ctx, &dah, nID) - require.ErrorIs(t, err, share.ErrNamespaceNotFound) + // namespace not found + randNamespace := sharetest.RandV0Namespace() + emptyShares, err := sg.GetSharesByNamespace(ctx, eh, randNamespace) + require.NoError(t, err) + require.Empty(t, emptyShares.Flatten()) // root not found - root := share.Root{} - _, err = sg.GetSharesByNamespace(ctx, &root, nID) + emptyRoot := da.MinDataAvailabilityHeader() + eh.DAH = &emptyRoot + _, err = sg.GetSharesByNamespace(ctx, eh, namespace) + require.ErrorIs(t, err, share.ErrNotFound) + }) + + t.Run("GetSharesFromNamespace removes corrupted shard", func(t *testing.T) { + randEds, namespace, eh := randomEDSWithDoubledNamespace(t, 4) + err = edsStore.Put(ctx, eh.DAH.Hash(), randEds) + require.NoError(t, err) + + // available + shares, err := sg.GetSharesByNamespace(ctx, eh, namespace) + require.NoError(t, err) + require.NoError(t, shares.Verify(eh.DAH, namespace)) + assert.Len(t, shares.Flatten(), 2) + + // 'corrupt' existing CAR by overwriting with a random EDS + f, err := os.OpenFile(tmpDir+"/blocks/"+eh.DAH.String(), os.O_WRONLY, 0644) + require.NoError(t, err) + edsToOverwriteWith, eh := randomEDS(t) + err = eds.WriteEDS(ctx, edsToOverwriteWith, f) + require.NoError(t, err) + + shares, err = sg.GetSharesByNamespace(ctx, eh, namespace) require.ErrorIs(t, err, share.ErrNotFound) + require.Nil(t, shares) + + // corruption detected, shard is removed + // try every 200ms until it passes or the context ends + ticker := time.NewTicker(200 * time.Millisecond) + defer ticker.Stop() + for { + select { + case <-ctx.Done(): + t.Fatal("context ended before successful retrieval") + case <-ticker.C: + has, err := edsStore.Has(ctx, eh.DAH.Hash()) + if err != nil { + t.Fatal(err) + } + if !has { + require.NoError(t, err) + return + } + } + } }) } @@ -151,37 +153,42 @@ func TestIPLDGetter(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - tmpDir := t.TempDir() + storeCfg := eds.DefaultParameters() ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - edsStore, err := eds.NewStore(tmpDir, ds) + edsStore, err := eds.NewStore(storeCfg, t.TempDir(), ds) require.NoError(t, err) err = edsStore.Start(ctx) require.NoError(t, err) - bserv := bsrv.New(edsStore.Blockstore(), offline.Exchange(edsStore.Blockstore())) + bStore := edsStore.Blockstore() + bserv := ipld.NewBlockservice(bStore, offline.Exchange(edsStore.Blockstore())) sg := NewIPLDGetter(bserv) t.Run("GetShare", func(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) t.Cleanup(cancel) - eds, dah := randomEDS(t) - err = edsStore.Put(ctx, dah.Hash(), eds) + randEds, eh := randomEDS(t) + err = edsStore.Put(ctx, eh.DAH.Hash(), randEds) require.NoError(t, err) - squareSize := int(eds.Width()) + squareSize := int(randEds.Width()) for i := 0; i < squareSize; i++ { for j := 0; j < squareSize; j++ { - share, err := sg.GetShare(ctx, &dah, i, j) + share, err := sg.GetShare(ctx, eh, i, j) require.NoError(t, err) - assert.Equal(t, eds.GetCell(uint(i), uint(j)), share) + assert.Equal(t, randEds.GetCell(uint(i), uint(j)), share) } } + // doesn't panic on indexes too high + _, err := sg.GetShare(ctx, eh, squareSize+1, squareSize+1) + require.ErrorIs(t, err, share.ErrOutOfBounds) + // root not found - _, dah = randomEDS(t) - _, err = sg.GetShare(ctx, &dah, 0, 0) + _, eh = randomEDS(t) + _, err = sg.GetShare(ctx, eh, 0, 0) require.ErrorIs(t, err, share.ErrNotFound) }) @@ -189,58 +196,149 @@ func TestIPLDGetter(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) t.Cleanup(cancel) - eds, dah := randomEDS(t) - err = edsStore.Put(ctx, dah.Hash(), eds) + randEds, eh := randomEDS(t) + err = edsStore.Put(ctx, eh.DAH.Hash(), randEds) require.NoError(t, err) - retrievedEDS, err := sg.GetEDS(ctx, &dah) + retrievedEDS, err := sg.GetEDS(ctx, eh) require.NoError(t, err) - assert.True(t, share.EqualEDS(eds, retrievedEDS)) + assert.True(t, randEds.Equals(retrievedEDS)) + + // Ensure blocks still exist after cleanup + colRoots, _ := retrievedEDS.ColRoots() + has, err := bStore.Has(ctx, ipld.MustCidFromNamespacedSha256(colRoots[0])) + assert.NoError(t, err) + assert.True(t, has) }) t.Run("GetSharesByNamespace", func(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) t.Cleanup(cancel) - eds, nID, dah := randomEDSWithDoubledNamespace(t, 4) - err = edsStore.Put(ctx, dah.Hash(), eds) + randEds, namespace, eh := randomEDSWithDoubledNamespace(t, 4) + err = edsStore.Put(ctx, eh.DAH.Hash(), randEds) require.NoError(t, err) // first check that shares are returned correctly if they exist - shares, err := sg.GetSharesByNamespace(ctx, &dah, nID) + shares, err := sg.GetSharesByNamespace(ctx, eh, namespace) require.NoError(t, err) - require.NoError(t, shares.Verify(&dah, nID)) + require.NoError(t, shares.Verify(eh.DAH, namespace)) assert.Len(t, shares.Flatten(), 2) - // nid not found - nID = make([]byte, 8) - emptyShares, err := sg.GetSharesByNamespace(ctx, &dah, nID) - require.ErrorIs(t, err, share.ErrNamespaceNotFound) - require.Nil(t, emptyShares) + // namespace not found + randNamespace := sharetest.RandV0Namespace() + emptyShares, err := sg.GetSharesByNamespace(ctx, eh, randNamespace) + require.NoError(t, err) + require.Empty(t, emptyShares.Flatten()) - // nid doesnt exist in root - root := share.Root{} - _, err = sg.GetSharesByNamespace(ctx, &root, nID) - require.ErrorIs(t, err, share.ErrNamespaceNotFound) + // nid doesn't exist in root + emptyRoot := da.MinDataAvailabilityHeader() + eh.DAH = &emptyRoot + emptyShares, err = sg.GetSharesByNamespace(ctx, eh, namespace) + require.NoError(t, err) + require.Empty(t, emptyShares.Flatten()) }) } -func randomEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, share.Root) { - eds := share.RandEDS(t, 4) - dah := da.NewDataAvailabilityHeader(eds) +// BenchmarkIPLDGetterOverBusyCache benchmarks the performance of the IPLDGetter when the +// cache size of the underlying blockstore is less than the number of blocks being requested in +// parallel. This is to ensure performance doesn't degrade when the cache is being frequently +// evicted. +// BenchmarkIPLDGetterOverBusyCache-10/128 1 12460428417 ns/op (~12s) +func BenchmarkIPLDGetterOverBusyCache(b *testing.B) { + const ( + blocks = 10 + size = 128 + ) - return eds, dah + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + b.Cleanup(cancel) + + dir := b.TempDir() + ds, err := dsbadger.NewDatastore(dir, &dsbadger.DefaultOptions) + require.NoError(b, err) + + newStore := func(params *eds.Parameters) *eds.Store { + edsStore, err := eds.NewStore(params, dir, ds) + require.NoError(b, err) + err = edsStore.Start(ctx) + require.NoError(b, err) + return edsStore + } + edsStore := newStore(eds.DefaultParameters()) + + // generate EDSs and store them + headers := make([]*header.ExtendedHeader, blocks) + for i := range headers { + eds := edstest.RandEDS(b, size) + dah, err := da.NewDataAvailabilityHeader(eds) + require.NoError(b, err) + err = edsStore.Put(ctx, dah.Hash(), eds) + require.NoError(b, err) + + eh := headertest.RandExtendedHeader(b) + eh.DAH = &dah + + // store cids for read loop later + headers[i] = eh + } + + // restart store to clear cache + require.NoError(b, edsStore.Stop(ctx)) + + // set BlockstoreCacheSize to 1 to force eviction on every read + params := eds.DefaultParameters() + params.BlockstoreCacheSize = 1 + edsStore = newStore(params) + bstore := edsStore.Blockstore() + bserv := ipld.NewBlockservice(bstore, offline.Exchange(bstore)) + + // start client + getter := NewIPLDGetter(bserv) + + // request blocks in parallel + b.ResetTimer() + g := sync.WaitGroup{} + g.Add(blocks) + for _, h := range headers { + h := h + go func() { + defer g.Done() + _, err := getter.GetEDS(ctx, h) + require.NoError(b, err) + }() + } + g.Wait() +} + +func randomEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, *header.ExtendedHeader) { + eds := edstest.RandEDS(t, 4) + dah, err := share.NewRoot(eds) + require.NoError(t, err) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) + return eds, eh } // randomEDSWithDoubledNamespace generates a random EDS and ensures that there are two shares in the // middle that share a namespace. -func randomEDSWithDoubledNamespace(t *testing.T, size int) (*rsmt2d.ExtendedDataSquare, []byte, share.Root) { +func randomEDSWithDoubledNamespace( + t *testing.T, + size int, +) (*rsmt2d.ExtendedDataSquare, []byte, *header.ExtendedHeader) { n := size * size - randShares := share.RandShares(t, n) + randShares := sharetest.RandShares(t, n) idx1 := (n - 1) / 2 idx2 := n / 2 - // make it so that two rows have the same namespace ID - copy(randShares[idx2][:8], randShares[idx1][:8]) + + // Make it so that the two shares in two different rows have a common + // namespace. For example if size=4, the original data square looks like + // this: + // _ _ _ _ + // _ _ _ D + // D _ _ _ + // _ _ _ _ + // where the D shares have a common namespace. + copy(share.GetNamespace(randShares[idx2]), share.GetNamespace(randShares[idx1])) eds, err := rsmt2d.ComputeExtendedDataSquare( randShares, @@ -248,7 +346,9 @@ func randomEDSWithDoubledNamespace(t *testing.T, size int) (*rsmt2d.ExtendedData wrapper.NewConstructor(uint64(size)), ) require.NoError(t, err, "failure to recompute the extended data square") - dah := da.NewDataAvailabilityHeader(eds) + dah, err := share.NewRoot(eds) + require.NoError(t, err) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) - return eds, randShares[idx1][:8], dah + return eds, share.GetNamespace(randShares[idx1]), eh } diff --git a/share/getters/ipld.go b/share/getters/ipld.go index 5ad8c92fb2..e9c930248d 100644 --- a/share/getters/ipld.go +++ b/share/getters/ipld.go @@ -7,16 +7,17 @@ import ( "sync" "sync/atomic" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/celestiaorg/nmt/namespace" "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/byzantine" "github.com/celestiaorg/celestia-node/share/ipld" ) @@ -39,10 +40,9 @@ func NewIPLDGetter(bServ blockservice.BlockService) *IPLDGetter { } // GetShare gets a single share at the given EDS coordinates from the bitswap network. -func (ig *IPLDGetter) GetShare(ctx context.Context, dah *share.Root, row, col int) (share.Share, error) { +func (ig *IPLDGetter) GetShare(ctx context.Context, header *header.ExtendedHeader, row, col int) (share.Share, error) { var err error ctx, span := tracer.Start(ctx, "ipld/get-share", trace.WithAttributes( - attribute.String("root", dah.String()), attribute.Int("row", row), attribute.Int("col", col), )) @@ -50,11 +50,18 @@ func (ig *IPLDGetter) GetShare(ctx context.Context, dah *share.Root, row, col in utils.SetStatusAndEnd(span, err) }() + dah := header.DAH + upperBound := len(dah.RowRoots) + if row >= upperBound || col >= upperBound { + err := share.ErrOutOfBounds + span.RecordError(err) + return nil, err + } root, leaf := ipld.Translate(dah, row, col) // wrap the blockservice in a session if it has been signaled in the context. blockGetter := getGetter(ctx, ig.bServ) - s, err := share.GetShare(ctx, blockGetter, root, leaf, len(dah.RowsRoots)) + s, err := ipld.GetShare(ctx, blockGetter, root, leaf, len(dah.RowRoots)) if errors.Is(err, ipld.ErrNodeNotFound) { // convert error to satisfy getter interface contract err = share.ErrNotFound @@ -66,20 +73,25 @@ func (ig *IPLDGetter) GetShare(ctx context.Context, dah *share.Root, row, col in return s, nil } -func (ig *IPLDGetter) GetEDS(ctx context.Context, root *share.Root) (eds *rsmt2d.ExtendedDataSquare, err error) { - ctx, span := tracer.Start(ctx, "ipld/get-eds", trace.WithAttributes( - attribute.String("root", root.String()), - )) +func (ig *IPLDGetter) GetEDS( + ctx context.Context, + header *header.ExtendedHeader, +) (eds *rsmt2d.ExtendedDataSquare, err error) { + ctx, span := tracer.Start(ctx, "ipld/get-eds") defer func() { utils.SetStatusAndEnd(span, err) }() // rtrv.Retrieve calls shares.GetShares until enough shares are retrieved to reconstruct the EDS - eds, err = ig.rtrv.Retrieve(ctx, root) + eds, err = ig.rtrv.Retrieve(ctx, header.DAH) if errors.Is(err, ipld.ErrNodeNotFound) { // convert error to satisfy getter interface contract err = share.ErrNotFound } + var errByz *byzantine.ErrByzantine + if errors.As(err, &errByz) { + return nil, err + } if err != nil { return nil, fmt.Errorf("getter/ipld: failed to retrieve eds: %w", err) } @@ -88,25 +100,23 @@ func (ig *IPLDGetter) GetEDS(ctx context.Context, root *share.Root) (eds *rsmt2d func (ig *IPLDGetter) GetSharesByNamespace( ctx context.Context, - root *share.Root, - nID namespace.ID, + header *header.ExtendedHeader, + namespace share.Namespace, ) (shares share.NamespacedShares, err error) { ctx, span := tracer.Start(ctx, "ipld/get-shares-by-namespace", trace.WithAttributes( - attribute.String("root", root.String()), - attribute.String("nID", nID.String()), + attribute.String("namespace", namespace.String()), )) defer func() { utils.SetStatusAndEnd(span, err) }() - err = verifyNIDSize(nID) - if err != nil { - return nil, fmt.Errorf("getter/ipld: invalid namespace ID: %w", err) + if err = namespace.ValidateForData(); err != nil { + return nil, err } // wrap the blockservice in a session if it has been signaled in the context. blockGetter := getGetter(ctx, ig.bServ) - shares, err = collectSharesByNamespace(ctx, blockGetter, root, nID) + shares, err = eds.CollectSharesByNamespace(ctx, blockGetter, header.DAH, namespace) if errors.Is(err, ipld.ErrNodeNotFound) { // convert error to satisfy getter interface contract err = share.ErrNotFound diff --git a/share/getters/shrex.go b/share/getters/shrex.go index a8dda90045..826c6b1a10 100644 --- a/share/getters/shrex.go +++ b/share/getters/shrex.go @@ -6,16 +6,17 @@ import ( "fmt" "time" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" - "go.opentelemetry.io/otel/metric/instrument/syncint64" - "go.opentelemetry.io/otel/metric/unit" + "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/trace" - "github.com/celestiaorg/nmt/namespace" "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/ipld" "github.com/celestiaorg/celestia-node/share/p2p" "github.com/celestiaorg/celestia-node/share/p2p/peers" "github.com/celestiaorg/celestia-node/share/p2p/shrexeds" @@ -31,47 +32,45 @@ const ( defaultMinAttemptsCount = 3 ) -var meter = global.MeterProvider().Meter("shrex/getter") +var meter = otel.Meter("shrex/getter") type metrics struct { - edsAttempts syncint64.Histogram - ndAttempts syncint64.Histogram + edsAttempts metric.Int64Histogram + ndAttempts metric.Int64Histogram } func (m *metrics) recordEDSAttempt(ctx context.Context, attemptCount int, success bool) { if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } - m.edsAttempts.Record(ctx, int64(attemptCount), attribute.Bool("success", success)) + ctx = utils.ResetContextOnError(ctx) + m.edsAttempts.Record(ctx, int64(attemptCount), + metric.WithAttributes( + attribute.Bool("success", success))) } func (m *metrics) recordNDAttempt(ctx context.Context, attemptCount int, success bool) { if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } - m.ndAttempts.Record(ctx, int64(attemptCount), attribute.Bool("success", success)) + ctx = utils.ResetContextOnError(ctx) + m.ndAttempts.Record(ctx, int64(attemptCount), + metric.WithAttributes( + attribute.Bool("success", success))) } func (sg *ShrexGetter) WithMetrics() error { - edsAttemptHistogram, err := meter.SyncInt64().Histogram( + edsAttemptHistogram, err := meter.Int64Histogram( "getters_shrex_eds_attempts_per_request", - instrument.WithUnit(unit.Dimensionless), - instrument.WithDescription("Number of attempts per shrex/eds request"), + metric.WithDescription("Number of attempts per shrex/eds request"), ) if err != nil { return err } - ndAttemptHistogram, err := meter.SyncInt64().Histogram( + ndAttemptHistogram, err := meter.Int64Histogram( "getters_shrex_nd_attempts_per_request", - instrument.WithUnit(unit.Dimensionless), - instrument.WithDescription("Number of attempts per shrex/nd request"), + metric.WithDescription("Number of attempts per shrex/nd request"), ) if err != nil { return err @@ -118,15 +117,24 @@ func (sg *ShrexGetter) Stop(ctx context.Context) error { return sg.peerManager.Stop(ctx) } -func (sg *ShrexGetter) GetShare(context.Context, *share.Root, int, int) (share.Share, error) { +func (sg *ShrexGetter) GetShare(context.Context, *header.ExtendedHeader, int, int) (share.Share, error) { return nil, fmt.Errorf("getter/shrex: GetShare %w", errOperationNotSupported) } -func (sg *ShrexGetter) GetEDS(ctx context.Context, root *share.Root) (*rsmt2d.ExtendedDataSquare, error) { +func (sg *ShrexGetter) GetEDS(ctx context.Context, header *header.ExtendedHeader) (*rsmt2d.ExtendedDataSquare, error) { var ( attempt int err error ) + ctx, span := tracer.Start(ctx, "shrex/get-eds") + defer func() { + utils.SetStatusAndEnd(span, err) + }() + + // short circuit if the data root is empty + if header.DAH.Equals(share.EmptyRoot()) { + return share.EmptyExtendedDataSquare(), nil + } for { if ctx.Err() != nil { sg.metrics.recordEDSAttempt(ctx, attempt, false) @@ -134,10 +142,10 @@ func (sg *ShrexGetter) GetEDS(ctx context.Context, root *share.Root) (*rsmt2d.Ex } attempt++ start := time.Now() - peer, setStatus, getErr := sg.peerManager.Peer(ctx, root.Hash()) + peer, setStatus, getErr := sg.peerManager.Peer(ctx, header.DAH.Hash(), header.Height()) if getErr != nil { log.Debugw("eds: couldn't find peer", - "hash", root.String(), + "hash", header.DAH.String(), "err", getErr, "finished (s)", time.Since(start)) sg.metrics.recordEDSAttempt(ctx, attempt, false) @@ -146,11 +154,11 @@ func (sg *ShrexGetter) GetEDS(ctx context.Context, root *share.Root) (*rsmt2d.Ex reqStart := time.Now() reqCtx, cancel := ctxWithSplitTimeout(ctx, sg.minAttemptsCount-attempt+1, sg.minRequestTimeout) - eds, getErr := sg.edsClient.RequestEDS(reqCtx, root.Hash(), peer) + eds, getErr := sg.edsClient.RequestEDS(reqCtx, header.DAH.Hash(), peer) cancel() switch { case getErr == nil: - setStatus(peers.ResultSynced) + setStatus(peers.ResultNoop) sg.metrics.recordEDSAttempt(ctx, attempt, true) return eds, nil case errors.Is(getErr, context.DeadlineExceeded), @@ -169,7 +177,7 @@ func (sg *ShrexGetter) GetEDS(ctx context.Context, root *share.Root) (*rsmt2d.Ex err = errors.Join(err, getErr) } log.Debugw("eds: request failed", - "hash", root.String(), + "hash", header.DAH.String(), "peer", peer.String(), "attempt", attempt, "err", getErr, @@ -179,18 +187,28 @@ func (sg *ShrexGetter) GetEDS(ctx context.Context, root *share.Root) (*rsmt2d.Ex func (sg *ShrexGetter) GetSharesByNamespace( ctx context.Context, - root *share.Root, - id namespace.ID, + header *header.ExtendedHeader, + namespace share.Namespace, ) (share.NamespacedShares, error) { + if err := namespace.ValidateForData(); err != nil { + return nil, err + } var ( attempt int err error ) + ctx, span := tracer.Start(ctx, "shrex/get-shares-by-namespace", trace.WithAttributes( + attribute.String("namespace", namespace.String()), + )) + defer func() { + utils.SetStatusAndEnd(span, err) + }() // verify that the namespace could exist inside the roots before starting network requests - roots := filterRootsByNamespace(root, id) + dah := header.DAH + roots := ipld.FilterRootByNamespace(dah, namespace) if len(roots) == 0 { - return nil, share.ErrNamespaceNotFound + return []share.NamespacedRow{}, nil } for { @@ -200,10 +218,11 @@ func (sg *ShrexGetter) GetSharesByNamespace( } attempt++ start := time.Now() - peer, setStatus, getErr := sg.peerManager.Peer(ctx, root.Hash()) + peer, setStatus, getErr := sg.peerManager.Peer(ctx, header.DAH.Hash(), header.Height()) if getErr != nil { log.Debugw("nd: couldn't find peer", - "hash", root.String(), + "hash", dah.String(), + "namespace", namespace.String(), "err", getErr, "finished (s)", time.Since(start)) sg.metrics.recordNDAttempt(ctx, attempt, false) @@ -212,22 +231,19 @@ func (sg *ShrexGetter) GetSharesByNamespace( reqStart := time.Now() reqCtx, cancel := ctxWithSplitTimeout(ctx, sg.minAttemptsCount-attempt+1, sg.minRequestTimeout) - nd, getErr := sg.ndClient.RequestND(reqCtx, root, id, peer) + nd, getErr := sg.ndClient.RequestND(reqCtx, dah, namespace, peer) cancel() switch { case getErr == nil: - if getErr = nd.Verify(root, id); getErr != nil { + // both inclusion and non-inclusion cases needs verification + if verErr := nd.Verify(dah, namespace); verErr != nil { + getErr = verErr setStatus(peers.ResultBlacklistPeer) break } setStatus(peers.ResultNoop) sg.metrics.recordNDAttempt(ctx, attempt, true) - return nd, getErr - case errors.Is(getErr, share.ErrNamespaceNotFound): - // TODO: will be merged with first case once non-inclusion proofs are ready - setStatus(peers.ResultNoop) - sg.metrics.recordNDAttempt(ctx, attempt, true) - return nd, getErr + return nd, nil case errors.Is(getErr, context.DeadlineExceeded), errors.Is(getErr, context.Canceled): setStatus(peers.ResultCooldownPeer) @@ -244,7 +260,8 @@ func (sg *ShrexGetter) GetSharesByNamespace( err = errors.Join(err, getErr) } log.Debugw("nd: request failed", - "hash", root.String(), + "hash", dah.String(), + "namespace", namespace.String(), "peer", peer.String(), "attempt", attempt, "err", getErr, diff --git a/share/getters/shrex_test.go b/share/getters/shrex_test.go index 9d727635a8..075735579b 100644 --- a/share/getters/shrex_test.go +++ b/share/getters/shrex_test.go @@ -2,32 +2,33 @@ package getters import ( "context" + "encoding/binary" + "errors" "testing" "time" "github.com/ipfs/go-datastore" ds_sync "github.com/ipfs/go-datastore/sync" - routinghelpers "github.com/libp2p/go-libp2p-routing-helpers" "github.com/libp2p/go-libp2p/core/host" - routingdisc "github.com/libp2p/go-libp2p/p2p/discovery/routing" "github.com/libp2p/go-libp2p/p2p/net/conngater" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/stretchr/testify/require" - "github.com/celestiaorg/celestia-app/pkg/da" libhead "github.com/celestiaorg/go-header" - "github.com/celestiaorg/nmt/namespace" + "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" - "github.com/celestiaorg/celestia-node/share/p2p/discovery" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/ipld" "github.com/celestiaorg/celestia-node/share/p2p/peers" "github.com/celestiaorg/celestia-node/share/p2p/shrexeds" "github.com/celestiaorg/celestia-node/share/p2p/shrexnd" "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" + "github.com/celestiaorg/celestia-node/share/sharetest" ) func TestShrexGetter(t *testing.T) { @@ -55,21 +56,23 @@ func TestShrexGetter(t *testing.T) { getter := NewShrexGetter(edsClient, ndClient, peerManager) require.NoError(t, getter.Start(ctx)) - t.Run("ND_Available", func(t *testing.T) { - ctx, cancel := context.WithTimeout(ctx, time.Second) + t.Run("ND_Available, total data size > 1mb", func(t *testing.T) { + ctx, cancel := context.WithTimeout(ctx, time.Second*10) t.Cleanup(cancel) // generate test data - eds, dah, nID := generateTestEDS(t) - require.NoError(t, edsStore.Put(ctx, dah.Hash(), eds)) + namespace := sharetest.RandV0Namespace() + randEDS, dah := edstest.RandEDSWithNamespace(t, namespace, 64) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) + require.NoError(t, edsStore.Put(ctx, dah.Hash(), randEDS)) peerManager.Validate(ctx, srvHost.ID(), shrexsub.Notification{ DataHash: dah.Hash(), Height: 1, }) - got, err := getter.GetSharesByNamespace(ctx, &dah, nID) + got, err := getter.GetSharesByNamespace(ctx, eh, namespace) require.NoError(t, err) - require.NoError(t, got.Verify(&dah, nID)) + require.NoError(t, got.Verify(dah, namespace)) }) t.Run("ND_err_not_found", func(t *testing.T) { @@ -77,50 +80,83 @@ func TestShrexGetter(t *testing.T) { t.Cleanup(cancel) // generate test data - _, dah, nID := generateTestEDS(t) + _, dah, namespace := generateTestEDS(t) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) peerManager.Validate(ctx, srvHost.ID(), shrexsub.Notification{ DataHash: dah.Hash(), Height: 1, }) - _, err := getter.GetSharesByNamespace(ctx, &dah, nID) + _, err := getter.GetSharesByNamespace(ctx, eh, namespace) require.ErrorIs(t, err, share.ErrNotFound) }) - t.Run("ND_namespace_not_found", func(t *testing.T) { + t.Run("ND_namespace_not_included", func(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) t.Cleanup(cancel) // generate test data - eds, dah, nID := generateTestEDS(t) + eds, dah, maxNamespace := generateTestEDS(t) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) require.NoError(t, edsStore.Put(ctx, dah.Hash(), eds)) peerManager.Validate(ctx, srvHost.ID(), shrexsub.Notification{ DataHash: dah.Hash(), Height: 1, }) - // corrupt NID - nID[4]++ + nID, err := addToNamespace(maxNamespace, -1) + require.NoError(t, err) + // check for namespace to be between max and min namespace in root + require.Len(t, ipld.FilterRootByNamespace(dah, nID), 1) - _, err := getter.GetSharesByNamespace(ctx, &dah, nID) - require.ErrorIs(t, err, share.ErrNamespaceNotFound) + emptyShares, err := getter.GetSharesByNamespace(ctx, eh, nID) + require.NoError(t, err) + // no shares should be returned + require.Empty(t, emptyShares.Flatten()) + require.Nil(t, emptyShares.Verify(dah, nID)) }) - t.Run("EDS_Available", func(t *testing.T) { + t.Run("ND_namespace_not_in_dah", func(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) t.Cleanup(cancel) // generate test data - eds, dah, _ := generateTestEDS(t) + eds, dah, maxNamespace := generateTestEDS(t) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) require.NoError(t, edsStore.Put(ctx, dah.Hash(), eds)) peerManager.Validate(ctx, srvHost.ID(), shrexsub.Notification{ DataHash: dah.Hash(), Height: 1, }) - got, err := getter.GetEDS(ctx, &dah) + namespace, err := addToNamespace(maxNamespace, 1) require.NoError(t, err) - require.Equal(t, eds.Flattened(), got.Flattened()) + // check for namespace to be not in root + require.Len(t, ipld.FilterRootByNamespace(dah, namespace), 0) + + emptyShares, err := getter.GetSharesByNamespace(ctx, eh, namespace) + require.NoError(t, err) + // no shares should be returned + require.Empty(t, emptyShares.Flatten()) + require.Nil(t, emptyShares.Verify(dah, namespace)) + }) + + t.Run("EDS_Available", func(t *testing.T) { + ctx, cancel := context.WithTimeout(ctx, time.Second) + t.Cleanup(cancel) + + // generate test data + randEDS, dah, _ := generateTestEDS(t) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) + require.NoError(t, edsStore.Put(ctx, dah.Hash(), randEDS)) + peerManager.Validate(ctx, srvHost.ID(), shrexsub.Notification{ + DataHash: dah.Hash(), + Height: 1, + }) + + got, err := getter.GetEDS(ctx, eh) + require.NoError(t, err) + require.Equal(t, randEDS.Flattened(), got.Flattened()) }) t.Run("EDS_ctx_deadline", func(t *testing.T) { @@ -128,13 +164,14 @@ func TestShrexGetter(t *testing.T) { // generate test data _, dah, _ := generateTestEDS(t) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) peerManager.Validate(ctx, srvHost.ID(), shrexsub.Notification{ DataHash: dah.Hash(), Height: 1, }) cancel() - _, err := getter.GetEDS(ctx, &dah) + _, err := getter.GetEDS(ctx, eh) require.ErrorIs(t, err, context.Canceled) }) @@ -144,12 +181,13 @@ func TestShrexGetter(t *testing.T) { // generate test data _, dah, _ := generateTestEDS(t) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) peerManager.Validate(ctx, srvHost.ID(), shrexsub.Notification{ DataHash: dah.Hash(), Height: 1, }) - _, err := getter.GetEDS(ctx, &dah) + _, err := getter.GetEDS(ctx, eh) require.ErrorIs(t, err, share.ErrNotFound) }) } @@ -157,16 +195,16 @@ func TestShrexGetter(t *testing.T) { func newStore(t *testing.T) (*eds.Store, error) { t.Helper() - tmpDir := t.TempDir() ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - return eds.NewStore(tmpDir, ds) + return eds.NewStore(eds.DefaultParameters(), t.TempDir(), ds) } -func generateTestEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, da.DataAvailabilityHeader, namespace.ID) { - eds := share.RandEDS(t, 4) - dah := da.NewDataAvailabilityHeader(eds) - randNID := dah.RowsRoots[(len(dah.RowsRoots)-1)/2][:8] - return eds, dah, randNID +func generateTestEDS(t *testing.T) (*rsmt2d.ExtendedDataSquare, *share.Root, share.Namespace) { + eds := edstest.RandEDS(t, 4) + dah, err := share.NewRoot(eds) + require.NoError(t, err) + max := nmt.MaxNamespace(dah.RowRoots[(len(dah.RowRoots))/2-1], share.NamespaceSize) + return eds, dah, max } func testManager( @@ -177,22 +215,15 @@ func testManager( return nil, err } - disc := discovery.NewDiscovery(nil, - routingdisc.NewRoutingDiscovery(routinghelpers.Null{}), - discovery.WithPeersLimit(10), - discovery.WithAdvertiseInterval(time.Second), - ) connGater, err := conngater.NewBasicConnectionGater(ds_sync.MutexWrap(datastore.NewMapDatastore())) if err != nil { return nil, err } manager, err := peers.NewManager( peers.DefaultParameters(), - headerSub, - shrexSub, - disc, host, connGater, + peers.WithShrexSubPools(shrexSub, headerSub), ) return manager, err } @@ -203,7 +234,7 @@ func newNDClientServer( params := shrexnd.DefaultParameters() // create server and register handler - server, err := shrexnd.NewServer(params, srvHost, edsStore, NewStoreGetter(edsStore)) + server, err := shrexnd.NewServer(params, srvHost, edsStore) require.NoError(t, err) require.NoError(t, server.Start(ctx)) @@ -236,3 +267,103 @@ func newEDSClientServer( require.NoError(t, err) return client, server } + +// addToNamespace adds arbitrary int value to namespace, treating namespace as big-endian +// implementation of int +func addToNamespace(namespace share.Namespace, val int) (share.Namespace, error) { + if val == 0 { + return namespace, nil + } + // Convert the input integer to a byte slice and add it to result slice + result := make([]byte, len(namespace)) + if val > 0 { + binary.BigEndian.PutUint64(result[len(namespace)-8:], uint64(val)) + } else { + binary.BigEndian.PutUint64(result[len(namespace)-8:], uint64(-val)) + } + + // Perform addition byte by byte + var carry int + for i := len(namespace) - 1; i >= 0; i-- { + sum := 0 + if val > 0 { + sum = int(namespace[i]) + int(result[i]) + carry + } else { + sum = int(namespace[i]) - int(result[i]) + carry + } + + switch { + case sum > 255: + carry = 1 + sum -= 256 + case sum < 0: + carry = -1 + sum += 256 + default: + carry = 0 + } + + result[i] = uint8(sum) + } + + // Handle any remaining carry + if carry != 0 { + return nil, errors.New("namespace overflow") + } + + return result, nil +} + +func TestAddToNamespace(t *testing.T) { + testCases := []struct { + name string + value int + input share.Namespace + expected share.Namespace + expectedError error + }{ + { + name: "Positive value addition", + value: 42, + input: share.Namespace{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}, + expected: share.Namespace{0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x2b}, + expectedError: nil, + }, + { + name: "Negative value addition", + value: -42, + input: share.Namespace{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}, + expected: share.Namespace{0x1, 0x1, 0x1, 0x1, 0x1, 0x01, 0x1, 0x1, 0x1, 0x0, 0xd7}, + expectedError: nil, + }, + { + name: "Overflow error", + value: 1, + input: share.Namespace{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, + expected: nil, + expectedError: errors.New("namespace overflow"), + }, + { + name: "Overflow error negative", + value: -1, + input: share.Namespace{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, + expected: nil, + expectedError: errors.New("namespace overflow"), + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := addToNamespace(tc.input, tc.value) + if tc.expectedError == nil { + require.NoError(t, err) + require.Equal(t, tc.expected, result) + return + } + require.Error(t, err) + if err.Error() != tc.expectedError.Error() { + t.Errorf("Unexpected error message. Expected: %v, Got: %v", tc.expectedError, err) + } + }) + } +} diff --git a/share/getters/store.go b/share/getters/store.go index 156c9cf2ee..d66a057c56 100644 --- a/share/getters/store.go +++ b/share/getters/store.go @@ -8,9 +8,9 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" - "github.com/celestiaorg/nmt/namespace" "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" @@ -34,10 +34,10 @@ func NewStoreGetter(store *eds.Store) *StoreGetter { // GetShare gets a single share at the given EDS coordinates from the eds.Store through the // corresponding CAR-level blockstore. -func (sg *StoreGetter) GetShare(ctx context.Context, dah *share.Root, row, col int) (share.Share, error) { +func (sg *StoreGetter) GetShare(ctx context.Context, header *header.ExtendedHeader, row, col int) (share.Share, error) { + dah := header.DAH var err error ctx, span := tracer.Start(ctx, "store/get-share", trace.WithAttributes( - attribute.String("root", dah.String()), attribute.Int("row", row), attribute.Int("col", col), )) @@ -45,6 +45,12 @@ func (sg *StoreGetter) GetShare(ctx context.Context, dah *share.Root, row, col i utils.SetStatusAndEnd(span, err) }() + upperBound := len(dah.RowRoots) + if row >= upperBound || col >= upperBound { + err := share.ErrOutOfBounds + span.RecordError(err) + return nil, err + } root, leaf := ipld.Translate(dah, row, col) bs, err := sg.store.CARBlockstore(ctx, dah.Hash()) if errors.Is(err, eds.ErrNotFound) { @@ -54,10 +60,15 @@ func (sg *StoreGetter) GetShare(ctx context.Context, dah *share.Root, row, col i if err != nil { return nil, fmt.Errorf("getter/store: failed to retrieve blockstore: %w", err) } + defer func() { + if err := bs.Close(); err != nil { + log.Warnw("closing blockstore", "err", err) + } + }() // wrap the read-only CAR blockstore in a getter blockGetter := eds.NewBlockGetter(bs) - s, err := share.GetShare(ctx, blockGetter, root, leaf, len(dah.RowsRoots)) + s, err := ipld.GetShare(ctx, blockGetter, root, leaf, len(dah.RowRoots)) if errors.Is(err, ipld.ErrNodeNotFound) { // convert error to satisfy getter interface contract err = share.ErrNotFound @@ -70,15 +81,15 @@ func (sg *StoreGetter) GetShare(ctx context.Context, dah *share.Root, row, col i } // GetEDS gets the EDS identified by the given root from the EDS store. -func (sg *StoreGetter) GetEDS(ctx context.Context, root *share.Root) (data *rsmt2d.ExtendedDataSquare, err error) { - ctx, span := tracer.Start(ctx, "store/get-eds", trace.WithAttributes( - attribute.String("root", root.String()), - )) +func (sg *StoreGetter) GetEDS( + ctx context.Context, header *header.ExtendedHeader, +) (data *rsmt2d.ExtendedDataSquare, err error) { + ctx, span := tracer.Start(ctx, "store/get-eds") defer func() { utils.SetStatusAndEnd(span, err) }() - data, err = sg.store.Get(ctx, root.Hash()) + data, err = sg.store.Get(ctx, header.DAH.Hash()) if errors.Is(err, eds.ErrNotFound) { // convert error to satisfy getter interface contract err = share.ErrNotFound @@ -93,41 +104,19 @@ func (sg *StoreGetter) GetEDS(ctx context.Context, root *share.Root) (data *rsmt // corresponding CAR-level blockstore. func (sg *StoreGetter) GetSharesByNamespace( ctx context.Context, - root *share.Root, - nID namespace.ID, + header *header.ExtendedHeader, + namespace share.Namespace, ) (shares share.NamespacedShares, err error) { ctx, span := tracer.Start(ctx, "store/get-shares-by-namespace", trace.WithAttributes( - attribute.String("root", root.String()), - attribute.String("nID", nID.String()), + attribute.String("namespace", namespace.String()), )) defer func() { utils.SetStatusAndEnd(span, err) }() - err = verifyNIDSize(nID) - if err != nil { - return nil, fmt.Errorf("getter/store: invalid namespace ID: %w", err) - } - - bs, err := sg.store.CARBlockstore(ctx, root.Hash()) - if errors.Is(err, eds.ErrNotFound) { - // convert error to satisfy getter interface contract - err = share.ErrNotFound - } - if err != nil { - return nil, fmt.Errorf("getter/store: failed to retrieve blockstore: %w", err) - } - - // wrap the read-only CAR blockstore in a getter - blockGetter := eds.NewBlockGetter(bs) - shares, err = collectSharesByNamespace(ctx, blockGetter, root, nID) - if errors.Is(err, ipld.ErrNodeNotFound) { - // convert error to satisfy getter interface contract - err = share.ErrNotFound - } + ns, err := eds.RetrieveNamespaceFromStore(ctx, sg.store, header.DAH, namespace) if err != nil { - return nil, fmt.Errorf("getter/store: failed to retrieve shares by namespace: %w", err) + return nil, fmt.Errorf("getter/store: %w", err) } - - return shares, nil + return ns, nil } diff --git a/share/getters/tee.go b/share/getters/tee.go deleted file mode 100644 index 41e9073fd4..0000000000 --- a/share/getters/tee.go +++ /dev/null @@ -1,85 +0,0 @@ -package getters - -import ( - "context" - "errors" - "fmt" - - "github.com/filecoin-project/dagstore" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - - "github.com/celestiaorg/nmt/namespace" - "github.com/celestiaorg/rsmt2d" - - "github.com/celestiaorg/celestia-node/libs/utils" - "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/eds" -) - -var _ share.Getter = (*TeeGetter)(nil) - -// TeeGetter is a share.Getter that wraps a getter and stores the results of GetEDS into an -// eds.Store. -type TeeGetter struct { - getter share.Getter - store *eds.Store -} - -// NewTeeGetter creates a new TeeGetter. -func NewTeeGetter(getter share.Getter, store *eds.Store) *TeeGetter { - return &TeeGetter{ - getter: getter, - store: store, - } -} - -func (tg *TeeGetter) GetShare(ctx context.Context, root *share.Root, row, col int) (share share.Share, err error) { - ctx, span := tracer.Start(ctx, "tee/get-share", trace.WithAttributes( - attribute.String("root", root.String()), - attribute.Int("row", row), - attribute.Int("col", col), - )) - defer func() { - utils.SetStatusAndEnd(span, err) - }() - - return tg.getter.GetShare(ctx, root, row, col) -} - -func (tg *TeeGetter) GetEDS(ctx context.Context, root *share.Root) (eds *rsmt2d.ExtendedDataSquare, err error) { - ctx, span := tracer.Start(ctx, "tee/get-eds", trace.WithAttributes( - attribute.String("root", root.String()), - )) - defer func() { - utils.SetStatusAndEnd(span, err) - }() - - eds, err = tg.getter.GetEDS(ctx, root) - if err != nil { - return nil, err - } - - err = tg.store.Put(ctx, root.Hash(), eds) - if err != nil && !errors.Is(err, dagstore.ErrShardExists) { - return nil, fmt.Errorf("getter/tee: failed to store eds: %w", err) - } - - return eds, nil -} - -func (tg *TeeGetter) GetSharesByNamespace( - ctx context.Context, - root *share.Root, - id namespace.ID, -) (shares share.NamespacedShares, err error) { - ctx, span := tracer.Start(ctx, "tee/get-shares-by-namespace", trace.WithAttributes( - attribute.String("root", root.String()), - attribute.String("nID", id.String()), - )) - defer func() { - utils.SetStatusAndEnd(span, err) - }() - - return tg.getter.GetSharesByNamespace(ctx, root, id) -} diff --git a/share/getters/testing.go b/share/getters/testing.go index a90b937a51..fafeb0541c 100644 --- a/share/getters/testing.go +++ b/share/getters/testing.go @@ -5,20 +5,26 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/require" + "github.com/celestiaorg/celestia-app/pkg/da" - "github.com/celestiaorg/nmt/namespace" "github.com/celestiaorg/rsmt2d" + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/edstest" ) // TestGetter provides a testing SingleEDSGetter and the root of the EDS it holds. -func TestGetter(t *testing.T) (share.Getter, *share.Root) { - eds := share.RandEDS(t, 8) - dah := da.NewDataAvailabilityHeader(eds) +func TestGetter(t *testing.T) (share.Getter, *header.ExtendedHeader) { + eds := edstest.RandEDS(t, 8) + dah, err := share.NewRoot(eds) + eh := headertest.RandExtendedHeaderWithRoot(t, dah) + require.NoError(t, err) return &SingleEDSGetter{ EDS: eds, - }, &dah + }, eh } // SingleEDSGetter contains a single EDS where data is retrieved from. @@ -28,8 +34,12 @@ type SingleEDSGetter struct { } // GetShare gets a share from a kept EDS if exist and if the correct root is given. -func (seg *SingleEDSGetter) GetShare(_ context.Context, root *share.Root, row, col int) (share.Share, error) { - err := seg.checkRoot(root) +func (seg *SingleEDSGetter) GetShare( + _ context.Context, + header *header.ExtendedHeader, + row, col int, +) (share.Share, error) { + err := seg.checkRoot(header.DAH) if err != nil { return nil, err } @@ -37,8 +47,11 @@ func (seg *SingleEDSGetter) GetShare(_ context.Context, root *share.Root, row, c } // GetEDS returns a kept EDS if the correct root is given. -func (seg *SingleEDSGetter) GetEDS(_ context.Context, root *share.Root) (*rsmt2d.ExtendedDataSquare, error) { - err := seg.checkRoot(root) +func (seg *SingleEDSGetter) GetEDS( + _ context.Context, + header *header.ExtendedHeader, +) (*rsmt2d.ExtendedDataSquare, error) { + err := seg.checkRoot(header.DAH) if err != nil { return nil, err } @@ -46,13 +59,16 @@ func (seg *SingleEDSGetter) GetEDS(_ context.Context, root *share.Root) (*rsmt2d } // GetSharesByNamespace returns NamespacedShares from a kept EDS if the correct root is given. -func (seg *SingleEDSGetter) GetSharesByNamespace(context.Context, *share.Root, namespace.ID, +func (seg *SingleEDSGetter) GetSharesByNamespace(context.Context, *header.ExtendedHeader, share.Namespace, ) (share.NamespacedShares, error) { panic("SingleEDSGetter: GetSharesByNamespace is not implemented") } func (seg *SingleEDSGetter) checkRoot(root *share.Root) error { - dah := da.NewDataAvailabilityHeader(seg.EDS) + dah, err := da.NewDataAvailabilityHeader(seg.EDS) + if err != nil { + return err + } if !root.Equals(&dah) { return fmt.Errorf("unknown EDS: have %s, asked %s", dah.String(), root.String()) } diff --git a/share/getters/utils.go b/share/getters/utils.go index b80478af15..2260183b4f 100644 --- a/share/getters/utils.go +++ b/share/getters/utils.go @@ -3,23 +3,10 @@ package getters import ( "context" "errors" - "fmt" "time" - "github.com/ipfs/go-blockservice" - "github.com/ipfs/go-cid" logging "github.com/ipfs/go-log/v2" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/trace" - "golang.org/x/sync/errgroup" - - "github.com/celestiaorg/nmt" - "github.com/celestiaorg/nmt/namespace" - - "github.com/celestiaorg/celestia-node/libs/utils" - "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/ipld" ) var ( @@ -29,77 +16,6 @@ var ( errOperationNotSupported = errors.New("operation is not supported") ) -// filterRootsByNamespace returns the row roots from the given share.Root that contain the passed -// namespace ID. -func filterRootsByNamespace(root *share.Root, nID namespace.ID) []cid.Cid { - rowRootCIDs := make([]cid.Cid, 0, len(root.RowsRoots)) - for _, row := range root.RowsRoots { - if !nID.Less(nmt.MinNamespace(row, nID.Size())) && nID.LessOrEqual(nmt.MaxNamespace(row, nID.Size())) { - rowRootCIDs = append(rowRootCIDs, ipld.MustCidFromNamespacedSha256(row)) - } - } - return rowRootCIDs -} - -// collectSharesByNamespace collects NamespaceShares within the given namespace ID from the given -// share.Root. -func collectSharesByNamespace( - ctx context.Context, - bg blockservice.BlockGetter, - root *share.Root, - nID namespace.ID, -) (shares share.NamespacedShares, err error) { - ctx, span := tracer.Start(ctx, "collect-shares-by-namespace", trace.WithAttributes( - attribute.String("root", root.String()), - attribute.String("nid", nID.String()), - )) - defer func() { - utils.SetStatusAndEnd(span, err) - }() - - rootCIDs := filterRootsByNamespace(root, nID) - if len(rootCIDs) == 0 { - return nil, share.ErrNamespaceNotFound - } - - errGroup, ctx := errgroup.WithContext(ctx) - shares = make([]share.NamespacedRow, len(rootCIDs)) - for i, rootCID := range rootCIDs { - // shadow loop variables, to ensure correct values are captured - i, rootCID := i, rootCID - errGroup.Go(func() error { - row, proof, err := share.GetSharesByNamespace(ctx, bg, rootCID, nID, len(root.RowsRoots)) - shares[i] = share.NamespacedRow{ - Shares: row, - Proof: proof, - } - if err != nil { - return fmt.Errorf("retrieving nID %x for row %x: %w", nID, rootCID, err) - } - return nil - }) - } - - if err := errGroup.Wait(); err != nil { - return nil, err - } - - // return ErrNamespaceNotFound if no shares are found for the namespace.ID - if len(rootCIDs) == 1 && len(shares[0].Shares) == 0 { - return nil, share.ErrNamespaceNotFound - } - - return shares, nil -} - -func verifyNIDSize(nID namespace.ID) error { - if len(nID) != share.NamespaceSize { - return fmt.Errorf("expected namespace ID of size %d, got %d", - share.NamespaceSize, len(nID)) - } - return nil -} - // ctxWithSplitTimeout will split timeout stored in context by splitFactor and return the result if // it is greater than minTimeout. minTimeout == 0 will be ignored, splitFactor <= 0 will be ignored func ctxWithSplitTimeout( diff --git a/share/getters/utils_test.go b/share/getters/utils_test.go index 73e9400010..65de9d47f2 100644 --- a/share/getters/utils_test.go +++ b/share/getters/utils_test.go @@ -1,11 +1,14 @@ package getters import ( + "context" "errors" "fmt" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func Test_ErrorContains(t *testing.T) { @@ -101,3 +104,122 @@ func Test_ErrorContains(t *testing.T) { }) } } + +func Test_ctxWithSplitTimeout(t *testing.T) { + type args struct { + ctxTimeout time.Duration + splitFactor []int + minTimeout time.Duration + } + tests := []struct { + name string + args args + want time.Duration + }{ + { + name: "ctxTimeout > minTimeout, splitFactor <= 0", + args: args{ + ctxTimeout: 3 * time.Minute, + splitFactor: []int{-1, 0}, + minTimeout: time.Minute, + }, + want: time.Minute, + }, + { + name: "ctxTimeout > minTimeout, splitFactor = 1", + args: args{ + ctxTimeout: 3 * time.Minute, + splitFactor: []int{1}, + minTimeout: time.Minute, + }, + want: 3 * time.Minute, + }, + { + name: "ctxTimeout > minTimeout, splitFactor = 2", + args: args{ + ctxTimeout: 3 * time.Minute, + splitFactor: []int{2}, + minTimeout: time.Minute, + }, + want: 3 * time.Minute / 2, + }, + { + name: "ctxTimeout > minTimeout, resulted timeout limited by minTimeout", + args: args{ + ctxTimeout: 3 * time.Minute, + splitFactor: []int{3, 4, 5}, + minTimeout: time.Minute, + }, + want: time.Minute, + }, + { + name: "ctxTimeout < minTimeout", + args: args{ + ctxTimeout: time.Minute, + splitFactor: []int{-1, 0, 1, 2, 3}, + minTimeout: 2 * time.Minute, + }, + want: time.Minute, + }, + { + name: "minTimeout = 0, splitFactor <= 1", + args: args{ + ctxTimeout: time.Minute, + splitFactor: []int{-1, 0, 1}, + minTimeout: 0, + }, + want: time.Minute, + }, + { + name: "minTimeout = 0, splitFactor > 1", + args: args{ + ctxTimeout: time.Minute, + splitFactor: []int{2}, + minTimeout: 0, + }, + want: time.Minute / 2, + }, + { + name: "no context timeout", + args: args{ + ctxTimeout: 0, + splitFactor: []int{-1, 0, 1, 2}, + minTimeout: time.Minute, + }, + want: time.Minute, + }, + { + name: "no context timeout, minTimeout = 0", + args: args{ + ctxTimeout: 0, + splitFactor: []int{-1, 0, 1, 2}, + minTimeout: 0, + }, + want: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, sf := range tt.args.splitFactor { + ctx, cancel := context.WithCancel(context.Background()) + // add timeout if original context should have it + if tt.args.ctxTimeout > 0 { + ctx, cancel = context.WithTimeout(ctx, tt.args.ctxTimeout) + } + t.Cleanup(cancel) + got, _ := ctxWithSplitTimeout(ctx, sf, tt.args.minTimeout) + dl, ok := got.Deadline() + // in case no deadline is found in ctx or not expected to be found, check both cases apply at the + // same time + if !ok || tt.want == 0 { + require.False(t, ok) + require.Equal(t, tt.want, time.Duration(0)) + continue + } + d := time.Until(dl) + require.True(t, d <= tt.want+time.Second) + require.True(t, d >= tt.want-time.Second) + } + }) + } +} diff --git a/share/add.go b/share/ipld/add.go similarity index 57% rename from share/add.go rename to share/ipld/add.go index 02016cadf6..fbb743148c 100644 --- a/share/add.go +++ b/share/ipld/add.go @@ -1,24 +1,24 @@ -package share +package ipld import ( "context" "fmt" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "github.com/celestiaorg/celestia-app/pkg/wrapper" "github.com/celestiaorg/nmt" "github.com/celestiaorg/rsmt2d" "github.com/celestiaorg/celestia-node/libs/utils" - "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share" ) // AddShares erasures and extends shares to blockservice.BlockService using the provided // ipld.NodeAdder. func AddShares( ctx context.Context, - shares []Share, + shares []share.Share, adder blockservice.BlockService, ) (*rsmt2d.ExtendedDataSquare, error) { if len(shares) == 0 { @@ -26,12 +26,12 @@ func AddShares( } squareSize := int(utils.SquareSize(len(shares))) // create nmt adder wrapping batch adder with calculated size - batchAdder := ipld.NewNmtNodeAdder(ctx, adder, ipld.MaxSizeBatchOption(squareSize*2)) + batchAdder := NewNmtNodeAdder(ctx, adder, MaxSizeBatchOption(squareSize*2)) // create the nmt wrapper to generate row and col commitments // recompute the eds eds, err := rsmt2d.ComputeExtendedDataSquare( shares, - DefaultRSMT2DCodec(), + share.DefaultRSMT2DCodec(), wrapper.NewConstructor(uint64(squareSize), nmt.NodeVisitor(batchAdder.Visit)), ) @@ -39,7 +39,10 @@ func AddShares( return nil, fmt.Errorf("failure to recompute the extended data square: %w", err) } // compute roots - eds.RowRoots() + _, err = eds.RowRoots() + if err != nil { + return nil, err + } // commit the batch to ipfs return eds, batchAdder.Commit() } @@ -49,17 +52,18 @@ func AddShares( func ImportShares( ctx context.Context, shares [][]byte, - adder blockservice.BlockService) (*rsmt2d.ExtendedDataSquare, error) { + adder blockservice.BlockService, +) (*rsmt2d.ExtendedDataSquare, error) { if len(shares) == 0 { return nil, fmt.Errorf("ipld: importing empty data") } squareSize := int(utils.SquareSize(len(shares))) // create nmt adder wrapping batch adder with calculated size - batchAdder := ipld.NewNmtNodeAdder(ctx, adder, ipld.MaxSizeBatchOption(squareSize*2)) + batchAdder := NewNmtNodeAdder(ctx, adder, MaxSizeBatchOption(squareSize*2)) // recompute the eds eds, err := rsmt2d.ImportExtendedDataSquare( shares, - DefaultRSMT2DCodec(), + share.DefaultRSMT2DCodec(), wrapper.NewConstructor(uint64(squareSize/2), nmt.NodeVisitor(batchAdder.Visit)), ) @@ -67,36 +71,16 @@ func ImportShares( return nil, fmt.Errorf("failure to recompute the extended data square: %w", err) } // compute roots - eds.RowRoots() + _, err = eds.RowRoots() + if err != nil { + return nil, err + } // commit the batch to DAG return eds, batchAdder.Commit() } -// ExtractODS returns the original shares of the given ExtendedDataSquare. This -// is a helper function for circumstances where AddShares must be used after the EDS has already -// been generated. -func ExtractODS(eds *rsmt2d.ExtendedDataSquare) []Share { - origWidth := eds.Width() / 2 - origShares := make([][]byte, origWidth*origWidth) - for i := uint(0); i < origWidth; i++ { - row := eds.Row(i) - for j := uint(0); j < origWidth; j++ { - origShares[(i*origWidth)+j] = row[j] - } - } - return origShares -} - -// ExtractEDS takes an EDS and extracts all shares from it in a flattened slice(row by row). -func ExtractEDS(eds *rsmt2d.ExtendedDataSquare) []Share { - flattenedEDSSize := eds.Width() * eds.Width() - out := make([][]byte, flattenedEDSSize) - count := 0 - for i := uint(0); i < eds.Width(); i++ { - for _, share := range eds.Row(i) { - out[count] = share - count++ - } - } - return out +func ImportEDS(ctx context.Context, square *rsmt2d.ExtendedDataSquare, adder blockservice.BlockService) error { + shares := square.Flattened() + _, err := ImportShares(ctx, shares, adder) + return err } diff --git a/share/ipld/blockserv.go b/share/ipld/blockserv.go new file mode 100644 index 0000000000..2ed2a21c77 --- /dev/null +++ b/share/ipld/blockserv.go @@ -0,0 +1,30 @@ +package ipld + +import ( + "github.com/ipfs/boxo/blockservice" + "github.com/ipfs/boxo/blockstore" + "github.com/ipfs/boxo/exchange" + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/sync" +) + +// NewBlockservice constructs Blockservice for fetching NMTrees. +func NewBlockservice(bs blockstore.Blockstore, exchange exchange.Interface) blockservice.BlockService { + return blockservice.New(bs, exchange, blockservice.WithAllowlist(defaultAllowlist)) +} + +// NewMemBlockservice constructs Blockservice for fetching NMTrees with in-memory blockstore. +func NewMemBlockservice() blockservice.BlockService { + bstore := blockstore.NewBlockstore(sync.MutexWrap(datastore.NewMapDatastore())) + return NewBlockservice(bstore, nil) +} + +// defaultAllowlist keeps default list of hashes allowed in the network. +var defaultAllowlist allowlist + +type allowlist struct{} + +func (a allowlist) IsAllowed(code uint64) bool { + // we allow all codes except home-baked sha256NamespaceFlagged + return code == sha256NamespaceFlagged +} diff --git a/share/ipld/corrupted_data_test.go b/share/ipld/corrupted_data_test.go index df1d0d7888..0d0af6dd35 100644 --- a/share/ipld/corrupted_data_test.go +++ b/share/ipld/corrupted_data_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/celestiaorg/celestia-node/header/headertest" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/availability/full" availability_test "github.com/celestiaorg/celestia-node/share/availability/test" @@ -24,24 +25,27 @@ func TestNamespaceHasher_CorruptedData(t *testing.T) { t.Cleanup(cancel) net := availability_test.NewTestDAGNet(ctx, t) - requestor := full.Node(net) + requester := full.Node(net) provider, mockBS := availability_test.MockNode(t, net) - provider.Availability = full.TestAvailability(getters.NewIPLDGetter(provider.BlockService)) + provider.Availability = full.TestAvailability(t, getters.NewIPLDGetter(provider.BlockService)) net.ConnectAll() // before the provider starts attacking, we should be able to retrieve successfully. We pass a size // 16 block, but this is not important to the test and any valid block size behaves the same. root := availability_test.RandFillBS(t, 16, provider.BlockService) + + eh := headertest.RandExtendedHeaderWithRoot(t, root) getCtx, cancelGet := context.WithTimeout(ctx, sharesAvailableTimeout) t.Cleanup(cancelGet) - err := requestor.SharesAvailable(getCtx, root) + err := requester.SharesAvailable(getCtx, eh) require.NoError(t, err) // clear the storage of the requester so that it must retrieve again, then start attacking - requestor.ClearStorage() + // we reinitialize the node to clear the eds store + requester = full.Node(net) mockBS.Attacking = true getCtx, cancelGet = context.WithTimeout(ctx, sharesAvailableTimeout) t.Cleanup(cancelGet) - err = requestor.SharesAvailable(getCtx, root) + err = requester.SharesAvailable(getCtx, eh) require.ErrorIs(t, err, share.ErrNotAvailable) } diff --git a/share/ipld/get.go b/share/ipld/get.go index 70385f73f7..adf2ffa8c5 100644 --- a/share/ipld/get.go +++ b/share/ipld/get.go @@ -7,11 +7,11 @@ import ( "sync/atomic" "github.com/gammazero/workerpool" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" + + "github.com/celestiaorg/celestia-node/share" ) // NumWorkersLimit sets global limit for workers spawned by GetShares. @@ -26,7 +26,7 @@ import ( // // TODO(@Wondertan): This assumes we have parallelized DASer implemented. Sync the values once it is shipped. // TODO(@Wondertan): Allow configuration of values without global state. -var NumWorkersLimit = MaxSquareSize * MaxSquareSize / 2 * NumConcurrentSquares +var NumWorkersLimit = share.MaxSquareSize * share.MaxSquareSize / 2 * NumConcurrentSquares // NumConcurrentSquares limits the amount of squares that are fetched // concurrently/simultaneously. @@ -98,12 +98,9 @@ func GetLeaves(ctx context.Context, maxShares int, put func(int, ipld.Node), ) { - ctx, span := tracer.Start(ctx, "get-leaves") - defer span.End() - // this buffer ensures writes to 'jobs' are never blocking (bin-tree-feat) jobs := make(chan *job, (maxShares+1)/2) // +1 for the case where 'maxShares' is 1 - jobs <- &job{id: root, ctx: ctx} + jobs <- &job{cid: root, ctx: ctx} // total is an amount of routines spawned and total amount of nodes we process (bin-tree-feat) // so we can specify exact amount of loops we do, and wait for this amount // of routines to finish processing @@ -118,21 +115,12 @@ func GetLeaves(ctx context.Context, // work over each job concurrently, s.t. shares do not block // processing of each other pool.Submit(func() { - ctx, span := tracer.Start(j.ctx, "process-job") - defer span.End() defer wg.Done() - span.SetAttributes( - attribute.String("cid", j.id.String()), - attribute.Int("pos", j.sharePos), - ) - - nd, err := GetNode(ctx, bGetter, j.id) + nd, err := GetNode(ctx, bGetter, j.cid) if err != nil { // we don't really care about errors here // just fetch as much as possible - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) return } // check links to know what we should do with the node @@ -140,7 +128,6 @@ func GetLeaves(ctx context.Context, if len(lnks) == 0 { // successfully fetched a share/leaf // ladies and gentlemen, we got em! - span.SetStatus(codes.Ok, "") put(j.sharePos, nd) return } @@ -149,7 +136,7 @@ func GetLeaves(ctx context.Context, // send those to be processed select { case jobs <- &job{ - id: lnk.Cid, + cid: lnk.Cid, // calc position for children nodes (bin-tree-feat), // s.t. 'if' above knows where to put a share sharePos: j.sharePos*2 + i, @@ -213,7 +200,7 @@ func GetProof( // chanGroup implements an atomic wait group, closing a jobs chan // when fully done. type chanGroup struct { - jobs chan *job + jobs chan job counter int64 } @@ -233,8 +220,29 @@ func (w *chanGroup) done() { // job represents an encountered node to investigate during the `GetLeaves` // and `CollectLeavesByNamespace` routines. type job struct { - id cid.Cid + // we pass the context to job so that spans are tracked in a tree + // structure + ctx context.Context + // cid of the node that will be handled + cid cid.Cid + // sharePos represents potential share position in share slice sharePos int - depth int - ctx context.Context + // depth represents the number of edges present in path from the root node of a tree to that node + depth int + // isAbsent indicates if target namespaceID is not included, only collect absence proofs + isAbsent bool +} + +func (j job) next(direction direction, cid cid.Cid, isAbsent bool) job { + var i int + if direction == right { + i++ + } + return job{ + ctx: j.ctx, + cid: cid, + sharePos: j.sharePos*2 + i, + depth: j.depth + 1, + isAbsent: isAbsent, + } } diff --git a/share/get.go b/share/ipld/get_shares.go similarity index 61% rename from share/get.go rename to share/ipld/get_shares.go index 9ec4c3c2c5..98db7012b5 100644 --- a/share/get.go +++ b/share/ipld/get_shares.go @@ -1,16 +1,15 @@ -package share +package ipld import ( "context" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "github.com/ipfs/go-cid" format "github.com/ipfs/go-ipld-format" "github.com/celestiaorg/nmt" - "github.com/celestiaorg/nmt/namespace" - "github.com/celestiaorg/celestia-node/share/ipld" + "github.com/celestiaorg/celestia-node/share" ) // GetShare fetches and returns the data for leaf `leafIndex` of root `rootCid`. @@ -20,8 +19,8 @@ func GetShare( rootCid cid.Cid, leafIndex int, totalLeafs int, // this corresponds to the extended square width -) (Share, error) { - nd, err := ipld.GetLeaf(ctx, bGetter, rootCid, leafIndex, totalLeafs) +) (share.Share, error) { + nd, err := GetLeaf(ctx, bGetter, rootCid, leafIndex, totalLeafs) if err != nil { return nil, err } @@ -32,30 +31,24 @@ func GetShare( // GetShares walks the tree of a given root and puts shares into the given 'put' func. // Does not return any error, and returns/unblocks only on success // (got all shares) or on context cancellation. -func GetShares(ctx context.Context, bGetter blockservice.BlockGetter, root cid.Cid, shares int, put func(int, Share)) { - ctx, span := tracer.Start(ctx, "get-shares") - defer span.End() - +func GetShares(ctx context.Context, bg blockservice.BlockGetter, root cid.Cid, shares int, put func(int, share.Share)) { putNode := func(i int, leaf format.Node) { put(i, leafToShare(leaf)) } - ipld.GetLeaves(ctx, bGetter, root, shares, putNode) + GetLeaves(ctx, bg, root, shares, putNode) } // GetSharesByNamespace walks the tree of a given root and returns its shares within the given -// namespace.ID. If a share could not be retrieved, err is not nil, and the returned array +// Namespace. If a share could not be retrieved, err is not nil, and the returned array // contains nil shares in place of the shares it was unable to retrieve. func GetSharesByNamespace( ctx context.Context, bGetter blockservice.BlockGetter, root cid.Cid, - nID namespace.ID, + namespace share.Namespace, maxShares int, -) ([]Share, *nmt.Proof, error) { - ctx, span := tracer.Start(ctx, "get-shares-by-namespace") - defer span.End() - - data := ipld.NewNamespaceData(maxShares, nID, ipld.WithLeaves(), ipld.WithProofs()) +) ([]share.Share, *nmt.Proof, error) { + data := NewNamespaceData(maxShares, namespace, WithLeaves(), WithProofs()) err := data.CollectLeavesByNamespace(ctx, bGetter, root) if err != nil { return nil, nil, err @@ -63,7 +56,7 @@ func GetSharesByNamespace( leaves := data.Leaves() - shares := make([]Share, len(leaves)) + shares := make([]share.Share, len(leaves)) for i, leaf := range leaves { if leaf != nil { shares[i] = leafToShare(leaf) @@ -73,8 +66,8 @@ func GetSharesByNamespace( } // leafToShare converts an NMT leaf into a Share. -func leafToShare(nd format.Node) Share { +func leafToShare(nd format.Node) share.Share { // * Additional namespace is prepended so that parity data can be identified with a parity // namespace, which we cut off - return nd.RawData()[NamespaceSize:] + return share.GetData(nd.RawData()) } diff --git a/share/ipld/get_shares_test.go b/share/ipld/get_shares_test.go new file mode 100644 index 0000000000..580efcb69b --- /dev/null +++ b/share/ipld/get_shares_test.go @@ -0,0 +1,504 @@ +package ipld + +import ( + "bytes" + "context" + "crypto/sha256" + "errors" + mrand "math/rand" + "sort" + "strconv" + "testing" + "time" + + "github.com/ipfs/boxo/blockservice" + "github.com/ipfs/go-cid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-app/pkg/wrapper" + "github.com/celestiaorg/rsmt2d" + + "github.com/celestiaorg/celestia-node/libs/utils" + "github.com/celestiaorg/celestia-node/share" + "github.com/celestiaorg/celestia-node/share/eds/edstest" + "github.com/celestiaorg/celestia-node/share/sharetest" +) + +func TestGetShare(t *testing.T) { + const size = 8 + + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + bServ := NewMemBlockservice() + + // generate random shares for the nmt + shares := sharetest.RandShares(t, size*size) + eds, err := AddShares(ctx, shares, bServ) + require.NoError(t, err) + + for i, leaf := range shares { + row := i / size + pos := i - (size * row) + rowRoots, err := eds.RowRoots() + require.NoError(t, err) + share, err := GetShare(ctx, bServ, MustCidFromNamespacedSha256(rowRoots[row]), pos, size*2) + require.NoError(t, err) + assert.Equal(t, leaf, share) + } +} + +func TestBlockRecovery(t *testing.T) { + originalSquareWidth := 8 + shareCount := originalSquareWidth * originalSquareWidth + extendedSquareWidth := 2 * originalSquareWidth + extendedShareCount := extendedSquareWidth * extendedSquareWidth + + // generate test data + quarterShares := sharetest.RandShares(t, shareCount) + allShares := sharetest.RandShares(t, shareCount) + + testCases := []struct { + name string + shares []share.Share + expectErr bool + errString string + d int // number of shares to delete + }{ + {"missing 1/2 shares", quarterShares, false, "", extendedShareCount / 2}, + {"missing 1/4 shares", quarterShares, false, "", extendedShareCount / 4}, + {"max missing data", quarterShares, false, "", (originalSquareWidth + 1) * (originalSquareWidth + 1)}, + {"missing all but one shares", allShares, true, "failed to solve data square", extendedShareCount - 1}, + } + for _, tc := range testCases { + tc := tc + + t.Run(tc.name, func(t *testing.T) { + squareSize := utils.SquareSize(len(tc.shares)) + + testEds, err := rsmt2d.ComputeExtendedDataSquare( + tc.shares, + share.DefaultRSMT2DCodec(), + wrapper.NewConstructor(squareSize), + ) + require.NoError(t, err) + + // calculate roots using the first complete square + rowRoots, err := testEds.RowRoots() + require.NoError(t, err) + colRoots, err := testEds.ColRoots() + require.NoError(t, err) + + flat := testEds.Flattened() + + // recover a partially complete square + rdata := removeRandShares(flat, tc.d) + testEds, err = rsmt2d.ImportExtendedDataSquare( + rdata, + share.DefaultRSMT2DCodec(), + wrapper.NewConstructor(squareSize), + ) + require.NoError(t, err) + + err = testEds.Repair(rowRoots, colRoots) + if tc.expectErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errString) + return + } + assert.NoError(t, err) + + reds, err := rsmt2d.ImportExtendedDataSquare(rdata, share.DefaultRSMT2DCodec(), wrapper.NewConstructor(squareSize)) + require.NoError(t, err) + // check that the squares are equal + assert.Equal(t, testEds.Flattened(), reds.Flattened()) + }) + } +} + +func Test_ConvertEDStoShares(t *testing.T) { + squareWidth := 16 + shares := sharetest.RandShares(t, squareWidth*squareWidth) + + // compute extended square + testEds, err := rsmt2d.ComputeExtendedDataSquare( + shares, + share.DefaultRSMT2DCodec(), + wrapper.NewConstructor(uint64(squareWidth)), + ) + require.NoError(t, err) + + resshares := testEds.FlattenedODS() + require.Equal(t, shares, resshares) +} + +// removes d shares from data +func removeRandShares(data [][]byte, d int) [][]byte { + count := len(data) + // remove shares randomly + for i := 0; i < d; { + ind := mrand.Intn(count) + if len(data[ind]) == 0 { + continue + } + data[ind] = nil + i++ + } + return data +} + +func TestGetSharesByNamespace(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + bServ := NewMemBlockservice() + + var tests = []struct { + rawData []share.Share + }{ + {rawData: sharetest.RandShares(t, 4)}, + {rawData: sharetest.RandShares(t, 16)}, + } + + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + // choose random namespace from rand shares + expected := tt.rawData[len(tt.rawData)/2] + namespace := share.GetNamespace(expected) + + // change rawData to contain several shares with same namespace + tt.rawData[(len(tt.rawData)/2)+1] = expected + // put raw data in BlockService + eds, err := AddShares(ctx, tt.rawData, bServ) + require.NoError(t, err) + + var shares []share.Share + rowRoots, err := eds.RowRoots() + require.NoError(t, err) + for _, row := range rowRoots { + rcid := MustCidFromNamespacedSha256(row) + rowShares, _, err := GetSharesByNamespace(ctx, bServ, rcid, namespace, len(rowRoots)) + if errors.Is(err, ErrNamespaceOutsideRange) { + continue + } + require.NoError(t, err) + + shares = append(shares, rowShares...) + } + + assert.Equal(t, 2, len(shares)) + for _, share := range shares { + assert.Equal(t, expected, share) + } + }) + } +} + +func TestCollectLeavesByNamespace_IncompleteData(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + bServ := NewMemBlockservice() + + shares := sharetest.RandShares(t, 16) + + // set all shares to the same namespace id + namespace := share.GetNamespace(shares[0]) + for _, shr := range shares { + copy(share.GetNamespace(shr), namespace) + } + + eds, err := AddShares(ctx, shares, bServ) + require.NoError(t, err) + + roots, err := eds.RowRoots() + require.NoError(t, err) + + // remove the second share from the first row + rcid := MustCidFromNamespacedSha256(roots[0]) + node, err := GetNode(ctx, bServ, rcid) + require.NoError(t, err) + + // Left side of the tree contains the original shares + data, err := GetNode(ctx, bServ, node.Links()[0].Cid) + require.NoError(t, err) + + // Second share is the left side's right child + l, err := GetNode(ctx, bServ, data.Links()[0].Cid) + require.NoError(t, err) + r, err := GetNode(ctx, bServ, l.Links()[1].Cid) + require.NoError(t, err) + err = bServ.DeleteBlock(ctx, r.Cid()) + require.NoError(t, err) + + namespaceData := NewNamespaceData(len(shares), namespace, WithLeaves()) + err = namespaceData.CollectLeavesByNamespace(ctx, bServ, rcid) + require.Error(t, err) + leaves := namespaceData.Leaves() + assert.Nil(t, leaves[1]) + assert.Equal(t, 4, len(leaves)) +} + +func TestCollectLeavesByNamespace_AbsentNamespaceId(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + bServ := NewMemBlockservice() + + shares := sharetest.RandShares(t, 1024) + + // set all shares to the same namespace + namespaces, err := randomNamespaces(5) + require.NoError(t, err) + minNamespace := namespaces[0] + minIncluded := namespaces[1] + midNamespace := namespaces[2] + maxIncluded := namespaces[3] + maxNamespace := namespaces[4] + + secondNamespaceFrom := mrand.Intn(len(shares)-2) + 1 + for i, shr := range shares { + if i < secondNamespaceFrom { + copy(share.GetNamespace(shr), minIncluded) + continue + } + copy(share.GetNamespace(shr), maxIncluded) + } + + var tests = []struct { + name string + data []share.Share + missingNamespace share.Namespace + isAbsence bool + }{ + {name: "Namespace less than the minimum namespace in data", data: shares, missingNamespace: minNamespace}, + {name: "Namespace greater than the maximum namespace in data", data: shares, missingNamespace: maxNamespace}, + {name: "Namespace in range but still missing", data: shares, missingNamespace: midNamespace, isAbsence: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eds, err := AddShares(ctx, shares, bServ) + require.NoError(t, err) + assertNoRowContainsNID(ctx, t, bServ, eds, tt.missingNamespace, tt.isAbsence) + }) + } +} + +func TestCollectLeavesByNamespace_MultipleRowsContainingSameNamespaceId(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + bServ := NewMemBlockservice() + + shares := sharetest.RandShares(t, 16) + + // set all shares to the same namespace and data but the last one + namespace := share.GetNamespace(shares[0]) + commonNamespaceData := shares[0] + + for i, nspace := range shares { + if i == len(shares)-1 { + break + } + + copy(nspace, commonNamespaceData) + } + + eds, err := AddShares(ctx, shares, bServ) + require.NoError(t, err) + + rowRoots, err := eds.RowRoots() + require.NoError(t, err) + + for _, row := range rowRoots { + rcid := MustCidFromNamespacedSha256(row) + data := NewNamespaceData(len(shares), namespace, WithLeaves()) + err := data.CollectLeavesByNamespace(ctx, bServ, rcid) + if errors.Is(err, ErrNamespaceOutsideRange) { + continue + } + assert.Nil(t, err) + leaves := data.Leaves() + for _, node := range leaves { + // test that the data returned by collectLeavesByNamespace for nid + // matches the commonNamespaceData that was copied across almost all data + assert.Equal(t, commonNamespaceData, share.GetData(node.RawData())) + } + } +} + +func TestGetSharesWithProofsByNamespace(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + t.Cleanup(cancel) + bServ := NewMemBlockservice() + + var tests = []struct { + rawData []share.Share + }{ + {rawData: sharetest.RandShares(t, 4)}, + {rawData: sharetest.RandShares(t, 16)}, + {rawData: sharetest.RandShares(t, 64)}, + } + + for i, tt := range tests { + t.Run(strconv.Itoa(i), func(t *testing.T) { + rand := mrand.New(mrand.NewSource(time.Now().UnixNano())) + // choose random range in shares slice + from := rand.Intn(len(tt.rawData)) + to := rand.Intn(len(tt.rawData)) + + if to < from { + from, to = to, from + } + + expected := tt.rawData[from] + namespace := share.GetNamespace(expected) + + // change rawData to contain several shares with same namespace + for i := from; i <= to; i++ { + tt.rawData[i] = expected + } + + // put raw data in BlockService + eds, err := AddShares(ctx, tt.rawData, bServ) + require.NoError(t, err) + + var shares []share.Share + rowRoots, err := eds.RowRoots() + require.NoError(t, err) + for _, row := range rowRoots { + rcid := MustCidFromNamespacedSha256(row) + rowShares, proof, err := GetSharesByNamespace(ctx, bServ, rcid, namespace, len(rowRoots)) + if namespace.IsOutsideRange(row, row) { + require.ErrorIs(t, err, ErrNamespaceOutsideRange) + continue + } + require.NoError(t, err) + if len(rowShares) > 0 { + require.NotNil(t, proof) + // append shares to check integrity later + shares = append(shares, rowShares...) + + // construct nodes from shares by prepending namespace + var leaves [][]byte + for _, shr := range rowShares { + leaves = append(leaves, append(share.GetNamespace(shr), shr...)) + } + + // verify namespace + verified := proof.VerifyNamespace( + sha256.New(), + namespace.ToNMT(), + leaves, + NamespacedSha256FromCID(rcid)) + require.True(t, verified) + + // verify inclusion + verified = proof.VerifyInclusion( + sha256.New(), + namespace.ToNMT(), + rowShares, + NamespacedSha256FromCID(rcid)) + require.True(t, verified) + } + } + + // validate shares + assert.Equal(t, to-from+1, len(shares)) + for _, share := range shares { + assert.Equal(t, expected, share) + } + }) + } +} + +func TestBatchSize(t *testing.T) { + tests := []struct { + name string + origWidth int + }{ + {"2", 2}, + {"4", 4}, + {"8", 8}, + {"16", 16}, + {"32", 32}, + {"64", 64}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*time.Duration(tt.origWidth)) + defer cancel() + + bs := NewMemBlockservice() + + randEds := edstest.RandEDS(t, tt.origWidth) + _, err := AddShares(ctx, randEds.FlattenedODS(), bs) + require.NoError(t, err) + + out, err := bs.Blockstore().AllKeysChan(ctx) + require.NoError(t, err) + + var count int + for range out { + count++ + } + extendedWidth := tt.origWidth * 2 + assert.Equalf(t, count, BatchSize(extendedWidth), "batchSize(%v)", extendedWidth) + }) + } +} + +func assertNoRowContainsNID( + ctx context.Context, + t *testing.T, + bServ blockservice.BlockService, + eds *rsmt2d.ExtendedDataSquare, + namespace share.Namespace, + isAbsent bool, +) { + rowRoots, err := eds.RowRoots() + require.NoError(t, err) + rowRootCount := len(rowRoots) + // get all row root cids + rowRootCIDs := make([]cid.Cid, rowRootCount) + for i, rowRoot := range rowRoots { + rowRootCIDs[i] = MustCidFromNamespacedSha256(rowRoot) + } + + // for each row root cid check if the min namespace exists + var absentCount, foundAbsenceRows int + for _, rowRoot := range rowRoots { + var outsideRange bool + if !namespace.IsOutsideRange(rowRoot, rowRoot) { + // namespace does belong to namespace range of the row + absentCount++ + } else { + outsideRange = true + } + data := NewNamespaceData(rowRootCount, namespace, WithProofs()) + rootCID := MustCidFromNamespacedSha256(rowRoot) + err := data.CollectLeavesByNamespace(ctx, bServ, rootCID) + if outsideRange { + require.ErrorIs(t, err, ErrNamespaceOutsideRange) + continue + } + require.NoError(t, err) + + // if no error returned, check absence proof + foundAbsenceRows++ + verified := data.Proof().VerifyNamespace(sha256.New(), namespace.ToNMT(), nil, rowRoot) + require.True(t, verified) + } + + if isAbsent { + require.Equal(t, foundAbsenceRows, absentCount) + // there should be max 1 row that has namespace range containing namespace + require.LessOrEqual(t, absentCount, 1) + } +} + +func randomNamespaces(total int) ([]share.Namespace, error) { + namespaces := make([]share.Namespace, total) + for i := range namespaces { + namespaces[i] = sharetest.RandV0Namespace() + } + sort.Slice(namespaces, func(i, j int) bool { return bytes.Compare(namespaces[i], namespaces[j]) < 0 }) + return namespaces, nil +} diff --git a/share/ipld/namespace_data.go b/share/ipld/namespace_data.go index 9698ef990d..5a6fd2abb4 100644 --- a/share/ipld/namespace_data.go +++ b/share/ipld/namespace_data.go @@ -7,16 +7,18 @@ import ( "sync" "sync/atomic" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/codes" "github.com/celestiaorg/nmt" - "github.com/celestiaorg/nmt/namespace" + + "github.com/celestiaorg/celestia-node/share" ) +var ErrNamespaceOutsideRange = errors.New("share/ipld: " + + "target namespace is outside of namespace range for the given root") + // Option is the functional option that is applied to the NamespaceData instance // to configure data that needs to be stored. type Option func(*NamespaceData) @@ -39,21 +41,25 @@ func WithProofs() Option { // NamespaceData stores all leaves under the given namespace with their corresponding proofs. type NamespaceData struct { - leaves []ipld.Node - proofs *proofCollector + leaves []ipld.Node + proofs *proofCollector + bounds fetchedBounds maxShares int - nID namespace.ID + namespace share.Namespace + + isAbsentNamespace atomic.Bool + absenceProofLeaf ipld.Node } -func NewNamespaceData(maxShares int, nID namespace.ID, options ...Option) *NamespaceData { +func NewNamespaceData(maxShares int, namespace share.Namespace, options ...Option) *NamespaceData { data := &NamespaceData{ // we don't know where in the tree the leaves in the namespace are, // so we keep track of the bounds to return the correct slice // maxShares acts as a sentinel to know if we find any leaves bounds: fetchedBounds{int64(maxShares), 0}, maxShares: maxShares, - nID: nID, + namespace: namespace, } for _, opt := range options { @@ -62,14 +68,19 @@ func NewNamespaceData(maxShares int, nID namespace.ID, options ...Option) *Names return data } -func (n *NamespaceData) validate() error { - if len(n.nID) != NamespaceSize { - return fmt.Errorf("expected namespace ID of size %d, got %d", NamespaceSize, len(n.nID)) +func (n *NamespaceData) validate(rootCid cid.Cid) error { + if err := n.namespace.Validate(); err != nil { + return err } if n.leaves == nil && n.proofs == nil { return errors.New("share/ipld: empty NamespaceData, nothing specified to retrieve") } + + root := NamespacedSha256FromCID(rootCid) + if n.namespace.IsOutsideRange(root, root) { + return ErrNamespaceOutsideRange + } return nil } @@ -77,6 +88,14 @@ func (n *NamespaceData) addLeaf(pos int, nd ipld.Node) { // bounds will be needed in `Proof` method n.bounds.update(int64(pos)) + if n.isAbsentNamespace.Load() { + if n.absenceProofLeaf != nil { + log.Fatal("there should be only one absence leaf") + } + n.absenceProofLeaf = nd + return + } + if n.leaves == nil { return } @@ -116,7 +135,7 @@ func (n *NamespaceData) addProof(d direction, cid cid.Cid, depth int) { // Leaves returns retrieved leaves within the bounds in case `WithLeaves` option was passed, // otherwise nil will be returned. func (n *NamespaceData) Leaves() []ipld.Node { - if n.leaves == nil || n.noLeaves() { + if n.leaves == nil || n.noLeaves() || n.isAbsentNamespace.Load() { return nil } return n.leaves[n.bounds.lowest : n.bounds.highest+1] @@ -139,6 +158,16 @@ func (n *NamespaceData) Proof() *nmt.Proof { nodes[i] = NamespacedSha256FromCID(node) } + if n.isAbsentNamespace.Load() { + proof := nmt.NewAbsenceProof( + int(n.bounds.lowest), + int(n.bounds.highest)+1, + nodes, + NamespacedSha256FromCID(n.absenceProofLeaf.Cid()), + NMTIgnoreMaxNamespace, + ) + return &proof + } proof := nmt.NewInclusionProof( int(n.bounds.lowest), int(n.bounds.highest)+1, @@ -149,7 +178,7 @@ func (n *NamespaceData) Proof() *nmt.Proof { } // CollectLeavesByNamespace collects leaves and corresponding proof that could be used to verify -// leaves inclusion. It returns as many leaves from the given root with the given namespace.ID as +// leaves inclusion. It returns as many leaves from the given root with the given Namespace as // it can retrieve. If no shares are found, it returns error as nil. A // non-nil error means that only partial data is returned, because at least one share retrieval // failed. The following implementation is based on `GetShares`. @@ -158,22 +187,14 @@ func (n *NamespaceData) CollectLeavesByNamespace( bGetter blockservice.BlockGetter, root cid.Cid, ) error { - if err := n.validate(); err != nil { + if err := n.validate(root); err != nil { return err } - ctx, span := tracer.Start(ctx, "get-leaves-by-namespace") - defer span.End() - - span.SetAttributes( - attribute.String("namespace", n.nID.String()), - attribute.String("root", root.String()), - ) - // buffer the jobs to avoid blocking, we only need as many // queued as the number of shares in the second-to-last layer - jobs := make(chan *job, (n.maxShares+1)/2) - jobs <- &job{id: root, ctx: ctx} + jobs := make(chan job, (n.maxShares+1)/2) + jobs <- job{cid: root, ctx: ctx} var wg chanGroup wg.jobs = jobs @@ -185,7 +206,7 @@ func (n *NamespaceData) CollectLeavesByNamespace( ) for { - var j *job + var j job var ok bool select { case j, ok = <-jobs: @@ -197,28 +218,20 @@ func (n *NamespaceData) CollectLeavesByNamespace( return retrievalErr } pool.Submit(func() { - ctx, span := tracer.Start(j.ctx, "process-job") - defer span.End() defer wg.done() - span.SetAttributes( - attribute.String("cid", j.id.String()), - attribute.Int("pos", j.sharePos), - ) - // if an error is likely to be returned or not depends on // the underlying impl of the blockservice, currently it is not a realistic probability - nd, err := GetNode(ctx, bGetter, j.id) + nd, err := GetNode(ctx, bGetter, j.cid) if err != nil { singleErr.Do(func() { retrievalErr = err }) - log.Errorw("getLeavesWithProofsByNamespace:could not retrieve node", - "nID", n.nID, + log.Errorw("could not retrieve IPLD node", + "namespace", n.namespace.String(), "pos", j.sharePos, "err", err, ) - span.SetStatus(codes.Error, err.Error()) // we still need to update the bounds n.addLeaf(j.sharePos, nil) return @@ -227,45 +240,17 @@ func (n *NamespaceData) CollectLeavesByNamespace( links := nd.Links() if len(links) == 0 { // successfully fetched a leaf belonging to the namespace - span.SetStatus(codes.Ok, "") // we found a leaf, so we update the bounds n.addLeaf(j.sharePos, nd) return } // this node has links in the namespace, so keep walking - for i, lnk := range links { - newJob := &job{ - id: lnk.Cid, - // sharePos represents potential share position in share slice - sharePos: j.sharePos*2 + i, - // depth represents the number of edges present in path from the root node of a tree to that node - depth: j.depth + 1, - // we pass the context to job so that spans are tracked in a tree - // structure - ctx: ctx, - } - // if the link's nID isn't in range we don't need to create a new job for it, - // but need to collect a proof - jobNid := NamespacedSha256FromCID(newJob.id) - - // proof is on the right side, if the nID is less than min namespace of jobNid - if n.nID.Less(nmt.MinNamespace(jobNid, n.nID.Size())) { - n.addProof(right, lnk.Cid, newJob.depth) - continue - } - - // proof is on the left side, if the nID is bigger than max namespace of jobNid - if !n.nID.LessOrEqual(nmt.MaxNamespace(jobNid, n.nID.Size())) { - n.addProof(left, lnk.Cid, newJob.depth) - continue - } - - // by passing the previous check, we know we will have one more node to process - // note: it is important to increase the counter before sending to the channel + newJobs := n.traverseLinks(j, links) + for _, j := range newJobs { wg.add(1) select { - case jobs <- newJob: + case jobs <- j: case <-ctx.Done(): return } @@ -274,6 +259,57 @@ func (n *NamespaceData) CollectLeavesByNamespace( } } +func (n *NamespaceData) traverseLinks(j job, links []*ipld.Link) []job { + if j.isAbsent { + return n.collectAbsenceProofs(j, links) + } + return n.collectNDWithProofs(j, links) +} + +func (n *NamespaceData) collectAbsenceProofs(j job, links []*ipld.Link) []job { + leftLink := links[0].Cid + rightLink := links[1].Cid + // traverse to the left node, while collecting right node as proof + n.addProof(right, rightLink, j.depth) + return []job{j.next(left, leftLink, j.isAbsent)} +} + +func (n *NamespaceData) collectNDWithProofs(j job, links []*ipld.Link) []job { + leftCid := links[0].Cid + rightCid := links[1].Cid + leftLink := NamespacedSha256FromCID(leftCid) + rightLink := NamespacedSha256FromCID(rightCid) + + var nextJobs []job + // check if target namespace is outside of boundaries of both links + if n.namespace.IsOutsideRange(leftLink, rightLink) { + log.Fatalf("target namespace outside of boundaries of links at depth: %v", j.depth) + } + + if !n.namespace.IsAboveMax(leftLink) { + // namespace is within the range of left link + nextJobs = append(nextJobs, j.next(left, leftCid, false)) + } else { + // proof is on the left side, if the namespace is on the right side of the range of left link + n.addProof(left, leftCid, j.depth) + if n.namespace.IsBelowMin(rightLink) { + // namespace is not included in either links, convert to absence collector + n.isAbsentNamespace.Store(true) + nextJobs = append(nextJobs, j.next(right, rightCid, true)) + return nextJobs + } + } + + if !n.namespace.IsBelowMin(rightLink) { + // namespace is within the range of right link + nextJobs = append(nextJobs, j.next(right, rightCid, false)) + } else { + // proof is on the right side, if the namespace is on the left side of the range of right link + n.addProof(right, rightCid, j.depth) + } + return nextJobs +} + type fetchedBounds struct { lowest int64 highest int64 diff --git a/share/ipld/nmt.go b/share/ipld/nmt.go index 91c413e05b..6dba300965 100644 --- a/share/ipld/nmt.go +++ b/share/ipld/nmt.go @@ -8,23 +8,23 @@ import ( "hash" "math/rand" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" + blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" - blocks "github.com/ipfs/go-libipfs/blocks" logging "github.com/ipfs/go-log/v2" mh "github.com/multiformats/go-multihash" mhcore "github.com/multiformats/go-multihash/core" - "go.opentelemetry.io/otel" "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/pkg/da" "github.com/celestiaorg/nmt" + + "github.com/celestiaorg/celestia-node/share" ) var ( - tracer = otel.Tracer("ipld") - log = logging.Logger("ipld") + log = logging.Logger("ipld") ) const ( @@ -34,25 +34,18 @@ const ( // nmtCodec is the codec used for leaf and inner nodes of a Namespaced Merkle Tree. nmtCodec = 0x7700 - // sha256Namespace8Flagged is the multihash code used to hash blocks + // sha256NamespaceFlagged is the multihash code used to hash blocks // that contain an NMT node (inner and leaf nodes). - sha256Namespace8Flagged = 0x7701 - - // MaxSquareSize is currently the maximum size supported for unerasured data in - // rsmt2d.ExtendedDataSquare. - MaxSquareSize = appconsts.DefaultMaxSquareSize - - // NamespaceSize is a system-wide size for NMT namespaces. - NamespaceSize = appconsts.NamespaceSize + sha256NamespaceFlagged = 0x7701 // NmtHashSize is the size of a digest created by an NMT in bytes. - NmtHashSize = 2*NamespaceSize + sha256.Size + NmtHashSize = 2*share.NamespaceSize + sha256.Size // innerNodeSize is the size of data in inner nodes. innerNodeSize = NmtHashSize * 2 // leafNodeSize is the size of data in leaf nodes. - leafNodeSize = NamespaceSize + appconsts.ShareSize + leafNodeSize = share.NamespaceSize + appconsts.ShareSize // cidPrefixSize is the size of the prepended buffer of the CID encoding // for NamespacedSha256. For more information, see: @@ -60,15 +53,15 @@ const ( cidPrefixSize = 4 // NMTIgnoreMaxNamespace is currently used value for IgnoreMaxNamespace option in NMT. - // IgnoreMaxNamespace defines whether the largest possible namespace.ID MAX_NID should be 'ignored'. + // IgnoreMaxNamespace defines whether the largest possible Namespace MAX_NID should be 'ignored'. // If set to true, this allows for shorter proofs in particular use-cases. NMTIgnoreMaxNamespace = true ) func init() { // required for Bitswap to hash and verify inbound data correctly - mhcore.Register(sha256Namespace8Flagged, func() hash.Hash { - nh := nmt.NewNmtHasher(sha256.New(), NamespaceSize, true) + mhcore.Register(sha256NamespaceFlagged, func() hash.Hash { + nh := nmt.NewNmtHasher(sha256.New(), share.NamespaceSize, true) nh.Reset() return nh }) @@ -144,7 +137,7 @@ func CidFromNamespacedSha256(namespacedHash []byte) (cid.Cid, error) { if got, want := len(namespacedHash), NmtHashSize; got != want { return cid.Cid{}, fmt.Errorf("invalid namespaced hash length, got: %v, want: %v", got, want) } - buf, err := mh.Encode(namespacedHash, sha256Namespace8Flagged) + buf, err := mh.Encode(namespacedHash, sha256NamespaceFlagged) if err != nil { return cid.Undef, err } @@ -159,7 +152,7 @@ func MustCidFromNamespacedSha256(hash []byte) cid.Cid { panic( fmt.Sprintf("malformed hash: %s, codec: %v", err, - mh.Codes[sha256Namespace8Flagged]), + mh.Codes[sha256NamespaceFlagged]), ) } return cidFromHash @@ -172,7 +165,7 @@ func Translate(dah *da.DataAvailabilityHeader, row, col int) (cid.Cid, int) { return MustCidFromNamespacedSha256(dah.ColumnRoots[col]), row } - return MustCidFromNamespacedSha256(dah.RowsRoots[row]), col + return MustCidFromNamespacedSha256(dah.RowRoots[row]), col } // NamespacedSha256FromCID derives the Namespaced hash from the given CID. diff --git a/share/ipld/nmt_adder.go b/share/ipld/nmt_adder.go index 386a0083a3..7ce52859b2 100644 --- a/share/ipld/nmt_adder.go +++ b/share/ipld/nmt_adder.go @@ -5,10 +5,18 @@ import ( "fmt" "sync" - "github.com/ipfs/go-blockservice" + "github.com/ipfs/boxo/blockservice" + "github.com/ipfs/boxo/ipld/merkledag" "github.com/ipfs/go-cid" ipld "github.com/ipfs/go-ipld-format" - "github.com/ipfs/go-merkledag" + + "github.com/celestiaorg/nmt" +) + +type ctxKey int + +const ( + proofsAdderKey ctxKey = iota ) // NmtNodeAdder adds ipld.Nodes to the underlying ipld.Batch if it is inserted @@ -55,26 +63,6 @@ func (n *NmtNodeAdder) Visit(hash []byte, children ...[]byte) { } } -// VisitInnerNodes is a NodeVisitor that does not store leaf nodes to the blockservice. -func (n *NmtNodeAdder) VisitInnerNodes(hash []byte, children ...[]byte) { - n.lock.Lock() - defer n.lock.Unlock() - - if n.err != nil { - return // protect from further visits if there is an error - } - - id := MustCidFromNamespacedSha256(hash) - switch len(children) { - case 1: - break - case 2: - n.err = n.add.Add(n.ctx, newNMTNode(id, append(children[0], children[1]...))) - default: - panic("expected a binary tree") - } -} - // Commit checks for errors happened during Visit and if absent commits data to inner Batch. func (n *NmtNodeAdder) Commit() error { n.lock.Lock() @@ -112,3 +100,96 @@ func BatchSize(squareSize int) int { // and for the last two layers as well: return (squareSize*2-1)*squareSize*2 - (squareSize * squareSize) } + +// ProofsAdder is used to collect proof nodes, while traversing merkle tree +type ProofsAdder struct { + lock sync.RWMutex + proofs map[cid.Cid][]byte +} + +// NewProofsAdder creates new instance of ProofsAdder. +func NewProofsAdder(squareSize int) *ProofsAdder { + return &ProofsAdder{ + // preallocate map to fit all inner nodes for given square size + proofs: make(map[cid.Cid][]byte, innerNodesAmount(squareSize)), + } +} + +// CtxWithProofsAdder creates context, that will contain ProofsAdder. If context is leaked to +// another go-routine, proofs will be not collected by gc. To prevent it, use Purge after Proofs +// are collected from adder, to preemptively release memory allocated for proofs. +func CtxWithProofsAdder(ctx context.Context, adder *ProofsAdder) context.Context { + return context.WithValue(ctx, proofsAdderKey, adder) +} + +// ProofsAdderFromCtx extracts ProofsAdder from context +func ProofsAdderFromCtx(ctx context.Context) *ProofsAdder { + val := ctx.Value(proofsAdderKey) + adder, ok := val.(*ProofsAdder) + if !ok || adder == nil { + return nil + } + return adder +} + +// Proofs returns proofs collected by ProofsAdder +func (a *ProofsAdder) Proofs() map[cid.Cid][]byte { + if a == nil { + return nil + } + + a.lock.RLock() + defer a.lock.RUnlock() + return a.proofs +} + +// VisitFn returns NodeVisitorFn, that will collect proof nodes while traversing merkle tree. +func (a *ProofsAdder) VisitFn() nmt.NodeVisitorFn { + if a == nil { + return nil + } + + a.lock.RLock() + defer a.lock.RUnlock() + + // proofs are already collected, don't collect second time + if len(a.proofs) > 0 { + return nil + } + return a.visitInnerNodes +} + +// Purge removed proofs from ProofsAdder allowing GC to collect the memory +func (a *ProofsAdder) Purge() { + if a == nil { + return + } + + a.lock.Lock() + defer a.lock.Unlock() + + a.proofs = nil +} + +func (a *ProofsAdder) visitInnerNodes(hash []byte, children ...[]byte) { + switch len(children) { + case 1: + break + case 2: + id := MustCidFromNamespacedSha256(hash) + a.addProof(id, append(children[0], children[1]...)) + default: + panic("expected a binary tree") + } +} + +func (a *ProofsAdder) addProof(id cid.Cid, proof []byte) { + a.lock.Lock() + defer a.lock.Unlock() + a.proofs[id] = proof +} + +// innerNodesAmount return amount of inner nodes in eds with given size +func innerNodesAmount(squareSize int) int { + return 2 * (squareSize - 1) * squareSize +} diff --git a/share/ipld/nmt_test.go b/share/ipld/nmt_test.go index cd2f22cb5b..77268d7112 100644 --- a/share/ipld/nmt_test.go +++ b/share/ipld/nmt_test.go @@ -1,40 +1,35 @@ package ipld import ( - "bytes" - "crypto/rand" - "sort" "strconv" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/celestiaorg/celestia-app/pkg/appconsts" "github.com/celestiaorg/celestia-app/pkg/da" + "github.com/celestiaorg/rsmt2d" - "github.com/celestiaorg/celestia-node/libs/utils" + "github.com/celestiaorg/celestia-node/share/eds/edstest" ) // TestNamespaceFromCID checks that deriving the Namespaced hash from // the given CID works correctly. func TestNamespaceFromCID(t *testing.T) { var tests = []struct { - randData [][]byte + eds *rsmt2d.ExtendedDataSquare }{ - {randData: generateRandNamespacedRawData(4, appconsts.NamespaceSize, appconsts.ShareSize-appconsts.NamespaceSize)}, - {randData: generateRandNamespacedRawData(16, appconsts.NamespaceSize, appconsts.ShareSize-appconsts.NamespaceSize)}, + // note that the number of shares must be a power of two + {eds: edstest.RandEDS(t, 4)}, + {eds: edstest.RandEDS(t, 16)}, } for i, tt := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { - // create DAH from rand data - squareSize := utils.SquareSize(len(tt.randData)) - eds, err := da.ExtendShares(squareSize, tt.randData) + dah, err := da.NewDataAvailabilityHeader(tt.eds) require.NoError(t, err) - dah := da.NewDataAvailabilityHeader(eds) // check to make sure NamespacedHash is correctly derived from CID - for _, row := range dah.RowsRoots { + for _, row := range dah.RowRoots { c, err := CidFromNamespacedSha256(row) require.NoError(t, err) @@ -44,27 +39,3 @@ func TestNamespaceFromCID(t *testing.T) { }) } } - -// generateRandNamespacedRawData returns random namespaced raw data for testing purposes. -func generateRandNamespacedRawData(total, nidSize, leafSize uint32) [][]byte { - data := make([][]byte, total) - for i := uint32(0); i < total; i++ { - nid := make([]byte, nidSize) - - _, _ = rand.Read(nid) - data[i] = nid - } - sortByteArrays(data) - for i := uint32(0); i < total; i++ { - d := make([]byte, leafSize) - - _, _ = rand.Read(d) - data[i] = append(data[i], d...) - } - - return data -} - -func sortByteArrays(src [][]byte) { - sort.Slice(src, func(i, j int) bool { return bytes.Compare(src[i], src[j]) < 0 }) -} diff --git a/share/ipld/utils.go b/share/ipld/utils.go new file mode 100644 index 0000000000..d3e987e7f3 --- /dev/null +++ b/share/ipld/utils.go @@ -0,0 +1,18 @@ +package ipld + +import ( + "github.com/ipfs/go-cid" + + "github.com/celestiaorg/celestia-node/share" +) + +// FilterRootByNamespace returns the row roots from the given share.Root that contain the namespace. +func FilterRootByNamespace(root *share.Root, namespace share.Namespace) []cid.Cid { + rowRootCIDs := make([]cid.Cid, 0, len(root.RowRoots)) + for _, row := range root.RowRoots { + if !namespace.IsOutsideRange(row, row) { + rowRootCIDs = append(rowRootCIDs, MustCidFromNamespacedSha256(row)) + } + } + return rowRootCIDs +} diff --git a/share/mocks/getter.go b/share/mocks/getter.go index 12c36cb015..738e2b246c 100644 --- a/share/mocks/getter.go +++ b/share/mocks/getter.go @@ -8,12 +8,10 @@ import ( context "context" reflect "reflect" - gomock "github.com/golang/mock/gomock" - - da "github.com/celestiaorg/celestia-app/pkg/da" + header "github.com/celestiaorg/celestia-node/header" share "github.com/celestiaorg/celestia-node/share" - namespace "github.com/celestiaorg/nmt/namespace" rsmt2d "github.com/celestiaorg/rsmt2d" + gomock "github.com/golang/mock/gomock" ) // MockGetter is a mock of Getter interface. @@ -40,7 +38,7 @@ func (m *MockGetter) EXPECT() *MockGetterMockRecorder { } // GetEDS mocks base method. -func (m *MockGetter) GetEDS(arg0 context.Context, arg1 *da.DataAvailabilityHeader) (*rsmt2d.ExtendedDataSquare, error) { +func (m *MockGetter) GetEDS(arg0 context.Context, arg1 *header.ExtendedHeader) (*rsmt2d.ExtendedDataSquare, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetEDS", arg0, arg1) ret0, _ := ret[0].(*rsmt2d.ExtendedDataSquare) @@ -55,7 +53,7 @@ func (mr *MockGetterMockRecorder) GetEDS(arg0, arg1 interface{}) *gomock.Call { } // GetShare mocks base method. -func (m *MockGetter) GetShare(arg0 context.Context, arg1 *da.DataAvailabilityHeader, arg2, arg3 int) ([]byte, error) { +func (m *MockGetter) GetShare(arg0 context.Context, arg1 *header.ExtendedHeader, arg2, arg3 int) ([]byte, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetShare", arg0, arg1, arg2, arg3) ret0, _ := ret[0].([]byte) @@ -70,7 +68,7 @@ func (mr *MockGetterMockRecorder) GetShare(arg0, arg1, arg2, arg3 interface{}) * } // GetSharesByNamespace mocks base method. -func (m *MockGetter) GetSharesByNamespace(arg0 context.Context, arg1 *da.DataAvailabilityHeader, arg2 namespace.ID) (share.NamespacedShares, error) { +func (m *MockGetter) GetSharesByNamespace(arg0 context.Context, arg1 *header.ExtendedHeader, arg2 share.Namespace) (share.NamespacedShares, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetSharesByNamespace", arg0, arg1, arg2) ret0, _ := ret[0].(share.NamespacedShares) diff --git a/share/namespace.go b/share/namespace.go new file mode 100644 index 0000000000..df4ad74058 --- /dev/null +++ b/share/namespace.go @@ -0,0 +1,184 @@ +package share + +import ( + "bytes" + "encoding/hex" + "fmt" + + appns "github.com/celestiaorg/celestia-app/pkg/namespace" + "github.com/celestiaorg/nmt/namespace" +) + +// NamespaceSize is a system-wide size for NMT namespaces. +const NamespaceSize = appns.NamespaceSize + +// Various reserved namespaces. +var ( + // MaxPrimaryReservedNamespace is the highest primary reserved namespace. + // Namespaces lower than this are reserved for protocol use. + MaxPrimaryReservedNamespace = Namespace(appns.MaxPrimaryReservedNamespace.Bytes()) + // MinSecondaryReservedNamespace is the lowest secondary reserved namespace + // reserved for protocol use. Namespaces higher than this are reserved for + // protocol use. + MinSecondaryReservedNamespace = Namespace(appns.MinSecondaryReservedNamespace.Bytes()) + ParitySharesNamespace = Namespace(appns.ParitySharesNamespace.Bytes()) + TailPaddingNamespace = Namespace(appns.TailPaddingNamespace.Bytes()) + PrimaryReservedPaddingNamespace = Namespace(appns.PrimaryReservedPaddingNamespace.Bytes()) + TxNamespace = Namespace(appns.TxNamespace.Bytes()) + PayForBlobNamespace = Namespace(appns.PayForBlobNamespace.Bytes()) + ISRNamespace = Namespace(appns.IntermediateStateRootsNamespace.Bytes()) +) + +// Namespace represents namespace of a Share. +// Consists of version byte and namespace ID. +type Namespace []byte + +// NewBlobNamespaceV0 takes a variable size byte slice and creates a valid version 0 Blob Namespace. +// The byte slice must be <= 10 bytes. +// If it is less than 10 bytes, it will be left padded to size 10 with 0s. +// Use predefined namespaces above, if non-blob namespace is needed. +func NewBlobNamespaceV0(id []byte) (Namespace, error) { + if len(id) == 0 || len(id) > appns.NamespaceVersionZeroIDSize { + return nil, fmt.Errorf( + "namespace id must be > 0 && <= %d, but it was %d bytes", appns.NamespaceVersionZeroIDSize, len(id)) + } + + n := make(Namespace, NamespaceSize) + // version and zero padding are already set as zero, + // so simply copying subNID to the end is enough to comply the V0 spec + copy(n[len(n)-len(id):], id) + return n, n.ValidateForBlob() +} + +// NamespaceFromBytes converts bytes into Namespace and validates it. +func NamespaceFromBytes(b []byte) (Namespace, error) { + n := Namespace(b) + return n, n.Validate() +} + +// Version reports version of the Namespace. +func (n Namespace) Version() byte { + return n[appns.NamespaceVersionSize-1] +} + +// ID reports ID of the Namespace. +func (n Namespace) ID() namespace.ID { + return namespace.ID(n[appns.NamespaceVersionSize:]) +} + +// ToNMT converts the whole Namespace(both Version and ID parts) into NMT's namespace.ID +// NOTE: Once https://github.com/celestiaorg/nmt/issues/206 is closed Namespace should become NNT's +// type. +func (n Namespace) ToNMT() namespace.ID { + return namespace.ID(n) +} + +// ToAppNamespace converts the Namespace to App's definition of Namespace. +// TODO: Unify types between node and app +func (n Namespace) ToAppNamespace() appns.Namespace { + return appns.Namespace{Version: n.Version(), ID: n.ID()} +} + +// Len reports the total length of the namespace. +func (n Namespace) Len() int { + return len(n) +} + +// String stringifies the Namespace. +func (n Namespace) String() string { + return hex.EncodeToString(n) +} + +// Equals compares two Namespaces. +func (n Namespace) Equals(target Namespace) bool { + return bytes.Equal(n, target) +} + +// Validate checks if the namespace is correct. +func (n Namespace) Validate() error { + if n.Len() != NamespaceSize { + return fmt.Errorf("invalid namespace length: expected %d, got %d", NamespaceSize, n.Len()) + } + if n.Version() != appns.NamespaceVersionZero && n.Version() != appns.NamespaceVersionMax { + return fmt.Errorf("invalid namespace version %v", n.Version()) + } + if len(n.ID()) != appns.NamespaceIDSize { + return fmt.Errorf("invalid namespace id length: expected %d, got %d", appns.NamespaceIDSize, n.ID().Size()) + } + if n.Version() == appns.NamespaceVersionZero && !bytes.HasPrefix(n.ID(), appns.NamespaceVersionZeroPrefix) { + return fmt.Errorf("invalid namespace id: expect %d leading zeroes", len(appns.NamespaceVersionZeroPrefix)) + } + return nil +} + +// ValidateForData checks if the Namespace is of real/useful data. +func (n Namespace) ValidateForData() error { + if err := n.Validate(); err != nil { + return err + } + if n.Equals(ParitySharesNamespace) || n.Equals(TailPaddingNamespace) { + return fmt.Errorf("invalid data namespace(%s): parity and tail padding namespace are forbidden", n) + } + if n.Version() != appns.NamespaceVersionZero { + return fmt.Errorf("invalid data namespace(%s): only version 0 is supported", n) + } + return nil +} + +// ValidateForBlob checks if the Namespace is valid blob namespace. +func (n Namespace) ValidateForBlob() error { + if err := n.ValidateForData(); err != nil { + return err + } + if bytes.Compare(n, MaxPrimaryReservedNamespace) < 1 { + return fmt.Errorf("invalid blob namespace(%s): reserved namespaces are forbidden", n) + } + if bytes.Compare(n, MinSecondaryReservedNamespace) > -1 { + return fmt.Errorf("invalid blob namespace(%s): reserved namespaces are forbidden", n) + } + return nil +} + +// IsAboveMax checks if the namespace is above the maximum namespace of the given hash. +func (n Namespace) IsAboveMax(nodeHash []byte) bool { + return !n.IsLessOrEqual(nodeHash[n.Len() : n.Len()*2]) +} + +// IsBelowMin checks if the target namespace is below the minimum namespace of the given hash. +func (n Namespace) IsBelowMin(nodeHash []byte) bool { + return n.IsLess(nodeHash[:n.Len()]) +} + +// IsOutsideRange checks if the namespace is outside the min-max range of the given hashes. +func (n Namespace) IsOutsideRange(leftNodeHash, rightNodeHash []byte) bool { + return n.IsBelowMin(leftNodeHash) || n.IsAboveMax(rightNodeHash) +} + +// Repeat copies the Namespace t times. +func (n Namespace) Repeat(t int) []Namespace { + ns := make([]Namespace, t) + for i := 0; i < t; i++ { + ns[i] = n + } + return ns +} + +// IsLess reports if the Namespace is less than the target. +func (n Namespace) IsLess(target Namespace) bool { + return bytes.Compare(n, target) == -1 +} + +// IsLessOrEqual reports if the Namespace is less than the target. +func (n Namespace) IsLessOrEqual(target Namespace) bool { + return bytes.Compare(n, target) < 1 +} + +// IsGreater reports if the Namespace is greater than the target. +func (n Namespace) IsGreater(target Namespace) bool { + return bytes.Compare(n, target) == 1 +} + +// IsGreaterOrEqualThan reports if the Namespace is greater or equal than the target. +func (n Namespace) IsGreaterOrEqualThan(target Namespace) bool { + return bytes.Compare(n, target) > -1 +} diff --git a/share/namespace_test.go b/share/namespace_test.go new file mode 100644 index 0000000000..786441b043 --- /dev/null +++ b/share/namespace_test.go @@ -0,0 +1,216 @@ +package share + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + appns "github.com/celestiaorg/celestia-app/pkg/namespace" +) + +var ( + validID = append( + appns.NamespaceVersionZeroPrefix, + bytes.Repeat([]byte{1}, appns.NamespaceVersionZeroIDSize)..., + ) + tooShortID = append(appns.NamespaceVersionZeroPrefix, []byte{1}...) + tooLongID = append(appns.NamespaceVersionZeroPrefix, bytes.Repeat([]byte{1}, NamespaceSize)...) + invalidPrefixID = bytes.Repeat([]byte{1}, NamespaceSize) +) + +func TestNewNamespaceV0(t *testing.T) { + type testCase struct { + name string + subNid []byte + expected Namespace + wantErr bool + } + testCases := []testCase{ + { + name: "8 byte id, gets left padded", + subNid: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, + expected: Namespace{ + 0x0, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // filled zeros + 0x0, 0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8}, // id with left padding + wantErr: false, + }, + { + name: "10 byte id, no padding", + subNid: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x9, 0x10}, + expected: Namespace{ + 0x0, // version + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // filled zeros + 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0x10}, // id + wantErr: false, + }, + { + name: "11 byte id", + subNid: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x9, 0x10, 0x11}, + expected: []byte{}, + wantErr: true, + }, + { + name: "nil id", + subNid: nil, + expected: []byte{}, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := NewBlobNamespaceV0(tc.subNid) + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.expected, got) + }) + } +} + +func TestFrom(t *testing.T) { + type testCase struct { + name string + bytes []byte + wantErr bool + want Namespace + } + validNamespace := []byte{} + validNamespace = append(validNamespace, appns.NamespaceVersionZero) + validNamespace = append(validNamespace, appns.NamespaceVersionZeroPrefix...) + validNamespace = append(validNamespace, bytes.Repeat([]byte{0x1}, appns.NamespaceVersionZeroIDSize)...) + parityNamespace := bytes.Repeat([]byte{0xFF}, NamespaceSize) + + testCases := []testCase{ + { + name: "valid namespace", + bytes: validNamespace, + wantErr: false, + want: append([]byte{appns.NamespaceVersionZero}, validID...), + }, + { + name: "parity namespace", + bytes: parityNamespace, + wantErr: false, + want: append([]byte{appns.NamespaceVersionMax}, bytes.Repeat([]byte{0xFF}, appns.NamespaceIDSize)...), + }, + { + name: "unsupported version", + bytes: append([]byte{1}, append( + appns.NamespaceVersionZeroPrefix, + bytes.Repeat([]byte{1}, NamespaceSize-len(appns.NamespaceVersionZeroPrefix))..., + )...), + wantErr: true, + }, + { + name: "unsupported id: too short", + bytes: append([]byte{appns.NamespaceVersionZero}, tooShortID...), + wantErr: true, + }, + { + name: "unsupported id: too long", + bytes: append([]byte{appns.NamespaceVersionZero}, tooLongID...), + wantErr: true, + }, + { + name: "unsupported id: invalid prefix", + bytes: append([]byte{appns.NamespaceVersionZero}, invalidPrefixID...), + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, err := NamespaceFromBytes(tc.bytes) + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.Equal(t, tc.want, got) + }) + } +} + +func TestValidateForBlob(t *testing.T) { + type testCase struct { + name string + ns Namespace + wantErr bool + } + + validNamespace, err := NewBlobNamespaceV0(bytes.Repeat([]byte{0x1}, appns.NamespaceVersionZeroIDSize)) + require.NoError(t, err) + + testCases := []testCase{ + { + name: "valid blob namespace", + ns: validNamespace, + wantErr: false, + }, + { + name: "invalid blob namespace: parity shares namespace", + ns: ParitySharesNamespace, + wantErr: true, + }, + { + name: "invalid blob namespace: tail padding namespace", + ns: TailPaddingNamespace, + wantErr: true, + }, + { + name: "invalid blob namespace: tx namespace", + ns: TxNamespace, + wantErr: true, + }, + { + name: "invalid blob namespace: namespace version max", + ns: append([]byte{appns.NamespaceVersionMax}, bytes.Repeat([]byte{0x0}, appns.NamespaceIDSize)...), + wantErr: true, + }, + { + name: "invalid blob namespace: primary reserved namespace", + ns: primaryReservedNamespace(0x10), + wantErr: true, + }, + { + name: "invalid blob namespace: secondary reserved namespace", + ns: secondaryReservedNamespace(0x10), + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + err := tc.ns.ValidateForBlob() + + if tc.wantErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + }) + } +} + +func primaryReservedNamespace(lastByte byte) Namespace { + result := make([]byte, NamespaceSize) + result = append(result, appns.NamespaceVersionZero) + result = append(result, appns.NamespaceVersionZeroPrefix...) + result = append(result, bytes.Repeat([]byte{0x0}, appns.NamespaceVersionZeroIDSize-1)...) + result = append(result, lastByte) + return result +} + +func secondaryReservedNamespace(lastByte byte) Namespace { + result := make([]byte, NamespaceSize) + result = append(result, appns.NamespaceVersionMax) + result = append(result, bytes.Repeat([]byte{0xFF}, appns.NamespaceIDSize-1)...) + result = append(result, lastByte) + return result +} diff --git a/share/p2p/discovery/backoff.go b/share/p2p/discovery/backoff.go index 70581bc519..7294915727 100644 --- a/share/p2p/discovery/backoff.go +++ b/share/p2p/discovery/backoff.go @@ -91,6 +91,8 @@ func (b *backoffConnector) HasBackoff(p peer.ID) bool { // GC is a perpetual GCing loop. func (b *backoffConnector) GC(ctx context.Context) { ticker := time.NewTicker(gcInterval) + defer ticker.Stop() + for { select { case <-ctx.Done(): diff --git a/share/p2p/discovery/discovery.go b/share/p2p/discovery/discovery.go index 23489c3147..0f44d42dbe 100644 --- a/share/p2p/discovery/discovery.go +++ b/share/p2p/discovery/discovery.go @@ -19,29 +19,31 @@ import ( var log = logging.Logger("share/discovery") const ( - // rendezvousPoint is the namespace where peers advertise and discover each other. - rendezvousPoint = "full" - // eventbusBufSize is the size of the buffered channel to handle // events in libp2p. We specify a larger buffer size for the channel // to avoid overflowing and blocking subscription during disconnection bursts. // (by default it is 16) eventbusBufSize = 64 - // findPeersStuckWarnDelay is the duration after which discover will log an error message to - // notify that it is stuck. - findPeersStuckWarnDelay = time.Minute + // findPeersTimeout limits the FindPeers operation in time + findPeersTimeout = time.Minute + + // retryTimeout defines time interval between discovery and advertise attempts. + retryTimeout = time.Second - // defaultRetryTimeout defines time interval between discovery attempts. - defaultRetryTimeout = time.Second + // logInterval defines the time interval at which a warning message will be logged + // if the desired number of nodes is not detected. + logInterval = 5 * time.Minute ) -// defaultRetryTimeout defines time interval between discovery attempts. -var discoveryRetryTimeout = defaultRetryTimeout +// discoveryRetryTimeout defines time interval between discovery attempts, needed for tests +var discoveryRetryTimeout = retryTimeout // Discovery combines advertise and discover services and allows to store discovered nodes. // TODO: The code here gets horribly hairy, so we should refactor this at some point type Discovery struct { + // Tag is used as rondezvous point for discovery service + tag string set *limitedSet host host.Host disc discovery.Discovery @@ -55,43 +57,50 @@ type Discovery struct { cancel context.CancelFunc - params Parameters + params *Parameters } type OnUpdatedPeers func(peerID peer.ID, isAdded bool) +func (f OnUpdatedPeers) add(next OnUpdatedPeers) OnUpdatedPeers { + return func(peerID peer.ID, isAdded bool) { + f(peerID, isAdded) + next(peerID, isAdded) + } +} + // NewDiscovery constructs a new discovery. func NewDiscovery( + params *Parameters, h host.Host, d discovery.Discovery, + tag string, opts ...Option, -) *Discovery { - params := DefaultParameters() - - for _, opt := range opts { - opt(¶ms) +) (*Discovery, error) { + if err := params.Validate(); err != nil { + return nil, err } + if tag == "" { + return nil, fmt.Errorf("discovery: tag cannot be empty") + } + o := newOptions(opts...) return &Discovery{ + tag: tag, set: newLimitedSet(params.PeersLimit), host: h, disc: d, connector: newBackoffConnector(h, defaultBackoffFactory), - onUpdatedPeers: func(peer.ID, bool) {}, + onUpdatedPeers: o.onUpdatedPeers, params: params, triggerDisc: make(chan struct{}), - } + }, nil } func (d *Discovery) Start(context.Context) error { ctx, cancel := context.WithCancel(context.Background()) d.cancel = cancel - if d.params.PeersLimit == 0 { - log.Warn("peers limit is set to 0. Skipping discovery...") - return nil - } - sub, err := d.host.EventBus().Subscribe(&event.EvtPeerConnectednessChanged{}, eventbus.BufSize(eventbusBufSize)) if err != nil { return fmt.Errorf("subscribing for connection events: %w", err) @@ -108,15 +117,6 @@ func (d *Discovery) Stop(context.Context) error { return nil } -// WithOnPeersUpdate chains OnPeersUpdate callbacks on every update of discovered peers list. -func (d *Discovery) WithOnPeersUpdate(f OnUpdatedPeers) { - prev := d.onUpdatedPeers - d.onUpdatedPeers = func(peerID peer.ID, isAdded bool) { - prev(peerID, isAdded) - f(peerID, isAdded) - } -} - // Peers provides a list of discovered peers in the "full" topic. // If Discovery hasn't found any peers, it blocks until at least one peer is found. func (d *Discovery) Peers(ctx context.Context) ([]peer.ID, error) { @@ -130,11 +130,11 @@ func (d *Discovery) Discard(id peer.ID) bool { return false } - d.host.ConnManager().Unprotect(id, rendezvousPoint) + d.host.ConnManager().Unprotect(id, d.tag) d.connector.Backoff(id) d.set.Remove(id) d.onUpdatedPeers(id, false) - log.Debugw("removed peer from the peer set", "peer", id) + log.Debugw("removed peer from the peer set", "peer", id.String()) if d.set.Size() < d.set.Limit() { // trigger discovery @@ -150,23 +150,20 @@ func (d *Discovery) Discard(id peer.ID) bool { // Advertise is a utility function that persistently advertises a service through an Advertiser. // TODO: Start advertising only after the reachability is confirmed by AutoNAT func (d *Discovery) Advertise(ctx context.Context) { - if d.params.AdvertiseInterval == -1 { - log.Warn("AdvertiseInterval is set to -1. Skipping advertising...") - return - } - timer := time.NewTimer(d.params.AdvertiseInterval) defer timer.Stop() for { - _, err := d.disc.Advertise(ctx, rendezvousPoint) + _, err := d.disc.Advertise(ctx, d.tag) d.metrics.observeAdvertise(ctx, err) if err != nil { if ctx.Err() != nil { return } - log.Warnw("error advertising", "rendezvous", rendezvousPoint, "err", err) + log.Warnw("error advertising", "rendezvous", d.tag, "err", err) - errTimer := time.NewTimer(time.Minute) + // we don't want retry indefinitely in busy loop + // internal discovery mechanism may need some time before attempts + errTimer := time.NewTimer(retryTimeout) select { case <-errTimer.C: errTimer.Stop() @@ -194,26 +191,34 @@ func (d *Discovery) Advertise(ctx context.Context) { } // discoveryLoop ensures we always have '~peerLimit' connected peers. -// It starts peer discovery per request and restarts the process until the soft limit reached. +// It initiates peer discovery upon request and restarts the process until the soft limit is +// reached. func (d *Discovery) discoveryLoop(ctx context.Context) { t := time.NewTicker(discoveryRetryTimeout) defer t.Stop() + + warnTicker := time.NewTicker(logInterval) + defer warnTicker.Stop() + for { - // drain all previous ticks from channel + // drain all previous ticks from the channel drainChannel(t.C) select { case <-t.C: - found := d.discover(ctx) - if !found { - // rerun discovery if amount of peers didn't reach the limit + if !d.discover(ctx) { + // rerun discovery if the number of peers hasn't reached the limit continue } - case <-ctx.Done(): - return - } - - select { - case <-d.triggerDisc: + case <-warnTicker.C: + if d.set.Size() < d.set.Limit() { + log.Warnf( + "Potentially degraded connectivity, unable to discover the desired amount of full node peers in %v. "+ + "Number of peers discovered: %d. Required: %d.", + logInterval, d.set.Size(), d.set.Limit(), + ) + } + // Do not break the loop; just continue + continue case <-ctx.Done(): return } @@ -250,47 +255,34 @@ func (d *Discovery) discover(ctx context.Context) bool { log.Debugw("reached soft peer limit, skipping discovery", "size", size) return true } - log.Infow("discovering peers", "want", want) + // TODO @renaynay: eventually, have a mechanism to catch if wanted amount of peers + // has not been discovered in X amount of time so that users are warned of degraded + // FN connectivity. + log.Debugw("discovering peers", "want", want) // we use errgroup as it provide limits var wg errgroup.Group // limit to minimize chances of overreaching the limit wg.SetLimit(int(d.set.Limit())) - // stop discovery when we are done - findCtx, findCancel := context.WithCancel(ctx) + findCtx, findCancel := context.WithTimeout(ctx, findPeersTimeout) defer func() { // some workers could still be running, wait them to finish before canceling findCtx wg.Wait() //nolint:errcheck findCancel() }() - peers, err := d.disc.FindPeers(findCtx, rendezvousPoint) + peers, err := d.disc.FindPeers(findCtx, d.tag) if err != nil { log.Error("unable to start discovery", "err", err) return false } - ticker := time.NewTicker(findPeersStuckWarnDelay) - defer ticker.Stop() for { - ticker.Reset(findPeersStuckWarnDelay) - // drain all previous ticks from channel - drainChannel(ticker.C) select { - case <-findCtx.Done(): - d.metrics.observeFindPeers(ctx, true, true) - return true - case <-ticker.C: - d.metrics.observeDiscoveryStuck(ctx) - log.Warn("wasn't able to find new peers for long time") - continue case p, ok := <-peers: if !ok { - isEnoughPeers := d.set.Size() >= d.set.Limit() - d.metrics.observeFindPeers(ctx, ctx.Err() != nil, isEnoughPeers) - log.Debugw("discovery channel closed", "find_is_canceled", findCtx.Err() != nil) - return isEnoughPeers + break } peer := p @@ -307,32 +299,36 @@ func (d *Discovery) discover(ctx context.Context) bool { } size := d.set.Size() - log.Debugw("found peer", "peer", peer.ID, "found_amount", size) + log.Debugw("found peer", "peer", peer.ID.String(), "found_amount", size) if size < d.set.Limit() { return nil } log.Infow("discovered wanted peers", "amount", size) - findCancel() + findCancel() // stop discovery when we are done return nil }) + + continue + case <-findCtx.Done(): } + + isEnoughPeers := d.set.Size() >= d.set.Limit() + d.metrics.observeFindPeers(ctx, isEnoughPeers) + log.Debugw("discovery finished", "discovered_wanted", isEnoughPeers) + return isEnoughPeers } } // handleDiscoveredPeer adds peer to the internal if can connect or is connected. // Report whether it succeeded. func (d *Discovery) handleDiscoveredPeer(ctx context.Context, peer peer.AddrInfo) bool { - logger := log.With("peer", peer.ID) + logger := log.With("peer", peer.ID.String()) switch { case peer.ID == d.host.ID(): d.metrics.observeHandlePeer(ctx, handlePeerSkipSelf) logger.Debug("skip handle: self discovery") return false - case len(peer.Addrs) == 0: - d.metrics.observeHandlePeer(ctx, handlePeerEmptyAddrs) - logger.Debug("skip handle: empty address list") - return false case d.set.Size() >= d.set.Limit(): d.metrics.observeHandlePeer(ctx, handlePeerEnoughPeers) logger.Debug("skip handle: enough peers found") @@ -367,11 +363,11 @@ func (d *Discovery) handleDiscoveredPeer(ctx context.Context, peer peer.AddrInfo d.metrics.observeHandlePeer(ctx, handlePeerConnected) logger.Debug("added peer to set") - // tag to protect peer from being killed by ConnManager + // Tag to protect peer from being killed by ConnManager // NOTE: This is does not protect from remote killing the connection. // In the future, we should design a protocol that keeps bidirectional agreement on whether // connection should be kept or not, similar to mesh link in GossipSub. - d.host.ConnManager().Protect(peer.ID, rendezvousPoint) + d.host.ConnManager().Protect(peer.ID, d.tag) return true } diff --git a/share/p2p/discovery/discovery_test.go b/share/p2p/discovery/discovery_test.go index f0935086ef..1d0078196f 100644 --- a/share/p2p/discovery/discovery_test.go +++ b/share/p2p/discovery/discovery_test.go @@ -1,3 +1,5 @@ +// go:build !race + package discovery import ( @@ -11,38 +13,53 @@ import ( "github.com/libp2p/go-libp2p/core/peer" "github.com/libp2p/go-libp2p/p2p/discovery/routing" basic "github.com/libp2p/go-libp2p/p2p/host/basic" + "github.com/libp2p/go-libp2p/p2p/host/eventbus" swarmt "github.com/libp2p/go-libp2p/p2p/net/swarm/testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const ( + fullNodesTag = "full" +) + func TestDiscovery(t *testing.T) { const nodes = 10 // higher number brings higher coverage discoveryRetryTimeout = time.Millisecond * 100 // defined in discovery.go - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) t.Cleanup(cancel) tn := newTestnet(ctx, t) - peerA := tn.discovery( - WithPeersLimit(nodes), - WithAdvertiseInterval(-1), - ) - type peerUpdate struct { peerID peer.ID isAdded bool } updateCh := make(chan peerUpdate) - peerA.WithOnPeersUpdate(func(peerID peer.ID, isAdded bool) { + submit := func(peerID peer.ID, isAdded bool) { updateCh <- peerUpdate{peerID: peerID, isAdded: isAdded} - }) + } + + host, routingDisc := tn.peer() + params := DefaultParameters() + params.PeersLimit = nodes + // start discovery listener service for peerA + peerA := tn.startNewDiscovery(params, host, routingDisc, fullNodesTag, + WithOnPeersUpdate(submit), + ) + + // start discovery advertisement services for other peers + params.AdvertiseInterval = time.Millisecond * 100 discs := make([]*Discovery, nodes) for i := range discs { - discs[i] = tn.discovery(WithPeersLimit(0), WithAdvertiseInterval(time.Millisecond*100)) + host, routingDisc := tn.peer() + disc, err := NewDiscovery(params, host, routingDisc, fullNodesTag) + require.NoError(t, err) + go disc.Advertise(tn.ctx) + discs[i] = tn.startNewDiscovery(params, host, routingDisc, fullNodesTag) select { case res := <-updateCh: @@ -55,6 +72,7 @@ func TestDiscovery(t *testing.T) { assert.EqualValues(t, nodes, peerA.set.Size()) + // disconnect peerA from all peers and check that notifications are received on updateCh channel for _, disc := range discs { peerID := disc.host.ID() err := peerA.host.Network().ClosePeer(peerID) @@ -72,6 +90,51 @@ func TestDiscovery(t *testing.T) { assert.EqualValues(t, 0, peerA.set.Size()) } +func TestDiscoveryTagged(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + t.Cleanup(cancel) + + tn := newTestnet(ctx, t) + + // launch 2 peers, that advertise with different tags + adv1, routingDisc1 := tn.peer() + adv2, routingDisc2 := tn.peer() + + // sub will discover both peers, but on different tags + sub, routingDisc := tn.peer() + + params := DefaultParameters() + + // create 2 discovery services for sub, each with a different tag + done1 := make(chan struct{}) + tn.startNewDiscovery(params, sub, routingDisc, "tag1", + WithOnPeersUpdate(checkPeer(t, adv1.ID(), done1))) + + done2 := make(chan struct{}) + tn.startNewDiscovery(params, sub, routingDisc, "tag2", + WithOnPeersUpdate(checkPeer(t, adv2.ID(), done2))) + + // run discovery services for advertisers + ds1 := tn.startNewDiscovery(params, adv1, routingDisc1, "tag1") + go ds1.Advertise(tn.ctx) + + ds2 := tn.startNewDiscovery(params, adv2, routingDisc2, "tag2") + go ds2.Advertise(tn.ctx) + + // wait for discovery services to discover each other on different tags + select { + case <-done1: + case <-ctx.Done(): + t.Fatal("did not discover peer in time") + } + + select { + case <-done2: + case <-ctx.Done(): + t.Fatal("did not discover peer in time") + } +} + type testnet struct { ctx context.Context T *testing.T @@ -80,8 +143,9 @@ type testnet struct { } func newTestnet(ctx context.Context, t *testing.T) *testnet { - swarm := swarmt.GenSwarm(t, swarmt.OptDisableTCP) - hst, err := basic.NewHost(swarm, &basic.HostOpts{}) + bus := eventbus.NewBus() + swarm := swarmt.GenSwarm(t, swarmt.OptDisableTCP, swarmt.EventBus(bus)) + hst, err := basic.NewHost(swarm, &basic.HostOpts{EventBus: bus}) require.NoError(t, err) hst.Start() @@ -95,23 +159,28 @@ func newTestnet(ctx context.Context, t *testing.T) *testnet { return &testnet{ctx: ctx, T: t, bootstrapper: *host.InfoFromHost(hst)} } -func (t *testnet) discovery(opts ...Option) *Discovery { - hst, routingDisc := t.peer() - disc := NewDiscovery(hst, routingDisc, opts...) - err := disc.Start(t.ctx) +func (t *testnet) startNewDiscovery( + params *Parameters, + hst host.Host, + routingDisc discovery.Discovery, + tag string, + opts ...Option, +) *Discovery { + disc, err := NewDiscovery(params, hst, routingDisc, tag, opts...) + require.NoError(t.T, err) + err = disc.Start(t.ctx) require.NoError(t.T, err) t.T.Cleanup(func() { err := disc.Stop(t.ctx) require.NoError(t.T, err) }) - - go disc.Advertise(t.ctx) return disc } func (t *testnet) peer() (host.Host, discovery.Discovery) { - swarm := swarmt.GenSwarm(t.T, swarmt.OptDisableTCP) - hst, err := basic.NewHost(swarm, &basic.HostOpts{}) + bus := eventbus.NewBus() + swarm := swarmt.GenSwarm(t.T, swarmt.OptDisableTCP, swarmt.EventBus(bus)) + hst, err := basic.NewHost(swarm, &basic.HostOpts{EventBus: bus}) require.NoError(t.T, err) hst.Start() @@ -131,3 +200,11 @@ func (t *testnet) peer() (host.Host, discovery.Discovery) { return hst, routing.NewRoutingDiscovery(dht) } + +func checkPeer(t *testing.T, expected peer.ID, done chan struct{}) func(peerID peer.ID, isAdded bool) { + return func(peerID peer.ID, isAdded bool) { + defer close(done) + require.Equal(t, expected, peerID) + require.True(t, isAdded) + } +} diff --git a/share/p2p/discovery/metrics.go b/share/p2p/discovery/metrics.go index c147a2eeeb..78b62a7d97 100644 --- a/share/p2p/discovery/metrics.go +++ b/share/p2p/discovery/metrics.go @@ -5,20 +5,18 @@ import ( "fmt" "github.com/libp2p/go-libp2p/core/peer" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" - "go.opentelemetry.io/otel/metric/instrument/asyncint64" - "go.opentelemetry.io/otel/metric/instrument/syncint64" + "go.opentelemetry.io/otel/metric" + + "github.com/celestiaorg/celestia-node/libs/utils" ) const ( - discoveryEnougPeersKey = "enough_peers" - discoveryFindCancledKey = "is_canceled" + discoveryEnoughPeersKey = "enough_peers" handlePeerResultKey = "result" handlePeerSkipSelf handlePeerResult = "skip_self" - handlePeerEmptyAddrs handlePeerResult = "skip_empty_addresses" handlePeerEnoughPeers handlePeerResult = "skip_enough_peers" handlePeerBackoff handlePeerResult = "skip_backoff" handlePeerConnected handlePeerResult = "connected" @@ -28,20 +26,17 @@ const ( advertiseFailedKey = "failed" ) -var ( - meter = global.MeterProvider().Meter("share_discovery") -) +var meter = otel.Meter("share_discovery") type handlePeerResult string type metrics struct { - peersAmount asyncint64.Gauge - discoveryResult syncint64.Counter // attributes: enough_peers[bool],is_canceled[bool] - discoveryStuck syncint64.Counter - handlePeerResult syncint64.Counter // attributes: result[string] - advertise syncint64.Counter // attributes: failed[bool] - peerAdded syncint64.Counter - peerRemoved syncint64.Counter + peersAmount metric.Int64ObservableGauge + discoveryResult metric.Int64Counter // attributes: enough_peers[bool],is_canceled[bool] + handlePeerResult metric.Int64Counter // attributes: result[string] + advertise metric.Int64Counter // attributes: failed[bool] + peerAdded metric.Int64Counter + peerRemoved metric.Int64Counter } // WithMetrics turns on metric collection in discoery. @@ -51,55 +46,49 @@ func (d *Discovery) WithMetrics() error { return fmt.Errorf("discovery: init metrics: %w", err) } d.metrics = metrics - d.WithOnPeersUpdate(metrics.observeOnPeersUpdate) + d.onUpdatedPeers = d.onUpdatedPeers.add(metrics.observeOnPeersUpdate) return nil } func initMetrics(d *Discovery) (*metrics, error) { - peersAmount, err := meter.AsyncInt64().Gauge("discovery_amount_of_peers", - instrument.WithDescription("amount of peers in discovery set")) + peersAmount, err := meter.Int64ObservableGauge("discovery_amount_of_peers", + metric.WithDescription("amount of peers in discovery set")) if err != nil { return nil, err } - discoveryResult, err := meter.SyncInt64().Counter("discovery_find_peers_result", - instrument.WithDescription("result of find peers run")) + discoveryResult, err := meter.Int64Counter("discovery_find_peers_result", + metric.WithDescription("result of find peers run")) if err != nil { return nil, err } - discoveryStuck, err := meter.SyncInt64().Counter("discovery_lookup_is_stuck", - instrument.WithDescription("indicates discovery wasn't able to find peers for more than 1 min")) + handlePeerResultCounter, err := meter.Int64Counter("discovery_handler_peer_result", + metric.WithDescription("result handling found peer")) if err != nil { return nil, err } - handlePeerResultCounter, err := meter.SyncInt64().Counter("discovery_handler_peer_result", - instrument.WithDescription("result handling found peer")) + advertise, err := meter.Int64Counter("discovery_advertise_event", + metric.WithDescription("advertise events counter")) if err != nil { return nil, err } - advertise, err := meter.SyncInt64().Counter("discovery_advertise_event", - instrument.WithDescription("advertise events counter")) + peerAdded, err := meter.Int64Counter("discovery_add_peer", + metric.WithDescription("add peer to discovery set counter")) if err != nil { return nil, err } - peerAdded, err := meter.SyncInt64().Counter("discovery_add_peer", - instrument.WithDescription("add peer to discovery set counter")) + peerRemoved, err := meter.Int64Counter("discovery_remove_peer", + metric.WithDescription("remove peer from discovery set counter")) if err != nil { return nil, err } - peerRemoved, err := meter.SyncInt64().Counter("discovery_remove_peer", - instrument.WithDescription("remove peer from discovery set counter")) - if err != nil { - return nil, err - } - - backOffSize, err := meter.AsyncInt64().Gauge("discovery_backoff_amount", - instrument.WithDescription("amount of peers in backoff")) + backOffSize, err := meter.Int64ObservableGauge("discovery_backoff_amount", + metric.WithDescription("amount of peers in backoff")) if err != nil { return nil, err } @@ -107,64 +96,55 @@ func initMetrics(d *Discovery) (*metrics, error) { metrics := &metrics{ peersAmount: peersAmount, discoveryResult: discoveryResult, - discoveryStuck: discoveryStuck, handlePeerResult: handlePeerResultCounter, advertise: advertise, peerAdded: peerAdded, peerRemoved: peerRemoved, } - err = meter.RegisterCallback( - []instrument.Asynchronous{ - peersAmount, - backOffSize, - }, - func(ctx context.Context) { - peersAmount.Observe(ctx, int64(d.set.Size())) - backOffSize.Observe(ctx, int64(d.connector.Size())) - }, - ) + callback := func(ctx context.Context, observer metric.Observer) error { + observer.ObserveInt64(peersAmount, int64(d.set.Size())) + observer.ObserveInt64(backOffSize, int64(d.connector.Size())) + return nil + } + _, err = meter.RegisterCallback(callback, peersAmount, backOffSize) if err != nil { return nil, fmt.Errorf("registering metrics callback: %w", err) } return metrics, nil } -func (m *metrics) observeFindPeers(ctx context.Context, canceled, isEnoughPeers bool) { +func (m *metrics) observeFindPeers(ctx context.Context, isEnoughPeers bool) { if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } + ctx = utils.ResetContextOnError(ctx) m.discoveryResult.Add(ctx, 1, - attribute.Bool(discoveryFindCancledKey, canceled), - attribute.Bool(discoveryEnougPeersKey, isEnoughPeers)) + metric.WithAttributes( + attribute.Bool(discoveryEnoughPeersKey, isEnoughPeers))) } func (m *metrics) observeHandlePeer(ctx context.Context, result handlePeerResult) { if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } + ctx = utils.ResetContextOnError(ctx) m.handlePeerResult.Add(ctx, 1, - attribute.String(handlePeerResultKey, string(result))) + metric.WithAttributes( + attribute.String(handlePeerResultKey, string(result)))) } func (m *metrics) observeAdvertise(ctx context.Context, err error) { if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } + ctx = utils.ResetContextOnError(ctx) m.advertise.Add(ctx, 1, - attribute.Bool(advertiseFailedKey, err != nil)) + metric.WithAttributes( + attribute.Bool(advertiseFailedKey, err != nil))) } func (m *metrics) observeOnPeersUpdate(_ peer.ID, isAdded bool) { @@ -179,14 +159,3 @@ func (m *metrics) observeOnPeersUpdate(_ peer.ID, isAdded bool) { } m.peerRemoved.Add(ctx, 1) } - -func (m *metrics) observeDiscoveryStuck(ctx context.Context) { - if m == nil { - return - } - if ctx.Err() != nil { - ctx = context.Background() - } - - m.discoveryStuck.Add(ctx, 1) -} diff --git a/share/p2p/discovery/options.go b/share/p2p/discovery/options.go index 8a6a162e11..de4b13a7db 100644 --- a/share/p2p/discovery/options.go +++ b/share/p2p/discovery/options.go @@ -3,6 +3,8 @@ package discovery import ( "fmt" "time" + + "github.com/libp2p/go-libp2p/core/peer" ) // Parameters is the set of Parameters that must be configured for the Discovery module @@ -16,44 +18,50 @@ type Parameters struct { AdvertiseInterval time.Duration } +// options is the set of options that can be configured for the Discovery module +type options struct { + // onUpdatedPeers will be called on peer set changes + onUpdatedPeers OnUpdatedPeers +} + // Option is a function that configures Discovery Parameters -type Option func(*Parameters) +type Option func(*options) // DefaultParameters returns the default Parameters' configuration values // for the Discovery module -func DefaultParameters() Parameters { - return Parameters{ - PeersLimit: 5, - // based on https://github.com/libp2p/go-libp2p-kad-dht/pull/793 - AdvertiseInterval: time.Hour * 22, +func DefaultParameters() *Parameters { + return &Parameters{ + PeersLimit: 5, + AdvertiseInterval: time.Hour, } } // Validate validates the values in Parameters func (p *Parameters) Validate() error { - if p.AdvertiseInterval <= 0 { - return fmt.Errorf( - "discovery: invalid option: value AdvertiseInterval %s, %s", - "is 0 or negative.", - "value must be positive", - ) + if p.PeersLimit <= 0 { + return fmt.Errorf("discovery: peers limit cannot be zero or negative") } + if p.AdvertiseInterval <= 0 { + return fmt.Errorf("discovery: advertise interval cannot be zero or negative") + } return nil } -// WithPeersLimit is a functional option that Discovery -// uses to set the PeersLimit configuration param -func WithPeersLimit(peersLimit uint) Option { - return func(p *Parameters) { - p.PeersLimit = peersLimit +// WithOnPeersUpdate chains OnPeersUpdate callbacks on every update of discovered peers list. +func WithOnPeersUpdate(f OnUpdatedPeers) Option { + return func(p *options) { + p.onUpdatedPeers = p.onUpdatedPeers.add(f) } } -// WithAdvertiseInterval is a functional option that Discovery -// uses to set the AdvertiseInterval configuration param -func WithAdvertiseInterval(advInterval time.Duration) Option { - return func(p *Parameters) { - p.AdvertiseInterval = advInterval +func newOptions(opts ...Option) *options { + defaults := &options{ + onUpdatedPeers: func(peer.ID, bool) {}, + } + + for _, opt := range opts { + opt(defaults) } + return defaults } diff --git a/share/p2p/doc.go b/share/p2p/doc.go new file mode 100644 index 0000000000..991ddf94db --- /dev/null +++ b/share/p2p/doc.go @@ -0,0 +1,18 @@ +// Package p2p provides p2p functionality that powers the share exchange protocols used by celestia-node. +// The available protocols are: +// +// - shrexsub : a floodsub-based pubsub protocol that is used to broadcast/subscribe to the event +// of new EDS in the network to peers. +// +// - shrexnd: a request/response protocol that is used to request shares by namespace or namespace data from peers. +// +// - shrexeds: a request/response protocol that is used to request extended data square shares from peers. +// This protocol exchanges the original data square in between the client and server, and it's up to the +// receiver to compute the extended data square. +// +// This package also defines a peer manager that is used to manage network peers that can be used to exchange +// shares. The peer manager is primarily responsible for providing peers to request shares from, +// and is primarily used by `getters.ShrexGetter` in share/getters/shrex.go. +// +// Find out more about each protocol in their respective sub-packages. +package p2p diff --git a/share/p2p/errors.go b/share/p2p/errors.go index 77f23c554e..cb7b596f47 100644 --- a/share/p2p/errors.go +++ b/share/p2p/errors.go @@ -9,6 +9,8 @@ import ( // available at the moment. The request may be retried later, but it's unlikely to succeed. var ErrNotFound = errors.New("the requested data or resource could not be found") +var ErrRateLimited = errors.New("server is overloaded and rate limited the request") + // ErrInvalidResponse is returned when a peer returns an invalid response or caused an internal // error. It is used to signal that the peer couldn't serve the data successfully, and should not be // retried. diff --git a/share/p2p/metrics.go b/share/p2p/metrics.go index 87c1e2eeb0..55aefda81d 100644 --- a/share/p2p/metrics.go +++ b/share/p2p/metrics.go @@ -4,18 +4,22 @@ import ( "context" "fmt" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" - "go.opentelemetry.io/otel/metric/instrument/syncint64" - "go.opentelemetry.io/otel/metric/unit" + "go.opentelemetry.io/otel/metric" + + "github.com/celestiaorg/celestia-node/libs/utils" ) -var meter = global.MeterProvider().Meter("shrex/eds") +var meter = otel.Meter("shrex/eds") type status string const ( + StatusBadRequest status = "bad_request" + StatusSendRespErr status = "send_resp_err" + StatusSendReqErr status = "send_req_err" + StatusReadRespErr status = "read_resp_err" StatusInternalErr status = "internal_err" StatusNotFound status = "not_found" StatusTimeout status = "timeout" @@ -24,7 +28,7 @@ const ( ) type Metrics struct { - totalRequestCounter syncint64.Counter + totalRequestCounter metric.Int64Counter } // ObserveRequests increments the total number of requests sent with the given status as an @@ -33,17 +37,17 @@ func (m *Metrics) ObserveRequests(ctx context.Context, count int64, status statu if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } - m.totalRequestCounter.Add(ctx, count, attribute.String("status", string(status))) + ctx = utils.ResetContextOnError(ctx) + m.totalRequestCounter.Add(ctx, count, + metric.WithAttributes( + attribute.String("status", string(status)), + )) } func InitClientMetrics(protocol string) (*Metrics, error) { - totalRequestCounter, err := meter.SyncInt64().Counter( + totalRequestCounter, err := meter.Int64Counter( fmt.Sprintf("shrex_%s_client_total_requests", protocol), - instrument.WithUnit(unit.Dimensionless), - instrument.WithDescription(fmt.Sprintf("Total count of sent shrex/%s requests", protocol)), + metric.WithDescription(fmt.Sprintf("Total count of sent shrex/%s requests", protocol)), ) if err != nil { return nil, err @@ -55,10 +59,9 @@ func InitClientMetrics(protocol string) (*Metrics, error) { } func InitServerMetrics(protocol string) (*Metrics, error) { - totalRequestCounter, err := meter.SyncInt64().Counter( + totalRequestCounter, err := meter.Int64Counter( fmt.Sprintf("shrex_%s_server_total_responses", protocol), - instrument.WithUnit(unit.Dimensionless), - instrument.WithDescription(fmt.Sprintf("Total count of sent shrex/%s responses", protocol)), + metric.WithDescription(fmt.Sprintf("Total count of sent shrex/%s responses", protocol)), ) if err != nil { return nil, err diff --git a/share/p2p/params.go b/share/p2p/params.go index f11179293d..6636e38fc5 100644 --- a/share/p2p/params.go +++ b/share/p2p/params.go @@ -45,7 +45,7 @@ func (p *Parameters) Validate() error { return fmt.Errorf("invalid write timeout: %v, %s", p.ServerWriteTimeout, errSuffix) } if p.HandleRequestTimeout <= 0 { - return fmt.Errorf("invalid hadnle request timeout: %v, %s", p.HandleRequestTimeout, errSuffix) + return fmt.Errorf("invalid handle request timeout: %v, %s", p.HandleRequestTimeout, errSuffix) } if p.ConcurrencyLimit <= 0 { return fmt.Errorf("invalid concurrency limit: %s", errSuffix) diff --git a/share/p2p/peers/doc.go b/share/p2p/peers/doc.go new file mode 100644 index 0000000000..bc1647eb42 --- /dev/null +++ b/share/p2p/peers/doc.go @@ -0,0 +1,52 @@ +// Package peers provides a peer manager that handles peer discovery and peer selection for the shrex getter. +// +// The peer manager is responsible for: +// - Discovering peers +// - Selecting peers for data retrieval +// - Validating peers +// - Blacklisting peers +// - Garbage collecting peers +// +// The peer manager is not responsible for: +// - Connecting to peers +// - Disconnecting from peers +// - Sending data to peers +// - Receiving data from peers +// +// The peer manager is a mechanism to store peers from shrexsub, a mechanism that +// handles "peer discovery" and "peer selection" by relying on a shrexsub subscription +// and header subscriptions, such that it listens for new headers and +// new shares and uses this information to pool peers by shares. +// +// This gives the peer manager an ability to block peers that gossip invalid shares, but also access a list of peers +// that are known to have been gossiping valid shares. +// The peers are then returned on request using a round-robin algorithm to return a different peer each time. +// If no peers are found, the peer manager will rely on full nodes retrieved from discovery. +// +// The peer manager is only concerned with recent heights, thus it retrieves peers that +// were active since `initialHeight`. +// The peer manager will also garbage collect peers such that it blacklists peers that +// have been active since `initialHeight` but have been found to be invalid. +// +// The peer manager is passed to the shrex getter and is used at request time to +// select peers for a given data hash for data retrieval. +// +// # Usage +// +// The peer manager is created using [NewManager] constructor: +// +// peerManager := peers.NewManager(headerSub, shrexSub, discovery, host, connGater, opts...) +// +// After creating the peer manager, it should be started to kick off listening and +// validation routines that enable peer selection and retrieval: +// +// err := peerManager.Start(ctx) +// +// The peer manager can be stopped at any time to stop all peer discovery and validation routines: +// +// err := peerManager.Stop(ctx) +// +// The peer manager can be used to select peers for a given datahash for shares retrieval: +// +// peer, err := peerManager.Peer(ctx, hash) +package peers diff --git a/share/p2p/peers/manager.go b/share/p2p/peers/manager.go index 313cca8db0..1a00059628 100644 --- a/share/p2p/peers/manager.go +++ b/share/p2p/peers/manager.go @@ -7,7 +7,6 @@ import ( "sync" "sync/atomic" "time" - "unsafe" logging "github.com/ipfs/go-log/v2" pubsub "github.com/libp2p/go-libp2p-pubsub" @@ -22,15 +21,12 @@ import ( "github.com/celestiaorg/celestia-node/header" "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/p2p/discovery" "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" ) const ( // ResultNoop indicates operation was successful and no extra action is required ResultNoop result = "result_noop" - // ResultSynced will save the status of pool as "synced" and will remove peers from it - ResultSynced = "result_synced" // ResultCooldownPeer will put returned peer on cooldown, meaning it won't be available by Peer // method for some time ResultCooldownPeer = "result_cooldown_peer" @@ -41,6 +37,10 @@ const ( // eventbusBufSize is the size of the buffered channel to handle // events in libp2p eventbusBufSize = 32 + + // storedPoolsAmount is the amount of pools for recent headers that will be stored in the peer + // manager + storedPoolsAmount = 10 ) type result string @@ -55,15 +55,17 @@ type Manager struct { // header subscription is necessary in order to Validate the inbound eds hash headerSub libhead.Subscriber[*header.ExtendedHeader] shrexSub *shrexsub.PubSub - disc *discovery.Discovery host host.Host connGater *conngater.BasicConnectionGater - // pools collecting peers from shrexSub + // pools collecting peers from shrexSub and stores them by datahash pools map[string]*syncPool - // messages from shrex.Sub with height below initialHeight will be ignored, since we don't need to - // track peers for those headers + + // initialHeight is the height of the first header received from headersub initialHeight atomic.Uint64 + // messages from shrex.Sub with height below storeFrom will be ignored, since we don't need to + // track peers for those headers + storeFrom atomic.Uint64 // fullNodes collects full nodes peer.ID found via discovery fullNodes *pool @@ -88,22 +90,17 @@ type syncPool struct { // isValidatedDataHash indicates if datahash was validated by receiving corresponding extended // header from headerSub isValidatedDataHash atomic.Bool - // headerHeight is the height of header corresponding to syncpool - headerHeight atomic.Uint64 - // isSynced will be true if DoneFunc was called with ResultSynced. It indicates that given datahash - // was synced and peer-manager no longer need to keep peers for it - isSynced atomic.Bool + // height is the height of the header that corresponds to datahash + height uint64 // createdAt is the syncPool creation time createdAt time.Time } func NewManager( params Parameters, - headerSub libhead.Subscriber[*header.ExtendedHeader], - shrexSub *shrexsub.PubSub, - discovery *discovery.Discovery, host host.Host, connGater *conngater.BasicConnectionGater, + options ...Option, ) (*Manager, error) { if err := params.Validate(); err != nil { return nil, err @@ -111,10 +108,7 @@ func NewManager( s := &Manager{ params: params, - headerSub: headerSub, - shrexSub: shrexSub, connGater: connGater, - disc: discovery, host: host, pools: make(map[string]*syncPool), blacklistedHashes: make(map[string]bool), @@ -122,24 +116,14 @@ func NewManager( disconnectedPeersDone: make(chan struct{}), } - s.fullNodes = newPool(s.params.PeerCooldown) - - discovery.WithOnPeersUpdate( - func(peerID peer.ID, isAdded bool) { - if isAdded { - if s.isBlacklistedPeer(peerID) { - log.Debugw("got blacklisted peer from discovery", "peer", peerID) - return - } - s.fullNodes.add(peerID) - log.Debugw("added to full nodes", "peer", peerID) - return - } - - log.Debugw("removing peer from discovered full nodes", "peer", peerID) - s.fullNodes.remove(peerID) - }) + for _, opt := range options { + err := opt(s) + if err != nil { + return nil, err + } + } + s.fullNodes = newPool(s.params.PeerCooldown) return s, nil } @@ -147,12 +131,17 @@ func (m *Manager) Start(startCtx context.Context) error { ctx, cancel := context.WithCancel(context.Background()) m.cancel = cancel + // pools will only be populated with senders of shrexsub notifications if the WithShrexSubPools + // option is used. + if m.shrexSub == nil && m.headerSub == nil { + return nil + } + validatorFn := m.metrics.validationObserver(m.Validate) err := m.shrexSub.AddValidator(validatorFn) if err != nil { return fmt.Errorf("registering validator: %w", err) } - err = m.shrexSub.Start(startCtx) if err != nil { return fmt.Errorf("starting shrexsub: %w", err) @@ -168,16 +157,21 @@ func (m *Manager) Start(startCtx context.Context) error { return fmt.Errorf("subscribing to libp2p events: %w", err) } - go m.subscribeDisconnectedPeers(ctx, sub) go m.subscribeHeader(ctx, headerSub) + go m.subscribeDisconnectedPeers(ctx, sub) go m.GC(ctx) - return nil } func (m *Manager) Stop(ctx context.Context) error { m.cancel() + // we do not need to wait for headersub and disconnected peers to finish + // here, since they were never started + if m.headerSub == nil && m.shrexSub == nil { + return nil + } + select { case <-m.headerSubDone: case <-ctx.Done(): @@ -198,16 +192,15 @@ func (m *Manager) Stop(ctx context.Context) error { // full nodes, it will wait until any peer appear in either source or timeout happen. // After fetching data using given peer, caller is required to call returned DoneFunc using // appropriate result value -func (m *Manager) Peer( - ctx context.Context, datahash share.DataHash, +func (m *Manager) Peer(ctx context.Context, datahash share.DataHash, height uint64, ) (peer.ID, DoneFunc, error) { - p := m.validatedPool(datahash.String()) + p := m.validatedPool(datahash.String(), height) // first, check if a peer is available for the given datahash peerID, ok := p.tryGet() if ok { if m.removeIfUnreachable(p, peerID) { - return m.Peer(ctx, datahash) + return m.Peer(ctx, datahash, height) } return m.newPeer(ctx, datahash, peerID, sourceShrexSub, p.len(), 0) } @@ -224,7 +217,7 @@ func (m *Manager) Peer( select { case peerID = <-p.next(ctx): if m.removeIfUnreachable(p, peerID) { - return m.Peer(ctx, datahash) + return m.Peer(ctx, datahash, height) } return m.newPeer(ctx, datahash, peerID, sourceShrexSub, p.len(), time.Since(start)) case peerID = <-m.fullNodes.next(ctx): @@ -234,6 +227,22 @@ func (m *Manager) Peer( } } +// UpdateFullNodePool is called by discovery when new full node is discovered or removed +func (m *Manager) UpdateFullNodePool(peerID peer.ID, isAdded bool) { + if isAdded { + if m.isBlacklistedPeer(peerID) { + log.Debugw("got blacklisted peer from discovery", "peer", peerID.String()) + return + } + m.fullNodes.add(peerID) + log.Debugw("added to full nodes", "peer", peerID) + return + } + + log.Debugw("removing peer from discovered full nodes", "peer", peerID.String()) + m.fullNodes.remove(peerID) +} + func (m *Manager) newPeer( ctx context.Context, datahash share.DataHash, @@ -256,20 +265,18 @@ func (m *Manager) doneFunc(datahash share.DataHash, peerID peer.ID, source peerS return func(result result) { log.Debugw("set peer result", "hash", datahash.String(), - "peer", peerID, + "peer", peerID.String(), "source", source, "result", result) m.metrics.observeDoneResult(source, result) switch result { case ResultNoop: - case ResultSynced: - m.markPoolAsSynced(datahash.String()) case ResultCooldownPeer: if source == sourceFullNodes { m.fullNodes.putOnCooldown(peerID) return } - m.getOrCreatePool(datahash.String()).putOnCooldown(peerID) + m.getPool(datahash.String()).putOnCooldown(peerID) case ResultBlacklistPeer: m.blacklistPeers(reasonMisbehave, peerID) } @@ -290,12 +297,16 @@ func (m *Manager) subscribeHeader(ctx context.Context, headerSub libhead.Subscri log.Errorw("get next header from sub", "err", err) continue } - m.validatedPool(h.DataHash.String()) + m.validatedPool(h.DataHash.String(), h.Height()) // store first header for validation purposes - if m.initialHeight.CompareAndSwap(0, uint64(h.Height())) { + if m.initialHeight.CompareAndSwap(0, h.Height()) { log.Debugw("stored initial height", "height", h.Height()) } + + // update storeFrom if header height + m.storeFrom.Store(uint64(max(0, int(h.Height())-storedPoolsAmount))) + log.Debugw("updated lowest stored height", "height", h.Height()) } } @@ -318,7 +329,7 @@ func (m *Manager) subscribeDisconnectedPeers(ctx context.Context, sub event.Subs if connStatus.Connectedness == network.NotConnected { peer := connStatus.Peer if m.fullNodes.has(peer) { - log.Debugw("peer disconnected, removing from full nodes", "peer", peer) + log.Debugw("peer disconnected, removing from full nodes", "peer", peer.String()) m.fullNodes.remove(peer) } } @@ -328,7 +339,7 @@ func (m *Manager) subscribeDisconnectedPeers(ctx context.Context, sub event.Subs // Validate will collect peer.ID into corresponding peer pool func (m *Manager) Validate(_ context.Context, peerID peer.ID, msg shrexsub.Notification) pubsub.ValidationResult { - logger := log.With("peer", peerID, "hash", msg.DataHash.String()) + logger := log.With("peer", peerID.String(), "hash", msg.DataHash.String()) // messages broadcast from self should bypass the validation with Accept if peerID == m.host.ID() { @@ -347,22 +358,12 @@ func (m *Manager) Validate(_ context.Context, peerID peer.ID, msg shrexsub.Notif return pubsub.ValidationReject } - if msg.Height == 0 { - logger.Debug("received message with 0 height") - return pubsub.ValidationReject - } - - if msg.Height < m.initialHeight.Load() { - // we can use peers from discovery for headers before the first one from headerSub - // if we allow pool creation for those headers, there is chance the pool will not be validated in - // time and will be false-positively trigger blacklisting of hash and all peers that sent msgs for - // that hash + if msg.Height < m.storeFrom.Load() { logger.Debug("received message for past header") return pubsub.ValidationIgnore } - p := m.getOrCreatePool(msg.DataHash.String()) - p.headerHeight.Store(msg.Height) + p := m.getOrCreatePool(msg.DataHash.String(), msg.Height) logger.Debugw("got hash from shrex-sub") p.add(peerID) @@ -373,13 +374,20 @@ func (m *Manager) Validate(_ context.Context, peerID peer.ID, msg shrexsub.Notif return pubsub.ValidationIgnore } -func (m *Manager) getOrCreatePool(datahash string) *syncPool { +func (m *Manager) getPool(datahash string) *syncPool { + m.lock.Lock() + defer m.lock.Unlock() + return m.pools[datahash] +} + +func (m *Manager) getOrCreatePool(datahash string, height uint64) *syncPool { m.lock.Lock() defer m.lock.Unlock() p, ok := m.pools[datahash] if !ok { p = &syncPool{ + height: height, pool: newPool(m.params.PeerCooldown), createdAt: time.Now(), } @@ -390,15 +398,16 @@ func (m *Manager) getOrCreatePool(datahash string) *syncPool { } func (m *Manager) blacklistPeers(reason blacklistPeerReason, peerIDs ...peer.ID) { - log.Debugw("blacklisting peers", - "peers", peerIDs, - "reason", reason) m.metrics.observeBlacklistPeers(reason, len(peerIDs)) - if !m.params.EnableBlackListing { - return - } for _, peerID := range peerIDs { + // blacklisted peers will be logged regardless of EnableBlackListing whether option being is + // enabled, until blacklisting is not properly tested and enabled by default. + log.Debugw("blacklisting peer", "peer", peerID.String(), "reason", reason) + if !m.params.EnableBlackListing { + continue + } + m.fullNodes.remove(peerID) // add peer to the blacklist, so we can't connect to it in the future. err := m.connGater.BlockPeer(peerID) @@ -423,8 +432,8 @@ func (m *Manager) isBlacklistedHash(hash share.DataHash) bool { return m.blacklistedHashes[hash.String()] } -func (m *Manager) validatedPool(hashStr string) *syncPool { - p := m.getOrCreatePool(hashStr) +func (m *Manager) validatedPool(hashStr string, height uint64) *syncPool { + p := m.getOrCreatePool(hashStr, height) if p.isValidatedDataHash.CompareAndSwap(false, true) { log.Debugw("pool marked validated", "datahash", hashStr) // if pool is proven to be valid, add all collected peers to full nodes @@ -473,12 +482,24 @@ func (m *Manager) cleanUp() []peer.ID { addToBlackList := make(map[peer.ID]struct{}) for h, p := range m.pools { - if !p.isValidatedDataHash.Load() && time.Since(p.createdAt) > m.params.PoolValidationTimeout { - delete(m.pools, h) - if p.headerHeight.Load() < m.initialHeight.Load() { - // outdated pools could still be valid even if not validated, no need to blacklist - continue + if p.isValidatedDataHash.Load() { + // remove pools that are outdated + if p.height < m.storeFrom.Load() { + delete(m.pools, h) } + continue + } + + // can't validate datahashes below initial height + if p.height < m.initialHeight.Load() { + delete(m.pools, h) + continue + } + + // find pools that are not validated in time + if time.Since(p.createdAt) > m.params.PoolValidationTimeout { + delete(m.pools, h) + log.Debug("blacklisting datahash with all corresponding peers", "hash", h, "peer_list", p.peersList) @@ -498,19 +519,3 @@ func (m *Manager) cleanUp() []peer.ID { } return blacklist } - -func (m *Manager) markPoolAsSynced(datahash string) { - p := m.getOrCreatePool(datahash) - if p.isSynced.CompareAndSwap(false, true) { - p.isSynced.Store(true) - old := (*unsafe.Pointer)(unsafe.Pointer(&p.pool)) - // release pointer to old pool to free up memory - atomic.StorePointer(old, unsafe.Pointer(newPool(time.Second))) - } -} - -func (p *syncPool) add(peers ...peer.ID) { - if !p.isSynced.Load() { - p.pool.add(peers...) - } -} diff --git a/share/p2p/peers/manager_test.go b/share/p2p/peers/manager_test.go index e10e820e84..d4a188ff56 100644 --- a/share/p2p/peers/manager_test.go +++ b/share/p2p/peers/manager_test.go @@ -2,21 +2,21 @@ package peers import ( "context" - sync2 "sync" + "sync" "testing" "time" "github.com/ipfs/go-datastore" - "github.com/ipfs/go-datastore/sync" + dssync "github.com/ipfs/go-datastore/sync" dht "github.com/libp2p/go-libp2p-kad-dht" pubsub "github.com/libp2p/go-libp2p-pubsub" - routinghelpers "github.com/libp2p/go-libp2p-routing-helpers" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/peer" routingdisc "github.com/libp2p/go-libp2p/p2p/discovery/routing" "github.com/libp2p/go-libp2p/p2p/net/conngater" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/rand" libhead "github.com/celestiaorg/go-header" @@ -26,10 +26,9 @@ import ( "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" ) -// TODO: add broadcast to tests func TestManager(t *testing.T) { t.Run("Validate pool by headerSub", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*50) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) t.Cleanup(cancel) // create headerSub mock @@ -64,21 +63,16 @@ func TestManager(t *testing.T) { result := manager.Validate(ctx, peerID, msg) require.Equal(t, pubsub.ValidationIgnore, result) - pID, done, err := manager.Peer(ctx, h.DataHash.Bytes()) + pID, _, err := manager.Peer(ctx, h.DataHash.Bytes(), h.Height()) require.NoError(t, err) require.Equal(t, peerID, pID) // check pool validation - require.True(t, manager.getOrCreatePool(h.DataHash.String()).isValidatedDataHash.Load()) - - done(ResultSynced) - // pool should not be removed after success - require.Len(t, manager.pools, 1) - require.Len(t, manager.getOrCreatePool(h.DataHash.String()).pool.peersList, 0) + require.True(t, manager.getPool(h.DataHash.String()).isValidatedDataHash.Load()) }) t.Run("validator", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*50) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) t.Cleanup(cancel) // create headerSub mock @@ -100,7 +94,7 @@ func TestManager(t *testing.T) { require.Equal(t, pubsub.ValidationIgnore, result) // mark peer as misbehaved to blacklist it - pID, done, err := manager.Peer(ctx, h.DataHash.Bytes()) + pID, done, err := manager.Peer(ctx, h.DataHash.Bytes(), h.Height()) require.NoError(t, err) require.Equal(t, peerID, pID) manager.params.EnableBlackListing = true @@ -132,19 +126,21 @@ func TestManager(t *testing.T) { // create unvalidated pool peerID := peer.ID("peer1") msg := shrexsub.Notification{ - DataHash: share.DataHash("datahash1"), + DataHash: share.DataHash("datahash1datahash1datahash1datahash1datahash1"), Height: 2, } manager.Validate(ctx, peerID, msg) // create validated pool validDataHash := share.DataHash("datahash2") - manager.fullNodes.add("full") // add FN to unblock Peer call - manager.Peer(ctx, validDataHash) //nolint:errcheck + manager.fullNodes.add("full") // add FN to unblock Peer call + manager.Peer(ctx, validDataHash, h.Height()) //nolint:errcheck + require.Len(t, manager.pools, 3) // trigger cleanup blacklisted := manager.cleanUp() require.Contains(t, blacklisted, peerID) + require.Len(t, manager.pools, 2) // messages with blacklisted hash should be rejected right away peerID2 := peer.ID("peer2") @@ -172,8 +168,7 @@ func TestManager(t *testing.T) { peers := []peer.ID{"peer1", "peer2", "peer3"} manager.fullNodes.add(peers...) - peerID, done, err := manager.Peer(ctx, h.DataHash.Bytes()) - done(ResultSynced) + peerID, _, err := manager.Peer(ctx, h.DataHash.Bytes(), h.Height()) require.NoError(t, err) require.Contains(t, peers, peerID) @@ -195,7 +190,7 @@ func TestManager(t *testing.T) { // make sure peers are not returned before timeout timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Millisecond) t.Cleanup(cancel) - _, _, err = manager.Peer(timeoutCtx, h.DataHash.Bytes()) + _, _, err = manager.Peer(timeoutCtx, h.DataHash.Bytes(), h.Height()) require.ErrorIs(t, err, context.DeadlineExceeded) peers := []peer.ID{"peer1", "peer2", "peer3"} @@ -204,8 +199,7 @@ func TestManager(t *testing.T) { doneCh := make(chan struct{}) go func() { defer close(doneCh) - peerID, done, err := manager.Peer(ctx, h.DataHash.Bytes()) - done(ResultSynced) + peerID, _, err := manager.Peer(ctx, h.DataHash.Bytes(), h.Height()) require.NoError(t, err) require.Contains(t, peers, peerID) }() @@ -223,38 +217,7 @@ func TestManager(t *testing.T) { stopManager(t, manager) }) - t.Run("mark pool synced", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - t.Cleanup(cancel) - - h := testHeader() - headerSub := newSubLock(h, nil) - - // start test manager - manager, err := testManager(ctx, headerSub) - require.NoError(t, err) - - peerID, msg := peer.ID("peer1"), newShrexSubMsg(h) - result := manager.Validate(ctx, peerID, msg) - require.Equal(t, pubsub.ValidationIgnore, result) - - pID, done, err := manager.Peer(ctx, h.DataHash.Bytes()) - require.NoError(t, err) - require.Equal(t, peerID, pID) - done(ResultSynced) - - // check pool is soft deleted and marked synced - pool := manager.getOrCreatePool(h.DataHash.String()) - require.Len(t, pool.peersList, 0) - require.True(t, pool.isSynced.Load()) - - // add peer on synced pool should be noop - result = manager.Validate(ctx, "peer2", msg) - require.Equal(t, pubsub.ValidationIgnore, result) - require.Len(t, pool.peersList, 0) - }) - - t.Run("shrexSub sends a message lower than first headerSub header height, msg first", func(t *testing.T) { + t.Run("shrexSub sends a message lower than first headerSub header height, headerSub first", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) t.Cleanup(cancel) @@ -274,16 +237,20 @@ func TestManager(t *testing.T) { // create shrexSub msg with height lower than first header from headerSub msg := shrexsub.Notification{ DataHash: share.DataHash("datahash"), - Height: uint64(h.Height() - 1), + Height: h.Height() - 1, } result := manager.Validate(ctx, "peer", msg) require.Equal(t, pubsub.ValidationIgnore, result) + // pool will be created for first shrexSub message + require.Len(t, manager.pools, 2) - // amount of pools should not change + blacklisted := manager.cleanUp() + require.Empty(t, blacklisted) + // trigger cleanup and outdated pool should be removed require.Len(t, manager.pools, 1) }) - t.Run("shrexSub sends a message lower than first headerSub header height, headerSub first", func(t *testing.T) { + t.Run("shrexSub sends a message lower than first headerSub header height, shrexSub first", func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) t.Cleanup(cancel) @@ -298,23 +265,55 @@ func TestManager(t *testing.T) { // create shrexSub msg with height lower than first header from headerSub msg := shrexsub.Notification{ DataHash: share.DataHash("datahash"), - Height: uint64(h.Height() - 1), + Height: h.Height() - 1, } result := manager.Validate(ctx, "peer", msg) require.Equal(t, pubsub.ValidationIgnore, result) - // unlock header sub after message validator + // pool will be created for first shrexSub message + require.Len(t, manager.pools, 1) + + // unlock headerSub to allow it to send next message require.NoError(t, headerSub.wait(ctx, 1)) - // pool will be created for first headerSub header datahash + // second pool should be created require.Len(t, manager.pools, 2) - // trigger cleanup and check that no peers or hashes were blacklisted - manager.params.PoolValidationTimeout = 0 + // trigger cleanup and outdated pool should be removed blacklisted := manager.cleanUp() + require.Len(t, manager.pools, 1) + + // check that no peers or hashes were blacklisted + manager.params.PoolValidationTimeout = 0 require.Len(t, blacklisted, 0) require.Len(t, manager.blacklistedHashes, 0) + }) + + t.Run("pools store window", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Cleanup(cancel) + + h := testHeader() + h.RawHeader.Height = storedPoolsAmount * 2 + headerSub := newSubLock(h, nil) + + // start test manager + manager, err := testManager(ctx, headerSub) + require.NoError(t, err) + + // unlock headerSub to read first header + require.NoError(t, headerSub.wait(ctx, 1)) + // pool will be created for first headerSub header datahash + require.Len(t, manager.pools, 1) + + // create shrexSub msg with height lower than storedPoolsAmount + msg := shrexsub.Notification{ + DataHash: share.DataHash("datahash"), + Height: h.Height() - storedPoolsAmount - 3, + } + result := manager.Validate(ctx, "peer", msg) + require.Equal(t, pubsub.ValidationIgnore, result) - // outdated pool should be removed + // shrexSub message should be discarded and amount of pools should not change require.Len(t, manager.pools, 1) }) } @@ -348,28 +347,29 @@ func TestIntegration(t *testing.T) { time.Sleep(time.Millisecond * 100) // broadcast from BN - peerHash := share.DataHash("peer1") + randHash := rand.Bytes(32) require.NoError(t, bnPubSub.Broadcast(ctx, shrexsub.Notification{ - DataHash: peerHash, + DataHash: randHash, Height: 1, })) // FN should get message - peerID, _, err := fnPeerManager.Peer(ctx, peerHash) + gotPeer, _, err := fnPeerManager.Peer(ctx, randHash, 13) require.NoError(t, err) - // check that peerID matched bridge node - require.Equal(t, nw.Hosts()[0].ID(), peerID) + // check that gotPeer matched bridge node + require.Equal(t, nw.Hosts()[0].ID(), gotPeer) }) t.Run("get peer from discovery", func(t *testing.T) { + fullNodesTag := "fullNodes" nw, err := mocknet.FullMeshConnected(3) require.NoError(t, err) ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) t.Cleanup(cancel) // set up bootstrapper - bsHost := nw.Hosts()[2] + bsHost := nw.Hosts()[0] bs := host.InfoFromHost(bsHost) opts := []dht.Option{ dht.Mode(dht.ModeAuto), @@ -387,64 +387,74 @@ func TestIntegration(t *testing.T) { require.NoError(t, bsRouter.Bootstrap(ctx)) // set up broadcaster node - bnHost := nw.Hosts()[0] - router1, err := dht.New(ctx, bnHost, opts...) + bnHost := nw.Hosts()[1] + bnRouter, err := dht.New(ctx, bnHost, opts...) require.NoError(t, err) - bnDisc := discovery.NewDiscovery( - nw.Hosts()[0], - routingdisc.NewRoutingDiscovery(router1), - discovery.WithPeersLimit(0), - discovery.WithAdvertiseInterval(time.Second), + + params := discovery.DefaultParameters() + params.AdvertiseInterval = time.Second + + bnDisc, err := discovery.NewDiscovery( + params, + bnHost, + routingdisc.NewRoutingDiscovery(bnRouter), + fullNodesTag, ) + require.NoError(t, err) // set up full node / receiver node - fnHost := nw.Hosts()[0] - router2, err := dht.New(ctx, fnHost, opts...) + fnHost := nw.Hosts()[2] + fnRouter, err := dht.New(ctx, fnHost, opts...) require.NoError(t, err) - fnDisc := discovery.NewDiscovery( - nw.Hosts()[1], - routingdisc.NewRoutingDiscovery(router2), - discovery.WithPeersLimit(10), - discovery.WithAdvertiseInterval(time.Second), - ) - err = fnDisc.Start(ctx) - require.NoError(t, err) - t.Cleanup(func() { - err = fnDisc.Stop(ctx) - require.NoError(t, err) - }) - // hook peer manager to discovery - connGater, err := conngater.NewBasicConnectionGater(sync.MutexWrap(datastore.NewMapDatastore())) + // init peer manager for full node + connGater, err := conngater.NewBasicConnectionGater(dssync.MutexWrap(datastore.NewMapDatastore())) require.NoError(t, err) fnPeerManager, err := NewManager( DefaultParameters(), nil, - nil, - fnDisc, - nil, connGater, ) require.NoError(t, err) waitCh := make(chan struct{}) - fnDisc.WithOnPeersUpdate(func(peerID peer.ID, isAdded bool) { + checkDiscoveredPeer := func(peerID peer.ID, isAdded bool) { defer close(waitCh) - // check that obtained peer id is same as BN - require.Equal(t, nw.Hosts()[0].ID(), peerID) + // check that obtained peer id is BN + require.Equal(t, bnHost.ID(), peerID) + } + + // set up discovery for full node with hook to peer manager and check discovered peer + params = discovery.DefaultParameters() + params.AdvertiseInterval = time.Second + params.PeersLimit = 10 + + fnDisc, err := discovery.NewDiscovery( + params, + fnHost, + routingdisc.NewRoutingDiscovery(fnRouter), + fullNodesTag, + discovery.WithOnPeersUpdate(fnPeerManager.UpdateFullNodePool), + discovery.WithOnPeersUpdate(checkDiscoveredPeer), + ) + require.NoError(t, fnDisc.Start(ctx)) + t.Cleanup(func() { + err = fnDisc.Stop(ctx) + require.NoError(t, err) }) - require.NoError(t, router1.Bootstrap(ctx)) - require.NoError(t, router2.Bootstrap(ctx)) + require.NoError(t, bnRouter.Bootstrap(ctx)) + require.NoError(t, fnRouter.Bootstrap(ctx)) go bnDisc.Advertise(ctx) select { case <-waitCh: - require.Contains(t, fnPeerManager.fullNodes.peersList, fnHost.ID()) + require.Contains(t, fnPeerManager.fullNodes.peersList, bnHost.ID()) case <-ctx.Done(): require.NoError(t, ctx.Err()) } + }) } @@ -458,23 +468,17 @@ func testManager(ctx context.Context, headerSub libhead.Subscriber[*header.Exten return nil, err } - disc := discovery.NewDiscovery(nil, - routingdisc.NewRoutingDiscovery(routinghelpers.Null{}), - discovery.WithPeersLimit(0), - discovery.WithAdvertiseInterval(time.Second), - ) - connGater, err := conngater.NewBasicConnectionGater(sync.MutexWrap(datastore.NewMapDatastore())) + connGater, err := conngater.NewBasicConnectionGater(dssync.MutexWrap(datastore.NewMapDatastore())) if err != nil { return nil, err } manager, err := NewManager( DefaultParameters(), - headerSub, - shrexSub, - disc, host, connGater, + WithShrexSubPools(shrexSub, headerSub), ) + if err != nil { return nil, err } @@ -491,14 +495,15 @@ func stopManager(t *testing.T, m *Manager) { func testHeader() *header.ExtendedHeader { return &header.ExtendedHeader{ RawHeader: header.RawHeader{ - Height: 1, + Height: 1, + DataHash: rand.Bytes(32), }, } } type subLock struct { next chan struct{} - wg *sync2.WaitGroup + wg *sync.WaitGroup expected []*header.ExtendedHeader } @@ -524,7 +529,7 @@ func (s subLock) release(ctx context.Context) error { } func newSubLock(expected ...*header.ExtendedHeader) *subLock { - wg := &sync2.WaitGroup{} + wg := &sync.WaitGroup{} wg.Add(1) return &subLock{ next: make(chan struct{}), @@ -537,7 +542,7 @@ func (s *subLock) Subscribe() (libhead.Subscription[*header.ExtendedHeader], err return s, nil } -func (s *subLock) AddValidator(func(context.Context, *header.ExtendedHeader) pubsub.ValidationResult) error { +func (s *subLock) SetVerifier(func(context.Context, *header.ExtendedHeader) error) error { panic("implement me") } @@ -561,6 +566,6 @@ func (s *subLock) Cancel() { func newShrexSubMsg(h *header.ExtendedHeader) shrexsub.Notification { return shrexsub.Notification{ DataHash: h.DataHash.Bytes(), - Height: uint64(h.Height()), + Height: h.Height(), } } diff --git a/share/p2p/peers/metrics.go b/share/p2p/peers/metrics.go index bf4d544d9f..098610c595 100644 --- a/share/p2p/peers/metrics.go +++ b/share/p2p/peers/metrics.go @@ -8,12 +8,11 @@ import ( pubsub "github.com/libp2p/go-libp2p-pubsub" "github.com/libp2p/go-libp2p/core/peer" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" - "go.opentelemetry.io/otel/metric/instrument/asyncint64" - "go.opentelemetry.io/otel/metric/instrument/syncint64" + "go.opentelemetry.io/otel/metric" + "github.com/celestiaorg/celestia-node/libs/utils" "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" ) @@ -41,19 +40,14 @@ const ( poolStatusKey = "pool_status" poolStatusCreated poolStatus = "created" poolStatusValidated poolStatus = "validated" - poolStatusSynced poolStatus = "synced" poolStatusBlacklisted poolStatus = "blacklisted" // Pool status model: // created(unvalidated) // / \ - // validated(unsynced) blacklisted - // | - // synced + // validated blacklisted ) -var ( - meter = global.MeterProvider().Meter("shrex_peer_manager") -) +var meter = otel.Meter("shrex_peer_manager") type blacklistPeerReason string @@ -64,63 +58,63 @@ type poolStatus string type peerSource string type metrics struct { - getPeer syncint64.Counter // attributes: source, is_instant - getPeerWaitTimeHistogram syncint64.Histogram // attributes: source - getPeerPoolSizeHistogram syncint64.Histogram // attributes: source - doneResult syncint64.Counter // attributes: source, done_result - validationResult syncint64.Counter // attributes: validation_result - - shrexPools asyncint64.Gauge // attributes: pool_status - fullNodesPool asyncint64.Gauge // attributes: pool_status + getPeer metric.Int64Counter // attributes: source, is_instant + getPeerWaitTimeHistogram metric.Int64Histogram // attributes: source + getPeerPoolSizeHistogram metric.Int64Histogram // attributes: source + doneResult metric.Int64Counter // attributes: source, done_result + validationResult metric.Int64Counter // attributes: validation_result + + shrexPools metric.Int64ObservableGauge // attributes: pool_status + fullNodesPool metric.Int64ObservableGauge // attributes: pool_status blacklistedPeersByReason sync.Map - blacklistedPeers asyncint64.Gauge // attributes: blacklist_reason + blacklistedPeers metric.Int64ObservableGauge // attributes: blacklist_reason } func initMetrics(manager *Manager) (*metrics, error) { - getPeer, err := meter.SyncInt64().Counter("peer_manager_get_peer_counter", - instrument.WithDescription("get peer counter")) + getPeer, err := meter.Int64Counter("peer_manager_get_peer_counter", + metric.WithDescription("get peer counter")) if err != nil { return nil, err } - getPeerWaitTimeHistogram, err := meter.SyncInt64().Histogram("peer_manager_get_peer_ms_time_hist", - instrument.WithDescription("get peer time histogram(ms), observed only for async get(is_instant = false)")) + getPeerWaitTimeHistogram, err := meter.Int64Histogram("peer_manager_get_peer_ms_time_hist", + metric.WithDescription("get peer time histogram(ms), observed only for async get(is_instant = false)")) if err != nil { return nil, err } - getPeerPoolSizeHistogram, err := meter.SyncInt64().Histogram("peer_manager_get_peer_pool_size_hist", - instrument.WithDescription("amount of available active peers in pool at time when get was called")) + getPeerPoolSizeHistogram, err := meter.Int64Histogram("peer_manager_get_peer_pool_size_hist", + metric.WithDescription("amount of available active peers in pool at time when get was called")) if err != nil { return nil, err } - doneResult, err := meter.SyncInt64().Counter("peer_manager_done_result_counter", - instrument.WithDescription("done results counter")) + doneResult, err := meter.Int64Counter("peer_manager_done_result_counter", + metric.WithDescription("done results counter")) if err != nil { return nil, err } - validationResult, err := meter.SyncInt64().Counter("peer_manager_validation_result_counter", - instrument.WithDescription("validation result counter")) + validationResult, err := meter.Int64Counter("peer_manager_validation_result_counter", + metric.WithDescription("validation result counter")) if err != nil { return nil, err } - shrexPools, err := meter.AsyncInt64().Gauge("peer_manager_pools_gauge", - instrument.WithDescription("pools amount")) + shrexPools, err := meter.Int64ObservableGauge("peer_manager_pools_gauge", + metric.WithDescription("pools amount")) if err != nil { return nil, err } - fullNodesPool, err := meter.AsyncInt64().Gauge("peer_manager_full_nodes_gauge", - instrument.WithDescription("full nodes pool peers amount")) + fullNodesPool, err := meter.Int64ObservableGauge("peer_manager_full_nodes_gauge", + metric.WithDescription("full nodes pool peers amount")) if err != nil { return nil, err } - blacklisted, err := meter.AsyncInt64().Gauge("peer_manager_blacklisted_peers", - instrument.WithDescription("blacklisted peers amount")) + blacklisted, err := meter.Int64ObservableGauge("peer_manager_blacklisted_peers", + metric.WithDescription("blacklisted peers amount")) if err != nil { return nil, err } @@ -136,33 +130,31 @@ func initMetrics(manager *Manager) (*metrics, error) { blacklistedPeers: blacklisted, } - err = meter.RegisterCallback( - []instrument.Asynchronous{ - shrexPools, - fullNodesPool, - blacklisted, - }, - func(ctx context.Context) { - for poolStatus, count := range manager.shrexPools() { - shrexPools.Observe(ctx, count, - attribute.String(poolStatusKey, string(poolStatus))) - } - - fullNodesPool.Observe(ctx, int64(manager.fullNodes.len()), - attribute.String(peerStatusKey, string(peerStatusActive))) - fullNodesPool.Observe(ctx, int64(manager.fullNodes.cooldown.len()), - attribute.String(peerStatusKey, string(peerStatusCooldown))) - - metrics.blacklistedPeersByReason.Range(func(key, value any) bool { - reason := key.(blacklistPeerReason) - amount := value.(int) - blacklisted.Observe(ctx, int64(amount), - attribute.String(blacklistPeerReasonKey, string(reason))) - return true - }) - }, - ) + callback := func(ctx context.Context, observer metric.Observer) error { + for poolStatus, count := range manager.shrexPools() { + observer.ObserveInt64(shrexPools, count, + metric.WithAttributes( + attribute.String(poolStatusKey, string(poolStatus)))) + } + observer.ObserveInt64(fullNodesPool, int64(manager.fullNodes.len()), + metric.WithAttributes( + attribute.String(peerStatusKey, string(peerStatusActive)))) + observer.ObserveInt64(fullNodesPool, int64(manager.fullNodes.cooldown.len()), + metric.WithAttributes( + attribute.String(peerStatusKey, string(peerStatusCooldown)))) + + metrics.blacklistedPeersByReason.Range(func(key, value any) bool { + reason := key.(blacklistPeerReason) + amount := value.(int) + observer.ObserveInt64(blacklisted, int64(amount), + metric.WithAttributes( + attribute.String(blacklistPeerReasonKey, string(reason)))) + return true + }) + return nil + } + _, err = meter.RegisterCallback(callback, shrexPools, fullNodesPool, blacklisted) if err != nil { return nil, fmt.Errorf("registering metrics callback: %w", err) } @@ -176,21 +168,22 @@ func (m *metrics) observeGetPeer( if m == nil { return } - if ctx.Err() != nil { - ctx = context.Background() - } + ctx = utils.ResetContextOnError(ctx) m.getPeer.Add(ctx, 1, - attribute.String(sourceKey, string(source)), - attribute.Bool(isInstantKey, waitTime == 0)) + metric.WithAttributes( + attribute.String(sourceKey, string(source)), + attribute.Bool(isInstantKey, waitTime == 0))) if source == sourceShrexSub { m.getPeerPoolSizeHistogram.Record(ctx, int64(poolSize), - attribute.String(sourceKey, string(source))) + metric.WithAttributes( + attribute.String(sourceKey, string(source)))) } // record wait time only for async gets if waitTime > 0 { m.getPeerWaitTimeHistogram.Record(ctx, waitTime.Milliseconds(), - attribute.String(sourceKey, string(source))) + metric.WithAttributes( + attribute.String(sourceKey, string(source)))) } } @@ -201,8 +194,9 @@ func (m *metrics) observeDoneResult(source peerSource, result result) { ctx := context.Background() m.doneResult.Add(ctx, 1, - attribute.String(sourceKey, string(source)), - attribute.String(doneResultKey, string(result))) + metric.WithAttributes( + attribute.String(sourceKey, string(source)), + attribute.String(doneResultKey, string(result)))) } // validationObserver is a middleware that observes validation results as metrics @@ -225,12 +219,11 @@ func (m *metrics) validationObserver(validator shrexsub.ValidatorFn) shrexsub.Va resStr = "unknown" } - if ctx.Err() != nil { - ctx = context.Background() - } + ctx = utils.ResetContextOnError(ctx) m.validationResult.Add(ctx, 1, - attribute.String(validationResultKey, resStr)) + metric.WithAttributes( + attribute.String(validationResultKey, resStr))) return res } } @@ -265,11 +258,6 @@ func (m *Manager) shrexPools() map[poolStatus]int64 { continue } - if p.isSynced.Load() { - shrexPools[poolStatusSynced]++ - continue - } - // pool is validated but not synced shrexPools[poolStatusValidated]++ } diff --git a/share/p2p/peers/options.go b/share/p2p/peers/options.go index cfda906071..2970dd2465 100644 --- a/share/p2p/peers/options.go +++ b/share/p2p/peers/options.go @@ -3,6 +3,11 @@ package peers import ( "fmt" "time" + + libhead "github.com/celestiaorg/go-header" + + "github.com/celestiaorg/celestia-node/header" + "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" ) type Parameters struct { @@ -21,6 +26,8 @@ type Parameters struct { EnableBlackListing bool } +type Option func(*Manager) error + // Validate validates the values in Parameters func (p *Parameters) Validate() error { if p.PoolValidationTimeout <= 0 { @@ -38,7 +45,7 @@ func (p *Parameters) Validate() error { return nil } -// DefaultParameters returns the default configuration values for the daser parameters +// DefaultParameters returns the default configuration values for the peer manager parameters func DefaultParameters() Parameters { return Parameters{ // PoolValidationTimeout's default value is based on the default daser sampling timeout of 1 minute. @@ -56,6 +63,16 @@ func DefaultParameters() Parameters { } } +// WithShrexSubPools passes a shrexsub and headersub instance to be used to populate and validate +// pools from shrexsub notifications. +func WithShrexSubPools(shrexSub *shrexsub.PubSub, headerSub libhead.Subscriber[*header.ExtendedHeader]) Option { + return func(m *Manager) error { + m.shrexSub = shrexSub + m.headerSub = headerSub + return nil + } +} + // WithMetrics turns on metric collection in peer manager. func (m *Manager) WithMetrics() error { metrics, err := initMetrics(m) diff --git a/share/p2p/peers/pool.go b/share/p2p/peers/pool.go index c43bbc963b..365ef0306d 100644 --- a/share/p2p/peers/pool.go +++ b/share/p2p/peers/pool.go @@ -89,8 +89,11 @@ func (p *pool) next(ctx context.Context) <-chan peer.ID { return } + p.m.RLock() + hasPeerCh := p.hasPeerCh + p.m.RUnlock() select { - case <-p.hasPeerCh: + case <-hasPeerCh: case <-ctx.Done(): return } diff --git a/share/p2p/shrexeds/client.go b/share/p2p/shrexeds/client.go index 7c2591bcbd..7602bb5fb0 100644 --- a/share/p2p/shrexeds/client.go +++ b/share/p2p/shrexeds/client.go @@ -54,7 +54,7 @@ func (c *Client) RequestEDS( if err == nil { return eds, nil } - log.Debugw("client: eds request to peer failed", "peer", peer, "hash", dataHash.String(), "error", err) + log.Debugw("client: eds request to peer failed", "peer", peer.String(), "hash", dataHash.String(), "error", err) if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { c.metrics.ObserveRequests(ctx, 1, p2p.StatusTimeout) return nil, err @@ -70,7 +70,7 @@ func (c *Client) RequestEDS( } if err != p2p.ErrNotFound { log.Warnw("client: eds request to peer failed", - "peer", peer, + "peer", peer.String(), "hash", dataHash.String(), "err", err) } @@ -89,13 +89,14 @@ func (c *Client) doRequest( if err != nil { return nil, fmt.Errorf("failed to open stream: %w", err) } + defer stream.Close() c.setStreamDeadlines(ctx, stream) req := &pb.EDSRequest{Hash: dataHash} // request ODS - log.Debugf("client: requesting ods %s from peer %s", dataHash.String(), to) + log.Debugw("client: requesting ods", "hash", dataHash.String(), "peer", to.String()) _, err = serde.Write(stream, req) if err != nil { stream.Reset() //nolint:errcheck diff --git a/share/p2p/shrexeds/doc.go b/share/p2p/shrexeds/doc.go new file mode 100644 index 0000000000..6bad3061f9 --- /dev/null +++ b/share/p2p/shrexeds/doc.go @@ -0,0 +1,51 @@ +// This package defines a protocol that is used to request +// extended data squares from peers in the network. +// +// This protocol is a request/response protocol that allows for sending requests for extended data squares by data root +// to the peers in the network and receiving a response containing the original data square(s), which is used +// to recompute the extended data square. +// +// The streams are established using the protocol ID: +// +// - "{networkID}/shrex/eds/v0.0.1" where networkID is the network ID of the network. (e.g. "arabica") +// +// When a peer receives a request for extended data squares, it will read +// the original data square from the EDS store by retrieving the underlying +// CARv1 file containing the full extended data square, but will limit reading +// to the original data square shares only. +// The client on the other hand will take care of computing the extended data squares from +// the original data square on receipt. +// +// # Usage +// +// To use a shrexeds client to request extended data squares from a peer, you must +// first create a new `shrexeds.Client` instance by: +// +// client, err := shrexeds.NewClient(params, host) +// +// where `params` is a `shrexeds.Parameters` instance and `host` is a `libp2p.Host` instance. +// +// To request extended data squares from a peer, you must first create a `Client.RequestEDS` instance by: +// +// eds, err := client.RequestEDS(ctx, dataHash, peer) +// +// where: +// - `ctx` is a `context.Context` instance, +// - `dataHash` is the data root of the extended data square and +// - `peer` is the peer ID of the peer to request the extended data square from. +// +// To use a shrexeds server to respond to requests for extended data squares from peers +// you must first create a new `shrexeds.Server` instance by: +// +// server, err := shrexeds.NewServer(params, host, store) +// +// where `params` is a [Parameters] instance, `host` is a libp2p.Host instance and `store` is a [eds.Store] instance. +// +// To start the server, you must call `Start` on the server: +// +// err := server.Start(ctx) +// +// To stop the server, you must call `Stop` on the server: +// +// err := server.Stop(ctx) +package shrexeds diff --git a/share/p2p/shrexeds/exchange_test.go b/share/p2p/shrexeds/exchange_test.go index b0e11e3587..9155be6dec 100644 --- a/share/p2p/shrexeds/exchange_test.go +++ b/share/p2p/shrexeds/exchange_test.go @@ -14,10 +14,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/celestiaorg/celestia-app/pkg/da" - "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/edstest" "github.com/celestiaorg/celestia-node/share/p2p" ) @@ -34,8 +33,9 @@ func TestExchange_RequestEDS(t *testing.T) { // Testcase: EDS is immediately available t.Run("EDS_Available", func(t *testing.T) { - eds := share.RandEDS(t, 4) - dah := da.NewDataAvailabilityHeader(eds) + eds := edstest.RandEDS(t, 4) + dah, err := share.NewRoot(eds) + require.NoError(t, err) err = store.Put(ctx, dah.Hash(), eds) require.NoError(t, err) @@ -46,20 +46,27 @@ func TestExchange_RequestEDS(t *testing.T) { // Testcase: EDS is unavailable initially, but is found after multiple requests t.Run("EDS_AvailableAfterDelay", func(t *testing.T) { - storageDelay := time.Second - eds := share.RandEDS(t, 4) - dah := da.NewDataAvailabilityHeader(eds) + eds := edstest.RandEDS(t, 4) + dah, err := share.NewRoot(eds) + require.NoError(t, err) + + lock := make(chan struct{}) go func() { - time.Sleep(storageDelay) + <-lock err = store.Put(ctx, dah.Hash(), eds) - // require.NoError(t, err) + require.NoError(t, err) + lock <- struct{}{} }() requestedEDS, err := client.RequestEDS(ctx, dah.Hash(), server.host.ID()) assert.ErrorIs(t, err, p2p.ErrNotFound) assert.Nil(t, requestedEDS) - time.Sleep(storageDelay * 2) + // unlock write + lock <- struct{}{} + // wait for write to finish + <-lock + requestedEDS, err = client.RequestEDS(ctx, dah.Hash(), server.host.ID()) assert.NoError(t, err) assert.Equal(t, eds.Flattened(), requestedEDS.Flattened()) @@ -76,9 +83,10 @@ func TestExchange_RequestEDS(t *testing.T) { t.Run("EDS_err_not_found", func(t *testing.T) { timeoutCtx, cancel := context.WithTimeout(ctx, time.Second) t.Cleanup(cancel) - eds := share.RandEDS(t, 4) - dah := da.NewDataAvailabilityHeader(eds) - _, err := client.RequestEDS(timeoutCtx, dah.Hash(), server.host.ID()) + eds := edstest.RandEDS(t, 4) + dah, err := share.NewRoot(eds) + require.NoError(t, err) + _, err = client.RequestEDS(timeoutCtx, dah.Hash(), server.host.ID()) require.ErrorIs(t, err, p2p.ErrNotFound) }) @@ -128,9 +136,9 @@ func TestExchange_RequestEDS(t *testing.T) { func newStore(t *testing.T) *eds.Store { t.Helper() - tmpDir := t.TempDir() + storeCfg := eds.DefaultParameters() ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - store, err := eds.NewStore(tmpDir, ds) + store, err := eds.NewStore(storeCfg, t.TempDir(), ds) require.NoError(t, err) return store } diff --git a/share/p2p/shrexeds/server.go b/share/p2p/shrexeds/server.go index 60e4198842..11b99a3438 100644 --- a/share/p2p/shrexeds/server.go +++ b/share/p2p/shrexeds/server.go @@ -70,7 +70,7 @@ func (s *Server) observeRateLimitedRequests() { } func (s *Server) handleStream(stream network.Stream) { - logger := log.With("peer", stream.Conn().RemotePeer()) + logger := log.With("peer", stream.Conn().RemotePeer().String()) logger.Debug("server: handling eds request") s.observeRateLimitedRequests() @@ -91,7 +91,7 @@ func (s *Server) handleStream(stream network.Stream) { stream.Reset() //nolint:errcheck return } - logger = logger.With("hash", hash) + logger = logger.With("hash", hash.String()) ctx, cancel := context.WithTimeout(s.ctx, s.params.HandleRequestTimeout) defer cancel() @@ -100,8 +100,15 @@ func (s *Server) handleStream(stream network.Stream) { // we do not close the reader, so that other requests will not need to re-open the file. // closing is handled by the LRU cache. edsReader, err := s.store.GetCAR(ctx, hash) - status := p2p_pb.Status_OK + var status p2p_pb.Status switch { + case err == nil: + defer func() { + if err := edsReader.Close(); err != nil { + log.Warnw("closing car reader", "err", err) + } + }() + status = p2p_pb.Status_OK case errors.Is(err, eds.ErrNotFound): logger.Warnw("server: request hash not found") s.metrics.ObserveRequests(ctx, 1, p2p.StatusNotFound) diff --git a/share/p2p/shrexnd/client.go b/share/p2p/shrexnd/client.go index ab407126de..86c5150095 100644 --- a/share/p2p/shrexnd/client.go +++ b/share/p2p/shrexnd/client.go @@ -15,10 +15,8 @@ import ( "github.com/celestiaorg/go-libp2p-messenger/serde" "github.com/celestiaorg/nmt" - "github.com/celestiaorg/nmt/namespace" "github.com/celestiaorg/celestia-node/share" - "github.com/celestiaorg/celestia-node/share/ipld" "github.com/celestiaorg/celestia-node/share/p2p" pb "github.com/celestiaorg/celestia-node/share/p2p/shrexnd/pb" ) @@ -47,16 +45,20 @@ func NewClient(params *Parameters, host host.Host) (*Client, error) { } // RequestND requests namespaced data from the given peer. -// Returns valid data with its verified inclusion against the share.Root. +// Returns NamespacedShares with unverified inclusion proofs against the share.Root. func (c *Client) RequestND( ctx context.Context, root *share.Root, - nID namespace.ID, + namespace share.Namespace, peer peer.ID, ) (share.NamespacedShares, error) { - shares, err := c.doRequest(ctx, root, nID, peer) + if err := namespace.ValidateForData(); err != nil { + return nil, err + } + + shares, err := c.doRequest(ctx, root, namespace, peer) if err == nil { - return shares, err + return shares, nil } if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { c.metrics.ObserveRequests(ctx, 1, p2p.StatusTimeout) @@ -71,7 +73,7 @@ func (c *Client) RequestND( return nil, context.DeadlineExceeded } } - if err != p2p.ErrNotFound && err != share.ErrNamespaceNotFound { + if err != p2p.ErrNotFound && err != p2p.ErrRateLimited { log.Warnw("client-nd: peer returned err", "err", err) } return nil, err @@ -80,7 +82,7 @@ func (c *Client) RequestND( func (c *Client) doRequest( ctx context.Context, root *share.Root, - nID namespace.ID, + namespace share.Namespace, peerID peer.ID, ) (share.NamespacedShares, error) { stream, err := c.host.NewStream(ctx, peerID, c.protocolID) @@ -92,12 +94,13 @@ func (c *Client) doRequest( c.setStreamDeadlines(ctx, stream) req := &pb.GetSharesByNamespaceRequest{ - RootHash: root.Hash(), - NamespaceId: nID, + RootHash: root.Hash(), + Namespace: namespace, } _, err = serde.Write(stream, req) if err != nil { + c.metrics.ObserveRequests(ctx, 1, p2p.StatusSendReqErr) stream.Reset() //nolint:errcheck return nil, fmt.Errorf("client-nd: writing request: %w", err) } @@ -107,50 +110,70 @@ func (c *Client) doRequest( log.Debugw("client-nd: closing write side of the stream", "err", err) } - var resp pb.GetSharesByNamespaceResponse - _, err = serde.Read(stream, &resp) + if err := c.readStatus(ctx, stream); err != nil { + return nil, err + } + return c.readNamespacedShares(ctx, stream) +} + +func (c *Client) readStatus(ctx context.Context, stream network.Stream) error { + var resp pb.GetSharesByNamespaceStatusResponse + _, err := serde.Read(stream, &resp) if err != nil { // server is overloaded and closed the stream if errors.Is(err, io.EOF) { c.metrics.ObserveRequests(ctx, 1, p2p.StatusRateLimited) - return nil, p2p.ErrNotFound + return p2p.ErrRateLimited } + c.metrics.ObserveRequests(ctx, 1, p2p.StatusReadRespErr) stream.Reset() //nolint:errcheck - return nil, fmt.Errorf("client-nd: reading response: %w", err) - } - - if err = c.statusToErr(ctx, resp.Status); err != nil { - return nil, fmt.Errorf("client-nd: response code is not OK: %w", err) + return fmt.Errorf("client-nd: reading status response: %w", err) } - shares, err := convertToNamespacedShares(resp.Rows) - if err != nil { - return nil, fmt.Errorf("client-nd: converting response to shares: %w", err) - } - return shares, nil + return c.convertStatusToErr(ctx, resp.Status) } -// convertToNamespacedShares converts proto Rows to share.NamespacedShares -func convertToNamespacedShares(rows []*pb.Row) (share.NamespacedShares, error) { - shares := make([]share.NamespacedRow, 0, len(rows)) - for _, row := range rows { - var proof *nmt.Proof +// readNamespacedShares converts proto Rows to share.NamespacedShares +func (c *Client) readNamespacedShares( + ctx context.Context, + stream network.Stream, +) (share.NamespacedShares, error) { + var shares share.NamespacedShares + for { + var row pb.NamespaceRowResponse + _, err := serde.Read(stream, &row) + if err != nil { + if errors.Is(err, io.EOF) { + // all data is received and steam is closed by server + return shares, nil + } + c.metrics.ObserveRequests(ctx, 1, p2p.StatusReadRespErr) + return nil, err + } + var proof nmt.Proof if row.Proof != nil { - tmpProof := nmt.NewInclusionProof( - int(row.Proof.Start), - int(row.Proof.End), - row.Proof.Nodes, - ipld.NMTIgnoreMaxNamespace, - ) - proof = &tmpProof + if len(row.Shares) != 0 { + proof = nmt.NewInclusionProof( + int(row.Proof.Start), + int(row.Proof.End), + row.Proof.Nodes, + row.Proof.IsMaxNamespaceIgnored, + ) + } else { + proof = nmt.NewAbsenceProof( + int(row.Proof.Start), + int(row.Proof.End), + row.Proof.Nodes, + row.Proof.LeafHash, + row.Proof.IsMaxNamespaceIgnored, + ) + } } - shares = append(shares, share.NamespacedRow{ Shares: row.Shares, - Proof: proof, + Proof: &proof, }) } - return shares, nil } func (c *Client) setStreamDeadlines(ctx context.Context, stream network.Stream) { @@ -181,18 +204,16 @@ func (c *Client) setStreamDeadlines(ctx context.Context, stream network.Stream) } } -func (c *Client) statusToErr(ctx context.Context, code pb.StatusCode) error { - switch code { +func (c *Client) convertStatusToErr(ctx context.Context, status pb.StatusCode) error { + switch status { case pb.StatusCode_OK: c.metrics.ObserveRequests(ctx, 1, p2p.StatusSuccess) return nil case pb.StatusCode_NOT_FOUND: c.metrics.ObserveRequests(ctx, 1, p2p.StatusNotFound) return p2p.ErrNotFound - case pb.StatusCode_NAMESPACE_NOT_FOUND: - return share.ErrNamespaceNotFound case pb.StatusCode_INVALID: - log.Debug("client-nd: invalid request") + log.Warn("client-nd: invalid request") fallthrough case pb.StatusCode_INTERNAL: fallthrough diff --git a/share/p2p/shrexnd/doc.go b/share/p2p/shrexnd/doc.go new file mode 100644 index 0000000000..74ba7397e8 --- /dev/null +++ b/share/p2p/shrexnd/doc.go @@ -0,0 +1,43 @@ +// This package defines a protocol that is used to request namespaced data from peers in the network. +// +// This protocol is a request/response protocol that sends a request for specific data that +// lives in a specific namespace ID and receives a response with the data. +// +// The streams are established using the protocol ID: +// +// - "{networkID}/shrex/nd/0.0.1" where networkID is the network ID of the network. (e.g. "arabica") +// +// The protocol uses protobuf to serialize and deserialize messages. +// +// # Usage +// +// To use a shrexnd client to request data from a peer, you must first create a new `shrexnd.Client` instance by: +// +// 1. Create a new client using `NewClient` and pass in the parameters of the protocol and the host: +// +// client, err := shrexnd.NewClient(params, host) +// +// 2. Request data from a peer by calling [Client.RequestND] on the client and +// pass in the context, the data root, the namespace ID and the peer ID: +// +// data, err := client.RequestND(ctx, dataRoot, peerID, namespaceID) +// +// where data is of type [share.NamespacedShares] +// +// To use a shrexnd server to respond to requests from peers, you must first create a new `shrexnd.Server` instance by: +// +// 1. Create a new server using `NewServer` and pass in the parameters of +// the protocol, the host, the store and store share getter: +// +// server, err := shrexnd.NewServer(params, host, store, storeShareGetter) +// +// where store is of type [share.Store] and storeShareGetter is of type [share.Getter] +// +// 2. Start the server by calling `Start` on the server: +// +// err := server.Start(ctx) +// +// 3. Stop the server by calling `Stop` on the server: +// +// err := server.Stop(ctx) +package shrexnd diff --git a/share/p2p/shrexnd/exchange_test.go b/share/p2p/shrexnd/exchange_test.go index 944b763229..cb8bbe9d74 100644 --- a/share/p2p/shrexnd/exchange_test.go +++ b/share/p2p/shrexnd/exchange_test.go @@ -13,19 +13,17 @@ import ( mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/stretchr/testify/require" - "github.com/celestiaorg/celestia-app/pkg/da" - "github.com/celestiaorg/nmt/namespace" - "github.com/celestiaorg/rsmt2d" - "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" + "github.com/celestiaorg/celestia-node/share/eds/edstest" "github.com/celestiaorg/celestia-node/share/p2p" + "github.com/celestiaorg/celestia-node/share/sharetest" ) func TestExchange_RequestND_NotFound(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) t.Cleanup(cancel) - edsStore, client, server := makeExchange(t, notFoundGetter{}) + edsStore, client, server := makeExchange(t) require.NoError(t, edsStore.Start(ctx)) require.NoError(t, server.Start(ctx)) @@ -34,8 +32,8 @@ func TestExchange_RequestND_NotFound(t *testing.T) { t.Cleanup(cancel) root := share.Root{} - nID := make([]byte, 8) - _, err := client.RequestND(ctx, &root, nID, server.host.ID()) + namespace := sharetest.RandV0Namespace() + _, err := client.RequestND(ctx, &root, namespace, server.host.ID()) require.ErrorIs(t, err, p2p.ErrNotFound) }) @@ -43,13 +41,15 @@ func TestExchange_RequestND_NotFound(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) t.Cleanup(cancel) - eds := share.RandEDS(t, 4) - dah := da.NewDataAvailabilityHeader(eds) + eds := edstest.RandEDS(t, 4) + dah, err := share.NewRoot(eds) + require.NoError(t, err) require.NoError(t, edsStore.Put(ctx, dah.Hash(), eds)) - randNID := dah.RowsRoots[(len(dah.RowsRoots)-1)/2][:8] - _, err := client.RequestND(ctx, &dah, randNID, server.host.ID()) - require.ErrorIs(t, err, share.ErrNamespaceNotFound) + namespace := sharetest.RandV0Namespace() + emptyShares, err := client.RequestND(ctx, dah, namespace, server.host.ID()) + require.NoError(t, err) + require.Empty(t, emptyShares.Flatten()) }) } @@ -60,7 +60,7 @@ func TestExchange_RequestND(t *testing.T) { client, err := NewClient(DefaultParameters(), net.Hosts()[0]) require.NoError(t, err) - server, err := NewServer(DefaultParameters(), net.Hosts()[1], nil, nil) + server, err := NewServer(DefaultParameters(), net.Hosts()[1], nil) require.NoError(t, err) require.NoError(t, server.Start(context.Background())) @@ -90,43 +90,23 @@ func TestExchange_RequestND(t *testing.T) { // take server concurrency slots with blocked requests for i := 0; i < rateLimit; i++ { go func(i int) { - client.RequestND(ctx, nil, nil, server.host.ID()) //nolint:errcheck + client.RequestND(ctx, nil, sharetest.RandV0Namespace(), server.host.ID()) //nolint:errcheck }(i) } // wait until all server slots are taken wg.Wait() - _, err = client.RequestND(ctx, nil, nil, server.host.ID()) - require.ErrorIs(t, err, p2p.ErrNotFound) + _, err = client.RequestND(ctx, nil, sharetest.RandV0Namespace(), server.host.ID()) + require.ErrorIs(t, err, p2p.ErrRateLimited) }) } -type notFoundGetter struct{} - -func (m notFoundGetter) GetShare( - _ context.Context, _ *share.Root, _, _ int, -) (share.Share, error) { - return nil, share.ErrNotFound -} - -func (m notFoundGetter) GetEDS( - _ context.Context, _ *share.Root, -) (*rsmt2d.ExtendedDataSquare, error) { - return nil, share.ErrNotFound -} - -func (m notFoundGetter) GetSharesByNamespace( - _ context.Context, _ *share.Root, _ namespace.ID, -) (share.NamespacedShares, error) { - return nil, share.ErrNamespaceNotFound -} - func newStore(t *testing.T) *eds.Store { t.Helper() - tmpDir := t.TempDir() + storeCfg := eds.DefaultParameters() ds := ds_sync.MutexWrap(datastore.NewMapDatastore()) - store, err := eds.NewStore(tmpDir, ds) + store, err := eds.NewStore(storeCfg, t.TempDir(), ds) require.NoError(t, err) return store } @@ -140,14 +120,14 @@ func createMocknet(t *testing.T, amount int) []libhost.Host { return net.Hosts() } -func makeExchange(t *testing.T, getter share.Getter) (*eds.Store, *Client, *Server) { +func makeExchange(t *testing.T) (*eds.Store, *Client, *Server) { t.Helper() store := newStore(t) hosts := createMocknet(t, 2) client, err := NewClient(DefaultParameters(), hosts[0]) require.NoError(t, err) - server, err := NewServer(DefaultParameters(), hosts[1], store, getter) + server, err := NewServer(DefaultParameters(), hosts[1], store) require.NoError(t, err) return store, client, server diff --git a/share/p2p/shrexnd/params.go b/share/p2p/shrexnd/params.go index a645267962..8489627a07 100644 --- a/share/p2p/shrexnd/params.go +++ b/share/p2p/shrexnd/params.go @@ -8,7 +8,7 @@ import ( "github.com/celestiaorg/celestia-node/share/p2p" ) -const protocolString = "/shrex/nd/v0.0.2" +const protocolString = "/shrex/nd/v0.0.3" var log = logging.Logger("shrex/nd") diff --git a/share/p2p/shrexnd/pb/share.pb.go b/share/p2p/shrexnd/pb/share.pb.go index d902570410..7e3c11416f 100644 --- a/share/p2p/shrexnd/pb/share.pb.go +++ b/share/p2p/shrexnd/pb/share.pb.go @@ -5,6 +5,7 @@ package share_p2p_shrex_nd import ( fmt "fmt" + pb "github.com/celestiaorg/nmt/pb" proto "github.com/gogo/protobuf/proto" io "io" math "math" @@ -25,11 +26,10 @@ const _ = proto.GoGoProtoPackageIsVersion3 // please upgrade the proto package type StatusCode int32 const ( - StatusCode_INVALID StatusCode = 0 - StatusCode_OK StatusCode = 1 - StatusCode_NOT_FOUND StatusCode = 2 - StatusCode_INTERNAL StatusCode = 3 - StatusCode_NAMESPACE_NOT_FOUND StatusCode = 4 + StatusCode_INVALID StatusCode = 0 + StatusCode_OK StatusCode = 1 + StatusCode_NOT_FOUND StatusCode = 2 + StatusCode_INTERNAL StatusCode = 3 ) var StatusCode_name = map[int32]string{ @@ -37,15 +37,13 @@ var StatusCode_name = map[int32]string{ 1: "OK", 2: "NOT_FOUND", 3: "INTERNAL", - 4: "NAMESPACE_NOT_FOUND", } var StatusCode_value = map[string]int32{ - "INVALID": 0, - "OK": 1, - "NOT_FOUND": 2, - "INTERNAL": 3, - "NAMESPACE_NOT_FOUND": 4, + "INVALID": 0, + "OK": 1, + "NOT_FOUND": 2, + "INTERNAL": 3, } func (x StatusCode) String() string { @@ -57,8 +55,8 @@ func (StatusCode) EnumDescriptor() ([]byte, []int) { } type GetSharesByNamespaceRequest struct { - RootHash []byte `protobuf:"bytes,1,opt,name=root_hash,json=rootHash,proto3" json:"root_hash,omitempty"` - NamespaceId []byte `protobuf:"bytes,2,opt,name=namespace_id,json=namespaceId,proto3" json:"namespace_id,omitempty"` + RootHash []byte `protobuf:"bytes,1,opt,name=root_hash,json=rootHash,proto3" json:"root_hash,omitempty"` + Namespace []byte `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` } func (m *GetSharesByNamespaceRequest) Reset() { *m = GetSharesByNamespaceRequest{} } @@ -101,30 +99,29 @@ func (m *GetSharesByNamespaceRequest) GetRootHash() []byte { return nil } -func (m *GetSharesByNamespaceRequest) GetNamespaceId() []byte { +func (m *GetSharesByNamespaceRequest) GetNamespace() []byte { if m != nil { - return m.NamespaceId + return m.Namespace } return nil } -type GetSharesByNamespaceResponse struct { +type GetSharesByNamespaceStatusResponse struct { Status StatusCode `protobuf:"varint,1,opt,name=status,proto3,enum=share.p2p.shrex.nd.StatusCode" json:"status,omitempty"` - Rows []*Row `protobuf:"bytes,2,rep,name=rows,proto3" json:"rows,omitempty"` } -func (m *GetSharesByNamespaceResponse) Reset() { *m = GetSharesByNamespaceResponse{} } -func (m *GetSharesByNamespaceResponse) String() string { return proto.CompactTextString(m) } -func (*GetSharesByNamespaceResponse) ProtoMessage() {} -func (*GetSharesByNamespaceResponse) Descriptor() ([]byte, []int) { +func (m *GetSharesByNamespaceStatusResponse) Reset() { *m = GetSharesByNamespaceStatusResponse{} } +func (m *GetSharesByNamespaceStatusResponse) String() string { return proto.CompactTextString(m) } +func (*GetSharesByNamespaceStatusResponse) ProtoMessage() {} +func (*GetSharesByNamespaceStatusResponse) Descriptor() ([]byte, []int) { return fileDescriptor_ed9f13149b0de397, []int{1} } -func (m *GetSharesByNamespaceResponse) XXX_Unmarshal(b []byte) error { +func (m *GetSharesByNamespaceStatusResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } -func (m *GetSharesByNamespaceResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { +func (m *GetSharesByNamespaceStatusResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { - return xxx_messageInfo_GetSharesByNamespaceResponse.Marshal(b, m, deterministic) + return xxx_messageInfo_GetSharesByNamespaceStatusResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) @@ -134,49 +131,42 @@ func (m *GetSharesByNamespaceResponse) XXX_Marshal(b []byte, deterministic bool) return b[:n], nil } } -func (m *GetSharesByNamespaceResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_GetSharesByNamespaceResponse.Merge(m, src) +func (m *GetSharesByNamespaceStatusResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSharesByNamespaceStatusResponse.Merge(m, src) } -func (m *GetSharesByNamespaceResponse) XXX_Size() int { +func (m *GetSharesByNamespaceStatusResponse) XXX_Size() int { return m.Size() } -func (m *GetSharesByNamespaceResponse) XXX_DiscardUnknown() { - xxx_messageInfo_GetSharesByNamespaceResponse.DiscardUnknown(m) +func (m *GetSharesByNamespaceStatusResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetSharesByNamespaceStatusResponse.DiscardUnknown(m) } -var xxx_messageInfo_GetSharesByNamespaceResponse proto.InternalMessageInfo +var xxx_messageInfo_GetSharesByNamespaceStatusResponse proto.InternalMessageInfo -func (m *GetSharesByNamespaceResponse) GetStatus() StatusCode { +func (m *GetSharesByNamespaceStatusResponse) GetStatus() StatusCode { if m != nil { return m.Status } return StatusCode_INVALID } -func (m *GetSharesByNamespaceResponse) GetRows() []*Row { - if m != nil { - return m.Rows - } - return nil -} - -type Row struct { - Shares [][]byte `protobuf:"bytes,1,rep,name=shares,proto3" json:"shares,omitempty"` - Proof *Proof `protobuf:"bytes,2,opt,name=proof,proto3" json:"proof,omitempty"` +type NamespaceRowResponse struct { + Shares [][]byte `protobuf:"bytes,1,rep,name=shares,proto3" json:"shares,omitempty"` + Proof *pb.Proof `protobuf:"bytes,2,opt,name=proof,proto3" json:"proof,omitempty"` } -func (m *Row) Reset() { *m = Row{} } -func (m *Row) String() string { return proto.CompactTextString(m) } -func (*Row) ProtoMessage() {} -func (*Row) Descriptor() ([]byte, []int) { +func (m *NamespaceRowResponse) Reset() { *m = NamespaceRowResponse{} } +func (m *NamespaceRowResponse) String() string { return proto.CompactTextString(m) } +func (*NamespaceRowResponse) ProtoMessage() {} +func (*NamespaceRowResponse) Descriptor() ([]byte, []int) { return fileDescriptor_ed9f13149b0de397, []int{2} } -func (m *Row) XXX_Unmarshal(b []byte) error { +func (m *NamespaceRowResponse) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) } -func (m *Row) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { +func (m *NamespaceRowResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { if deterministic { - return xxx_messageInfo_Row.Marshal(b, m, deterministic) + return xxx_messageInfo_NamespaceRowResponse.Marshal(b, m, deterministic) } else { b = b[:cap(b)] n, err := m.MarshalToSizedBuffer(b) @@ -186,129 +176,64 @@ func (m *Row) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return b[:n], nil } } -func (m *Row) XXX_Merge(src proto.Message) { - xxx_messageInfo_Row.Merge(m, src) +func (m *NamespaceRowResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_NamespaceRowResponse.Merge(m, src) } -func (m *Row) XXX_Size() int { +func (m *NamespaceRowResponse) XXX_Size() int { return m.Size() } -func (m *Row) XXX_DiscardUnknown() { - xxx_messageInfo_Row.DiscardUnknown(m) +func (m *NamespaceRowResponse) XXX_DiscardUnknown() { + xxx_messageInfo_NamespaceRowResponse.DiscardUnknown(m) } -var xxx_messageInfo_Row proto.InternalMessageInfo +var xxx_messageInfo_NamespaceRowResponse proto.InternalMessageInfo -func (m *Row) GetShares() [][]byte { +func (m *NamespaceRowResponse) GetShares() [][]byte { if m != nil { return m.Shares } return nil } -func (m *Row) GetProof() *Proof { +func (m *NamespaceRowResponse) GetProof() *pb.Proof { if m != nil { return m.Proof } return nil } -type Proof struct { - Start int64 `protobuf:"varint,1,opt,name=start,proto3" json:"start,omitempty"` - End int64 `protobuf:"varint,2,opt,name=end,proto3" json:"end,omitempty"` - Nodes [][]byte `protobuf:"bytes,3,rep,name=Nodes,proto3" json:"Nodes,omitempty"` -} - -func (m *Proof) Reset() { *m = Proof{} } -func (m *Proof) String() string { return proto.CompactTextString(m) } -func (*Proof) ProtoMessage() {} -func (*Proof) Descriptor() ([]byte, []int) { - return fileDescriptor_ed9f13149b0de397, []int{3} -} -func (m *Proof) XXX_Unmarshal(b []byte) error { - return m.Unmarshal(b) -} -func (m *Proof) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - if deterministic { - return xxx_messageInfo_Proof.Marshal(b, m, deterministic) - } else { - b = b[:cap(b)] - n, err := m.MarshalToSizedBuffer(b) - if err != nil { - return nil, err - } - return b[:n], nil - } -} -func (m *Proof) XXX_Merge(src proto.Message) { - xxx_messageInfo_Proof.Merge(m, src) -} -func (m *Proof) XXX_Size() int { - return m.Size() -} -func (m *Proof) XXX_DiscardUnknown() { - xxx_messageInfo_Proof.DiscardUnknown(m) -} - -var xxx_messageInfo_Proof proto.InternalMessageInfo - -func (m *Proof) GetStart() int64 { - if m != nil { - return m.Start - } - return 0 -} - -func (m *Proof) GetEnd() int64 { - if m != nil { - return m.End - } - return 0 -} - -func (m *Proof) GetNodes() [][]byte { - if m != nil { - return m.Nodes - } - return nil -} - func init() { proto.RegisterEnum("share.p2p.shrex.nd.StatusCode", StatusCode_name, StatusCode_value) proto.RegisterType((*GetSharesByNamespaceRequest)(nil), "share.p2p.shrex.nd.GetSharesByNamespaceRequest") - proto.RegisterType((*GetSharesByNamespaceResponse)(nil), "share.p2p.shrex.nd.GetSharesByNamespaceResponse") - proto.RegisterType((*Row)(nil), "share.p2p.shrex.nd.Row") - proto.RegisterType((*Proof)(nil), "share.p2p.shrex.nd.Proof") + proto.RegisterType((*GetSharesByNamespaceStatusResponse)(nil), "share.p2p.shrex.nd.GetSharesByNamespaceStatusResponse") + proto.RegisterType((*NamespaceRowResponse)(nil), "share.p2p.shrex.nd.NamespaceRowResponse") } func init() { proto.RegisterFile("share/p2p/shrexnd/pb/share.proto", fileDescriptor_ed9f13149b0de397) } var fileDescriptor_ed9f13149b0de397 = []byte{ - // 386 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x92, 0xc1, 0xae, 0x93, 0x40, - 0x14, 0x86, 0x81, 0xb9, 0xc5, 0x7b, 0x0f, 0x68, 0xc8, 0x68, 0xbc, 0x98, 0x6b, 0x48, 0x65, 0xd5, - 0x68, 0x02, 0x09, 0x26, 0xee, 0x69, 0x8b, 0x4a, 0xac, 0xd3, 0x66, 0x5a, 0x75, 0x65, 0x08, 0x95, - 0x31, 0xb8, 0x90, 0x19, 0x99, 0x69, 0xaa, 0x6b, 0x5f, 0xc0, 0xc7, 0x72, 0xd9, 0xa5, 0x4b, 0xd3, - 0xbe, 0x88, 0x61, 0xa8, 0x76, 0x61, 0x77, 0xfc, 0xff, 0xf9, 0xce, 0x7f, 0xce, 0x21, 0x03, 0x43, - 0x59, 0x97, 0x2d, 0x8b, 0x45, 0x22, 0x62, 0x59, 0xb7, 0xec, 0x6b, 0x53, 0xc5, 0x62, 0x1d, 0x6b, - 0x33, 0x12, 0x2d, 0x57, 0x1c, 0xe3, 0xa3, 0x48, 0x44, 0xa4, 0x89, 0xa8, 0xa9, 0xc2, 0xf7, 0x70, - 0xf3, 0x82, 0xa9, 0x65, 0x57, 0x90, 0xe3, 0x6f, 0xa4, 0xfc, 0xcc, 0xa4, 0x28, 0x3f, 0x30, 0xca, - 0xbe, 0x6c, 0x98, 0x54, 0xf8, 0x06, 0xae, 0x5a, 0xce, 0x55, 0x51, 0x97, 0xb2, 0xf6, 0xcd, 0xa1, - 0x39, 0x72, 0xe9, 0x65, 0x67, 0xbc, 0x2c, 0x65, 0x8d, 0x1f, 0x81, 0xdb, 0xfc, 0x6d, 0x28, 0x3e, - 0x55, 0xbe, 0xa5, 0xeb, 0xce, 0x3f, 0x2f, 0xaf, 0xc2, 0xef, 0x26, 0x3c, 0x3c, 0x9f, 0x2f, 0x05, - 0x6f, 0x24, 0xc3, 0xcf, 0xc0, 0x96, 0xaa, 0x54, 0x1b, 0xa9, 0xd3, 0xef, 0x24, 0x41, 0xf4, 0xff, - 0x92, 0xd1, 0x52, 0x13, 0x13, 0x5e, 0x31, 0x7a, 0xa4, 0xf1, 0x13, 0xb8, 0x68, 0xf9, 0x56, 0xfa, - 0xd6, 0x10, 0x8d, 0x9c, 0xe4, 0xfa, 0x5c, 0x17, 0xe5, 0x5b, 0xaa, 0xa1, 0x90, 0x00, 0xa2, 0x7c, - 0x8b, 0xef, 0x83, 0xad, 0xb1, 0x6e, 0x16, 0x1a, 0xb9, 0xf4, 0xa8, 0x70, 0x0c, 0x03, 0xd1, 0x72, - 0xfe, 0x51, 0x1f, 0xe0, 0x24, 0x0f, 0xce, 0x85, 0x2d, 0x3a, 0x80, 0xf6, 0x5c, 0x98, 0xc1, 0x40, - 0x6b, 0x7c, 0x0f, 0x06, 0x52, 0x95, 0xad, 0xd2, 0xcb, 0x23, 0xda, 0x0b, 0xec, 0x01, 0x62, 0x4d, - 0xff, 0x3b, 0x10, 0xed, 0x3e, 0x3b, 0x8e, 0xf0, 0x8a, 0x49, 0x1f, 0xe9, 0xc1, 0xbd, 0x78, 0xfc, - 0x0e, 0xe0, 0x74, 0x19, 0x76, 0xe0, 0x56, 0x4e, 0xde, 0xa6, 0xb3, 0x7c, 0xea, 0x19, 0xd8, 0x06, - 0x6b, 0xfe, 0xca, 0x33, 0xf1, 0x6d, 0xb8, 0x22, 0xf3, 0x55, 0xf1, 0x7c, 0xfe, 0x86, 0x4c, 0x3d, - 0x0b, 0xbb, 0x70, 0x99, 0x93, 0x55, 0x46, 0x49, 0x3a, 0xf3, 0x10, 0xbe, 0x86, 0xbb, 0x24, 0x7d, - 0x9d, 0x2d, 0x17, 0xe9, 0x24, 0x2b, 0x4e, 0xd8, 0xc5, 0xd8, 0xff, 0xb9, 0x0f, 0xcc, 0xdd, 0x3e, - 0x30, 0x7f, 0xef, 0x03, 0xf3, 0xc7, 0x21, 0x30, 0x76, 0x87, 0xc0, 0xf8, 0x75, 0x08, 0x8c, 0xb5, - 0xad, 0x5f, 0xc2, 0xd3, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xba, 0xc1, 0x4e, 0xec, 0x2d, 0x02, - 0x00, 0x00, + // 326 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x90, 0x4f, 0x4b, 0xf3, 0x40, + 0x10, 0xc6, 0x93, 0x96, 0x37, 0x6f, 0x3b, 0xad, 0x35, 0x2c, 0x22, 0xc5, 0xca, 0x52, 0x02, 0x42, + 0xf1, 0xb0, 0x81, 0x08, 0x1e, 0x85, 0xd6, 0xfa, 0xa7, 0x58, 0x52, 0xd9, 0xb6, 0xe2, 0x41, 0x28, + 0x1b, 0xbb, 0x92, 0x8b, 0xd9, 0x35, 0xbb, 0x45, 0xfd, 0x16, 0x7e, 0x2c, 0x8f, 0x3d, 0x7a, 0x94, + 0xf6, 0x8b, 0x48, 0xb6, 0xd1, 0x1c, 0xf4, 0xb6, 0xf3, 0xcc, 0x33, 0xbf, 0x7d, 0x66, 0xa0, 0xad, + 0x62, 0x96, 0x72, 0x5f, 0x06, 0xd2, 0x57, 0x71, 0xca, 0x5f, 0x92, 0xb9, 0x2f, 0x23, 0xdf, 0x88, + 0x44, 0xa6, 0x42, 0x0b, 0x84, 0xf2, 0x22, 0x90, 0xc4, 0x38, 0x48, 0x32, 0xdf, 0x6b, 0xc8, 0xc8, + 0x97, 0xa9, 0x10, 0x0f, 0x1b, 0x8f, 0x77, 0x0b, 0xad, 0x0b, 0xae, 0xc7, 0x99, 0x51, 0xf5, 0x5e, + 0x43, 0xf6, 0xc8, 0x95, 0x64, 0xf7, 0x9c, 0xf2, 0xa7, 0x05, 0x57, 0x1a, 0xb5, 0xa0, 0x9a, 0x0a, + 0xa1, 0x67, 0x31, 0x53, 0x71, 0xd3, 0x6e, 0xdb, 0x9d, 0x3a, 0xad, 0x64, 0xc2, 0x25, 0x53, 0x31, + 0xda, 0x87, 0x6a, 0xf2, 0x3d, 0xd0, 0x2c, 0x99, 0x66, 0x21, 0x78, 0x77, 0xe0, 0xfd, 0x45, 0x1e, + 0x6b, 0xa6, 0x17, 0x8a, 0x72, 0x25, 0x45, 0xa2, 0x38, 0x3a, 0x06, 0x47, 0x19, 0xc5, 0xd0, 0x1b, + 0x01, 0x26, 0xbf, 0x43, 0x93, 0xcd, 0xcc, 0xa9, 0x98, 0x73, 0x9a, 0xbb, 0xbd, 0x29, 0xec, 0x14, + 0x61, 0xc5, 0xf3, 0x0f, 0x6f, 0x17, 0x1c, 0x03, 0xc8, 0x78, 0xe5, 0x4e, 0x9d, 0xe6, 0x15, 0x3a, + 0x80, 0x7f, 0x66, 0x6d, 0x93, 0xb3, 0x16, 0x6c, 0x93, 0xfc, 0x08, 0x11, 0xb9, 0xce, 0x1e, 0x74, + 0xd3, 0x3d, 0x3c, 0x01, 0x28, 0x3e, 0x43, 0x35, 0xf8, 0x3f, 0x08, 0x6f, 0xba, 0xc3, 0x41, 0xdf, + 0xb5, 0x90, 0x03, 0xa5, 0xd1, 0x95, 0x6b, 0xa3, 0x2d, 0xa8, 0x86, 0xa3, 0xc9, 0xec, 0x7c, 0x34, + 0x0d, 0xfb, 0x6e, 0x09, 0xd5, 0xa1, 0x32, 0x08, 0x27, 0x67, 0x34, 0xec, 0x0e, 0xdd, 0x72, 0xaf, + 0xf9, 0xbe, 0xc2, 0xf6, 0x72, 0x85, 0xed, 0xcf, 0x15, 0xb6, 0xdf, 0xd6, 0xd8, 0x5a, 0xae, 0xb1, + 0xf5, 0xb1, 0xc6, 0x56, 0xe4, 0x98, 0x7b, 0x1f, 0x7d, 0x05, 0x00, 0x00, 0xff, 0xff, 0x1a, 0x53, + 0xb4, 0x86, 0xb7, 0x01, 0x00, 0x00, } func (m *GetSharesByNamespaceRequest) Marshal() (dAtA []byte, err error) { @@ -331,10 +256,10 @@ func (m *GetSharesByNamespaceRequest) MarshalToSizedBuffer(dAtA []byte) (int, er _ = i var l int _ = l - if len(m.NamespaceId) > 0 { - i -= len(m.NamespaceId) - copy(dAtA[i:], m.NamespaceId) - i = encodeVarintShare(dAtA, i, uint64(len(m.NamespaceId))) + if len(m.Namespace) > 0 { + i -= len(m.Namespace) + copy(dAtA[i:], m.Namespace) + i = encodeVarintShare(dAtA, i, uint64(len(m.Namespace))) i-- dAtA[i] = 0x12 } @@ -348,7 +273,7 @@ func (m *GetSharesByNamespaceRequest) MarshalToSizedBuffer(dAtA []byte) (int, er return len(dAtA) - i, nil } -func (m *GetSharesByNamespaceResponse) Marshal() (dAtA []byte, err error) { +func (m *GetSharesByNamespaceStatusResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) @@ -358,30 +283,16 @@ func (m *GetSharesByNamespaceResponse) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *GetSharesByNamespaceResponse) MarshalTo(dAtA []byte) (int, error) { +func (m *GetSharesByNamespaceStatusResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *GetSharesByNamespaceResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { +func (m *GetSharesByNamespaceStatusResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int _ = l - if len(m.Rows) > 0 { - for iNdEx := len(m.Rows) - 1; iNdEx >= 0; iNdEx-- { - { - size, err := m.Rows[iNdEx].MarshalToSizedBuffer(dAtA[:i]) - if err != nil { - return 0, err - } - i -= size - i = encodeVarintShare(dAtA, i, uint64(size)) - } - i-- - dAtA[i] = 0x12 - } - } if m.Status != 0 { i = encodeVarintShare(dAtA, i, uint64(m.Status)) i-- @@ -390,7 +301,7 @@ func (m *GetSharesByNamespaceResponse) MarshalToSizedBuffer(dAtA []byte) (int, e return len(dAtA) - i, nil } -func (m *Row) Marshal() (dAtA []byte, err error) { +func (m *NamespaceRowResponse) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalToSizedBuffer(dAtA[:size]) @@ -400,12 +311,12 @@ func (m *Row) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *Row) MarshalTo(dAtA []byte) (int, error) { +func (m *NamespaceRowResponse) MarshalTo(dAtA []byte) (int, error) { size := m.Size() return m.MarshalToSizedBuffer(dAtA[:size]) } -func (m *Row) MarshalToSizedBuffer(dAtA []byte) (int, error) { +func (m *NamespaceRowResponse) MarshalToSizedBuffer(dAtA []byte) (int, error) { i := len(dAtA) _ = i var l int @@ -434,48 +345,6 @@ func (m *Row) MarshalToSizedBuffer(dAtA []byte) (int, error) { return len(dAtA) - i, nil } -func (m *Proof) Marshal() (dAtA []byte, err error) { - size := m.Size() - dAtA = make([]byte, size) - n, err := m.MarshalToSizedBuffer(dAtA[:size]) - if err != nil { - return nil, err - } - return dAtA[:n], nil -} - -func (m *Proof) MarshalTo(dAtA []byte) (int, error) { - size := m.Size() - return m.MarshalToSizedBuffer(dAtA[:size]) -} - -func (m *Proof) MarshalToSizedBuffer(dAtA []byte) (int, error) { - i := len(dAtA) - _ = i - var l int - _ = l - if len(m.Nodes) > 0 { - for iNdEx := len(m.Nodes) - 1; iNdEx >= 0; iNdEx-- { - i -= len(m.Nodes[iNdEx]) - copy(dAtA[i:], m.Nodes[iNdEx]) - i = encodeVarintShare(dAtA, i, uint64(len(m.Nodes[iNdEx]))) - i-- - dAtA[i] = 0x1a - } - } - if m.End != 0 { - i = encodeVarintShare(dAtA, i, uint64(m.End)) - i-- - dAtA[i] = 0x10 - } - if m.Start != 0 { - i = encodeVarintShare(dAtA, i, uint64(m.Start)) - i-- - dAtA[i] = 0x8 - } - return len(dAtA) - i, nil -} - func encodeVarintShare(dAtA []byte, offset int, v uint64) int { offset -= sovShare(v) base := offset @@ -497,14 +366,14 @@ func (m *GetSharesByNamespaceRequest) Size() (n int) { if l > 0 { n += 1 + l + sovShare(uint64(l)) } - l = len(m.NamespaceId) + l = len(m.Namespace) if l > 0 { n += 1 + l + sovShare(uint64(l)) } return n } -func (m *GetSharesByNamespaceResponse) Size() (n int) { +func (m *GetSharesByNamespaceStatusResponse) Size() (n int) { if m == nil { return 0 } @@ -513,16 +382,10 @@ func (m *GetSharesByNamespaceResponse) Size() (n int) { if m.Status != 0 { n += 1 + sovShare(uint64(m.Status)) } - if len(m.Rows) > 0 { - for _, e := range m.Rows { - l = e.Size() - n += 1 + l + sovShare(uint64(l)) - } - } return n } -func (m *Row) Size() (n int) { +func (m *NamespaceRowResponse) Size() (n int) { if m == nil { return 0 } @@ -541,27 +404,6 @@ func (m *Row) Size() (n int) { return n } -func (m *Proof) Size() (n int) { - if m == nil { - return 0 - } - var l int - _ = l - if m.Start != 0 { - n += 1 + sovShare(uint64(m.Start)) - } - if m.End != 0 { - n += 1 + sovShare(uint64(m.End)) - } - if len(m.Nodes) > 0 { - for _, b := range m.Nodes { - l = len(b) - n += 1 + l + sovShare(uint64(l)) - } - } - return n -} - func sovShare(x uint64) (n int) { return (math_bits.Len64(x|1) + 6) / 7 } @@ -633,7 +475,7 @@ func (m *GetSharesByNamespaceRequest) Unmarshal(dAtA []byte) error { iNdEx = postIndex case 2: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field NamespaceId", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Namespace", wireType) } var byteLen int for shift := uint(0); ; shift += 7 { @@ -660,9 +502,9 @@ func (m *GetSharesByNamespaceRequest) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.NamespaceId = append(m.NamespaceId[:0], dAtA[iNdEx:postIndex]...) - if m.NamespaceId == nil { - m.NamespaceId = []byte{} + m.Namespace = append(m.Namespace[:0], dAtA[iNdEx:postIndex]...) + if m.Namespace == nil { + m.Namespace = []byte{} } iNdEx = postIndex default: @@ -686,7 +528,7 @@ func (m *GetSharesByNamespaceRequest) Unmarshal(dAtA []byte) error { } return nil } -func (m *GetSharesByNamespaceResponse) Unmarshal(dAtA []byte) error { +func (m *GetSharesByNamespaceStatusResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -709,10 +551,10 @@ func (m *GetSharesByNamespaceResponse) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: GetSharesByNamespaceResponse: wiretype end group for non-group") + return fmt.Errorf("proto: GetSharesByNamespaceStatusResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: GetSharesByNamespaceResponse: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: GetSharesByNamespaceStatusResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -734,40 +576,6 @@ func (m *GetSharesByNamespaceResponse) Unmarshal(dAtA []byte) error { break } } - case 2: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Rows", wireType) - } - var msglen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - msglen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if msglen < 0 { - return ErrInvalidLengthShare - } - postIndex := iNdEx + msglen - if postIndex < 0 { - return ErrInvalidLengthShare - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Rows = append(m.Rows, &Row{}) - if err := m.Rows[len(m.Rows)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { - return err - } - iNdEx = postIndex default: iNdEx = preIndex skippy, err := skipShare(dAtA[iNdEx:]) @@ -789,7 +597,7 @@ func (m *GetSharesByNamespaceResponse) Unmarshal(dAtA []byte) error { } return nil } -func (m *Row) Unmarshal(dAtA []byte) error { +func (m *NamespaceRowResponse) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -812,10 +620,10 @@ func (m *Row) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: Row: wiretype end group for non-group") + return fmt.Errorf("proto: NamespaceRowResponse: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: Row: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: NamespaceRowResponse: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -880,7 +688,7 @@ func (m *Row) Unmarshal(dAtA []byte) error { return io.ErrUnexpectedEOF } if m.Proof == nil { - m.Proof = &Proof{} + m.Proof = &pb.Proof{} } if err := m.Proof.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err @@ -907,126 +715,6 @@ func (m *Row) Unmarshal(dAtA []byte) error { } return nil } -func (m *Proof) Unmarshal(dAtA []byte) error { - l := len(dAtA) - iNdEx := 0 - for iNdEx < l { - preIndex := iNdEx - var wire uint64 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - wire |= uint64(b&0x7F) << shift - if b < 0x80 { - break - } - } - fieldNum := int32(wire >> 3) - wireType := int(wire & 0x7) - if wireType == 4 { - return fmt.Errorf("proto: Proof: wiretype end group for non-group") - } - if fieldNum <= 0 { - return fmt.Errorf("proto: Proof: illegal tag %d (wire type %d)", fieldNum, wire) - } - switch fieldNum { - case 1: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field Start", wireType) - } - m.Start = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.Start |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 2: - if wireType != 0 { - return fmt.Errorf("proto: wrong wireType = %d for field End", wireType) - } - m.End = 0 - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - m.End |= int64(b&0x7F) << shift - if b < 0x80 { - break - } - } - case 3: - if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Nodes", wireType) - } - var byteLen int - for shift := uint(0); ; shift += 7 { - if shift >= 64 { - return ErrIntOverflowShare - } - if iNdEx >= l { - return io.ErrUnexpectedEOF - } - b := dAtA[iNdEx] - iNdEx++ - byteLen |= int(b&0x7F) << shift - if b < 0x80 { - break - } - } - if byteLen < 0 { - return ErrInvalidLengthShare - } - postIndex := iNdEx + byteLen - if postIndex < 0 { - return ErrInvalidLengthShare - } - if postIndex > l { - return io.ErrUnexpectedEOF - } - m.Nodes = append(m.Nodes, make([]byte, postIndex-iNdEx)) - copy(m.Nodes[len(m.Nodes)-1], dAtA[iNdEx:postIndex]) - iNdEx = postIndex - default: - iNdEx = preIndex - skippy, err := skipShare(dAtA[iNdEx:]) - if err != nil { - return err - } - if (skippy < 0) || (iNdEx+skippy) < 0 { - return ErrInvalidLengthShare - } - if (iNdEx + skippy) > l { - return io.ErrUnexpectedEOF - } - iNdEx += skippy - } - } - - if iNdEx > l { - return io.ErrUnexpectedEOF - } - return nil -} func skipShare(dAtA []byte) (n int, err error) { l := len(dAtA) iNdEx := 0 diff --git a/share/p2p/shrexnd/pb/share.proto b/share/p2p/shrexnd/pb/share.proto index 3d6a896641..a5bdbfa071 100644 --- a/share/p2p/shrexnd/pb/share.proto +++ b/share/p2p/shrexnd/pb/share.proto @@ -1,15 +1,15 @@ syntax = "proto3"; package share.p2p.shrex.nd; +import "pb/proof.proto"; message GetSharesByNamespaceRequest{ bytes root_hash = 1; - bytes namespace_id = 2; + bytes namespace = 2; } -message GetSharesByNamespaceResponse{ +message GetSharesByNamespaceStatusResponse{ StatusCode status = 1; - repeated Row rows = 2; } enum StatusCode { @@ -17,16 +17,9 @@ enum StatusCode { OK = 1; NOT_FOUND = 2; INTERNAL = 3; - NAMESPACE_NOT_FOUND = 4; }; -message Row { +message NamespaceRowResponse { repeated bytes shares = 1; - Proof proof = 2; -} - -message Proof { - int64 start = 1; - int64 end = 2; - repeated bytes Nodes = 3; + proof.pb.Proof proof = 2; } diff --git a/share/p2p/shrexnd/server.go b/share/p2p/shrexnd/server.go index 748d9fbd10..33e61ff472 100644 --- a/share/p2p/shrexnd/server.go +++ b/share/p2p/shrexnd/server.go @@ -2,6 +2,7 @@ package shrexnd import ( "context" + "crypto/sha256" "errors" "fmt" "time" @@ -9,14 +10,13 @@ import ( "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" "github.com/libp2p/go-libp2p/core/protocol" - "github.com/minio/sha256-simd" "go.uber.org/zap" "github.com/celestiaorg/go-libp2p-messenger/serde" + nmt_pb "github.com/celestiaorg/nmt/pb" "github.com/celestiaorg/celestia-node/share" "github.com/celestiaorg/celestia-node/share/eds" - "github.com/celestiaorg/celestia-node/share/ipld" "github.com/celestiaorg/celestia-node/share/p2p" pb "github.com/celestiaorg/celestia-node/share/p2p/shrexnd/pb" ) @@ -29,8 +29,8 @@ type Server struct { host host.Host protocolID protocol.ID - getter share.Getter - store *eds.Store + handler network.StreamHandler + store *eds.Store params *Parameters middleware *p2p.Middleware @@ -38,13 +38,12 @@ type Server struct { } // NewServer creates new Server -func NewServer(params *Parameters, host host.Host, store *eds.Store, getter share.Getter) (*Server, error) { +func NewServer(params *Parameters, host host.Host, store *eds.Store) (*Server, error) { if err := params.Validate(); err != nil { return nil, fmt.Errorf("shrex-nd: server creation failed: %w", err) } srv := &Server{ - getter: getter, store: store, host: host, params: params, @@ -52,18 +51,16 @@ func NewServer(params *Parameters, host host.Host, store *eds.Store, getter shar middleware: p2p.NewMiddleware(params.ConcurrencyLimit), } + ctx, cancel := context.WithCancel(context.Background()) + srv.cancel = cancel + + srv.handler = srv.middleware.RateLimitHandler(srv.streamHandler(ctx)) return srv, nil } // Start starts the server func (srv *Server) Start(context.Context) error { - ctx, cancel := context.WithCancel(context.Background()) - srv.cancel = cancel - - handler := func(s network.Stream) { - srv.handleNamespacedData(ctx, s) - } - srv.host.SetStreamHandler(srv.protocolID, srv.middleware.RateLimitHandler(handler)) + srv.host.SetStreamHandler(srv.protocolID, srv.handler) return nil } @@ -74,6 +71,24 @@ func (srv *Server) Stop(context.Context) error { return nil } +func (srv *Server) streamHandler(ctx context.Context) network.StreamHandler { + return func(s network.Stream) { + err := srv.handleNamespacedData(ctx, s) + if err != nil { + s.Reset() //nolint:errcheck + return + } + if err = s.Close(); err != nil { + log.Debugw("server: closing stream", "err", err) + } + } +} + +// SetHandler sets server handler +func (srv *Server) SetHandler(handler network.StreamHandler) { + srv.handler = handler +} + func (srv *Server) observeRateLimitedRequests() { numRateLimited := srv.middleware.DrainCounter() if numRateLimited > 0 { @@ -81,159 +96,159 @@ func (srv *Server) observeRateLimitedRequests() { } } -func (srv *Server) handleNamespacedData(ctx context.Context, stream network.Stream) { - logger := log.With("peer", stream.Conn().RemotePeer()) - logger.Debug("server: handling nd request") +func (srv *Server) handleNamespacedData(ctx context.Context, stream network.Stream) error { + logger := log.With("source", "server", "peer", stream.Conn().RemotePeer().String()) + logger.Debug("handling nd request") srv.observeRateLimitedRequests() + req, err := srv.readRequest(logger, stream) + if err != nil { + logger.Warnw("read request", "err", err) + srv.metrics.ObserveRequests(ctx, 1, p2p.StatusBadRequest) + return err + } + + logger = logger.With("namespace", share.Namespace(req.Namespace).String(), + "hash", share.DataHash(req.RootHash).String()) + + ctx, cancel := context.WithTimeout(ctx, srv.params.HandleRequestTimeout) + defer cancel() + + shares, status, err := srv.getNamespaceData(ctx, req.RootHash, req.Namespace) + if err != nil { + // server should respond with status regardless if there was an error getting data + sendErr := srv.respondStatus(ctx, logger, stream, status) + if sendErr != nil { + logger.Errorw("sending response", "err", sendErr) + srv.metrics.ObserveRequests(ctx, 1, p2p.StatusSendRespErr) + } + logger.Errorw("handling request", "err", err) + return errors.Join(err, sendErr) + } + + err = srv.respondStatus(ctx, logger, stream, status) + if err != nil { + logger.Errorw("sending response", "err", err) + srv.metrics.ObserveRequests(ctx, 1, p2p.StatusSendRespErr) + return err + } + + err = srv.sendNamespacedShares(shares, stream) + if err != nil { + logger.Errorw("send nd data", "err", err) + srv.metrics.ObserveRequests(ctx, 1, p2p.StatusSendRespErr) + return err + } + return nil +} +func (srv *Server) readRequest( + logger *zap.SugaredLogger, + stream network.Stream, +) (*pb.GetSharesByNamespaceRequest, error) { err := stream.SetReadDeadline(time.Now().Add(srv.params.ServerReadTimeout)) if err != nil { - logger.Debugw("server: setting read deadline", "err", err) + logger.Debugw("setting read deadline", "err", err) } var req pb.GetSharesByNamespaceRequest _, err = serde.Read(stream, &req) if err != nil { - logger.Warnw("server: reading request", "err", err) - stream.Reset() //nolint:errcheck - return + return nil, fmt.Errorf("reading request: %w", err) + } - logger = logger.With("namespaceId", string(req.NamespaceId), "hash", string(req.RootHash)) - logger.Debugw("server: new request") + logger.Debugw("new request") err = stream.CloseRead() if err != nil { - logger.Debugw("server: closing read side of the stream", "err", err) + logger.Debugw("closing read side of the stream", "err", err) } err = validateRequest(req) if err != nil { - logger.Warnw("server: invalid request", "err", err) - stream.Reset() //nolint:errcheck - return + return nil, fmt.Errorf("invalid request: %w", err) } + return &req, nil +} - ctx, cancel := context.WithTimeout(ctx, srv.params.HandleRequestTimeout) - defer cancel() - - dah, err := srv.store.GetDAH(ctx, req.RootHash) +func (srv *Server) getNamespaceData(ctx context.Context, + hash share.DataHash, namespace share.Namespace) (share.NamespacedShares, pb.StatusCode, error) { + dah, err := srv.store.GetDAH(ctx, hash) if err != nil { if errors.Is(err, eds.ErrNotFound) { - logger.Warn("server: DAH not found") - srv.respondNotFoundError(ctx, logger, stream) - return + return nil, pb.StatusCode_NOT_FOUND, nil } - logger.Errorw("server: retrieving DAH", "err", err) - srv.respondInternalError(ctx, logger, stream) - return + return nil, pb.StatusCode_INTERNAL, fmt.Errorf("retrieving DAH: %w", err) } - shares, err := srv.getter.GetSharesByNamespace(ctx, dah, req.NamespaceId) - switch { - case errors.Is(err, share.ErrNotFound): - logger.Warn("server: nd not found") - srv.respondNotFoundError(ctx, logger, stream) - return - case errors.Is(err, share.ErrNamespaceNotFound): - srv.respondNamespaceNotFoundError(ctx, logger, stream) - return - case err != nil: - logger.Errorw("server: retrieving shares", "err", err) - srv.respondInternalError(ctx, logger, stream) - return - } - - resp := namespacedSharesToResponse(shares) - srv.respond(ctx, logger, stream, resp) -} - -// validateRequest checks correctness of the request -func validateRequest(req pb.GetSharesByNamespaceRequest) error { - if len(req.NamespaceId) != ipld.NamespaceSize { - return fmt.Errorf("incorrect namespace id length: %v", len(req.NamespaceId)) - } - if len(req.RootHash) != sha256.Size { - return fmt.Errorf("incorrect root hash length: %v", len(req.RootHash)) + shares, err := eds.RetrieveNamespaceFromStore(ctx, srv.store, dah, namespace) + if err != nil { + return nil, pb.StatusCode_INTERNAL, fmt.Errorf("retrieving shares: %w", err) } - return nil + return shares, pb.StatusCode_OK, nil } -// respondNotFoundError sends a not found response to client -func (srv *Server) respondNotFoundError(ctx context.Context, - logger *zap.SugaredLogger, stream network.Stream) { - resp := &pb.GetSharesByNamespaceResponse{ - Status: pb.StatusCode_NOT_FOUND, - } - srv.respond(ctx, logger, stream, resp) -} +func (srv *Server) respondStatus( + ctx context.Context, + logger *zap.SugaredLogger, + stream network.Stream, + status pb.StatusCode, +) error { + srv.observeStatus(ctx, status) -// respondNamespaceNotFoundError sends a namespace not found response to client -func (srv *Server) respondNamespaceNotFoundError(ctx context.Context, - logger *zap.SugaredLogger, stream network.Stream) { - resp := &pb.GetSharesByNamespaceResponse{ - Status: pb.StatusCode_NAMESPACE_NOT_FOUND, + err := stream.SetWriteDeadline(time.Now().Add(srv.params.ServerWriteTimeout)) + if err != nil { + logger.Debugw("setting write deadline", "err", err) } - srv.respond(ctx, logger, stream, resp) -} -// respondInternalError sends internal error response to client -func (srv *Server) respondInternalError(ctx context.Context, - logger *zap.SugaredLogger, stream network.Stream) { - resp := &pb.GetSharesByNamespaceResponse{ - Status: pb.StatusCode_INTERNAL, + _, err = serde.Write(stream, &pb.GetSharesByNamespaceStatusResponse{Status: status}) + if err != nil { + return fmt.Errorf("writing response: %w", err) } - srv.respond(ctx, logger, stream, resp) + + return nil } -// namespacedSharesToResponse encodes shares into proto and sends it to client with OK status code -func namespacedSharesToResponse(shares share.NamespacedShares) *pb.GetSharesByNamespaceResponse { - rows := make([]*pb.Row, 0, len(shares)) +// sendNamespacedShares encodes shares into proto messages and sends it to client +func (srv *Server) sendNamespacedShares(shares share.NamespacedShares, stream network.Stream) error { for _, row := range shares { - proof := &pb.Proof{ - Start: int64(row.Proof.Start()), - End: int64(row.Proof.End()), - Nodes: row.Proof.Nodes(), - } - - row := &pb.Row{ + row := &pb.NamespaceRowResponse{ Shares: row.Shares, - Proof: proof, + Proof: &nmt_pb.Proof{ + Start: int64(row.Proof.Start()), + End: int64(row.Proof.End()), + Nodes: row.Proof.Nodes(), + LeafHash: row.Proof.LeafHash(), + IsMaxNamespaceIgnored: row.Proof.IsMaxNamespaceIDIgnored(), + }, + } + _, err := serde.Write(stream, row) + if err != nil { + return fmt.Errorf("writing nd data to stream: %w", err) } - - rows = append(rows, row) - } - - return &pb.GetSharesByNamespaceResponse{ - Status: pb.StatusCode_OK, - Rows: rows, } + return nil } -func (srv *Server) respond(ctx context.Context, - logger *zap.SugaredLogger, stream network.Stream, resp *pb.GetSharesByNamespaceResponse) { - err := stream.SetWriteDeadline(time.Now().Add(srv.params.ServerWriteTimeout)) - if err != nil { - logger.Debugw("server: setting write deadline", "err", err) - } - - _, err = serde.Write(stream, resp) - if err != nil { - logger.Warnw("server: writing response", "err", err) - stream.Reset() //nolint:errcheck - return - } - +func (srv *Server) observeStatus(ctx context.Context, status pb.StatusCode) { switch { - case resp.Status == pb.StatusCode_OK: + case status == pb.StatusCode_OK: srv.metrics.ObserveRequests(ctx, 1, p2p.StatusSuccess) - case resp.Status == pb.StatusCode_NOT_FOUND: + case status == pb.StatusCode_NOT_FOUND: srv.metrics.ObserveRequests(ctx, 1, p2p.StatusNotFound) - case resp.Status == pb.StatusCode_INTERNAL: + case status == pb.StatusCode_INTERNAL: srv.metrics.ObserveRequests(ctx, 1, p2p.StatusInternalErr) } - if err = stream.Close(); err != nil { - logger.Debugw("server: closing stream", "err", err) +} + +// validateRequest checks correctness of the request +func validateRequest(req pb.GetSharesByNamespaceRequest) error { + if err := share.Namespace(req.Namespace).ValidateForData(); err != nil { + return err } + if len(req.RootHash) != sha256.Size { + return fmt.Errorf("incorrect root hash length: %v", len(req.RootHash)) + } + return nil } diff --git a/share/p2p/shrexsub/doc.go b/share/p2p/shrexsub/doc.go new file mode 100644 index 0000000000..95d08361a2 --- /dev/null +++ b/share/p2p/shrexsub/doc.go @@ -0,0 +1,58 @@ +// This package defines a protocol that is used to broadcast shares to peers over a pubsub network. +// +// This protocol runs on a rudimentary floodsub network is primarily a pubsub protocol +// that broadcasts and listens for shares over a pubsub topic. +// +// The pubsub topic used by this protocol is: +// +// "{networkID}/eds-sub/v0.1.0" +// +// where networkID is the network ID of the celestia-node that is running the protocol. (e.g. "arabica") +// +// # Usage +// +// To use this protocol, you must first create a new `shrexsub.PubSub` instance by: +// +// pubsub, err := shrexsub.NewPubSub(ctx, host, networkID) +// +// where host is the libp2p host that is running the protocol, and networkID is the network ID of the celestia-node +// that is running the protocol. +// +// After this, you can start the pubsub protocol by: +// +// err := pubsub.Start(ctx) +// +// Once you have started the `shrexsub.PubSub` instance, you can broadcast a share by: +// +// err := pubsub.Broadcast(ctx, notification) +// +// where `notification` is of type [shrexsub.Notification]. +// +// and `DataHash` is the hash of the share that you want to broadcast, and `Height` is the height of the share. +// +// You can also subscribe to the pubsub topic by: +// +// sub, err := pubsub.Subscribe(ctx) +// +// and then receive notifications by: +// +// for { +// select { +// case <-ctx.Done(): +// sub.Cancel() +// return +// case notification, err := <-sub.Next(): +// // handle notification or err +// } +// } +// +// You can also manipulate the received pubsub messages by using the [PubSub.AddValidator] method: +// +// pubsub.AddValidator(validator ValidatorFn) +// +// where `validator` is of type [shrexsub.ValidatorFn] and `Notification` is the same as above. +// +// You can also stop the pubsub protocol by: +// +// err := pubsub.Stop(ctx) +package shrexsub diff --git a/share/p2p/shrexsub/pubsub.go b/share/p2p/shrexsub/pubsub.go index 82ed256e33..ed713b4614 100644 --- a/share/p2p/shrexsub/pubsub.go +++ b/share/p2p/shrexsub/pubsub.go @@ -102,8 +102,9 @@ func (v ValidatorFn) validate(ctx context.Context, p peer.ID, msg *pubsub.Messag DataHash: pbmsg.DataHash, Height: pbmsg.Height, } - if n.DataHash.IsEmptyRoot() { - // we don't send empty EDS data hashes, but If someone sent it to us - do hard reject + if n.Height == 0 || n.DataHash.IsEmptyRoot() || n.DataHash.Validate() != nil { + // hard reject malicious height (height 0 does not exist) and + // empty/invalid datahashes return pubsub.ValidationReject } return v(ctx, p, n) diff --git a/share/p2p/shrexsub/pubsub_test.go b/share/p2p/shrexsub/pubsub_test.go index 4198b64379..85b16c055b 100644 --- a/share/p2p/shrexsub/pubsub_test.go +++ b/share/p2p/shrexsub/pubsub_test.go @@ -6,8 +6,10 @@ import ( "time" pubsub "github.com/libp2p/go-libp2p-pubsub" + "github.com/libp2p/go-libp2p/core/peer" mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/libs/rand" pb "github.com/celestiaorg/celestia-node/share/p2p/shrexsub/pb" ) @@ -23,6 +25,13 @@ func TestPubSub(t *testing.T) { pSub2, err := NewPubSub(ctx, h.Hosts()[1], "test") require.NoError(t, err) + err = pSub2.AddValidator( + func(ctx context.Context, p peer.ID, n Notification) pubsub.ValidationResult { + // only testing shrexsub validation here + return pubsub.ValidationAccept + }, + ) + require.NoError(t, err) require.NoError(t, pSub1.Start(ctx)) require.NoError(t, pSub2.Start(ctx)) @@ -30,23 +39,86 @@ func TestPubSub(t *testing.T) { subs, err := pSub2.Subscribe() require.NoError(t, err) - notification := Notification{ - DataHash: []byte("data"), - Height: 1, + var tests = []struct { + name string + notif Notification + errExpected bool + }{ + { + name: "valid height, valid hash", + notif: Notification{ + Height: 1, + DataHash: rand.Bytes(32), + }, + errExpected: false, + }, + { + name: "valid height, invalid hash (<32 bytes)", + notif: Notification{ + Height: 2, + DataHash: rand.Bytes(20), + }, + errExpected: true, + }, + { + name: "valid height, invalid hash (>32 bytes)", + notif: Notification{ + Height: 2, + DataHash: rand.Bytes(64), + }, + errExpected: true, + }, + { + name: "invalid height, valid hash", + notif: Notification{ + Height: 0, + DataHash: rand.Bytes(32), + }, + errExpected: true, + }, + { + name: "invalid height, nil hash", + notif: Notification{ + Height: 0, + DataHash: nil, + }, + errExpected: true, + }, + { + name: "valid height, nil hash", + notif: Notification{ + Height: 30, + DataHash: nil, + }, + errExpected: true, + }, } - msg := pb.RecentEDSNotification{ - Height: notification.Height, - DataHash: notification.DataHash, - } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := pb.RecentEDSNotification{ + Height: tt.notif.Height, + DataHash: tt.notif.DataHash, + } + data, err := msg.Marshal() + require.NoError(t, err) - data, err := msg.Marshal() - require.NoError(t, err) + err = pSub1.topic.Publish(ctx, data, pubsub.WithReadiness(pubsub.MinTopicSize(1))) + require.NoError(t, err) - err = pSub1.topic.Publish(ctx, data, pubsub.WithReadiness(pubsub.MinTopicSize(1))) - require.NoError(t, err) + reqCtx, reqCtxCancel := context.WithTimeout(context.Background(), time.Millisecond*100) + defer reqCtxCancel() + + got, err := subs.Next(reqCtx) + if tt.errExpected { + require.Error(t, err) + require.ErrorIs(t, err, context.DeadlineExceeded) + return + } + require.NoError(t, err) + require.NoError(t, err) + require.Equal(t, tt.notif, got) + }) + } - got, err := subs.Next(ctx) - require.NoError(t, err) - require.Equal(t, notification, got) } diff --git a/share/share.go b/share/share.go index 643f0c477a..4079028d82 100644 --- a/share/share.go +++ b/share/share.go @@ -2,29 +2,26 @@ package share import ( "bytes" + "encoding/hex" "fmt" - "go.opentelemetry.io/otel" - "github.com/celestiaorg/celestia-app/pkg/appconsts" - "github.com/celestiaorg/nmt/namespace" ) var ( - tracer = otel.Tracer("share") - // DefaultRSMT2DCodec sets the default rsmt2d.Codec for shares. DefaultRSMT2DCodec = appconsts.DefaultCodec ) const ( + // Size is a system-wide size of a share, including both data and namespace GetNamespace + Size = appconsts.ShareSize +) + +var ( // MaxSquareSize is currently the maximum size supported for unerasured data in // rsmt2d.ExtendedDataSquare. - MaxSquareSize = appconsts.DefaultMaxSquareSize - // NamespaceSize is a system-wide size for NMT namespaces. - NamespaceSize = appconsts.NamespaceSize - // Size is a system-wide size of a share, including both data and namespace ID - Size = appconsts.ShareSize + MaxSquareSize = appconsts.SquareSizeUpperBound(appconsts.LatestVersion) ) // Share contains the raw share data without the corresponding namespace. @@ -33,13 +30,13 @@ const ( // on it. type Share = []byte -// ID gets the namespace ID from the share. -func ID(s Share) namespace.ID { +// GetNamespace slices Namespace out of the Share. +func GetNamespace(s Share) Namespace { return s[:NamespaceSize] } -// Data gets data from the share. -func Data(s Share) []byte { +// GetData slices out data of the Share. +func GetData(s Share) []byte { return s[NamespaceSize:] } @@ -61,3 +58,16 @@ func (dh DataHash) String() string { func (dh DataHash) IsEmptyRoot() bool { return bytes.Equal(EmptyRoot().Hash(), dh) } + +// MustDataHashFromString converts a hex string to a valid datahash. +func MustDataHashFromString(datahash string) DataHash { + dh, err := hex.DecodeString(datahash) + if err != nil { + panic(fmt.Sprintf("datahash conversion: passed string was not valid hex: %s", datahash)) + } + err = DataHash(dh).Validate() + if err != nil { + panic(fmt.Sprintf("datahash validation: passed hex string failed: %s", err)) + } + return dh +} diff --git a/share/sharetest/testing.go b/share/sharetest/testing.go new file mode 100644 index 0000000000..3889260393 --- /dev/null +++ b/share/sharetest/testing.go @@ -0,0 +1,78 @@ +package sharetest + +import ( + "bytes" + "math/rand" + "sort" + "sync" + "time" + + "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-app/pkg/namespace" + + "github.com/celestiaorg/celestia-node/share" +) + +// RandShares generate 'total' amount of shares filled with random data. It uses require.TestingT +// to be able to take both a *testing.T and a *testing.B. +func RandShares(t require.TestingT, total int) []share.Share { + if total&(total-1) != 0 { + t.Errorf("total must be power of 2: %d", total) + t.FailNow() + } + + shares := make([]share.Share, total) + for i := range shares { + shr := make([]byte, share.Size) + copy(share.GetNamespace(shr), RandV0Namespace()) + rndMu.Lock() + _, err := rnd.Read(share.GetData(shr)) + rndMu.Unlock() + require.NoError(t, err) + shares[i] = shr + } + sort.Slice(shares, func(i, j int) bool { return bytes.Compare(shares[i], shares[j]) < 0 }) + + return shares +} + +// RandSharesWithNamespace is same the as RandShares, but sets same namespace for all shares. +func RandSharesWithNamespace(t require.TestingT, namespace share.Namespace, total int) []share.Share { + if total&(total-1) != 0 { + t.Errorf("total must be power of 2: %d", total) + t.FailNow() + } + + shares := make([]share.Share, total) + rnd := rand.New(rand.NewSource(time.Now().Unix())) //nolint:gosec + for i := range shares { + shr := make([]byte, share.Size) + copy(share.GetNamespace(shr), namespace) + _, err := rnd.Read(share.GetData(shr)) + require.NoError(t, err) + shares[i] = shr + } + sort.Slice(shares, func(i, j int) bool { return bytes.Compare(shares[i], shares[j]) < 0 }) + return shares +} + +// RandV0Namespace generates random valid data namespace for testing purposes. +func RandV0Namespace() share.Namespace { + rb := make([]byte, namespace.NamespaceVersionZeroIDSize) + rndMu.Lock() + rnd.Read(rb) + rndMu.Unlock() + for { + namespace, _ := share.NewBlobNamespaceV0(rb) + if err := namespace.ValidateForData(); err != nil { + continue + } + return namespace + } +} + +var ( + rnd = rand.New(rand.NewSource(time.Now().Unix())) //nolint:gosec + rndMu sync.Mutex +) diff --git a/share/test_helpers.go b/share/test_helpers.go deleted file mode 100644 index f3cb6776f2..0000000000 --- a/share/test_helpers.go +++ /dev/null @@ -1,68 +0,0 @@ -package share - -import ( - "bytes" - "crypto/rand" - "sort" - - "github.com/stretchr/testify/require" - - "github.com/celestiaorg/celestia-app/pkg/wrapper" - "github.com/celestiaorg/rsmt2d" -) - -// EqualEDS check whether two given EDSes are equal. -// TODO(Wondertan): Move to rsmt2d -// TODO(Wondertan): Propose use of int by default instead of uint for the sake convenience and -// Golang practices -func EqualEDS(a *rsmt2d.ExtendedDataSquare, b *rsmt2d.ExtendedDataSquare) bool { - if a.Width() != b.Width() { - return false - } - - for i := uint(0); i < a.Width(); i++ { - ar, br := a.Row(i), b.Row(i) - for j := 0; j < len(ar); j++ { - if !bytes.Equal(ar[j], br[j]) { - return false - } - } - } - - return true -} - -// RandEDS generates EDS filled with the random data with the given size for original square. It -// uses require.TestingT to be able to take both a *testing.T and a *testing.B. -func RandEDS(t require.TestingT, size int) *rsmt2d.ExtendedDataSquare { - shares := RandShares(t, size*size) - // recompute the eds - eds, err := rsmt2d.ComputeExtendedDataSquare(shares, DefaultRSMT2DCodec(), wrapper.NewConstructor(uint64(size))) - require.NoError(t, err, "failure to recompute the extended data square") - return eds -} - -// RandShares generate 'total' amount of shares filled with random data. It uses require.TestingT -// to be able to take both a *testing.T and a *testing.B. -func RandShares(t require.TestingT, total int) []Share { - if total&(total-1) != 0 { - t.Errorf("Namespace total must be power of 2: %d", total) - t.FailNow() - } - - shares := make([]Share, total) - for i := range shares { - nid := make([]byte, Size) - _, err := rand.Read(nid[:NamespaceSize]) - require.NoError(t, err) - shares[i] = nid - } - sort.Slice(shares, func(i, j int) bool { return bytes.Compare(shares[i], shares[j]) < 0 }) - - for i := range shares { - _, err := rand.Read(shares[i][NamespaceSize:]) - require.NoError(t, err) - } - - return shares -} diff --git a/state/address_test.go b/state/address_test.go new file mode 100644 index 0000000000..d701b38aa8 --- /dev/null +++ b/state/address_test.go @@ -0,0 +1,57 @@ +package state + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAddressMarshalling(t *testing.T) { + testCases := []struct { + name string + addressString string + addressFromStr func(string) (interface{}, error) + marshalJSON func(interface{}) ([]byte, error) + unmarshalJSON func([]byte) (interface{}, error) + }{ + { + name: "Account Address", + addressString: "celestia1377k5an3f94v6wyaceu0cf4nq6gk2jtpc46g7h", + addressFromStr: func(s string) (interface{}, error) { return sdk.AccAddressFromBech32(s) }, + marshalJSON: func(addr interface{}) ([]byte, error) { return addr.(sdk.AccAddress).MarshalJSON() }, + unmarshalJSON: func(b []byte) (interface{}, error) { + var addr sdk.AccAddress + err := addr.UnmarshalJSON(b) + return addr, err + }, + }, + { + name: "Validator Address", + addressString: "celestiavaloper1q3v5cugc8cdpud87u4zwy0a74uxkk6u4q4gx4p", + addressFromStr: func(s string) (interface{}, error) { return sdk.ValAddressFromBech32(s) }, + marshalJSON: func(addr interface{}) ([]byte, error) { return addr.(sdk.ValAddress).MarshalJSON() }, + unmarshalJSON: func(b []byte) (interface{}, error) { + var addr sdk.ValAddress + err := addr.UnmarshalJSON(b) + return addr, err + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + addr, err := tc.addressFromStr(tc.addressString) + require.NoError(t, err) + + addrBytes, err := tc.marshalJSON(addr) + assert.NoError(t, err) + assert.Equal(t, []byte("\""+tc.addressString+"\""), addrBytes) + + addrUnmarshalled, err := tc.unmarshalJSON(addrBytes) + assert.NoError(t, err) + assert.Equal(t, addr, addrUnmarshalled) + }) + } +} diff --git a/state/core_access.go b/state/core_access.go index aed7db3df3..c3fbd4836a 100644 --- a/state/core_access.go +++ b/state/core_access.go @@ -4,12 +4,17 @@ import ( "context" "errors" "fmt" + "math" + "sync" "time" + sdkErrors "cosmossdk.io/errors" "github.com/cosmos/cosmos-sdk/api/tendermint/abci" + nodeservice "github.com/cosmos/cosmos-sdk/client/grpc/node" storetypes "github.com/cosmos/cosmos-sdk/store/types" sdktypes "github.com/cosmos/cosmos-sdk/types" sdktx "github.com/cosmos/cosmos-sdk/types/tx" + auth "github.com/cosmos/cosmos-sdk/x/auth/types" banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" logging "github.com/ipfs/go-log/v2" @@ -20,12 +25,13 @@ import ( "google.golang.org/grpc/credentials/insecure" "github.com/celestiaorg/celestia-app/app" + apperrors "github.com/celestiaorg/celestia-app/app/errors" "github.com/celestiaorg/celestia-app/pkg/appconsts" - "github.com/celestiaorg/celestia-app/x/blob" + appblob "github.com/celestiaorg/celestia-app/x/blob" apptypes "github.com/celestiaorg/celestia-app/x/blob/types" libhead "github.com/celestiaorg/go-header" - "github.com/celestiaorg/nmt/namespace" + "github.com/celestiaorg/celestia-node/blob" "github.com/celestiaorg/celestia-node/header" ) @@ -34,6 +40,8 @@ var ( ErrInvalidAmount = errors.New("state: amount must be greater than zero") ) +const maxRetries = 5 + // CoreAccessor implements service over a gRPC connection // with a celestia-core node. type CoreAccessor struct { @@ -54,8 +62,15 @@ type CoreAccessor struct { rpcPort string grpcPort string + // these fields are mutatable and thus need to be protected by a mutex + lock sync.Mutex lastPayForBlob int64 payForBlobCount int64 + // minGasPrice is the minimum gas price that the node will accept. + // NOTE: just because the first node accepts the transaction, does not mean it + // will find a proposer that does accept the transaction. Better would be + // to set a global min gas price that correct processes conform to. + minGasPrice float64 } // NewCoreAccessor dials the given celestia-core endpoint and @@ -90,7 +105,11 @@ func (ca *CoreAccessor) Start(ctx context.Context) error { // dial given celestia-core endpoint endpoint := fmt.Sprintf("%s:%s", ca.coreIP, ca.grpcPort) - client, err := grpc.DialContext(ctx, endpoint, grpc.WithTransportCredentials(insecure.NewCredentials())) + client, err := grpc.DialContext( + ctx, + endpoint, + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) if err != nil { return err } @@ -108,6 +127,11 @@ func (ca *CoreAccessor) Start(ctx context.Context) error { } ca.rpcCli = cli + ca.minGasPrice, err = ca.queryMinimumGasPrice(ctx) + if err != nil { + return fmt.Errorf("querying minimum gas price: %w", err) + } + return nil } @@ -156,36 +180,97 @@ func (ca *CoreAccessor) constructSignedTx( return ca.signer.EncodeTx(tx) } +// SubmitPayForBlob builds, signs, and synchronously submits a MsgPayForBlob. It blocks until the +// transaction is committed and returns the TxResponse. If gasLim is set to 0, the method will +// automatically estimate the gas limit. If the fee is negative, the method will use the nodes min +// gas price multiplied by the gas limit. func (ca *CoreAccessor) SubmitPayForBlob( ctx context.Context, - nID namespace.ID, - data []byte, fee Int, gasLim uint64, + blobs []*blob.Blob, ) (*TxResponse, error) { - b := &apptypes.Blob{NamespaceId: nID, Data: data, ShareVersion: uint32(appconsts.DefaultShareVersion)} - response, err := blob.SubmitPayForBlob( - ctx, - ca.signer, - ca.coreConn, - []*apptypes.Blob{b}, - apptypes.SetGasLimit(gasLim), - withFee(fee), - ) - // metrics should only be counted on a successful PFD tx - if err == nil && response.Code == 0 { - ca.lastPayForBlob = time.Now().UnixMilli() - ca.payForBlobCount++ - } - return response, err + if len(blobs) == 0 { + return nil, errors.New("state: no blobs provided") + } + + appblobs := make([]*apptypes.Blob, len(blobs)) + for i := range blobs { + if err := blobs[i].Namespace().ValidateForBlob(); err != nil { + return nil, err + } + appblobs[i] = &blobs[i].Blob + } + + // we only estimate gas if the user wants us to (by setting the gasLim to 0). In the future we may + // want to make these arguments optional. + if gasLim == 0 { + blobSizes := make([]uint32, len(blobs)) + for i, blob := range blobs { + blobSizes[i] = uint32(len(blob.Data)) + } + + // TODO (@cmwaters): the default gas per byte and the default tx size cost per byte could be changed + // through governance. This section could be more robust by tracking these values and adjusting the + // gas limit accordingly (as is done for the gas price) + gasLim = apptypes.EstimateGas(blobSizes, appconsts.DefaultGasPerBlobByte, auth.DefaultTxSizeCostPerByte) + } + + minGasPrice := ca.getMinGasPrice() + + // set the fee for the user as the minimum gas price multiplied by the gas limit + estimatedFee := false + if fee.IsNegative() { + estimatedFee = true + fee = sdktypes.NewInt(int64(math.Ceil(minGasPrice * float64(gasLim)))) + } + + var lastErr error + for attempt := 0; attempt < maxRetries; attempt++ { + response, err := appblob.SubmitPayForBlob( + ctx, + ca.signer, + ca.coreConn, + sdktx.BroadcastMode_BROADCAST_MODE_BLOCK, + appblobs, + apptypes.SetGasLimit(gasLim), + withFee(fee), + ) + + // the node is capable of changing the min gas price at any time so we must be able to detect it and + // update our version accordingly + if apperrors.IsInsufficientMinGasPrice(err) && estimatedFee { + // The error message contains enough information to parse the new min gas price + minGasPrice, err = apperrors.ParseInsufficientMinGasPrice(err, minGasPrice, gasLim) + if err != nil { + return nil, fmt.Errorf("parsing insufficient min gas price error: %w", err) + } + ca.setMinGasPrice(minGasPrice) + lastErr = err + // update the fee to retry again + fee = sdktypes.NewInt(int64(math.Ceil(minGasPrice * float64(gasLim)))) + continue + } + + // metrics should only be counted on a successful PFD tx + if err == nil && response.Code == 0 { + ca.markSuccessfulPFB() + } + + if response != nil && response.Code != 0 { + err = errors.Join(err, sdkErrors.ABCIError(response.Codespace, response.Code, response.Logs.String())) + } + return response, err + } + return nil, fmt.Errorf("failed to submit blobs after %d attempts: %w", maxRetries, lastErr) } func (ca *CoreAccessor) AccountAddress(context.Context) (Address, error) { addr, err := ca.signer.GetSignerInfo().GetAddress() if err != nil { - return nil, err + return Address{nil}, err } - return addr, nil + return Address{addr}, nil } func (ca *CoreAccessor) Balance(ctx context.Context) (*Balance, error) { @@ -193,7 +278,7 @@ func (ca *CoreAccessor) Balance(ctx context.Context) (*Balance, error) { if err != nil { return nil, err } - return ca.BalanceForAddress(ctx, addr) + return ca.BalanceForAddress(ctx, Address{addr}) } func (ca *CoreAccessor) BalanceForAddress(ctx context.Context, addr Address) (*Balance, error) { @@ -210,7 +295,7 @@ func (ca *CoreAccessor) BalanceForAddress(ctx context.Context, addr Address) (*B abciReq := abci.RequestQuery{ // TODO @renayay: once https://github.com/cosmos/cosmos-sdk/pull/12674 is merged, use const instead Path: fmt.Sprintf("store/%s/key", banktypes.StoreKey), - Height: head.Height() - 1, + Height: int64(head.Height() - 1), Data: prefixedAccountKey, Prove: true, } @@ -441,8 +526,51 @@ func (ca *CoreAccessor) QueryRedelegations( }) } -func (ca *CoreAccessor) IsStopped(context.Context) bool { - return ca.ctx.Err() != nil +func (ca *CoreAccessor) LastPayForBlob() int64 { + ca.lock.Lock() + defer ca.lock.Unlock() + return ca.lastPayForBlob +} + +func (ca *CoreAccessor) PayForBlobCount() int64 { + ca.lock.Lock() + defer ca.lock.Unlock() + return ca.payForBlobCount +} + +func (ca *CoreAccessor) markSuccessfulPFB() { + ca.lock.Lock() + defer ca.lock.Unlock() + ca.lastPayForBlob = time.Now().UnixMilli() + ca.payForBlobCount++ +} + +func (ca *CoreAccessor) setMinGasPrice(minGasPrice float64) { + ca.lock.Lock() + defer ca.lock.Unlock() + ca.minGasPrice = minGasPrice +} + +func (ca *CoreAccessor) getMinGasPrice() float64 { + ca.lock.Lock() + defer ca.lock.Unlock() + return ca.minGasPrice +} + +// QueryMinimumGasPrice returns the minimum gas price required by the node. +func (ca *CoreAccessor) queryMinimumGasPrice( + ctx context.Context, +) (float64, error) { + rsp, err := nodeservice.NewServiceClient(ca.coreConn).Config(ctx, &nodeservice.ConfigRequest{}) + if err != nil { + return 0, err + } + + coins, err := sdktypes.ParseDecCoins(rsp.MinimumGasPrice) + if err != nil { + return 0, err + } + return coins.AmountOf(app.BondDenom).MustFloat64(), nil } func withFee(fee Int) apptypes.TxBuilderOption { diff --git a/state/core_access_test.go b/state/core_access_test.go index 5018cee4f2..ad7b916ea3 100644 --- a/state/core_access_test.go +++ b/state/core_access_test.go @@ -1,33 +1,98 @@ +//go:build !race + package state import ( "context" + "errors" + "fmt" + "strings" "testing" + "time" + "cosmossdk.io/math" + sdktypes "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" + + "github.com/celestiaorg/celestia-app/app" + "github.com/celestiaorg/celestia-app/pkg/appconsts" + "github.com/celestiaorg/celestia-app/test/util/testnode" + blobtypes "github.com/celestiaorg/celestia-app/x/blob/types" + + "github.com/celestiaorg/celestia-node/blob" + "github.com/celestiaorg/celestia-node/share" ) -func TestLifecycle(t *testing.T) { - ca := NewCoreAccessor(nil, nil, "", "", "") +func TestSubmitPayForBlob(t *testing.T) { + accounts := []string{"jimy", "rob"} + tmCfg := testnode.DefaultTendermintConfig() + tmCfg.Consensus.TimeoutCommit = time.Millisecond * 1 + appConf := testnode.DefaultAppConfig() + appConf.API.Enable = true + appConf.MinGasPrices = fmt.Sprintf("0.002%s", app.BondDenom) + + config := testnode.DefaultConfig().WithTendermintConfig(tmCfg).WithAppConfig(appConf).WithAccounts(accounts) + cctx, rpcAddr, grpcAddr := testnode.NewNetwork(t, config) ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + signer := blobtypes.NewKeyringSigner(cctx.Keyring, accounts[0], cctx.ChainID) + ca := NewCoreAccessor(signer, nil, "127.0.0.1", extractPort(rpcAddr), extractPort(grpcAddr)) // start the accessor err := ca.Start(ctx) require.NoError(t, err) - // ensure accessor isn't stopped - require.False(t, ca.IsStopped(ctx)) - // cancel the top level context (this should not affect the lifecycle of the - // accessor as it should manage its own internal context) - cancel() - // ensure accessor was unaffected by top-level context cancellation - require.False(t, ca.IsStopped(ctx)) - // stop the accessor - stopCtx, stopCancel := context.WithCancel(context.Background()) - t.Cleanup(stopCancel) - err = ca.Stop(stopCtx) + t.Cleanup(func() { + _ = ca.Stop(ctx) + }) + + ns, err := share.NewBlobNamespaceV0([]byte("namespace")) require.NoError(t, err) - // ensure accessor is stopped - require.True(t, ca.IsStopped(ctx)) - // ensure that stopping the accessor again does not return an error - err = ca.Stop(stopCtx) + blobbyTheBlob, err := blob.NewBlobV0(ns, []byte("data")) require.NoError(t, err) + + minGas, err := ca.queryMinimumGasPrice(ctx) + require.NoError(t, err) + require.Equal(t, appconsts.DefaultMinGasPrice, minGas) + + testcases := []struct { + name string + blobs []*blob.Blob + fee math.Int + gasLim uint64 + expErr error + }{ + { + name: "empty blobs", + blobs: []*blob.Blob{}, + fee: sdktypes.ZeroInt(), + gasLim: 0, + expErr: errors.New("state: no blobs provided"), + }, + { + name: "good blob with user provided gas and fees", + blobs: []*blob.Blob{blobbyTheBlob}, + fee: sdktypes.NewInt(10_000), // roughly 0.12 utia per gas (should be good) + gasLim: blobtypes.DefaultEstimateGas([]uint32{uint32(len(blobbyTheBlob.Data))}), + expErr: nil, + }, + // TODO: add more test cases. The problem right now is that the celestia-app doesn't + // correctly construct the node (doesn't pass the min gas price) hence the price on + // everything is zero and we can't actually test the correct behavior + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + resp, err := ca.SubmitPayForBlob(ctx, tc.fee, tc.gasLim, tc.blobs) + require.Equal(t, tc.expErr, err) + if err == nil { + require.EqualValues(t, 0, resp.Code) + } + }) + } + +} + +func extractPort(addr string) string { + splitStr := strings.Split(addr, ":") + return splitStr[len(splitStr)-1] } diff --git a/state/integration_test.go b/state/integration_test.go index 25b8e00a2d..193e7bddc7 100644 --- a/state/integration_test.go +++ b/state/integration_test.go @@ -17,9 +17,10 @@ import ( "google.golang.org/grpc" "github.com/celestiaorg/celestia-app/app" - "github.com/celestiaorg/celestia-app/testutil/testfactory" - "github.com/celestiaorg/celestia-app/testutil/testnode" + "github.com/celestiaorg/celestia-app/test/util/testfactory" + "github.com/celestiaorg/celestia-app/test/util/testnode" blobtypes "github.com/celestiaorg/celestia-app/x/blob/types" + libhead "github.com/celestiaorg/go-header" "github.com/celestiaorg/celestia-node/core" "github.com/celestiaorg/celestia-node/header" @@ -95,7 +96,10 @@ type localHeader struct { client rpcclient.Client } -func (l localHeader) Head(ctx context.Context) (*header.ExtendedHeader, error) { +func (l localHeader) Head( + ctx context.Context, + _ ...libhead.HeadOption[*header.ExtendedHeader], +) (*header.ExtendedHeader, error) { latest, err := l.client.Block(ctx, nil) if err != nil { return nil, err @@ -110,7 +114,7 @@ func (s *IntegrationTestSuite) TestGetBalance() { require := s.Require() expectedBal := sdk.NewCoin(app.BondDenom, sdk.NewInt(int64(99999999999999999))) for _, acc := range s.accounts { - bal, err := s.accessor.BalanceForAddress(context.Background(), s.getAddress(acc)) + bal, err := s.accessor.BalanceForAddress(context.Background(), Address{s.getAddress(acc)}) require.NoError(err) require.Equal(&expectedBal, bal) } diff --git a/state/metrics.go b/state/metrics.go index e465e2833d..aa166e901d 100644 --- a/state/metrics.go +++ b/state/metrics.go @@ -3,32 +3,28 @@ package state import ( "context" - "go.opentelemetry.io/otel/metric/global" - "go.opentelemetry.io/otel/metric/instrument" - "go.opentelemetry.io/otel/metric/unit" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/metric" ) -var meter = global.MeterProvider().Meter("state") +var meter = otel.Meter("state") func WithMetrics(ca *CoreAccessor) { - pfbCounter, _ := meter.AsyncInt64().Counter( + pfbCounter, _ := meter.Int64ObservableCounter( "pfb_count", - instrument.WithUnit(unit.Dimensionless), - instrument.WithDescription("Total count of submitted PayForBlob transactions"), + metric.WithDescription("Total count of submitted PayForBlob transactions"), ) - lastPfbTimestamp, _ := meter.AsyncInt64().Counter( + lastPfbTimestamp, _ := meter.Int64ObservableCounter( "last_pfb_timestamp", - instrument.WithUnit(unit.Milliseconds), - instrument.WithDescription("Timestamp of the last submitted PayForBlob transaction"), + metric.WithDescription("Timestamp of the last submitted PayForBlob transaction"), ) - err := meter.RegisterCallback( - []instrument.Asynchronous{pfbCounter, lastPfbTimestamp}, - func(ctx context.Context) { - pfbCounter.Observe(ctx, ca.payForBlobCount) - lastPfbTimestamp.Observe(ctx, ca.lastPayForBlob) - }, - ) + callback := func(ctx context.Context, observer metric.Observer) error { + observer.ObserveInt64(pfbCounter, ca.PayForBlobCount()) + observer.ObserveInt64(lastPfbTimestamp, ca.LastPayForBlob()) + return nil + } + _, err := meter.RegisterCallback(callback, pfbCounter, lastPfbTimestamp) if err != nil { panic(err) } diff --git a/state/state.go b/state/state.go index 987a783239..d55bb6901c 100644 --- a/state/state.go +++ b/state/state.go @@ -1,6 +1,9 @@ package state import ( + "fmt" + "strings" + "cosmossdk.io/math" sdk "github.com/cosmos/cosmos-sdk/types" coretypes "github.com/tendermint/tendermint/types" @@ -15,8 +18,11 @@ type Tx = coretypes.Tx // TxResponse is an alias to the TxResponse type from Cosmos-SDK. type TxResponse = sdk.TxResponse -// Address is an alias to the Address type from Cosmos-SDK. -type Address = sdk.Address +// Address is an alias to the Address type from Cosmos-SDK. It is embedded into a struct to provide +// a non-interface type for JSON serialization. +type Address struct { + sdk.Address +} // ValAddress is an alias to the ValAddress type from Cosmos-SDK. type ValAddress = sdk.ValAddress @@ -26,3 +32,27 @@ type AccAddress = sdk.AccAddress // Int is an alias to the Int type from Cosmos-SDK. type Int = math.Int + +func (a *Address) UnmarshalJSON(data []byte) error { + // To convert the string back to a concrete type, we have to determine the correct implementation + var addr AccAddress + addrString := strings.Trim(string(data), "\"") + addr, err := sdk.AccAddressFromBech32(addrString) + if err != nil { + // first check if it is a validator address and can be converted + valAddr, err := sdk.ValAddressFromBech32(addrString) + if err != nil { + return fmt.Errorf("address must be a valid account or validator address: %w", err) + } + a.Address = valAddr + return nil + } + + a.Address = addr + return nil +} + +func (a Address) MarshalJSON() ([]byte, error) { + // The address is marshaled into a simple string value + return []byte("\"" + a.Address.String() + "\""), nil +}